Merge pull request #11173 from Tainan404/develop+pr10919+pr10838
Chat with adapter and context
This commit is contained in:
commit
1476deb268
@ -25,6 +25,10 @@ import JoinHandler from '/imports/ui/components/join-handler/component';
|
||||
import AuthenticatedHandler from '/imports/ui/components/authenticated-handler/component';
|
||||
import Subscriptions from '/imports/ui/components/subscriptions/component';
|
||||
import IntlStartup from '/imports/startup/client/intl';
|
||||
import ContextProviders from '/imports/ui/components/context-providers/component';
|
||||
import ChatAdapter from '/imports/ui/components/components-data/chat-context/adapter';
|
||||
import UsersAdapter from '/imports/ui/components/components-data/users-context/adapter';
|
||||
import GroupChatAdapter from '/imports/ui/components/components-data/group-chat-context/adapter';
|
||||
|
||||
Meteor.startup(() => {
|
||||
// Logs all uncaught exceptions to the client logger
|
||||
@ -51,15 +55,22 @@ Meteor.startup(() => {
|
||||
|
||||
// TODO make this a Promise
|
||||
render(
|
||||
<JoinHandler>
|
||||
<AuthenticatedHandler>
|
||||
<Subscriptions>
|
||||
<IntlStartup>
|
||||
<Base />
|
||||
</IntlStartup>
|
||||
</Subscriptions>
|
||||
</AuthenticatedHandler>
|
||||
</JoinHandler>,
|
||||
<ContextProviders>
|
||||
<React.Fragment>
|
||||
<JoinHandler>
|
||||
<AuthenticatedHandler>
|
||||
<Subscriptions>
|
||||
<IntlStartup>
|
||||
<Base />
|
||||
</IntlStartup>
|
||||
</Subscriptions>
|
||||
</AuthenticatedHandler>
|
||||
</JoinHandler>
|
||||
<UsersAdapter />
|
||||
<ChatAdapter />
|
||||
<GroupChatAdapter />
|
||||
</React.Fragment>
|
||||
</ContextProviders>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
});
|
||||
|
@ -7,6 +7,7 @@ export default function clearGroupChatMsg(meetingId, chatId) {
|
||||
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 {
|
||||
@ -15,6 +16,7 @@ export default function clearGroupChatMsg(meetingId, chatId) {
|
||||
if (numberAffected) {
|
||||
Logger.info(`Cleared GroupChatMsg (${meetingId}, ${chatId})`);
|
||||
const clearMsg = {
|
||||
id: `${SYSTEM_CHAT_TYPE}-${CHAT_CLEAR_MESSAGE}`,
|
||||
color: '0',
|
||||
timestamp: Date.now(),
|
||||
correlationId: `${PUBLIC_CHAT_SYSTEM_ID}-${Date.now()}`,
|
||||
|
@ -19,12 +19,13 @@ function groupChatMsg(chatsIds) {
|
||||
|
||||
Logger.debug('Publishing group-chat-msg', { meetingId, userId });
|
||||
|
||||
return GroupChatMsg.find({
|
||||
const selector = {
|
||||
$or: [
|
||||
{ meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } },
|
||||
{ chatId: { $in: chatsIds } },
|
||||
],
|
||||
});
|
||||
};
|
||||
return GroupChatMsg.find(selector);
|
||||
}
|
||||
|
||||
function publish(...args) {
|
||||
|
@ -12,7 +12,8 @@ const ChatAlertContainer = props => (
|
||||
|
||||
export default withTracker(() => {
|
||||
const AppSettings = Settings.application;
|
||||
const activeChats = UserListService.getActiveChats();
|
||||
const activeChats = [];
|
||||
// UserListService.getActiveChats();
|
||||
const { loginTime } = Users.findOne({ userId: Auth.userID }, { fields: { loginTime: 1 } });
|
||||
|
||||
const openPanel = Session.get('openPanel');
|
||||
|
@ -50,10 +50,20 @@ class ChatDropdown extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.clipboard = new Clipboard('#clipboardButton', {
|
||||
text: () => ChatService.exportChat(ChatService.getPublicGroupMessages()),
|
||||
text: () => '',
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const { timeWindowsValues } = this.props;
|
||||
const { isSettingOpen } = this.state;
|
||||
if (prevState.isSettingOpen !== isSettingOpen) {
|
||||
this.clipboard = new Clipboard('#clipboardButton', {
|
||||
text: () => ChatService.exportChat(timeWindowsValues),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.clipboard.destroy();
|
||||
}
|
||||
@ -72,13 +82,12 @@ class ChatDropdown extends PureComponent {
|
||||
|
||||
getAvailableActions() {
|
||||
const {
|
||||
intl, isMeteorConnected, amIModerator, meetingIsBreakout, meetingName,
|
||||
intl, isMeteorConnected, amIModerator, meetingIsBreakout, meetingName, timeWindowsValues,
|
||||
} = this.props;
|
||||
|
||||
const clearIcon = 'delete';
|
||||
const saveIcon = 'download';
|
||||
const copyIcon = 'copy';
|
||||
|
||||
return _.compact([
|
||||
<DropdownListItem
|
||||
data-test="chatSave"
|
||||
@ -91,12 +100,11 @@ class ChatDropdown extends PureComponent {
|
||||
const date = new Date();
|
||||
const time = `${date.getHours()}-${date.getMinutes()}`;
|
||||
const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
|
||||
|
||||
link.setAttribute('download', `bbb-${meetingName}[public-chat]_${dateString}.txt`);
|
||||
link.setAttribute(
|
||||
'href',
|
||||
`data: ${mimeType} ;charset=utf-8,
|
||||
${encodeURIComponent(ChatService.exportChat(ChatService.getPublicGroupMessages()))}`,
|
||||
${encodeURIComponent(ChatService.exportChat(timeWindowsValues))}`,
|
||||
);
|
||||
link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
|
||||
}}
|
||||
|
@ -0,0 +1,37 @@
|
||||
|
||||
|
||||
class ChatLogger {
|
||||
constructor() {
|
||||
this.logLevel = 'info';
|
||||
this.levels = Object.freeze({
|
||||
error: 1,
|
||||
info: 2,
|
||||
debug: 3,
|
||||
trace: 4,
|
||||
});
|
||||
Object.keys(this.levels).forEach((i) => {
|
||||
this[i] = this.logger.bind(this, i);
|
||||
});
|
||||
}
|
||||
|
||||
setLogLevel(level) {
|
||||
if (this.levels[level]) {
|
||||
this.logLevel = level;
|
||||
} else {
|
||||
throw new Error('This Level not exist');
|
||||
}
|
||||
}
|
||||
|
||||
getLogLevel() {
|
||||
return this.logLevel;
|
||||
}
|
||||
|
||||
logger(level, ...text) {
|
||||
const logLevel = this.levels[level];
|
||||
if (this.levels[this.logLevel] >= logLevel) {
|
||||
console.log(`${level}:`, ...text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ChatLogger();
|
@ -7,7 +7,7 @@ import { Session } from 'meteor/session';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import { styles } from './styles.scss';
|
||||
import MessageForm from './message-form/container';
|
||||
import MessageList from './message-list/container';
|
||||
import TimeWindowList from './time-window-list/container';
|
||||
import ChatDropdownContainer from './chat-dropdown/container';
|
||||
|
||||
const ELEMENT_ID = 'chat-messages';
|
||||
@ -22,10 +22,10 @@ const intlMessages = defineMessages({
|
||||
description: 'aria-label for hiding chat button',
|
||||
},
|
||||
});
|
||||
|
||||
const Chat = (props) => {
|
||||
const {
|
||||
chatID,
|
||||
chatName,
|
||||
title,
|
||||
messages,
|
||||
partnerIsLoggedOut,
|
||||
@ -42,11 +42,12 @@ const Chat = (props) => {
|
||||
maxMessageLength,
|
||||
amIModerator,
|
||||
meetingIsBreakout,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
count,
|
||||
} = props;
|
||||
|
||||
const HIDE_CHAT_AK = shortcuts.hidePrivateChat;
|
||||
const CLOSE_CHAT_AK = shortcuts.closePrivateChat;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test={chatID !== 'public' ? 'privateChat' : 'publicChat'}
|
||||
@ -90,26 +91,29 @@ const Chat = (props) => {
|
||||
accessKey={CLOSE_CHAT_AK}
|
||||
/>
|
||||
)
|
||||
: <ChatDropdownContainer {...{ meetingIsBreakout, isMeteorConnected, amIModerator }} />
|
||||
: <ChatDropdownContainer {...{ meetingIsBreakout, isMeteorConnected, amIModerator, timeWindowsValues }} />
|
||||
}
|
||||
</header>
|
||||
<MessageList
|
||||
<TimeWindowList
|
||||
id={ELEMENT_ID}
|
||||
chatId={chatID}
|
||||
handleScrollUpdate={actions.handleScrollUpdate}
|
||||
handleReadMessage={actions.handleReadMessage}
|
||||
{...{
|
||||
partnerIsLoggedOut,
|
||||
lastReadMessageTime,
|
||||
hasUnreadMessages,
|
||||
scrollPosition,
|
||||
messages,
|
||||
currentUserIsModerator: amIModerator,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
count,
|
||||
}}
|
||||
/>
|
||||
<MessageForm
|
||||
{...{
|
||||
UnsentMessagesCollection,
|
||||
chatName,
|
||||
title,
|
||||
minMessageLength,
|
||||
maxMessageLength,
|
||||
}}
|
||||
@ -130,7 +134,6 @@ export default withShortcutHelper(injectWbResizeEvent(injectIntl(memo(Chat))), [
|
||||
|
||||
const propTypes = {
|
||||
chatID: PropTypes.string.isRequired,
|
||||
chatName: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
messages: PropTypes.arrayOf(PropTypes.objectOf(PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
|
@ -1,19 +1,30 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import React, { useEffect, useContext, useState } from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
import _ from 'lodash';
|
||||
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 ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import Chat from './component';
|
||||
import ChatService from './service';
|
||||
|
||||
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 CONNECTION_STATUS = 'online';
|
||||
const DEBOUNCE_TIME = 1000;
|
||||
|
||||
const sysMessagesIds = {
|
||||
welcomeId: `${SYSTEM_CHAT_TYPE}-welcome-msg`,
|
||||
moderatorId: `${SYSTEM_CHAT_TYPE}-moderator-msg`
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
[CHAT_CLEAR]: {
|
||||
@ -34,153 +45,144 @@ const intlMessages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
class ChatContainer extends PureComponent {
|
||||
componentDidMount() {
|
||||
// in case of reopening a chat, need to make sure it's removed from closed list
|
||||
let previousChatId = null;
|
||||
let debounceTimeout = null;
|
||||
let messages = null;
|
||||
let globalAppplyStateToProps = () => { }
|
||||
|
||||
const throttledFunc = _.throttle(() => {
|
||||
globalAppplyStateToProps();
|
||||
}, DEBOUNCE_TIME, { trailing: true, leading: true });
|
||||
|
||||
const ChatContainer = (props) => {
|
||||
useEffect(() => {
|
||||
ChatService.removeFromClosedChatsSession();
|
||||
}
|
||||
}, []);
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
unmounting,
|
||||
} = this.props;
|
||||
const modOnlyMessage = Storage.getItem('ModeratorOnlyMessage');
|
||||
const { welcomeProp } = ChatService.getWelcomeProp();
|
||||
|
||||
if (unmounting === true) {
|
||||
return null;
|
||||
const {
|
||||
children,
|
||||
unmounting,
|
||||
chatID,
|
||||
amIModerator,
|
||||
loginTime,
|
||||
intl,
|
||||
} = props;
|
||||
|
||||
const isPublicChat = chatID === PUBLIC_CHAT_KEY;
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Chat {...this.props}>
|
||||
{children}
|
||||
</Chat>
|
||||
);
|
||||
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 { groupChat } = usingGroupChatContext;
|
||||
const participants = groupChat[chatID]?.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);
|
||||
|
||||
if (unmounting === true) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
applyPropsToState = () => {
|
||||
if (!_.isEqualWith(lastMsg, stateLastMsg) || previousChatId !== chatID) {
|
||||
const timeWindowsValues = isPublicChat
|
||||
? [...Object.values(contextChat?.preJoinMessages || {}), ...systemMessagesIds.map((item) => systemMessages[item]),
|
||||
...Object.values(contextChat?.posJoinMessages || {})]
|
||||
: [...Object.values(contextChat?.messageGroups || {})];
|
||||
if (previousChatId !== chatID) {
|
||||
previousChatId = chatID;
|
||||
}
|
||||
|
||||
setLastMsg(lastMsg ? { ...lastMsg } : lastMsg);
|
||||
setTimeWindows(timeWindowsValues);
|
||||
}
|
||||
}
|
||||
globalAppplyStateToProps = applyPropsToState;
|
||||
throttledFunc();
|
||||
|
||||
return (
|
||||
<Chat {...{
|
||||
...props,
|
||||
chatID,
|
||||
amIModerator,
|
||||
count: (contextChat?.unreadTimeWindows.size || 0),
|
||||
timeWindowsValues: stateTimeWindows,
|
||||
dispatch: usingChatContext?.dispatch,
|
||||
title,
|
||||
chatName,
|
||||
contextChat,
|
||||
}}>
|
||||
{children}
|
||||
</Chat>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(withTracker(({ intl }) => {
|
||||
const chatID = Session.get('idChatOpen');
|
||||
let messages = [];
|
||||
let isChatLocked = ChatService.isChatLocked(chatID);
|
||||
let title = intl.formatMessage(intlMessages.titlePublic);
|
||||
let chatName = title;
|
||||
|
||||
// let chatName = title;
|
||||
let partnerIsLoggedOut = false;
|
||||
let systemMessageIntl = {};
|
||||
|
||||
const currentUser = ChatService.getUser(Auth.userID);
|
||||
const amIModerator = currentUser.role === ROLE_MODERATOR;
|
||||
|
||||
if (chatID === PUBLIC_CHAT_KEY) {
|
||||
const { welcomeProp } = ChatService.getWelcomeProp();
|
||||
|
||||
messages = ChatService.getPublicGroupMessages();
|
||||
|
||||
const time = currentUser.loginTime;
|
||||
const welcomeId = `welcome-msg-${time}`;
|
||||
|
||||
const welcomeMsg = {
|
||||
id: welcomeId,
|
||||
content: [{
|
||||
id: welcomeId,
|
||||
text: welcomeProp.welcomeMsg,
|
||||
time,
|
||||
}],
|
||||
time,
|
||||
sender: null,
|
||||
};
|
||||
|
||||
let moderatorMsg;
|
||||
const modOnlyMessage = Storage.getItem('ModeratorOnlyMessage');
|
||||
if (amIModerator && modOnlyMessage) {
|
||||
const moderatorTime = time + 1;
|
||||
const moderatorId = `moderator-msg-${moderatorTime}`;
|
||||
|
||||
moderatorMsg = {
|
||||
id: moderatorId,
|
||||
content: [{
|
||||
id: moderatorId,
|
||||
text: modOnlyMessage,
|
||||
time: moderatorTime,
|
||||
}],
|
||||
time: moderatorTime,
|
||||
sender: null,
|
||||
};
|
||||
}
|
||||
|
||||
const messagesBeforeWelcomeMsg = ChatService.reduceAndDontMapGroupMessages(
|
||||
messages.filter(message => message.timestamp < time),
|
||||
);
|
||||
const messagesAfterWelcomeMsg = ChatService.reduceAndDontMapGroupMessages(
|
||||
messages.filter(message => message.timestamp >= time),
|
||||
);
|
||||
|
||||
const messagesFormated = messagesBeforeWelcomeMsg
|
||||
.concat(welcomeMsg)
|
||||
.concat((amIModerator && modOnlyMessage) ? moderatorMsg : [])
|
||||
.concat(messagesAfterWelcomeMsg);
|
||||
|
||||
messages = messagesFormated.sort((a, b) => (a.time - b.time));
|
||||
} else if (chatID) {
|
||||
messages = ChatService.getPrivateGroupMessages();
|
||||
|
||||
const receiverUser = ChatService.getUser(chatID);
|
||||
const privateChat = ChatService.getPrivateChatByUsers(chatID);
|
||||
|
||||
chatName = receiverUser?.name || privateChat.participants.filter(u => u.id === chatID).pop().name;
|
||||
|
||||
systemMessageIntl = { 0: chatName };
|
||||
title = intl.formatMessage(intlMessages.titlePrivate, systemMessageIntl);
|
||||
partnerIsLoggedOut = !!receiverUser;
|
||||
|
||||
if (!partnerIsLoggedOut) {
|
||||
const time = Date.now();
|
||||
const id = `partner-disconnected-${time}`;
|
||||
const messagePartnerLoggedOut = {
|
||||
id,
|
||||
content: [{
|
||||
id,
|
||||
text: 'partnerDisconnected',
|
||||
time,
|
||||
}],
|
||||
time,
|
||||
sender: null,
|
||||
};
|
||||
|
||||
messages.push(messagePartnerLoggedOut);
|
||||
isChatLocked = true;
|
||||
}
|
||||
} else {
|
||||
if (!chatID) {
|
||||
// No chatID is set so the panel is closed, about to close, or wasn't opened correctly
|
||||
return {
|
||||
unmounting: true,
|
||||
};
|
||||
}
|
||||
|
||||
messages = messages.map((message) => {
|
||||
if (message.sender && message.sender.id !== SYSTEM_CHAT_TYPE) return message;
|
||||
|
||||
return {
|
||||
...message,
|
||||
content: message.content ? message.content.map(content => ({
|
||||
...content,
|
||||
text: content.text in intlMessages
|
||||
? `<b><i>${intl.formatMessage(intlMessages[content.text], systemMessageIntl)}</i></b>` : content.text,
|
||||
})) : [],
|
||||
};
|
||||
});
|
||||
|
||||
const { connected: isMeteorConnected } = Meteor.status();
|
||||
|
||||
return {
|
||||
chatID,
|
||||
chatName,
|
||||
title,
|
||||
messages,
|
||||
intl,
|
||||
messages: [],
|
||||
partnerIsLoggedOut,
|
||||
isChatLocked,
|
||||
isMeteorConnected,
|
||||
amIModerator,
|
||||
meetingIsBreakout: meetingIsBreakout(),
|
||||
loginTime: getLoginTime(),
|
||||
actions: {
|
||||
handleClosePrivateChat: ChatService.closePrivateChat,
|
||||
},
|
||||
|
@ -16,7 +16,6 @@ const propTypes = {
|
||||
minMessageLength: PropTypes.number.isRequired,
|
||||
maxMessageLength: PropTypes.number.isRequired,
|
||||
chatTitle: PropTypes.string.isRequired,
|
||||
chatName: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
chatAreaId: PropTypes.string.isRequired,
|
||||
handleSendMessage: PropTypes.func.isRequired,
|
||||
@ -267,7 +266,7 @@ class MessageForm extends PureComponent {
|
||||
const {
|
||||
intl,
|
||||
chatTitle,
|
||||
chatName,
|
||||
title,
|
||||
disabled,
|
||||
className,
|
||||
chatAreaId,
|
||||
@ -286,7 +285,7 @@ class MessageForm extends PureComponent {
|
||||
className={styles.input}
|
||||
id="message-input"
|
||||
innerRef={(ref) => { this.textarea = ref; return this.textarea; }}
|
||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: chatName })}
|
||||
placeholder={intl.formatMessage(messages.inputPlaceholder, { 0: title })}
|
||||
aria-controls={chatAreaId}
|
||||
aria-label={intl.formatMessage(messages.inputLabel, { 0: chatTitle })}
|
||||
aria-invalid={hasErrors ? 'true' : 'false'}
|
||||
|
@ -18,7 +18,7 @@ class ChatContainer extends PureComponent {
|
||||
|
||||
export default withTracker(() => {
|
||||
const cleanScrollAndSendMessage = (message) => {
|
||||
ChatService.updateScrollPosition(null);
|
||||
ChatService.setUserSentMessage(true);
|
||||
return ChatService.sendGroupMessage(message);
|
||||
};
|
||||
|
||||
|
@ -13,6 +13,7 @@
|
||||
width: 100%;
|
||||
position: relative;
|
||||
margin-bottom: calc(-1 * var(--sm-padding-x));
|
||||
margin-top: .2rem;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
@ -140,6 +141,7 @@
|
||||
> span {
|
||||
display: block;
|
||||
margin-right: 0.05rem;
|
||||
margin-left: 0.05rem;
|
||||
line-height: var(--font-size-md);
|
||||
}
|
||||
|
||||
|
@ -280,7 +280,6 @@ class MessageList extends Component {
|
||||
});
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
console.log('caiu aqui');
|
||||
if (e.deltaY < 0) {
|
||||
this.setState({
|
||||
userScrolledBack: true,
|
||||
|
@ -28,11 +28,20 @@ const ScrollCollection = new Mongo.Collection(null);
|
||||
|
||||
const UnsentMessagesCollection = new Mongo.Collection(null);
|
||||
|
||||
export const UserSentMessageCollection = new Mongo.Collection(null);
|
||||
|
||||
// session for closed chat list
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
const POLL_MESSAGE_PREFIX = 'bbb-published-poll-<br/>';
|
||||
|
||||
const setUserSentMessage = (bool) => {
|
||||
UserSentMessageCollection.upsert(
|
||||
{ userId: Auth.userID },
|
||||
{ $set: { sent: bool } },
|
||||
);
|
||||
}
|
||||
|
||||
const getUser = userId => Users.findOne({ userId });
|
||||
|
||||
const getPrivateChatByUsers = userId => GroupChat
|
||||
@ -47,6 +56,7 @@ const mapGroupMessage = (message) => {
|
||||
content: message.content,
|
||||
time: message.timestamp || message.time,
|
||||
sender: null,
|
||||
key: message.key
|
||||
};
|
||||
|
||||
if (message.sender && message.sender.id !== SYSTEM_CHAT_TYPE) {
|
||||
@ -75,7 +85,7 @@ const reduceGroupMessages = (previous, current) => {
|
||||
time: current.timestamp,
|
||||
color: current.color,
|
||||
}];
|
||||
if (!lastMessage || !currentMessage.chatId === PUBLIC_GROUP_CHAT_ID) {
|
||||
if (!lastMessage) {
|
||||
return previous.concat(currentMessage);
|
||||
}
|
||||
// Check if the last message is from the same user and time discrepancy
|
||||
@ -95,6 +105,35 @@ const reduceGroupMessages = (previous, current) => {
|
||||
return previous.concat(currentMessage);
|
||||
};
|
||||
|
||||
const getChatMessages = (chatId) => {
|
||||
return []
|
||||
if (chatId === PUBLIC_CHAT_ID) {
|
||||
return GroupChatMsg.find({
|
||||
meetingId: Auth.meetingID,
|
||||
chatId: PUBLIC_GROUP_CHAT_ID,
|
||||
|
||||
}, { sort: ['timestamp'] }).fetch();
|
||||
}
|
||||
const senderId = Auth.userID;
|
||||
|
||||
const privateChat = GroupChat.findOne({
|
||||
meetingId: Auth.meetingID,
|
||||
users: { $all: [chatId, senderId] },
|
||||
access: PRIVATE_CHAT_TYPE,
|
||||
});
|
||||
|
||||
if (privateChat) {
|
||||
const {
|
||||
chatId: id,
|
||||
} = privateChat;
|
||||
|
||||
return GroupChatMsg.find({
|
||||
meetingId: Auth.meetingID,
|
||||
chatId: id,
|
||||
}, { sort: ['timestamp'] }).fetch();
|
||||
}
|
||||
};
|
||||
|
||||
const reduceAndMapGroupMessages = messages => (messages
|
||||
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
|
||||
|
||||
@ -102,6 +141,7 @@ const reduceAndDontMapGroupMessages = messages => (messages
|
||||
.reduce(reduceGroupMessages, []));
|
||||
|
||||
const getPublicGroupMessages = () => {
|
||||
return [];
|
||||
const publicGroupMessages = GroupChatMsg.find({
|
||||
meetingId: Auth.meetingID,
|
||||
chatId: PUBLIC_GROUP_CHAT_ID,
|
||||
@ -110,6 +150,7 @@ const getPublicGroupMessages = () => {
|
||||
};
|
||||
|
||||
const getPrivateGroupMessages = () => {
|
||||
return [];
|
||||
const chatID = Session.get('idChatOpen');
|
||||
const senderId = Auth.userID;
|
||||
|
||||
@ -177,7 +218,11 @@ const lastReadMessageTime = (receiverID) => {
|
||||
};
|
||||
|
||||
const sendGroupMessage = (message) => {
|
||||
const chatID = Session.get('idChatOpen');
|
||||
// TODO: Refactor to use chatId directly
|
||||
const chatIdToSent = Session.get('idChatOpen') === PUBLIC_CHAT_ID ? PUBLIC_GROUP_CHAT_ID : Session.get('idChatOpen')
|
||||
const chat = GroupChat.findOne({ chatId: chatIdToSent },
|
||||
{ fields: { users: 1 } });
|
||||
const chatID = Session.get('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;
|
||||
@ -238,12 +283,11 @@ const updateUnreadMessage = (timestamp) => {
|
||||
|
||||
const clearPublicChatHistory = () => (makeCall('clearPublicChatHistory'));
|
||||
|
||||
const closePrivateChat = () => {
|
||||
const chatID = Session.get('idChatOpen');
|
||||
const closePrivateChat = (chatId) => {
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
|
||||
if (_.indexOf(currentClosedChats, chatID) < 0) {
|
||||
currentClosedChats.push(chatID);
|
||||
if (_.indexOf(currentClosedChats, chatId) < 0) {
|
||||
currentClosedChats.push(chatId);
|
||||
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, currentClosedChats);
|
||||
}
|
||||
@ -268,45 +312,27 @@ const htmlDecode = (input) => {
|
||||
};
|
||||
|
||||
// Export the chat as [Hour:Min] user: message
|
||||
const exportChat = (messageList) => {
|
||||
const { welcomeProp } = getWelcomeProp();
|
||||
const { loginTime } = Users.findOne({ userId: Auth.userID }, { fields: { loginTime: 1 } });
|
||||
const { welcomeMsg } = welcomeProp;
|
||||
const exportChat = (timeWindowList) => {
|
||||
const messageList = timeWindowList.reduce( (acc, timeWindow) => [...acc, ...timeWindow.content], []);
|
||||
messageList.sort((a, b) => a.time - b.time);
|
||||
|
||||
const clearMessage = messageList.filter(message => message.message === PUBLIC_CHAT_CLEAR);
|
||||
|
||||
const hasClearMessage = clearMessage.length;
|
||||
|
||||
if (!hasClearMessage || (hasClearMessage && clearMessage[0].timestamp < loginTime)) {
|
||||
messageList.push({
|
||||
timestamp: loginTime,
|
||||
message: welcomeMsg,
|
||||
type: SYSTEM_CHAT_TYPE,
|
||||
sender: {
|
||||
id: PUBLIC_CHAT_USER_ID,
|
||||
name: ''
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
messageList.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
return messageList.map((message) => {
|
||||
const date = new Date(message.timestamp);
|
||||
return messageList.map(message => {
|
||||
const date = new Date(message.time);
|
||||
const hour = date.getHours().toString().padStart(2, 0);
|
||||
const min = date.getMinutes().toString().padStart(2, 0);
|
||||
const hourMin = `[${hour}:${min}]`;
|
||||
if (message.type === SYSTEM_CHAT_TYPE) {
|
||||
return `${hourMin} ${message.message}`;
|
||||
}
|
||||
const userName = message.sender.id === PUBLIC_CHAT_USER_ID
|
||||
|
||||
const userName = message.id.endsWith('welcome-msg')
|
||||
? ''
|
||||
: `${message.sender.name} :`;
|
||||
return `${hourMin} ${userName} ${htmlDecode(message.message)}`;
|
||||
: `${message.name} :`;
|
||||
return `${hourMin} ${userName} ${htmlDecode(message.text)}`;
|
||||
}).join('\n');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const getAllMessages = (chatID) => {
|
||||
return [];
|
||||
const filter = {
|
||||
'sender.id': { $ne: Auth.userID },
|
||||
};
|
||||
@ -335,9 +361,11 @@ const getLastMessageTimestampFromChatList = activeChats => activeChats
|
||||
.reduce(maxNumberReducer, 0);
|
||||
|
||||
export default {
|
||||
setUserSentMessage,
|
||||
mapGroupMessage,
|
||||
reduceAndMapGroupMessages,
|
||||
reduceAndDontMapGroupMessages,
|
||||
getChatMessages,
|
||||
getPublicGroupMessages,
|
||||
getPrivateGroupMessages,
|
||||
getUser,
|
||||
|
@ -0,0 +1,316 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import { List, AutoSizer,CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
||||
import { styles } from './styles';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import TimeWindowChatItem from './time-window-chat-item/container';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
const propTypes = {
|
||||
scrollPosition: PropTypes.number,
|
||||
chatId: PropTypes.string.isRequired,
|
||||
hasUnreadMessages: PropTypes.bool.isRequired,
|
||||
handleScrollUpdate: PropTypes.func.isRequired,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
lastReadMessageTime: PropTypes.number,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
scrollPosition: null,
|
||||
lastReadMessageTime: 0,
|
||||
};
|
||||
|
||||
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',
|
||||
},
|
||||
});
|
||||
class TimeWindowList extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
minHeight: 18,
|
||||
keyMapper: (rowIndex, columnIndex) => {
|
||||
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.systemMessagesResized = {};
|
||||
|
||||
this.state = {
|
||||
scrollArea: null,
|
||||
shouldScrollToPosition: false,
|
||||
scrollPosition: 0,
|
||||
userScrolledBack: false,
|
||||
lastMessage: {},
|
||||
};
|
||||
this.welcomeMessageIndex = -1;
|
||||
|
||||
this.listRef = null;
|
||||
this.virualRef = null;
|
||||
|
||||
this.lastWidth = 0;
|
||||
|
||||
this.scrollInterval = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// TODO: re-implement scroll to position using virtualized list
|
||||
}
|
||||
|
||||
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,
|
||||
} = this.props;
|
||||
|
||||
const {timeWindowsValues: prevTimeWindowsValues, chatId: prevChatId} = prevProps;
|
||||
|
||||
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 (lastTimeWindow && (chatId !== prevChatId)) {
|
||||
this.listRef.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (userSentMessage && !prevProps.userSentMessage){
|
||||
this.setState({
|
||||
userScrolledBack: false,
|
||||
}, ()=> setUserSentMessage(false));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.scrollInterval);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
rowRender({
|
||||
index,
|
||||
parent,
|
||||
style,
|
||||
key,
|
||||
}) {
|
||||
const {
|
||||
id,
|
||||
timeWindowsValues,
|
||||
dispatch,
|
||||
chatId,
|
||||
} = this.props;
|
||||
|
||||
const { scrollArea } = this.state;
|
||||
const message = timeWindowsValues[index];
|
||||
|
||||
if (message.key === `${SYSTEM_CHAT_TYPE}-welcome-msg`) {
|
||||
if (index !== this.welcomeMessageIndex) {
|
||||
this.welcomeMessageIndex = index;
|
||||
[500, 1000, 2000, 3000, 4000, 5000].forEach((i)=>{
|
||||
setTimeout(() => {
|
||||
if (this.listRef) {
|
||||
this.cache.clear(index);
|
||||
this.listRef.recomputeRowHeights(index);
|
||||
}
|
||||
}, i);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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}`}
|
||||
>
|
||||
<TimeWindowChatItem
|
||||
key={key}
|
||||
message={message}
|
||||
messageId={message.id}
|
||||
chatAreaId={id}
|
||||
scrollArea={scrollArea}
|
||||
dispatch={dispatch}
|
||||
chatId={chatId}
|
||||
/>
|
||||
</span>
|
||||
</CellMeasurer>
|
||||
);
|
||||
}
|
||||
|
||||
renderUnreadNotification() {
|
||||
const {
|
||||
intl,
|
||||
count,
|
||||
} = this.props;
|
||||
const { userScrolledBack } = this.state;
|
||||
|
||||
if (count && userScrolledBack) {
|
||||
return (
|
||||
<Button
|
||||
aria-hidden="true"
|
||||
className={styles.unreadButton}
|
||||
color="primary"
|
||||
size="sm"
|
||||
key="unread-messages"
|
||||
label={intl.formatMessage(intlMessages.moreMessages)}
|
||||
onClick={()=> this.setState({
|
||||
userScrolledBack: false,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
timeWindowsValues,
|
||||
} = this.props;
|
||||
const {
|
||||
scrollArea,
|
||||
userScrolledBack,
|
||||
} = this.state;
|
||||
ChatLogger.debug('TimeWindowList::render', {...this.props}, {...this.state}, new Date());
|
||||
|
||||
return (
|
||||
[<div
|
||||
onMouseDown={()=> {
|
||||
this.setState({
|
||||
userScrolledBack: true,
|
||||
});
|
||||
}}
|
||||
onWheel={(e) => {
|
||||
if (e.deltaY < 0) {
|
||||
this.setState({
|
||||
userScrolledBack: true,
|
||||
});
|
||||
this.userScrolledBack = true
|
||||
}
|
||||
}}
|
||||
className={styles.messageListWrapper}
|
||||
key="chat-list"
|
||||
data-test="chatMessages"
|
||||
ref={node => this.messageListWrapper = node}
|
||||
>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
if (width !== this.lastWidth) {
|
||||
this.lastWidth = width;
|
||||
this.cache.clearAll();
|
||||
}
|
||||
|
||||
return (
|
||||
<List
|
||||
ref={(ref) => {
|
||||
if (ref !== null) {
|
||||
this.listRef = ref;
|
||||
|
||||
if (!scrollArea) {
|
||||
this.setState({ scrollArea: findDOMNode(this.listRef) });
|
||||
}
|
||||
}
|
||||
}}
|
||||
isScrolling={true}
|
||||
rowHeight={this.cache.rowHeight}
|
||||
className={styles.messageList}
|
||||
rowRenderer={this.rowRender}
|
||||
rowCount={timeWindowsValues.length}
|
||||
height={height}
|
||||
width={width}
|
||||
overscanRowCount={0}
|
||||
deferredMeasurementCache={this.cache}
|
||||
scrollToIndex={
|
||||
!userScrolledBack ? timeWindowsValues.length - 1 : undefined
|
||||
}
|
||||
onScroll={({ clientHeight, scrollHeight, scrollTop })=> {
|
||||
const scrollSize = scrollTop + clientHeight;
|
||||
if (scrollSize >= scrollHeight) {
|
||||
this.setState({
|
||||
userScrolledBack: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
</div>,
|
||||
this.renderUnreadNotification()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeWindowList.propTypes = propTypes;
|
||||
TimeWindowList.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(TimeWindowList);
|
@ -0,0 +1,31 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import TimeWindowList from './component';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import ChatService, { UserSentMessageCollection } from '../service';
|
||||
export default class TimeWindowListContainer extends PureComponent {
|
||||
render() {
|
||||
const { chatId } = this.props;
|
||||
const hasUnreadMessages = ChatService.hasUnreadMessages(chatId);
|
||||
const scrollPosition = ChatService.getScrollPosition(chatId);
|
||||
const lastReadMessageTime = ChatService.lastReadMessageTime(chatId);
|
||||
const userSentMessage = UserSentMessageCollection.findOne({ userId: Auth.userID, sent: true });
|
||||
ChatLogger.debug('TimeWindowListContainer::render', { ...this.props }, new Date());
|
||||
return (
|
||||
<TimeWindowList
|
||||
{
|
||||
...{
|
||||
...this.props,
|
||||
hasUnreadMessages,
|
||||
scrollPosition,
|
||||
lastReadMessageTime,
|
||||
handleScrollUpdate: ChatService.updateScrollPosition,
|
||||
userSentMessage,
|
||||
setUserSentMessage: ChatService.setUserSentMessage,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
@import "/imports/ui/stylesheets/mixins/_scrollable";
|
||||
@import "/imports/ui/stylesheets/variables/placeholders";
|
||||
|
||||
.messageListWrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-left: var(--sm-padding-x);
|
||||
margin-left: calc(-1 * var(--md-padding-x));
|
||||
padding-right: var(--sm-padding-y);
|
||||
margin-right: calc(-1 * var(--md-padding-y));
|
||||
padding-bottom: var(--md-padding-x);
|
||||
margin-bottom: calc(-1 * var(--md-padding-x));
|
||||
z-index: 2;
|
||||
[dir="rtl"] & {
|
||||
padding-right: var(--md-padding-x);
|
||||
margin-right: calc(-1 * var(--md-padding-x));
|
||||
padding-left: var(--md-padding-y);
|
||||
margin-left: calc(-1 * var(--md-padding-x));
|
||||
}
|
||||
}
|
||||
|
||||
.messageList {
|
||||
@include scrollbox-vertical();
|
||||
flex-flow: column;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin: 0 auto 0 0;
|
||||
right: 0 var(--md-padding-x) 0 0;
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
outline-style: none;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 auto;
|
||||
padding: 0 0 0 var(--md-padding-x);
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: var(--md-padding-x);
|
||||
}
|
||||
}
|
||||
|
||||
.unreadButton {
|
||||
flex-shrink: 0;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: .25rem;
|
||||
z-index: 3;
|
||||
@extend %text-elipsis;
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedTime, defineMessages, injectIntl } from 'react-intl';
|
||||
import _ from 'lodash';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import UserAvatar from '/imports/ui/components/user-avatar/component';
|
||||
import cx from 'classnames';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import MessageChatItem from './message-chat-item/component';
|
||||
import { styles } from './styles';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const CHAT_CLEAR_MESSAGE = CHAT_CONFIG.system_messages_keys.chat_clear;
|
||||
|
||||
const propTypes = {
|
||||
user: PropTypes.shape({
|
||||
color: PropTypes.string,
|
||||
isModerator: PropTypes.bool,
|
||||
isOnline: PropTypes.bool,
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
messages: PropTypes.arrayOf(Object).isRequired,
|
||||
time: 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,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
user: null,
|
||||
scrollArea: null,
|
||||
lastReadMessageTime: 0,
|
||||
time: 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',
|
||||
}
|
||||
});
|
||||
|
||||
class TimeWindowChatItem extends PureComponent {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::props', { ...this.props }, { ...prevProps });
|
||||
ChatLogger.debug('TimeWindowChatItem::componentDidUpdate::state', { ...this.state }, { ...prevState });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillMount::props', { ...this.props });
|
||||
ChatLogger.debug('TimeWindowChatItem::componentWillMount::state', { ...this.state });
|
||||
}
|
||||
|
||||
renderSystemMessage() {
|
||||
const {
|
||||
messages,
|
||||
chatAreaId,
|
||||
handleReadMessage,
|
||||
messageKey,
|
||||
intl,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.item} key={`time-window-chat-item-${messageKey}`}>
|
||||
<div className={styles.messages}>
|
||||
{messages.map(message => (
|
||||
message.text !== ''
|
||||
? (
|
||||
<MessageChatItem
|
||||
className={(message.id ? styles.systemMessage : styles.systemMessageNoBorder)}
|
||||
key={message.id ? message.id : _.uniqueId('id-')}
|
||||
text={intlMessages[message.text] ? intl.formatMessage(intlMessages[message.text]) : message.text }
|
||||
time={message.time}
|
||||
isSystemMessage={message.id ? true : false}
|
||||
chatAreaId={chatAreaId}
|
||||
handleReadMessage={handleReadMessage}
|
||||
/>
|
||||
) : null
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessageItem() {
|
||||
const {
|
||||
user,
|
||||
time,
|
||||
chatAreaId,
|
||||
scrollArea,
|
||||
intl,
|
||||
messages,
|
||||
messageKey,
|
||||
dispatch,
|
||||
chatId,
|
||||
read,
|
||||
} = this.props;
|
||||
|
||||
if (messages && messages[0].text.includes('bbb-published-poll-<br/>')) {
|
||||
return this.renderPollItem();
|
||||
}
|
||||
|
||||
const dateTime = new Date(time);
|
||||
const regEx = /<a[^>]+>/i;
|
||||
ChatLogger.debug('TimeWindowChatItem::renderMessageItem', this.props);
|
||||
const defaultAvatarString = user?.name?.toLowerCase().slice(0, 2) || " ";
|
||||
return (
|
||||
<div className={styles.item} key={`time-window-${messageKey}`}>
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.avatarWrapper}>
|
||||
<UserAvatar
|
||||
className={styles.avatar}
|
||||
color={user.color}
|
||||
moderator={user.isModerator}
|
||||
avatar={user.avatar}
|
||||
>
|
||||
{defaultAvatarString}
|
||||
</UserAvatar>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.meta}>
|
||||
<div className={user.isOnline ? styles.name : styles.logout}>
|
||||
<span>{user.name}</span>
|
||||
{user.isOnline
|
||||
? null
|
||||
: (
|
||||
<span className={styles.offline}>
|
||||
{`(${intl.formatMessage(intlMessages.offline)})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<time className={styles.time} dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</time>
|
||||
</div>
|
||||
<div className={styles.messages}>
|
||||
{messages.map(message => (
|
||||
<MessageChatItem
|
||||
className={(regEx.test(message.text) ? styles.hyperlink : styles.message)}
|
||||
key={message.id}
|
||||
text={message.text}
|
||||
time={message.time}
|
||||
chatAreaId={chatAreaId}
|
||||
dispatch={dispatch}
|
||||
read={message.read}
|
||||
handleReadMessage={(timestamp) => {
|
||||
if (!read) {
|
||||
dispatch({
|
||||
type: 'last_read_message_timestamp_changed',
|
||||
value: {
|
||||
chatId,
|
||||
timestamp,
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
scrollArea={scrollArea}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderPollItem() {
|
||||
const {
|
||||
user,
|
||||
time,
|
||||
intl,
|
||||
isDefaultPoll,
|
||||
messages,
|
||||
scrollArea,
|
||||
chatAreaId,
|
||||
lastReadMessageTime,
|
||||
handleReadMessage,
|
||||
} = this.props;
|
||||
|
||||
const dateTime = new Date(time);
|
||||
|
||||
return messages ? (
|
||||
<div className={styles.item} key={_.uniqueId('message-poll-item-')}>
|
||||
<div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
|
||||
<div className={styles.avatarWrapper}>
|
||||
<UserAvatar
|
||||
className={styles.avatar}
|
||||
color={user.color}
|
||||
moderator={user.isModerator}
|
||||
>
|
||||
{<Icon className={styles.isPoll} iconName="polling" />}
|
||||
</UserAvatar>
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.meta}>
|
||||
<div className={styles.name}>
|
||||
<span>{intl.formatMessage(intlMessages.pollResult)}</span>
|
||||
</div>
|
||||
<time className={styles.time} dateTime={dateTime}>
|
||||
<FormattedTime value={dateTime} />
|
||||
</time>
|
||||
</div>
|
||||
<MessageChatItem
|
||||
type="poll"
|
||||
className={cx(styles.message, styles.pollWrapper)}
|
||||
key={messages[0].id}
|
||||
text={messages[0].text}
|
||||
time={messages[0].time}
|
||||
chatAreaId={chatAreaId}
|
||||
lastReadMessageTime={lastReadMessageTime}
|
||||
handleReadMessage={handleReadMessage}
|
||||
scrollArea={scrollArea}
|
||||
color={user.color}
|
||||
isDefaultPoll={isDefaultPoll(messages[0].text.replace('bbb-published-poll-<br/>', ''))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
user,
|
||||
systemMessage,
|
||||
} = this.props;
|
||||
ChatLogger.debug('TimeWindowChatItem::render', {...this.props});
|
||||
if (systemMessage) {
|
||||
return this.renderSystemMessage();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
{this.renderMessageItem()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TimeWindowChatItem.propTypes = propTypes;
|
||||
TimeWindowChatItem.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(TimeWindowChatItem);
|
@ -0,0 +1,47 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import TimeWindowChatItem from './component';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import ChatService from '../../service';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
|
||||
const isDefaultPoll = (pollText) => {
|
||||
const pollValue = pollText.replace(/<br\/>|[ :|%\n\d+]/g, '');
|
||||
switch (pollValue) {
|
||||
case 'A': case 'AB': case 'ABC': case 'ABCD':
|
||||
case 'ABCDE': case 'YesNo': case 'TrueFalse':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
export default class TimeWindowChatItemContainer extends PureComponent {
|
||||
render() {
|
||||
ChatLogger.debug('TimeWindowChatItemContainer::render', { ...this.props });
|
||||
const messages = this.props.message.content;
|
||||
|
||||
const user = this.props.message.sender;
|
||||
const messageKey = this.props.message.key;
|
||||
const time = this.props.message.time;
|
||||
|
||||
return (
|
||||
<TimeWindowChatItem
|
||||
{
|
||||
...{
|
||||
read: this.props.message.read,
|
||||
messages,
|
||||
isDefaultPoll,
|
||||
user,
|
||||
time,
|
||||
systemMessage: this.props.messageId.startsWith(SYSTEM_CHAT_TYPE) || !user,
|
||||
messageKey,
|
||||
handleReadMessage: ChatService.updateUnreadMessage,
|
||||
...this.props,
|
||||
}
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,224 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import _ from 'lodash';
|
||||
import fastdom from 'fastdom';
|
||||
import { defineMessages, 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
|
||||
&& rect.left >= 0
|
||||
&& rect.bottom <= (window.innerHeight || document.documentElement.clientHeight)
|
||||
&& rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
||||
);
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
legendTitle: {
|
||||
id: 'app.polling.pollingTitle',
|
||||
description: 'heading for chat poll legend',
|
||||
},
|
||||
});
|
||||
|
||||
class MessageChatItem extends PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.ticking = false;
|
||||
|
||||
this.handleMessageInViewport = _.debounce(this.handleMessageInViewport.bind(this), 50);
|
||||
|
||||
this.renderPollListItem = this.renderPollListItem.bind(this);
|
||||
}
|
||||
|
||||
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) && !read) {
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderPollListItem() {
|
||||
const {
|
||||
intl,
|
||||
text,
|
||||
className,
|
||||
color,
|
||||
isDefaultPoll,
|
||||
} = this.props;
|
||||
|
||||
const formatBoldBlack = s => s.bold().fontcolor('black');
|
||||
|
||||
let _text = text.replace('bbb-published-poll-<br/>', '');
|
||||
|
||||
if (!isDefaultPoll) {
|
||||
const entries = _text.split('<br/>');
|
||||
const options = [];
|
||||
entries.map((e) => { options.push([e.slice(0, e.indexOf(':'))]); return e; });
|
||||
options.map((o, idx) => {
|
||||
_text = formatBoldBlack(_text.replace(o, idx + 1));
|
||||
return _text;
|
||||
});
|
||||
_text += formatBoldBlack(`<br/><br/>${intl.formatMessage(intlMessages.legendTitle)}`);
|
||||
options.map((o, idx) => { _text += `<br/>${idx + 1}: ${o}`; return _text; });
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
style={{ borderLeft: `3px ${color} solid` }}
|
||||
ref={(ref) => { this.text = ref; }}
|
||||
dangerouslySetInnerHTML={{ __html: isDefaultPoll ? formatBoldBlack(_text) : _text }}
|
||||
data-test="chatPollMessageText"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
text,
|
||||
type,
|
||||
className,
|
||||
isSystemMessage,
|
||||
chatUserMessageItem,
|
||||
} = this.props;
|
||||
ChatLogger.debug('MessageChatItem::render', this.props);
|
||||
if (type === 'poll') return this.renderPollListItem();
|
||||
|
||||
return (
|
||||
<p
|
||||
className={className}
|
||||
ref={(ref) => { this.text = ref; }}
|
||||
dangerouslySetInnerHTML={{ __html: text }}
|
||||
data-test={isSystemMessage ? 'chatWelcomeMessageText' : chatUserMessageItem ? 'chatUserMessageText' : 'chatClearMessageText'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MessageChatItem.propTypes = propTypes;
|
||||
MessageChatItem.defaultProps = defaultProps;
|
||||
|
||||
export default injectIntl(MessageChatItem);
|
@ -0,0 +1,173 @@
|
||||
@import "/imports/ui/stylesheets/variables/placeholders";
|
||||
|
||||
:root {
|
||||
--systemMessage-background-color: #F9FBFC;
|
||||
--systemMessage-border-color: #C5CDD4;
|
||||
--systemMessage-font-color: var(--color-dark-grey);
|
||||
--chat-poll-margin-sm: .5rem;
|
||||
}
|
||||
|
||||
.item {
|
||||
padding: calc(var(--line-height-computed) / 4) 0 calc(var(--line-height-computed) / 2) 0;
|
||||
font-size: var(--font-size-base);
|
||||
pointer-events: auto;
|
||||
[dir="rtl"] & {
|
||||
direction: rtl;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
margin:var(--border-size) 0 0 var(--border-size);
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: var(--border-size) var(--border-size) 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.systemMessage {
|
||||
background: var(--systemMessage-background-color);
|
||||
border: 1px solid var(--systemMessage-border-color);
|
||||
border-radius: var(--border-radius);
|
||||
font-weight: var(--btn-font-weight);
|
||||
padding: var(--font-size-base);
|
||||
//margin-bottom: var(--line-height-computed);
|
||||
color: var(--systemMessage-font-color);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.systemMessageNoBorder {
|
||||
color: var(--systemMessage-font-color);
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.avatarWrapper {
|
||||
flex-basis: 2.25rem;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
margin: 0 calc(var(--line-height-computed) / 2) 0 0;
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 0 0 calc(var(--line-height-computed) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
font-size: .85rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
overflow-x: hidden;
|
||||
width: calc(100% - 1.7rem);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row;
|
||||
line-height: 1.35;
|
||||
|
||||
& + .message {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.name,
|
||||
.logout {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
|
||||
:first-child {
|
||||
@extend %text-elipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
.logout {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.offline {
|
||||
color: var(--color-gray-light);
|
||||
font-weight: 100;
|
||||
text-transform: lowercase;
|
||||
font-style: italic;
|
||||
font-size: 90%;
|
||||
line-height: 1;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.time {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: 3.5rem;
|
||||
color: var(--color-gray-light);
|
||||
text-transform: uppercase;
|
||||
font-size: 75%;
|
||||
margin: 0 0 0 calc(var(--line-height-computed) / 2);
|
||||
|
||||
[dir="rtl"] & {
|
||||
margin: 0 calc(var(--line-height-computed) / 2) 0 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
vertical-align: sub;
|
||||
}
|
||||
}
|
||||
|
||||
.messages {
|
||||
> .message:first-child {
|
||||
margin-top: calc(var(--line-height-computed) / 4);
|
||||
}
|
||||
}
|
||||
|
||||
.message, .hyperlink {
|
||||
flex: 1;
|
||||
margin-top: calc(var(--line-height-computed) / 3);
|
||||
margin-bottom: 0;
|
||||
color: var(--color-text);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.hyperlink {
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.isPoll {
|
||||
bottom: var(--border-size-large);
|
||||
}
|
||||
|
||||
.pollWrapper {
|
||||
background: var(--systemMessage-background-color);
|
||||
border: solid 1px var(--color-gray-lighter);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--chat-poll-margin-sm);
|
||||
padding-left: 1rem;
|
||||
margin-top: var(--chat-poll-margin-sm) !important;
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import _ from 'lodash';
|
||||
import { ChatContext, ACTIONS } from './context';
|
||||
import { UsersContext } from '../users-context/context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
let usersData = {};
|
||||
let messageQueue = [];
|
||||
const Adapter = () => {
|
||||
const usingChatContext = useContext(ChatContext);
|
||||
const { dispatch } = usingChatContext;
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { users } = usingUsersContext;
|
||||
ChatLogger.trace('chatAdapter::body::users', users);
|
||||
|
||||
useEffect(() => {
|
||||
usersData = users;
|
||||
}, [usingUsersContext]);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: listen to websocket message to avoid full list comparsion
|
||||
const throttledDispatch = _.throttle(() => {
|
||||
const dispatchedMessageQueue = [...messageQueue];
|
||||
messageQueue = [];
|
||||
dispatch({
|
||||
type: ACTIONS.ADDED,
|
||||
value: dispatchedMessageQueue,
|
||||
});
|
||||
}, 1000, { trailing: true, leading: true });
|
||||
|
||||
Meteor.connection._stream.socket.addEventListener('message', (msg) => {
|
||||
if (msg.data.indexOf('{"msg":"added","collection":"group-chat-msg"') != -1) {
|
||||
const parsedMsg = JSON.parse(msg.data);
|
||||
if (parsedMsg.msg === 'added') {
|
||||
messageQueue.push({
|
||||
msg: parsedMsg.fields,
|
||||
senderData: usersData[parsedMsg.fields.sender.id],
|
||||
});
|
||||
throttledDispatch();
|
||||
}
|
||||
}
|
||||
if (msg.data.indexOf('{"msg":"removed","collection":"group-chat-msg"') != -1) {
|
||||
dispatch({
|
||||
type: ACTIONS.REMOVED,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Adapter;
|
@ -0,0 +1,337 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
|
||||
import Users from '/imports/api/users';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
import { _ } from 'lodash';
|
||||
|
||||
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 SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
export const ACTIONS = {
|
||||
TEST: 'test',
|
||||
ADDED: 'added',
|
||||
CHANGED: 'changed',
|
||||
REMOVED: 'removed',
|
||||
USER_STATUS_CHANGED: 'user_status_changed',
|
||||
LAST_READ_MESSAGE_TIMESTAMP_CHANGED: 'last_read_message_timestamp_changed',
|
||||
INIT: 'initial_structure',
|
||||
};
|
||||
|
||||
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
|
||||
|
||||
export const getGroupingTime = () => Meteor.settings.public.chat.grouping_messages_window;
|
||||
export const getGroupChatId = () => Meteor.settings.public.chat.public_group_id;
|
||||
export const getLoginTime = () => (Users.findOne({ userId: Auth.userID }) || {}).loginTime || 0;
|
||||
|
||||
const generateTimeWindow = (timestamp) => {
|
||||
const groupingTime = getGroupingTime();
|
||||
dateInMilliseconds = Math.floor(timestamp);
|
||||
groupIndex = Math.floor(dateInMilliseconds / groupingTime)
|
||||
date = groupIndex * 30000;
|
||||
return date;
|
||||
}
|
||||
|
||||
export const ChatContext = createContext();
|
||||
|
||||
const generateStateWithNewMessage = ({ msg, senderData }, state) => {
|
||||
|
||||
const timeWindow = generateTimeWindow(msg.timestamp);
|
||||
const userId = msg.sender.id;
|
||||
const keyName = userId + '-' + timeWindow;
|
||||
const msgBuilder = ({msg, senderData}, chat) => {
|
||||
const msgTimewindow = generateTimeWindow(msg.timestamp);
|
||||
const key = msg.sender.id + '-' + msgTimewindow;
|
||||
const chatIndex = chat?.chatIndexes[key];
|
||||
const {
|
||||
_id,
|
||||
...restMsg
|
||||
} = msg;
|
||||
const senderInfo = {
|
||||
id: senderData?.userId || msg.sender.id,
|
||||
avatar: senderData?.avatar,
|
||||
color: senderData?.color ,
|
||||
isModerator: senderData?.role === ROLE_MODERATOR, // TODO: get isModerator from message
|
||||
name: senderData?.name || msg.sender.name,
|
||||
isOnline: !!senderData,
|
||||
};
|
||||
|
||||
const indexValue = chatIndex ? (chatIndex + 1) : 1;
|
||||
const messageKey = key + '-' + indexValue;
|
||||
const tempGroupMessage = {
|
||||
[messageKey]: {
|
||||
...restMsg,
|
||||
key: messageKey,
|
||||
lastTimestamp: msg.timestamp,
|
||||
read: msg.chatId === PUBLIC_CHAT_KEY && msg.timestamp <= getLoginTime() ? true : false,
|
||||
content: [
|
||||
{ id: msg.id, name: msg.sender.name, text: msg.message, time: msg.timestamp },
|
||||
],
|
||||
sender: senderInfo,
|
||||
}
|
||||
};
|
||||
|
||||
return [tempGroupMessage, msg.sender, indexValue];
|
||||
};
|
||||
|
||||
let stateMessages = state[msg.chatId];
|
||||
|
||||
if (!stateMessages) {
|
||||
if (msg.chatId === getGroupChatId()) {
|
||||
state[msg.chatId] = {
|
||||
count: 0,
|
||||
chatIndexes: {},
|
||||
preJoinMessages: {},
|
||||
posJoinMessages: {},
|
||||
unreadTimeWindows: new Set(),
|
||||
unreadCount: 0,
|
||||
};
|
||||
} else {
|
||||
state[msg.chatId] = {
|
||||
count: 0,
|
||||
lastSender: '',
|
||||
chatIndexes: {},
|
||||
messageGroups: {},
|
||||
unreadTimeWindows: new Set(),
|
||||
unreadCount: 0,
|
||||
};
|
||||
stateMessages = state[msg.chatId];
|
||||
}
|
||||
|
||||
stateMessages = state[msg.chatId];
|
||||
}
|
||||
|
||||
const forPublicChat = msg.timestamp < getLoginTime() ? stateMessages.preJoinMessages : stateMessages.posJoinMessages;
|
||||
const forPrivateChat = stateMessages.messageGroups;
|
||||
const messageGroups = msg.chatId === getGroupChatId() ? forPublicChat : forPrivateChat;
|
||||
const timewindowIndex = stateMessages.chatIndexes[keyName];
|
||||
const groupMessage = messageGroups[keyName + '-' + timewindowIndex];
|
||||
|
||||
if (!groupMessage || (groupMessage && groupMessage.sender.id !== stateMessages.lastSender.id)) {
|
||||
|
||||
const [tempGroupMessage, sender, newIndex] = msgBuilder({msg, senderData}, stateMessages);
|
||||
stateMessages.lastSender = sender;
|
||||
stateMessages.chatIndexes[keyName] = newIndex;
|
||||
stateMessages.lastTimewindow = keyName + '-' + newIndex;
|
||||
ChatLogger.trace('ChatContext::formatMsg::msgBuilder::tempGroupMessage', tempGroupMessage);
|
||||
|
||||
const messageGroupsKeys = Object.keys(tempGroupMessage);
|
||||
messageGroupsKeys.forEach(key => {
|
||||
messageGroups[key] = tempGroupMessage[key];
|
||||
const message = tempGroupMessage[key];
|
||||
if (message.sender.id !== Auth.userID && !message.id.startsWith(SYSTEM_CHAT_TYPE)) {
|
||||
stateMessages.unreadTimeWindows.add(key);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (groupMessage) {
|
||||
if (groupMessage.sender.id === stateMessages.lastSender.id) {
|
||||
const previousMessage = msg.timestamp <= getLoginTime();
|
||||
const timeWindowKey = keyName + '-' + stateMessages.chatIndexes[keyName];
|
||||
messageGroups[timeWindowKey] = {
|
||||
...groupMessage,
|
||||
lastTimestamp: msg.timestamp,
|
||||
read: previousMessage ? true : false,
|
||||
content: [
|
||||
...groupMessage.content,
|
||||
{ id: msg.id, name: groupMessage.sender.name, text: msg.message, time: msg.timestamp }
|
||||
],
|
||||
};
|
||||
if (!previousMessage && groupMessage.sender.id !== Auth.userID) {
|
||||
stateMessages.unreadTimeWindows.add(timeWindowKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.TEST: {
|
||||
ChatLogger.debug(ACTIONS.TEST);
|
||||
return {
|
||||
...state,
|
||||
...action.value,
|
||||
};
|
||||
}
|
||||
case ACTIONS.ADDED: {
|
||||
ChatLogger.debug(ACTIONS.ADDED);
|
||||
|
||||
const batchMsgs = action.value;
|
||||
const closedChatsToOpen = new Set();
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
const loginTime = getLoginTime();
|
||||
const newState = batchMsgs.reduce((acc, i)=> {
|
||||
const message = i.msg;
|
||||
const chatId = message.chatId;
|
||||
if (
|
||||
chatId !== PUBLIC_GROUP_CHAT_KEY
|
||||
&& message.timestamp > loginTime
|
||||
&& currentClosedChats.includes(chatId) ){
|
||||
closedChatsToOpen.add(chatId)
|
||||
}
|
||||
|
||||
return generateStateWithNewMessage(i, acc);
|
||||
}, state);
|
||||
|
||||
if (closedChatsToOpen.size) {
|
||||
const closedChats = currentClosedChats.filter(chatId => !closedChatsToOpen.has(chatId));
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, closedChats);
|
||||
}
|
||||
// const newState = generateStateWithNewMessage(action.value, state);
|
||||
return {...newState};
|
||||
}
|
||||
case ACTIONS.CHANGED: {
|
||||
return {
|
||||
...state,
|
||||
...action.value,
|
||||
};
|
||||
}
|
||||
case ACTIONS.REMOVED: {
|
||||
ChatLogger.debug(ACTIONS.REMOVED);
|
||||
if (state[PUBLIC_GROUP_CHAT_KEY]){
|
||||
state[PUBLIC_GROUP_CHAT_KEY] = {
|
||||
count: 0,
|
||||
lastSender: '',
|
||||
chatIndexes: {},
|
||||
preJoinMessages: {},
|
||||
posJoinMessages: {},
|
||||
unreadTimeWindows: new Set(),
|
||||
unreadCount: 0,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case ACTIONS.USER_STATUS_CHANGED: {
|
||||
ChatLogger.debug(ACTIONS.USER_STATUS_CHANGED);
|
||||
const newState = {
|
||||
...state,
|
||||
};
|
||||
const affectedChats = [];
|
||||
// select all groups of users
|
||||
Object.keys(newState).forEach(chatId => {
|
||||
const affectedGroups = Object.keys(newState[chatId])
|
||||
.filter(groupId => groupId.startsWith(action.value.userId));
|
||||
if (affectedGroups.length) {
|
||||
affectedChats[chatId] = affectedGroups;
|
||||
}
|
||||
});
|
||||
|
||||
//Apply change to new state
|
||||
Object.keys(affectedChats).forEach((chatId) => {
|
||||
// force new reference
|
||||
newState[chatId] = {
|
||||
...newState[chatId]
|
||||
};
|
||||
//Apply change
|
||||
affectedChats[chatId].forEach(groupId => {
|
||||
newState[chatId][groupId] = {
|
||||
...newState[chatId][groupId] //TODO: sort by time (for incromental loading)
|
||||
};
|
||||
newState[chatId][groupId].status = action.value.status;
|
||||
});
|
||||
});
|
||||
return newState
|
||||
}
|
||||
case ACTIONS.LAST_READ_MESSAGE_TIMESTAMP_CHANGED: {
|
||||
ChatLogger.debug(ACTIONS.LAST_READ_MESSAGE_TIMESTAMP_CHANGED);
|
||||
const { timestamp, chatId } = action.value;
|
||||
const newState = {
|
||||
...state,
|
||||
};
|
||||
const selectedChatId = chatId === PUBLIC_CHAT_KEY ? PUBLIC_GROUP_CHAT_KEY : chatId;
|
||||
const chat = state[selectedChatId];
|
||||
['posJoinMessages','preJoinMessages','messageGroups'].forEach( messageGroupName => {
|
||||
const messageGroup = chat[messageGroupName];
|
||||
if (messageGroup){
|
||||
const timeWindowsids = Object.keys(messageGroup);
|
||||
timeWindowsids.forEach( timeWindowId => {
|
||||
const timeWindow = messageGroup[timeWindowId];
|
||||
if(timeWindow) {
|
||||
if (!timeWindow.read) {
|
||||
if (timeWindow.lastTimestamp <= timestamp){
|
||||
newState[selectedChatId].unreadTimeWindows.delete(timeWindowId);
|
||||
|
||||
newState[selectedChatId][messageGroupName][timeWindowId] = {
|
||||
...timeWindow,
|
||||
read: true,
|
||||
};
|
||||
|
||||
|
||||
newState[selectedChatId] = {
|
||||
...newState[selectedChatId],
|
||||
};
|
||||
newState[selectedChatId][messageGroupName] = {
|
||||
...newState[selectedChatId][messageGroupName],
|
||||
};
|
||||
newState[chatId === PUBLIC_CHAT_KEY ? PUBLIC_GROUP_CHAT_KEY : chatId][messageGroupName][timeWindowId] = {
|
||||
...newState[selectedChatId][messageGroupName][timeWindowId],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return newState;
|
||||
}
|
||||
case ACTIONS.INIT: {
|
||||
ChatLogger.debug(ACTIONS.INIT);
|
||||
const { chatId } = action;
|
||||
const newState = { ...state };
|
||||
|
||||
if (!newState[chatId]){
|
||||
newState[chatId] = {
|
||||
count: 0,
|
||||
lastSender: '',
|
||||
chatIndexes: {},
|
||||
messageGroups: {},
|
||||
unreadTimeWindows: new Set(),
|
||||
unreadCount: 0,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ChatContextProvider = (props) => {
|
||||
const [chatContextState, chatContextDispatch] = useReducer(reducer, {});
|
||||
ChatLogger.debug('dispatch', chatContextDispatch);
|
||||
return (
|
||||
<ChatContext.Provider value={
|
||||
{
|
||||
dispatch: chatContextDispatch,
|
||||
chats: chatContextState,
|
||||
...props,
|
||||
}
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</ChatContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export const ContextConsumer = Component => props => (
|
||||
<ChatContext.Consumer>
|
||||
{contexts => <Component {...props} {...contexts} />}
|
||||
</ChatContext.Consumer>
|
||||
);
|
||||
|
||||
export default {
|
||||
ContextConsumer,
|
||||
ChatContextProvider,
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import React, { 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 alreadyDispatched = new Set();
|
||||
const notDispatchedCount = { count: 100 };
|
||||
// TODO: listen to websocket message to avoid full list comparsion
|
||||
const diffAndDispatch = () => {
|
||||
setTimeout(() => {
|
||||
const groupChatCursor = GroupChat.find({}, { reactive: false }).fetch();
|
||||
const notDispatched = groupChatCursor.filter(objMsg => !alreadyDispatched.has(objMsg._id));
|
||||
notDispatchedCount.count = notDispatched.length;
|
||||
|
||||
notDispatched.forEach((groupChat) => {
|
||||
dispatch({
|
||||
type: ACTIONS.ADDED,
|
||||
value: {
|
||||
groupChat,
|
||||
},
|
||||
});
|
||||
|
||||
alreadyDispatched.add(groupChat._id);
|
||||
});
|
||||
diffAndDispatch();
|
||||
}, notDispatchedCount.count >= 10 ? 1000 : 500);
|
||||
};
|
||||
diffAndDispatch();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Adapter;
|
@ -0,0 +1,76 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
export const ACTIONS = {
|
||||
TEST: 'test',
|
||||
ADDED: 'added',
|
||||
CHANGED: 'changed',
|
||||
REMOVED: 'removed',
|
||||
};
|
||||
|
||||
export const GroupChatContext = createContext();
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.TEST: {
|
||||
return {
|
||||
...state,
|
||||
...action.value,
|
||||
};
|
||||
}
|
||||
|
||||
case ACTIONS.ADDED:
|
||||
case ACTIONS.CHANGED: {
|
||||
ChatLogger.debug('GroupChatContextProvider::reducer::added', { ...action });
|
||||
const { groupChat } = action.value;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
[groupChat.chatId]: {
|
||||
...groupChat,
|
||||
},
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
case ACTIONS.REMOVED: {
|
||||
ChatLogger.debug('GroupChatContextProvider::reducer::removed', { ...action });
|
||||
|
||||
return state;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const GroupChatContextProvider = (props) => {
|
||||
const [groupChatContextState, groupChatContextDispatch] = useReducer(reducer, {});
|
||||
ChatLogger.debug('UsersContextProvider::groupChatContextState', groupChatContextState);
|
||||
return (
|
||||
<GroupChatContext.Provider value={
|
||||
{
|
||||
...props,
|
||||
dispatch: groupChatContextDispatch,
|
||||
groupChat: { ...groupChatContextState },
|
||||
}
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</GroupChatContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const GroupChatContextConsumer = Component => props => (
|
||||
<GroupChatContext.Consumer>
|
||||
{contexts => <Component {...props} {...contexts} />}
|
||||
</GroupChatContext.Consumer>
|
||||
);
|
||||
|
||||
export default {
|
||||
GroupChatContextConsumer,
|
||||
GroupChatContextProvider,
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
import { useContext, useEffect } from 'react';
|
||||
import Users from '/imports/api/users';
|
||||
import { UsersContext, ACTIONS } from './context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
const Adapter = () => {
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const { dispatch } = usingUsersContext;
|
||||
useEffect(() => {
|
||||
const usersCursor = Users.find({}, { sort: { timestamp: 1 } });
|
||||
usersCursor.observe({
|
||||
added: (obj) => {
|
||||
ChatLogger.debug("usersAdapter::observe::added", obj);
|
||||
dispatch({
|
||||
type: ACTIONS.ADDED,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
changed: (obj) => {
|
||||
dispatch({
|
||||
type: ACTIONS.CHANGED,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
removed: (obj) => {
|
||||
dispatch({
|
||||
type: ACTIONS.REMOVED,
|
||||
value: {
|
||||
user: obj,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default Adapter;
|
@ -0,0 +1,83 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
export const ACTIONS = {
|
||||
TEST: 'test',
|
||||
ADDED: 'added',
|
||||
CHANGED: 'changed',
|
||||
REMOVED: 'removed',
|
||||
};
|
||||
|
||||
export const UsersContext = createContext();
|
||||
|
||||
const reducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case ACTIONS.TEST: {
|
||||
return {
|
||||
...state,
|
||||
...action.value,
|
||||
};
|
||||
}
|
||||
|
||||
case ACTIONS.ADDED:
|
||||
case ACTIONS.CHANGED: {
|
||||
ChatLogger.debug('UsersContextProvider::reducer::added', { ...action });
|
||||
const { user } = action.value;
|
||||
|
||||
const newState = {
|
||||
...state,
|
||||
[user.userId]: {
|
||||
...user,
|
||||
},
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
case ACTIONS.REMOVED: {
|
||||
ChatLogger.debug('UsersContextProvider::reducer::removed', { ...action });
|
||||
|
||||
const { user } = action.value;
|
||||
if (state[user.userId]) {
|
||||
const newState = { ...state };
|
||||
delete newState[user.userId];
|
||||
return newState;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const UsersContextProvider = (props) => {
|
||||
const [usersContextState, usersContextDispatch] = useReducer(reducer, {});
|
||||
ChatLogger.debug('UsersContextProvider::usersContextState', usersContextState);
|
||||
return (
|
||||
<UsersContext.Provider value={
|
||||
{
|
||||
...props,
|
||||
dispatch: usersContextDispatch,
|
||||
users: { ...usersContextState },
|
||||
}
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
</UsersContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const UsersContextConsumer = Component => props => (
|
||||
<UsersContext.Consumer>
|
||||
{contexts => <Component {...props} {...contexts} />}
|
||||
</UsersContext.Consumer>
|
||||
);
|
||||
|
||||
export default {
|
||||
UsersContextConsumer,
|
||||
UsersContextProvider,
|
||||
};
|
||||
|
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { ChatContextProvider } from '/imports/ui/components/components-data/chat-context/context';
|
||||
import { UsersContextProvider } from '/imports/ui/components/components-data/users-context/context';
|
||||
import { GroupChatContextProvider } from '/imports/ui/components/components-data/group-chat-context/context';
|
||||
|
||||
|
||||
const providersList = [
|
||||
ChatContextProvider,
|
||||
GroupChatContextProvider,
|
||||
UsersContextProvider,
|
||||
];
|
||||
|
||||
const ContextProvidersComponent = props => providersList.reduce((acc, Component) => (
|
||||
<Component>
|
||||
{acc}
|
||||
</Component>), props.children);
|
||||
|
||||
export default ContextProvidersComponent;
|
@ -8,6 +8,7 @@ import Button from '/imports/ui/components/button/component';
|
||||
import Toggle from '/imports/ui/components/switch/component';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import { withLayoutConsumer } from '/imports/ui/components/layout/context';
|
||||
import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
modalClose: {
|
||||
@ -49,6 +50,7 @@ class DebugWindow extends Component {
|
||||
|
||||
this.state = {
|
||||
showDebugWindow: false,
|
||||
logLevel: ChatLogger.getLogLevel(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -87,7 +89,8 @@ class DebugWindow extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { showDebugWindow } = this.state;
|
||||
const { showDebugWindow, logLevel } = this.state;
|
||||
const chatLoggerLevelsNames = Object.keys(ChatLogger.levels);
|
||||
|
||||
if (!DEBUG_WINDOW_ENABLED || !showDebugWindow) return false;
|
||||
|
||||
@ -191,6 +194,41 @@ class DebugWindow extends Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.cell}>
|
||||
testando o chatLogger levels:
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellContent}>
|
||||
<select
|
||||
style={{ marginRight: '1rem' }}
|
||||
onChange={(ev) => {
|
||||
this.setState({
|
||||
logLevel: ev.target.value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{
|
||||
chatLoggerLevelsNames.map((i, index) => {
|
||||
return (<option key={`${i}-${index}`}>{i}</option>);
|
||||
})
|
||||
}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
disabled={logLevel === ChatLogger.getLogLevel()}
|
||||
onClick={() => {
|
||||
ChatLogger.setLogLevel(logLevel);
|
||||
this.setState({
|
||||
logLevel: ChatLogger.getLogLevel(),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Aplicar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -41,7 +41,8 @@ export default withTracker(() => {
|
||||
}
|
||||
|
||||
const checkUnreadMessages = () => {
|
||||
const activeChats = userListService.getActiveChats();
|
||||
const activeChats = [];
|
||||
// userListService.getActiveChats()
|
||||
const hasUnreadMessages = activeChats
|
||||
.filter(chat => chat.userId !== Session.get('idChatOpen'))
|
||||
.some(chat => chat.unreadCounter > 0);
|
||||
|
@ -70,26 +70,6 @@ export default withTracker(() => {
|
||||
subscriptionsHandlers.push(Meteor.subscribe('breakouts', currentUser.role, subscriptionErrorHandler));
|
||||
}
|
||||
|
||||
let groupChatMessageHandler = {};
|
||||
|
||||
if (CHAT_ENABLED) {
|
||||
const chats = GroupChat.find({
|
||||
$or: [
|
||||
{
|
||||
meetingId,
|
||||
access: PUBLIC_CHAT_TYPE,
|
||||
chatId: { $ne: PUBLIC_GROUP_CHAT_ID },
|
||||
},
|
||||
{ meetingId, users: { $all: [requesterUserId] } },
|
||||
],
|
||||
}).fetch();
|
||||
|
||||
const chatIds = chats.map(chat => chat.chatId);
|
||||
|
||||
groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatIds, subscriptionErrorHandler);
|
||||
subscriptionsHandlers.push(groupChatMessageHandler);
|
||||
}
|
||||
|
||||
const annotationsHandler = Meteor.subscribe('annotations', {
|
||||
onReady: () => {
|
||||
const activeTextShapeId = AnnotationsTextService.activeTextShapeId();
|
||||
@ -108,6 +88,23 @@ export default withTracker(() => {
|
||||
|
||||
subscriptionsHandlers = subscriptionsHandlers.filter(obj => obj);
|
||||
const ready = subscriptionsHandlers.every(handler => handler.ready());
|
||||
let groupChatMessageHandler = {};
|
||||
|
||||
if (CHAT_ENABLED && ready) {
|
||||
const chats = GroupChat.find({
|
||||
$or: [
|
||||
{
|
||||
meetingId,
|
||||
access: PUBLIC_CHAT_TYPE,
|
||||
chatId: { $ne: PUBLIC_GROUP_CHAT_ID },
|
||||
},
|
||||
{ meetingId, users: { $all: [requesterUserId] } },
|
||||
],
|
||||
}).fetch();
|
||||
|
||||
const chatIds = chats.map(chat => chat.chatId);
|
||||
groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatIds, subscriptionErrorHandler);
|
||||
}
|
||||
|
||||
return {
|
||||
subscriptionsReady: ready,
|
||||
|
@ -1,14 +1,25 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Session } from 'meteor/session';
|
||||
import _ from 'lodash';
|
||||
import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
|
||||
import { styles } from './styles';
|
||||
import ChatAvatar from './chat-avatar/component';
|
||||
import ChatIcon from './chat-icon/component';
|
||||
import ChatUnreadCounter from './chat-unread-messages/component';
|
||||
|
||||
const DEBOUNCE_TIME = 1000;
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
||||
|
||||
let globalAppplyStateToProps = ()=>{};
|
||||
|
||||
const throttledFunc = _.debounce(() => {
|
||||
globalAppplyStateToProps();
|
||||
}, DEBOUNCE_TIME, { trailing: true, leading: false });
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
titlePublic: {
|
||||
id: 'app.chat.titlePublic',
|
||||
@ -71,10 +82,29 @@ const ChatListItem = (props) => {
|
||||
chatPanelOpen,
|
||||
} = props;
|
||||
|
||||
|
||||
|
||||
const isCurrentChat = chat.userId === activeChatId && chatPanelOpen;
|
||||
const linkClasses = {};
|
||||
linkClasses[styles.active] = isCurrentChat;
|
||||
|
||||
const [stateUreadCount, setStateUreadCount] = useState(0);
|
||||
|
||||
if (chat.unreadCounter !== stateUreadCount && (stateUreadCount < chat.unreadCounter)) {
|
||||
globalAppplyStateToProps = () => {
|
||||
setStateUreadCount(chat.unreadCounter);
|
||||
};
|
||||
throttledFunc();
|
||||
} else if (chat.unreadCounter !== stateUreadCount && (stateUreadCount > chat.unreadCounter)) {
|
||||
setStateUreadCount(chat.unreadCounter);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (chat.userId !== PUBLIC_CHAT_KEY && chat.userId === activeChatId) {
|
||||
Session.set('idChatOpen', chat.chatId);
|
||||
}
|
||||
}, [activeChatId]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-test="chatButton"
|
||||
@ -83,7 +113,7 @@ const ChatListItem = (props) => {
|
||||
aria-expanded={isCurrentChat}
|
||||
tabIndex={tabIndex}
|
||||
accessKey={isPublicChat(chat) ? TOGGLE_CHAT_PUB_AK : null}
|
||||
onClick={() => handleClickToggleChat(chat.userId)}
|
||||
onClick={() => handleClickToggleChat(chat.chatId)}
|
||||
id="chat-toggle-button"
|
||||
aria-label={isPublicChat(chat) ? intl.formatMessage(intlMessages.titlePublic) : chat.name}
|
||||
>
|
||||
@ -110,10 +140,10 @@ const ChatListItem = (props) => {
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{(chat.unreadCounter > 0)
|
||||
{(stateUreadCount > 0)
|
||||
? (
|
||||
<ChatUnreadCounter
|
||||
counter={chat.unreadCounter}
|
||||
counter={stateUreadCount}
|
||||
/>
|
||||
)
|
||||
: null}
|
||||
|
@ -7,7 +7,6 @@ import CustomLogo from './custom-logo/component';
|
||||
import UserContentContainer from './user-list-content/container';
|
||||
|
||||
const propTypes = {
|
||||
activeChats: PropTypes.arrayOf(String).isRequired,
|
||||
compact: PropTypes.bool,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
@ -28,7 +27,6 @@ class UserList extends PureComponent {
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
activeChats,
|
||||
compact,
|
||||
setEmojiStatus,
|
||||
isPublicChat,
|
||||
@ -50,7 +48,6 @@ class UserList extends PureComponent {
|
||||
{<UserContentContainer
|
||||
{...{
|
||||
intl,
|
||||
activeChats,
|
||||
compact,
|
||||
setEmojiStatus,
|
||||
isPublicChat,
|
||||
|
@ -6,7 +6,6 @@ import Service from './service';
|
||||
import UserList from './component';
|
||||
|
||||
const propTypes = {
|
||||
activeChats: PropTypes.arrayOf(String).isRequired,
|
||||
isPublicChat: PropTypes.func.isRequired,
|
||||
setEmojiStatus: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
@ -17,9 +16,8 @@ const UserListContainer = props => <UserList {...props} />;
|
||||
|
||||
UserListContainer.propTypes = propTypes;
|
||||
|
||||
export default withTracker(({ chatID, compact }) => ({
|
||||
export default withTracker(({ compact }) => ({
|
||||
hasBreakoutRoom: Service.hasBreakoutRoom(),
|
||||
activeChats: Service.getActiveChats(chatID),
|
||||
isPublicChat: Service.isPublicChat,
|
||||
setEmojiStatus: Service.setEmojiStatus,
|
||||
roving: Service.roving,
|
||||
|
@ -1,11 +1,9 @@
|
||||
import Users from '/imports/api/users';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
import GroupChat from '/imports/api/group-chat';
|
||||
import { GroupChatMsg } from '/imports/api/group-chat-msg';
|
||||
import Breakouts from '/imports/api/breakouts/';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import UnreadMessages from '/imports/ui/services/unread-messages';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import { EMOJI_STATUSES } from '/imports/utils/statuses';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
@ -216,82 +214,76 @@ const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID }
|
||||
|
||||
const isMe = userId => userId === Auth.userID;
|
||||
|
||||
const getActiveChats = (chatID) => {
|
||||
const privateChat = GroupChat
|
||||
.find({ users: { $all: [Auth.userID] } })
|
||||
.fetch()
|
||||
.map(chat => chat.chatId);
|
||||
|
||||
const filter = {
|
||||
chatId: { $ne: PUBLIC_GROUP_CHAT_ID },
|
||||
};
|
||||
const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
|
||||
|
||||
if (privateChat) {
|
||||
filter.chatId = { $in: privateChat };
|
||||
}
|
||||
if (_.isEmpty(groupChats) || _.isEmpty(users)) return [];
|
||||
|
||||
const chatIds = Object.keys(groupChats);
|
||||
const lastTimeWindows = chatIds.reduce((acc, chatId) => {
|
||||
const chat = groupChatsMessages[chatId];
|
||||
const lastTimewindowKey = chat?.lastTimewindow;
|
||||
const lastTimeWindow = lastTimewindowKey?.split('-')[1];
|
||||
return {
|
||||
...acc,
|
||||
chatId: lastTimeWindow,
|
||||
}
|
||||
}, {});
|
||||
|
||||
let activeChats = GroupChatMsg
|
||||
.find(filter)
|
||||
.fetch();
|
||||
|
||||
const idsWithTimeStamp = {};
|
||||
|
||||
activeChats.map((chat) => {
|
||||
idsWithTimeStamp[`${chat.sender.id}`] = chat.timestamp;
|
||||
chatIds.sort((a,b) => {
|
||||
if (a === PUBLIC_GROUP_CHAT_ID) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (lastTimeWindows[a] === lastTimeWindows[b]){
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
});
|
||||
|
||||
const chatInfo = chatIds.map((chatId) => {
|
||||
const contextChat = groupChatsMessages[chatId];
|
||||
const isPublicChat = chatId === PUBLIC_GROUP_CHAT_ID;
|
||||
let unreadMessagesCount = 0;
|
||||
if (contextChat) {
|
||||
const unreadTimewindows = contextChat.unreadTimeWindows;
|
||||
for (const unreadTimeWindowId of unreadTimewindows) {
|
||||
const timeWindow = (isPublicChat
|
||||
? contextChat.preJoinMessages[unreadTimeWindowId] || contextChat.posJoinMessages[unreadTimeWindowId]
|
||||
: contextChat.messageGroups[unreadTimeWindowId]);
|
||||
unreadMessagesCount += timeWindow.content.length;
|
||||
}
|
||||
}
|
||||
|
||||
activeChats = activeChats.map(mapActiveChats);
|
||||
|
||||
if (chatID) {
|
||||
activeChats.push(chatID);
|
||||
}
|
||||
|
||||
activeChats = _.uniqBy(_.compact(activeChats), 'id');
|
||||
|
||||
activeChats = activeChats.map(({ id, name }) => {
|
||||
const user = Users.findOne({ userId: id }, { fields: { color: 1, role: 1 } });
|
||||
if (chatId !== PUBLIC_GROUP_CHAT_ID) {
|
||||
const groupChatsParticipants = groupChats[chatId].participants;
|
||||
const otherParticipant = groupChatsParticipants.filter((user)=> user.id !== Auth.userID)[0];
|
||||
const user = users[otherParticipant.id];
|
||||
|
||||
return {
|
||||
color: user?.color || '#7b1fa2',
|
||||
isModerator: user?.role === ROLE_MODERATOR,
|
||||
lastActivity: idsWithTimeStamp[id],
|
||||
name,
|
||||
unreadCounter: UnreadMessages.count(id),
|
||||
userId: id,
|
||||
name: user?.name || otherParticipant.name,
|
||||
chatId,
|
||||
unreadCounter: unreadMessagesCount,
|
||||
userId: user?.userId || otherParticipant.id,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
userId: 'public',
|
||||
name: 'Public Chat',
|
||||
icon: 'group_chat',
|
||||
chatId: 'public',
|
||||
unreadCounter: unreadMessagesCount,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
|
||||
const filteredChatList = [];
|
||||
return chatInfo.filter(chat => !currentClosedChats.includes(chat.chatId));
|
||||
}
|
||||
|
||||
activeChats.forEach((op) => {
|
||||
// When a new private chat message is received, ensure the conversation view is restored.
|
||||
if (op.unreadCounter > 0) {
|
||||
if (_.indexOf(currentClosedChats, op.userId) > -1) {
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, op.userId));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare activeChats with session and push it into filteredChatList
|
||||
// if one of the activeChat is not in session.
|
||||
// It will pass to activeChats.
|
||||
if (_.indexOf(currentClosedChats, op.userId) < 0) {
|
||||
filteredChatList.push(op);
|
||||
}
|
||||
});
|
||||
|
||||
activeChats = filteredChatList;
|
||||
|
||||
activeChats.push({
|
||||
userId: 'public',
|
||||
name: 'Public Chat',
|
||||
icon: 'group_chat',
|
||||
unreadCounter: UnreadMessages.count(PUBLIC_GROUP_CHAT_ID),
|
||||
});
|
||||
|
||||
return activeChats
|
||||
.sort(sortChats);
|
||||
};
|
||||
|
||||
const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
|
||||
|
||||
@ -536,8 +528,14 @@ const hasPrivateChatBetweenUsers = (senderId, receiverId) => GroupChat
|
||||
.findOne({ users: { $all: [receiverId, senderId] } });
|
||||
|
||||
const getGroupChatPrivate = (senderUserId, receiver) => {
|
||||
if (!hasPrivateChatBetweenUsers(senderUserId, receiver.userId)) {
|
||||
const chat = hasPrivateChatBetweenUsers(senderUserId, receiver.userId);
|
||||
if (!chat) {
|
||||
makeCall('createGroupChat', receiver);
|
||||
} else {
|
||||
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
|
||||
if (_.indexOf(currentClosedChats, chat.chatId) > -1) {
|
||||
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, chat.chatId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styles } from './styles';
|
||||
import UserParticipantsContainer from './user-participants/container';
|
||||
import UserMessages from './user-messages/component';
|
||||
import UserMessages from './user-messages/container';
|
||||
import UserNotesContainer from './user-notes/container';
|
||||
import UserCaptionsContainer from './user-captions/container';
|
||||
import WaitingUsers from './waiting-users/component';
|
||||
@ -10,7 +10,6 @@ import UserPolls from './user-polls/component';
|
||||
import BreakoutRoomItem from './breakout-room/component';
|
||||
|
||||
const propTypes = {
|
||||
activeChats: PropTypes.arrayOf(String).isRequired,
|
||||
compact: PropTypes.bool,
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
@ -39,12 +38,12 @@ class UserContent extends PureComponent {
|
||||
setEmojiStatus,
|
||||
roving,
|
||||
isPublicChat,
|
||||
activeChats,
|
||||
pollIsOpen,
|
||||
forcePollOpen,
|
||||
hasBreakoutRoom,
|
||||
pendingUsers,
|
||||
requestUserInformation,
|
||||
currentClosedChats,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@ -57,10 +56,10 @@ class UserContent extends PureComponent {
|
||||
? (<UserMessages
|
||||
{...{
|
||||
isPublicChat,
|
||||
activeChats,
|
||||
compact,
|
||||
intl,
|
||||
roving,
|
||||
currentClosedChats,
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
|
@ -2,15 +2,19 @@ import React from 'react';
|
||||
import { withTracker } from 'meteor/react-meteor-data';
|
||||
import { Session } from 'meteor/session';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import Storage from '/imports/ui/services/storage/session';
|
||||
import UserContent from './component';
|
||||
import GuestUsers from '/imports/api/guest-users/';
|
||||
import Users from '/imports/api/users';
|
||||
|
||||
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
|
||||
|
||||
const UserContentContainer = props => <UserContent {...props} />;
|
||||
|
||||
export default withTracker(() => ({
|
||||
pollIsOpen: Session.equals('isPollOpen', true),
|
||||
forcePollOpen: Session.equals('forcePollOpen', true),
|
||||
currentClosedChats: Storage.getItem(CLOSED_CHAT_LIST_KEY) || [],
|
||||
currentUser: Users.findOne({ userId: Auth.userID }, {
|
||||
fields: {
|
||||
userId: 1,
|
||||
|
@ -0,0 +1,19 @@
|
||||
import React, { useEffect, useContext, useState } from 'react';
|
||||
import UserMessages from './component';
|
||||
import { ChatContext } from '/imports/ui/components/components-data/chat-context/context';
|
||||
import { GroupChatContext } from '/imports/ui/components/components-data/group-chat-context/context';
|
||||
import { UsersContext } from '/imports/ui/components/components-data/users-context/context';
|
||||
import Service from '/imports/ui/components/user-list/service';
|
||||
|
||||
const UserMessagesContainer = (props) => {
|
||||
const usingChatContext = useContext(ChatContext);
|
||||
const usingUsersContext = useContext(UsersContext);
|
||||
const usingGroupChatContext = useContext(GroupChatContext);
|
||||
const { chats: groupChatsMessages } = usingChatContext;
|
||||
const { users } = usingUsersContext;
|
||||
const { groupChat: groupChats } = usingGroupChatContext;
|
||||
const activeChats = Service.getActiveChats({ groupChatsMessages, groupChats, users });
|
||||
return <UserMessages {...{ ...props, activeChats }} />;
|
||||
};
|
||||
|
||||
export default UserMessagesContainer;
|
@ -34,8 +34,9 @@ class UnreadMessagesTracker {
|
||||
|
||||
return this._unreadChats[chatID];
|
||||
}
|
||||
|
||||
|
||||
getUnreadMessages(chatID) {
|
||||
return [];
|
||||
const filter = {
|
||||
timestamp: {
|
||||
$gt: this.get(chatID),
|
||||
@ -55,7 +56,6 @@ class UnreadMessagesTracker {
|
||||
}
|
||||
}
|
||||
const messages = GroupChatMsg.find(filter).fetch();
|
||||
return messages;
|
||||
}
|
||||
|
||||
count(chatID) {
|
||||
|
Loading…
Reference in New Issue
Block a user