Skip to content

Commit

Permalink
Implements chat using react context as middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Tainan404 committed Jan 19, 2021
1 parent 99c14af commit 8e2fe47
Show file tree
Hide file tree
Showing 21 changed files with 1,764 additions and 149 deletions.
27 changes: 18 additions & 9 deletions bigbluebutton-html5/client/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ 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';

Meteor.startup(() => {
// Logs all uncaught exceptions to the client logger
Expand All @@ -51,15 +54,21 @@ 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 />
</React.Fragment>
</ContextProviders>,
document.getElementById('app'),
);
});
Original file line number Diff line number Diff line change
@@ -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();
13 changes: 10 additions & 3 deletions bigbluebutton-html5/imports/ui/components/chat/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -22,6 +22,7 @@ const intlMessages = defineMessages({
description: 'aria-label for hiding chat button',
},
});

const Chat = (props) => {
const {
chatID,
Expand All @@ -42,8 +43,10 @@ const Chat = (props) => {
maxMessageLength,
amIModerator,
meetingIsBreakout,
timeWindowsValues,
dispatch,
count,
} = props;

const HIDE_CHAT_AK = shortcuts.hidePrivateChat;
const CLOSE_CHAT_AK = shortcuts.closePrivateChat;

Expand Down Expand Up @@ -93,7 +96,7 @@ const Chat = (props) => {
: <ChatDropdownContainer {...{ meetingIsBreakout, isMeteorConnected, amIModerator }} />
}
</header>
<MessageList
<TimeWindowList
id={ELEMENT_ID}
chatId={chatID}
handleScrollUpdate={actions.handleScrollUpdate}
Expand All @@ -104,6 +107,10 @@ const Chat = (props) => {
hasUnreadMessages,
scrollPosition,
messages,
currentUserIsModerator: amIModerator,
timeWindowsValues,
dispatch,
count,
}}
/>
<MessageForm
Expand Down
207 changes: 94 additions & 113 deletions bigbluebutton-html5/imports/ui/components/chat/container.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
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 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 ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
import Chat from './component';
import _ from 'lodash';
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 sysMessagesIds = {
welcomeId: `${SYSTEM_CHAT_TYPE}-welcome-msg`,
moderatorId: `${SYSTEM_CHAT_TYPE}-moderator-msg`
};

const intlMessages = defineMessages({
[CHAT_CLEAR]: {
id: 'app.chat.clearPublicChatMessage',
Expand All @@ -34,153 +43,125 @@ const intlMessages = defineMessages({
},
});

class ChatContainer extends PureComponent {
componentDidMount() {
// in case of reopening a chat, need to make sure it's removed from closed list
ChatService.removeFromClosedChatsSession();
}
let lastExecution = 0;
let applyPropsToState = ()=> {};

render() {
const {
children,
unmounting,
} = this.props;
const applyPropsToStateDebounced = _.debounce(()=> {
const now = Date.now();
ChatLogger.trace('lastExecution', lastExecution, now - lastExecution);
lastExecution = now;

if (unmounting === true) {
return null;
applyPropsToState();
}, 100);

const ChatContainer = (props) => {
useEffect(() => {
ChatService.removeFromClosedChatsSession();
}, []);
const modOnlyMessage = Storage.getItem('ModeratorOnlyMessage');
const { welcomeProp } = ChatService.getWelcomeProp();
const {
children,
unmounting,
chatID,
amIModerator,
loginTime,
} = props;


const systemMessages = {
[sysMessagesIds.welcomeId]:{
id: sysMessagesIds.welcomeId,
content: [{
id: sysMessagesIds.welcomeId,
text: welcomeProp.welcomeMsg,
time: loginTime,
}],
key: sysMessagesIds.welcomeId,
time: loginTime,
sender: null,
},
[sysMessagesIds.moderatorId]: {
id: sysMessagesIds.moderatorId,
content: [{
id: sysMessagesIds.moderatorId,
text: modOnlyMessage,
time: loginTime+1,
}],
key: sysMessagesIds.moderatorId,
time: loginTime+1,
sender: null,
}
};

return (
<Chat {...this.props}>
{children}
</Chat>
);
const systemMessagesIds = [sysMessagesIds.welcomeId, amIModerator && modOnlyMessage && sysMessagesIds.moderatorId].filter(i=>i);

const usingChatContext = useContext(ChatContext);
const [stateLastMsg, setLastMsg] = useState(null);
const [stateTimeWindows, setTimeWindows] = useState(chatID === PUBLIC_CHAT_KEY ? [...systemMessagesIds.map((item)=> systemMessages[item])]: [] );


if (unmounting === true) {
return null;
}
}

const contextChat = usingChatContext?.chats[chatID === PUBLIC_CHAT_KEY ? PUBLIC_GROUP_CHAT_KEY : chatID];
const lastTimeWindow = contextChat?.lastTimewindow;
const lastMsg = contextChat && (chatID === PUBLIC_CHAT_KEY
? contextChat.preJoinMessages[lastTimeWindow] || contextChat.posJoinMessages[lastTimeWindow]
: contextChat.messageGroups[lastTimeWindow]);

let timeWindowsValues = [];

applyPropsToState = ()=> {
if (!_.isEqualWith(lastMsg, stateLastMsg) && lastMsg) {
timeWindowsValues = chatID === PUBLIC_CHAT_KEY
? [...Object.values(contextChat.preJoinMessages), ...systemMessagesIds.map((item)=> systemMessages[item]), ...Object.values(contextChat.posJoinMessages)]
: [...Object.values(contextChat.messageGroups)];
setLastMsg({ ...lastMsg });
setTimeWindows(timeWindowsValues);
}
}
applyPropsToStateDebounced();
return (
<Chat {...{ ...props, chatID, amIModerator, count: 1, timeWindowsValues: stateTimeWindows, dispatch: usingChatContext?.dispatch }}>
{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 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,
messages: [],
partnerIsLoggedOut,
isChatLocked,
isMeteorConnected,
amIModerator,
meetingIsBreakout: meetingIsBreakout(),
loginTime: getLoginTime(),
actions: {
handleClosePrivateChat: ChatService.closePrivateChat,
},
Expand Down

0 comments on commit 8e2fe47

Please sign in to comment.