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 VideoStreams from '/imports/api/video-streams';
|
||||||
import VoiceUsers from '/imports/api/voice-users';
|
import VoiceUsers from '/imports/api/voice-users';
|
||||||
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
|
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
|
||||||
import GroupChat from '/imports/api/group-chat';
|
|
||||||
import ConnectionStatus from '/imports/api/connection-status';
|
import ConnectionStatus from '/imports/api/connection-status';
|
||||||
import Captions from '/imports/api/captions';
|
import Captions from '/imports/api/captions';
|
||||||
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
|
import Pads, { PadsSessions, PadsUpdates } from '/imports/api/pads';
|
||||||
@ -21,7 +20,6 @@ import guestUsers from '/imports/api/guest-users';
|
|||||||
import Meetings, {
|
import Meetings, {
|
||||||
RecordMeetings, ExternalVideoMeetings, MeetingTimeRemaining, Notifications,
|
RecordMeetings, ExternalVideoMeetings, MeetingTimeRemaining, Notifications,
|
||||||
} from '/imports/api/meetings';
|
} from '/imports/api/meetings';
|
||||||
import { UsersTyping } from '/imports/api/group-chat-msg';
|
|
||||||
import Users, { CurrentUser } from '/imports/api/users';
|
import Users, { CurrentUser } from '/imports/api/users';
|
||||||
|
|
||||||
// Custom Publishers
|
// Custom Publishers
|
||||||
@ -40,7 +38,6 @@ export const localCollectionRegistry = {
|
|||||||
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
localVideoStreamsSync: new AbstractCollection(VideoStreams, VideoStreams),
|
||||||
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
localVoiceUsersSync: new AbstractCollection(VoiceUsers, VoiceUsers),
|
||||||
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
localWhiteboardMultiUserSync: new AbstractCollection(WhiteboardMultiUser, WhiteboardMultiUser),
|
||||||
localGroupChatSync: new AbstractCollection(GroupChat, GroupChat),
|
|
||||||
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
|
localConnectionStatusSync: new AbstractCollection(ConnectionStatus, ConnectionStatus),
|
||||||
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
localCaptionsSync: new AbstractCollection(Captions, Captions),
|
||||||
localPadsSync: new AbstractCollection(Pads, Pads),
|
localPadsSync: new AbstractCollection(Pads, Pads),
|
||||||
@ -53,7 +50,6 @@ export const localCollectionRegistry = {
|
|||||||
ExternalVideoMeetings,
|
ExternalVideoMeetings,
|
||||||
),
|
),
|
||||||
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
|
localMeetingTimeRemainingSync: new AbstractCollection(MeetingTimeRemaining, MeetingTimeRemaining),
|
||||||
localUsersTypingSync: new AbstractCollection(UsersTyping, UsersTyping),
|
|
||||||
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
|
localBreakoutsSync: new AbstractCollection(Breakouts, Breakouts),
|
||||||
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
|
localBreakoutsHistorySync: new AbstractCollection(BreakoutsHistory, BreakoutsHistory),
|
||||||
localGuestUsersSync: new AbstractCollection(guestUsers, guestUsers),
|
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 ContextProviders from '/imports/ui/components/context-providers/component';
|
||||||
import ChatAdapter from '/imports/ui/components/components-data/chat-context/adapter';
|
import ChatAdapter from '/imports/ui/components/components-data/chat-context/adapter';
|
||||||
import UsersAdapter from '/imports/ui/components/components-data/users-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 GraphqlProvider from '/imports/ui/components/graphql-provider/component';
|
||||||
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
|
import { liveDataEventBrokerInitializer } from '/imports/ui/services/LiveDataEventBroker/LiveDataEventBroker';
|
||||||
// The adapter import is "unused" as far as static code is concerned, but it
|
// The adapter import is "unused" as far as static code is concerned, but it
|
||||||
@ -95,7 +94,6 @@ Meteor.startup(() => {
|
|||||||
</JoinHandler>
|
</JoinHandler>
|
||||||
<UsersAdapter />
|
<UsersAdapter />
|
||||||
<ChatAdapter />
|
<ChatAdapter />
|
||||||
<GroupChatAdapter />
|
|
||||||
</>
|
</>
|
||||||
</ContextProviders>,
|
</ContextProviders>,
|
||||||
document.getElementById('app'),
|
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 './methods';
|
||||||
import './publishers';
|
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
import { Meteor } from 'meteor/meteor';
|
import { Meteor } from 'meteor/meteor';
|
||||||
import clearPublicChatHistory from './methods/clearPublicChatHistory';
|
import clearPublicChatHistory from './methods/clearPublicChatHistory';
|
||||||
import startUserTyping from './methods/startUserTyping';
|
import startUserTyping from './methods/startUserTyping';
|
||||||
import stopUserTyping from './methods/stopUserTyping';
|
|
||||||
import chatMessageBeforeJoinCounter from './methods/chatMessageBeforeJoinCounter';
|
|
||||||
import fetchMessagePerPage from './methods/fetchMessagePerPage';
|
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
fetchMessagePerPage,
|
|
||||||
chatMessageBeforeJoinCounter,
|
|
||||||
clearPublicChatHistory,
|
clearPublicChatHistory,
|
||||||
startUserTyping,
|
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 = {
|
const CHAT_ACCESS = {
|
||||||
PUBLIC: 'PUBLIC_ACCESS',
|
PUBLIC: 'PUBLIC_ACCESS',
|
||||||
PRIVATE: 'PRIVATE_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 '/imports/api/group-chat-msg/server';
|
||||||
import './eventHandlers';
|
|
||||||
import './methods';
|
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 { 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 Logger from '/imports/startup/server/logger';
|
||||||
import addSystemMsg from '/imports/api/group-chat-msg/server/modifiers/addSystemMsg';
|
|
||||||
|
|
||||||
export default async function handleTimeRemainingUpdate({ body }, meetingId) {
|
export default async function handleTimeRemainingUpdate({ body }, meetingId) {
|
||||||
check(meetingId, String);
|
check(meetingId, String);
|
||||||
|
|
||||||
check(body, {
|
check(body, {
|
||||||
timeLeftInSec: Number,
|
timeLeftInSec: Number,
|
||||||
timeUpdatedInMinutes: Number,
|
|
||||||
});
|
});
|
||||||
const { timeLeftInSec, timeUpdatedInMinutes } = body;
|
const { timeLeftInSec } = body;
|
||||||
|
|
||||||
const selector = {
|
const selector = {
|
||||||
meetingId,
|
meetingId,
|
||||||
@ -27,34 +25,4 @@ export default async function handleTimeRemainingUpdate({ body }, meetingId) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(`Changing recording time: ${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 clearUsers from '/imports/api/users/server/modifiers/clearUsers';
|
||||||
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
|
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 clearGuestUsers from '/imports/api/guest-users/server/modifiers/clearGuestUsers';
|
||||||
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
|
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
|
||||||
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
|
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
|
||||||
@ -39,7 +38,6 @@ export default async function meetingHasEnded(meetingId) {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
clearCaptions(meetingId),
|
clearCaptions(meetingId),
|
||||||
clearPads(meetingId),
|
clearPads(meetingId),
|
||||||
clearGroupChat(meetingId),
|
|
||||||
clearGuestUsers(meetingId),
|
clearGuestUsers(meetingId),
|
||||||
clearBreakouts(meetingId),
|
clearBreakouts(meetingId),
|
||||||
clearPolls(meetingId),
|
clearPolls(meetingId),
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { check } from 'meteor/check';
|
import { check } from 'meteor/check';
|
||||||
import setPublishedPoll from '../../../meetings/server/modifiers/setPublishedPoll';
|
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) {
|
export default function pollPublished({ body }, meetingId) {
|
||||||
const { pollId } = body;
|
const { pollId } = body;
|
||||||
@ -11,8 +8,4 @@ export default function pollPublished({ body }, meetingId) {
|
|||||||
check(pollId, String);
|
check(pollId, String);
|
||||||
|
|
||||||
setPublishedPoll(meetingId, true);
|
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 Meetings from '/imports/api/meetings';
|
||||||
import Users from '/imports/api/users';
|
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 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(
|
const user = Users.findOne(
|
||||||
{ meetingId, userId },
|
{ meetingId, userId },
|
||||||
{
|
{
|
||||||
@ -40,33 +35,4 @@ export default function sendAwayStatusChatMsg(meetingId, userId, newAwayStatus)
|
|||||||
return null;
|
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 useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
|
||||||
import {
|
import {
|
||||||
startUserTyping,
|
startUserTyping,
|
||||||
stopUserTyping,
|
|
||||||
textToMarkdown,
|
textToMarkdown,
|
||||||
} from './service';
|
} from './service';
|
||||||
import { Chat } from '/imports/ui/Types/chat';
|
import { Chat } from '/imports/ui/Types/chat';
|
||||||
@ -273,7 +272,6 @@ const ChatMessageForm: React.FC<ChatMessageFormProps> = ({
|
|||||||
updateUnreadMessages(chatId, '');
|
updateUnreadMessages(chatId, '');
|
||||||
setHasErrors(false);
|
setHasErrors(false);
|
||||||
setShowEmojiPicker(false);
|
setShowEmojiPicker(false);
|
||||||
if (ENABLE_TYPING_INDICATOR) stopUserTyping();
|
|
||||||
const sentMessageEvent = new CustomEvent(ChatEvents.SENT_MESSAGE);
|
const sentMessageEvent = new CustomEvent(ChatEvents.SENT_MESSAGE);
|
||||||
window.dispatchEvent(sentMessageEvent);
|
window.dispatchEvent(sentMessageEvent);
|
||||||
|
|
||||||
|
@ -13,7 +13,6 @@ export const startUserTyping = throttle(
|
|||||||
START_TYPING_THROTTLE_INTERVAL,
|
START_TYPING_THROTTLE_INTERVAL,
|
||||||
{ leading: true, trailing: false },
|
{ leading: true, trailing: false },
|
||||||
);
|
);
|
||||||
export const stopUserTyping = () => makeCall('stopUserTyping');
|
|
||||||
|
|
||||||
export const textToMarkdown = (message: string) => {
|
export const textToMarkdown = (message: string) => {
|
||||||
let parsedMessage = message || '';
|
let parsedMessage = message || '';
|
||||||
@ -31,6 +30,5 @@ export const textToMarkdown = (message: string) => {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
startUserTyping,
|
startUserTyping,
|
||||||
stopUserTyping,
|
|
||||||
textToMarkdown,
|
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 Users from '/imports/api/users';
|
||||||
import Meetings from '/imports/api/meetings';
|
import Meetings from '/imports/api/meetings';
|
||||||
import GroupChat from '/imports/api/group-chat';
|
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||||
import Storage from '/imports/ui/services/storage/session';
|
import Storage from '/imports/ui/services/storage/session';
|
||||||
@ -70,9 +69,6 @@ const setUserSentMessage = (bool) => {
|
|||||||
|
|
||||||
const getUser = (userId) => Users.findOne({ userId });
|
const getUser = (userId) => Users.findOne({ userId });
|
||||||
|
|
||||||
const getPrivateChatByUsers = (userId) => GroupChat
|
|
||||||
.findOne({ users: { $all: [userId, Auth.userID] } });
|
|
||||||
|
|
||||||
const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
|
const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
|
||||||
{ fields: { welcomeProp: 1 } });
|
{ fields: { welcomeProp: 1 } });
|
||||||
|
|
||||||
@ -183,53 +179,6 @@ const lastReadMessageTime = (receiverID) => {
|
|||||||
return UnreadMessages.get(chatType);
|
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 getScrollPosition = (receiverID) => {
|
||||||
const scroll = ScrollCollection.findOne({ receiver: receiverID },
|
const scroll = ScrollCollection.findOne({ receiver: receiverID },
|
||||||
{ fields: { position: 1 } }) || { position: null };
|
{ fields: { position: 1 } }) || { position: null };
|
||||||
@ -361,7 +310,6 @@ export default {
|
|||||||
reduceAndMapGroupMessages,
|
reduceAndMapGroupMessages,
|
||||||
reduceAndDontMapGroupMessages,
|
reduceAndDontMapGroupMessages,
|
||||||
getUser,
|
getUser,
|
||||||
getPrivateChatByUsers,
|
|
||||||
getWelcomeProp,
|
getWelcomeProp,
|
||||||
getScrollPosition,
|
getScrollPosition,
|
||||||
lastReadMessageTime,
|
lastReadMessageTime,
|
||||||
@ -369,7 +317,6 @@ export default {
|
|||||||
isChatClosed,
|
isChatClosed,
|
||||||
updateScrollPosition,
|
updateScrollPosition,
|
||||||
updateUnreadMessage,
|
updateUnreadMessage,
|
||||||
sendGroupMessage,
|
|
||||||
closePrivateChat,
|
closePrivateChat,
|
||||||
removeFromClosedChatsSession,
|
removeFromClosedChatsSession,
|
||||||
exportChat,
|
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 PropTypes from 'prop-types';
|
||||||
import Resizable from 're-resizable';
|
import Resizable from 're-resizable';
|
||||||
import { ACTIONS, PANELS } from '../layout/enums';
|
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 NotesContainer from '/imports/ui/components/notes/container';
|
||||||
import PollContainer from '/imports/ui/components/poll/container';
|
import PollContainer from '/imports/ui/components/poll/container';
|
||||||
import CaptionsContainer from '/imports/ui/components/captions/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 { withTracker } from 'meteor/react-meteor-data';
|
||||||
import Auth from '/imports/ui/services/auth';
|
import Auth from '/imports/ui/services/auth';
|
||||||
import logger from '/imports/startup/client/logger';
|
import logger from '/imports/startup/client/logger';
|
||||||
import GroupChat from '/imports/api/group-chat';
|
|
||||||
import Users from '/imports/api/users';
|
import Users from '/imports/api/users';
|
||||||
import { localCollectionRegistry } from '/client/collection-mirror-initializer';
|
import { localCollectionRegistry } from '/client/collection-mirror-initializer';
|
||||||
import SubscriptionRegistry, {
|
import SubscriptionRegistry, {
|
||||||
@ -24,7 +23,6 @@ const SUBSCRIPTIONS = [
|
|||||||
'users-infos',
|
'users-infos',
|
||||||
'meeting-time-remaining',
|
'meeting-time-remaining',
|
||||||
'local-settings',
|
'local-settings',
|
||||||
'users-typing',
|
|
||||||
'record-meetings',
|
'record-meetings',
|
||||||
'video-streams',
|
'video-streams',
|
||||||
'connection-status',
|
'connection-status',
|
||||||
@ -40,7 +38,6 @@ const SUBSCRIPTIONS = [
|
|||||||
'layout-meetings',
|
'layout-meetings',
|
||||||
'user-reaction',
|
'user-reaction',
|
||||||
'timer',
|
'timer',
|
||||||
// 'group-chat'
|
|
||||||
];
|
];
|
||||||
const {
|
const {
|
||||||
localBreakoutsSync,
|
localBreakoutsSync,
|
||||||
@ -96,6 +93,7 @@ export default withTracker(() => {
|
|||||||
},
|
},
|
||||||
'Error while subscribing to collections'
|
'Error while subscribing to collections'
|
||||||
);
|
);
|
||||||
|
console.log('-------------------------', {error});
|
||||||
Session.set('codeError', error.error);
|
Session.set('codeError', error.error);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -151,15 +149,6 @@ export default withTracker(() => {
|
|||||||
|
|
||||||
subscriptionsHandlers = subscriptionsHandlers.filter((obj) => obj);
|
subscriptionsHandlers = subscriptionsHandlers.filter((obj) => obj);
|
||||||
const ready = subscriptionsHandlers.every((handler) => handler.ready());
|
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
|
// TODO: Refactor all the late subscribers
|
||||||
let usersPersistentDataHandler = {};
|
let usersPersistentDataHandler = {};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Users from '/imports/api/users';
|
import Users from '/imports/api/users';
|
||||||
import VoiceUsers from '/imports/api/voice-users';
|
import VoiceUsers from '/imports/api/voice-users';
|
||||||
import GroupChat from '/imports/api/group-chat';
|
|
||||||
import Breakouts from '/imports/api/breakouts';
|
import Breakouts from '/imports/api/breakouts';
|
||||||
import Meetings from '/imports/api/meetings';
|
import Meetings from '/imports/api/meetings';
|
||||||
import UserReaction from '/imports/api/user-reaction';
|
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) => {
|
const toggleUserLock = (userId, lockStatus) => {
|
||||||
makeCall('toggleUserLock', userId, lockStatus);
|
makeCall('toggleUserLock', userId, lockStatus);
|
||||||
};
|
};
|
||||||
@ -788,11 +764,9 @@ export default {
|
|||||||
isPublicChat,
|
isPublicChat,
|
||||||
roving,
|
roving,
|
||||||
getCustomLogoUrl,
|
getCustomLogoUrl,
|
||||||
getGroupChatPrivate,
|
|
||||||
hasBreakoutRoom,
|
hasBreakoutRoom,
|
||||||
getEmojiList: () => EMOJI_STATUSES,
|
getEmojiList: () => EMOJI_STATUSES,
|
||||||
getEmoji,
|
getEmoji,
|
||||||
hasPrivateChatBetweenUsers,
|
|
||||||
toggleUserLock,
|
toggleUserLock,
|
||||||
requestUserInformation,
|
requestUserInformation,
|
||||||
focusFirstDropDownItem,
|
focusFirstDropDownItem,
|
||||||
|
Loading…
Reference in New Issue
Block a user