feat: Lobby chat (#10847)

* feat(lobby): lobby chat

lobby chat support
knocking participants list updates
knocking participants conditonal checks to show message button
handle lobby chat message events
lobby messages from or to moderators only

Co-authored-by: Fecri Kaan Ulubey <f.kaan93@gmail.com>

* squash: Drop typos.

Co-authored-by: Kusi Musah Hussein <kusimusah@gmail.com>
Co-authored-by: Fecri Kaan Ulubey <f.kaan93@gmail.com>
Co-authored-by: Дамян Минков <damencho@jitsi.org>
This commit is contained in:
Doganbros 2022-03-03 20:29:38 +03:00 committed by GitHub
parent 7a4a234f8e
commit 7522de033a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1401 additions and 95 deletions

View File

@ -126,6 +126,7 @@ import {
maybeOpenFeedbackDialog,
submitFeedback
} from './react/features/feedback';
import { maybeSetLobbyChatMessageListener } from './react/features/lobby/actions.any';
import {
isModerationNotificationDisplayed,
showNotification,
@ -2102,6 +2103,10 @@ export default {
if (this.isLocalId(id)) {
logger.info(`My role changed, new role: ${role}`);
if (role === 'moderator') {
APP.store.dispatch(maybeSetLobbyChatMessageListener());
}
APP.store.dispatch(localParticipantRoleChanged(role));
APP.API.notifyUserRoleChanged(id, role);
} else {

View File

@ -473,6 +473,9 @@ var config = {
// If Lobby is enabled starts knocking automatically.
// autoKnockLobby: false,
// Enable lobby chat.
// enableLobbyChat: true,
// DEPRECATED! Use `breakoutRooms.hideAddRoomButton` instead.
// Hides add breakout room button
// hideAddRoomButton: false,

View File

@ -85,6 +85,10 @@
fill: white;
}
}
&.lobby-chat-recipient {
background-color: $chatLobbyMessageBackgroundColor;
}
}
@ -455,6 +459,9 @@
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
&.lobbymessage {
background-color: $chatLobbyMessageBackgroundColor;
}
}
.display-name {
@ -494,6 +501,10 @@
justify-content: center;
padding: 5px;
&.lobbychatmessageactions {
border-left-color: $chatLobbyActionsSeparatorColor;
}
.toolbox-icon {
cursor: pointer;
}
@ -511,6 +522,9 @@
&.privatemessage {
background-color: $chatPrivateMessageBackgroundColor;
}
&.lobbymessage {
background-color: $chatLobbyMessageBackgroundColor;
}
}
}

View File

@ -83,6 +83,8 @@ $modalTextColor: #333;
$chatActionsSeparatorColor: rgb(173, 105, 112);
$chatBackgroundColor: #131519;
$chatInputSeparatorColor: #A4B8D1;
$chatLobbyMessageBackgroundColor: #6A50D3;
$chatLobbyActionsSeparatorColor: #6A50D3;
$chatLocalMessageBackgroundColor: #484A4F;
$chatPrivateMessageBackgroundColor: rgb(153, 69, 77);
$chatRemoteMessageBackgroundColor: #242528;

View File

@ -12,11 +12,29 @@
margin: 8px;
}
.lobby-chat-container {
background-color: $chatBackgroundColor;
width: 100%;
height: 314px;
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: 16px;
border-radius: 5px;
.lobby-chat-header {
display: none;
}
}
.joining-message {
color: white;
margin: 24px auto;
text-align: center;
}
.open-chat-button {
display: none;
}
}
}
@ -40,3 +58,149 @@
}
}
}
#notification-participant-list {
background-color: $newToolbarBackgroundColor;
border: 1px solid rgba(255, 255, 255, .4);
border-radius: 8px;
left: 0;
margin: 20px;
max-height: 600px;
overflow: hidden;
overflow-y: auto;
position: fixed;
top: 30px;
z-index: $toolbarZ + 1;
&:empty {
border: none;
}
&.toolbox-visible {
// Same as toolbox subject position
top: 120px;
}
&.avoid-chat {
left: 315px;
}
.title {
background-color: rgba(0, 0, 0, .2);
font-size: 1.2em;
padding: 15px
}
button {
align-self: stretch;
margin-bottom: 8px 0;
padding: 12px;
transition: .2s transform ease;
&:disabled {
opacity: .5;
}
&:hover {
transform: scale(1.05);
&:disabled {
transform: none;
}
}
&.borderLess {
background-color: transparent;
border-width: 0;
}
&.primary {
background-color: rgb(3, 118, 218);
border-width: 0;
}
}
}
.knocking-participants-container {
list-style-type: none;
padding: 0 15px 15px 15px;
}
.knocking-participant {
align-items: center;
display: flex;
flex-direction: row;
margin: 8px 0;
.details {
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-evenly;
margin: 0 30px 0 10px;
}
button {
align-self: unset;
margin: 0 5px;
}
}
@media (max-width: 300px) {
#knocking-participant-list {
margin: 0;
text-align: center;
width: 100%;
.avatar {
display: none;
}
}
.knocking-participant {
flex-direction: column;
.details {
margin: 0;
}
}
}
@media (max-width: 1000px) {
.lobby-screen-content {
.lobby-chat-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 255;
&.hidden {
display: none;
}
.lobby-chat-header {
display: flex;
flex-direction: row;
padding-top: 20px;
padding-left: 16px;
padding-right: 16px;
.title {
flex: 1;
color: #fff;
font-size: 20px;
font-weight: 600;
line-height: 28px;
letter-spacing: -1.2%;
}
}
}
.open-chat-button {
display: block;
}
}
}

View File

@ -68,6 +68,7 @@
"enter": "Odaya gir",
"error": "Hata: Mesajınız gönderilmedi. Neden: {{error}}",
"fieldPlaceHolder": "Mesajınızı buraya yazın",
"lobbyChatMessageTo": "{{recipient}} adlı kişiye lobi mesajı",
"message": "Mesaj",
"messageAccessibleTitle": "{{user}} diyor:",
"messageAccessibleTitleMe": "ben diyorum:",
@ -526,6 +527,7 @@
"admitAll": "Hepsini kabul et",
"allow": "İzin ver",
"backToKnockModeButton": "Parola yok, bunun yerine katılmayı isteyin",
"chat": "Sohbet et",
"dialogTitle": "Lobi modu",
"disableDialogContent": "Lobi modu şu anda etkin. Bu özellik, istenmeyen katılımcıların toplantınıza katılamamasını sağlar. Devre dışı bırakmak istiyor musunuz?",
"disableDialogSubmit": "Devre Dışı",
@ -546,6 +548,8 @@
"knockButton": "Katılmak için sor",
"knockTitle": "Birisi toplantıya katılmak istiyor",
"knockingParticipantList": "Kapıyı çalan katılımcı listesi",
"lobbyChatStartedNotification": "{{moderator}} {{attendee}} adlı kişiyle lobi mesajlaşması başlattı",
"lobbyChatStartedTitle": "{{moderator}} sizinle lobi mesajlaşması başlattı",
"nameField": "Adınızı giriniz",
"notificationLobbyAccessDenied": "{{targetParticipantName}} adlı katılımcı {{originParticipantName}} tarafından reddedildi",
"notificationLobbyAccessGranted": "{{targetParticipantName}} adlı katılımcı {{originParticipantName}} tarafından kabul edildi",

View File

@ -83,6 +83,7 @@
"enter": "Enter room",
"error": "Error: your message was not sent. Reason: {{error}}",
"fieldPlaceHolder": "Type your message here",
"lobbyChatMessageTo": "Lobby chat message to {{recipient}}",
"message": "Message",
"messageAccessibleTitle": "{{user}} says:",
"messageAccessibleTitleMe": "me says:",
@ -529,6 +530,7 @@
"admitAll": "Admit all",
"allow": "Allow",
"backToKnockModeButton": "Ask to join",
"chat": "Chat",
"dialogTitle": "Lobby mode",
"disableDialogContent": "Lobby mode is currently enabled. This feature ensures that unwanted participants can't join your meeting. Do you want to disable it?",
"disableDialogSubmit": "Disable",
@ -549,6 +551,8 @@
"knockButton": "Ask to Join",
"knockTitle": "Someone wants to join the meeting",
"knockingParticipantList": "Knocking participant list",
"lobbyChatStartedNotification": "{{moderator}} started a lobby chat with {{attendee}}",
"lobbyChatStartedTitle": "{{moderator}} has started a lobby chat with you.",
"nameField": "Enter your name",
"notificationLobbyAccessDenied": "{{targetParticipantName}} has been rejected to join by {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} has been allowed to join by {{originParticipantName}}",

View File

