bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/chat/service.js

385 lines
13 KiB
JavaScript
Raw Normal View History

import Users from '/imports/api/users';
import Meetings from '/imports/api/meetings';
import GroupChat from '/imports/api/group-chat';
import Auth from '/imports/ui/services/auth';
2016-07-05 02:53:47 +08:00
import UnreadMessages from '/imports/ui/services/unread-messages';
2017-03-01 06:40:16 +08:00
import Storage from '/imports/ui/services/storage/session';
2017-04-26 21:47:44 +08:00
import { makeCall } from '/imports/ui/services/api';
import { stripTags, unescapeHtml } from '/imports/utils/string-utils';
import { meetingIsBreakout } from '/imports/ui/components/app/service';
2021-04-13 19:43:08 +08:00
import { defineMessages } from 'react-intl';
import PollService from '/imports/ui/components/poll/service';
const APP = Meteor.settings.public.app;
const CHAT_CONFIG = Meteor.settings.public.chat;
const GROUPING_MESSAGES_WINDOW = CHAT_CONFIG.grouping_messages_window;
const CHAT_EMPHASIZE_TEXT = CHAT_CONFIG.moderatorChatEmphasized;
2016-06-02 00:33:19 +08:00
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
2016-06-02 00:33:19 +08:00
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const PUBLIC_CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
2016-07-01 01:10:36 +08:00
const ScrollCollection = new Mongo.Collection(null);
2019-02-05 20:24:45 +08:00
const UnsentMessagesCollection = new Mongo.Collection(null);
export const UserSentMessageCollection = new Mongo.Collection(null);
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
2021-04-12 20:13:42 +08:00
const intlMessages = defineMessages({
publicChatClear: {
id: 'app.chat.clearPublicChatMessage',
description: 'message of when clear the public chat',
},
pollResult: {
id: 'app.chat.pollResult',
description: 'used in place of user name who published poll to chat',
},
download: {
id: 'app.presentation.downloadLabel',
description: 'used as label for presentation download link',
},
notAccessibleWarning: {
id: 'app.presentationUploader.export.notAccessibleWarning',
description: 'used for indicating that a link may be not accessible',
},
original: {
id: 'app.presentationUploader.export.originalLabel',
description: 'Label to identify original presentation exported',
},
2023-08-30 21:59:54 +08:00
withWhiteboardAnnotations: {
id: 'app.presentationUploader.export.withWhiteboardAnnotations',
2023-06-17 00:40:09 +08:00
description: 'Label to identify in current state presentation exported',
},
2021-04-12 20:13:42 +08:00
});
const setUserSentMessage = (bool) => {
UserSentMessageCollection.upsert(
{ userId: Auth.userID },
{ $set: { sent: bool } },
);
2021-05-18 04:25:07 +08:00
};
2021-05-18 04:25:07 +08:00
const getUser = (userId) => Users.findOne({ userId });
2017-07-12 21:18:26 +08:00
2021-05-18 04:25:07 +08:00
const getPrivateChatByUsers = (userId) => GroupChat
.findOne({ users: { $all: [userId, Auth.userID] } });
const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { welcomeProp: 1 } });
const mapGroupMessage = (message) => {
const mappedMessage = {
id: message._id || message.id,
content: message.content,
time: message.timestamp || message.time,
sender: null,
2021-05-18 04:25:07 +08:00
key: message.key,
chatId: message.chatId,
extra: message.extra,
};
2021-04-10 04:35:05 +08:00
if (message.sender && message.sender !== SYSTEM_CHAT_TYPE) {
2021-05-18 04:25:07 +08:00
const sender = Users.findOne(
{ userId: message.sender },
{
fields: { avatar: 1, role: 1, name: 1 },
},
);
const mappedSender = {
avatar: sender?.avatar,
color: message.color,
2022-09-20 20:57:08 +08:00
isModerator: message.senderRole === ROLE_MODERATOR,
name: message.senderName,
isOnline: !!sender,
};
mappedMessage.sender = mappedSender;
}
return mappedMessage;
};
const reduceGroupMessages = (previous, current) => {
const lastMessage = previous[previous.length - 1];
const currentMessage = current;
currentMessage.content = [{
id: current.id,
text: current.message,
time: current.timestamp,
color: current.color,
}];
if (!lastMessage) {
return previous.concat(currentMessage);
}
// Check if the last message is from the same user and time discrepancy
// between the two messages exceeds window and then group current
// message with the last one
const timeOfLastMessage = lastMessage.content[lastMessage.content.length - 1].time;
const isOrWasPoll = currentMessage.id.includes(CHAT_POLL_RESULTS_MESSAGE)
|| lastMessage.id.includes(CHAT_POLL_RESULTS_MESSAGE);
const groupingWindow = isOrWasPoll ? 0 : GROUPING_MESSAGES_WINDOW;
if (lastMessage.sender.id === currentMessage.sender.id
&& (currentMessage.timestamp - timeOfLastMessage) <= groupingWindow) {
lastMessage.content.push(currentMessage.content.pop());
return previous;
}
return previous.concat(currentMessage);
};
2021-05-18 04:25:07 +08:00
const reduceAndMapGroupMessages = (messages) => (messages
2019-01-14 21:23:35 +08:00
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
2021-05-18 04:25:07 +08:00
const reduceAndDontMapGroupMessages = (messages) => (messages
.reduce(reduceGroupMessages, []));
2016-06-14 01:00:38 +08:00
const isChatLocked = (receiverID) => {
const isPublic = receiverID === PUBLIC_CHAT_ID;
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'lockSettingsProps.disablePublicChat': 1, 'lockSettingsProps.disablePrivateChat': 1 } });
const user = Users.findOne({ meetingId: Auth.meetingID, userId: Auth.userID },
{ fields: { locked: 1, role: 1 } });
const receiver = Users.findOne({ meetingId: Auth.meetingID, userId: receiverID },
{ fields: { role: 1 } });
const isReceiverModerator = receiver && receiver.role === ROLE_MODERATOR;
2017-07-13 04:02:55 +08:00
// disable private chat in breakouts
if (meetingIsBreakout()) {
return !isPublic;
}
if (meeting.lockSettingsProps !== undefined) {
if (user.locked && user.role !== ROLE_MODERATOR) {
if (isPublic) {
2019-04-10 21:44:34 +08:00
return meeting.lockSettingsProps.disablePublicChat;
}
return !isReceiverModerator
&& meeting.lockSettingsProps.disablePrivateChat;
}
2016-06-14 01:00:38 +08:00
}
2017-09-07 03:57:24 +08:00
return false;
2016-06-14 01:00:38 +08:00
};
2023-07-11 22:32:15 +08:00
const isChatClosed = (chatId) => {
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
return !!currentClosedChats.find(closedChat => closedChat.chatId === chatId);
};
2016-07-12 03:42:54 +08:00
const lastReadMessageTime = (receiverID) => {
const isPublic = receiverID === PUBLIC_CHAT_ID;
const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : receiverID;
2016-07-12 03:42:54 +08:00
2017-07-12 21:18:26 +08:00
return UnreadMessages.get(chatType);
2016-07-12 03:42:54 +08:00
};
2021-05-18 04:25:07 +08:00
const sendGroupMessage = (message, idChatOpen) => {
const chatIdToSent = idChatOpen === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : idChatOpen;
const chat = GroupChat.findOne({ chatId: chatIdToSent },
2021-05-18 04:25:07 +08:00
{ fields: { users: 1 } });
const chatID = idChatOpen === PUBLIC_CHAT_ID
? PUBLIC_GROUP_CHAT_ID
: chat.users.filter((id) => id !== Auth.userID)[0];
const isPublicChat = chatID === PUBLIC_CHAT_ID;
let destinationChatId = PUBLIC_GROUP_CHAT_ID;
const { userID: senderUserId } = Auth;
const receiverId = { id: chatID };
if (!isPublicChat) {
const privateChat = GroupChat.findOne({ users: { $all: [chatID, senderUserId] } },
{ fields: { chatId: 1 } });
if (privateChat) {
const { chatId: privateChatId } = privateChat;
destinationChatId = privateChatId;
}
}
const payload = {
correlationId: `${senderUserId}-${Date.now()}`,
sender: {
id: senderUserId,
name: '',
role: '',
},
chatEmphasizedText: CHAT_EMPHASIZE_TEXT,
2017-06-03 03:25:02 +08:00
message,
2016-06-03 02:40:27 +08:00
};
2016-06-02 00:33:19 +08:00
2017-06-03 03:25:02 +08:00
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
2017-03-03 05:42:39 +08:00
// Remove the chat that user send messages from the session.
2023-07-11 22:32:15 +08:00
if (isChatClosed(receiverId.id)) {
const closedChats = currentClosedChats.filter(closedChat => closedChat.chatId !== receiverId.id);
Storage.setItem(CLOSED_CHAT_LIST_KEY,closedChats);
2017-03-01 06:40:16 +08:00
}
return makeCall('sendGroupChatMsg', destinationChatId, payload);
2016-06-02 00:33:19 +08:00
};
2016-07-01 01:10:36 +08:00
const getScrollPosition = (receiverID) => {
const scroll = ScrollCollection.findOne({ receiver: receiverID },
{ fields: { position: 1 } }) || { position: null };
2016-07-01 01:10:36 +08:00
return scroll.position;
};
2021-05-18 04:25:07 +08:00
const updateScrollPosition = (position, idChatOpen) => ScrollCollection.upsert(
{ receiver: idChatOpen },
2019-01-14 21:23:35 +08:00
{ $set: { position } },
);
2016-07-05 02:53:47 +08:00
2021-05-18 04:25:07 +08:00
const updateUnreadMessage = (timestamp, idChatOpen) => {
const chatID = idChatOpen;
const isPublic = chatID === PUBLIC_CHAT_ID;
const chatType = isPublic ? PUBLIC_GROUP_CHAT_ID : chatID;
2017-07-12 21:18:26 +08:00
return UnreadMessages.update(chatType, timestamp);
2016-07-01 01:10:36 +08:00
};
2017-08-03 01:05:20 +08:00
const clearPublicChatHistory = () => (makeCall('clearPublicChatHistory'));
2021-02-12 22:07:53 +08:00
const closePrivateChat = (chatId) => {
2017-06-03 03:25:02 +08:00
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
2017-03-03 04:33:49 +08:00
2023-07-11 22:32:15 +08:00
if (!isChatClosed(chatId)) {
currentClosedChats.push({ chatId, timestamp: Date.now() });
2017-03-03 04:33:49 +08:00
Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
}
2017-03-03 04:33:49 +08:00
};
// if this private chat has been added to the list of closed ones, remove it
2021-05-18 04:25:07 +08:00
const removeFromClosedChatsSession = (idChatOpen) => {
const chatID = idChatOpen;
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
2023-07-11 22:32:15 +08:00
if (isChatClosed(chatID)) {
const closedChats = currentClosedChats.filter(closedChat => closedChat.chatId !== chatID);
Storage.setItem(CLOSED_CHAT_LIST_KEY,closedChats);
}
2017-12-19 01:41:26 +08:00
};
const htmlDecode = (input) => {
const replacedBRs = input.replaceAll('<br/>', '\n');
return unescapeHtml(stripTags(replacedBRs));
};
// Export the chat as [Hour:Min] user: message
2022-05-20 02:53:48 +08:00
const exportChat = (timeWindowList, intl) => {
2021-04-12 20:13:42 +08:00
const messageList = timeWindowList.reduce((acc, timeWindow) => {
2021-05-18 04:25:07 +08:00
const msgs = timeWindow.content.map((message) => {
2021-03-25 04:20:33 +08:00
const date = new Date(message.time);
const hour = date.getHours().toString().padStart(2, 0);
const min = date.getMinutes().toString().padStart(2, 0);
const hourMin = `[${hour}:${min}]`;
2021-07-22 00:57:49 +08:00
// Skip the reduce aggregation for the sync messages because they aren't localized, causing an error in line 268
// Also they're temporary (preliminary) messages, so it doesn't make sense export them
2021-06-18 02:34:07 +08:00
if (['SYSTEM_MESSAGE-sync-msg', 'synced'].includes(message.id)) return acc;
2021-07-22 00:57:49 +08:00
let userName = message.id.startsWith(SYSTEM_CHAT_TYPE)
2021-03-25 04:20:33 +08:00
? ''
2022-05-20 02:53:48 +08:00
: `${timeWindow.senderName}: `;
let messageText = '';
if (message.text === PUBLIC_CHAT_CLEAR) {
messageText = intl.formatMessage(intlMessages.publicChatClear);
} else if (message.id.includes(CHAT_POLL_RESULTS_MESSAGE)) {
userName = `${intl.formatMessage(intlMessages.pollResult)}:\n`;
const { pollResultData } = timeWindow.extra;
const pollText = htmlDecode(PollService.getPollResultString(pollResultData, intl).split('<br/>').join('\n'));
// remove last \n to avoid empty line
messageText = pollText.slice(0, -1);
} else {
messageText = message.text;
}
2021-04-12 20:13:42 +08:00
return `${hourMin} ${userName}${htmlDecode(messageText)}`;
2021-03-25 04:20:33 +08:00
});
return [...acc, ...msgs];
2021-05-18 04:25:07 +08:00
}, []);
2021-03-25 04:20:33 +08:00
return messageList.join('\n');
2021-05-18 04:25:07 +08:00
};
2021-02-13 03:13:21 +08:00
const getAllMessages = (chatID, messages) => {
2021-05-18 04:25:07 +08:00
if (!messages[chatID]) {
return [];
}
return (chatID === PUBLIC_GROUP_CHAT_ID)
2021-05-18 04:25:07 +08:00
? Object.values(messages[chatID].posJoinMessages)
: Object.values(messages[chatID].messageGroups);
};
2019-01-14 21:23:35 +08:00
const maxTimestampReducer = (max, el) => ((el.timestamp > max) ? el.timestamp : max);
const maxNumberReducer = (max, el) => ((el > max) ? el : max);
const getLastMessageTimestampFromChatList = (activeChats, messages) => activeChats
.map((chat) => ((chat.userId === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.chatId))
.map((chatId) => getAllMessages(chatId, messages).reduce(maxTimestampReducer, 0))
2019-01-14 21:23:35 +08:00
.reduce(maxNumberReducer, 0);
const removePackagedClassAttribute = (classnames, attribute) => {
2021-05-18 04:25:07 +08:00
classnames.forEach((c) => {
const elements = document.getElementsByClassName(c);
2021-05-18 04:25:07 +08:00
if (elements) {
// eslint-disable-next-line
for (const [, v] of Object.entries(elements)) {
v.removeAttribute(attribute);
}
}
});
2021-05-18 04:25:07 +08:00
};
2023-08-18 21:57:50 +08:00
const getExportedPresentationString = (fileURI, filename, intl, fileStateType) => {
const sanitizedFilename = stripTags(filename);
2023-08-30 21:59:54 +08:00
const intlFileStateType = fileStateType === 'Original' ? intlMessages.original : intlMessages.withWhiteboardAnnotations;
const href = `${APP.bbbWebBase}/${fileURI}`;
const warningIcon = '<i class="icon-bbb-warning"></i>';
const label = `<span>${intl.formatMessage(intlMessages.download)}</span>`;
const notAccessibleWarning = `<span title="${intl.formatMessage(intlMessages.notAccessibleWarning)}">${warningIcon}</span>`;
const link = `<a aria-label="${intl.formatMessage(intlMessages.notAccessibleWarning)}" href=${href} type="application/pdf" target="_blank" rel="noopener, noreferrer" download>${label}&nbsp;${notAccessibleWarning}</a>`;
const name = `<span>${sanitizedFilename} (${intl.formatMessage(intlFileStateType)})</span>`;
return `${name}</br>${link}`;
};
2016-06-02 00:33:19 +08:00
export default {
setUserSentMessage,
mapGroupMessage,
reduceAndMapGroupMessages,
reduceAndDontMapGroupMessages,
2016-06-07 22:19:19 +08:00
getUser,
getPrivateChatByUsers,
getWelcomeProp,
2016-07-01 01:10:36 +08:00
getScrollPosition,
2016-07-12 03:42:54 +08:00
lastReadMessageTime,
2016-06-14 01:00:38 +08:00
isChatLocked,
2023-07-11 22:32:15 +08:00
isChatClosed,
2016-07-05 02:53:47 +08:00
updateScrollPosition,
updateUnreadMessage,
sendGroupMessage,
closePrivateChat,
removeFromClosedChatsSession,
exportChat,
2017-08-03 01:05:20 +08:00
clearPublicChatHistory,
2019-01-14 21:23:35 +08:00
maxTimestampReducer,
getLastMessageTimestampFromChatList,
2019-02-05 20:24:45 +08:00
UnsentMessagesCollection,
removePackagedClassAttribute,
getExportedPresentationString,
2016-06-02 00:33:19 +08:00
};