feat(bbb-html5): Ban specific users from the public chat (#20585)
* Initial user lock changes * Show lock icon
This commit is contained in:
parent
15525a6936
commit
fa0ad14c35
@ -0,0 +1,36 @@
|
|||||||
|
package org.bigbluebutton.core.apps.users
|
||||||
|
|
||||||
|
import org.bigbluebutton.LockSettingsUtil
|
||||||
|
import org.bigbluebutton.common2.msgs._
|
||||||
|
import org.bigbluebutton.core.models.{ Users2x, VoiceUsers }
|
||||||
|
import org.bigbluebutton.core.running.{ MeetingActor, OutMsgRouter }
|
||||||
|
import org.bigbluebutton.core.apps.{ PermissionCheck, RightsManagementTrait }
|
||||||
|
|
||||||
|
trait LockUserChatInMeetingCmdMsgHdlr extends RightsManagementTrait {
|
||||||
|
this: MeetingActor =>
|
||||||
|
|
||||||
|
val outGW: OutMsgRouter
|
||||||
|
|
||||||
|
def handleLockUserChatInMeetingCmdMsg(msg: LockUserChatInMeetingCmdMsg) {
|
||||||
|
|
||||||
|
def build(meetingId: String, userId: String, isLocked: Boolean): BbbCommonEnvCoreMsg = {
|
||||||
|
val routing = Routing.addMsgToClientRouting(MessageTypes.BROADCAST_TO_MEETING, meetingId, userId)
|
||||||
|
val envelope = BbbCoreEnvelope(LockUserChatInMeetingEvtMsg.NAME, routing)
|
||||||
|
val body = LockUserChatInMeetingEvtMsgBody(userId, isLocked)
|
||||||
|
val header = BbbClientMsgHeader(LockUserChatInMeetingEvtMsg.NAME, meetingId, userId)
|
||||||
|
val event = LockUserChatInMeetingEvtMsg(header, body)
|
||||||
|
|
||||||
|
BbbCommonEnvCoreMsg(envelope, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionFailed(PermissionCheck.MOD_LEVEL, PermissionCheck.VIEWER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
|
||||||
|
val meetingId = liveMeeting.props.meetingProp.intId
|
||||||
|
val reason = "No permission to lock user chat in meeting."
|
||||||
|
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, outGW, liveMeeting)
|
||||||
|
} else {
|
||||||
|
log.info("Lock user chat. meetingId=" + props.meetingProp.intId + " userId=" + msg.body.userId + " isLocked=" + msg.body.isLocked)
|
||||||
|
val event = build(props.meetingProp.intId, msg.body.userId, msg.body.isLocked)
|
||||||
|
outGW.send(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import org.bigbluebutton.core.running.MeetingActor
|
|||||||
trait UsersApp2x
|
trait UsersApp2x
|
||||||
extends UserLeaveReqMsgHdlr
|
extends UserLeaveReqMsgHdlr
|
||||||
with LockUserInMeetingCmdMsgHdlr
|
with LockUserInMeetingCmdMsgHdlr
|
||||||
|
with LockUserChatInMeetingCmdMsgHdlr
|
||||||
with LockUsersInMeetingCmdMsgHdlr
|
with LockUsersInMeetingCmdMsgHdlr
|
||||||
with GetLockSettingsReqMsgHdlr
|
with GetLockSettingsReqMsgHdlr
|
||||||
with ChangeUserEmojiCmdMsgHdlr
|
with ChangeUserEmojiCmdMsgHdlr
|
||||||
|
@ -386,6 +386,8 @@ class ReceivedJsonMsgHandlerActor(
|
|||||||
// Lock settings
|
// Lock settings
|
||||||
case LockUserInMeetingCmdMsg.NAME =>
|
case LockUserInMeetingCmdMsg.NAME =>
|
||||||
routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode)
|
routeGenericMsg[LockUserInMeetingCmdMsg](envelope, jsonNode)
|
||||||
|
case LockUserChatInMeetingCmdMsg.NAME =>
|
||||||
|
routeGenericMsg[LockUserChatInMeetingCmdMsg](envelope, jsonNode)
|
||||||
case ChangeLockSettingsInMeetingCmdMsg.NAME =>
|
case ChangeLockSettingsInMeetingCmdMsg.NAME =>
|
||||||
routeGenericMsg[ChangeLockSettingsInMeetingCmdMsg](envelope, jsonNode)
|
routeGenericMsg[ChangeLockSettingsInMeetingCmdMsg](envelope, jsonNode)
|
||||||
case LockUsersInMeetingCmdMsg.NAME =>
|
case LockUsersInMeetingCmdMsg.NAME =>
|
||||||
|
@ -514,6 +514,7 @@ class MeetingActor(
|
|||||||
handleSetLockSettings(m)
|
handleSetLockSettings(m)
|
||||||
updateUserLastActivity(m.body.setBy)
|
updateUserLastActivity(m.body.setBy)
|
||||||
case m: LockUserInMeetingCmdMsg => handleLockUserInMeetingCmdMsg(m)
|
case m: LockUserInMeetingCmdMsg => handleLockUserInMeetingCmdMsg(m)
|
||||||
|
case m: LockUserChatInMeetingCmdMsg => handleLockUserChatInMeetingCmdMsg(m)
|
||||||
case m: LockUsersInMeetingCmdMsg => handleLockUsersInMeetingCmdMsg(m)
|
case m: LockUsersInMeetingCmdMsg => handleLockUsersInMeetingCmdMsg(m)
|
||||||
case m: GetLockSettingsReqMsg => handleGetLockSettingsReqMsg(m)
|
case m: GetLockSettingsReqMsg => handleGetLockSettingsReqMsg(m)
|
||||||
|
|
||||||
|
@ -347,6 +347,20 @@ object UserLockedInMeetingEvtMsg { val NAME = "UserLockedInMeetingEvtMsg" }
|
|||||||
case class UserLockedInMeetingEvtMsg(header: BbbClientMsgHeader, body: UserLockedInMeetingEvtMsgBody) extends BbbCoreMsg
|
case class UserLockedInMeetingEvtMsg(header: BbbClientMsgHeader, body: UserLockedInMeetingEvtMsgBody) extends BbbCoreMsg
|
||||||
case class UserLockedInMeetingEvtMsgBody(userId: String, locked: Boolean, lockedBy: String)
|
case class UserLockedInMeetingEvtMsgBody(userId: String, locked: Boolean, lockedBy: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sent from client to lock user in meeting.
|
||||||
|
*/
|
||||||
|
object LockUserChatInMeetingCmdMsg { val NAME = "LockUserChatInMeetingCmdMsg" }
|
||||||
|
case class LockUserChatInMeetingCmdMsg(header: BbbClientMsgHeader, body: LockUserChatInMeetingCmdMsgBody) extends StandardMsg
|
||||||
|
case class LockUserChatInMeetingCmdMsgBody(userId: String, isLocked: Boolean, lockedBy: String)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send to client that user has been locked.
|
||||||
|
*/
|
||||||
|
object LockUserChatInMeetingEvtMsg { val NAME = "LockUserChatInMeetingEvtMsg" }
|
||||||
|
case class LockUserChatInMeetingEvtMsg(header: BbbClientMsgHeader, body: LockUserChatInMeetingEvtMsgBody) extends BbbCoreMsg
|
||||||
|
case class LockUserChatInMeetingEvtMsgBody(userId: String, isLocked: Boolean)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sent by client to lock users.
|
* Sent by client to lock users.
|
||||||
*/
|
*/
|
||||||
|
@ -7,6 +7,7 @@ import handleMeetingLocksChange from './handlers/meetingLockChange';
|
|||||||
import handleGuestPolicyChanged from './handlers/guestPolicyChanged';
|
import handleGuestPolicyChanged from './handlers/guestPolicyChanged';
|
||||||
import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
|
import handleGuestLobbyMessageChanged from './handlers/guestLobbyMessageChanged';
|
||||||
import handleUserLockChange from './handlers/userLockChange';
|
import handleUserLockChange from './handlers/userLockChange';
|
||||||
|
import handleUserChatLockChange from './handlers/handleUserChatLockChange';
|
||||||
import handleRecordingStatusChange from './handlers/recordingStatusChange';
|
import handleRecordingStatusChange from './handlers/recordingStatusChange';
|
||||||
import handleRecordingTimerChange from './handlers/recordingTimerChange';
|
import handleRecordingTimerChange from './handlers/recordingTimerChange';
|
||||||
import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate';
|
import handleTimeRemainingUpdate from './handlers/timeRemainingUpdate';
|
||||||
@ -24,6 +25,7 @@ RedisPubSub.on('MeetingEndingEvtMsg', handleMeetingEnd);
|
|||||||
RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
|
RedisPubSub.on('MeetingDestroyedEvtMsg', handleMeetingDestruction);
|
||||||
RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
|
RedisPubSub.on('LockSettingsInMeetingChangedEvtMsg', handleMeetingLocksChange);
|
||||||
RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
|
RedisPubSub.on('UserLockedInMeetingEvtMsg', handleUserLockChange);
|
||||||
|
RedisPubSub.on('LockUserChatInMeetingEvtMsg', handleUserChatLockChange);
|
||||||
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
|
RedisPubSub.on('RecordingStatusChangedEvtMsg', handleRecordingStatusChange);
|
||||||
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
|
RedisPubSub.on('UpdateRecordingTimerEvtMsg', handleRecordingTimerChange);
|
||||||
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
|
RedisPubSub.on('WebcamsOnlyForModeratorChangedEvtMsg', handleChangeWebcamOnlyModerator);
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import changeUserChatLock from '../modifiers/changeUserChatLock';
|
||||||
|
|
||||||
|
export default async function handleUserChatLockChange({ body }, meetingId) {
|
||||||
|
await changeUserChatLock(meetingId, body);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
import Users from '/imports/api/users';
|
||||||
|
import Meetings from '/imports/api/meetings';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
|
||||||
|
export default async function changeUserChatLock(meetingId, payload) {
|
||||||
|
check(meetingId, String);
|
||||||
|
check(payload, {
|
||||||
|
userId: String,
|
||||||
|
isLocked: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { userId, isLocked } = payload;
|
||||||
|
|
||||||
|
const userSelector = {
|
||||||
|
meetingId,
|
||||||
|
userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const userModifier = {
|
||||||
|
$set: {
|
||||||
|
chatLocked: isLocked,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { numberAffected } = await Users.upsertAsync(userSelector, userModifier);
|
||||||
|
|
||||||
|
if ( numberAffected ) {
|
||||||
|
Logger.info(`Updated lock settings in meeting ${meetingId}: disablePublicChat=${isLocked}`);
|
||||||
|
} else {
|
||||||
|
Logger.info(`Kept lock settings in meeting ${meetingId}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`Changing user chat lock setting: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,7 @@ import assignPresenter from './methods/assignPresenter';
|
|||||||
import changeRole from './methods/changeRole';
|
import changeRole from './methods/changeRole';
|
||||||
import removeUser from './methods/removeUser';
|
import removeUser from './methods/removeUser';
|
||||||
import toggleUserLock from './methods/toggleUserLock';
|
import toggleUserLock from './methods/toggleUserLock';
|
||||||
|
import toggleUserChatLock from './methods/toggleUserChatLock';
|
||||||
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
|
import setUserEffectiveConnectionType from './methods/setUserEffectiveConnectionType';
|
||||||
import userActivitySign from './methods/userActivitySign';
|
import userActivitySign from './methods/userActivitySign';
|
||||||
import userLeftMeeting from './methods/userLeftMeeting';
|
import userLeftMeeting from './methods/userLeftMeeting';
|
||||||
@ -31,6 +32,7 @@ Meteor.methods({
|
|||||||
removeUser,
|
removeUser,
|
||||||
validateAuthToken,
|
validateAuthToken,
|
||||||
toggleUserLock,
|
toggleUserLock,
|
||||||
|
toggleUserChatLock,
|
||||||
setUserEffectiveConnectionType,
|
setUserEffectiveConnectionType,
|
||||||
userActivitySign,
|
userActivitySign,
|
||||||
userLeftMeeting,
|
userLeftMeeting,
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Meteor } from 'meteor/meteor';
|
||||||
|
import { check } from 'meteor/check';
|
||||||
|
import RedisPubSub from '/imports/startup/server/redis';
|
||||||
|
import Logger from '/imports/startup/server/logger';
|
||||||
|
import { extractCredentials } from '/imports/api/common/server/helpers';
|
||||||
|
|
||||||
|
export default function toggleUserChatLock(userId, isLocked) {
|
||||||
|
try {
|
||||||
|
const REDIS_CONFIG = Meteor.settings.private.redis;
|
||||||
|
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||||
|
const EVENT_NAME = 'LockUserChatInMeetingCmdMsg';
|
||||||
|
|
||||||
|
const { meetingId, requesterUserId: lockedBy } = extractCredentials(this.userId);
|
||||||
|
|
||||||
|
check(meetingId, String);
|
||||||
|
check(lockedBy, String);
|
||||||
|
check(userId, String);
|
||||||
|
check(isLocked, Boolean);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
lockedBy,
|
||||||
|
userId,
|
||||||
|
isLocked,
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.verbose('Updated chat lock status for user', {
|
||||||
|
meetingId, userId, isLocked, lockedBy,
|
||||||
|
});
|
||||||
|
|
||||||
|
RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, lockedBy, payload);
|
||||||
|
} catch (err) {
|
||||||
|
Logger.error(`Exception while invoking method toggleUserChatLock ${err.stack}`);
|
||||||
|
}
|
||||||
|
}
|
@ -150,6 +150,8 @@ const ChatContainer = (props) => {
|
|||||||
partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut
|
partnerIsLoggedOut = !!(users[Auth.meetingID][idUser]?.loggedOut
|
||||||
|| users[Auth.meetingID][idUser]?.ejected);
|
|| users[Auth.meetingID][idUser]?.ejected);
|
||||||
isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR);
|
isChatLocked = isChatLockedPrivate && !(users[Auth.meetingID][idUser]?.role === ROLE_MODERATOR);
|
||||||
|
} else if (users[Auth.meetingID][Auth.userID]?.chatLocked === true) {
|
||||||
|
isChatLocked = true;
|
||||||
} else {
|
} else {
|
||||||
isChatLocked = isChatLockedPublic;
|
isChatLocked = isChatLockedPublic;
|
||||||
}
|
}
|
||||||
|
@ -169,6 +169,10 @@ const isChatLocked = (receiverID) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.chatLocked === true && user.role !== ROLE_MODERATOR) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -674,6 +674,10 @@ const getGroupChatPrivate = (senderUserId, receiver) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleUserChatLock = (userId, isLocked) => {
|
||||||
|
makeCall('toggleUserChatLock', userId, isLocked);
|
||||||
|
}
|
||||||
|
|
||||||
const toggleUserLock = (userId, lockStatus) => {
|
const toggleUserLock = (userId, lockStatus) => {
|
||||||
makeCall('toggleUserLock', userId, lockStatus);
|
makeCall('toggleUserLock', userId, lockStatus);
|
||||||
};
|
};
|
||||||
@ -820,6 +824,7 @@ export default {
|
|||||||
roving,
|
roving,
|
||||||
getCustomLogoUrl,
|
getCustomLogoUrl,
|
||||||
getGroupChatPrivate,
|
getGroupChatPrivate,
|
||||||
|
toggleUserChatLock,
|
||||||
hasBreakoutRoom,
|
hasBreakoutRoom,
|
||||||
getEmojiList: () => EMOJI_STATUSES,
|
getEmojiList: () => EMOJI_STATUSES,
|
||||||
getEmoji,
|
getEmoji,
|
||||||
|
@ -47,6 +47,14 @@ const messages = defineMessages({
|
|||||||
id: 'app.audio.backLabel',
|
id: 'app.audio.backLabel',
|
||||||
description: 'label for option to hide emoji menu',
|
description: 'label for option to hide emoji menu',
|
||||||
},
|
},
|
||||||
|
lockPublicChat: {
|
||||||
|
id: 'app.userList.menu.lockPublicChat.label',
|
||||||
|
description: 'label for option to lock user\'s public chat',
|
||||||
|
},
|
||||||
|
unlockPublicChat: {
|
||||||
|
id: 'app.userList.menu.unlockPublicChat.label',
|
||||||
|
description: 'label for option to lock user\'s public chat',
|
||||||
|
},
|
||||||
StartPrivateChat: {
|
StartPrivateChat: {
|
||||||
id: 'app.userList.menu.chat.label',
|
id: 'app.userList.menu.chat.label',
|
||||||
description: 'label for option to start a new private chat',
|
description: 'label for option to start a new private chat',
|
||||||
@ -175,6 +183,7 @@ const propTypes = {
|
|||||||
normalizeEmojiName: PropTypes.func.isRequired,
|
normalizeEmojiName: PropTypes.func.isRequired,
|
||||||
getScrollContainerRef: PropTypes.func.isRequired,
|
getScrollContainerRef: PropTypes.func.isRequired,
|
||||||
toggleUserLock: PropTypes.func.isRequired,
|
toggleUserLock: PropTypes.func.isRequired,
|
||||||
|
toggleUserChatLock: PropTypes.func.isRequired,
|
||||||
isMeteorConnected: PropTypes.bool.isRequired,
|
isMeteorConnected: PropTypes.bool.isRequired,
|
||||||
isMe: PropTypes.func.isRequired,
|
isMe: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
@ -286,6 +295,7 @@ class UserListItem extends PureComponent {
|
|||||||
voiceUser,
|
voiceUser,
|
||||||
getAvailableActions,
|
getAvailableActions,
|
||||||
getGroupChatPrivate,
|
getGroupChatPrivate,
|
||||||
|
toggleUserChatLock,
|
||||||
getEmojiList,
|
getEmojiList,
|
||||||
setEmojiStatus,
|
setEmojiStatus,
|
||||||
setUserAway,
|
setUserAway,
|
||||||
@ -345,6 +355,7 @@ class UserListItem extends PureComponent {
|
|||||||
|
|
||||||
const { allowUserLookup } = Meteor.settings.public.app;
|
const { allowUserLookup } = Meteor.settings.public.app;
|
||||||
const userLocked = user.locked && user.role !== ROLE_MODERATOR;
|
const userLocked = user.locked && user.role !== ROLE_MODERATOR;
|
||||||
|
const userChatLocked = user.chatLocked;
|
||||||
|
|
||||||
const availableActions = [
|
const availableActions = [
|
||||||
{
|
{
|
||||||
@ -406,6 +417,29 @@ class UserListItem extends PureComponent {
|
|||||||
icon: 'chat',
|
icon: 'chat',
|
||||||
dataTest: 'startPrivateChat',
|
dataTest: 'startPrivateChat',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
allowed: isChatEnabled()
|
||||||
|
&& user.role !== ROLE_MODERATOR
|
||||||
|
&& currentUser.role === ROLE_MODERATOR
|
||||||
|
&& !isDialInUser
|
||||||
|
&& isMeteorConnected,
|
||||||
|
key: 'lockChat',
|
||||||
|
label: userChatLocked ? intl.formatMessage(messages.unlockPublicChat) : intl.formatMessage(messages.lockPublicChat),
|
||||||
|
onClick: () => {
|
||||||
|
this.handleClose();
|
||||||
|
toggleUserChatLock(user.userId, !userChatLocked);
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
layoutContextDispatch({
|
||||||
|
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
|
||||||
|
value: PANELS.CHAT,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
icon: userChatLocked ? 'unlock' : 'lock',
|
||||||
|
dataTest: 'togglePublicChat',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
allowed: allowedToResetStatus
|
allowed: allowedToResetStatus
|
||||||
&& user.emoji !== 'none'
|
&& user.emoji !== 'none'
|
||||||
@ -786,7 +820,7 @@ class UserListItem extends PureComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isThisMeetingLocked && user.locked && user.role !== ROLE_MODERATOR) {
|
if (((isThisMeetingLocked && user.locked) || user.chatLocked) && user.role !== ROLE_MODERATOR) {
|
||||||
userNameSub.push(
|
userNameSub.push(
|
||||||
<span key={uniqueId('lock-')}>
|
<span key={uniqueId('lock-')}>
|
||||||
<Icon iconName="lock" />
|
<Icon iconName="lock" />
|
||||||
|
@ -14,6 +14,7 @@ const UserListItemContainer = (props) => {
|
|||||||
toggleVoice,
|
toggleVoice,
|
||||||
removeUser,
|
removeUser,
|
||||||
toggleUserLock,
|
toggleUserLock,
|
||||||
|
toggleUserChatLock,
|
||||||
changeRole,
|
changeRole,
|
||||||
ejectUserCameras,
|
ejectUserCameras,
|
||||||
assignPresenter,
|
assignPresenter,
|
||||||
@ -29,6 +30,7 @@ const UserListItemContainer = (props) => {
|
|||||||
toggleVoice,
|
toggleVoice,
|
||||||
removeUser,
|
removeUser,
|
||||||
toggleUserLock,
|
toggleUserLock,
|
||||||
|
toggleUserChatLock,
|
||||||
changeRole,
|
changeRole,
|
||||||
ejectUserCameras,
|
ejectUserCameras,
|
||||||
assignPresenter,
|
assignPresenter,
|
||||||
|
@ -151,6 +151,8 @@
|
|||||||
"app.userList.menu.away": "Set yourself as away",
|
"app.userList.menu.away": "Set yourself as away",
|
||||||
"app.userList.menu.notAway": "Set yourself as active",
|
"app.userList.menu.notAway": "Set yourself as active",
|
||||||
"app.userList.menu.chat.label": "Start a private chat",
|
"app.userList.menu.chat.label": "Start a private chat",
|
||||||
|
"app.userList.menu.lockPublicChat.label": "Lock public chat",
|
||||||
|
"app.userList.menu.unlockPublicChat.label": "Unlock public chat",
|
||||||
"app.userList.menu.clearStatus.label": "Clear status",
|
"app.userList.menu.clearStatus.label": "Clear status",
|
||||||
"app.userList.menu.removeUser.label": "Remove user",
|
"app.userList.menu.removeUser.label": "Remove user",
|
||||||
"app.userList.menu.removeConfirmation.label": "Remove user ({0})",
|
"app.userList.menu.removeConfirmation.label": "Remove user ({0})",
|
||||||
|
Loading…
Reference in New Issue
Block a user