@ -17,6 +17,8 @@ export default {
'Chat': {
displayName: 'rgb(94, 109, 121)',
localMsgBackground: 'rgb(215, 230, 249)',
lobbyMsgBackground: 'rgb(106, 80, 211)',
lobbyMsgNotice: 'rgb(16, 10, 41)',
privateMsgBackground: 'rgb(250, 219, 219)',
privateMsgNotice: 'rgb(186, 39, 58)',
remoteMsgBackground: 'rgb(241, 242, 246)',

View File

@ -9,6 +9,7 @@ import {
sendAnalytics
} from '../../analytics';
import { reloadNow } from '../../app/actions';
import { removeLobbyChatParticipant } from '../../chat/actions.any';
import { openDisplayNamePrompt } from '../../display-name';
import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification } from '../../notifications';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
@ -212,6 +213,8 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
const { pendingSubjectChange } = getState()['features/base/conference'];
const { requireDisplayName, disableBeforeUnloadHandlers = false } = getState()['features/base/config'];
dispatch(removeLobbyChatParticipant(true));
pendingSubjectChange && dispatch(setSubject(pendingSubjectChange));
// FIXME: Very dirty solution. This will work on web only.

View File

@ -142,6 +142,7 @@ export default [
'enableInsecureRoomNameWarning',
'enableLayerSuspension',
'enableLipSync',
'enableLobbyChat',
'enableOpusRed',
'enableRemb',
'enableSaveLogs',

View File

@ -84,3 +84,32 @@ export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
* }
*/
export const SET_IS_POLL_TAB_FOCUSED = 'SET_IS_POLL_TAB_FOCUSED';
/**
* The type of action which sets the current recipient for lobby messages.
*
* {
* participant: Object,
* type: SET_LOBBY_CHAT_RECIPIENT
* }
*/
export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT';
/**
* The type of action sets the state of lobby messaging status.
*
* {
* type: SET_LOBBY_CHAT_ACTIVE_STATE
* payload: boolean
* }
*/
export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE';
/**
* The type of action removes the lobby messaging from participant.
*
* {
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
* }
*/
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';

View File

@ -1,4 +1,9 @@
// @flow
import { type Dispatch } from 'redux';
import { getCurrentConference } from '../base/conference';
import { getLocalParticipant } from '../base/participants';
import { LOBBY_CHAT_INITIALIZED } from '../lobby/constants';
import {
ADD_MESSAGE,
@ -7,7 +12,10 @@ import {
EDIT_MESSAGE,
SEND_MESSAGE,
SET_PRIVATE_MESSAGE_RECIPIENT,
SET_IS_POLL_TAB_FOCUSED
SET_IS_POLL_TAB_FOCUSED,
SET_LOBBY_CHAT_RECIPIENT,
REMOVE_LOBBY_CHAT_PARTICIPANT,
SET_LOBBY_CHAT_ACTIVE_STATE
} from './actionTypes';
/**
@ -132,3 +140,124 @@ export function setIsPollsTabFocused(isPollsTabFocused: boolean) {
type: SET_IS_POLL_TAB_FOCUSED
};
}
/**
* Initiates the sending of messages between a moderator and a lobby attendee.
*
* @param {Object} lobbyChatInitializedInfo - The information about the attendee and the moderator
* that is going to chat.
*
* @returns {Function}
*/
export function onLobbyChatInitialized(lobbyChatInitializedInfo: Object) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const conference = getCurrentConference(state);
const lobbyLocalId = conference.myLobbyUserId();
if (!lobbyLocalId) {
return;
}
if (lobbyChatInitializedInfo.moderator.id === lobbyLocalId) {
dispatch({
type: SET_LOBBY_CHAT_RECIPIENT,
participant: lobbyChatInitializedInfo.attendee,
open: true
});
}
if (lobbyChatInitializedInfo.attendee.id === lobbyLocalId) {
return dispatch({
type: SET_LOBBY_CHAT_RECIPIENT,
participant: lobbyChatInitializedInfo.moderator,
open: false
});
}
};
}
/**
* Sets the lobby room's chat active state.
*
* @param {boolean} value - The active state.
*
* @returns {Object}
*/
export function setLobbyChatActiveState(value: boolean) {
return {
type: SET_LOBBY_CHAT_ACTIVE_STATE,
payload: value
};
}
/**
* Removes lobby type messages.
*
* @param {boolean} removeLobbyChatMessages - Should remove messages from chat (works only for accepted users).
* If not specified, it will delete all lobby messages.
*
* @returns {Object}
*/
export function removeLobbyChatParticipant(removeLobbyChatMessages: ?boolean) {
return {
type: REMOVE_LOBBY_CHAT_PARTICIPANT,
removeLobbyChatMessages
};
}
/**
* Handles initial setup of lobby message between
* Moderator and participant.
*
* @param {string} participantId - The participant id.
*
* @returns {Object}
*/
export function handleLobbyChatInitialized(participantId: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
if (!participantId) {
return;
}
const state = getState();
const conference = state['features/base/conference'].conference;
const { knockingParticipants } = state['features/lobby'];
const { lobbyMessageRecipient } = state['features/chat'];
const me = getLocalParticipant(state);
const lobbyLocalId = conference.myLobbyUserId();
if (lobbyMessageRecipient && lobbyMessageRecipient.id === participantId) {
return dispatch(setLobbyChatActiveState(true));
}
const attendee = knockingParticipants.find(p => p.id === participantId);
if (attendee && attendee.chattingWithModerator === lobbyLocalId) {
return dispatch({
type: SET_LOBBY_CHAT_RECIPIENT,
participant: attendee,
open: true
});
}
if (!attendee) {
return;
}
const payload = { type: LOBBY_CHAT_INITIALIZED,
moderator: {
...me,
name: 'Moderator',
id: lobbyLocalId
},
attendee };
// notify attendee privately.
conference.sendLobbyMessage(payload, attendee.id);
// notify other moderators.
return conference.sendLobbyMessage(payload);
};
}

View File

@ -38,6 +38,11 @@ export type Props = {
*/
showTimestamp: boolean,
/**
* Whether current participant is currently knocking in the lobby room.
*/
knocking: boolean,
/**
* Invoked to receive translated strings.
*/

View File

@ -2,8 +2,9 @@
import { PureComponent } from 'react';
import { getParticipantDisplayName } from '../../base/participants';
import { getParticipantDisplayName, isLocalParticipantModerator } from '../../base/participants';
import { setPrivateMessageRecipient } from '../actions';
import { setLobbyChatActiveState } from '../actions.any';
export type Props = {
@ -17,10 +18,30 @@ export type Props = {
*/
_onRemovePrivateMessageRecipient: Function,
/**
* Function to make the lobby message receipient inactive.
*/
_onHideLobbyChatRecipient: Function,
/**
* The name of the message recipient, if any.
*/
_privateMessageRecipient: ?string
_privateMessageRecipient: ?string,
/**
* Is lobby messaging active.
*/
_isLobbyChatActive: boolean,
/**
* The name of the lobby message recipient, if any.
*/
_lobbyMessageRecipient: ?string,
/**
* Shows widget if it is necessary.
*/
_visible: boolean;
};
/**
@ -40,6 +61,9 @@ export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
return {
_onRemovePrivateMessageRecipient: () => {
dispatch(setPrivateMessageRecipient());
},
_onHideLobbyChatRecipient: () => {
dispatch(setLobbyChatActiveState(false));
}
};
}
@ -51,10 +75,14 @@ export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
* @returns {Props}
*/
export function _mapStateToProps(state: Object): $Shape<Props> {
const { privateMessageRecipient } = state['features/chat'];
const { privateMessageRecipient, lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
return {
_privateMessageRecipient:
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined,
_isLobbyChatActive: isLobbyChatActive,
_lobbyMessageRecipient:
isLobbyChatActive && lobbyMessageRecipient ? lobbyMessageRecipient.name : undefined,
_visible: isLobbyChatActive ? isLocalParticipantModerator(state) : true
};
}

View File

@ -34,9 +34,9 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @inheritdoc
*/
render() {
const { _styles, message } = this.props;
const { _styles, message, knocking } = this.props;
const localMessage = message.messageType === MESSAGE_TYPE_LOCAL;
const { privateMessage } = message;
const { privateMessage, lobbyChat } = message;
// Style arrays that need to be updated in various scenarios, such as
// error messages or others.
@ -71,6 +71,10 @@ class ChatMessage extends AbstractChatMessage<Props> {
messageBubbleStyle.push(_styles.privateMessageBubble);
}
if (lobbyChat && !knocking) {
messageBubbleStyle.push(_styles.lobbyMessageBubble);
}
return (
<View style = { styles.messageWrapper } >
{ this._renderAvatar() }
@ -141,14 +145,14 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {React$Element<*> | null}
*/
_renderPrivateNotice() {
const { _styles, message } = this.props;
const { _styles, message, knocking } = this.props;
if (!message.privateMessage) {
if (!(message.privateMessage || (message.lobbyChat && !knocking))) {
return null;
}
return (
<Text style = { _styles.privateNotice }>
<Text style = { message.lobbyChat ? _styles.lobbyMsgNotice : _styles.privateNotice }>
{ this._getPrivateNoticeMessage() }
</Text>
);
@ -160,16 +164,17 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {React$Element<*> | null}
*/
_renderPrivateReplyButton() {
const { _styles, message } = this.props;
const { messageType, privateMessage } = message;
const { _styles, message, knocking } = this.props;
const { messageType, privateMessage, lobbyChat } = message;
if (!privateMessage || messageType === MESSAGE_TYPE_LOCAL) {
if (!(privateMessage || lobbyChat) || messageType === MESSAGE_TYPE_LOCAL || knocking) {
return null;
}
return (
<View style = { _styles.replyContainer }>
<PrivateMessageButton
isLobbyMessage = { lobbyChat }
participantID = { message.id }
reply = { true }
showLabel = { false }
@ -204,7 +209,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
*/
function _mapStateToProps(state) {
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
_styles: ColorSchemeRegistry.get(state, 'Chat'),
knocking: state['features/lobby'].knocking
};
}

View File

@ -11,7 +11,7 @@ import { type StyleType } from '../../../base/styles';
import {
setParams
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { setPrivateMessageRecipient } from '../../actions.any';
import { setPrivateMessageRecipient, setLobbyChatActiveState } from '../../actions.any';
import AbstractMessageRecipient, {
type Props as AbstractProps
} from '../AbstractMessageRecipient';
@ -28,6 +28,16 @@ type Props = AbstractProps & {
*/
dispatch: Function,
/**
* Is lobby messaging active.
*/
isLobbyChatActive: boolean,
/**
* The participant string for lobby chat messaging.
*/
lobbyMessageRecipient: Object,
/**
* The participant object set for private messaging.
*/
@ -48,6 +58,20 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
super(props);
this._onResetPrivateMessageRecipient = this._onResetPrivateMessageRecipient.bind(this);
this._onResetLobbyMessageRecipient = this._onResetLobbyMessageRecipient.bind(this);
}
_onResetLobbyMessageRecipient: () => void;
/**
* Resets lobby message recipient from state.
*
* @returns {void}
*/
_onResetLobbyMessageRecipient() {
const { dispatch } = this.props;
dispatch(setLobbyChatActiveState(false));
}
_onResetPrivateMessageRecipient: () => void;
@ -74,7 +98,27 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* @returns {ReactElement}
*/
render() {
const { _styles, privateMessageRecipient, t } = this.props;
const { _styles, privateMessageRecipient, t,
isLobbyChatActive, lobbyMessageRecipient } = this.props;
if (isLobbyChatActive) {
return (
<View style = { _styles.lobbyMessageRecipientContainer }>
<Text style = { _styles.messageRecipientText }>
{ t('chat.lobbyChatMessageTo', {
recipient: lobbyMessageRecipient.name
}) }
</Text>
<TouchableHighlight
onPress = { this._onResetLobbyMessageRecipient }>
<Icon
src = { IconCancelSelection }
style = { _styles.messageRecipientCancelIcon } />
</TouchableHighlight>
</View>
);
}
if (!privateMessageRecipient) {
return null;
@ -105,8 +149,12 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
return {
_styles: ColorSchemeRegistry.get(state, 'Chat')
_styles: ColorSchemeRegistry.get(state, 'Chat'),
isLobbyChatActive,
lobbyMessageRecipient
};
}

View File

@ -6,11 +6,17 @@ import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { handleLobbyChatInitialized } from '../../../chat/actions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
export type Props = AbstractButtonProps & {
/**
* The Redux Dispatch function.
*/
dispatch: Function,
/**
* The ID of the participant that the message is to be sent.
*/
@ -31,6 +37,11 @@ export type Props = AbstractButtonProps & {
*/
_isPollsDisabled: boolean,
/**
* True if message is a lobby chat message.
*/
_isLobbyMessage: boolean,
/**
* The participant object retrieved from Redux.
*/
@ -53,6 +64,9 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
* @returns {void}
*/
_handleClick() {
if (this.props._isLobbyMessage) {
this.props.dispatch(handleLobbyChatInitialized(this.props.participantID));
}
this.props._isPollsDisabled
? navigate(screen.conference.chat, {
privateMessageRecipient: this.props._participant
@ -88,11 +102,12 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
const { disablePolls } = state['features/base/config'];
const { visible = enabled } = ownProps;
const { visible = enabled, isLobbyMessage } = ownProps;
return {
_isPollsDisabled: disablePolls,
_participant: getParticipantById(state, ownProps.participantID),
_isLobbyMessage: isLobbyMessage,
visible
};
}

View File

@ -170,6 +170,23 @@ ColorSchemeRegistry.register('Chat', {
textAlign: 'center'
},
lobbyMessageBubble: {
backgroundColor: schemeColor('lobbyMsgBackground')
},
lobbyMsgNotice: {
color: schemeColor('lobbyMsgNotice'),
fontSize: 11,
marginTop: 6
},
lobbyMessageRecipientContainer: {
alignItems: 'center',
backgroundColor: schemeColor('lobbyMsgBackground'),
flexDirection: 'row',
padding: BoxModel.padding
},
localMessageBubble: {
backgroundColor: schemeColor('localMsgBackground'),
borderTopRightRadius: 0

View File

@ -4,6 +4,7 @@ import React from 'react';
import { translate } from '../../../base/i18n';
import Message from '../../../base/react/components/web/Message';
import { connect } from '../../../base/redux';
import { MESSAGE_TYPE_LOCAL } from '../../constants';
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
@ -20,13 +21,15 @@ class ChatMessage extends AbstractChatMessage<Props> {
* @returns {ReactElement}
*/
render() {
const { message, t } = this.props;
const { message, t, knocking } = this.props;
return (
<div
className = 'chatmessage-wrapper'
tabIndex = { -1 }>
<div className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''}` }>
<div
className = { `chatmessage ${message.privateMessage ? 'privatemessage' : ''} ${
message.lobbyChat && !knocking ? 'lobbymessage' : ''}` }>
<div className = 'replywrapper'>
<div className = 'messagecontent'>
{ this.props.showDisplayName && this._renderDisplayName() }
@ -39,12 +42,17 @@ class ChatMessage extends AbstractChatMessage<Props> {
</span>
<Message text = { this._getMessageText() } />
</div>
{ message.privateMessage && this._renderPrivateNotice() }
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& this._renderPrivateNotice() }
</div>
{ message.privateMessage && message.messageType !== MESSAGE_TYPE_LOCAL
{ (message.privateMessage || (message.lobbyChat && !knocking))
&& message.messageType !== MESSAGE_TYPE_LOCAL
&& (
<div className = 'messageactions'>
<div
className = { `messageactions ${
message.lobbyChat ? 'lobbychatmessageactions' : ''}` }>
<PrivateMessageButton
isLobbyMessage = { message.lobbyChat }
participantID = { message.id }
reply = { true }
showLabel = { false } />
@ -105,4 +113,18 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
export default translate(ChatMessage);
/**
* Maps part of the Redux store to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state: Object): $Shape<Props> {
const { knocking } = state['features/lobby'];
return {
knocking
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@ -38,9 +38,16 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* @returns {void}
*/
_onKeyPress(e) {
if (this.props._onRemovePrivateMessageRecipient && (e.key === ' ' || e.key === 'Enter')) {
if (
(this.props._onRemovePrivateMessageRecipient || this.props._onHideLobbyChatRecipient)
&& (e.key === ' ' || e.key === 'Enter')
) {
e.preventDefault();
this.props._onRemovePrivateMessageRecipient();
if (this.props._isLobbyChatActive && this.props._onHideLobbyChatRecipient) {
this.props._onHideLobbyChatRecipient();
} else if (this.props._onRemovePrivateMessageRecipient) {
this.props._onRemovePrivateMessageRecipient();
}
}
}
@ -50,9 +57,10 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
* @inheritdoc
*/
render() {
const { _privateMessageRecipient } = this.props;
const { _privateMessageRecipient, _isLobbyChatActive,
_lobbyMessageRecipient, _visible } = this.props;
if (!_privateMessageRecipient) {
if ((!_privateMessageRecipient && !_isLobbyChatActive) || !_visible) {
return null;
}
@ -60,16 +68,18 @@ class MessageRecipient extends AbstractMessageRecipient<Props> {
return (
<div
className = { _isLobbyChatActive ? 'lobby-chat-recipient' : '' }
id = 'chat-recipient'
role = 'alert'>
<span>
{ t('chat.messageTo', {
recipient: _privateMessageRecipient
{ t(_isLobbyChatActive ? 'chat.lobbyChatMessageTo' : 'chat.messageTo', {
recipient: _isLobbyChatActive ? _lobbyMessageRecipient : _privateMessageRecipient
}) }
</span>
<div
aria-label = { t('dialog.close') }
onClick = { this.props._onRemovePrivateMessageRecipient }
onClick = { _isLobbyChatActive
? this.props._onHideLobbyChatRecipient : this.props._onRemovePrivateMessageRecipient }
onKeyPress = { this._onKeyPress }
role = 'button'
tabIndex = { 0 }>

View File

@ -6,10 +6,15 @@ import { IconMessage, IconReply } from '../../../base/icons';
import { getParticipantById } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
import { openChat } from '../../actions';
import { openChat, handleLobbyChatInitialized } from '../../actions';
export type Props = AbstractButtonProps & {
/**
* True if the message is a lobby chat message.
*/
isLobbyMessage: boolean,
/**
* The ID of the participant that the message is to be sent.
*/
@ -52,9 +57,13 @@ class PrivateMessageButton extends AbstractButton<Props, any> {
* @returns {void}
*/
_handleClick() {
const { _participant, dispatch } = this.props;
const { _participant, participantID, dispatch, isLobbyMessage } = this.props;
dispatch(openChat(_participant));
if (isLobbyMessage) {
dispatch(handleLobbyChatInitialized(participantID));
} else {
dispatch(openChat(_participant));
}
}
/**

View File

@ -30,6 +30,12 @@ export const MESSAGE_TYPE_REMOTE = 'remote';
export const SMALL_WIDTH_THRESHOLD = 580;
/**
* Lobby message type.
*/
export const LOBBY_CHAT_MESSAGE = 'LOBBY_CHAT_MESSAGE';
/**
* The modes of the buttons of the chat and polls tabs.
*/

View File

@ -1,4 +1,5 @@
// @flow
import { type Dispatch } from 'redux';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
import {
@ -35,6 +36,7 @@ import { closeChat } from './actions.any';
import { ChatPrivacyDialog } from './components';
import {
INCOMING_MSG_SOUND_ID,
LOBBY_CHAT_MESSAGE,
MESSAGE_TYPE_ERROR,
MESSAGE_TYPE_LOCAL,
MESSAGE_TYPE_REMOTE
@ -122,7 +124,7 @@ MiddlewareRegistry.register(store => next => action => {
case SEND_MESSAGE: {
const state = store.getState();
const { conference } = state['features/base/conference'];
const conference = getCurrentConference(state);
if (conference) {
// There may be cases when we intend to send a private message but we forget to set the
@ -137,13 +139,20 @@ MiddlewareRegistry.register(store => next => action => {
} else {
// Sending the message if privacy notice doesn't need to be shown.
const { privateMessageRecipient } = state['features/chat'];
const { privateMessageRecipient, isLobbyChatActive, lobbyMessageRecipient }
= state['features/chat'];
if (typeof APP !== 'undefined') {
APP.API.notifySendingChatMessage(action.message, Boolean(privateMessageRecipient));
}
if (privateMessageRecipient) {
if (isLobbyChatActive && lobbyMessageRecipient) {
conference.sendLobbyMessage({
type: LOBBY_CHAT_MESSAGE,
message: action.message
}, lobbyMessageRecipient.id);
_persistSentPrivateMessage(store, lobbyMessageRecipient.id, action.message, true);
} else if (privateMessageRecipient) {
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
} else {
@ -159,7 +168,8 @@ MiddlewareRegistry.register(store => next => action => {
id: localParticipant.id,
message: action.message,
privateMessage: false,
timestamp: Date.now()
timestamp: Date.now(),
lobbyChat: false
}, false, true);
}
}
@ -220,6 +230,7 @@ function _addChatMsgListener(conference, store) {
id,
message,
privateMessage: false,
lobbyChat: false,
timestamp
});
}
@ -232,6 +243,7 @@ function _addChatMsgListener(conference, store) {
id,
message,
privateMessage: true,
lobbyChat: false,
timestamp
});
}
@ -258,6 +270,7 @@ function _addChatMsgListener(conference, store) {
id: _id,
message: getReactionMessageFromBuffer(eventData.reactions),
privateMessage: false,
lobbyChat: false,
timestamp: eventData.timestamp
}, false, true);
}
@ -287,6 +300,49 @@ function _handleChatError({ dispatch }, error) {
}));
}
/**
* Function to handle an incoming chat message from lobby room.
*
* @param {string} message - The message received.
* @param {string} participantId - The participant id.
* @returns {Function}
*/
export function handleLobbyMessageReceived(message: string, participantId: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
_handleReceivedMessage({ dispatch,
getState }, { id: participantId,
message,
privateMessage: false,
lobbyChat: true,
timestamp: Date.now() });
};
}
/**
* Function to get lobby chat user display name.
*
* @param {Store} state - The Redux store.
* @param {string} id - The knocking participant id.
* @returns {string}
*/
function getLobbyChatDisplayName(state, id) {
const { knockingParticipants } = state['features/lobby'];
const { lobbyMessageRecipient } = state['features/chat'];
if (id === lobbyMessageRecipient.id) {
return lobbyMessageRecipient.name;
}
const knockingParticipant = knockingParticipants.find(p => p.id === id);
if (knockingParticipant) {
return knockingParticipant.name;
}
}
/**
* Function to handle an incoming chat message.
*
@ -297,7 +353,7 @@ function _handleChatError({ dispatch }, error) {
* @returns {void}
*/
function _handleReceivedMessage({ dispatch, getState },
{ id, message, privateMessage, timestamp },
{ id, message, privateMessage, timestamp, lobbyChat },
shouldPlaySound = true,
isReaction = false
) {
@ -316,7 +372,9 @@ function _handleReceivedMessage({ dispatch, getState },
const participant = getParticipantById(state, id) || {};
const localParticipant = getLocalParticipant(getState);
const displayName = getParticipantDisplayName(state, id);
const displayName = lobbyChat
? getLobbyChatDisplayName(state, id)
: getParticipantDisplayName(state, id);
const hasRead = participant.local || isChatOpen;
const timestampToDate = timestamp ? new Date(timestamp) : new Date();
const millisecondsTimestamp = timestampToDate.getTime();
@ -332,6 +390,7 @@ function _handleReceivedMessage({ dispatch, getState },
messageType: participant.local ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
message,
privateMessage,
lobbyChat,
recipient: getParticipantDisplayName(state, localParticipant.id),
timestamp: millisecondsTimestamp,
isReaction
@ -371,11 +430,14 @@ function _handleReceivedMessage({ dispatch, getState },
* @param {Store} store - The Redux store.
* @param {string} recipientID - The ID of the recipient the private message was sent to.
* @param {string} message - The sent message.
* @param {boolean} isLobbyPrivateMessage - Is a lobby message.
* @returns {void}
*/
function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message) {
const localParticipant = getLocalParticipant(getState);
const displayName = getParticipantDisplayName(getState, localParticipant.id);
function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message, isLobbyPrivateMessage = false) {
const state = getState();
const localParticipant = getLocalParticipant(state);
const displayName = getParticipantDisplayName(state, localParticipant.id);
const { lobbyMessageRecipient } = state['features/chat'];
dispatch(addMessage({
displayName,
@ -383,8 +445,11 @@ function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message
id: localParticipant.id,
messageType: MESSAGE_TYPE_LOCAL,
message,
privateMessage: true,
recipient: getParticipantDisplayName(getState, recipientID),
privateMessage: !isLobbyPrivateMessage,
lobbyChat: isLobbyPrivateMessage,
recipient: isLobbyPrivateMessage
? lobbyMessageRecipient && lobbyMessageRecipient.name
: getParticipantDisplayName(getState, recipientID),
timestamp: Date.now()
}));
}

View File

@ -11,7 +11,10 @@ import {
EDIT_MESSAGE,
OPEN_CHAT,
SET_PRIVATE_MESSAGE_RECIPIENT,
SET_IS_POLL_TAB_FOCUSED
SET_IS_POLL_TAB_FOCUSED,
SET_LOBBY_CHAT_RECIPIENT,
SET_LOBBY_CHAT_ACTIVE_STATE,
REMOVE_LOBBY_CHAT_PARTICIPANT
} from './actionTypes';
const DEFAULT_STATE = {
@ -21,7 +24,9 @@ const DEFAULT_STATE = {
lastReadPoll: undefined,
messages: [],
nbUnreadMessages: 0,
privateMessageRecipient: undefined
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false
};
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
@ -36,6 +41,7 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
messageType: action.messageType,
message: action.message,
privateMessage: action.privateMessage,
lobbyChat: action.lobbyChat,
recipient: action.recipient,
timestamp: action.timestamp
};
@ -110,7 +116,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
isOpen: false,
lastReadMessage: state.messages[
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
privateMessageRecipient: action.participant
privateMessageRecipient: action.participant,
isLobbyChatActive: false
};
case SET_IS_POLL_TAB_FOCUSED: {
@ -119,6 +126,36 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
isPollsTabFocused: action.isPollsTabFocused,
nbUnreadMessages: 0
}; }
case SET_LOBBY_CHAT_RECIPIENT:
return {
...state,
isLobbyChatActive: true,
lobbyMessageRecipient: action.participant,
privateMessageRecipient: undefined,
isOpen: action.open
};
case SET_LOBBY_CHAT_ACTIVE_STATE:
return {
...state,
isLobbyChatActive: action.payload,
isOpen: action.payload || state.isOpen,
privateMessageRecipient: undefined
};
case REMOVE_LOBBY_CHAT_PARTICIPANT:
return {
...state,
messages: state.messages.filter(m => {
if (action.removeLobbyChatMessages) {
return !m.lobbyChat;
}
return true;
}),
isOpen: state.isOpen && state.isLobbyChatActive ? false : state.isOpen,
isLobbyChatActive: false,
lobbyMessageRecipient: undefined
};
}
return state;

View File

@ -29,3 +29,13 @@ export const SET_LOBBY_VISIBILITY = 'TOGGLE_LOBBY_VISIBILITY';
* Action type to set the password join failed status.
*/
export const SET_PASSWORD_JOIN_FAILED = 'SET_PASSWORD_JOIN_FAILED';
/**
* Action type to set a lobby chat participant's state to chatting
*/
export const SET_LOBBY_PARTICIPANT_CHAT_STATE = 'SET_LOBBY_PARTICIPANT_CHAT_STATE';
/**
* Action type to remove chattingWithModerator field
*/
export const REMOVE_LOBBY_CHAT_WITH_MODERATOR = 'REMOVE_LOBBY_CHAT_WITH_MODERATOR';

View File

@ -9,7 +9,11 @@ import {
setPassword
} from '../base/conference';
import { getLocalParticipant } from '../base/participants';
import { onLobbyChatInitialized, removeLobbyChatParticipant, sendMessage } from '../chat/actions.any';
import { LOBBY_CHAT_MESSAGE } from '../chat/constants';
import { handleLobbyMessageReceived } from '../chat/middleware';
import { hideNotification, LOBBY_NOTIFICATION_ID } from '../notifications';
import { showNotification } from '../notifications/actions';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
@ -17,8 +21,12 @@ import {
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_PASSWORD_JOIN_FAILED,
SET_LOBBY_VISIBILITY
SET_LOBBY_VISIBILITY,
SET_LOBBY_PARTICIPANT_CHAT_STATE,
REMOVE_LOBBY_CHAT_WITH_MODERATOR
} from './actionTypes';
import { LOBBY_CHAT_INITIALIZED, MODERATOR_IN_CHAT_WITH_LEFT } from './constants';
import { getKnockingParticipants, getLobbyEnabled } from './functions';
/**
* Tries to join with a preset password.
@ -211,6 +219,7 @@ export function startKnocking() {
sendLocalParticipant(state, membersOnly);
membersOnly.joinLobby(localParticipant.name, localParticipant.email);
dispatch(setLobbyMessageListener());
dispatch(setKnockingState(true));
};
}
@ -256,3 +265,149 @@ export function hideLobbyScreen() {
visible: false
};
}
/**
* Action to handle chat initialized in the lobby room.
*
* @param {Object} payload - The payload received,
* contains the information about the two participants
* that will chat with each other in the lobby room.
*
* @returns {Promise<void>}
*/
export function handleLobbyChatInitialized(payload: Object) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const conference = getCurrentConference(state);
const id = conference.myLobbyUserId();
dispatch({
type: SET_LOBBY_PARTICIPANT_CHAT_STATE,
participant: payload.attendee,
moderator: payload.moderator
});
dispatch(onLobbyChatInitialized(payload));
const attendeeIsKnocking = getKnockingParticipants(state).some(p => p.id === payload.attendee.id);
if (attendeeIsKnocking && conference.getRole() === 'moderator' && payload.moderator.id !== id) {
dispatch(showNotification({
titleKey: 'lobby.lobbyChatStartedNotification',
titleArguments: {
moderator: payload.moderator.name,
attendee: payload.attendee.name
}
}));
}
};
}
/**
* Action to send message to the moderator.
*
* @param {string} message - The message to be sent.
*
* @returns {Promise<void>}
*/
export function onSendMessage(message: string) {
return async (dispatch: Dispatch<any>) => {
dispatch(sendMessage(message));
};
}
/**
* Action to send lobby message to every participant. Only allowed for moderators.
*
* @param {Object} message - The message to be sent.
*
* @returns {Promise<void>}
*/
export function sendLobbyChatMessage(message: Object) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const conference = getCurrentConference(getState);
conference.sendLobbyMessage(message);
};
}
/**
* Sets lobby listeners if lobby has been enabled.
*
* @returns {Function}
*/
export function maybeSetLobbyChatMessageListener() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const lobbyEnabled = getLobbyEnabled(state);
if (lobbyEnabled) {
dispatch(setLobbyMessageListener());
}
};
}
/**
* Action to handle the event when a moderator leaves during lobby chat.
*
* @param {string} participantId - The participant id of the moderator who left.
* @returns {Function}
*/
export function updateLobbyParticipantOnLeave(participantId: string) {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const { knocking, knockingParticipants } = state['features/lobby'];
const { lobbyMessageRecipient } = state['features/chat'];
const { conference } = state['features/base/conference'];
if (knocking && lobbyMessageRecipient && lobbyMessageRecipient.id === participantId) {
return dispatch(removeLobbyChatParticipant(true));
}
if (!knocking) {
// inform knocking participant when their moderator leaves
const participantToNotify = knockingParticipants.find(p => p.chattingWithModerator === participantId);
if (participantToNotify) {
conference.sendLobbyMessage({
type: MODERATOR_IN_CHAT_WITH_LEFT,
moderatorId: participantToNotify.chattingWithModerator
}, participantToNotify.id);
}
dispatch({
type: REMOVE_LOBBY_CHAT_WITH_MODERATOR,
moderatorId: participantId
});
}
};
}
/**
* Handles all messages received in the lobby room.
*
* @returns {Function}
*/
export function setLobbyMessageListener() {
return async (dispatch: Dispatch<any>, getState: Function) => {
const state = getState();
const conference = getCurrentConference(state);
const { enableLobbyChat = true } = state['features/base/config'];
if (!enableLobbyChat) {
return;
}
conference.addLobbyMessageListener((message: Object, participantId: string) => {
if (message.type === LOBBY_CHAT_MESSAGE) {
return dispatch(handleLobbyMessageReceived(message.message, participantId));
}
if (message.type === LOBBY_CHAT_INITIALIZED) {
return dispatch(handleLobbyChatInitialized(message));
}
if (message.type === MODERATOR_IN_CHAT_WITH_LEFT) {
return dispatch(updateLobbyParticipantOnLeave(message.moderatorId));
}
});
};
}

View File

@ -8,7 +8,7 @@ import { getLocalParticipant } from '../../base/participants';
import { getFieldValue } from '../../base/react';
import { updateSettings } from '../../base/settings';
import { isDeviceStatusVisible } from '../../prejoin/functions';
import { cancelKnocking, joinWithPassword, setPasswordJoinFailed, startKnocking } from '../actions';
import { cancelKnocking, joinWithPassword, setPasswordJoinFailed, startKnocking, onSendMessage } from '../actions';
export const SCREEN_STATES = {
EDIT: 1,
@ -28,6 +28,21 @@ export type Props = {
*/
_knocking: boolean,
/**
* Lobby messages between moderator and the participant.
*/
_lobbyChatMessages: Object,
/**
* Name of the lobby chat recipient.
*/
_lobbyMessageRecipient: string,
/**
* True if moderator initiated a chat session with the participant.
*/
_isLobbyChatActive: boolean,
/**
* The name of the meeting we're about to join.
*/
@ -91,6 +106,11 @@ type State = {
*/
email: string,
/**
* True if lobby chat widget is open.
*/
isChatOpen: boolean,
/**
* The password value entered into the field.
*/
@ -122,6 +142,7 @@ export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent
this.state = {
displayName: props._participantName || '',
email: props._participantEmail || '',
isChatOpen: true,
password: '',
passwordJoinFailed: false,
screenState: props._participantName ? SCREEN_STATES.VIEW : SCREEN_STATES.EDIT
@ -134,8 +155,10 @@ export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent
this._onChangePassword = this._onChangePassword.bind(this);
this._onEnableEdit = this._onEnableEdit.bind(this);
this._onJoinWithPassword = this._onJoinWithPassword.bind(this);
this._onSendMessage = this._onSendMessage.bind(this);
this._onSwitchToKnockMode = this._onSwitchToKnockMode.bind(this);
this._onSwitchToPasswordMode = this._onSwitchToPasswordMode.bind(this);
this._onToggleChat = this._onToggleChat.bind(this);
}
/**
@ -164,7 +187,7 @@ export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent
const passwordPrompt = screenState === SCREEN_STATES.PASSWORD;
return !passwordPrompt && this.props._knocking
? 'lobby.joiningTitle'
? this.props._isLobbyChatActive ? 'lobby.lobbyChatStartedTitle' : 'lobby.joiningTitle'
: passwordPrompt ? 'lobby.enterPasswordTitle' : 'lobby.joinTitle';
}
@ -280,6 +303,18 @@ export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent
this.props.dispatch(joinWithPassword(this.state.password));
}
_onSendMessage: () => void;
/**
* Callback to be invoked for sending lobby chat messages.
*
* @param {string} message - Message to be sent.
* @returns {void}
*/
_onSendMessage(message) {
this.props.dispatch(onSendMessage(message));
}
_onSwitchToKnockMode: () => void;
/**
@ -311,6 +346,21 @@ export default class AbstractLobbyScreen<P: Props = Props> extends PureComponent
});
}
_onToggleChat: () => void;
/**
* Callback to be invoked for toggling lobby chat visibility.
*
* @returns {void}
*/
_onToggleChat() {
this.setState(_prevState => {
return {
isChatOpen: !_prevState.isChatOpen
};
});
}
/**
* Renders the content of the dialog.
*
@ -396,10 +446,14 @@ export function _mapStateToProps(state: Object): $Shape<Props> {
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
const deviceStatusVisible = isDeviceStatusVisible(state);
const { membersOnly } = state['features/base/conference'];
const { isLobbyChatActive, lobbyMessageRecipient, messages } = state['features/chat'];
return {
_deviceStatusVisible: deviceStatusVisible,
_knocking: knocking,
_lobbyChatMessages: messages,
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
_isLobbyChatActive: isLobbyChatActive,
_meetingName: getConferenceName(state),
_membersOnlyConference: membersOnly,
_participantEmail: localParticipant?.email,

View File

@ -7,9 +7,12 @@ import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { handleLobbyChatInitialized } from '../../../chat/actions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { setKnockingParticipantApproval } from '../../actions';
import { HIDDEN_EMAILS } from '../../constants';
import { getKnockingParticipants, getLobbyEnabled } from '../../functions';
import { showLobbyChatButton, getKnockingParticipants, getLobbyEnabled } from '../../functions';
import styles from './styles';
@ -28,6 +31,16 @@ export type Props = {
*/
_visible: boolean,
/**
* True if the polls feature is disabled.
*/
_isPollsDisabled: boolean,
/**
* Returns true if the lobby chat button should be shown.
*/
_showChatButton: Function,
/**
* The Redux Dispatch function.
*/
@ -61,7 +74,7 @@ class KnockingParticipantList extends PureComponent<Props> {
* @inheritdoc
*/
render() {
const { _participants, _visible, t } = this.props;
const { _participants, _visible, _showChatButton, t } = this.props;
if (!_visible) {
return null;
@ -108,6 +121,18 @@ class KnockingParticipantList extends PureComponent<Props> {
{ t('lobby.reject') }
</Text>
</TouchableOpacity>
{_showChatButton(p) ? (
<TouchableOpacity
onPress = { this._onInitializeLobbyChat(p.id) }
style = { [
styles.knockingParticipantListButton,
styles.knockingParticipantListSecondaryButton
] }>
<Text style = { styles.knockingParticipantListText }>
{ t('lobby.chat') }
</Text>
</TouchableOpacity>
) : null}
</View>
)) }
</ScrollView>
@ -128,6 +153,24 @@ class KnockingParticipantList extends PureComponent<Props> {
this.props.dispatch(setKnockingParticipantApproval(id, approve));
};
}
_onInitializeLobbyChat: (string) => Function;
/**
* Function that constructs a callback for the lobby chat button.
*
* @param {string} id - The id of the knocking participant.
* @returns {Function}
*/
_onInitializeLobbyChat(id) {
return () => {
this.props.dispatch(handleLobbyChatInitialized(id));
if (this.props._isPollsDisabled) {
return navigate(screen.conference.chat);
}
navigate(screen.conference.chatandpolls.main);
};
}
}
/**
@ -139,9 +182,12 @@ class KnockingParticipantList extends PureComponent<Props> {
function _mapStateToProps(state): Object {
const lobbyEnabled = getLobbyEnabled(state);
const knockingParticipants = getKnockingParticipants(state);
const { disablePolls } = state['features/base/config'];
return {
_visible: lobbyEnabled && isLocalParticipantModerator(state),
_showChatButton: participant => showLobbyChatButton(participant)(state),
_isPollsDisabled: disablePolls,
// On mobile we only show a portion of the list for screen real estate reasons
_participants: knockingParticipants.slice(0, 2)

View File

@ -6,10 +6,12 @@ import { SafeAreaView } from 'react-native-safe-area-context';
import { Avatar } from '../../../base/avatar';
import { translate } from '../../../base/i18n';
import { Icon, IconEdit } from '../../../base/icons';
import { Icon, IconClose, IconEdit } from '../../../base/icons';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import ChatInputBar from '../../../chat/components/native/ChatInputBar';
import MessageContainer from '../../../chat/components/native/MessageContainer';
import AbstractLobbyScreen, { _mapStateToProps } from '../AbstractLobbyScreen';
import styles from './styles';
@ -28,16 +30,21 @@ class LobbyScreen extends AbstractLobbyScreen {
return (
<JitsiScreen
style = { styles.contentWrapper }>
<SafeAreaView>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey()) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent() }
</SafeAreaView>
style = { this.props._isLobbyChatActive && this.state.isChatOpen
? styles.lobbyChatWrapper
: styles.contentWrapper }>
{this.props._isLobbyChatActive && this.state.isChatOpen
? this._renderLobbyChat()
: <SafeAreaView>
<Text style = { styles.dialogTitle }>
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
</Text>
<Text style = { styles.secondaryText }>
{ _meetingName }
</Text>
{ this._renderContent()}
</SafeAreaView> }
</JitsiScreen>
);
}
@ -62,8 +69,38 @@ class LobbyScreen extends AbstractLobbyScreen {
_onSwitchToPasswordMode: () => void;
_onSendMessage: () => void;
_onToggleChat: () => void;
_renderContent: () => React$Element<*>;
/**
* Renders the lobby chat.
*
* @inheritdoc
*/
_renderLobbyChat() {
const { t } = this.props;
return (
<>
<View style = { styles.lobbyChatHeader }>
<Text style = { styles.lobbyChatTitle }>
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
</Text>
<TouchableOpacity onPress = { this._onToggleChat }>
<Icon
src = { IconClose }
style = { styles.lobbyChatCloseButton } />
</TouchableOpacity>
</View>
<MessageContainer messages = { this.props._lobbyChatMessages } />
<ChatInputBar onSend = { this._onSendMessage } />
</>
);
}
/**
* Renders the joining (waiting) fragment of the screen.
*
@ -210,7 +247,7 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderStandardButtons() {
const { _knocking, _renderPassword, t } = this.props;
const { _knocking, _renderPassword, _isLobbyChatActive, t } = this.props;
return (
<>
@ -225,6 +262,16 @@ class LobbyScreen extends AbstractLobbyScreen {
{ t('lobby.knockButton') }
</Text>
</TouchableOpacity> }
{ _knocking && _isLobbyChatActive && <TouchableOpacity
onPress = { this._onToggleChat }
style = { [
styles.button,
styles.secondaryButton
] }>
<Text>
{ t('toolbar.openChat') }
</Text>
</TouchableOpacity>}
{ _renderPassword && <TouchableOpacity
onPress = { this._onSwitchToPasswordMode }
style = { [

View File

@ -12,6 +12,32 @@ export default {
paddingVertical: BaseTheme.spacing[2]
},
lobbyChatWrapper: {
backgroundColor: BaseTheme.palette.ui01,
alignItems: 'stretch',
flexDirection: 'column',
justifyItems: 'center',
height: '100%'
},
lobbyChatHeader: {
flexDirection: 'row',
padding: 20
},
lobbyChatTitle: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
flexShrink: 1
},
lobbyChatCloseButton: {
fontSize: 20,
marginLeft: 20,
color: '#fff'
},
contentWrapper: {
alignItems: 'center',
display: 'flex',

View File

@ -3,17 +3,61 @@
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { ActionButton, InputField, PreMeetingScreen } from '../../../base/premeeting';
import { LoadingIndicator } from '../../../base/react';
import { connect } from '../../../base/redux';
import ChatInput from '../../../chat/components/web/ChatInput';
import MessageContainer from '../../../chat/components/web/MessageContainer';
import AbstractLobbyScreen, {
_mapStateToProps
_mapStateToProps,
type Props
} from '../AbstractLobbyScreen';
/**
* Implements a waiting screen that represents the participant being in the lobby.
*/
class LobbyScreen extends AbstractLobbyScreen {
class LobbyScreen extends AbstractLobbyScreen<Props> {
/**
* Reference to the React Component for displaying chat messages. Used for
* scrolling to the end of the chat messages.
*/
_messageContainerRef: Object;
/**
* Initializes a new {@code LobbyScreen} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this._messageContainerRef = React.createRef();
}
/**
* Implements {@code Component#componentDidMount}.
*
* @inheritdoc
*/
componentDidMount() {
this._scrollMessageContainerToBottom(true);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps) {
if (this.props._lobbyChatMessages !== prevProps._lobbyChatMessages) {
this._scrollMessageContainerToBottom(true);
} else if (this.props._isLobbyChatActive && !prevProps._isLobbyChatActive) {
this._scrollMessageContainerToBottom(false);
}
}
/**
* Implements {@code PureComponent#render}.
*
@ -27,7 +71,7 @@ class LobbyScreen extends AbstractLobbyScreen {
className = 'lobby-screen'
showCopyUrlButton = { showCopyUrlButton }
showDeviceStatus = { _deviceStatusVisible }
title = { t(this._getScreenTitleKey()) }>
title = { t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }>
{ this._renderContent() }
</PreMeetingScreen>
);
@ -49,12 +93,16 @@ class LobbyScreen extends AbstractLobbyScreen {
_onJoinWithPassword: () => void;
_onSendMessage: () => void;
_onSubmit: () => boolean;
_onSwitchToKnockMode: () => void;
_onSwitchToPasswordMode: () => void;
_onToggleChat: () => void;
_renderContent: () => React$Element<*>;
/**
@ -63,19 +111,56 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderJoining() {
const { _isLobbyChatActive } = this.props;
return (
<div className = 'lobby-screen-content'>
<div className = 'spinner'>
<LoadingIndicator size = 'large' />
</div>
<span className = 'joining-message'>
{ this.props.t('lobby.joiningMessage') }
</span>
{_isLobbyChatActive
? this._renderLobbyChat()
: (
<>
<div className = 'spinner'>
<LoadingIndicator size = 'large' />
</div>
<span className = 'joining-message'>
{ this.props.t('lobby.joiningMessage') }
</span>
</>
)}
{ this._renderStandardButtons() }
</div>
);
}
/**
* Renders the widget to chat with the moderator before allowed in.
*
* @inheritdoc
*/
_renderLobbyChat() {
const { _lobbyChatMessages, t } = this.props;
const { isChatOpen } = this.state;
return (
<div className = { `lobby-chat-container ${isChatOpen ? 'hidden' : ''}` }>
<div className = 'lobby-chat-header'>
<h1 className = 'title'>
{ t(this._getScreenTitleKey(), { moderator: this.props._lobbyMessageRecipient }) }
</h1>
<Icon
ariaLabel = { t('toolbar.closeChat') }
onClick = { this._onToggleChat }
role = 'button'
src = { IconClose } />
</div>
<MessageContainer
messages = { _lobbyChatMessages }
ref = { this._messageContainerRef } />
<ChatInput onSend = { this._onSendMessage } />
</div>
);
}
/**
* Renders the participant form to let the knocking participant enter its details.
*
@ -163,7 +248,7 @@ class LobbyScreen extends AbstractLobbyScreen {
* @inheritdoc
*/
_renderStandardButtons() {
const { _knocking, _renderPassword, t } = this.props;
const { _knocking, _isLobbyChatActive, _renderPassword, t } = this.props;
return (
<>
@ -174,6 +259,13 @@ class LobbyScreen extends AbstractLobbyScreen {
type = 'primary'>
{ t('lobby.knockButton') }
</ActionButton> }
{ (_knocking && _isLobbyChatActive) && <ActionButton
className = 'open-chat-button'
onClick = { this._onToggleChat }
testId = 'toolbar.openChat'
type = 'primary' >
{ t('toolbar.openChat') }
</ActionButton> }
{_renderPassword && <ActionButton
onClick = { this._onSwitchToPasswordMode }
testId = 'lobby.enterPasswordButton'
@ -183,6 +275,20 @@ class LobbyScreen extends AbstractLobbyScreen {
</>
);
}
/**
* Scrolls the chat messages so the latest message is visible.
*
* @param {boolean} withAnimation - Whether or not to show a scrolling
* animation.
* @private
* @returns {void}
*/
_scrollMessageContainerToBottom(withAnimation) {
if (this._messageContainerRef.current) {
this._messageContainerRef.current.scrollToBottom(withAnimation);
}
}
}
export default translate(connect(_mapStateToProps)(LobbyScreen));

View File

@ -9,3 +9,17 @@ export const HIDDEN_EMAILS = [ 'inbound-sip-jibri@jitsi.net', 'outbound-sip-jibr
* @type {string}
*/
export const KNOCKING_PARTICIPANT_SOUND_ID = 'KNOCKING_PARTICIPANT_SOUND';
/**
* Lobby chat initialized message type.
*
* @type {string}
*/
export const LOBBY_CHAT_INITIALIZED = 'LOBBY_CHAT_INITIALIZED';
/**
* Event message sent to knocking participant when moderator in chat with leaves.
*
* @type {string}
*/
export const MODERATOR_IN_CHAT_WITH_LEFT = 'MODERATOR_IN_CHAT_WITH_LEFT';

View File

@ -1,5 +1,7 @@
// @flow
import { getCurrentConference } from '../base/conference';
/**
* Selector to return lobby enable state.
*
@ -39,3 +41,43 @@ export function getIsLobbyVisible(state: any) {
export function getKnockingParticipantsById(state: any) {
return getKnockingParticipants(state).map(participant => participant.id);
}
/**
* Function that handles the visibility of the lobby chat message.
*
* @param {Object} participant - Lobby Participant.
* @returns {Function}
*/
export function showLobbyChatButton(
participant: Object
) {
return function(state: Object) {
const { enableLobbyChat = true } = state['features/base/config'];
const { lobbyMessageRecipient, isLobbyChatActive } = state['features/chat'];
const conference = getCurrentConference(state);
const lobbyLocalId = conference.myLobbyUserId();
if (!enableLobbyChat) {
return false;
}
if (!isLobbyChatActive
&& (!participant.chattingWithModerator
|| participant.chattingWithModerator === lobbyLocalId)
) {
return true;
}
if (isLobbyChatActive && lobbyMessageRecipient
&& participant.id !== lobbyMessageRecipient.id
&& (!participant.chattingWithModerator
|| participant.chattingWithModerator === lobbyLocalId)) {
return true;
}
return false;
};
}

View File

@ -14,6 +14,7 @@ import { getFirstLoadableAvatarUrl, getParticipantDisplayName } from '../base/pa
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { isTestModeEnabled } from '../base/testing';
import { handleLobbyChatInitialized, removeLobbyChatParticipant } from '../chat/actions.any';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
import {
LOBBY_NOTIFICATION_ID,
@ -35,10 +36,12 @@ import {
participantIsKnockingOrUpdated,
setLobbyModeEnabled,
startKnocking,
setPasswordJoinFailed
setPasswordJoinFailed,
setLobbyMessageListener
} from './actions';
import { updateLobbyParticipantOnLeave } from './actions.any';
import { KNOCKING_PARTICIPANT_SOUND_ID } from './constants';
import { getKnockingParticipants } from './functions';
import { getKnockingParticipants, showLobbyChatButton } from './functions';
import { KNOCKING_PARTICIPANT_FILE } from './sounds';
declare var APP: Object;
@ -87,6 +90,9 @@ StateListenerRegistry.register(
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.MEMBERS_ONLY_CHANGED, enabled => {
dispatch(setLobbyModeEnabled(enabled));
if (enabled) {
dispatch(setLobbyMessageListener());
}
});
conference.on(JitsiConferenceEvents.LOBBY_USER_JOINED, (id, name) => {
@ -110,6 +116,57 @@ StateListenerRegistry.register(
getState
});
let notificationTitle;
let customActionNameKey;
let customActionHandler;
let descriptionKey;
let icon;
const knockingParticipants = getKnockingParticipants(getState());
const firstParticipant = knockingParticipants[0];
const showChat = showLobbyChatButton(firstParticipant)(getState());
if (knockingParticipants.length > 1) {
descriptionKey = 'notify.participantsWantToJoin';
notificationTitle = i18n.t('notify.waitingParticipants', {
waitingParticipants: knockingParticipants.length
});
icon = NOTIFICATION_ICON.PARTICIPANTS;
customActionNameKey = [ 'notify.viewLobby' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(openParticipantsPane());
}) ];
} else {
descriptionKey = 'notify.participantWantsToJoin';
notificationTitle = firstParticipant.name;
icon = NOTIFICATION_ICON.PARTICIPANT;
customActionNameKey = [ 'lobby.admit', 'lobby.reject' ];
customActionHandler = [ () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(approveKnockingParticipant(firstParticipant.id));
}),
() => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(rejectKnockingParticipant(firstParticipant.id));
}) ];
if (showChat) {
customActionNameKey.splice(1, 0, 'lobby.chat');
customActionHandler.splice(1, 0, () => batch(() => {
dispatch(hideNotification(LOBBY_NOTIFICATION_ID));
dispatch(handleLobbyChatInitialized(firstParticipant.id));
}));
}
}
dispatch(showNotification({
title: notificationTitle,
descriptionKey,
uid: LOBBY_NOTIFICATION_ID,
customActionNameKey,
customActionHandler,
icon
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
if (typeof APP !== 'undefined') {
APP.API.notifyKnockingParticipant({
id,
@ -129,7 +186,11 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.LOBBY_USER_LEFT, id => {
dispatch(knockingParticipantLeft(id));
batch(() => {
dispatch(knockingParticipantLeft(id));
dispatch(removeLobbyChatParticipant());
dispatch(updateLobbyParticipantOnLeave(id));
});
});
conference.on(JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, (origin, sender) =>

View File

@ -6,8 +6,10 @@ import { ReducerRegistry } from '../base/redux';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
KNOCKING_PARTICIPANT_LEFT,
REMOVE_LOBBY_CHAT_WITH_MODERATOR,
SET_KNOCKING_STATE,
SET_LOBBY_MODE_ENABLED,
SET_LOBBY_PARTICIPANT_CHAT_STATE,
SET_LOBBY_VISIBILITY,
SET_PASSWORD_JOIN_FAILED
} from './actionTypes';
@ -70,6 +72,34 @@ ReducerRegistry.register('features/lobby', (state = DEFAULT_STATE, action) => {
...state,
passwordJoinFailed: action.failed
};
case SET_LOBBY_PARTICIPANT_CHAT_STATE:
return {
...state,
knockingParticipants: state.knockingParticipants.map(participant => {
if (participant.id === action.participant.id) {
return {
...participant,
chattingWithModerator: action.moderator.id
};
}
return participant;
})
};
case REMOVE_LOBBY_CHAT_WITH_MODERATOR:
return {
...state,
knockingParticipants: state.knockingParticipants.map(participant => {
if (participant.chattingWithModerator === action.moderatorId) {
return {
...participant,
chattingWithModerator: undefined
};
}
return participant;
})
};
}
return state;

View File

@ -1,10 +1,14 @@
// @flow
import { makeStyles } from '@material-ui/styles';
import React from 'react';
import React, { useCallback, useState, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useSelector } from 'react-redux';
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
import { Icon, IconChat, IconCloseCircle, IconHorizontalPoints } from '../../../base/icons';
import { hasRaisedHand } from '../../../base/participants';
import { showLobbyChatButton } from '../../../lobby/functions';
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
import { useLobbyActions } from '../../hooks';
@ -33,6 +37,16 @@ const useStyles = makeStyles(theme => {
return {
button: {
marginRight: `${theme.spacing(2)}px`
},
moreButton: {
paddingRight: '6px',
paddingLeft: '6px',
marginRight: `${theme.spacing(2)}px`
},
contextMenu: {
position: 'fixed',
top: 'auto',
marginRight: '8px'
}
};
});
@ -43,10 +57,27 @@ export const LobbyParticipantItem = ({
openDrawerForParticipant
}: Props) => {
const { id } = p;
const [ admit, reject ] = useLobbyActions({ participantID: id });
const [ admit, reject, chat ] = useLobbyActions({ participantID: id });
const { t } = useTranslation();
const [ isOpen, setIsOpen ] = useState(false);
const styles = useStyles();
const showChat = useSelector(showLobbyChatButton(p));
const moreButtonRef = useRef();
const openContextMenu = useCallback(() => setIsOpen(true));
const closeContextMenu = useCallback(() => setIsOpen(false));
const renderAdmitButton = () => (
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.admit')} ${p.name}` }
className = { styles.button }
onClick = { admit }
testId = { `admit-${id}` }>
{t('lobby.admit')}
</LobbyParticipantQuickAction>);
return (
<ParticipantItem
actionsTrigger = { ACTION_TRIGGER.PERMANENT }
@ -59,20 +90,51 @@ export const LobbyParticipantItem = ({
raisedHand = { hasRaisedHand(p) }
videoMediaState = { MEDIA_STATE.NONE }
youText = { t('chat.you') }>
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.reject')} ${p.name}` }
className = { styles.button }
onClick = { reject }
secondary = { true }
testId = { `reject-${id}` }>
{t('lobby.reject') }
</LobbyParticipantQuickAction>
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.admit')} ${p.name}` }
onClick = { admit }
testId = { `admit-${id}` }>
{t('lobby.admit')}
</LobbyParticipantQuickAction>
{showChat ? <>
{renderAdmitButton()}
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('participantsPane.actions.moreModerationActions')} ${p.name}` }
className = { styles.moreButton }
onClick = { openContextMenu }
ref = { moreButtonRef }
secondary = { true }>
<Icon src = { IconHorizontalPoints } />
</LobbyParticipantQuickAction>
<ContextMenu
className = { styles.contextMenu }
hidden = { !isOpen }
offsetTarget = { moreButtonRef.current }
onMouseLeave = { closeContextMenu }>
<ContextMenuItemGroup
actions = { [ {
accessibilityLabel: `${t('lobby.chat')} ${p.name}`,
onClick: chat,
testId: `lobby-chat-${id}`,
icon: IconChat,
text: t('lobby.chat')
} ] } />
<ContextMenuItemGroup
actions = { [ {
accessibilityLabel: `${t('lobby.reject')} ${p.name}`,
onClick: reject,
testId: `reject-${id}`,
icon: IconCloseCircle,
text: t('lobby.reject')
} ] } />
</ContextMenu>
</> : <>
<LobbyParticipantQuickAction
accessibilityLabel = { `${t('lobby.reject')} ${p.name}` }
className = { styles.button }
onClick = { reject }
secondary = { true }
testId = { `reject-${id}` }>
{t('lobby.reject') }
</LobbyParticipantQuickAction>
{renderAdmitButton()}
</>
}
</ParticipantItem>
);
};

View File

@ -1,6 +1,7 @@
import { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import { handleLobbyChatInitialized } from '../chat/actions.any';
import { approveKnockingParticipant, rejectKnockingParticipant } from '../lobby/actions';
/**
@ -23,7 +24,11 @@ export function useLobbyActions(participant, closeDrawer) {
useCallback(() => {
dispatch(rejectKnockingParticipant(participant && participant.participantID));
closeDrawer && closeDrawer();
}, [ dispatch, closeDrawer ])
}, [ dispatch, closeDrawer ]),
useCallback(() => {
dispatch(handleLobbyChatInitialized(participant && participant.participantID));
}, [ dispatch ])
];
}

View File

@ -158,6 +158,26 @@ function filter_stanza(stanza)
elseif stanza.name == 'iq' and stanza:get_child('query', DISCO_INFO_NS) then
-- allow disco info from the lobby component
return stanza;
elseif stanza.name == 'message' then
-- allow messages to or from moderator
local lobby_room_jid = jid_bare(stanza.attr.from);
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
local from_real_jid;
if from_occupant then
for real_jid in from_occupant:each_session() do
from_real_jid = real_jid;
end
end
local is_from_moderator = lobby_room:get_affiliation(from_real_jid) == 'owner';
if is_to_moderator or is_from_moderator then
return stanza;
end
return nil;
end
return nil;