refactor: remove unused chat code (#19215)
This commit is contained in:
parent
4aa83d243a
commit
067144bf86
@ -10,7 +10,6 @@ import UserSettings from '/imports/api/users-settings';
|
||||
import VideoStreams from '/imports/api/video-streams';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import ConnectionStatus from '/imports/api/connection-status';
|
||||
import Captions from '/imports/api/captions';
|
||||
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
|
||||
@ -21,7 +20,6 @@ import guestUsers from '/imports/api/guest-users';
|
||||
import Meetings, {
|
||||
RecordMeetings, ExternalVideoMeetings, MeetingTimeRemaining, Notifications,
|
||||
} from '/imports/api/meetings';
|
||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
||||
import Users, { CurrentUser } from '/imports/api/users';
|
||||
|
||||
// Custom Publishers
|
||||
@ -40,7 +38,6 @@ export const localCollectionRegistry = {
|
||||
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
||||
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
||||
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
||||
localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
|
||||
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
|
||||
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
||||
localPadsSync: new AbstractCollection(Pads, Pads),
|
||||
@ -53,7 +50,6 @@ export const localCollectionRegistry = {
|
||||
ExternalVideoMeetings,
|
||||
),
|
||||
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
|
||||
localUsersTypingSync: new AbstractCollection(UsersTyping, UsersTyping),
|
||||
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
|
||||
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
|
||||
localGuestUsersSync: new AbstractCollection(guestUsers, guestUsers),
|
||||
|
@ -30,7 +30,6 @@ import IntlStartup from '/imports/startup/client/intl';
|
||||
import ContextProviders from '/imports/ui/components/context-providers/component';
|
||||
import ChatAdapter from '/imports/ui/components/components-data/chat-context/adapter';
|
||||
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
|
||||
import GroupChatAdapter from '/imports/ui/components/components-data/group-chat-context/adapter';
|
||||
import GraphqlProvider from '/imports/ui/components/graphql-provider/component';
|
||||
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
|
||||
// The adapter import is "unused" as far as static code is concerned, but it
|
||||
@ -95,7 +94,6 @@ Meteor.startup(() => {
|
||||
</JoinHandler>
|
||||
<UsersAdapter />
|
||||
<ChatAdapter />
|
||||
<GroupChatAdapter />
|
||||
</>
|
||||
</ContextProviders>,
|
||||
document.getElementById('app'),
|
||||
|
@ -1,20 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const collectionOptions = Meteor.isClient ? {
|
||||
connection: null,
|
||||
} : {};
|
||||
|
||||
const GroupChatMsg = new Mongo.Collection('group-chat-msg');
|
||||
const UsersTyping = new Mongo.Collection('users-typing', collectionOptions);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
GroupChatMsg.createIndexAsync({ meetingId: 1, chatId: 1 });
|
||||
UsersTyping.createIndexAsync({ meetingId: 1, isTypingTo: 1 });
|
||||
}
|
||||
|
||||
// As we store chat in context, skip adding to mini mongo
|
||||
if (Meteor.isClient) {
|
||||
GroupChatMsg.onAdded = () => false;
|
||||
}
|
||||
|
||||
export { GroupChatMsg, UsersTyping };
|
@ -1,12 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleGroupChatMsgBroadcast from './handlers/groupChatMsgBroadcast';
|
||||
import handleClearPublicGroupChat from './handlers/clearPublicGroupChat';
|
||||
import handleUserTyping from './handlers/userTyping';
|
||||
import handleSyncGroupChatMsg from './handlers/syncGroupsChat';
|
||||
import { processForHTML5ServerOnly } from '/imports/api/common/server/helpers';
|
||||
|
||||
// RedisPubSub.on('GetGroupChatMsgsRespMsg', processForHTML5ServerOnly(handleSyncGroupChatMsg));
|
||||
// RedisPubSub.on('GroupChatMessageBroadcastEvtMsg', handleGroupChatMsgBroadcast);
|
||||
// RedisPubSub.on('ClearPublicChatHistoryEvtMsg', handleClearPublicGroupChat);
|
||||
// RedisPubSub.on('SyncGetGroupChatMsgsRespMsg', handleSyncGroupChatMsg);
|
||||
// RedisPubSub.on('UserTypingEvtMsg', handleUserTyping);
|
@ -1,13 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import clearGroupChatMsg from '../modifiers/clearGroupChatMsg';
|
||||
|
||||
export default async function clearPublicChatHistory({ header, body }) {
|
||||
const { meetingId } = header;
|
||||
const { chatId } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
|
||||
const result = clearGroupChatMsg(meetingId, chatId);
|
||||
return result;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import addGroupChatMsg from '../modifiers/addGroupChatMsg';
|
||||
import addBulkGroupChatMsgs from '../modifiers/addBulkGroupChatMsgs';
|
||||
|
||||
const { bufferChatInsertsMs } = Meteor.settings.public.chat;
|
||||
|
||||
const msgBuffer = [];
|
||||
|
||||
const bulkFn = throttle(addBulkGroupChatMsgs, bufferChatInsertsMs);
|
||||
|
||||
export default async function handleGroupChatMsgBroadcast({ body }, meetingId) {
|
||||
const { chatId, msg } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
check(msg, Object);
|
||||
|
||||
if (bufferChatInsertsMs) {
|
||||
msgBuffer.push({ meetingId, chatId, msg });
|
||||
bulkFn(msgBuffer);
|
||||
} else {
|
||||
await addGroupChatMsg(meetingId, chatId, msg);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { Match, check } from 'meteor/check';
|
||||
import syncMeetingChatMsgs from '../modifiers/syncMeetingChatMsgs';
|
||||
|
||||
export default function handleSyncGroupChat({ body }, meetingId) {
|
||||
const { chatId, msgs } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
check(msgs, Match.Maybe(Array));
|
||||
|
||||
syncMeetingChatMsgs(meetingId, chatId, msgs);
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import startTyping from '../modifiers/startTyping';
|
||||
|
||||
export default async function handleUserTyping({ body }, meetingId) {
|
||||
const { chatId, userId } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(chatId, String);
|
||||
|
||||
await startTyping(meetingId, userId, chatId);
|
||||
}
|
@ -1,3 +1 @@
|
||||
import './eventHandlers';
|
||||
import './methods';
|
||||
import './publishers';
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import clearPublicChatHistory from './methods/clearPublicChatHistory';
|
||||
import startUserTyping from './methods/startUserTyping';
|
||||
import stopUserTyping from './methods/stopUserTyping';
|
||||
import chatMessageBeforeJoinCounter from './methods/chatMessageBeforeJoinCounter';
|
||||
import fetchMessagePerPage from './methods/fetchMessagePerPage';
|
||||
|
||||
Meteor.methods({
|
||||
fetchMessagePerPage,
|
||||
chatMessageBeforeJoinCounter,
|
||||
clearPublicChatHistory,
|
||||
startUserTyping,
|
||||
stopUserTyping,
|
||||
});
|
||||
|
@ -1,45 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import Users from '/imports/api/users';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
|
||||
|
||||
export default async function chatMessageBeforeJoinCounter() {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const groupChats = GroupChat.find({
|
||||
$or: [
|
||||
{ meetingId, access: PUBLIC_CHAT_TYPE },
|
||||
{ meetingId, users: { $all: [requesterUserId] } },
|
||||
],
|
||||
}).fetch();
|
||||
|
||||
const User = await Users.findOneAsync({ userId: requesterUserId, meetingId });
|
||||
|
||||
const chatIdWithCounter = groupChats.map((groupChat) => {
|
||||
const msgCount = GroupChatMsg.find({
|
||||
meetingId,
|
||||
chatId: groupChat.chatId,
|
||||
timestamp: { $lt: User.authTokenValidatedTime },
|
||||
}).count();
|
||||
return {
|
||||
chatId: groupChat.chatId,
|
||||
count: msgCount,
|
||||
};
|
||||
}).filter((chat) => chat.count);
|
||||
return chatIdWithCounter;
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method chatMessageBeforeJoinCounter ${err.stack}`);
|
||||
}
|
||||
//True returned because the function requires a return.
|
||||
return true;
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import Users from '/imports/api/users';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
|
||||
|
||||
export default async function fetchMessagePerPage(chatId, page) {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(chatId, String);
|
||||
check(page, Number);
|
||||
|
||||
const User = await Users.findOneAsync({ userId: requesterUserId, meetingId });
|
||||
|
||||
const messages = await GroupChatMsg.find(
|
||||
{ chatId, meetingId, timestamp: { $lt: User.authTokenValidatedTime } },
|
||||
{
|
||||
sort: { timestamp: 1 },
|
||||
skip: page > 0 ? ((page - 1) * ITENS_PER_PAGE) : 0,
|
||||
limit: ITENS_PER_PAGE,
|
||||
},
|
||||
)
|
||||
.fetchAsync();
|
||||
return messages;
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method fetchMessagePerPage ${err.stack}`);
|
||||
}
|
||||
//True returned because the function requires a return.
|
||||
return true;
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
||||
import stopTyping from '../modifiers/stopTyping';
|
||||
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
|
||||
export default async function stopUserTyping() {
|
||||
try {
|
||||
const { meetingId, requesterUserId } = extractCredentials(this.userId);
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
|
||||
const userTyping = await UsersTyping.findOneAsync({
|
||||
meetingId,
|
||||
userId: requesterUserId,
|
||||
});
|
||||
|
||||
if (userTyping && meetingId && requesterUserId) {
|
||||
stopTyping(meetingId, requesterUserId, true);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Exception while invoking method stopUserTyping ${err.stack}`);
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import flat from 'flat';
|
||||
import { parseMessage } from './addGroupChatMsg';
|
||||
|
||||
export default async function addBulkGroupChatMsgs(msgs) {
|
||||
if (!msgs.length) return;
|
||||
|
||||
const mappedMsgs = msgs
|
||||
.map(({ chatId, meetingId, msg }) => {
|
||||
const {
|
||||
sender,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
return {
|
||||
_id: new Mongo.ObjectID()._str,
|
||||
...restMsg,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
sender: sender.id,
|
||||
senderName: sender.name,
|
||||
senderRole: sender.role,
|
||||
};
|
||||
})
|
||||
.map((el) => flat(el, { safe: true }))
|
||||
.map((msg)=>{
|
||||
const groupChat = GroupChat.findOne({ meetingId: msg.meetingId, chatId: msg.chatId });
|
||||
return {
|
||||
...msg,
|
||||
participants: [...groupChat.users],
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const { insertedCount } = await GroupChatMsg.rawCollection().insertMany(mappedMsgs);
|
||||
msgs.length = 0;
|
||||
|
||||
if (insertedCount) {
|
||||
Logger.info(`Inserted ${insertedCount} messages`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on bulk insert. ${err}`);
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { Match, check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import { BREAK_LINE } from '/imports/utils/lineEndings';
|
||||
import changeHasMessages from '/imports/api/users-persistent-data/server/modifiers/changeHasMessages';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
|
||||
export function parseMessage(message) {
|
||||
let parsedMessage = message || '';
|
||||
|
||||
// Replace \r and \n to <br/>
|
||||
parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
|
||||
|
||||
// Replace flash links to html valid ones
|
||||
parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\'');
|
||||
parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="');
|
||||
|
||||
return parsedMessage;
|
||||
}
|
||||
|
||||
export default async function addGroupChatMsg(meetingId, chatId, msg) {
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
check(msg, {
|
||||
id: Match.Maybe(String),
|
||||
timestamp: Number,
|
||||
sender: Object,
|
||||
chatEmphasizedText: Boolean,
|
||||
message: String,
|
||||
correlationId: Match.Maybe(String),
|
||||
});
|
||||
|
||||
const {
|
||||
sender,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
const groupChat = GroupChat.findOne({ meetingId, chatId });
|
||||
|
||||
const msgDocument = {
|
||||
...restMsg,
|
||||
sender: sender.id,
|
||||
senderName: sender.name,
|
||||
senderRole: sender.role,
|
||||
meetingId,
|
||||
chatId,
|
||||
participants: [...groupChat.users],
|
||||
message: parseMessage(msg.message),
|
||||
};
|
||||
|
||||
try {
|
||||
const insertedId = await GroupChatMsg.insertAsync(msgDocument);
|
||||
|
||||
if (insertedId) {
|
||||
await changeHasMessages(true, sender.id, meetingId, chatId);
|
||||
Logger.info(`Added group-chat-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on adding group-chat-msg to collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
import { Match, check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import { BREAK_LINE } from '/imports/utils/lineEndings';
|
||||
|
||||
export function parseMessage(message) {
|
||||
let parsedMessage = message || '';
|
||||
|
||||
// Replace \r and \n to <br/>
|
||||
parsedMessage = parsedMessage.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, `$1${BREAK_LINE}$2`);
|
||||
|
||||
// Replace flash links to html valid ones
|
||||
parsedMessage = parsedMessage.split('<a href=\'event:').join('<a target="_blank" href=\'');
|
||||
parsedMessage = parsedMessage.split('<a href="event:').join('<a target="_blank" href="');
|
||||
|
||||
return parsedMessage;
|
||||
}
|
||||
|
||||
export default async function addSystemMsg(meetingId, chatId, msg) {
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
check(msg, {
|
||||
id: Match.Maybe(String),
|
||||
timestamp: Number,
|
||||
sender: Object,
|
||||
message: String,
|
||||
messageValues: Match.Maybe(Object),
|
||||
extra: Match.Maybe(Object),
|
||||
correlationId: Match.Maybe(String),
|
||||
});
|
||||
const msgDocument = {
|
||||
...msg,
|
||||
sender: msg.sender.id,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
};
|
||||
|
||||
try {
|
||||
const insertedId = await GroupChatMsg.insertAsync(msgDocument);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added system-msg msgId=${msg.id} chatId=${chatId} meetingId=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on adding system-msg to collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
|
||||
import clearChatHasMessages from '/imports/api/users-persistent-data/server/modifiers/clearChatHasMessages';
|
||||
import UsersPersistentData from '/imports/api/users-persistent-data';
|
||||
|
||||
export default async function clearGroupChatMsg(meetingId, chatId) {
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
if (chatId) {
|
||||
try {
|
||||
const numberAffected = await GroupChatMsg.removeAsync({ meetingId, chatId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared GroupChatMsg (${meetingId}, ${chatId})`);
|
||||
const clearMsg = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`,
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: CHAT_CLEAR_MESSAGE,
|
||||
};
|
||||
await addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, clearMsg);
|
||||
await clearChatHasMessages(meetingId, chatId);
|
||||
|
||||
//clear offline users' data
|
||||
const selector = {
|
||||
meetingId,
|
||||
'shouldPersist.hasConnectionStatus': { $ne: true },
|
||||
'shouldPersist.hasMessages.private': { $ne: true },
|
||||
loggedOut: true,
|
||||
};
|
||||
await UsersPersistentData.removeAsync(selector);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing GroupChat (${meetingId}, ${chatId}). ${err}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (meetingId) {
|
||||
try {
|
||||
const numberAffected = await GroupChatMsg.removeAsync({ meetingId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared GroupChatMsg (${meetingId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing GroupChatMsg (${meetingId}). ${err}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const numberAffected = await GroupChatMsg
|
||||
.removeAsync({ chatId: { $eq: PUBLIC_GROUP_CHAT_ID } });
|
||||
|
||||
if (numberAffected) {
|
||||
await clearChatHasMessages(meetingId, chatId=PUBLIC_GROUP_CHAT_ID);
|
||||
|
||||
Logger.info('Cleared GroupChatMsg (all)');
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing GroupChatMsg (all). ${err}`);
|
||||
}
|
||||
}
|
||||
//True resturned because the function requires a return.
|
||||
return true;
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import GroupChatMsg from '/imports/api/group-chat-msg';
|
||||
|
||||
export default async function removeGroupChatMsg(meetingId, chatId) {
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
|
||||
const selector = {
|
||||
chatId,
|
||||
meetingId,
|
||||
};
|
||||
|
||||
try {
|
||||
const numberAffected = await GroupChatMsg.removeAsync(selector);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Removed group-chat-msg id=${chatId} meeting=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Removing group-chat-msg from collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Users from '/imports/api/users';
|
||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
||||
import stopTyping from './stopTyping';
|
||||
|
||||
const TYPING_TIMEOUT = 5000;
|
||||
|
||||
export default async function startTyping(meetingId, userId, chatId) {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const user = await Users.findOneAsync(selector, { fields: { name: 1, role: 1 } });
|
||||
|
||||
const modifier = {
|
||||
meetingId,
|
||||
userId,
|
||||
name: user.name,
|
||||
isTypingTo: chatId,
|
||||
role: user.role,
|
||||
time: (new Date()),
|
||||
};
|
||||
|
||||
const typingUser = await UsersTyping.findOneAsync(selector, {
|
||||
fields: {
|
||||
time: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (typingUser) {
|
||||
if (modifier.time - typingUser.time <= TYPING_TIMEOUT - 100) return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { numberAffected } = await UsersTyping.upsertAsync(selector, modifier);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.debug('Typing indicator update', { userId, chatId });
|
||||
Meteor.setTimeout(() => {
|
||||
stopTyping(meetingId, userId);
|
||||
}, TYPING_TIMEOUT);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Typing indicator update error: ${err}`);
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
||||
|
||||
export default async function stopTyping(meetingId, userId, sendMsgInitiated = false) {
|
||||
check(meetingId, String);
|
||||
check(userId, String);
|
||||
check(sendMsgInitiated, Boolean);
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId,
|
||||
};
|
||||
|
||||
const user = await UsersTyping.findOneAsync(selector);
|
||||
const stillTyping = !sendMsgInitiated && user && (new Date()) - user.time < 3000;
|
||||
if (stillTyping) return;
|
||||
|
||||
try {
|
||||
const numberAffected = await UsersTyping.removeAsync(selector);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.debug('Stopped typing indicator', { userId });
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Stop user=${userId} typing indicator error: ${err}`);
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { Match, check } from 'meteor/check';
|
||||
import flat from 'flat';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import { parseMessage } from './addGroupChatMsg';
|
||||
|
||||
export default function syncMeetingChatMsgs(meetingId, chatId, msgs) {
|
||||
if (!msgs.length) return;
|
||||
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
check(msgs, Match.Maybe(Array));
|
||||
|
||||
try {
|
||||
const bulkOperations = GroupChatMsg.rawCollection().initializeOrderedBulkOp();
|
||||
|
||||
msgs
|
||||
.forEach((msg) => {
|
||||
const {
|
||||
sender,
|
||||
...restMsg
|
||||
} = msg;
|
||||
|
||||
const msgToSync = {
|
||||
...restMsg,
|
||||
meetingId,
|
||||
chatId,
|
||||
message: parseMessage(msg.message),
|
||||
sender: sender.id,
|
||||
};
|
||||
|
||||
const modifier = flat(msgToSync, { safe: true });
|
||||
|
||||
bulkOperations
|
||||
.find({ chatId, meetingId, id: msg.id })
|
||||
.upsert()
|
||||
.updateOne({
|
||||
$setOnInsert: { _id: new Mongo.ObjectID()._str },
|
||||
$set: { ...modifier },
|
||||
});
|
||||
});
|
||||
|
||||
bulkOperations.execute();
|
||||
|
||||
Logger.info('Chat messages synchronized', { chatId, meetingId });
|
||||
} catch (err) {
|
||||
Logger.error(`Error on sync chat messages: ${err}`);
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
import { GroupChatMsg, UsersTyping } from '/imports/api/group-chat-msg';
|
||||
import Users from '/imports/api/users';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
|
||||
async function groupChatMsg() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
.findOneAsync({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing GroupChatMsg was requested by unauth connection ${this.connection.id}`);
|
||||
return GroupChatMsg.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
|
||||
Logger.debug('Publishing group-chat-msg', { meetingId, userId });
|
||||
|
||||
const chats = await GroupChat.find({
|
||||
$or: [
|
||||
{ meetingId, users: { $all: [userId] } },
|
||||
],
|
||||
}).fetchAsync();
|
||||
|
||||
const chatsIds = chats.map((ct) => ct.chatId);
|
||||
|
||||
const User = await Users.findOneAsync({ userId, meetingId });
|
||||
const selector = {
|
||||
timestamp: { $gte: User.authTokenValidatedTime },
|
||||
$or: [
|
||||
{ meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } },
|
||||
{ meetingId, participants: { $in: [userId] } },
|
||||
],
|
||||
};
|
||||
return GroupChatMsg.find(selector, { fields: { participants: 0 } });
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundGroupChat = groupChatMsg.bind(this);
|
||||
return boundGroupChat(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('group-chat-msg', publish);
|
||||
|
||||
function usersTyping() {
|
||||
const tokenValidation = AuthTokenValidation.findOne({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing users-typing was requested by unauth connection ${this.connection.id}`);
|
||||
return UsersTyping.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
|
||||
Logger.debug('Publishing users-typing', { meetingId, userId });
|
||||
|
||||
return UsersTyping.find({ meetingId });
|
||||
}
|
||||
|
||||
function pubishUsersTyping(...args) {
|
||||
const boundUsersTyping = usersTyping.bind(this);
|
||||
return boundUsersTyping(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('users-typing', pubishUsersTyping);
|
@ -1,19 +1,3 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
const collectionOptions = Meteor.isClient ? {
|
||||
connection: null,
|
||||
} : {};
|
||||
|
||||
const GroupChat = new Mongo.Collection('group-chat', collectionOptions);
|
||||
|
||||
if (Meteor.isServer) {
|
||||
GroupChat.createIndexAsync({
|
||||
meetingId: 1, chatId: 1, access: 1, users: 1,
|
||||
});
|
||||
}
|
||||
|
||||
export default GroupChat;
|
||||
|
||||
const CHAT_ACCESS = {
|
||||
PUBLIC: 'PUBLIC_ACCESS',
|
||||
PRIVATE: 'PRIVATE_ACCESS',
|
||||
|
@ -1,10 +0,0 @@
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
import handleGroupChats from './handlers/groupChats';
|
||||
import handleGroupChatCreated from './handlers/groupChatCreated';
|
||||
import handleGroupChatDestroyed from './handlers/groupChatDestroyed';
|
||||
import { processForHTML5ServerOnly } from '/imports/api/common/server/helpers';
|
||||
|
||||
// RedisPubSub.on('GetGroupChatsRespMsg', processForHTML5ServerOnly(handleGroupChats));
|
||||
// RedisPubSub.on('GroupChatCreatedEvtMsg', handleGroupChatCreated);
|
||||
// RedisPubSub.on('GroupChatDestroyedEvtMsg', handleGroupChatDestroyed);
|
||||
// RedisPubSub.on('SyncGetGroupChatsRespMsg', handleGroupChats);
|
@ -1,9 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import addGroupChat from '../modifiers/addGroupChat';
|
||||
|
||||
export default async function handleGroupChatCreated({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
check(body, Object);
|
||||
|
||||
await addGroupChat(meetingId, body);
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import addGroupChat from '../modifiers/addGroupChat';
|
||||
|
||||
export default async function handleGroupChatDestroyed({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
check(body, Object);
|
||||
|
||||
await addGroupChat(meetingId, body);
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import addGroupChat from '../modifiers/addGroupChat';
|
||||
|
||||
export default async function handleGroupChats({ body }, meetingId) {
|
||||
const { chats } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(chats, Array);
|
||||
|
||||
await new Promise
|
||||
.all(chats.map(async (chat) => {
|
||||
await addGroupChat(meetingId, chat);
|
||||
}));
|
||||
}
|
@ -1,4 +1,2 @@
|
||||
import '/imports/api/group-chat-msg/server';
|
||||
import './eventHandlers';
|
||||
import './methods';
|
||||
import './publishers';
|
||||
|
@ -1,47 +0,0 @@
|
||||
import flat from 'flat';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
|
||||
export default async function addGroupChat(meetingId, chat) {
|
||||
check(meetingId, String);
|
||||
check(chat, {
|
||||
id: Match.Maybe(String),
|
||||
chatId: Match.Maybe(String),
|
||||
correlationId: Match.Maybe(String),
|
||||
access: String,
|
||||
createdBy: Object,
|
||||
users: Array,
|
||||
msg: Match.Maybe(Array),
|
||||
});
|
||||
|
||||
const chatDocument = {
|
||||
meetingId,
|
||||
chatId: chat.chatId || chat.id,
|
||||
access: chat.access,
|
||||
users: chat.users.map((u) => u.id),
|
||||
participants: chat.users,
|
||||
createdBy: chat.createdBy.id,
|
||||
};
|
||||
|
||||
const selector = {
|
||||
chatId: chatDocument.chatId,
|
||||
meetingId,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: flat(chatDocument, { safe: true }),
|
||||
};
|
||||
|
||||
try {
|
||||
const { insertedId } = await GroupChat.upsertAsync(selector, modifier);
|
||||
|
||||
if (insertedId) {
|
||||
Logger.info(`Added group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`);
|
||||
} else {
|
||||
Logger.info(`Upserted group-chat chatId=${chatDocument.chatId} meetingId=${meetingId}`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Adding group-chat to collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import clearGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg';
|
||||
|
||||
export default async function clearGroupChat(meetingId) {
|
||||
try {
|
||||
await clearGroupChatMsg(meetingId);
|
||||
const numberAffected = await GroupChat.removeAsync({ meetingId });
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared GroupChat (${meetingId})`);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Error on clearing GroupChat (${meetingId}). ${err}`);
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import clearGroupChatMsg from '/imports/api/group-chat-msg/server/modifiers/clearGroupChatMsg';
|
||||
|
||||
export default async function removeGroupChat(meetingId, chatId) {
|
||||
check(meetingId, String);
|
||||
check(chatId, String);
|
||||
|
||||
const selector = {
|
||||
chatId,
|
||||
meetingId,
|
||||
};
|
||||
|
||||
try {
|
||||
const numberAffected = await GroupChat.removeAsync(selector);
|
||||
|
||||
if (numberAffected) {
|
||||
Logger.info(`Removed group-chat id=${chatId} meeting=${meetingId}`);
|
||||
clearGroupChatMsg(meetingId, chatId);
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error(`Removing group-chat from collection: ${err}`);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import AuthTokenValidation, { ValidationStates } from '/imports/api/auth-token-validation';
|
||||
|
||||
async function groupChat() {
|
||||
const tokenValidation = await AuthTokenValidation
|
||||
.findOneAsync({ connectionId: this.connection.id });
|
||||
|
||||
if (!tokenValidation || tokenValidation.validationStatus !== ValidationStates.VALIDATED) {
|
||||
Logger.warn(`Publishing GroupChat was requested by unauth connection ${this.connection.id}`);
|
||||
return GroupChat.find({ meetingId: '' });
|
||||
}
|
||||
|
||||
const { meetingId, userId } = tokenValidation;
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
|
||||
|
||||
Logger.debug('Publishing group-chat', { meetingId, userId });
|
||||
|
||||
return GroupChat.find({
|
||||
$or: [
|
||||
{ meetingId, access: PUBLIC_CHAT_TYPE },
|
||||
{ meetingId, users: { $all: [userId] } },
|
||||
],
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
const boundGroupChat = groupChat.bind(this);
|
||||
return boundGroupChat(...args);
|
||||
}
|
||||
|
||||
Meteor.publish('group-chat', publish);
|
@ -1,16 +1,14 @@
|
||||
import { check } from 'meteor/check';
|
||||
import Meetings, { MeetingTimeRemaining } from '/imports/api/meetings';
|
||||
import { MeetingTimeRemaining } from '/imports/api/meetings';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
|
||||
|
||||
export default async function handleTimeRemainingUpdate({ body }, meetingId) {
|
||||
check(meetingId, String);
|
||||
|
||||
check(body, {
|
||||
timeLeftInSec: Number,
|
||||
timeUpdatedInMinutes: Number,
|
||||
});
|
||||
const { timeLeftInSec, timeUpdatedInMinutes } = body;
|
||||
const { timeLeftInSec } = body;
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
@ -27,34 +25,4 @@ export default async function handleTimeRemainingUpdate({ body }, meetingId) {
|
||||
} catch (err) {
|
||||
Logger.error(`Changing recording time: ${err}`);
|
||||
}
|
||||
|
||||
if (timeUpdatedInMinutes > 0) {
|
||||
const Meeting = await Meetings.findOneAsync({ meetingId });
|
||||
|
||||
if (Meeting.meetingProp.isBreakout) {
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
|
||||
const PUBLIC_CHAT_INFO = CHAT_CONFIG.system_messages_keys.chat_info;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
const messageValues = {
|
||||
0: timeUpdatedInMinutes,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${PUBLIC_CHAT_INFO}`,
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: 'breakoutDurationUpdated',
|
||||
messageValues,
|
||||
};
|
||||
|
||||
await addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { removeExternalVideoStreamer } from '/imports/api/external-videos/server
|
||||
|
||||
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
|
||||
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
|
||||
import clearGroupChat from '/imports/api/group-chat/server/modifiers/clearGroupChat';
|
||||
import clearGuestUsers from '/imports/api/guest-users/server/modifiers/clearGuestUsers';
|
||||
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
|
||||
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
|
||||
@ -39,7 +38,6 @@ export default async function meetingHasEnded(meetingId) {
|
||||
await Promise.all([
|
||||
clearCaptions(meetingId),
|
||||
clearPads(meetingId),
|
||||
clearGroupChat(meetingId),
|
||||
clearGuestUsers(meetingId),
|
||||
clearBreakouts(meetingId),
|
||||
clearPolls(meetingId),
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { check } from 'meteor/check';
|
||||
import setPublishedPoll from '../../../meetings/server/modifiers/setPublishedPoll';
|
||||
import handleSendSystemChatForPublishedPoll from './sendPollChatMsg';
|
||||
|
||||
const POLL_CHAT_MESSAGE = Meteor.settings.public.poll.chatMessage;
|
||||
|
||||
export default function pollPublished({ body }, meetingId) {
|
||||
const { pollId } = body;
|
||||
@ -11,8 +8,4 @@ export default function pollPublished({ body }, meetingId) {
|
||||
check(pollId, String);
|
||||
|
||||
setPublishedPoll(meetingId, true);
|
||||
|
||||
if (POLL_CHAT_MESSAGE) {
|
||||
handleSendSystemChatForPublishedPoll({ body }, meetingId);
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
import addSystemMsg from '../../../group-chat-msg/server/modifiers/addSystemMsg';
|
||||
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
|
||||
|
||||
export default function sendPollChatMsg({ body }, meetingId) {
|
||||
const { poll } = body;
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const PUBLIC_CHAT_SYSTEM_ID = CHAT_CONFIG.system_userid;
|
||||
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
const pollResultData = poll;
|
||||
const answers = pollResultData.answers.reduce(caseInsensitiveReducer, []);
|
||||
|
||||
const extra = {
|
||||
type: 'poll',
|
||||
pollResultData: {
|
||||
...pollResultData,
|
||||
answers,
|
||||
},
|
||||
};
|
||||
|
||||
const payload = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${CHAT_POLL_RESULTS_MESSAGE}`,
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_SYSTEM_ID,
|
||||
name: '',
|
||||
},
|
||||
message: '',
|
||||
extra,
|
||||
};
|
||||
|
||||
return addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, payload);
|
||||
}
|
@ -2,15 +2,10 @@ import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Users from '/imports/api/users';
|
||||
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
|
||||
|
||||
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
|
||||
const CHAT_USER_STATUS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_status_message;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
export default function sendAwayStatusChatMsg(meetingId, userId, newAwayStatus) {
|
||||
export default function sendAwayStatusChatMsg(meetingId, userId) {
|
||||
const user = Users.findOne(
|
||||
{ meetingId, userId },
|
||||
{
|
||||
@ -40,33 +35,4 @@ export default function sendAwayStatusChatMsg(meetingId, userId, newAwayStatus)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Send message if previous emoji or actual emoji is 'away'
|
||||
let status;
|
||||
if (user.away && !newAwayStatus) {
|
||||
status = 'notAway';
|
||||
} else if (!user.away && newAwayStatus) {
|
||||
status = 'away';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extra = {
|
||||
type: 'status',
|
||||
status,
|
||||
};
|
||||
|
||||
const payload = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${CHAT_USER_STATUS_MESSAGE}`,
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${userId}-${Date.now()}`,
|
||||
sender: {
|
||||
id: userId,
|
||||
name: user.name,
|
||||
},
|
||||
message: '',
|
||||
extra,
|
||||
};
|
||||
|
||||
return addSystemMsg(meetingId, PUBLIC_GROUP_CHAT_ID, payload);
|
||||
}
|
||||
|
@ -1,161 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import BBBMenu from '/imports/ui/components/common/menu/component';
|
||||
import { getDateString, uniqueId } from '/imports/utils/string-utils';
|
||||
import Trigger from '/imports/ui/components/common/control-header/right/component';
|
||||
|
||||
import ChatService from '../service';
|
||||
import { addNewAlert } from '../../screenreader-alert/service';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
clear: {
|
||||
id: 'app.chat.dropdown.clear',
|
||||
description: 'Clear button label',
|
||||
},
|
||||
save: {
|
||||
id: 'app.chat.dropdown.save',
|
||||
description: 'Clear button label',
|
||||
},
|
||||
copy: {
|
||||
id: 'app.chat.dropdown.copy',
|
||||
description: 'Copy button label',
|
||||
},
|
||||
copySuccess: {
|
||||
id: 'app.chat.copySuccess',
|
||||
description: 'aria success alert',
|
||||
},
|
||||
copyErr: {
|
||||
id: 'app.chat.copyErr',
|
||||
description: 'aria error alert',
|
||||
},
|
||||
options: {
|
||||
id: 'app.chat.dropdown.options',
|
||||
description: 'Chat Options',
|
||||
},
|
||||
});
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const ENABLE_SAVE_AND_COPY_PUBLIC_CHAT = CHAT_CONFIG.enableSaveAndCopyPublicChat;
|
||||
|
||||
class ChatDropdown extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.actionsKey = [
|
||||
uniqueId('action-item-'),
|
||||
uniqueId('action-item-'),
|
||||
uniqueId('action-item-'),
|
||||
];
|
||||
}
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
intl,
|
||||
isMeteorConnected,
|
||||
amIModerator,
|
||||
meetingIsBreakout,
|
||||
meetingName,
|
||||
timeWindowsValues,
|
||||
} = this.props;
|
||||
|
||||
const clearIcon = 'delete';
|
||||
const saveIcon = 'download';
|
||||
const copyIcon = 'copy';
|
||||
|
||||
this.menuItems = [];
|
||||
|
||||
if (ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) {
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: this.actionsKey[0],
|
||||
icon: saveIcon,
|
||||
dataTest: 'chatSave',
|
||||
label: intl.formatMessage(intlMessages.save),
|
||||
onClick: () => {
|
||||
const link = document.createElement('a');
|
||||
const mimeType = 'text/plain';
|
||||
link.setAttribute('download', `bbb-${meetingName}[public-chat]_${getDateString()}.txt`);
|
||||
link.setAttribute(
|
||||
'href',
|
||||
`data: ${mimeType};charset=utf-8,`
|
||||
+ `${encodeURIComponent(ChatService.exportChat(timeWindowsValues, intl))}`,
|
||||
);
|
||||
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) {
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: this.actionsKey[1],
|
||||
icon: copyIcon,
|
||||
id: 'clipboardButton',
|
||||
dataTest: 'chatCopy',
|
||||
label: intl.formatMessage(intlMessages.copy),
|
||||
onClick: () => {
|
||||
const chatHistory = ChatService.exportChat(timeWindowsValues, intl);
|
||||
navigator.clipboard.writeText(chatHistory).then(() => {
|
||||
addNewAlert(intl.formatMessage(intlMessages.copySuccess));
|
||||
}).catch(() => {
|
||||
addNewAlert(intl.formatMessage(intlMessages.copyErr));
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!meetingIsBreakout && amIModerator && isMeteorConnected) {
|
||||
this.menuItems.push(
|
||||
{
|
||||
key: this.actionsKey[2],
|
||||
icon: clearIcon,
|
||||
dataTest: 'chatClear',
|
||||
label: intl.formatMessage(intlMessages.clear),
|
||||
onClick: () => ChatService.clearPublicChatHistory(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.menuItems;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
amIModerator,
|
||||
isRTL,
|
||||
} = this.props;
|
||||
|
||||
if (!amIModerator && !ENABLE_SAVE_AND_COPY_PUBLIC_CHAT) return null;
|
||||
return (
|
||||
<>
|
||||
<BBBMenu
|
||||
trigger={
|
||||
<Trigger
|
||||
data-test="chatOptionsMenu"
|
||||
icon="more"
|
||||
label={intl.formatMessage(intlMessages.options)}
|
||||
aria-label={intl.formatMessage(intlMessages.options)}
|
||||
onClick={() => null}
|
||||
/>
|
||||
}
|
||||
opts={{
|
||||
id: 'chat-options-dropdown-menu',
|
||||
keepMounted: true,
|
||||
transitionDuration: 0,
|
||||
elevation: 3,
|
||||
getcontentanchorel: null,
|
||||
fullwidth: 'true',
|
||||
anchorOrigin: { vertical: 'bottom', horizontal: isRTL ? 'right' : 'left' },
|
||||
transformOrigin: { vertical: 'top', horizontal: isRTL ? 'right' : 'left' },
|
||||
}}
|
||||
actions={this.getAvailableActions()}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ChatDropdown);
|
@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import ChatDropdown from './component';
|
||||
import { layoutSelect } from '../../layout/context';
|
||||
|
||||
const ChatDropdownContainer = ({ ...props }) => {
|
||||
const isRTL = layoutSelect((i) => i.isRTL);
|
||||
|
||||
return <ChatDropdown {...{ isRTL, ...props }} />;
|
||||
};
|
||||
|
||||
export default withTracker(() => {
|
||||
const getMeetingName = () => {
|
||||
const m = Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { 'meetingProp.name': 1 } });
|
||||
return m.meetingProp.name;
|
||||
};
|
||||
|
||||
return {
|
||||
meetingName: getMeetingName(),
|
||||
};
|
||||
})(ChatDropdownContainer);
|
@ -18,7 +18,6 @@ import useChat from '/imports/ui/core/hooks/useChat';
|
||||
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||
import {
|
||||
startUserTyping,
|
||||
stopUserTyping,
|
||||
textToMarkdown,
|
||||
} from './service';
|
||||
import { Chat } from '/imports/ui/Types/chat';
|
||||
@ -273,7 +272,6 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
||||
updateUnreadMessages(chatId, '');
|
||||
setHasErrors(false);
|
||||
setShowEmojiPicker(false);
|
||||
if (ENABLE_TYPING_INDICATOR) stopUserTyping();
|
||||
const sentMessageEvent = new CustomEvent(ChatEvents.SENT_MESSAGE);
|
||||
window.dispatchEvent(sentMessageEvent);
|
||||
|
||||
|
@ -13,7 +13,6 @@ export const startUserTyping = throttle(
|
||||
START_TYPING_THROTTLE_INTERVAL,
|
||||
{ leading: true, trailing: false },
|
||||
);
|
||||
export const stopUserTyping = () => makeCall('stopUserTyping');
|
||||
|
||||
export const textToMarkdown = (message: string) => {
|
||||
let parsedMessage = message || '';
|
||||
@ -31,6 +30,5 @@ export const textToMarkdown = (message: string) => {
|
||||
|
||||
export default {
|
||||
startUserTyping,
|
||||
stopUserTyping,
|
||||
textToMarkdown,
|
||||
};
|
||||
|
@ -1,144 +0,0 @@
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import Styled from './styles';
|
||||
import MessageFormContainer from './message-form/container';
|
||||
import TimeWindowList from './time-window-list/container';
|
||||
import ChatDropdownContainer from './chat-dropdown/container';
|
||||
import { PANELS, ACTIONS } from '../layout/enums';
|
||||
import { UserSentMessageCollection } from './service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import browserInfo from '/imports/utils/browserInfo';
|
||||
import Header from '/imports/ui/components/common/control-header/component';
|
||||
import { CLOSE_PRIVATE_CHAT_MUTATION } from '../user-list/user-list-content/user-messages/chat-list/queries';
|
||||
import ChatPopup from './chat-graphql/chat-popup/component';
|
||||
import { useMutation, gql } from '@apollo/client';
|
||||
import ChatHeader from './chat-graphql/chat-header/component';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_ID = CHAT_CONFIG.public_id;
|
||||
const ELEMENT_ID = 'chat-messages';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
closeChatLabel: {
|
||||
id: 'app.chat.closeChatLabel',
|
||||
description: 'aria-label for closing chat button',
|
||||
},
|
||||
hideChatLabel: {
|
||||
id: 'app.chat.hideChatLabel',
|
||||
description: 'aria-label for hiding chat button',
|
||||
},
|
||||
});
|
||||
|
||||
const Chat = (props) => {
|
||||
const {
|
||||
chatID,
|
||||
title,
|
||||
messages,
|
||||
partnerIsLoggedOut,
|
||||
isChatLocked,
|
||||
actions,
|
||||
intl,
|
||||
shortcuts,
|
||||
isMeteorConnected,
|
||||
lastReadMessageTime,
|
||||
hasUnreadMessages,
|
||||
scrollPosition,
|
||||
amIModerator,
|
||||
meetingIsBreakout,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
count,
|
||||
layoutContextDispatch,
|
||||
syncing,
|
||||
syncedPercent,
|
||||
lastTimeWindowValuesBuild,
|
||||
width,
|
||||
} = props;
|
||||
|
||||
const [updateVisible] = useMutation(CLOSE_PRIVATE_CHAT_MUTATION);
|
||||
|
||||
const handleClosePrivateChat = () => {
|
||||
updateVisible(({
|
||||
variables: {
|
||||
chatId: chatID
|
||||
},
|
||||
}))
|
||||
actions.handleClosePrivateChat(chatID);
|
||||
};
|
||||
|
||||
const userSentMessage = UserSentMessageCollection.findOne({ userId: Auth.userID, sent: true });
|
||||
const { isChrome } = browserInfo;
|
||||
|
||||
const HIDE_CHAT_AK = shortcuts.hideprivatechat;
|
||||
const CLOSE_CHAT_AK = shortcuts.closeprivatechat;
|
||||
const isPublicChat = chatID === PUBLIC_CHAT_ID;
|
||||
ChatLogger.debug('ChatComponent::render', props);
|
||||
return (
|
||||
<Styled.Chat
|
||||
isChrome={isChrome}
|
||||
data-test={isPublicChat ? 'publicChat' : 'privateChat'}
|
||||
>
|
||||
<ChatHeader />
|
||||
<TimeWindowList
|
||||
id={ELEMENT_ID}
|
||||
chatId={chatID}
|
||||
handleScrollUpdate={actions.handleScrollUpdate}
|
||||
{...{
|
||||
partnerIsLoggedOut,
|
||||
lastReadMessageTime,
|
||||
hasUnreadMessages,
|
||||
scrollPosition,
|
||||
messages,
|
||||
currentUserIsModerator: amIModerator,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
count,
|
||||
syncing,
|
||||
syncedPercent,
|
||||
lastTimeWindowValuesBuild,
|
||||
userSentMessage,
|
||||
width,
|
||||
}}
|
||||
/>
|
||||
<MessageFormContainer
|
||||
title={title}
|
||||
chatId={chatID}
|
||||
chatTitle={title}
|
||||
chatAreaId={ELEMENT_ID}
|
||||
disabled={isChatLocked || !isMeteorConnected}
|
||||
connected={isMeteorConnected}
|
||||
locked={isChatLocked}
|
||||
partnerIsLoggedOut={partnerIsLoggedOut}
|
||||
/>
|
||||
</Styled.Chat>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withShortcutHelper(injectWbResizeEvent(injectIntl(Chat)), ['hidePrivateChat', 'closePrivateChat']));
|
||||
|
||||
const propTypes = {
|
||||
chatID: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
shortcuts: PropTypes.objectOf(PropTypes.string),
|
||||
partnerIsLoggedOut: PropTypes.bool.isRequired,
|
||||
isChatLocked: PropTypes.bool.isRequired,
|
||||
isMeteorConnected: PropTypes.bool.isRequired,
|
||||
actions: PropTypes.shape({
|
||||
handleClosePrivateChat: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
shortcuts: [],
|
||||
};
|
||||
|
||||
Chat.propTypes = propTypes;
|
||||
Chat.defaultProps = defaultProps;
|
@ -1,273 +0,0 @@
|
||||
import React, { useEffect, useContext, useState } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
import { ChatContext, getLoginTime } from '../components-data/chat-context/context';
|
||||
import { GroupChatContext } from '../components-data/group-chat-context/context';
|
||||
import { UsersContext } from '../components-data/users-context/context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import lockContextContainer from '/imports/ui/components/lock-viewers/context/container';
|
||||
// import Chat from '/imports/ui/components/chat/component';
|
||||
import ChatService from './service';
|
||||
import { layoutSelect, layoutDispatch } from '../layout/context';
|
||||
import { escapeHtml } from '/imports/utils/string-utils';
|
||||
|
||||
import Chat from './chat-graphql/component';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_GROUP_CHAT_KEY = CHAT_CONFIG.public_group_id;
|
||||
const CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
const DEBOUNCE_TIME = 1000;
|
||||
|
||||
const sysMessagesIds = {
|
||||
welcomeId: `${SYSTEM_CHAT_TYPE}-welcome-msg`,
|
||||
moderatorId: `${SYSTEM_CHAT_TYPE}-moderator-msg`,
|
||||
syncId: `${SYSTEM_CHAT_TYPE}-sync-msg`,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
[CHAT_CLEAR]: {
|
||||
id: 'app.chat.clearPublicChatMessage',
|
||||
description: 'message of when clear the public chat',
|
||||
},
|
||||
titlePublic: {
|
||||
id: 'app.chat.titlePublic',
|
||||
description: 'Public chat title',
|
||||
},
|
||||
titlePrivate: {
|
||||
id: 'app.chat.titlePrivate',
|
||||
description: 'Private chat title',
|
||||
},
|
||||
partnerDisconnected: {
|
||||
id: 'app.chat.partnerDisconnected',
|
||||
description: 'System chat message when the private chat partnet disconnect from the meeting',
|
||||
},
|
||||
loading: {
|
||||
id: 'app.chat.loading',
|
||||
description: 'loading message',
|
||||
},
|
||||
});
|
||||
|
||||
let previousChatId = null;
|
||||
let prevSync = false;
|
||||
let prevPartnerIsLoggedOut = false;
|
||||
|
||||
let globalAppplyStateToProps = () => { };
|
||||
|
||||
const throttledFunc = throttle(() => {
|
||||
globalAppplyStateToProps();
|
||||
}, DEBOUNCE_TIME, { trailing: true, leading: true });
|
||||
|
||||
const ChatContainer = (props) => {
|
||||
const {
|
||||
children,
|
||||
loginTime,
|
||||
intl,
|
||||
userLocks,
|
||||
lockSettings,
|
||||
isChatLockedPublic,
|
||||
isChatLockedPrivate,
|
||||
users: propUsers,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const idChatOpen = layoutSelect((i) => i.idChatOpen);
|
||||
const layoutContextDispatch = layoutDispatch();
|
||||
|
||||
const isPublicChat = idChatOpen === PUBLIC_CHAT_KEY;
|
||||
|
||||
const chatID = idChatOpen;
|
||||
|
||||
if (!chatID) return null;
|
||||
|
||||
useEffect(() => {
|
||||
ChatService.removeFromClosedChatsSession();
|
||||
}, []);
|
||||
|
||||
const modOnlyMessage = Storage.getItem('ModeratorOnlyMessage');
|
||||
const { welcomeProp } = ChatService.getWelcomeProp();
|
||||
|
||||
ChatLogger.debug('ChatContainer::render::props', props);
|
||||
|
||||
const systemMessages = {
|
||||
[sysMessagesIds.welcomeId]: {
|
||||
id: sysMessagesIds.welcomeId,
|
||||
content: [{
|
||||
id: sysMessagesIds.welcomeId,
|
||||
text: welcomeProp.welcomeMsg,
|
||||
time: loginTime,
|
||||
}],
|
||||
key: sysMessagesIds.welcomeId,
|
||||
time: loginTime,
|
||||
sender: null,
|
||||
},
|
||||
[sysMessagesIds.moderatorId]: {
|
||||
id: sysMessagesIds.moderatorId,
|
||||
content: [{
|
||||
id: sysMessagesIds.moderatorId,
|
||||
text: modOnlyMessage,
|
||||
time: loginTime + 1,
|
||||
}],
|
||||
key: sysMessagesIds.moderatorId,
|
||||
time: loginTime + 1,
|
||||
sender: null,
|
||||
},
|
||||
};
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
const currentUser = users[Auth.meetingID][Auth.userID];
|
||||
const amIModerator = currentUser.role === ROLE_MODERATOR;
|
||||
const systemMessagesIds = [
|
||||
sysMessagesIds.welcomeId,
|
||||
amIModerator && modOnlyMessage && sysMessagesIds.moderatorId,
|
||||
].filter((i) => i);
|
||||
|
||||
const usingChatContext = useContext(ChatContext);
|
||||
const usingGroupChatContext = useContext(GroupChatContext);
|
||||
const [stateLastMsg, setLastMsg] = useState(null);
|
||||
|
||||
const [
|
||||
stateTimeWindows, setTimeWindows,
|
||||
] = useState(isPublicChat ? [...systemMessagesIds.map((item) => systemMessages[item])] : []);
|
||||
const [lastTimeWindowValuesBuild, setLastTimeWindowValuesBuild] = useState(0);
|
||||
|
||||
const { groupChat } = usingGroupChatContext;
|
||||
const participants = groupChat[idChatOpen]?.participants;
|
||||
const chatName = participants?.filter((user) => user.id !== Auth.userID)[0]?.name;
|
||||
const title = chatName
|
||||
? intl.formatMessage(intlMessages.titlePrivate, { 0: chatName })
|
||||
: intl.formatMessage(intlMessages.titlePublic);
|
||||
|
||||
let partnerIsLoggedOut = false;
|
||||
|
||||
let isChatLocked;
|
||||
if (!isPublicChat) {
|
||||
const idUser = participants?.filter((user) => user.id !== Auth.userID)[0]?.id;
|
||||
partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut
|
||||
|| users[Auth.meetingID][idUser]?.ejected);
|
||||
isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR);
|
||||
} else {
|
||||
isChatLocked = isChatLockedPublic;
|
||||
}
|
||||
|
||||
const contextChat = usingChatContext?.chats[isPublicChat ? PUBLIC_GROUP_CHAT_KEY : chatID];
|
||||
const lastTimeWindow = contextChat?.lastTimewindow;
|
||||
const lastMsg = contextChat && (isPublicChat
|
||||
? contextChat?.preJoinMessages[lastTimeWindow] || contextChat?.posJoinMessages[lastTimeWindow]
|
||||
: contextChat?.messageGroups[lastTimeWindow]);
|
||||
ChatLogger.debug('ChatContainer::render::chatData', contextChat);
|
||||
const applyPropsToState = () => {
|
||||
ChatLogger.debug('ChatContainer::applyPropsToState::chatData', lastMsg, stateLastMsg, contextChat?.syncing);
|
||||
if (
|
||||
(lastMsg?.lastTimestamp !== stateLastMsg?.lastTimestamp)
|
||||
|| (previousChatId !== idChatOpen)
|
||||
|| (prevSync !== contextChat?.syncing)
|
||||
|| (prevPartnerIsLoggedOut !== partnerIsLoggedOut)
|
||||
) {
|
||||
prevSync = contextChat?.syncing;
|
||||
prevPartnerIsLoggedOut = partnerIsLoggedOut;
|
||||
|
||||
const timeWindowsValues = isPublicChat
|
||||
? [
|
||||
...(
|
||||
!contextChat?.syncing ? Object.values(contextChat?.preJoinMessages || {}) : [
|
||||
{
|
||||
id: sysMessagesIds.syncId,
|
||||
content: [{
|
||||
id: 'synced',
|
||||
text: intl.formatMessage(intlMessages.loading, { 0: contextChat?.syncedPercent }),
|
||||
time: loginTime + 1,
|
||||
}],
|
||||
key: sysMessagesIds.syncId,
|
||||
time: loginTime + 1,
|
||||
sender: null,
|
||||
},
|
||||
]
|
||||
), ...systemMessagesIds.map((item) => systemMessages[item]),
|
||||
...Object.values(contextChat?.posJoinMessages || {})]
|
||||
: [...Object.values(contextChat?.messageGroups || {})];
|
||||
if (previousChatId !== idChatOpen) {
|
||||
previousChatId = idChatOpen;
|
||||
}
|
||||
|
||||
if (partnerIsLoggedOut) {
|
||||
const time = Date.now();
|
||||
const id = `partner-disconnected-${time}`;
|
||||
const messagePartnerLoggedOut = {
|
||||
id,
|
||||
content: [{
|
||||
id,
|
||||
text: escapeHtml(intl.formatMessage(intlMessages.partnerDisconnected, { 0: chatName })),
|
||||
time,
|
||||
}],
|
||||
time,
|
||||
sender: null,
|
||||
};
|
||||
|
||||
timeWindowsValues.push(messagePartnerLoggedOut);
|
||||
}
|
||||
|
||||
setLastMsg(lastMsg ? { ...lastMsg } : lastMsg);
|
||||
setTimeWindows(timeWindowsValues);
|
||||
setLastTimeWindowValuesBuild(Date.now());
|
||||
}
|
||||
};
|
||||
globalAppplyStateToProps = applyPropsToState;
|
||||
throttledFunc();
|
||||
|
||||
ChatService.removePackagedClassAttribute(
|
||||
['ReactVirtualized__Grid', 'ReactVirtualized__Grid__innerScrollContainer'],
|
||||
'role',
|
||||
);
|
||||
|
||||
return (
|
||||
<Chat {...{
|
||||
idChatOpen,
|
||||
isChatLocked,
|
||||
...restProps,
|
||||
chatID,
|
||||
amIModerator,
|
||||
count: (contextChat?.unreadTimeWindows.size || 0),
|
||||
timeWindowsValues: stateTimeWindows,
|
||||
dispatch: usingChatContext?.dispatch,
|
||||
title,
|
||||
syncing: contextChat?.syncing,
|
||||
syncedPercent: contextChat?.syncedPercent,
|
||||
chatName,
|
||||
contextChat,
|
||||
layoutContextDispatch,
|
||||
lastTimeWindowValuesBuild,
|
||||
partnerIsLoggedOut,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Chat>
|
||||
);
|
||||
};
|
||||
|
||||
lockContextContainer(injectIntl(withTracker(({ intl, userLocks }) => {
|
||||
const isChatLockedPublic = userLocks.userPublicChat;
|
||||
const isChatLockedPrivate = userLocks.userPrivateChat;
|
||||
|
||||
const { connected: isMeteorConnected } = Meteor.status();
|
||||
|
||||
return {
|
||||
intl,
|
||||
isChatLockedPublic,
|
||||
isChatLockedPrivate,
|
||||
isMeteorConnected,
|
||||
meetingIsBreakout: meetingIsBreakout(),
|
||||
loginTime: getLoginTime(),
|
||||
actions: {
|
||||
handleClosePrivateChat: ChatService.closePrivateChat,
|
||||
},
|
||||
};
|
||||
})(ChatContainer)));
|
||||
|
||||
export default Chat;
|
@ -1,399 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { checkText } from 'smile2emoji';
|
||||
import deviceInfo from '/imports/utils/deviceInfo';
|
||||
import PropTypes from 'prop-types';
|
||||
import TypingIndicatorContainer from '/imports/ui/components/chat/chat-graphql/chat-typing-indicator/component';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ClickOutside from '/imports/ui/components/click-outside/component';
|
||||
import Styled from './styles';
|
||||
import { escapeHtml } from '/imports/utils/string-utils';
|
||||
import { isChatEnabled } from '/imports/ui/services/features';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
chatId: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
minMessageLength: PropTypes.number.isRequired,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
chatTitle: PropTypes.string.isRequired,
|
||||
chatAreaId: PropTypes.string.isRequired,
|
||||
handleSendMessage: PropTypes.func.isRequired,
|
||||
UnsentMessagesCollection: PropTypes.objectOf(Object).isRequired,
|
||||
connected: PropTypes.bool.isRequired,
|
||||
locked: PropTypes.bool.isRequired,
|
||||
partnerIsLoggedOut: PropTypes.bool.isRequired,
|
||||
stopUserTyping: PropTypes.func.isRequired,
|
||||
startUserTyping: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
submitLabel: {
|
||||
id: 'app.chat.submitLabel',
|
||||
description: 'Chat submit button label',
|
||||
},
|
||||
inputLabel: {
|
||||
id: 'app.chat.inputLabel',
|
||||
description: 'Chat message input label',
|
||||
},
|
||||
emojiButtonLabel: {
|
||||
id: 'app.chat.emojiButtonLabel',
|
||||
description: 'Chat message emoji picker button label',
|
||||
},
|
||||
inputPlaceholder: {
|
||||
id: 'app.chat.inputPlaceholder',
|
||||
description: 'Chat message input placeholder',
|
||||
},
|
||||
errorMaxMessageLength: {
|
||||
id: 'app.chat.errorMaxMessageLength',
|
||||
},
|
||||
errorServerDisconnected: {
|
||||
id: 'app.chat.disconnected',
|
||||
},
|
||||
errorChatLocked: {
|
||||
id: 'app.chat.locked',
|
||||
},
|
||||
singularTyping: {
|
||||
id: 'app.chat.singularTyping',
|
||||
description: 'used to indicate when 1 user is typing',
|
||||
},
|
||||
pluralTyping: {
|
||||
id: 'app.chat.pluralTyping',
|
||||
description: 'used to indicate when multiple user are typing',
|
||||
},
|
||||
severalPeople: {
|
||||
id: 'app.chat.severalPeople',
|
||||
description: 'displayed when 4 or more users are typing',
|
||||
},
|
||||
});
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const AUTO_CONVERT_EMOJI = Meteor.settings.public.chat.autoConvertEmoji;
|
||||
const ENABLE_EMOJI_PICKER = Meteor.settings.public.chat.emojiPicker.enable;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
const PUBLIC_CHAT_GROUP_KEY = CHAT_CONFIG.public_group_id;
|
||||
|
||||
class MessageForm extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
message: '',
|
||||
error: null,
|
||||
hasErrors: false,
|
||||
showEmojiPicker: false,
|
||||
};
|
||||
|
||||
this.handleMessageChange = this.handleMessageChange.bind(this);
|
||||
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.setMessageHint = this.setMessageHint.bind(this);
|
||||
this.handleUserTyping = this.handleUserTyping.bind(this);
|
||||
this.typingIndicator = CHAT_CONFIG.typingIndicator.enabled;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { isMobile } = deviceInfo;
|
||||
this.setMessageState();
|
||||
this.setMessageHint();
|
||||
|
||||
if (!isMobile) {
|
||||
if (this.textarea) this.textarea.focus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
chatId,
|
||||
connected,
|
||||
locked,
|
||||
partnerIsLoggedOut,
|
||||
} = this.props;
|
||||
const { message } = this.state;
|
||||
const { isMobile } = deviceInfo;
|
||||
|
||||
if (prevProps.chatId !== chatId && !isMobile) {
|
||||
if (this.textarea) this.textarea.focus();
|
||||
}
|
||||
|
||||
if (prevProps.chatId !== chatId) {
|
||||
this.updateUnsentMessagesCollection(prevProps.chatId, message);
|
||||
this.setState(
|
||||
{
|
||||
error: null,
|
||||
hasErrors: false,
|
||||
}, this.setMessageState(),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
connected !== prevProps.connected
|
||||
|| locked !== prevProps.locked
|
||||
|| partnerIsLoggedOut !== prevProps.partnerIsLoggedOut
|
||||
) {
|
||||
this.setMessageHint();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { chatId } = this.props;
|
||||
const { message } = this.state;
|
||||
this.updateUnsentMessagesCollection(chatId, message);
|
||||
this.setMessageState();
|
||||
}
|
||||
|
||||
handleClickOutside() {
|
||||
const { showEmojiPicker } = this.state;
|
||||
if (showEmojiPicker) {
|
||||
this.setState({ showEmojiPicker: false });
|
||||
}
|
||||
}
|
||||
|
||||
setMessageHint() {
|
||||
const {
|
||||
connected,
|
||||
disabled,
|
||||
intl,
|
||||
locked,
|
||||
partnerIsLoggedOut,
|
||||
} = this.props;
|
||||
|
||||
let chatDisabledHint = null;
|
||||
|
||||
if (disabled && !partnerIsLoggedOut) {
|
||||
if (connected) {
|
||||
if (locked) {
|
||||
chatDisabledHint = messages.errorChatLocked;
|
||||
}
|
||||
} else {
|
||||
chatDisabledHint = messages.errorServerDisconnected;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
hasErrors: disabled,
|
||||
error: chatDisabledHint ? intl.formatMessage(chatDisabledHint) : null,
|
||||
});
|
||||
}
|
||||
|
||||
setMessageState() {
|
||||
const { chatId, UnsentMessagesCollection } = this.props;
|
||||
const unsentMessageByChat = UnsentMessagesCollection.findOne({ chatId },
|
||||
{ fields: { message: 1 } });
|
||||
this.setState({ message: unsentMessageByChat ? unsentMessageByChat.message : '' });
|
||||
}
|
||||
|
||||
updateUnsentMessagesCollection(chatId, message) {
|
||||
const { UnsentMessagesCollection } = this.props;
|
||||
UnsentMessagesCollection.upsert(
|
||||
{ chatId },
|
||||
{ $set: { message } },
|
||||
);
|
||||
}
|
||||
|
||||
handleMessageKeyDown(e) {
|
||||
// TODO Prevent send message pressing enter on mobile and/or virtual keyboard
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
|
||||
const event = new Event('submit', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
|
||||
this.handleSubmit(event);
|
||||
}
|
||||
}
|
||||
|
||||
handleUserTyping(error) {
|
||||
const { startUserTyping, chatId } = this.props;
|
||||
if (error || !this.typingIndicator) return;
|
||||
startUserTyping(chatId);
|
||||
}
|
||||
|
||||
handleMessageChange(e) {
|
||||
const {
|
||||
intl,
|
||||
maxMessageLength,
|
||||
} = this.props;
|
||||
|
||||
let message = null;
|
||||
let error = null;
|
||||
|
||||
if (AUTO_CONVERT_EMOJI) {
|
||||
message = checkText(e.target.value);
|
||||
} else {
|
||||
message = e.target.value;
|
||||
}
|
||||
|
||||
if (message.length > maxMessageLength) {
|
||||
error = intl.formatMessage(
|
||||
messages.errorMaxMessageLength,
|
||||
{ 0: maxMessageLength },
|
||||
);
|
||||
message = message.substring(0, maxMessageLength);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
message,
|
||||
error,
|
||||
}, this.handleUserTyping(error));
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const {
|
||||
disabled,
|
||||
minMessageLength,
|
||||
maxMessageLength,
|
||||
handleSendMessage,
|
||||
stopUserTyping,
|
||||
} = this.props;
|
||||
const { message } = this.state;
|
||||
const msg = message.trim();
|
||||
|
||||
if (msg.length < minMessageLength) return;
|
||||
|
||||
if (disabled
|
||||
|| msg.length > maxMessageLength) {
|
||||
this.setState({ hasErrors: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = this.typingIndicator ? stopUserTyping : null;
|
||||
|
||||
handleSendMessage(escapeHtml(msg));
|
||||
this.setState({ message: '', error: '', hasErrors: false, showEmojiPicker: false }, callback);
|
||||
}
|
||||
|
||||
handleEmojiSelect(emojiObject) {
|
||||
const { message } = this.state;
|
||||
const cursor = this.textarea.selectionStart;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
message: message.slice(0, cursor)
|
||||
+ emojiObject.native
|
||||
+ message.slice(cursor),
|
||||
},
|
||||
);
|
||||
|
||||
const newCursor = cursor + emojiObject.native.length;
|
||||
setTimeout(() => this.textarea.setSelectionRange(newCursor, newCursor), 10);
|
||||
}
|
||||
|
||||
renderEmojiPicker() {
|
||||
const { showEmojiPicker } = this.state;
|
||||
|
||||
if (showEmojiPicker) {
|
||||
return (
|
||||
<Styled.EmojiPickerWrapper>
|
||||
<Styled.EmojiPicker
|
||||
onEmojiSelect={(emojiObject) => this.handleEmojiSelect(emojiObject)}
|
||||
/>
|
||||
</Styled.EmojiPickerWrapper>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderEmojiButton() {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<Styled.EmojiButton
|
||||
onClick={() => this.setState((prevState) => ({
|
||||
showEmojiPicker: !prevState.showEmojiPicker,
|
||||
}))}
|
||||
icon="happy"
|
||||
color="light"
|
||||
ghost
|
||||
type="button"
|
||||
circle
|
||||
hideLabel
|
||||
label={intl.formatMessage(messages.emojiButtonLabel)}
|
||||
data-test="emojiPickerButton"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm() {
|
||||
const {
|
||||
intl,
|
||||
chatTitle,
|
||||
title,
|
||||
disabled,
|
||||
idChatOpen,
|
||||
partnerIsLoggedOut,
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
hasErrors, error, message,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Styled.Form
|
||||
ref={(ref) => { this.form = ref; }}
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
{this.renderEmojiPicker()}
|
||||
<Styled.Wrapper>
|
||||
<Styled.Input
|
||||
id="message-input"
|
||||
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
|
||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
||||
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
|
||||
aria-invalid={hasErrors ? 'true' : 'false'}
|
||||
autoCorrect="off"
|
||||
autoComplete="off"
|
||||
spellCheck="true"
|
||||
disabled={disabled || partnerIsLoggedOut}
|
||||
value={message}
|
||||
onChange={this.handleMessageChange}
|
||||
onKeyDown={this.handleMessageKeyDown}
|
||||
onPaste={(e) => { e.stopPropagation(); }}
|
||||
onCut={(e) => { e.stopPropagation(); }}
|
||||
onCopy={(e) => { e.stopPropagation(); }}
|
||||
async
|
||||
/>
|
||||
{ENABLE_EMOJI_PICKER && this.renderEmojiButton()}
|
||||
<Styled.SendButton
|
||||
hideLabel
|
||||
circle
|
||||
aria-label={intl.formatMessage(messages.submitLabel)}
|
||||
type="submit"
|
||||
disabled={disabled || partnerIsLoggedOut}
|
||||
label={intl.formatMessage(messages.submitLabel)}
|
||||
color="primary"
|
||||
icon="send"
|
||||
onClick={() => { }}
|
||||
data-test="sendMessageButton"
|
||||
/>
|
||||
</Styled.Wrapper>
|
||||
<TypingIndicatorContainer
|
||||
{...{ idChatOpen, error }}
|
||||
isPrivate={idChatOpen !== PUBLIC_CHAT_KEY}
|
||||
isTypingTo={idChatOpen === PUBLIC_CHAT_KEY ? PUBLIC_CHAT_GROUP_KEY : idChatOpen}
|
||||
userId={Auth.userID}
|
||||
/>
|
||||
</Styled.Form>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!isChatEnabled()) return null;
|
||||
|
||||
return ENABLE_EMOJI_PICKER ? (
|
||||
<ClickOutside
|
||||
onClick={() => this.handleClickOutside()}
|
||||
>
|
||||
{this.renderForm()}
|
||||
</ClickOutside>
|
||||
) : this.renderForm();
|
||||
}
|
||||
}
|
||||
|
||||
MessageForm.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(MessageForm);
|
@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import { throttle } from '/imports/utils/throttle';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import MessageForm from './component';
|
||||
import ChatService from '/imports/ui/components/chat/service';
|
||||
import ChatMessageFormContainer from '../chat-graphql/chat-message-form/component';
|
||||
import { layoutSelect } from '../../layout/context';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const START_TYPING_THROTTLE_INTERVAL = 2000;
|
||||
|
||||
const MessageFormContainer = (props) => {
|
||||
const idChatOpen = layoutSelect((i) => i.idChatOpen);
|
||||
|
||||
const handleSendMessage = (message) => {
|
||||
ChatService.setUserSentMessage(true);
|
||||
return ChatService.sendGroupMessage(message, idChatOpen);
|
||||
};
|
||||
const startUserTyping = throttle(
|
||||
(chatId) => makeCall('startUserTyping', chatId),
|
||||
START_TYPING_THROTTLE_INTERVAL,
|
||||
);
|
||||
const stopUserTyping = () => makeCall('stopUserTyping');
|
||||
|
||||
return (
|
||||
<MessageForm
|
||||
{...{
|
||||
startUserTyping,
|
||||
stopUserTyping,
|
||||
UnsentMessagesCollection: ChatService.UnsentMessagesCollection,
|
||||
minMessageLength: CHAT_CONFIG.min_message_length,
|
||||
maxMessageLength: CHAT_CONFIG.max_message_length,
|
||||
handleSendMessage,
|
||||
idChatOpen,
|
||||
...props,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatMessageFormContainer;
|
@ -1,143 +0,0 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
colorBlueLight,
|
||||
colorText,
|
||||
colorGrayLighter,
|
||||
colorGrayLight,
|
||||
colorPrimary,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import {
|
||||
smPaddingX,
|
||||
smPaddingY,
|
||||
borderRadius,
|
||||
borderSize,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import EmojiPickerComponent from '/imports/ui/components/emoji-picker/component';
|
||||
import Button from '/imports/ui/components/common/button/component';
|
||||
|
||||
const Form = styled.form`
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
align-self: flex-end;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: calc(-1 * ${smPaddingX});
|
||||
margin-top: .2rem;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Input = styled(TextareaAutosize)`
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
background-clip: padding-box;
|
||||
margin: 0;
|
||||
color: ${colorText};
|
||||
-webkit-appearance: none;
|
||||
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
|
||||
resize: none;
|
||||
transition: none;
|
||||
border-radius: ${borderRadius};
|
||||
font-size: ${fontSizeBase};
|
||||
line-height: 1;
|
||||
min-height: 2.5rem;
|
||||
max-height: 10rem;
|
||||
border: 1px solid ${colorGrayLighter};
|
||||
box-shadow: 0 0 0 1px ${colorGrayLighter};
|
||||
|
||||
&:disabled,
|
||||
&[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: .75;
|
||||
background-color: rgba(167,179,189,0.25);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-radius: ${borderSize};
|
||||
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
outline: transparent;
|
||||
outline-style: dotted;
|
||||
outline-width: ${borderSize};
|
||||
}
|
||||
|
||||
${({ emojiEnabled }) => emojiEnabled ?
|
||||
css`
|
||||
padding-left: calc(${smPaddingX} * 3);
|
||||
`
|
||||
: null
|
||||
}
|
||||
`;
|
||||
|
||||
const SendButton = styled(Button)`
|
||||
margin:0 0 0 ${smPaddingX};
|
||||
align-self: center;
|
||||
font-size: 0.9rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 ${smPaddingX} 0 0;
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const EmojiButton = styled(Button)`
|
||||
margin:0 0 0 ${smPaddingX};
|
||||
align-self: center;
|
||||
font-size: 0.5rem;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 ${smPaddingX} 0 0;
|
||||
-webkit-transform: scale(-1, 1);
|
||||
-moz-transform: scale(-1, 1);
|
||||
-ms-transform: scale(-1, 1);
|
||||
-o-transform: scale(-1, 1);
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
`;
|
||||
|
||||
const EmojiPickerWrapper = styled.div`
|
||||
em-emoji-picker {
|
||||
width: 100%;
|
||||
max-height: 300px;
|
||||
}
|
||||
padding-bottom: 5px;
|
||||
`;
|
||||
|
||||
const EmojiPicker = styled(EmojiPickerComponent)``;
|
||||
|
||||
const EmojiButtonWrapper = styled.div`
|
||||
color: ${colorGrayLight};
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border: none;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: ${fontSizeBase};
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Form,
|
||||
Wrapper,
|
||||
Input,
|
||||
SendButton,
|
||||
EmojiButton,
|
||||
EmojiButtonWrapper,
|
||||
EmojiPicker,
|
||||
EmojiPickerWrapper,
|
||||
};
|
@ -1,139 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import {
|
||||
defineMessages, injectIntl, FormattedMessage,
|
||||
} from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import Styled from './styles';
|
||||
|
||||
const propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
typingUsers: PropTypes.arrayOf(Object).isRequired,
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
severalPeople: {
|
||||
id: 'app.chat.multi.typing',
|
||||
description: 'displayed when 4 or more users are typing',
|
||||
},
|
||||
someoneTyping: {
|
||||
id: 'app.chat.someone.typing',
|
||||
description: 'label used when one user is typing with disabled name',
|
||||
},
|
||||
});
|
||||
|
||||
class TypingIndicator extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.renderTypingElement = this.renderTypingElement.bind(this);
|
||||
}
|
||||
|
||||
renderTypingElement() {
|
||||
const {
|
||||
typingUsers, indicatorEnabled, indicatorShowNames, intl,
|
||||
} = this.props;
|
||||
|
||||
if (!indicatorEnabled || !typingUsers) return null;
|
||||
|
||||
const { length } = typingUsers;
|
||||
|
||||
let element = null;
|
||||
|
||||
if (indicatorShowNames) {
|
||||
const isSingleTyper = length === 1;
|
||||
const isCoupleTyper = length === 2;
|
||||
const isMultiTypers = length > 2;
|
||||
|
||||
if (isSingleTyper) {
|
||||
const { name } = typingUsers[0];
|
||||
element = (
|
||||
<FormattedMessage
|
||||
id="app.chat.one.typing"
|
||||
description="label used when one user is typing"
|
||||
values={{
|
||||
0: <Styled.SingleTyper>
|
||||
{`${name}`}
|
||||
|
||||
</Styled.SingleTyper>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCoupleTyper) {
|
||||
const {name} = typingUsers[0];
|
||||
const {name: name2} = typingUsers[1];
|
||||
element = (
|
||||
<FormattedMessage
|
||||
id="app.chat.two.typing"
|
||||
description="label used when two users are typing"
|
||||
values={{
|
||||
0: <Styled.CoupleTyper>
|
||||
{`${name}`}
|
||||
|
||||
</Styled.CoupleTyper>,
|
||||
1: <Styled.CoupleTyper>
|
||||
|
||||
{`${name2}`}
|
||||
|
||||
</Styled.CoupleTyper>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMultiTypers) {
|
||||
element = (
|
||||
<span>
|
||||
{`${intl.formatMessage(messages.severalPeople)}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Show no names in typing indicator
|
||||
const isSingleTyper = length === 1;
|
||||
const isMultiTypers = length > 1;
|
||||
|
||||
if (isSingleTyper) {
|
||||
element = (
|
||||
<span>
|
||||
{`${intl.formatMessage(messages.someoneTyping)}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMultiTypers) {
|
||||
element = (
|
||||
<span>
|
||||
{`${intl.formatMessage(messages.severalPeople)}`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
indicatorEnabled,
|
||||
} = this.props;
|
||||
|
||||
const typingElement = indicatorEnabled ? this.renderTypingElement() : null;
|
||||
|
||||
return (
|
||||
<Styled.TypingIndicatorWrapper
|
||||
error={!!error}
|
||||
info={!error}
|
||||
spacer={!!typingElement}
|
||||
>
|
||||
<Styled.TypingIndicator data-test="typingIndicator">{error || typingElement}</Styled.TypingIndicator>
|
||||
</Styled.TypingIndicatorWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TypingIndicator.propTypes = propTypes;
|
||||
|
||||
export default injectIntl(TypingIndicator);
|
@ -1,54 +0,0 @@
|
||||
import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
||||
import Users from '/imports/api/users';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import TypingIndicator from './component';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const USER_CONFIG = Meteor.settings.public.user;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
|
||||
const TYPING_SHOW_NAMES = CHAT_CONFIG.typingIndicator.showNames;
|
||||
|
||||
const TypingIndicatorContainer = props => <TypingIndicator {...props} />;
|
||||
|
||||
export default withTracker(({ idChatOpen }) => {
|
||||
const meeting = Meetings.findOne({ meetingId: Auth.meetingID }, {
|
||||
fields: {
|
||||
'lockSettingsProps.hideUserList': 1,
|
||||
},
|
||||
});
|
||||
|
||||
const selector = {
|
||||
meetingId: Auth.meetingID,
|
||||
isTypingTo: PUBLIC_CHAT_KEY,
|
||||
userId: { $ne: Auth.userID },
|
||||
};
|
||||
|
||||
if (idChatOpen !== PUBLIC_CHAT_KEY) {
|
||||
selector.isTypingTo = idChatOpen;
|
||||
}
|
||||
|
||||
const currentUser = Users.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
userId: Auth.userID,
|
||||
}, {
|
||||
fields: {
|
||||
role: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (meeting.lockSettingsProps.hideUserList && currentUser?.role === USER_CONFIG.role_viewer) {
|
||||
selector.role = { $ne: USER_CONFIG.role_viewer };
|
||||
}
|
||||
|
||||
const typingUsers = UsersTyping.find(selector).fetch();
|
||||
|
||||
return {
|
||||
typingUsers,
|
||||
indicatorEnabled: TYPING_INDICATOR_ENABLED,
|
||||
indicatorShowNames: TYPING_SHOW_NAMES,
|
||||
};
|
||||
})(TypingIndicatorContainer);
|
@ -1,73 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { colorDanger, colorGrayDark } from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import { borderSize } from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { fontSizeSmaller, fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
|
||||
const SingleTyper = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
font-size: ${fontSizeSmaller};
|
||||
max-width: 70%;
|
||||
`;
|
||||
|
||||
const CoupleTyper = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
font-size: ${fontSizeSmaller};
|
||||
max-width: 25%;
|
||||
`;
|
||||
|
||||
const TypingIndicator = styled.span`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
margin-right: 0.05rem;
|
||||
margin-left: 0.05rem;
|
||||
}
|
||||
|
||||
text-align: left;
|
||||
[dir="rtl"] & {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
const TypingIndicatorWrapper = styled.div`
|
||||
${({ error }) => error && `
|
||||
color: ${colorDanger};
|
||||
font-size: calc(${fontSizeBase} * .75);
|
||||
color: ${colorGrayDark};
|
||||
text-align: left;
|
||||
padding: ${borderSize} 0;
|
||||
position: relative;
|
||||
height: .93rem;
|
||||
max-height: .93rem;
|
||||
`}
|
||||
|
||||
${({ info }) => info && `
|
||||
font-size: calc(${fontSizeBase} * .75);
|
||||
color: ${colorGrayDark};
|
||||
text-align: left;
|
||||
padding: ${borderSize} 0;
|
||||
position: relative;
|
||||
height: .93rem;
|
||||
max-height: .93rem;
|
||||
`}
|
||||
|
||||
${({ spacer }) => spacer && `
|
||||
height: .93rem;
|
||||
max-height: .93rem;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
SingleTyper,
|
||||
CoupleTyper,
|
||||
TypingIndicator,
|
||||
TypingIndicatorWrapper,
|
||||
};
|
@ -1,6 +1,5 @@
|
||||
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';
|
||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
@ -70,9 +69,6 @@ const setUserSentMessage = (bool) => {
|
||||
|
||||
const getUser = (userId) => Users.findOne({ userId });
|
||||
|
||||
const getPrivateChatByUsers = (userId) => GroupChat
|
||||
.findOne({ users: { $all: [userId, Auth.userID] } });
|
||||
|
||||
const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
|
||||
{ fields: { welcomeProp: 1 } });
|
||||
|
||||
@ -183,53 +179,6 @@ const lastReadMessageTime = (receiverID) => {
|
||||
return UnreadMessages.get(chatType);
|
||||
};
|
||||
|
||||
const sendGroupMessage = (message, idChatOpen) => {
|
||||
const chatIdToSent = idChatOpen === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : idChatOpen;
|
||||
const chat = GroupChat.findOne({ chatId: chatIdToSent },
|
||||
{ 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,
|
||||
message,
|
||||
};
|
||||
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
||||
|
||||
// Remove the chat that user send messages from the session.
|
||||
if (isChatClosed(receiverId.id)) {
|
||||
const closedChats = currentClosedChats.filter(closedChat => closedChat.chatId !== receiverId.id);
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY,closedChats);
|
||||
}
|
||||
|
||||
return makeCall('sendGroupChatMsg', destinationChatId, payload);
|
||||
};
|
||||
|
||||
const getScrollPosition = (receiverID) => {
|
||||
const scroll = ScrollCollection.findOne({ receiver: receiverID },
|
||||
{ fields: { position: 1 } }) || { position: null };
|
||||
@ -361,7 +310,6 @@ export default {
|
||||
reduceAndMapGroupMessages,
|
||||
reduceAndDontMapGroupMessages,
|
||||
getUser,
|
||||
getPrivateChatByUsers,
|
||||
getWelcomeProp,
|
||||
getScrollPosition,
|
||||
lastReadMessageTime,
|
||||
@ -369,7 +317,6 @@ export default {
|
||||
isChatClosed,
|
||||
updateScrollPosition,
|
||||
updateUnreadMessage,
|
||||
sendGroupMessage,
|
||||
closePrivateChat,
|
||||
removeFromClosedChatsSession,
|
||||
exportChat,
|
||||
|
@ -1,386 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { debounce } from '/imports/utils/debounce';
|
||||
import { AutoSizer,CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||
import Styled from './styles';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import TimeWindowChatItem from './time-window-chat-item/container';
|
||||
import { convertRemToPixels } from '/imports/utils/dom-utils';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
|
||||
const propTypes = {
|
||||
scrollPosition: PropTypes.number,
|
||||
chatId: PropTypes.string.isRequired,
|
||||
handleScrollUpdate: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
scrollPosition: null,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
moreMessages: {
|
||||
id: 'app.chat.moreMessages',
|
||||
description: 'Chat message when the user has unread messages below the scroll',
|
||||
},
|
||||
emptyLogLabel: {
|
||||
id: 'app.chat.emptyLogLabel',
|
||||
description: 'aria-label used when chat log is empty',
|
||||
},
|
||||
});
|
||||
|
||||
const updateChatSemantics = () => {
|
||||
setTimeout(() => {
|
||||
const msgListItem = document.querySelector('span[data-test="msgListItem"]');
|
||||
if (msgListItem) {
|
||||
const virtualizedGridInnerScrollContainer = msgListItem.parentElement;
|
||||
const virtualizedGrid = virtualizedGridInnerScrollContainer.parentElement;
|
||||
virtualizedGridInnerScrollContainer.setAttribute('role', 'list');
|
||||
virtualizedGridInnerScrollContainer.setAttribute('tabIndex', 0);
|
||||
virtualizedGrid.removeAttribute('tabIndex');
|
||||
virtualizedGrid.removeAttribute('aria-label');
|
||||
virtualizedGrid.removeAttribute('aria-readonly');
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
class TimeWindowList extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
minHeight: 18,
|
||||
keyMapper: (rowIndex) => {
|
||||
const { timeWindowsValues } = this.props;
|
||||
const timewindow = timeWindowsValues[rowIndex];
|
||||
|
||||
const key = timewindow?.key;
|
||||
const contentCount = timewindow?.content?.length;
|
||||
return `${key}-${contentCount}`;
|
||||
},
|
||||
});
|
||||
this.userScrolledBack = false;
|
||||
this.handleScrollUpdate = debounce(this.handleScrollUpdate.bind(this), 150);
|
||||
this.rowRender = this.rowRender.bind(this);
|
||||
this.forceCacheUpdate = this.forceCacheUpdate.bind(this);
|
||||
this.systemMessagesResized = {};
|
||||
|
||||
this.state = {
|
||||
scrollArea: null,
|
||||
shouldScrollToPosition: false,
|
||||
scrollPosition: 0,
|
||||
userScrolledBack: false,
|
||||
lastMessage: {},
|
||||
fontsLoaded: false,
|
||||
};
|
||||
this.systemMessageIndexes = [];
|
||||
|
||||
this.listRef = null;
|
||||
this.virualRef = null;
|
||||
|
||||
this.lastWidth = 0;
|
||||
|
||||
document.fonts.onloadingdone = () => this.setState({fontsLoaded: true});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { scrollPosition: scrollProps } = this.props;
|
||||
|
||||
this.setState({
|
||||
scrollPosition: scrollProps,
|
||||
});
|
||||
|
||||
updateChatSemantics();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
ChatLogger.debug('TimeWindowList::componentDidUpdate', { ...this.props }, { ...prevProps });
|
||||
if (this.virualRef) {
|
||||
if (this.virualRef.style.direction !== document.documentElement.dir) {
|
||||
this.virualRef.style.direction = document.documentElement.dir;
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
userSentMessage,
|
||||
setUserSentMessage,
|
||||
timeWindowsValues,
|
||||
chatId,
|
||||
syncing,
|
||||
syncedPercent,
|
||||
lastTimeWindowValuesBuild,
|
||||
scrollPosition: scrollProps,
|
||||
count,
|
||||
} = this.props;
|
||||
|
||||
const { userScrolledBack } = this.state;
|
||||
|
||||
if((count > 0 && !userScrolledBack) || userSentMessage || !scrollProps) {
|
||||
const lastItemIndex = timeWindowsValues.length - 1;
|
||||
|
||||
this.setState({
|
||||
scrollPosition: lastItemIndex,
|
||||
}, ()=> this.handleScrollUpdate(lastItemIndex));
|
||||
}
|
||||
|
||||
const {
|
||||
timeWindowsValues: prevTimeWindowsValues,
|
||||
chatId: prevChatId,
|
||||
syncing: prevSyncing,
|
||||
syncedPercent: prevSyncedPercent
|
||||
} = prevProps;
|
||||
|
||||
if (prevChatId !== chatId) {
|
||||
this.setState({
|
||||
scrollPosition: scrollProps,
|
||||
});
|
||||
}
|
||||
|
||||
const prevTimeWindowsLength = prevTimeWindowsValues.length;
|
||||
const timeWindowsValuesLength = timeWindowsValues.length;
|
||||
const prevLastTimeWindow = prevTimeWindowsValues[prevTimeWindowsLength - 1];
|
||||
const lastTimeWindow = timeWindowsValues[prevTimeWindowsLength - 1];
|
||||
|
||||
if ((lastTimeWindow
|
||||
&& (prevLastTimeWindow?.content.length !== lastTimeWindow?.content.length))) {
|
||||
if (this.listRef) {
|
||||
this.cache.clear(timeWindowsValuesLength - 1);
|
||||
this.listRef.recomputeRowHeights(timeWindowsValuesLength - 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (userSentMessage && !prevProps.userSentMessage) {
|
||||
this.setState({
|
||||
userScrolledBack: false,
|
||||
}, () => setUserSentMessage(false));
|
||||
}
|
||||
|
||||
// this condition exist to the case where the chat has a single message and the chat is cleared
|
||||
// The component List from react-virtualized doesn't have a reference to the list of messages
|
||||
// so I need force the update to fix it
|
||||
if (
|
||||
(lastTimeWindow?.id === `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`)
|
||||
|| (prevSyncing && !syncing)
|
||||
|| (syncedPercent !== prevSyncedPercent)
|
||||
|| (chatId !== prevChatId)
|
||||
|| (lastTimeWindowValuesBuild !== prevProps.lastTimeWindowValuesBuild)
|
||||
) {
|
||||
this.listRef.forceUpdateGrid();
|
||||
}
|
||||
|
||||
updateChatSemantics();
|
||||
}
|
||||
|
||||
handleScrollUpdate(position, target) {
|
||||
const {
|
||||
handleScrollUpdate,
|
||||
} = this.props;
|
||||
|
||||
if (position !== null && position + target?.offsetHeight === target?.scrollHeight) {
|
||||
// I used one because the null value is used to notify that
|
||||
// the user has sent a message and the message list should scroll to bottom
|
||||
handleScrollUpdate(1);
|
||||
return;
|
||||
}
|
||||
|
||||
handleScrollUpdate(position || 1);
|
||||
}
|
||||
|
||||
scrollTo(position = null) {
|
||||
if (position) {
|
||||
setTimeout(() => this.setState({
|
||||
shouldScrollToPosition: true,
|
||||
scrollPosition: position,
|
||||
}), 200);
|
||||
}
|
||||
}
|
||||
|
||||
forceCacheUpdate(index) {
|
||||
if (index >= 0) {
|
||||
this.cache.clear(index);
|
||||
this.listRef.recomputeRowHeights(index);
|
||||
}
|
||||
}
|
||||
|
||||
rowRender({
|
||||
index,
|
||||
parent,
|
||||
style,
|
||||
key,
|
||||
}) {
|
||||
const {
|
||||
id,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
chatId,
|
||||
} = this.props;
|
||||
|
||||
const { scrollArea } = this.state;
|
||||
const message = timeWindowsValues[index];
|
||||
|
||||
ChatLogger.debug('TimeWindowList::rowRender', this.props);
|
||||
return (
|
||||
<CellMeasurer
|
||||
key={key}
|
||||
cache={this.cache}
|
||||
columnIndex={0}
|
||||
parent={parent}
|
||||
rowIndex={index}
|
||||
>
|
||||
<span
|
||||
style={style}
|
||||
key={`span-${key}-${index}`}
|
||||
role="listitem"
|
||||
data-test="msgListItem"
|
||||
>
|
||||
<TimeWindowChatItem
|
||||
key={key}
|
||||
message={message}
|
||||
messageId={message.id}
|
||||
chatAreaId={id}
|
||||
scrollArea={scrollArea}
|
||||
dispatch={dispatch}
|
||||
chatId={chatId}
|
||||
height={style.height}
|
||||
index={index}
|
||||
forceCacheUpdate={this.forceCacheUpdate}
|
||||
/>
|
||||
</span>
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
renderUnreadNotification() {
|
||||
const {
|
||||
intl,
|
||||
count,
|
||||
timeWindowsValues,
|
||||
} = this.props;
|
||||
const { userScrolledBack } = this.state;
|
||||
|
||||
if (count && userScrolledBack) {
|
||||
return (
|
||||
<Styled.UnreadButton
|
||||
aria-hidden="true"
|
||||
color="primary"
|
||||
size="sm"
|
||||
key="unread-messages"
|
||||
label={intl.formatMessage(intlMessages.moreMessages)}
|
||||
onClick={() => {
|
||||
const lastItemIndex = timeWindowsValues.length - 1;
|
||||
this.handleScrollUpdate(lastItemIndex);
|
||||
|
||||
this.setState({
|
||||
scrollPosition: lastItemIndex,
|
||||
userScrolledBack: false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
timeWindowsValues,
|
||||
width,
|
||||
} = this.props;
|
||||
const {
|
||||
scrollArea,
|
||||
scrollPosition,
|
||||
userScrolledBack,
|
||||
} = this.state;
|
||||
ChatLogger.debug('TimeWindowList::render', {...this.props}, {...this.state}, new Date());
|
||||
|
||||
const shouldAutoScroll = !!(
|
||||
scrollPosition
|
||||
&& timeWindowsValues.length >= scrollPosition
|
||||
&& !userScrolledBack
|
||||
);
|
||||
|
||||
const paddingValue = convertRemToPixels(2);
|
||||
|
||||
return (
|
||||
[
|
||||
<Styled.MessageListWrapper
|
||||
onMouseDown={() => {
|
||||
this.setState({
|
||||
userScrolledBack: true,
|
||||
});
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY < 0) {
|
||||
this.setState({
|
||||
userScrolledBack: true,
|
||||
});
|
||||
this.userScrolledBack = true
|
||||
}
|
||||
}}
|
||||
key="chat-list"
|
||||
data-test="chatMessages"
|
||||
ref={node => this.messageListWrapper = node}
|
||||
onCopy={(e) => { e.stopPropagation(); }}
|
||||
>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => {
|
||||
if (width !== this.lastWidth) {
|
||||
this.lastWidth = width;
|
||||
this.cache.clearAll();
|
||||
}
|
||||
return (
|
||||
<Styled.MessageList
|
||||
ref={(ref) => {
|
||||
if (ref !== null) {
|
||||
this.listRef = ref;
|
||||
|
||||
if (!scrollArea) {
|
||||
this.setState({ scrollArea: findDOMNode(this.listRef) });
|
||||
}
|
||||
}
|
||||
}}
|
||||
isScrolling
|
||||
rowHeight={this.cache.rowHeight}
|
||||
rowRenderer={this.rowRender}
|
||||
rowCount={timeWindowsValues.length}
|
||||
height={height}
|
||||
width={width - paddingValue}
|
||||
overscanRowCount={0}
|
||||
deferredMeasurementCache={this.cache}
|
||||
scrollToIndex={shouldAutoScroll ? scrollPosition : undefined}
|
||||
onRowsRendered={({ stopIndex }) => {
|
||||
this.handleScrollUpdate(stopIndex);
|
||||
}}
|
||||
onScroll={({ clientHeight, scrollHeight, scrollTop }) => {
|
||||
const scrollSize = scrollTop + clientHeight;
|
||||
if (scrollSize >= scrollHeight) {
|
||||
this.setState({
|
||||
userScrolledBack: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</Styled.MessageListWrapper>,
|
||||
this.renderUnreadNotification(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeWindowList.propTypes = propTypes;
|
||||
TimeWindowList.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(TimeWindowList);
|
@ -1,30 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import TimeWindowList from './component';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import ChatService from '../service';
|
||||
import ChatList from '../chat-graphql/chat-message-list/component';
|
||||
|
||||
class TimeWindowListContainer extends PureComponent {
|
||||
render() {
|
||||
const { chatId, userSentMessage } = this.props;
|
||||
const scrollPosition = ChatService.getScrollPosition(chatId);
|
||||
const lastReadMessageTime = ChatService.lastReadMessageTime(chatId);
|
||||
ChatLogger.debug('TimeWindowListContainer::render', { ...this.props }, new Date());
|
||||
return (
|
||||
<TimeWindowList
|
||||
{
|
||||
...{
|
||||
...this.props,
|
||||
scrollPosition,
|
||||
lastReadMessageTime,
|
||||
handleScrollUpdate: ChatService.updateScrollPosition,
|
||||
userSentMessage,
|
||||
setUserSentMessage: ChatService.setUserSentMessage,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ChatList;
|
@ -1,142 +0,0 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
smPaddingX,
|
||||
mdPaddingX,
|
||||
mdPaddingY,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { ButtonElipsis } from '/imports/ui/stylesheets/styled-components/placeholders';
|
||||
import { VirtualizedScrollboxVertical } from '/imports/ui/stylesheets/styled-components/scrollable';
|
||||
import MessageChatItem from '/imports/ui/components/chat/time-window-list/time-window-chat-item/message-chat-item/component';
|
||||
|
||||
const UnreadButton = styled(ButtonElipsis)`
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: .25rem;
|
||||
z-index: 3;
|
||||
`;
|
||||
|
||||
const MessageListWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-left: ${smPaddingX};
|
||||
margin-left: calc(-1 * ${mdPaddingX});
|
||||
padding-right: ${smPaddingX};
|
||||
margin-right: calc(-1 * ${mdPaddingY});
|
||||
padding-bottom: ${mdPaddingX};
|
||||
z-index: 2;
|
||||
[dir="rtl"] & {
|
||||
padding-right: ${mdPaddingX};
|
||||
margin-right: calc(-1 * ${mdPaddingX});
|
||||
padding-left: ${mdPaddingY};
|
||||
margin-left: calc(-1 * ${mdPaddingX});
|
||||
}
|
||||
`;
|
||||
|
||||
const MessageList = styled(VirtualizedScrollboxVertical)`
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 0 auto 0 0;
|
||||
right: 0 ${mdPaddingX} 0 0;
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
outline-style: none;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0 0 0 ${mdPaddingX};
|
||||
}
|
||||
`;
|
||||
|
||||
const Time = styled.time`
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
`;
|
||||
|
||||
const Meta = styled.div`
|
||||
`;
|
||||
|
||||
const Name = styled.div`
|
||||
`;
|
||||
|
||||
const Read = styled.div`
|
||||
`;
|
||||
|
||||
const Messages = styled.div`
|
||||
`;
|
||||
|
||||
const SystemMessageChatItem = styled(MessageChatItem)`
|
||||
${({ messageId }) => messageId ?
|
||||
css`
|
||||
`
|
||||
:
|
||||
css`
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
const Item = styled.div`
|
||||
`;
|
||||
|
||||
const OnlineIndicator = styled.div`
|
||||
${({ isOnline }) => isOnline ?
|
||||
css`
|
||||
color: red;
|
||||
`
|
||||
:
|
||||
css`
|
||||
color: blue;
|
||||
`
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatItem = styled.div`
|
||||
`;
|
||||
|
||||
const Offline = styled.div`
|
||||
`;
|
||||
|
||||
const PollIcon = styled.div`
|
||||
`;
|
||||
|
||||
const PollMessageChatItem = styled.div`
|
||||
`;
|
||||
|
||||
const StatusMessageChatItem = styled(MessageChatItem)`
|
||||
`;
|
||||
|
||||
export default {
|
||||
UnreadButton,
|
||||
MessageListWrapper,
|
||||
MessageList,
|
||||
Time,
|
||||
Content,
|
||||
Meta,
|
||||
Wrapper,
|
||||
AvatarWrapper,
|
||||
Name,
|
||||
Read,
|
||||
Messages,
|
||||
SystemMessageChatItem,
|
||||
Item,
|
||||
ChatItem,
|
||||
OnlineIndicator,
|
||||
Offline,
|
||||
PollIcon,
|
||||
PollMessageChatItem,
|
||||
StatusMessageChatItem,
|
||||
};
|
||||
|
@ -1,525 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
import Tooltip from '/imports/ui/components/common/tooltip/component';
|
||||
import Styled from './styles';
|
||||
import { uniqueId } from '/imports/utils/string-utils';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
const CHAT_POLL_RESULTS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_poll_result;
|
||||
const CHAT_USER_STATUS_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_status_message;
|
||||
const CHAT_PUBLIC_ID = CHAT_CONFIG.public_id;
|
||||
const CHAT_EMPHASIZE_TEXT = CHAT_CONFIG.moderatorChatEmphasized;
|
||||
const CHAT_EXPORTED_PRESENTATION_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_exported_presentation;
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.shape({
|
||||
color: PropTypes.string,
|
||||
messageFromModerator: PropTypes.bool,
|
||||
isOnline: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
messages: PropTypes.arrayOf(Object).isRequired,
|
||||
timestamp: PropTypes.number,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
scrollArea: PropTypes.instanceOf(Element),
|
||||
chatAreaId: PropTypes.string.isRequired,
|
||||
handleReadMessage: PropTypes.func.isRequired,
|
||||
lastReadMessageTime: PropTypes.number,
|
||||
lastReadByPartnerMessageTime: PropTypes.number,
|
||||
isMessageReadFeedbackEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
user: null,
|
||||
scrollArea: null,
|
||||
lastReadMessageTime: 0,
|
||||
lastReadByPartnerMessageTime: 0,
|
||||
isMessageReadFeedbackEnabled: false,
|
||||
timestamp: 0,
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
offline: {
|
||||
id: 'app.chat.offline',
|
||||
description: 'Offline',
|
||||
},
|
||||
pollResult: {
|
||||
id: 'app.chat.pollResult',
|
||||
description: 'used in place of user name who published poll to chat',
|
||||
},
|
||||
[CHAT_CLEAR_MESSAGE]: {
|
||||
id: 'app.chat.clearPublicChatMessage',
|
||||
description: 'message of when clear the public chat',
|
||||
},
|
||||
breakoutDurationUpdated: {
|
||||
id: 'app.chat.breakoutDurationUpdated',
|
||||
description: 'used when the breakout duration is updated',
|
||||
},
|
||||
away: {
|
||||
id: 'app.chat.away',
|
||||
description: 'away label',
|
||||
},
|
||||
notAway: {
|
||||
id: 'app.chat.notAway',
|
||||
description: 'not away label',
|
||||
},
|
||||
messageReadLabel: {
|
||||
id: 'app.chat.messageRead',
|
||||
description: 'Message read tooltip label',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
class TimeWindowChatItem extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
forcedUpdateCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillMount::props', { ...this.props });
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillMount::state', { ...this.state });
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { height, forceCacheUpdate, index } = this.props;
|
||||
const elementHeight = this.itemRef ? this.itemRef.clientHeight : null;
|
||||
|
||||
if (elementHeight && height !== 'auto' && elementHeight !== height && this.state.forcedUpdateCount < 10) {
|
||||
// forceCacheUpdate() internally calls forceUpdate(), so we need a stop flag
|
||||
// and cannot rely on shouldComponentUpdate() and other comparisons.
|
||||
forceCacheUpdate(index);
|
||||
this.setState(({ forcedUpdateCount }) => ({ forcedUpdateCount: forcedUpdateCount + 1 }));
|
||||
}
|
||||
|
||||
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::props', { ...this.props }, { ...prevProps });
|
||||
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::state', { ...this.state }, { ...prevState });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillUnmount::props', { ...this.props });
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillUnmount::state', { ...this.state });
|
||||
}
|
||||
|
||||
getText(message, messageValues) {
|
||||
const { intl } = this.props;
|
||||
|
||||
let { text } = message;
|
||||
|
||||
if (intlMessages[message.text]) {
|
||||
text = intl.formatMessage(
|
||||
intlMessages[message.text],
|
||||
messageValues || {},
|
||||
);
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
renderSystemMessage() {
|
||||
const {
|
||||
messages,
|
||||
messageValues,
|
||||
chatAreaId,
|
||||
handleReadMessage,
|
||||
messageKey,
|
||||
} = this.props;
|
||||
|
||||
if (messages && messages[0].id.includes(CHAT_POLL_RESULTS_MESSAGE)) {
|
||||
return this.renderPollItem();
|
||||
}
|
||||
|
||||
if (messages && messages[0].id.includes(CHAT_EXPORTED_PRESENTATION_MESSAGE)) {
|
||||
return this.renderExportedPresentationItem();
|
||||
}
|
||||
|
||||
if (messages && messages[0].id.includes(CHAT_USER_STATUS_MESSAGE)) {
|
||||
return this.renderStatusItem();
|
||||
}
|
||||
|
||||
return (
|
||||
<Styled.Item
|
||||
key={`time-window-chat-item-${messageKey}`}
|
||||
ref={(element) => this.itemRef = element}
|
||||
>
|
||||
<Styled.Messages>
|
||||
{messages.map((message) => (
|
||||
message.text !== ''
|
||||
? (
|
||||
<Styled.SystemMessageChatItem
|
||||
messageId={message.id}
|
||||
border={message.id}
|
||||
key={message.id ? message.id : uniqueId('id-')}
|
||||
text={this.getText(message, messageValues)}
|
||||
time={message.time}
|
||||
isSystemMessage={!!message.id}
|
||||
systemMessageType={message.text === CHAT_CLEAR_MESSAGE ? 'chatClearMessageText' : 'chatWelcomeMessageText'}
|
||||
chatAreaId={chatAreaId}
|
||||
handleReadMessage={handleReadMessage}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
</Styled.Messages>
|
||||
</Styled.Item>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessageItem() {
|
||||
const {
|
||||
timestamp,
|
||||
chatAreaId,
|
||||
scrollArea,
|
||||
intl,
|
||||
messages,
|
||||
messageKey,
|
||||
dispatch,
|
||||
chatId,
|
||||
read,
|
||||
isFromMe,
|
||||
lastReadByPartnerMessageTime,
|
||||
isMessageReadFeedbackEnabled,
|
||||
name,
|
||||
color,
|
||||
messageFromModerator,
|
||||
avatar,
|
||||
isOnline,
|
||||
isSystemSender,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(timestamp);
|
||||
const regEx = /<a[^>]+>/i;
|
||||
ChatLogger.debug('TimeWindowChatItem::renderMessageItem', this.props);
|
||||
const defaultAvatarString = name?.toLowerCase().slice(0, 2) || ' ';
|
||||
const shouldRenderPrivateMessageReadFeedback =
|
||||
isMessageReadFeedbackEnabled &&
|
||||
chatId !== CHAT_PUBLIC_ID &&
|
||||
isFromMe &&
|
||||
lastReadByPartnerMessageTime >= messages[messages.length - 1].time;
|
||||
const emphasizedText = messageFromModerator && CHAT_EMPHASIZE_TEXT && chatId === CHAT_PUBLIC_ID;
|
||||
|
||||
return (
|
||||
<Styled.Item
|
||||
key={`time-window-${messageKey}`}
|
||||
ref={element => this.itemRef = element}
|
||||
>
|
||||
<Styled.Wrapper isSystemSender={isSystemSender}>
|
||||
<Styled.AvatarWrapper>
|
||||
<UserAvatar
|
||||
color={color}
|
||||
moderator={messageFromModerator}
|
||||
avatar={avatar}
|
||||
>
|
||||
{defaultAvatarString}
|
||||
</UserAvatar>
|
||||
</Styled.AvatarWrapper>
|
||||
<Styled.Content>
|
||||
<Styled.Meta>
|
||||
<Styled.Name isOnline={isOnline}>
|
||||
<span>{name}</span>
|
||||
{isOnline
|
||||
? null
|
||||
: (
|
||||
<Styled.Offline>
|
||||
{`(${intl.formatMessage(intlMessages.offline)})`}
|
||||
</Styled.Offline>
|
||||
)}
|
||||
</Styled.Name>
|
||||
<Styled.Time dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</Styled.Time>
|
||||
{shouldRenderPrivateMessageReadFeedback
|
||||
&& (
|
||||
<Tooltip
|
||||
title={intl.formatMessage(intlMessages.messageReadLabel)}
|
||||
>
|
||||
<Styled.ReadIcon
|
||||
iconName="message_read"
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Styled.Meta>
|
||||
<Styled.Messages>
|
||||
{messages.map((message) => (
|
||||
<Styled.ChatItem
|
||||
hasLink={regEx.test(message.text)}
|
||||
emphasizedMessage={emphasizedText}
|
||||
key={message.id}
|
||||
text={message.text}
|
||||
time={message.time}
|
||||
chatAreaId={chatAreaId}
|
||||
dispatch={dispatch}
|
||||
read={message.read}
|
||||
chatUserMessageItem={true}
|
||||
handleReadMessage={(timestamp) => {
|
||||
if (!read) {
|
||||
dispatch({
|
||||
type: 'last_read_message_timestamp_changed',
|
||||
value: {
|
||||
chatId,
|
||||
timestamp,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
scrollArea={scrollArea}
|
||||
/>
|
||||
))}
|
||||
</Styled.Messages>
|
||||
</Styled.Content>
|
||||
</Styled.Wrapper>
|
||||
</Styled.Item>
|
||||
);
|
||||
}
|
||||
|
||||
renderStatusItem() {
|
||||
const {
|
||||
timestamp,
|
||||
color,
|
||||
intl,
|
||||
messages,
|
||||
scrollArea,
|
||||
chatAreaId,
|
||||
messageKey,
|
||||
dispatch,
|
||||
chatId,
|
||||
extra,
|
||||
read,
|
||||
name,
|
||||
messageFromModerator,
|
||||
avatar,
|
||||
isOnline,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(timestamp);
|
||||
ChatLogger.debug('TimeWindowChatItem::renderMessageItem', this.props);
|
||||
const defaultAvatarString = name?.toLowerCase().slice(0, 2) || ' ';
|
||||
const emphasizedTextClass = messageFromModerator && CHAT_EMPHASIZE_TEXT
|
||||
&& chatId === CHAT_PUBLIC_ID;
|
||||
|
||||
return messages ? (
|
||||
<Styled.Item key={`time-window-status-message${messageKey}`}>
|
||||
<Styled.Wrapper>
|
||||
<Styled.AvatarWrapper>
|
||||
<UserAvatar
|
||||
color={color}
|
||||
moderator={messageFromModerator}
|
||||
avatar={avatar}
|
||||
>
|
||||
{defaultAvatarString}
|
||||
</UserAvatar>
|
||||
</Styled.AvatarWrapper>
|
||||
<Styled.Content>
|
||||
<Styled.Meta>
|
||||
<Styled.Name isOnline={isOnline}>
|
||||
<span>{name}</span>
|
||||
{isOnline
|
||||
? null
|
||||
: (
|
||||
<Styled.Offline>
|
||||
{`(${intl.formatMessage(intlMessages.offline)})`}
|
||||
</Styled.Offline>
|
||||
)}
|
||||
</Styled.Name>
|
||||
<Styled.Time dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</Styled.Time>
|
||||
</Styled.Meta>
|
||||
<Styled.Messages>
|
||||
{messages.map((message) => {
|
||||
const isSystemMsg = message.id === `SYSTEM_MESSAGE-${CHAT_USER_STATUS_MESSAGE}`;
|
||||
return (
|
||||
<Styled.StatusMessageChatItem
|
||||
isSystemMsg={isSystemMsg}
|
||||
emphasizedMessage={emphasizedTextClass}
|
||||
key={message.id}
|
||||
text={isSystemMsg
|
||||
? extra.status === 'away'
|
||||
? `<span styles={{width: '100px', heigth: '100px'}} class='icon-bbb-clear_status'/></span> ${intl.formatMessage(intlMessages.away)}`
|
||||
: `<span class='icon-bbb-user'></span> ${intl.formatMessage(intlMessages.notAway)}`
|
||||
: message.text}
|
||||
time={message.time}
|
||||
chatAreaId={chatAreaId}
|
||||
dispatch={dispatch}
|
||||
read={message.read}
|
||||
chatUserMessageItem
|
||||
handleReadMessage={(time) => {
|
||||
if (!read) {
|
||||
dispatch({
|
||||
type: 'last_read_message_timestamp_changed',
|
||||
value: {
|
||||
chatId,
|
||||
timestamp: time,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
scrollArea={scrollArea}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Styled.Messages>
|
||||
</Styled.Content>
|
||||
</Styled.Wrapper>
|
||||
</Styled.Item>
|
||||
) : null;
|
||||
}
|
||||
|
||||
renderPollItem() {
|
||||
const {
|
||||
timestamp,
|
||||
color,
|
||||
intl,
|
||||
getPollResultString,
|
||||
messages,
|
||||
extra,
|
||||
scrollArea,
|
||||
chatAreaId,
|
||||
lastReadMessageTime,
|
||||
dispatch,
|
||||
chatId,
|
||||
read,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(timestamp);
|
||||
|
||||
return messages ? (
|
||||
<Styled.Item key={uniqueId('message-poll-item-')}>
|
||||
<Styled.Wrapper ref={(ref) => { this.item = ref; }}>
|
||||
<Styled.AvatarWrapper>
|
||||
<UserAvatar
|
||||
color={PollService.POLL_AVATAR_COLOR}
|
||||
moderator={true}
|
||||
>
|
||||
{<Styled.PollIcon iconName="polling" />}
|
||||
</UserAvatar>
|
||||
</Styled.AvatarWrapper>
|
||||
<Styled.Content>
|
||||
<Styled.Meta>
|
||||
<Styled.Name>
|
||||
<span>{intl.formatMessage(intlMessages.pollResult)}</span>
|
||||
</Styled.Name>
|
||||
<Styled.Time dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</Styled.Time>
|
||||
</Styled.Meta>
|
||||
<Styled.PollMessageChatItem
|
||||
type="poll"
|
||||
key={messages[0].id}
|
||||
text={getPollResultString(extra.pollResultData, intl)}
|
||||
time={messages[0].time}
|
||||
chatAreaId={chatAreaId}
|
||||
lastReadMessageTime={lastReadMessageTime}
|
||||
handleReadMessage={(time) => {
|
||||
if (!read) {
|
||||
dispatch({
|
||||
type: 'last_read_message_timestamp_changed',
|
||||
value: {
|
||||
chatId,
|
||||
timestamp: time,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
scrollArea={scrollArea}
|
||||
color={color}
|
||||
/>
|
||||
</Styled.Content>
|
||||
</Styled.Wrapper>
|
||||
</Styled.Item>
|
||||
) : null;
|
||||
}
|
||||
|
||||
renderExportedPresentationItem() {
|
||||
const {
|
||||
timestamp,
|
||||
color,
|
||||
intl,
|
||||
messages,
|
||||
extra,
|
||||
scrollArea,
|
||||
chatAreaId,
|
||||
lastReadMessageTime,
|
||||
handleReadMessage,
|
||||
dispatch,
|
||||
read,
|
||||
chatId,
|
||||
getExportedPresentationString,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(timestamp);
|
||||
|
||||
return messages ? (
|
||||
<Styled.Item
|
||||
key={uniqueId('message-presentation-item-')}
|
||||
onMouseDown={(e) => { e.stopPropagation(); }}
|
||||
>
|
||||
<Styled.PresentationWrapper ref={(ref) => { this.item = ref; }}>
|
||||
<Styled.AvatarWrapper>
|
||||
<UserAvatar color="#0F70D7">
|
||||
<Styled.PollIcon iconName="download" />
|
||||
</UserAvatar>
|
||||
</Styled.AvatarWrapper>
|
||||
<Styled.Content
|
||||
data-test="downloadPresentationContainer">
|
||||
<Styled.Meta>
|
||||
<Styled.Time dateTime={dateTime} style={{ margin: 0 }}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</Styled.Time>
|
||||
</Styled.Meta>
|
||||
<Styled.PresentationChatItem
|
||||
type="presentation"
|
||||
key={messages[0].id}
|
||||
text={getExportedPresentationString(extra.fileURI,
|
||||
extra.filename, intl, extra.fileStateType)}
|
||||
time={messages[0].time}
|
||||
chatAreaId={chatAreaId}
|
||||
lastReadMessageTime={lastReadMessageTime}
|
||||
handleReadMessage={(timestamp) => {
|
||||
handleReadMessage(timestamp);
|
||||
|
||||
if (!read) {
|
||||
dispatch({
|
||||
type: 'last_read_message_timestamp_changed',
|
||||
value: {
|
||||
chatId,
|
||||
timestamp,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
scrollArea={scrollArea}
|
||||
color={color}
|
||||
/>
|
||||
</Styled.Content>
|
||||
</Styled.PresentationWrapper>
|
||||
</Styled.Item>
|
||||
) : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
systemMessage,
|
||||
} = this.props;
|
||||
ChatLogger.debug('TimeWindowChatItem::render', { ...this.props });
|
||||
if (systemMessage) {
|
||||
return this.renderSystemMessage();
|
||||
}
|
||||
|
||||
return this.renderMessageItem();
|
||||
}
|
||||
}
|
||||
|
||||
TimeWindowChatItem.propTypes = propTypes;
|
||||
TimeWindowChatItem.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(TimeWindowChatItem);
|
@ -1,69 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TimeWindowChatItem from './component';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import ChatService from '../../service';
|
||||
import { layoutSelect } from '../../../layout/context';
|
||||
import PollService from '/imports/ui/components/poll/service';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
const TimeWindowChatItemContainer = (props) => {
|
||||
const { message, messageId } = props;
|
||||
|
||||
const idChatOpen = layoutSelect((i) => i.idChatOpen);
|
||||
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
const {
|
||||
sender,
|
||||
senderName,
|
||||
senderRole,
|
||||
key,
|
||||
timestamp,
|
||||
content,
|
||||
extra,
|
||||
messageValues,
|
||||
} = message;
|
||||
const messages = content;
|
||||
|
||||
const user = (sender === 'SYSTEM') ? {
|
||||
name: senderName,
|
||||
color: '#01579b',
|
||||
avatar: '',
|
||||
role: ROLE_MODERATOR,
|
||||
loggedOut: false,
|
||||
} : users[Auth.meetingID][sender];
|
||||
const messageKey = key;
|
||||
const handleReadMessage = (tstamp) => ChatService.updateUnreadMessage(tstamp, idChatOpen);
|
||||
return (
|
||||
<TimeWindowChatItem
|
||||
{
|
||||
...{
|
||||
color: user?.color,
|
||||
messageFromModerator: senderRole === ROLE_MODERATOR,
|
||||
isSystemSender: sender === 'SYSTEM',
|
||||
isOnline: !user?.loggedOut,
|
||||
avatar: user?.avatar,
|
||||
name: user?.name,
|
||||
read: message.read,
|
||||
messages,
|
||||
extra,
|
||||
messageValues,
|
||||
getPollResultString: PollService.getPollResultString,
|
||||
user,
|
||||
timestamp,
|
||||
systemMessage: messageId.startsWith(SYSTEM_CHAT_TYPE) || !sender,
|
||||
messageKey,
|
||||
handleReadMessage,
|
||||
getExportedPresentationString: ChatService.getExportedPresentationString,
|
||||
...props,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TimeWindowChatItemContainer;
|
@ -1,189 +0,0 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { debounce } from '/imports/utils/debounce';
|
||||
import fastdom from 'fastdom';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
const propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
time: PropTypes.number.isRequired,
|
||||
lastReadMessageTime: PropTypes.number,
|
||||
handleReadMessage: PropTypes.func.isRequired,
|
||||
scrollArea: PropTypes.instanceOf(Element),
|
||||
className: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
lastReadMessageTime: 0,
|
||||
scrollArea: undefined,
|
||||
};
|
||||
|
||||
const eventsToBeBound = [
|
||||
'scroll',
|
||||
'resize',
|
||||
];
|
||||
|
||||
const isElementInViewport = (el) => {
|
||||
if (!el) return false;
|
||||
const rect = el.getBoundingClientRect();
|
||||
|
||||
return (
|
||||
rect.top >= 0
|
||||
// This condition is for large messages that are bigger than client height
|
||||
|| rect.top + rect.height >= 0
|
||||
);
|
||||
};
|
||||
|
||||
class MessageChatItem extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.ticking = false;
|
||||
|
||||
this.handleMessageInViewport = debounce(this.handleMessageInViewport.bind(this), 50);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.listenToUnreadMessages();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
ChatLogger.debug('MessageChatItem::componentDidUpdate::props', { ...this.props }, { ...prevProps });
|
||||
ChatLogger.debug('MessageChatItem::componentDidUpdate::state', { ...this.state }, { ...prevState });
|
||||
this.listenToUnreadMessages();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This was added 3 years ago, but never worked. Leaving it around in case someone returns
|
||||
// and decides it needs to be fixed like the one in listenToUnreadMessages()
|
||||
// if (!lastReadMessageTime > time) {
|
||||
// return;
|
||||
// }
|
||||
ChatLogger.debug('MessageChatItem::componentWillUnmount', this.props);
|
||||
this.removeScrollListeners();
|
||||
}
|
||||
|
||||
addScrollListeners() {
|
||||
const {
|
||||
scrollArea,
|
||||
} = this.props;
|
||||
|
||||
if (scrollArea) {
|
||||
eventsToBeBound.forEach(
|
||||
e => scrollArea.addEventListener(e, this.handleMessageInViewport),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessageInViewport() {
|
||||
if (!this.ticking) {
|
||||
fastdom.measure(() => {
|
||||
const node = this.text;
|
||||
const {
|
||||
handleReadMessage,
|
||||
time,
|
||||
read,
|
||||
} = this.props;
|
||||
|
||||
if (read) {
|
||||
this.removeScrollListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isElementInViewport(node)) {
|
||||
handleReadMessage(time);
|
||||
this.removeScrollListeners();
|
||||
}
|
||||
|
||||
this.ticking = false;
|
||||
});
|
||||
}
|
||||
|
||||
this.ticking = true;
|
||||
}
|
||||
|
||||
removeScrollListeners() {
|
||||
const {
|
||||
scrollArea,
|
||||
read,
|
||||
} = this.props;
|
||||
|
||||
if (scrollArea && !read) {
|
||||
eventsToBeBound.forEach(
|
||||
e => scrollArea.removeEventListener(e, this.handleMessageInViewport),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// depending on whether the message is in viewport or not,
|
||||
// either read it or attach a listener
|
||||
listenToUnreadMessages() {
|
||||
const {
|
||||
handleReadMessage,
|
||||
time,
|
||||
read,
|
||||
} = this.props;
|
||||
|
||||
if (read) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = this.text;
|
||||
|
||||
fastdom.measure(() => {
|
||||
const {
|
||||
read: newRead,
|
||||
} = this.props;
|
||||
// this function is called after so we need to get the updated lastReadMessageTime
|
||||
|
||||
if (newRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isElementInViewport(node)) { // no need to listen, the message is already in viewport
|
||||
handleReadMessage(time);
|
||||
} else {
|
||||
this.addScrollListeners();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
text,
|
||||
type,
|
||||
className,
|
||||
isSystemMessage,
|
||||
chatUserMessageItem,
|
||||
systemMessageType,
|
||||
color,
|
||||
} = this.props;
|
||||
ChatLogger.debug('MessageChatItem::render', this.props);
|
||||
if (type === 'poll') {
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
style={{ borderLeft: `3px ${color} solid`, whiteSpace: 'pre-wrap' }}
|
||||
ref={(ref) => { this.text = ref; }}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
data-test="chatPollMessageText"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
ref={(ref) => { this.text = ref; }}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
data-test={isSystemMessage ? systemMessageType : chatUserMessageItem ? 'chatUserMessageText' : ''}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageChatItem.propTypes = propTypes;
|
||||
MessageChatItem.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(MessageChatItem);
|
@ -1,285 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
borderRadius,
|
||||
borderSize,
|
||||
chatPollMarginSm,
|
||||
} from '/imports/ui/stylesheets/styled-components/general';
|
||||
import { lineHeightComputed, fontSizeBase, btnFontWeight } from '/imports/ui/stylesheets/styled-components/typography';
|
||||
import {
|
||||
systemMessageBackgroundColor,
|
||||
systemMessageBorderColor,
|
||||
systemMessageFontColor,
|
||||
highlightedMessageBackgroundColor,
|
||||
highlightedMessageBorderColor,
|
||||
colorHeading,
|
||||
colorGrayLight,
|
||||
palettePlaceholderText,
|
||||
colorGrayLighter,
|
||||
colorPrimary,
|
||||
colorText,
|
||||
colorSuccess,
|
||||
} from '/imports/ui/stylesheets/styled-components/palette';
|
||||
import MessageChatItem from './message-chat-item/component';
|
||||
import Icon from '/imports/ui/components/common/icon/component';
|
||||
|
||||
const Item = styled.div`
|
||||
padding: calc(${lineHeightComputed} / 4) 0 calc(${lineHeightComputed} / 2) 0;
|
||||
font-size: ${fontSizeBase};
|
||||
pointer-events: auto;
|
||||
[dir="rtl"] & {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
|
||||
const Messages = styled.div`
|
||||
> * {
|
||||
&:first-child {
|
||||
margin-top: calc(${lineHeightComputed} / 4);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StatusMessageChatItem = styled(MessageChatItem)`
|
||||
background: ${systemMessageBackgroundColor};
|
||||
border: 1px solid ${systemMessageBorderColor};
|
||||
border-radius: ${borderRadius};
|
||||
font-weight: ${btnFontWeight};
|
||||
padding: ${fontSizeBase};
|
||||
color: ${systemMessageFontColor};
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
overflow-wrap: break-word;
|
||||
${({ isSystemMsg }) => !isSystemMsg && `
|
||||
flex: 1;
|
||||
margin-top: calc(${lineHeightComputed} / 3);
|
||||
margin-bottom: 0;
|
||||
color: ${colorText};
|
||||
word-wrap: break-word;
|
||||
background: unset;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
font-weight: unset;
|
||||
padding: unset;
|
||||
`}
|
||||
${({ emphasizedMessage }) => emphasizedMessage && `
|
||||
font-weight: bold;
|
||||
`}
|
||||
`;
|
||||
|
||||
const SystemMessageChatItem = styled(MessageChatItem)`
|
||||
${({ border }) => border && `
|
||||
background-color: ${systemMessageBackgroundColor};
|
||||
border: 1px solid ${systemMessageBorderColor};
|
||||
border-radius: ${borderRadius};
|
||||
font-weight: ${btnFontWeight};
|
||||
padding: ${fontSizeBase};
|
||||
color: ${systemMessageFontColor};
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
overflow-wrap: break-word;
|
||||
`}
|
||||
|
||||
${({ border }) => !border && `
|
||||
color: ${systemMessageFontColor};
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin: ${borderSize} 0 0 ${borderSize};
|
||||
|
||||
${({ isSystemSender }) => isSystemSender && `
|
||||
background-color: ${highlightedMessageBackgroundColor};
|
||||
border-left: 2px solid ${highlightedMessageBorderColor};
|
||||
border-radius: 0px 3px 3px 0px;
|
||||
padding: 8px 2px;
|
||||
`}
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: ${borderSize} ${borderSize} 0 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
flex-basis: 2.25rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin: 0 calc(${lineHeightComputed} / 2) 0 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 calc(${lineHeightComputed} / 2);
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow-x: hidden;
|
||||
width: calc(100% - 1.7rem);
|
||||
`;
|
||||
|
||||
const Meta = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row;
|
||||
line-height: 1.35;
|
||||
align-items: baseline;
|
||||
`;
|
||||
|
||||
const Name = styled.div`
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
min-width: 0;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
${({ isOnline }) => isOnline && `
|
||||
color: ${colorHeading};
|
||||
`}
|
||||
|
||||
${({ isOnline }) => !isOnline && `
|
||||
text-transform: capitalize;
|
||||
font-style: italic;
|
||||
|
||||
& > span {
|
||||
text-align: right;
|
||||
padding: 0 .1rem 0 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
text-align: left;
|
||||
padding: 0 0 0 .1rem;
|
||||
}
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Offline = styled.span`
|
||||
color: ${colorGrayLight};
|
||||
font-weight: 100;
|
||||
text-transform: lowercase;
|
||||
font-style: italic;
|
||||
font-size: 90%;
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const Time = styled.time`
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: 3.5rem;
|
||||
color: ${palettePlaceholderText};
|
||||
text-transform: uppercase;
|
||||
font-size: 75%;
|
||||
margin: 0 0 0 calc(${lineHeightComputed} / 2);
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 calc(${lineHeightComputed} / 2) 0 0;
|
||||
}
|
||||
|
||||
& > span {
|
||||
vertical-align: sub;
|
||||
}
|
||||
`;
|
||||
|
||||
const ChatItem = styled(MessageChatItem)`
|
||||
flex: 1;
|
||||
margin-top: calc(${lineHeightComputed} / 3);
|
||||
margin-bottom: 0;
|
||||
color: ${colorText};
|
||||
word-wrap: break-word;
|
||||
|
||||
${({ hasLink }) => hasLink && `
|
||||
& > a {
|
||||
color: ${colorPrimary};
|
||||
}
|
||||
`}
|
||||
|
||||
${({ emphasizedMessage }) => emphasizedMessage && `
|
||||
font-weight: bold;
|
||||
`}
|
||||
`;
|
||||
|
||||
const PollIcon = styled(Icon)`
|
||||
bottom: 1px;
|
||||
`;
|
||||
|
||||
const PollMessageChatItem = styled(MessageChatItem)`
|
||||
flex: 1;
|
||||
margin-top: calc(${lineHeightComputed} / 3);
|
||||
margin-bottom: 0;
|
||||
color: ${colorText};
|
||||
word-wrap: break-word;
|
||||
|
||||
background-color: ${systemMessageBackgroundColor};
|
||||
border: solid 1px ${colorGrayLighter};
|
||||
border-radius: ${borderRadius};
|
||||
padding: ${chatPollMarginSm};
|
||||
padding-left: 1rem;
|
||||
margin-top: ${chatPollMarginSm} !important;
|
||||
`;
|
||||
|
||||
const PresentationWrapper = styled(Wrapper)`
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin: ${borderSize} 0 0 ${borderSize};
|
||||
border-left: 2px solid ${colorPrimary};
|
||||
border-radius: 2px;
|
||||
padding: 6px 0 6px 6px;
|
||||
background-color: ${systemMessageBackgroundColor};
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: ${borderSize} ${borderSize} 0 0;
|
||||
border-right: 2px solid ${colorPrimary};
|
||||
border-left: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const PresentationChatItem = styled(MessageChatItem)`
|
||||
flex: 1;
|
||||
margin-top: ${chatPollMarginSm};
|
||||
margin-bottom: 0;
|
||||
color: ${colorText};
|
||||
word-wrap: break-word;
|
||||
`;
|
||||
|
||||
const ReadIcon = styled(Icon)`
|
||||
color: ${colorSuccess};
|
||||
align-self: center;
|
||||
margin-left: auto;
|
||||
margin-right: .5rem;
|
||||
`;
|
||||
|
||||
export default {
|
||||
Item,
|
||||
Messages,
|
||||
SystemMessageChatItem,
|
||||
Wrapper,
|
||||
AvatarWrapper,
|
||||
Content,
|
||||
Meta,
|
||||
Name,
|
||||
Offline,
|
||||
Time,
|
||||
ChatItem,
|
||||
PollIcon,
|
||||
PollMessageChatItem,
|
||||
PresentationChatItem,
|
||||
PresentationWrapper,
|
||||
StatusMessageChatItem,
|
||||
ReadIcon,
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import { GroupChatContext, ACTIONS } from './context';
|
||||
|
||||
const Adapter = () => {
|
||||
const usingGroupChatContext = useContext(GroupChatContext);
|
||||
const { dispatch } = usingGroupChatContext;
|
||||
|
||||
useEffect(() => {
|
||||
const groupChatCursor = GroupChat.find({});
|
||||
|
||||
groupChatCursor.observe({
|
||||
added: (obj) => {
|
||||
dispatch({
|
||||
type: ACTIONS.ADDED,
|
||||
value: {
|
||||
groupChat: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default Adapter;
|
@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Resizable from 're-resizable';
|
||||
import { ACTIONS, PANELS } from '../layout/enums';
|
||||
import ChatContainer from '/imports/ui/components/chat/container';
|
||||
import ChatContainer from '/imports/ui/components/chat/chat-graphql/component';
|
||||
import NotesContainer from '/imports/ui/components/notes/container';
|
||||
import PollContainer from '/imports/ui/components/poll/container';
|
||||
import CaptionsContainer from '/imports/ui/components/captions/container';
|
||||
|
@ -2,7 +2,6 @@ import { Component } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import logger from '/imports/startup/client/logger';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Users from '/imports/api/users';
|
||||
import { localCollectionRegistry } from '/client/collection-mirror-initializer';
|
||||
import SubscriptionRegistry, {
|
||||
@ -24,7 +23,6 @@ const SUBSCRIPTIONS = [
|
||||
'users-infos',
|
||||
'meeting-time-remaining',
|
||||
'local-settings',
|
||||
'users-typing',
|
||||
'record-meetings',
|
||||
'video-streams',
|
||||
'connection-status',
|
||||
@ -40,7 +38,6 @@ const SUBSCRIPTIONS = [
|
||||
'layout-meetings',
|
||||
'user-reaction',
|
||||
'timer',
|
||||
// 'group-chat'
|
||||
];
|
||||
const {
|
||||
localBreakoutsSync,
|
||||
@ -96,6 +93,7 @@ export default withTracker(() => {
|
||||
},
|
||||
'Error while subscribing to collections'
|
||||
);
|
||||
console.log('-------------------------', {error});
|
||||
Session.set('codeError', error.error);
|
||||
},
|
||||
};
|
||||
@ -151,15 +149,6 @@ export default withTracker(() => {
|
||||
|
||||
subscriptionsHandlers = subscriptionsHandlers.filter((obj) => obj);
|
||||
const ready = subscriptionsHandlers.every((handler) => handler.ready());
|
||||
let groupChatMessageHandler = {};
|
||||
|
||||
// if (isChatEnabled() && ready) {
|
||||
// const subHandler = {
|
||||
// ...subscriptionErrorHandler,
|
||||
// };
|
||||
|
||||
// groupChatMessageHandler = Meteor.subscribe('group-chat-msg', subHandler);
|
||||
// }
|
||||
|
||||
// TODO: Refactor all the late subscribers
|
||||
let usersPersistentDataHandler = {};
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import Users from '/imports/api/users';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import Breakouts from '/imports/api/breakouts';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import UserReaction from '/imports/api/user-reaction';
|
||||
@ -620,29 +619,6 @@ const roving = (...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasPrivateChatBetweenUsers = (senderId, receiverId) => GroupChat
|
||||
.findOne({ users: { $all: [receiverId, senderId] } });
|
||||
|
||||
const getGroupChatPrivate = (senderUserId, receiver) => {
|
||||
const chat = hasPrivateChatBetweenUsers(senderUserId, receiver.userId);
|
||||
if (!chat) {
|
||||
makeCall('createGroupChat', receiver);
|
||||
} else {
|
||||
const startedChats = Session.get(STARTED_CHAT_LIST_KEY) || [];
|
||||
if (indexOf(startedChats, chat.chatId) < 0) {
|
||||
startedChats.push(chat.chatId);
|
||||
Session.set(STARTED_CHAT_LIST_KEY, startedChats);
|
||||
}
|
||||
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
||||
|
||||
if (ChatService.isChatClosed(chat.chatId)) {
|
||||
const closedChats = currentClosedChats.filter(closedChat => closedChat.chatId !== chat.chatId);
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY,closedChats);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleUserLock = (userId, lockStatus) => {
|
||||
makeCall('toggleUserLock', userId, lockStatus);
|
||||
};
|
||||
@ -788,11 +764,9 @@ export default {
|
||||
isPublicChat,
|
||||
roving,
|
||||
getCustomLogoUrl,
|
||||
getGroupChatPrivate,
|
||||
hasBreakoutRoom,
|
||||
getEmojiList: () => EMOJI_STATUSES,
|
||||
getEmoji,
|
||||
hasPrivateChatBetweenUsers,
|
||||
toggleUserLock,
|
||||
requestUserInformation,
|
||||
focusFirstDropDownItem,
|
||||
|
Loading…
Reference in New Issue
Block a user