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:
parent
7a4a234f8e
commit
7522de033a
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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}}",
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -142,6 +142,7 @@ export default [
|
|||
'enableInsecureRoomNameWarning',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
'enableLobbyChat',
|
||||
'enableOpusRed',
|
||||
'enableRemb',
|
||||
'enableSaveLogs',
|
||||
|
|
|
@ -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';
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 }>
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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()
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 = { [
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) =>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 ])
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue