bigbluebutton-Github/bigbluebutton-html5/imports/ui/components/user-list/service.js
Mario Jr 19c1626c39 Do not return empty array when groupChats is empty
Instead, return empty array when both users and groupChats arrays are empty.
The public chat and user chats may be active when groupChats array is empty.
Fixes #11476
2021-02-23 17:52:19 -03:00

651 lines
18 KiB
JavaScript
Executable File

import Users from '/imports/api/users';
import VoiceUsers from '/imports/api/voice-users';
import GroupChat from '/imports/api/group-chat';
import Breakouts from '/imports/api/breakouts/';
import Meetings from '/imports/api/meetings';
import Auth from '/imports/ui/services/auth';
import Storage from '/imports/ui/services/storage/session';
import { EMOJI_STATUSES } from '/imports/utils/statuses';
import { makeCall } from '/imports/ui/services/api';
import _ from 'lodash';
import KEY_CODES from '/imports/utils/keyCodes';
import AudioService from '/imports/ui/components/audio/service';
import logger from '/imports/startup/client/logger';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
const DIAL_IN_CLIENT_TYPE = 'dial-in-user';
// session for closed chat list
const CLOSED_CHAT_LIST_KEY = 'closedChatList';
const mapActiveChats = (chat) => {
const currentUserId = Auth.userID;
const { chatId } = chat;
const userId = GroupChat
.findOne({ chatId })
.participants
.filter(user => user.id !== currentUserId);
return userId[0];
};
const CUSTOM_LOGO_URL_KEY = 'CustomLogoUrl';
export const setCustomLogoUrl = path => Storage.setItem(CUSTOM_LOGO_URL_KEY, path);
export const setModeratorOnlyMessage = msg => Storage.setItem('ModeratorOnlyMessage', msg);
const getCustomLogoUrl = () => Storage.getItem(CUSTOM_LOGO_URL_KEY);
const sortUsersByName = (a, b) => {
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
if (aName < bName) {
return -1;
} if (aName > bName) {
return 1;
} if (a.userId > b.userId) {
return -1;
} if (a.userId < b.userId) {
return 1;
}
return 0;
};
const sortUsersByEmoji = (a, b) => {
if (a.emoji && b.emoji && (a.emoji !== 'none' && b.emoji !== 'none')) {
if (a.emojiTime < b.emojiTime) {
return -1;
} if (a.emojiTime > b.emojiTime) {
return 1;
}
} if (a.emoji && a.emoji !== 'none') {
return -1;
} if (b.emoji && b.emoji !== 'none') {
return 1;
}
return 0;
};
const sortUsersByModerator = (a, b) => {
if (a.role === ROLE_MODERATOR && b.role === ROLE_MODERATOR) {
return 0;
} if (a.role === ROLE_MODERATOR) {
return -1;
} if (b.role === ROLE_MODERATOR) {
return 1;
}
return 0;
};
const sortUsersByPhoneUser = (a, b) => {
if (!a.clientType === DIAL_IN_CLIENT_TYPE && !b.clientType === DIAL_IN_CLIENT_TYPE) {
return 0;
} if (!a.clientType === DIAL_IN_CLIENT_TYPE) {
return -1;
} if (!b.clientType === DIAL_IN_CLIENT_TYPE) {
return 1;
}
return 0;
};
// current user's name is always on top
const sortUsersByCurrent = (a, b) => {
if (a.userId === Auth.userID) {
return -1;
} if (b.userId === Auth.userID) {
return 1;
}
return 0;
};
const sortUsers = (a, b) => {
let sort = sortUsersByCurrent(a, b);
if (sort === 0) {
sort = sortUsersByModerator(a, b);
}
if (sort === 0) {
sort = sortUsersByEmoji(a, b);
}
if (sort === 0) {
sort = sortUsersByPhoneUser(a, b);
}
if (sort === 0) {
sort = sortUsersByName(a, b);
}
return sort;
};
const sortChatsByName = (a, b) => {
if (a.name.toLowerCase() < b.name.toLowerCase()) {
return -1;
} if (a.name.toLowerCase() > b.name.toLowerCase()) {
return 1;
} if (a.userId.toLowerCase() > b.userId.toLowerCase()) {
return -1;
} if (a.userId.toLowerCase() < b.userId.toLowerCase()) {
return 1;
}
return 0;
};
const sortChatsByIcon = (a, b) => {
if (a.icon && b.icon) {
return sortChatsByName(a, b);
} if (a.icon) {
return -1;
} if (b.icon) {
return 1;
}
return 0;
};
const sortByRecentActivity = (a, b) => {
const _a = a.lastActivity;
const _b = b.lastActivity;
if (a.userId === 'public') return -1;
if (!_b || _a > _b) return -1;
if (!_a || _a < _b) return 1;
return 0;
};
const isPublicChat = chat => (
chat.userId === 'public'
);
const sortChats = (a, b) => {
let sort = sortChatsByIcon(a, b);
if (sort === 0) {
sort = sortChatsByName(a, b);
}
return sortByRecentActivity(a, b);
};
const userFindSorting = {
emojiTime: 1,
role: 1,
phoneUser: 1,
name: 1,
userId: 1,
};
const getUsers = () => {
let users = Users
.find({
meetingId: Auth.meetingID,
}, userFindSorting)
.fetch();
const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { role: 1, locked: 1 } });
if (currentUser && currentUser.role === ROLE_VIEWER && currentUser.locked) {
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'lockSettingsProps.hideUserList': 1 } });
if (meeting && meeting.lockSettingsProps && meeting.lockSettingsProps.hideUserList) {
const moderatorOrCurrentUser = u => u.role === ROLE_MODERATOR || u.userId === Auth.userID;
users = users.filter(moderatorOrCurrentUser);
}
}
return users.sort(sortUsers);
};
const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID },
{ fields: {} }).count() > 0;
const isMe = userId => userId === Auth.userID;
const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
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,
}
}, {});
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;
}
}
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,
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) || [];
return chatInfo.filter(chat => !currentClosedChats.includes(chat.chatId));
}
const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
const isMeetingLocked = (id) => {
const meeting = Meetings.findOne({ meetingId: id }, { fields: { lockSettingsProps: 1 } });
let isLocked = false;
if (meeting.lockSettingsProps !== undefined) {
const lockSettings = meeting.lockSettingsProps;
if (lockSettings.disableCam
|| lockSettings.disableMic
|| lockSettings.disablePrivateChat
|| lockSettings.disablePublicChat) {
isLocked = true;
}
}
return isLocked;
};
const getUsersProp = () => {
const meeting = Meetings.findOne(
{ meetingId: Auth.meetingID },
{
fields: {
'usersProp.allowModsToUnmuteUsers': 1,
'usersProp.authenticatedGuest': 1,
},
}
);
if (meeting.usersProp) return meeting.usersProp;
return {
allowModsToUnmuteUsers: false,
authenticatedGuest: false,
}
};
const curatedVoiceUser = (intId) => {
const voiceUser = VoiceUsers.findOne({ intId });
return {
isVoiceUser: voiceUser ? voiceUser.joined : false,
isMuted: voiceUser ? voiceUser.muted && !voiceUser.listenOnly : false,
isTalking: voiceUser ? voiceUser.talking && !voiceUser.muted : false,
isListenOnly: voiceUser ? voiceUser.listenOnly : false,
};
};
const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectVoiceUser, usersProp) => {
const isDialInUser = isVoiceOnlyUser(subjectUser.userId) || subjectUser.phone_user;
const amISubjectUser = isMe(subjectUser.userId);
const isSubjectUserModerator = subjectUser.role === ROLE_MODERATOR;
const isSubjectUserGuest = subjectUser.guest;
const hasAuthority = amIModerator || amISubjectUser;
const allowedToChatPrivately = !amISubjectUser && !isDialInUser;
const allowedToMuteAudio = hasAuthority
&& subjectVoiceUser.isVoiceUser
&& !subjectVoiceUser.isMuted
&& !subjectVoiceUser.isListenOnly;
const allowedToUnmuteAudio = hasAuthority
&& subjectVoiceUser.isVoiceUser
&& !subjectVoiceUser.isListenOnly
&& subjectVoiceUser.isMuted
&& (amISubjectUser || usersProp.allowedToUnmuteAudio);
const allowedToResetStatus = hasAuthority
&& subjectUser.emoji !== EMOJI_STATUSES.none
&& !isDialInUser;
// if currentUser is a moderator, allow removing other users
const allowedToRemove = amIModerator
&& !amISubjectUser
&& !isBreakoutRoom;
const allowedToSetPresenter = amIModerator
&& !subjectUser.presenter
&& !isDialInUser;
const allowedToPromote = amIModerator
&& !amISubjectUser
&& !isSubjectUserModerator
&& !isDialInUser
&& !isBreakoutRoom
&& !(isSubjectUserGuest && usersProp.authenticatedGuest);
const allowedToDemote = amIModerator
&& !amISubjectUser
&& isSubjectUserModerator
&& !isDialInUser
&& !isBreakoutRoom
&& !(isSubjectUserGuest && usersProp.authenticatedGuest);
const allowedToChangeStatus = amISubjectUser;
const allowedToChangeUserLockStatus = amIModerator
&& !isSubjectUserModerator
&& isMeetingLocked(Auth.meetingID);
return {
allowedToChatPrivately,
allowedToMuteAudio,
allowedToUnmuteAudio,
allowedToResetStatus,
allowedToRemove,
allowedToSetPresenter,
allowedToPromote,
allowedToDemote,
allowedToChangeStatus,
allowedToChangeUserLockStatus,
};
};
const normalizeEmojiName = emoji => (
emoji in EMOJI_STATUSES ? EMOJI_STATUSES[emoji] : emoji
);
const setEmojiStatus = (userId, emoji) => {
const statusAvailable = (Object.keys(EMOJI_STATUSES).includes(emoji));
return statusAvailable
? makeCall('setEmojiStatus', Auth.userID, emoji)
: makeCall('setEmojiStatus', userId, 'none');
};
const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
const removeUser = (userId, banUser) => {
if (isVoiceOnlyUser(userId)) {
makeCall('ejectUserFromVoice', userId);
} else {
makeCall('removeUser', userId, banUser);
}
};
const toggleVoice = (userId) => {
if (userId === Auth.userID) {
AudioService.toggleMuteMicrophone();
} else {
makeCall('toggleVoice', userId);
logger.info({
logCode: 'usermenu_option_mute_toggle_audio',
extraInfo: { logType: 'moderator_action', userId },
}, 'moderator muted user microphone');
}
};
const getEmoji = () => {
const currentUser = Users.findOne({ userId: Auth.userID },
{ fields: { emoji: 1 } });
if (!currentUser) {
return false;
}
return currentUser.emoji;
};
const muteAllUsers = (userId) => { makeCall('muteAllUsers', userId); };
const muteAllExceptPresenter = (userId) => { makeCall('muteAllExceptPresenter', userId); };
const changeRole = (userId, role) => { makeCall('changeRole', userId, role); };
const focusFirstDropDownItem = () => {
const dropdownContent = document.querySelector('div[data-test="dropdownContent"][style="visibility: visible;"]');
if (!dropdownContent) return;
const list = dropdownContent.getElementsByTagName('li');
list[0].focus();
};
const roving = (...args) => {
const [
event,
changeState,
elementsList,
element,
] = args;
this.selectedElement = element;
const numberOfChilds = elementsList.childElementCount;
const menuOpen = Session.get('dropdownOpen') || false;
if (menuOpen) {
const menuChildren = document.activeElement.getElementsByTagName('li');
if ([KEY_CODES.ESCAPE, KEY_CODES.ARROW_LEFT].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.click();
}
if ([KEY_CODES.ARROW_UP].includes(event.keyCode)) {
menuChildren[menuChildren.length - 1].focus();
}
if ([KEY_CODES.ARROW_DOWN].includes(event.keyCode)) {
for (let i = 0; i < menuChildren.length; i += 1) {
if (menuChildren[i].hasAttribute('tabIndex')) {
menuChildren[i].focus();
break;
}
}
}
return;
}
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
Session.set('dropdownOpen', false);
document.activeElement.blur();
changeState(null);
}
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
const firstElement = elementsList.firstChild;
let elRef = element && numberOfChilds > 1 ? element.nextSibling : firstElement;
elRef = elRef || firstElement;
changeState(elRef);
}
if (event.keyCode === KEY_CODES.ARROW_UP) {
const lastElement = elementsList.lastChild;
let elRef = element ? element.previousSibling : lastElement;
elRef = elRef || lastElement;
changeState(elRef);
}
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE, KEY_CODES.ENTER].includes(event.keyCode)) {
const tether = document.activeElement.firstChild;
const dropdownTrigger = tether.firstChild;
dropdownTrigger.click();
focusFirstDropDownItem();
}
};
const hasPrivateChatBetweenUsers = (senderId, receiverId) => GroupChat
.findOne({ users: { $all: [receiverId, senderId] } });
const getGroupChatPrivate = (senderUserId, receiver) => {
const chat = hasPrivateChatBetweenUsers(senderUserId, receiver.userId);
if (!chat) {
makeCall('createGroupChat', receiver);
} else {
const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY);
if (_.indexOf(currentClosedChats, chat.chatId) > -1) {
Storage.setItem(CLOSED_CHAT_LIST_KEY, _.without(currentClosedChats, chat.chatId));
}
}
};
const toggleUserLock = (userId, lockStatus) => {
makeCall('toggleUserLock', userId, lockStatus);
};
const requestUserInformation = (userId) => {
makeCall('requestUserInformation', userId);
};
const sortUsersByFirstName = (a, b) => {
if (!a.firstName && !b.firstName) return 0;
if (a.firstName && !b.firstName) return -1;
if (!a.firstName && b.firstName) return 1;
const aName = a.firstName.toLowerCase();
const bName = b.firstName.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
};
const sortUsersByLastName = (a, b) => {
if (!a.lastName && !b.lastName) return 0;
if (a.lastName && !b.lastName) return -1;
if (!a.lastName && b.lastName) return 1;
const aName = a.lastName.toLowerCase();
const bName = b.lastName.toLowerCase();
if (aName < bName) return -1;
if (aName > bName) return 1;
return 0;
};
const isUserPresenter = (userId) => {
const user = Users.findOne({ userId },
{ fields: { presenter: 1 } });
return user ? user.presenter : false;
};
export const getUserNamesLink = (docTitle, fnSortedLabel, lnSortedLabel) => {
const mimeType = 'text/plain';
const userNamesObj = getUsers()
.map((u) => {
const name = u.name.split(' ');
return ({
firstName: name[0],
middleNames: name.length > 2 ? name.slice(1, name.length - 1) : null,
lastName: name.length > 1 ? name[name.length - 1] : null,
});
});
const getUsernameString = (user) => {
const { firstName, middleNames, lastName } = user;
return `${firstName || ''} ${middleNames && middleNames.length > 0 ? middleNames.join(' ') : ''} ${lastName || ''}`;
};
const namesByFirstName = userNamesObj.sort(sortUsersByFirstName)
.map(u => getUsernameString(u)).join('\r\n');
const namesByLastName = userNamesObj.sort(sortUsersByLastName)
.map(u => getUsernameString(u)).join('\r\n');
const namesListsString = `${docTitle}\r\n\r\n${fnSortedLabel}\r\n${namesByFirstName}
\r\n\r\n${lnSortedLabel}\r\n${namesByLastName}`.replace(/ {2}/g, ' ');
const link = document.createElement('a');
const meeting = Meetings.findOne({ meetingId: Auth.meetingID },
{ fields: { 'meetingProp.name': 1 } });
const date = new Date();
const time = `${date.getHours()}-${date.getMinutes()}`;
const dateString = `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}_${time}`;
link.setAttribute('download', `bbb-${meeting.meetingProp.name}[users-list]_${dateString}.txt`);
link.setAttribute(
'href',
`data: ${mimeType} ;charset=utf-16,${encodeURIComponent(namesListsString)}`,
);
return link;
};
export default {
sortUsersByName,
sortUsers,
setEmojiStatus,
assignPresenter,
removeUser,
toggleVoice,
muteAllUsers,
muteAllExceptPresenter,
changeRole,
getUsers,
getActiveChats,
getAvailableActions,
curatedVoiceUser,
normalizeEmojiName,
isMeetingLocked,
isPublicChat,
roving,
getCustomLogoUrl,
getGroupChatPrivate,
hasBreakoutRoom,
getEmojiList: () => EMOJI_STATUSES,
getEmoji,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
focusFirstDropDownItem,
isUserPresenter,
getUsersProp,
};