Merge pull request #6545 from vitormateusalmeida/issue-6430

Fix chat alert (push and audio) #6430
This commit is contained in:
Anton Georgiev 2019-01-25 10:14:38 -05:00 committed by GitHub
commit 216c38b08d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 321 additions and 327 deletions

View File

@ -40,6 +40,7 @@ export default function handleValidateAuthToken({ body }, meetingId) {
$set: {
validated: valid,
approved: !waitForApproval,
loginTime: Date.now(),
},
};

View File

@ -78,7 +78,6 @@ export default function addUser(meetingId, user) {
roles: [ROLE_VIEWER.toLowerCase()],
sortName: user.name.trim().toLowerCase(),
color,
logTime: Date.now(),
},
flat(user),
),

View File

@ -1,41 +1,33 @@
import React from 'react';
import _ from 'lodash';
import PropTypes from 'prop-types';
const propTypes = {
play: PropTypes.bool.isRequired,
count: PropTypes.number.isRequired,
};
class ChatAudioAlert extends React.Component {
constructor(props) {
super(props);
this.audio = new Audio(`${Meteor.settings.public.app.basename}/resources/sounds/notify.mp3`);
this.handleAudioLoaded = this.handleAudioLoaded.bind(this);
this.playAudio = this.playAudio.bind(this);
this.componentDidUpdate = _.debounce(this.playAudio, 2000);
}
componentDidMount() {
this.audio.addEventListener('loadedmetadata', this.handleAudioLoaded);
}
shouldComponentUpdate(nextProps) {
return nextProps.count > this.props.count;
}
componentWillUnmount() {
this.audio.removeEventListener('loadedmetadata', this.handleAudioLoaded);
}
handleAudioLoaded() {
this.componentDidUpdate = _.debounce(this.playAudio, this.audio.duration * 1000);
this.componentDidUpdate = this.playAudio;
}
playAudio() {
if (!this.props.play) return;
const { play } = this.props;
if (!play) return;
this.audio.play();
}

View File

@ -10,9 +10,10 @@ import Service from '../service';
import { styles } from '../styles';
const propTypes = {
disableNotify: PropTypes.bool.isRequired,
openChats: PropTypes.arrayOf(PropTypes.object).isRequired,
disableAudio: PropTypes.bool.isRequired,
pushAlertDisabled: PropTypes.bool.isRequired,
activeChats: PropTypes.arrayOf(PropTypes.object).isRequired,
audioAlertDisabled: PropTypes.bool.isRequired,
joinTimestamp: PropTypes.number.isRequired,
};
const intlMessages = defineMessages({
@ -34,51 +35,105 @@ const intlMessages = defineMessages({
},
});
const PUBLIC_KEY = 'public';
const PRIVATE_KEY = 'private';
const ALERT_INTERVAL = 5000; // 5 seconds
const ALERT_DURATION = 4000; // 4 seconds
class ChatAlert extends PureComponent {
constructor(props) {
super(props);
this.state = {
notified: Service.getNotified(PRIVATE_KEY),
publicNotified: Service.getNotified(PUBLIC_KEY),
alertEnabledTimestamp: props.joinTimestamp,
lastAlertTimestampByChat: {},
pendingNotificationsByChat: {},
};
}
componentWillReceiveProps(nextProps) {
componentDidUpdate(prevProps) {
const {
openChats,
disableNotify,
pushAlertDisabled,
activeChats,
joinTimestamp,
} = this.props;
if (nextProps.disableNotify === false && disableNotify === true) {
const loadMessages = {};
openChats
.forEach((c) => {
loadMessages[c.id] = c.unreadCounter;
});
this.setState({ notified: loadMessages });
const {
alertEnabledTimestamp,
lastAlertTimestampByChat,
pendingNotificationsByChat,
} = this.state;
// Avoid alerting messages received before enabling alerts
if (prevProps.pushAlertDisabled && !pushAlertDisabled) {
const newAlertEnabledTimestamp = Service.getLastMessageTimestampFromChatList(activeChats);
this.setAlertEnabledTimestamp(newAlertEnabledTimestamp);
return;
}
const notifiedToClear = {};
openChats
.filter(c => c.unreadCounter === 0)
.forEach((c) => {
notifiedToClear[c.id] = 0;
// Keep track of messages that was not alerted yet
const unalertedMessagesByChatId = {};
activeChats
.filter(chat => chat.id !== Session.get('idChatOpen'))
.filter(chat => chat.unreadCounter > 0)
.forEach((chat) => {
const chatId = (chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id;
const thisChatUnreadMessages = UnreadMessages.getUnreadMessages(chatId);
unalertedMessagesByChatId[chatId] = thisChatUnreadMessages.filter((msg) => {
const messageChatId = (msg.chatId === 'MAIN-PUBLIC-GROUP-CHAT') ? msg.chatId : msg.sender;
const retorno = (msg
&& msg.timestamp > alertEnabledTimestamp
&& msg.timestamp > joinTimestamp
&& msg.timestamp > (lastAlertTimestampByChat[messageChatId] || 0)
);
return retorno;
});
if (!unalertedMessagesByChatId[chatId].length) delete unalertedMessagesByChatId[chatId];
});
this.setState(({ notified }) => ({
notified: {
...notified,
...notifiedToClear,
},
}), () => {
Service.setNotified(PRIVATE_KEY, this.state.notified);
const lastUnalertedMessageTimestampByChat = {};
Object.keys(unalertedMessagesByChatId).forEach((chatId) => {
lastUnalertedMessageTimestampByChat[chatId] = unalertedMessagesByChatId[chatId]
.reduce(Service.maxTimestampReducer, 0);
});
// Keep track of chats that need to be alerted now (considering alert interval)
const chatsWithPendingAlerts = Object.keys(lastUnalertedMessageTimestampByChat)
.filter(chatId => lastUnalertedMessageTimestampByChat[chatId]
> ((lastAlertTimestampByChat[chatId] || 0) + ALERT_INTERVAL)
&& !(chatId in pendingNotificationsByChat));
if (!chatsWithPendingAlerts.length) return;
const newPendingNotificationsByChat = Object.assign({},
...chatsWithPendingAlerts.map(chatId => ({ [chatId]: unalertedMessagesByChatId[chatId] })));
// Mark messages as alerted
const newLastAlertTimestampByChat = { ...lastAlertTimestampByChat };
chatsWithPendingAlerts.forEach(
(chatId) => {
newLastAlertTimestampByChat[chatId] = lastUnalertedMessageTimestampByChat[chatId];
},
);
if (!pushAlertDisabled) {
this.setChatMessagesState(newPendingNotificationsByChat, newLastAlertTimestampByChat);
}
}
setAlertEnabledTimestamp(newAlertEnabledTimestamp) {
const { alertEnabledTimestamp } = this.state;
if (newAlertEnabledTimestamp > 0 && alertEnabledTimestamp !== newAlertEnabledTimestamp) {
this.setState({ alertEnabledTimestamp: newAlertEnabledTimestamp });
}
}
setChatMessagesState(pendingNotificationsByChat, lastAlertTimestampByChat) {
this.setState({ pendingNotificationsByChat, lastAlertTimestampByChat });
}
mapContentText(message) {
const {
intl,
@ -86,12 +141,13 @@ class ChatAlert extends PureComponent {
const contentMessage = message
.map((content) => {
if (content.text === 'PUBLIC_CHAT_CLEAR') return intl.formatMessage(intlMessages.publicChatClear);
/* this code is to remove html tags that come in the server's messangens */
/* this code is to remove html tags that come in the server's messages */
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content.text;
const textWithoutTag = tempDiv.innerText;
return textWithoutTag;
});
return contentMessage;
}
@ -102,155 +158,64 @@ class ChatAlert extends PureComponent {
<div className={styles.contentMessage}>
{
this.mapContentText(message)
.reduce((acc, text) => [...acc, (<br />), text], []).splice(1)
.reduce((acc, text) => [...acc, (<br key={_.uniqueId('br_')} />), text], [])
}
</div>
</div>
);
}
notifyPrivateChat() {
const {
disableNotify,
openChats,
intl,
} = this.props;
if (disableNotify) return;
const hasUnread = ({ unreadCounter }) => unreadCounter > 0;
const isNotNotified = ({ id, unreadCounter }) => unreadCounter !== this.state.notified[id];
const isPrivate = ({ id }) => id !== PUBLIC_KEY;
const thisChatClosed = ({ id }) => !Session.equals('idChatOpen', id);
const chatsNotify = openChats
.filter(hasUnread)
.filter(isNotNotified)
.filter(isPrivate)
.filter(thisChatClosed)
.map(({
id,
name,
unreadCounter,
...rest
}) => ({
...rest,
name,
unreadCounter,
id,
message: intl.formatMessage(intlMessages.appToastChatPrivate),
}));
return (
<span>
{
chatsNotify.map(({ id, message, name }) => {
const getChatmessages = UnreadMessages.getUnreadMessages(id)
.filter(({ fromTime, fromUserId }) => fromTime > (this.state.notified[fromUserId] || 0));
const reduceMessages = Service
.reduceAndMapGroupMessages(getChatmessages);
if (!reduceMessages.length) return null;
const flatMessages = _.flatten(reduceMessages
.map(msg => this.createMessage(name, msg.content)));
const limitingMessages = flatMessages;
return (<ChatPushAlert
key={_.uniqueId('id-')}
chatId={id}
content={limitingMessages}
message={<span >{message}</span>}
onOpen={() => {
this.setState(({ notified }) => ({
notified: {
...notified,
[id]: new Date().getTime(),
},
}), () => {
Service.setNotified(PRIVATE_KEY, this.state.notified);
});
}}
/>);
})
}
</span>
);
}
notifyPublicChat() {
const {
publicUserId,
intl,
disableNotify,
} = this.props;
const publicUnread = UnreadMessages.getUnreadMessages(publicUserId);
const publicUnreadReduced = Service.reduceAndMapGroupMessages(publicUnread);
if (disableNotify) return;
if (!Service.hasUnreadMessages(publicUserId)) return;
if (Session.equals('idChatOpen', PUBLIC_KEY)) return;
const checkIfBeenNotified = ({ sender, time }) =>
time > (this.state.publicNotified[sender.id] || 0);
const chatsNotify = publicUnreadReduced
.map(msg => ({
...msg,
sender: {
name: msg.sender ? msg.sender.name : intl.formatMessage(intlMessages.appToastChatSystem),
...msg.sender,
},
}))
.filter(checkIfBeenNotified);
return (
<span>
{
chatsNotify.map(({ sender, time, content }) =>
(<ChatPushAlert
key={time}
chatId={PUBLIC_KEY}
name={sender.name}
message={
<span >
{ intl.formatMessage(intlMessages.appToastChatPublic) }
</span>
}
content={this.createMessage(sender.name, content)}
onOpen={() => {
this.setState(({ notified, publicNotified }) => ({
...notified,
publicNotified: {
...publicNotified,
[sender.id]: time,
},
}), () => {
Service.setNotified(PUBLIC_KEY, this.state.publicNotified);
});
}}
/>))
}
</span>
);
}
render() {
const {
openChats,
disableAudio,
audioAlertDisabled,
pushAlertDisabled,
intl,
} = this.props;
const unreadMessagesCount = openChats
.map(chat => chat.unreadCounter)
.reduce((a, b) => a + b, 0);
const shouldPlayAudio = !disableAudio && unreadMessagesCount > 0;
const {
pendingNotificationsByChat,
} = this.state;
const shouldPlay = Object.keys(pendingNotificationsByChat).length > 0;
return (
<span>
<ChatAudioAlert play={shouldPlayAudio} count={unreadMessagesCount} />
{ this.notifyPublicChat() }
{ this.notifyPrivateChat() }
{!audioAlertDisabled ? <ChatAudioAlert play={shouldPlay} /> : null}
{
!pushAlertDisabled
? Object.keys(pendingNotificationsByChat)
.map((chatId) => {
// Only display the latest group of messages (up to 5 messages)
const reducedMessage = Service
.reduceAndMapGroupMessages(pendingNotificationsByChat[chatId].slice(-5)).pop();
if (!reducedMessage) return null;
const content = this
.createMessage(reducedMessage.sender.name, reducedMessage.content);
return (
<ChatPushAlert
key={chatId}
chatId={chatId}
content={content}
title={
(chatId === 'MAIN-PUBLIC-GROUP-CHAT')
? <span>{intl.formatMessage(intlMessages.appToastChatPublic)}</span>
: <span>{intl.formatMessage(intlMessages.appToastChatPrivate)}</span>
}
onOpen={
() => {
let pendingNotifications = pendingNotificationsByChat;
delete pendingNotifications[chatId];
pendingNotifications = { ...pendingNotifications };
this.setState({ pendingNotificationsByChat: pendingNotifications });
}}
alertDuration={ALERT_DURATION}
/>
);
})
: null
}
</span>
);
}

View File

@ -3,6 +3,9 @@ import { withTracker } from 'meteor/react-meteor-data';
import UserListService from '/imports/ui/components/user-list/service';
import Settings from '/imports/ui/services/settings';
import ChatAlert from './component';
import ChatService from '/imports/ui/components/chat/service.js';
import Auth from '/imports/ui/services/auth';
import Users from '/imports/api/users';
const ChatAlertContainer = props => (
<ChatAlert {...props} />
@ -10,12 +13,13 @@ const ChatAlertContainer = props => (
export default withTracker(() => {
const AppSettings = Settings.application;
const openChats = UserListService.getOpenChats();
const activeChats = UserListService.getActiveChats();
const loginTime = Users.findOne({ userId: Auth.userID }).loginTime;
return {
disableAudio: !AppSettings.chatAudioAlerts,
disableNotify: !AppSettings.chatPushAlerts,
openChats,
audioAlertDisabled: !AppSettings.chatAudioAlerts,
pushAlertDisabled: !AppSettings.chatPushAlerts,
activeChats,
publicUserId: Meteor.settings.public.chat.public_group_id,
joinTimestamp: loginTime,
};
})(memo(ChatAlertContainer));

View File

@ -4,37 +4,44 @@ import _ from 'lodash';
import injectNotify from '/imports/ui/components/toast/inject-notify/component';
import { Session } from 'meteor/session';
const ALERT_INTERVAL = 2000; // 2 seconds
const ALERT_LIFETIME = 4000; // 4 seconds
const propTypes = {
notify: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired,
chatId: PropTypes.string.isRequired,
message: PropTypes.node.isRequired,
title: PropTypes.node.isRequired,
content: PropTypes.node.isRequired,
alertDuration: PropTypes.number.isRequired,
};
class ChatPushAlert extends PureComponent {
static link(message, chatId) {
static link(title, chatId) {
let chat = chatId;
if (chat === 'MAIN-PUBLIC-GROUP-CHAT') {
chat = 'public';
}
return (
<div
key={chatId}
role="button"
aria-label={message}
aria-label={title}
tabIndex={0}
onClick={() => {
Session.set('openPanel', 'chat');
Session.set('idChatOpen', chatId);
Session.set('idChatOpen', chat);
}}
onKeyPress={() => null}
>
{ message }
{title}
</div>
);
}
constructor(props) {
super(props);
this.showNotify = _.debounce(this.showNotify.bind(this), ALERT_INTERVAL);
this.showNotify = this.showNotify.bind(this);
this.componentDidMount = this.showNotify;
this.componentDidUpdate = this.showNotify;
@ -45,15 +52,16 @@ class ChatPushAlert extends PureComponent {
notify,
onOpen,
chatId,
message,
title,
content,
alertDuration,
} = this.props;
return notify(
ChatPushAlert.link(message, chatId),
ChatPushAlert.link(title, chatId),
'info',
'chat',
{ onOpen, autoClose: ALERT_LIFETIME },
{ onOpen, autoClose: alertDuration },
ChatPushAlert.link(content, chatId),
true,
);

View File

@ -56,6 +56,7 @@ const Chat = (props) => {
>
<Button
onClick={() => {
Session.set('idChatOpen', '');
Session.set('openPanel', 'userlist');
}}
aria-label={intl.formatMessage(intlMessages.hideChatLabel, { 0: title })}
@ -75,6 +76,7 @@ const Chat = (props) => {
hideLabel
onClick={() => {
actions.handleClosePrivateChat(chatID);
Session.set('idChatOpen', '');
Session.set('openPanel', 'userlist');
}}
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}

View File

@ -59,7 +59,7 @@ export default injectIntl(withTracker(({ intl }) => {
messages = ChatService.getPublicGroupMessages();
const time = user.logTime;
const time = user.loginTime;
const welcomeId = `welcome-msg-${time}`;
const welcomeMsg = {

View File

@ -92,16 +92,13 @@ const reduceGroupMessages = (previous, current) => {
return previous.concat(currentMessage);
};
const reduceAndMapGroupMessages = messages =>
(messages.reduce(reduceGroupMessages, []).map(mapGroupMessage));
const reduceAndMapGroupMessages = messages => (messages
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
const getPublicGroupMessages = () => {
const publicGroupMessages = GroupChatMsg.find({
chatId: PUBLIC_GROUP_CHAT_ID,
}, {
sort: ['timestamp'],
}).fetch();
}, { sort: ['timestamp'] }).fetch();
return publicGroupMessages;
};
@ -123,9 +120,7 @@ const getPrivateGroupMessages = () => {
messages = GroupChatMsg.find({
chatId,
}, {
sort: ['timestamp'],
}).fetch();
}, { sort: ['timestamp'] }).fetch();
}
return reduceAndMapGroupMessages(messages, []);
@ -141,8 +136,8 @@ const isChatLocked = (receiverID) => {
const isPubChatLocked = meeting.lockSettingsProp.disablePubChat;
const isPrivChatLocked = meeting.lockSettingsProp.disablePrivChat;
return mapUser(user).isLocked &&
((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
return mapUser(user).isLocked
&& ((isPublic && isPubChatLocked) || (!isPublic && isPrivChatLocked));
}
return false;
@ -206,11 +201,10 @@ const getScrollPosition = (receiverID) => {
return scroll.position;
};
const updateScrollPosition =
position => ScrollCollection.upsert(
{ receiver: Session.get('idChatOpen') },
{ $set: { position } },
);
const updateScrollPosition = position => ScrollCollection.upsert(
{ receiver: Session.get('idChatOpen') },
{ $set: { position } },
);
const updateUnreadMessage = (timestamp) => {
const chatID = Session.get('idChatOpen') || PUBLIC_CHAT_ID;
@ -251,16 +245,16 @@ const htmlDecode = (input) => {
// Export the chat as [Hour:Min] user: message
const exportChat = (messageList) => {
const { welcomeProp } = getMeeting();
const { logTime } = getUser(Auth.userID);
const { loginTime } = getUser(Auth.userID);
const { welcomeMsg } = welcomeProp;
const clearMessage = messageList.filter(message => message.message === PUBLIC_CHAT_CLEAR);
const hasClearMessage = clearMessage.length;
if (!hasClearMessage || (hasClearMessage && clearMessage[0].timestamp < logTime)) {
if (!hasClearMessage || (hasClearMessage && clearMessage[0].timestamp < loginTime)) {
messageList.push({
timestamp: logTime,
timestamp: loginTime,
message: welcomeMsg,
type: SYSTEM_CHAT_TYPE,
sender: PUBLIC_CHAT_USER_ID,
@ -282,28 +276,38 @@ const exportChat = (messageList) => {
}).join('\n');
};
const setNotified = (chatType, item) => {
const notified = Storage.getItem('notified');
const key = 'notified';
const userChat = { [chatType]: item };
if (notified) {
Storage.setItem(key, {
...notified,
...userChat,
});
return;
const getUnreadMessagesFromChatId = chatId => UnreadMessages.getUnreadMessages(chatId);
const getAllMessages = (chatID) => {
const filter = {
sender: { $ne: Auth.userID },
};
if (chatID === PUBLIC_GROUP_CHAT_ID) {
filter.chatId = { $eq: chatID };
} else {
const privateChat = GroupChat.findOne({ users: { $all: [chatID, Auth.userID] } });
filter.chatId = { $ne: PUBLIC_GROUP_CHAT_ID };
if (privateChat) {
filter.chatId = privateChat.chatId;
}
}
Storage.setItem(key, {
...userChat,
});
const messages = GroupChatMsg.find(filter).fetch();
return messages;
};
const getNotified = (chat) => {
const key = 'notified';
const notified = Storage.getItem(key);
if (notified) return notified[chat] || {};
return {};
};
const getlastMessage = lastMessages => lastMessages.sort((a,
b) => a.timestamp - b.timestamp).pop();
const maxTimestampReducer = (max, el) => ((el.timestamp > max) ? el.timestamp : max);
const maxNumberReducer = (max, el) => ((el > max) ? el : max);
const getLastMessageTimestampFromChatList = activeChats => activeChats
.map(chat => ((chat.id === 'public') ? 'MAIN-PUBLIC-GROUP-CHAT' : chat.id))
.map(chatId => getAllMessages(chatId).reduce(maxTimestampReducer, 0))
.reduce(maxNumberReducer, 0);
export default {
reduceAndMapGroupMessages,
@ -322,6 +326,10 @@ export default {
removeFromClosedChatsSession,
exportChat,
clearPublicChatHistory,
setNotified,
getNotified,
getlastMessage,
getUnreadMessagesFromChatId,
getAllMessages,
maxTimestampReducer,
maxNumberReducer,
getLastMessageTimestampFromChatList,
};

View File

@ -87,6 +87,16 @@ const openBreakoutJoinConfirmation = (breakout, breakoutName, mountModal) => mou
const closeBreakoutJoinConfirmation = mountModal => mountModal(null);
class NavBar extends PureComponent {
static handleToggleUserList() {
Session.set(
'openPanel',
Session.get('openPanel') !== ''
? ''
: 'userlist',
);
Session.set('idChatOpen', '');
}
constructor(props) {
super(props);
@ -97,7 +107,6 @@ class NavBar extends PureComponent {
};
this.incrementTime = this.incrementTime.bind(this);
this.handleToggleUserList = this.handleToggleUserList.bind(this);
}
componentDidMount() {
@ -162,15 +171,6 @@ class NavBar extends PureComponent {
clearInterval(this.interval);
}
handleToggleUserList() {
Session.set(
'openPanel',
Session.get('openPanel') !== ''
? ''
: 'userlist',
);
}
inviteUserToBreakout(breakout) {
const {
mountModal,
@ -279,7 +279,7 @@ class NavBar extends PureComponent {
<div className={styles.left}>
<Button
data-test="userListToggleButton"
onClick={this.handleToggleUserList}
onClick={NavBar.handleToggleUserList}
ghost
circle
hideLabel

View File

@ -30,7 +30,7 @@ const propTypes = {
name: PropTypes.string.isRequired,
unreadCounter: PropTypes.number.isRequired,
}).isRequired,
openChat: PropTypes.string,
activeChat: PropTypes.string,
compact: PropTypes.bool.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -41,7 +41,7 @@ const propTypes = {
};
const defaultProps = {
openChat: '',
activeChat: '',
shortcuts: '',
};
@ -51,13 +51,17 @@ const handleClickToggleChat = (id) => {
Session.get('openPanel') === 'chat' && Session.get('idChatOpen') === id
? 'userlist' : 'chat',
);
Session.set('idChatOpen', id);
if (Session.equals('openPanel', 'chat')) {
Session.set('idChatOpen', id);
} else {
Session.set('idChatOpen', '');
}
};
const ChatListItem = (props) => {
const {
chat,
openChat,
activeChat,
compact,
intl,
tabIndex,
@ -65,7 +69,7 @@ const ChatListItem = (props) => {
shortcuts: TOGGLE_CHAT_PUB_AK,
} = props;
const isCurrentChat = chat.id === openChat;
const isCurrentChat = chat.id === activeChat;
const linkClasses = {};
linkClasses[styles.active] = isCurrentChat;

View File

@ -7,7 +7,7 @@ import CustomLogo from './custom-logo/component';
import UserContentContainer from './user-list-content/container';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
activeChats: PropTypes.arrayOf(String).isRequired,
compact: PropTypes.bool,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -42,7 +42,7 @@ class UserList extends PureComponent {
render() {
const {
intl,
openChats,
activeChats,
compact,
currentUser,
isBreakoutRoom,
@ -73,14 +73,14 @@ class UserList extends PureComponent {
<div className={styles.userList}>
{
showBranding
&& !this.props.compact
&& CustomLogoUrl
&& !compact
&& CustomLogoUrl
? <CustomLogo CustomLogoUrl={CustomLogoUrl} /> : null
}
{<UserContentContainer
{...{
intl,
openChats,
activeChats,
compact,
currentUser,
isBreakoutRoom,
@ -104,7 +104,7 @@ class UserList extends PureComponent {
getUsersId,
hasPrivateChatBetweenUsers,
}
}
}
/>}
</div>
);

View File

@ -7,7 +7,7 @@ import Service from './service';
import UserList from './component';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
activeChats: PropTypes.arrayOf(String).isRequired,
currentUser: PropTypes.shape({}).isRequired,
getUsersId: PropTypes.func.isRequired,
isBreakoutRoom: PropTypes.bool.isRequired,
@ -34,7 +34,7 @@ export default withTracker(({ chatID, compact }) => ({
hasBreakoutRoom: Service.hasBreakoutRoom(),
getUsersId: Service.getUsersId,
currentUser: Service.getCurrentUser(),
openChats: Service.getOpenChats(chatID),
activeChats: Service.getActiveChats(chatID),
isBreakoutRoom: meetingIsBreakout(),
getAvailableActions: Service.getAvailableActions,
normalizeEmojiName: Service.normalizeEmojiName,

View File

@ -18,7 +18,7 @@ const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
const mapOpenChats = (chat) => {
const mapActiveChats = (chat) => {
const currentUserId = Auth.userID;
if (chat.sender !== currentUserId) {
@ -192,7 +192,7 @@ const getUsersId = () => getUsers().map(u => u.id);
const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID }).count() > 0;
const getOpenChats = (chatID) => {
const getActiveChats = (chatID) => {
const privateChat = GroupChat
.find({ users: { $all: [Auth.userID] } })
.fetch()
@ -206,30 +206,30 @@ const getOpenChats = (chatID) => {
filter.chatId = { $in: privateChat };
}
let openChats = GroupChatMsg
let activeChats = GroupChatMsg
.find(filter)
.fetch()
.map(mapOpenChats);
.map(mapActiveChats);
if (chatID) {
openChats.push(chatID);
activeChats.push(chatID);
}
openChats = _.uniq(_.compact(openChats));
activeChats = _.uniq(_.compact(activeChats));
openChats = Users
.find({ userId: { $in: openChats } })
activeChats = Users
.find({ userId: { $in: activeChats } })
.map(mapUser)
.map((op) => {
const openChat = op;
openChat.unreadCounter = UnreadMessages.count(op.id);
return openChat;
const activeChat = op;
activeChat.unreadCounter = UnreadMessages.count(op.id);
return activeChat;
});
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
const filteredChatList = [];
openChats.forEach((op) => {
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.id) > -1) {
@ -237,24 +237,24 @@ const getOpenChats = (chatID) => {
}
}
// Compare openChats with session and push it into filteredChatList
// if one of the openChat is not in session.
// It will pass to openChats.
// 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.id) < 0) {
filteredChatList.push(op);
}
});
openChats = filteredChatList;
activeChats = filteredChatList;
openChats.push({
activeChats.push({
id: 'public',
name: 'Public Chat',
icon: 'group_chat',
unreadCounter: UnreadMessages.count(PUBLIC_GROUP_CHAT_ID),
});
return openChats
return activeChats
.sort(sortChats);
};
@ -268,36 +268,36 @@ const getAvailableActions = (currentUser, user, isBreakoutRoom) => {
const allowedToChatPrivately = !user.isCurrent && !isDialInUser;
const allowedToMuteAudio = hasAuthority
&& user.isVoiceUser
&& !user.isMuted
&& !user.isListenOnly;
&& user.isVoiceUser
&& !user.isMuted
&& !user.isListenOnly;
const allowedToUnmuteAudio = hasAuthority
&& user.isVoiceUser
&& !user.isListenOnly
&& user.isMuted
&& user.isCurrent;
&& user.isVoiceUser
&& !user.isListenOnly
&& user.isMuted
&& user.isCurrent;
const allowedToResetStatus = hasAuthority
&& user.emoji.status !== EMOJI_STATUSES.none
&& !isDialInUser;
&& user.emoji.status !== EMOJI_STATUSES.none
&& !isDialInUser;
// if currentUser is a moderator, allow removing other users
const allowedToRemove = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
const allowedToSetPresenter = currentUser.isModerator
&& !user.isPresenter
&& !isDialInUser;
&& !user.isPresenter
&& !isDialInUser;
const allowedToPromote = currentUser.isModerator
&& !user.isCurrent
&& !user.isModerator
&& !isDialInUser;
&& !user.isCurrent
&& !user.isModerator
&& !isDialInUser;
const allowedToDemote = currentUser.isModerator
&& !user.isCurrent
&& user.isModerator
&& !isDialInUser;
&& !user.isCurrent
&& user.isModerator
&& !isDialInUser;
const allowedToChangeStatus = user.isCurrent;
@ -361,7 +361,13 @@ const removeUser = (userId) => {
}
};
const toggleVoice = (userId) => { userId === Auth.userID ? makeCall('toggleSelfVoice') : makeCall('toggleVoice', userId); };
const toggleVoice = (userId) => {
if (userId === Auth.userID) {
makeCall('toggleSelfVoice');
} else {
makeCall('toggleVoice', userId);
}
};
const muteAllUsers = (userId) => { makeCall('muteAllUsers', userId); };
@ -452,7 +458,7 @@ export default {
changeRole,
getUsers,
getUsersId,
getOpenChats,
getActiveChats,
getCurrentUser,
getAvailableActions,
normalizeEmojiName,

View File

@ -8,7 +8,7 @@ import UserPolls from './user-polls/component';
import BreakoutRoomItem from './breakout-room/component';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
activeChats: PropTypes.arrayOf(String).isRequired,
compact: PropTypes.bool,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -61,7 +61,7 @@ class UserContent extends PureComponent {
getEmojiList,
getEmoji,
isPublicChat,
openChats,
activeChats,
getGroupChatPrivate,
pollIsOpen,
forcePollOpen,
@ -79,7 +79,7 @@ class UserContent extends PureComponent {
<UserMessages
{...{
isPublicChat,
openChats,
activeChats,
compact,
intl,
roving,

View File

@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
import { defineMessages } from 'react-intl';
import cx from 'classnames';
import { styles } from '/imports/ui/components/user-list/user-list-content/styles';
import ChatListItem from './../../chat-list-item/component';
import ChatListItem from '../../chat-list-item/component';
const propTypes = {
openChats: PropTypes.arrayOf(String).isRequired,
openChat: PropTypes.string,
activeChats: PropTypes.arrayOf(String).isRequired,
activeChat: PropTypes.string,
compact: PropTypes.bool,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -19,7 +19,7 @@ const propTypes = {
const defaultProps = {
compact: false,
openChat: '',
activeChat: '',
};
const listTransition = {
@ -46,20 +46,21 @@ class UserMessages extends PureComponent {
index: -1,
};
this.openChatRefs = [];
this.activeChatRefs = [];
this.selectedIndex = -1;
this.focusOpenChatItem = this.focusOpenChatItem.bind(this);
this.focusActiveChatItem = this.focusActiveChatItem.bind(this);
this.changeState = this.changeState.bind(this);
}
componentDidMount() {
if (!this.props.compact) {
const { compact, roving, activeChats } = this.props;
if (!compact) {
this._msgsList.addEventListener(
'keydown',
event => this.props.roving(
event => roving(
event,
this.props.openChats.length,
activeChats.length,
this.changeState,
),
);
@ -67,26 +68,27 @@ class UserMessages extends PureComponent {
}
componentDidUpdate(prevProps, prevState) {
if (this.state.index === -1) {
const { index } = this.state;
if (index === -1) {
return;
}
if (this.state.index !== prevState.index) {
this.focusOpenChatItem(this.state.index);
if (index !== prevState.index) {
this.focusActiveChatItem(index);
}
}
getOpenChats() {
getActiveChats() {
const {
openChats,
openChat,
activeChats,
activeChat,
compact,
isPublicChat,
} = this.props;
let index = -1;
return openChats.map(chat => (
return activeChats.map(chat => (
<CSSTransition
classNames={listTransition}
appear
@ -97,11 +99,11 @@ class UserMessages extends PureComponent {
className={cx(styles.chatsList)}
key={chat.id}
>
<div ref={(node) => { this.openChatRefs[index += 1] = node; }}>
<div ref={(node) => { this.activeChatRefs[index += 1] = node; }}>
<ChatListItem
isPublicChat={isPublicChat}
compact={compact}
openChat={openChat}
activeChat={activeChat}
chat={chat}
tabIndex={-1}
/>
@ -114,12 +116,12 @@ class UserMessages extends PureComponent {
this.setState({ index: newIndex });
}
focusOpenChatItem(index) {
if (!this.openChatRefs[index]) {
focusActiveChatItem(index) {
if (!this.activeChatRefs[index]) {
return;
}
this.openChatRefs[index].firstChild.focus();
this.activeChatRefs[index].firstChild.focus();
}
render() {
@ -131,10 +133,13 @@ class UserMessages extends PureComponent {
return (
<div className={styles.messages}>
{
!compact ?
!compact ? (
<h2 className={styles.smallTitle}>
{intl.formatMessage(intlMessages.messagesTitle)}
</h2> : <hr className={styles.separator} />
</h2>
) : (
<hr className={styles.separator} />
)
}
<div
role="tabpanel"
@ -143,8 +148,8 @@ class UserMessages extends PureComponent {
ref={(ref) => { this._msgsList = ref; }}
>
<div className={styles.list}>
<TransitionGroup ref={(ref) => { this._msgItems = ref; }} >
{ this.getOpenChats() }
<TransitionGroup ref={(ref) => { this._msgItems = ref; }}>
{this.getActiveChats()}
</TransitionGroup>
</div>
</div>

View File

@ -257,7 +257,7 @@ class UserDropdown extends PureComponent {
if (enablePrivateChat) {
actions.push(this.makeDropdownItem(
'openChat',
'activeChat',
intl.formatMessage(messages.ChatLabel),
() => {
getGroupChatPrivate(currentUser, user);

View File

@ -30,7 +30,7 @@ const mapUser = (user) => {
isPhoneUser: user.phone_user,
isOnline: user.connectionStatus === 'online',
clientType: user.clientType,
logTime: user.logTime,
loginTime: user.loginTime,
};
mappedUser.isLocked = user.locked && !(mappedUser.isPresenter || mappedUser.isModerator);