feat: private messages
This commit is contained in:
parent
f270b50972
commit
42271b1b89
|
@ -80,6 +80,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#chat-recipient {
|
||||||
|
align-items: center;
|
||||||
|
background-color: $defaultWarningColor;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
svg {
|
||||||
|
cursor: pointer;
|
||||||
|
fill: white
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
background-color: $chatHeaderBackgroundColor;
|
background-color: $chatHeaderBackgroundColor;
|
||||||
height: 70px;
|
height: 70px;
|
||||||
|
@ -196,6 +217,11 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.privatemessagenotice {
|
||||||
|
color: $defaultWarningColor;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.smiley {
|
.smiley {
|
||||||
|
@ -228,6 +254,7 @@
|
||||||
.smileys-panel {
|
.smileys-panel {
|
||||||
bottom: 100%;
|
bottom: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background-color: rgba(0, 0, 0, .6) !important;
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
@ -312,6 +339,16 @@
|
||||||
|
|
||||||
.chatmessage-wrapper {
|
.chatmessage-wrapper {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|
||||||
|
.replywrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.toolbox-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chatmessage {
|
.chatmessage {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
min-width: 75px;
|
min-width: 75px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
width: 150px;
|
width: 180px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@include absoluteAligning();
|
@include absoluteAligning();
|
||||||
|
|
|
@ -28,6 +28,7 @@ $defaultColor: #F1F1F1;
|
||||||
$defaultSideBarFontColor: #44A5FF;
|
$defaultSideBarFontColor: #44A5FF;
|
||||||
$defaultSemiDarkColor: #ACACAC;
|
$defaultSemiDarkColor: #ACACAC;
|
||||||
$defaultDarkColor: #2b3d5c;
|
$defaultDarkColor: #2b3d5c;
|
||||||
|
$defaultWarningColor: rgb(215, 121, 118);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toolbar
|
* Toolbar
|
||||||
|
|
|
@ -48,11 +48,14 @@
|
||||||
"chat": {
|
"chat": {
|
||||||
"error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
|
"error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
|
||||||
"messagebox": "Type a message",
|
"messagebox": "Type a message",
|
||||||
|
"messageTo": "Private message to {{recipient}}",
|
||||||
"nickname": {
|
"nickname": {
|
||||||
"popover": "Choose a nickname",
|
"popover": "Choose a nickname",
|
||||||
"title": "Enter a nickname to use chat"
|
"title": "Enter a nickname to use chat"
|
||||||
},
|
},
|
||||||
"title": "Chat"
|
"privateNotice": "Private message to {{recipient}}",
|
||||||
|
"title": "Chat",
|
||||||
|
"you": "you"
|
||||||
},
|
},
|
||||||
"connectingOverlay": {
|
"connectingOverlay": {
|
||||||
"joiningRoom": "Connecting you to your meeting..."
|
"joiningRoom": "Connecting you to your meeting..."
|
||||||
|
@ -235,6 +238,10 @@
|
||||||
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
|
"screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
|
||||||
"screenSharingFirefoxPermissionDeniedTitle": "Oops! We weren’t able to start screen sharing!",
|
"screenSharingFirefoxPermissionDeniedTitle": "Oops! We weren’t able to start screen sharing!",
|
||||||
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
|
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
|
||||||
|
"sendPrivateMessage": "You recently received a private message. Did you intend to reply to that privately, or you want to send your message to the group?",
|
||||||
|
"sendPrivateMessageCancel": "Send to the group",
|
||||||
|
"sendPrivateMessageOk": "Send privately",
|
||||||
|
"sendPrivateMessageTitle": "Send privately?",
|
||||||
"serviceUnavailable": "Service unavailable",
|
"serviceUnavailable": "Service unavailable",
|
||||||
"sessTerminated": "Call terminated",
|
"sessTerminated": "Call terminated",
|
||||||
"Share": "Share",
|
"Share": "Share",
|
||||||
|
@ -571,6 +578,7 @@
|
||||||
"moreActionsMenu": "More actions menu",
|
"moreActionsMenu": "More actions menu",
|
||||||
"mute": "Toggle mute audio",
|
"mute": "Toggle mute audio",
|
||||||
"pip": "Toggle Picture-in-Picture mode",
|
"pip": "Toggle Picture-in-Picture mode",
|
||||||
|
"privateMessage": "Send private message",
|
||||||
"profile": "Edit your profile",
|
"profile": "Edit your profile",
|
||||||
"raiseHand": "Toggle raise hand",
|
"raiseHand": "Toggle raise hand",
|
||||||
"recording": "Toggle recording",
|
"recording": "Toggle recording",
|
||||||
|
@ -611,6 +619,7 @@
|
||||||
"mute": "Mute / Unmute",
|
"mute": "Mute / Unmute",
|
||||||
"openChat": "Open chat",
|
"openChat": "Open chat",
|
||||||
"pip": "Enter Picture-in-Picture mode",
|
"pip": "Enter Picture-in-Picture mode",
|
||||||
|
"privateMessage": "Send private message",
|
||||||
"profile": "Edit your profile",
|
"profile": "Edit your profile",
|
||||||
"raiseHand": "Raise / Lower your hand",
|
"raiseHand": "Raise / Lower your hand",
|
||||||
"raiseYourHand": "Raise your hand",
|
"raiseYourHand": "Raise your hand",
|
||||||
|
|
|
@ -36,6 +36,7 @@ export { default as IconMenu } from './menu.svg';
|
||||||
export { default as IconMenuDown } from './menu-down.svg';
|
export { default as IconMenuDown } from './menu-down.svg';
|
||||||
export { default as IconMenuThumb } from './thumb-menu.svg';
|
export { default as IconMenuThumb } from './thumb-menu.svg';
|
||||||
export { default as IconMenuUp } from './menu-up.svg';
|
export { default as IconMenuUp } from './menu-up.svg';
|
||||||
|
export { default as IconMessage } from './message.svg';
|
||||||
export { default as IconMicDisabled } from './mic-disabled.svg';
|
export { default as IconMicDisabled } from './mic-disabled.svg';
|
||||||
export { default as IconMicrophone } from './microphone.svg';
|
export { default as IconMicrophone } from './microphone.svg';
|
||||||
export { default as IconModerator } from './star.svg';
|
export { default as IconModerator } from './star.svg';
|
||||||
|
@ -48,6 +49,7 @@ export { default as IconRaisedHand } from './raised-hand.svg';
|
||||||
export { default as IconRec } from './rec.svg';
|
export { default as IconRec } from './rec.svg';
|
||||||
export { default as IconRemoteControlStart } from './play.svg';
|
export { default as IconRemoteControlStart } from './play.svg';
|
||||||
export { default as IconRemoteControlStop } from './stop.svg';
|
export { default as IconRemoteControlStop } from './stop.svg';
|
||||||
|
export { default as IconReply } from './reply.svg';
|
||||||
export { default as IconRestore } from './restore.svg';
|
export { default as IconRestore } from './restore.svg';
|
||||||
export { default as IconRoomLock } from './security.svg';
|
export { default as IconRoomLock } from './security.svg';
|
||||||
export { default as IconRoomUnlock } from './security-locked.svg';
|
export { default as IconRoomUnlock } from './security-locked.svg';
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-2 12H6v-2h12v2zm0-3H6V9h12v2zm0-3H6V6h12v2z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 256 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 9V5l-7 7 7 7v-4.1c5 0 8.5 1.6 11 5.1-1-5-4-10-11-11z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
|
After Width: | Height: | Size: 194 B |
|
@ -28,6 +28,7 @@ export const ColorPalette = {
|
||||||
overflowMenuItemUnderlay: '#EEEEEE',
|
overflowMenuItemUnderlay: '#EEEEEE',
|
||||||
red: '#D00000',
|
red: '#D00000',
|
||||||
transparent: 'rgba(0, 0, 0, 0)',
|
transparent: 'rgba(0, 0, 0, 0)',
|
||||||
|
warning: 'rgb(215, 121, 118)',
|
||||||
white: '#FFFFFF',
|
white: '#FFFFFF',
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -30,11 +30,23 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
|
||||||
*
|
*
|
||||||
* {
|
* {
|
||||||
* type: SEND_MESSAGE,
|
* type: SEND_MESSAGE,
|
||||||
|
* ignorePrivacy: boolean,
|
||||||
* message: string
|
* message: string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
export const SEND_MESSAGE = 'SEND_MESSAGE';
|
export const SEND_MESSAGE = 'SEND_MESSAGE';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of action which signals the initiation of sending of as private message to the
|
||||||
|
* supplied recipient.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* participant: Participant,
|
||||||
|
* type: SET_PRIVATE_MESSAGE_RECIPIENT
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of the action which signals to toggle the display of the chat panel.
|
* The type of the action which signals to toggle the display of the chat panel.
|
||||||
*
|
*
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
ADD_MESSAGE,
|
ADD_MESSAGE,
|
||||||
CLEAR_MESSAGES,
|
CLEAR_MESSAGES,
|
||||||
SEND_MESSAGE,
|
SEND_MESSAGE,
|
||||||
|
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||||
TOGGLE_CHAT
|
TOGGLE_CHAT
|
||||||
} from './actionTypes';
|
} from './actionTypes';
|
||||||
|
|
||||||
|
@ -53,18 +54,37 @@ export function clearMessages() {
|
||||||
* Sends a chat message to everyone in the conference.
|
* Sends a chat message to everyone in the conference.
|
||||||
*
|
*
|
||||||
* @param {string} message - The chat message to send out.
|
* @param {string} message - The chat message to send out.
|
||||||
|
* @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
|
||||||
* @returns {{
|
* @returns {{
|
||||||
* type: SEND_MESSAGE,
|
* type: SEND_MESSAGE,
|
||||||
|
* ignorePrivacy: boolean,
|
||||||
* message: string
|
* message: string
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export function sendMessage(message: string) {
|
export function sendMessage(message: string, ignorePrivacy: boolean = false) {
|
||||||
return {
|
return {
|
||||||
type: SEND_MESSAGE,
|
type: SEND_MESSAGE,
|
||||||
|
ignorePrivacy,
|
||||||
message
|
message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiates the sending of a private message to the supplied participant.
|
||||||
|
*
|
||||||
|
* @param {Participant} participant - The participant to set the recipient to.
|
||||||
|
* @returns {{
|
||||||
|
* participant: Participant,
|
||||||
|
* type: SET_PRIVATE_MESSAGE_RECIPIENT
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function setPrivateMessageRecipient(participant: Object) {
|
||||||
|
return {
|
||||||
|
participant,
|
||||||
|
type: SET_PRIVATE_MESSAGE_RECIPIENT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggles display of the chat side panel.
|
* Toggles display of the chat side panel.
|
||||||
*
|
*
|
||||||
|
|
|
@ -56,4 +56,17 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
|
||||||
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
|
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
|
||||||
.format(TIMESTAMP_FORMAT);
|
.format(TIMESTAMP_FORMAT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the message that is displayed as a notice for private messages.
|
||||||
|
*
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
_getPrivateNoticeMessage() {
|
||||||
|
const { message, t } = this.props;
|
||||||
|
|
||||||
|
return t('chat.privateNotice', {
|
||||||
|
recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { sendMessage, setPrivateMessageRecipient } from '../actions';
|
||||||
|
import { getParticipantById } from '../../base/participants';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message that is about to be sent.
|
||||||
|
*/
|
||||||
|
message: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the participant that we think the message may be intended to.
|
||||||
|
*/
|
||||||
|
participantID: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n keys.
|
||||||
|
*/
|
||||||
|
t: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prop to be invoked on sending the message.
|
||||||
|
*/
|
||||||
|
_onSendMessage: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prop to be invoked when the user wants to set a private recipient.
|
||||||
|
*/
|
||||||
|
_onSetMessageRecipient: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant retreived from Redux by the participanrID prop.
|
||||||
|
*/
|
||||||
|
_participant: Object
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for the dialog displayed to avoid mis-sending private messages.
|
||||||
|
*/
|
||||||
|
export class AbstractChatPrivacyDialog extends PureComponent<Props> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new instance.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onSendGroupMessage = this._onSendGroupMessage.bind(this);
|
||||||
|
this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSendGroupMessage: () => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked for cancel action (user wants to send a group message).
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_onSendGroupMessage() {
|
||||||
|
this.props._onSendMessage(this.props.message);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSendPrivateMessage: () => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked for submit action (user wants to send a private message).
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onSendPrivateMessage() {
|
||||||
|
const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
|
||||||
|
|
||||||
|
_onSetMessageRecipient(_participant);
|
||||||
|
_onSendMessage(message);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the props of this component to Redux actions.
|
||||||
|
*
|
||||||
|
* @param {Function} dispatch - The Redux dispatch function.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
|
||||||
|
return {
|
||||||
|
_onSendMessage: (message: Object) => {
|
||||||
|
dispatch(sendMessage(message, true));
|
||||||
|
},
|
||||||
|
|
||||||
|
_onSetMessageRecipient: participant => {
|
||||||
|
dispatch(setPrivateMessageRecipient(participant));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux store to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @param {Props} ownProps - The own props of the component.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
|
||||||
|
return {
|
||||||
|
_participant: getParticipantById(state, ownProps.participantID)
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
|
import { getParticipantDisplayName } from '../../base/participants';
|
||||||
|
|
||||||
|
import { setPrivateMessageRecipient } from '../actions';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to remove the recipent setting of the chat window.
|
||||||
|
*/
|
||||||
|
_onRemovePrivateMessageRecipient: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the message recipient, if any.
|
||||||
|
*/
|
||||||
|
_privateMessageRecipient: ?string
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class for the {@code MessageRecipient} component.
|
||||||
|
*/
|
||||||
|
export default class AbstractMessageRecipient extends PureComponent<Props> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the props of this component to Redux actions.
|
||||||
|
*
|
||||||
|
* @param {Function} dispatch - The Redux dispatch function.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
|
||||||
|
return {
|
||||||
|
_onRemovePrivateMessageRecipient: () => {
|
||||||
|
dispatch(setPrivateMessageRecipient());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux store to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapStateToProps(state: Object): $Shape<Props> {
|
||||||
|
const { privateMessageRecipient } = state['features/chat'];
|
||||||
|
|
||||||
|
return {
|
||||||
|
_privateMessageRecipient:
|
||||||
|
privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
|
||||||
|
|
||||||
|
import { translate } from '../../base/i18n';
|
||||||
|
import { IconMessage, IconReply } from '../../base/icons';
|
||||||
|
import { getParticipantById } from '../../base/participants';
|
||||||
|
import { connect } from '../../base/redux';
|
||||||
|
|
||||||
|
import { setPrivateMessageRecipient } from '../actions';
|
||||||
|
|
||||||
|
export type Props = AbstractButtonProps & {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of the participant that the message is to be sent.
|
||||||
|
*/
|
||||||
|
participantID: string,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the button is rendered as a reply button.
|
||||||
|
*/
|
||||||
|
reply: boolean,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to be used to translate i18n labels.
|
||||||
|
*/
|
||||||
|
t: Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The participant object retreived from Redux.
|
||||||
|
*/
|
||||||
|
_participant: Object,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to dispatch the result of the participant selection to send a private message.
|
||||||
|
*/
|
||||||
|
_setPrivateMessageRecipient: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to render a button that initiates the sending of a private message through chet.
|
||||||
|
*/
|
||||||
|
class PrivateMessageButton extends AbstractButton<Props, any> {
|
||||||
|
accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
|
||||||
|
icon = IconMessage;
|
||||||
|
label = 'toolbar.privateMessage';
|
||||||
|
toggledIcon = IconReply;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles clicking / pressing the button, and kicks the participant.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_handleClick() {
|
||||||
|
const { _participant, _setPrivateMessageRecipient } = this.props;
|
||||||
|
|
||||||
|
_setPrivateMessageRecipient(_participant);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to be implemented by subclasses, which must return a
|
||||||
|
* {@code boolean} value indicating if this button is toggled or not.
|
||||||
|
*
|
||||||
|
* @protected
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_isToggled() {
|
||||||
|
return this.props.reply;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the props of this component to Redux actions.
|
||||||
|
*
|
||||||
|
* @param {Function} dispatch - The Redux dispatch function.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
|
||||||
|
return {
|
||||||
|
_setPrivateMessageRecipient: participant => {
|
||||||
|
dispatch(setPrivateMessageRecipient(participant));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps part of the Redux store to the props of this component.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @param {Props} ownProps - The own props of the component.
|
||||||
|
* @returns {Props}
|
||||||
|
*/
|
||||||
|
export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
|
||||||
|
return {
|
||||||
|
_participant: getParticipantById(state, ownProps.participantID)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageButton));
|
|
@ -1,3 +1,4 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export * from './native';
|
export * from './native';
|
||||||
|
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
|
||||||
export * from './web';
|
export * from './web';
|
||||||
|
|
||||||
|
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||||
|
|
|
@ -16,6 +16,7 @@ import AbstractChat, {
|
||||||
|
|
||||||
import ChatInputBar from './ChatInputBar';
|
import ChatInputBar from './ChatInputBar';
|
||||||
import MessageContainer from './MessageContainer';
|
import MessageContainer from './MessageContainer';
|
||||||
|
import MessageRecipient from './MessageRecipient';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -53,6 +54,7 @@ class Chat extends AbstractChat<Props> {
|
||||||
onPressBack = { this._onClose } />
|
onPressBack = { this._onClose } />
|
||||||
<SafeAreaView style = { styles.backdrop }>
|
<SafeAreaView style = { styles.backdrop }>
|
||||||
<MessageContainer messages = { this.props._messages } />
|
<MessageContainer messages = { this.props._messages } />
|
||||||
|
<MessageRecipient />
|
||||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
|
||||||
import { replaceNonUnicodeEmojis } from '../../functions';
|
import { replaceNonUnicodeEmojis } from '../../functions';
|
||||||
|
|
||||||
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
|
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
|
||||||
|
import PrivateMessageButton from '../PrivateMessageButton';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
|
||||||
|
@ -57,14 +58,26 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
<View style = { styles.messageWrapper } >
|
<View style = { styles.messageWrapper } >
|
||||||
{ this._renderAvatar() }
|
{ this._renderAvatar() }
|
||||||
<View style = { detailsWrapperStyle }>
|
<View style = { detailsWrapperStyle }>
|
||||||
<View style = { textWrapperStyle } >
|
<View style = { styles.replyWrapper }>
|
||||||
{
|
<View style = { textWrapperStyle } >
|
||||||
this.props.showDisplayName
|
{
|
||||||
&& this._renderDisplayName()
|
this.props.showDisplayName
|
||||||
}
|
&& this._renderDisplayName()
|
||||||
<Linkify linkStyle = { styles.chatLink }>
|
}
|
||||||
{ replaceNonUnicodeEmojis(messageText) }
|
<Linkify linkStyle = { styles.chatLink }>
|
||||||
</Linkify>
|
{ replaceNonUnicodeEmojis(messageText) }
|
||||||
|
</Linkify>
|
||||||
|
{
|
||||||
|
message.privateMessage
|
||||||
|
&& this._renderPrivateNotice()
|
||||||
|
}
|
||||||
|
</View>
|
||||||
|
{ message.privateMessage && !localMessage
|
||||||
|
&& <PrivateMessageButton
|
||||||
|
participantID = { message.id }
|
||||||
|
reply = { true }
|
||||||
|
showLabel = { false }
|
||||||
|
toggledStyles = { styles.replyStyles } /> }
|
||||||
</View>
|
</View>
|
||||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||||
</View>
|
</View>
|
||||||
|
@ -74,6 +87,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
|
|
||||||
_getFormattedTimestamp: () => string;
|
_getFormattedTimestamp: () => string;
|
||||||
|
|
||||||
|
_getPrivateNoticeMessage: () => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the avatar of the sender.
|
* Renders the avatar of the sender.
|
||||||
*
|
*
|
||||||
|
@ -106,6 +121,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the message privacy notice.
|
||||||
|
*
|
||||||
|
* @returns {React$Element<*>}
|
||||||
|
*/
|
||||||
|
_renderPrivateNotice() {
|
||||||
|
return (
|
||||||
|
<Text style = { styles.privateNotice }>
|
||||||
|
{ this._getPrivateNoticeMessage() }
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the time at which the message was sent.
|
* Renders the time at which the message was sent.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ConfirmDialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
|
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a component for the dialog displayed to avoid mis-sending private messages.
|
||||||
|
*/
|
||||||
|
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ConfirmDialog
|
||||||
|
cancelKey = 'dialog.sendPrivateMessageCancel'
|
||||||
|
contentKey = 'dialog.sendPrivateMessage'
|
||||||
|
okKey = 'dialog.sendPrivateMessageOk'
|
||||||
|
onCancel = { this._onSendGroupMessage }
|
||||||
|
onSubmit = { this._onSendPrivateMessage } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSendGroupMessage: () => boolean;
|
||||||
|
|
||||||
|
_onSendPrivateMessage: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));
|
|
@ -0,0 +1,52 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Text, TouchableHighlight, View } from 'react-native';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Icon, IconCancelSelection } from '../../../base/icons';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
|
import AbstractMessageRecipient, {
|
||||||
|
_mapDispatchToProps,
|
||||||
|
_mapStateToProps
|
||||||
|
} from '../AbstractMessageRecipient';
|
||||||
|
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to implement the displaying of the recipient of the next message.
|
||||||
|
*/
|
||||||
|
class MessageRecipient extends AbstractMessageRecipient {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _privateMessageRecipient } = this.props;
|
||||||
|
|
||||||
|
if (!_privateMessageRecipient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.messageRecipientContainer }>
|
||||||
|
<Text style = { styles.messageRecipientText }>
|
||||||
|
{ t('chat.messageTo', {
|
||||||
|
recipient: _privateMessageRecipient
|
||||||
|
}) }
|
||||||
|
</Text>
|
||||||
|
<TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
|
||||||
|
<Icon
|
||||||
|
src = { IconCancelSelection }
|
||||||
|
style = { styles.messageRecipientCancelIcon } />
|
||||||
|
</TouchableHighlight>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
export { default as Chat } from './Chat';
|
export { default as Chat } from './Chat';
|
||||||
export { default as ChatButton } from './ChatButton';
|
export { default as ChatButton } from './ChatButton';
|
||||||
|
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';
|
||||||
|
|
|
@ -80,6 +80,23 @@ export default {
|
||||||
flex: 1
|
flex: 1
|
||||||
},
|
},
|
||||||
|
|
||||||
|
messageRecipientCancelIcon: {
|
||||||
|
color: ColorPalette.white,
|
||||||
|
fontSize: 18
|
||||||
|
},
|
||||||
|
|
||||||
|
messageRecipientContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: ColorPalette.warning,
|
||||||
|
flexDirection: 'row',
|
||||||
|
padding: BoxModel.padding
|
||||||
|
},
|
||||||
|
|
||||||
|
messageRecipientText: {
|
||||||
|
color: ColorPalette.white,
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The message text itself.
|
* The message text itself.
|
||||||
*/
|
*/
|
||||||
|
@ -115,6 +132,25 @@ export default {
|
||||||
borderTopRightRadius: 0
|
borderTopRightRadius: 0
|
||||||
},
|
},
|
||||||
|
|
||||||
|
replyWrapper: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
|
||||||
|
replyStyles: {
|
||||||
|
iconStyle: {
|
||||||
|
color: 'rgb(118, 136, 152)',
|
||||||
|
fontSize: 22,
|
||||||
|
margin: BoxModel.margin / 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
privateNotice: {
|
||||||
|
color: ColorPalette.warning,
|
||||||
|
fontSize: 13,
|
||||||
|
fontStyle: 'italic'
|
||||||
|
},
|
||||||
|
|
||||||
sendButtonIcon: {
|
sendButtonIcon: {
|
||||||
color: ColorPalette.darkGrey,
|
color: ColorPalette.darkGrey,
|
||||||
fontSize: 22
|
fontSize: 22
|
||||||
|
|
|
@ -14,6 +14,7 @@ import AbstractChat, {
|
||||||
import ChatInput from './ChatInput';
|
import ChatInput from './ChatInput';
|
||||||
import DisplayNameForm from './DisplayNameForm';
|
import DisplayNameForm from './DisplayNameForm';
|
||||||
import MessageContainer from './MessageContainer';
|
import MessageContainer from './MessageContainer';
|
||||||
|
import MessageRecipient from './MessageRecipient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Component for holding the chat feature in a side panel that slides in
|
* React Component for holding the chat feature in a side panel that slides in
|
||||||
|
@ -116,7 +117,10 @@ class Chat extends AbstractChat<Props> {
|
||||||
<MessageContainer
|
<MessageContainer
|
||||||
messages = { this.props._messages }
|
messages = { this.props._messages }
|
||||||
ref = { this._messageContainerRef } />
|
ref = { this._messageContainerRef } />
|
||||||
<ChatInput onResize = { this._onChatInputResize } />
|
<MessageRecipient />
|
||||||
|
<ChatInput
|
||||||
|
onResize = { this._onChatInputResize }
|
||||||
|
onSend = { this.props._onSendMessage } />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ import type { Dispatch } from 'redux';
|
||||||
import { translate } from '../../../base/i18n';
|
import { translate } from '../../../base/i18n';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
import { sendMessage } from '../../actions';
|
|
||||||
|
|
||||||
import SmileysPanel from './SmileysPanel';
|
import SmileysPanel from './SmileysPanel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,6 +26,11 @@ type Props = {
|
||||||
*/
|
*/
|
||||||
onResize: ?Function,
|
onResize: ?Function,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to invoke on message send.
|
||||||
|
*/
|
||||||
|
onSend: Function,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked to obtain translated strings.
|
* Invoked to obtain translated strings.
|
||||||
*/
|
*/
|
||||||
|
@ -163,7 +166,7 @@ class ChatInput extends Component<Props, State> {
|
||||||
const trimmed = this.state.message.trim();
|
const trimmed = this.state.message.trim();
|
||||||
|
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
this.props.dispatch(sendMessage(trimmed));
|
this.props.onSend(trimmed);
|
||||||
|
|
||||||
this.setState({ message: '' });
|
this.setState({ message: '' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
|
||||||
import AbstractChatMessage, {
|
import AbstractChatMessage, {
|
||||||
type Props
|
type Props
|
||||||
} from '../AbstractChatMessage';
|
} from '../AbstractChatMessage';
|
||||||
|
import PrivateMessageButton from '../PrivateMessageButton';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a single chat message.
|
* Renders a single chat message.
|
||||||
|
@ -45,11 +46,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className = 'chatmessage-wrapper'>
|
<div className = 'chatmessage-wrapper'>
|
||||||
<div className = 'chatmessage'>
|
<div className = 'replywrapper'>
|
||||||
{ this.props.showDisplayName && this._renderDisplayName() }
|
<div className = 'chatmessage'>
|
||||||
<div className = 'usermessage'>
|
{ this.props.showDisplayName && this._renderDisplayName() }
|
||||||
{ processedMessage }
|
<div className = 'usermessage'>
|
||||||
|
{ processedMessage }
|
||||||
|
</div>
|
||||||
|
{ message.privateMessage && this._renderPrivateNotice() }
|
||||||
</div>
|
</div>
|
||||||
|
{ message.privateMessage && message.messageType !== 'local'
|
||||||
|
&& <PrivateMessageButton
|
||||||
|
participantID = { message.id }
|
||||||
|
reply = { true }
|
||||||
|
showLabel = { false } /> }
|
||||||
</div>
|
</div>
|
||||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,6 +67,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
|
|
||||||
_getFormattedTimestamp: () => string;
|
_getFormattedTimestamp: () => string;
|
||||||
|
|
||||||
|
_getPrivateNoticeMessage: () => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the display name of the sender.
|
* Renders the display name of the sender.
|
||||||
*
|
*
|
||||||
|
@ -71,6 +82,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the message privacy notice.
|
||||||
|
*
|
||||||
|
* @returns {React$Element<*>}
|
||||||
|
*/
|
||||||
|
_renderPrivateNotice() {
|
||||||
|
return (
|
||||||
|
<div className = 'privatemessagenotice'>
|
||||||
|
{ this._getPrivateNoticeMessage() }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the time at which the message was sent.
|
* Renders the time at which the message was sent.
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Dialog } from '../../../base/dialog';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
|
import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a component for the dialog displayed to avoid mis-sending private messages.
|
||||||
|
*/
|
||||||
|
class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
cancelKey = 'dialog.sendPrivateMessageCancel'
|
||||||
|
okKey = 'dialog.sendPrivateMessageOk'
|
||||||
|
onCancel = { this._onSendGroupMessage }
|
||||||
|
onSubmit = { this._onSendPrivateMessage }
|
||||||
|
titleKey = 'dialog.sendPrivateMessageTitle'
|
||||||
|
width = 'small'>
|
||||||
|
<div>
|
||||||
|
{ this.props.t('dialog.sendPrivateMessage') }
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSendGroupMessage: () => boolean;
|
||||||
|
|
||||||
|
_onSendPrivateMessage: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));
|
|
@ -0,0 +1,48 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Icon, IconCancelSelection } from '../../../base/icons';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
|
||||||
|
import AbstractMessageRecipient, {
|
||||||
|
_mapDispatchToProps,
|
||||||
|
_mapStateToProps
|
||||||
|
} from '../AbstractMessageRecipient';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to implement the displaying of the recipient of the next message.
|
||||||
|
*/
|
||||||
|
class MessageRecipient extends AbstractMessageRecipient {
|
||||||
|
/**
|
||||||
|
* Implements {@code PureComponent#render}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { _privateMessageRecipient } = this.props;
|
||||||
|
|
||||||
|
if (!_privateMessageRecipient) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id = 'chat-recipient'>
|
||||||
|
<span>
|
||||||
|
{ t('chat.messageTo', {
|
||||||
|
recipient: _privateMessageRecipient
|
||||||
|
}) }
|
||||||
|
</span>
|
||||||
|
<div onClick = { this.props._onRemovePrivateMessageRecipient }>
|
||||||
|
<Icon
|
||||||
|
src = { IconCancelSelection } />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
export { default as Chat } from './Chat';
|
export { default as Chat } from './Chat';
|
||||||
export { default as ChatCounter } from './ChatCounter';
|
export { default as ChatCounter } from './ChatCounter';
|
||||||
|
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';
|
||||||
|
|
|
@ -5,8 +5,10 @@ import {
|
||||||
CONFERENCE_JOINED,
|
CONFERENCE_JOINED,
|
||||||
getCurrentConference
|
getCurrentConference
|
||||||
} from '../base/conference';
|
} from '../base/conference';
|
||||||
|
import { openDialog } from '../base/dialog';
|
||||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||||
import {
|
import {
|
||||||
|
getLocalParticipant,
|
||||||
getParticipantById,
|
getParticipantById,
|
||||||
getParticipantDisplayName
|
getParticipantDisplayName
|
||||||
} from '../base/participants';
|
} from '../base/participants';
|
||||||
|
@ -14,14 +16,23 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
||||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||||
import { isButtonEnabled, showToolbox } from '../toolbox';
|
import { isButtonEnabled, showToolbox } from '../toolbox';
|
||||||
|
|
||||||
import { SEND_MESSAGE } from './actionTypes';
|
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
|
||||||
import { addMessage, clearMessages, toggleChat } from './actions';
|
import { addMessage, clearMessages, toggleChat } from './actions';
|
||||||
|
import { ChatPrivacyDialog } from './components';
|
||||||
import { INCOMING_MSG_SOUND_ID } from './constants';
|
import { INCOMING_MSG_SOUND_ID } from './constants';
|
||||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||||
|
|
||||||
declare var APP: Object;
|
declare var APP: Object;
|
||||||
declare var interfaceConfig : Object;
|
declare var interfaceConfig : Object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timeout for when to show the privacy notice after a private message was received.
|
||||||
|
*
|
||||||
|
* E.g. if this value is 20 secs (20000ms), then we show the privacy notice when sending a non private
|
||||||
|
* message after we have received a private message in the last 20 seconds.
|
||||||
|
*/
|
||||||
|
const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the middleware of the chat feature.
|
* Implements the middleware of the chat feature.
|
||||||
*
|
*
|
||||||
|
@ -29,14 +40,16 @@ declare var interfaceConfig : Object;
|
||||||
* @returns {Function}
|
* @returns {Function}
|
||||||
*/
|
*/
|
||||||
MiddlewareRegistry.register(store => next => action => {
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
const { dispatch } = store;
|
||||||
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case APP_WILL_MOUNT:
|
case APP_WILL_MOUNT:
|
||||||
store.dispatch(
|
dispatch(
|
||||||
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case APP_WILL_UNMOUNT:
|
case APP_WILL_UNMOUNT:
|
||||||
store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CONFERENCE_JOINED:
|
case CONFERENCE_JOINED:
|
||||||
|
@ -44,16 +57,43 @@ MiddlewareRegistry.register(store => next => action => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND_MESSAGE: {
|
case SEND_MESSAGE: {
|
||||||
const { conference } = store.getState()['features/base/conference'];
|
const state = store.getState();
|
||||||
|
const { conference } = state['features/base/conference'];
|
||||||
|
|
||||||
if (conference) {
|
if (conference) {
|
||||||
if (typeof APP !== 'undefined') {
|
// There may be cases when we intend to send a private message but we forget to set the
|
||||||
APP.API.notifySendingChatMessage(action.message);
|
// recipient. This logic tries to mitigate this risk.
|
||||||
|
const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
|
||||||
|
|
||||||
|
if (shouldSendPrivateMessageTo) {
|
||||||
|
dispatch(openDialog(ChatPrivacyDialog, {
|
||||||
|
message: action.message,
|
||||||
|
participantID: shouldSendPrivateMessageTo
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// Sending the message if privacy notice doesn't need to be shown.
|
||||||
|
|
||||||
|
if (typeof APP !== 'undefined') {
|
||||||
|
APP.API.notifySendingChatMessage(action.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { privateMessageRecipient } = state['features/chat'];
|
||||||
|
|
||||||
|
if (privateMessageRecipient) {
|
||||||
|
conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
|
||||||
|
_persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
|
||||||
|
} else {
|
||||||
|
conference.sendTextMessage(action.message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
conference.sendTextMessage(action.message);
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case SET_PRIVATE_MESSAGE_RECIPIENT: {
|
||||||
|
_maybeFocusField();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return next(action);
|
return next(action);
|
||||||
|
@ -112,44 +152,183 @@ function _addChatMsgListener(conference, { dispatch, getState }) {
|
||||||
conference.on(
|
conference.on(
|
||||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||||
(id, message, timestamp, nick) => {
|
(id, message, timestamp, nick) => {
|
||||||
// Logic for all platforms:
|
_handleReceivedMessage({
|
||||||
const state = getState();
|
dispatch,
|
||||||
const { isOpen: isChatOpen } = state['features/chat'];
|
getState
|
||||||
|
}, {
|
||||||
if (!isChatOpen) {
|
|
||||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provide a default for for the case when a message is being
|
|
||||||
// backfilled for a participant that has left the conference.
|
|
||||||
const participant = getParticipantById(state, id) || {};
|
|
||||||
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
|
|
||||||
const hasRead = participant.local || isChatOpen;
|
|
||||||
const timestampToDate = timestamp
|
|
||||||
? new Date(timestamp) : new Date();
|
|
||||||
const millisecondsTimestamp = timestampToDate.getTime();
|
|
||||||
|
|
||||||
dispatch(addMessage({
|
|
||||||
displayName,
|
|
||||||
hasRead,
|
|
||||||
id,
|
id,
|
||||||
messageType: participant.local ? 'local' : 'remote',
|
|
||||||
message,
|
message,
|
||||||
timestamp: millisecondsTimestamp
|
nick,
|
||||||
}));
|
privateMessage: false,
|
||||||
|
timestamp
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (typeof APP !== 'undefined') {
|
conference.on(
|
||||||
// Logic for web only:
|
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||||
|
(id, message, timestamp) => {
|
||||||
APP.API.notifyReceivedChatMessage({
|
_handleReceivedMessage({
|
||||||
body: message,
|
dispatch,
|
||||||
id,
|
getState
|
||||||
nick: displayName,
|
}, {
|
||||||
ts: timestamp
|
id,
|
||||||
});
|
message,
|
||||||
|
privateMessage: true,
|
||||||
dispatch(showToolbox(4000));
|
timestamp,
|
||||||
}
|
nick: undefined
|
||||||
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to handle an incoming chat message.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The Redux store.
|
||||||
|
* @param {Object} message - The message object.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, privateMessage, timestamp }) {
|
||||||
|
// Logic for all platforms:
|
||||||
|
const state = getState();
|
||||||
|
const { isOpen: isChatOpen } = state['features/chat'];
|
||||||
|
|
||||||
|
if (!isChatOpen) {
|
||||||
|
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide a default for for the case when a message is being
|
||||||
|
// backfilled for a participant that has left the conference.
|
||||||
|
const participant = getParticipantById(state, id) || {};
|
||||||
|
const localParticipant = getLocalParticipant(getState);
|
||||||
|
const displayName = participant.name || nick || getParticipantDisplayName(state, id);
|
||||||
|
const hasRead = participant.local || isChatOpen;
|
||||||
|
const timestampToDate = timestamp
|
||||||
|
? new Date(timestamp) : new Date();
|
||||||
|
const millisecondsTimestamp = timestampToDate.getTime();
|
||||||
|
|
||||||
|
dispatch(addMessage({
|
||||||
|
displayName,
|
||||||
|
hasRead,
|
||||||
|
id,
|
||||||
|
messageType: participant.local ? 'local' : 'remote',
|
||||||
|
message,
|
||||||
|
privateMessage,
|
||||||
|
recipient: getParticipantDisplayName(state, localParticipant.id),
|
||||||
|
timestamp: millisecondsTimestamp
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (typeof APP !== 'undefined') {
|
||||||
|
// Logic for web only:
|
||||||
|
|
||||||
|
APP.API.notifyReceivedChatMessage({
|
||||||
|
body: message,
|
||||||
|
id,
|
||||||
|
nick: displayName,
|
||||||
|
ts: timestamp
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(showToolbox(4000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focuses the chat text field on web after the message recipient was updated, if needed.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _maybeFocusField() {
|
||||||
|
if (navigator.product !== 'ReactNative') {
|
||||||
|
const textField = document.getElementById('usermsg');
|
||||||
|
|
||||||
|
textField && textField.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the sent private messages as if they were received over the muc.
|
||||||
|
*
|
||||||
|
* This is required as we rely on the fact that we receive all messages from the muc that we send
|
||||||
|
* (as they are sent to everybody), but we don't receive the private messages we send to another participant.
|
||||||
|
* But those messages should be in the store as well, otherwise they don't appear in the chat window.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message) {
|
||||||
|
const localParticipant = getLocalParticipant(getState);
|
||||||
|
const displayName = getParticipantDisplayName(getState, localParticipant.id);
|
||||||
|
|
||||||
|
dispatch(addMessage({
|
||||||
|
displayName,
|
||||||
|
hasRead: true,
|
||||||
|
id: localParticipant.id,
|
||||||
|
messageType: 'local',
|
||||||
|
message,
|
||||||
|
privateMessage: true,
|
||||||
|
recipient: getParticipantDisplayName(getState, recipientID),
|
||||||
|
timestamp: Date.now()
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the participant who we may have wanted to send the message
|
||||||
|
* that we're about to send.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The Redux state.
|
||||||
|
* @param {Object} action - The action being dispatched now.
|
||||||
|
* @returns {string?}
|
||||||
|
*/
|
||||||
|
function _shouldSendPrivateMessageTo(state, action): ?string {
|
||||||
|
if (action.ignorePrivacy) {
|
||||||
|
// Shortcut: this is only true, if we already displayed the notice, so no need to show it again.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { messages, privateMessageRecipient } = state['features/chat'];
|
||||||
|
|
||||||
|
if (privateMessageRecipient) {
|
||||||
|
// We're already sending a private message, no need to warn about privacy.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messages.length) {
|
||||||
|
// No messages yet, no need to warn for privacy.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Platforms sort messages differently
|
||||||
|
const lastMessage = navigator.product === 'ReactNative'
|
||||||
|
? messages[0] : messages[messages.length - 1];
|
||||||
|
|
||||||
|
if (lastMessage.messageType === 'local') {
|
||||||
|
// The sender is probably aware of any private messages as already sent
|
||||||
|
// a message since then. Doesn't make sense to display the notice now.
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastMessage.privateMessage) {
|
||||||
|
// We show the notice if the last received message was private.
|
||||||
|
return lastMessage.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// But messages may come rapidly, we want to protect our users from mis-sending a message
|
||||||
|
// even when there was a reasonable recently received private message.
|
||||||
|
const now = Date.now();
|
||||||
|
const recentPrivateMessages = messages.filter(
|
||||||
|
message =>
|
||||||
|
message.messageType !== 'local'
|
||||||
|
&& message.privateMessage
|
||||||
|
&& message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
|
||||||
|
const recentPrivateMessage = navigator.product === 'ReactNative'
|
||||||
|
? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
|
||||||
|
|
||||||
|
if (recentPrivateMessage) {
|
||||||
|
return recentPrivateMessage.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
import { ReducerRegistry } from '../base/redux';
|
import { ReducerRegistry } from '../base/redux';
|
||||||
|
|
||||||
import { ADD_MESSAGE, CLEAR_MESSAGES, TOGGLE_CHAT } from './actionTypes';
|
import {
|
||||||
|
ADD_MESSAGE,
|
||||||
|
CLEAR_MESSAGES,
|
||||||
|
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||||
|
TOGGLE_CHAT
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
const DEFAULT_STATE = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
lastReadMessage: undefined,
|
lastReadMessage: undefined,
|
||||||
messages: []
|
messages: [],
|
||||||
|
privateMessageRecipient: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||||
|
@ -19,6 +25,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||||
id: action.id,
|
id: action.id,
|
||||||
messageType: action.messageType,
|
messageType: action.messageType,
|
||||||
message: action.message,
|
message: action.message,
|
||||||
|
privateMessage: action.privateMessage,
|
||||||
|
recipient: action.recipient,
|
||||||
timestamp: action.timestamp
|
timestamp: action.timestamp
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,12 +56,20 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||||
messages: []
|
messages: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case SET_PRIVATE_MESSAGE_RECIPIENT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
isOpen: Boolean(action.participant) || state.isOpen,
|
||||||
|
privateMessageRecipient: action.participant
|
||||||
|
};
|
||||||
|
|
||||||
case TOGGLE_CHAT:
|
case TOGGLE_CHAT:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
isOpen: !state.isOpen,
|
isOpen: !state.isOpen,
|
||||||
lastReadMessage: state.messages[
|
lastReadMessage: state.messages[
|
||||||
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1]
|
navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
|
||||||
|
privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { BottomSheet, isDialogOpen } from '../../../base/dialog';
|
||||||
import { getParticipantDisplayName } from '../../../base/participants';
|
import { getParticipantDisplayName } from '../../../base/participants';
|
||||||
import { connect } from '../../../base/redux';
|
import { connect } from '../../../base/redux';
|
||||||
import { StyleType } from '../../../base/styles';
|
import { StyleType } from '../../../base/styles';
|
||||||
|
import { PrivateMessageButton } from '../../../chat';
|
||||||
|
|
||||||
import { hideRemoteVideoMenu } from '../../actions';
|
import { hideRemoteVideoMenu } from '../../actions';
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ class RemoteVideoMenu extends Component<Props> {
|
||||||
<MuteButton { ...buttonProps } />
|
<MuteButton { ...buttonProps } />
|
||||||
<KickButton { ...buttonProps } />
|
<KickButton { ...buttonProps } />
|
||||||
<PinButton { ...buttonProps } />
|
<PinButton { ...buttonProps } />
|
||||||
|
<PrivateMessageButton { ...buttonProps } />
|
||||||
</BottomSheet>
|
</BottomSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { IconMessage } from '../../../base/icons';
|
||||||
|
import { connect } from '../../../base/redux';
|
||||||
|
import { _mapDispatchToProps, _mapStateToProps, type Props } from '../../../chat/components/PrivateMessageButton';
|
||||||
|
|
||||||
|
import RemoteVideoMenuButton from './RemoteVideoMenuButton';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom implementation of the PrivateMessageButton specialized for
|
||||||
|
* the web version of the remote video menu. When the web platform starts to use
|
||||||
|
* the {@code AbstractButton} component for the remote video menu, we can get rid
|
||||||
|
* of this component and use the generic button in the chat feature.
|
||||||
|
*/
|
||||||
|
class PrivateMessageMenuButton extends Component<Props> {
|
||||||
|
/**
|
||||||
|
* Instantiates a new Component instance.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { participantID, t } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RemoteVideoMenuButton
|
||||||
|
buttonText = { t('toolbar.privateMessage') }
|
||||||
|
icon = { IconMessage }
|
||||||
|
id = { `privmsglink_${participantID}` }
|
||||||
|
onClick = { this._onClick } />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick: () => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback to be invoked on pressing the button.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onClick() {
|
||||||
|
const { _participant, _setPrivateMessageRecipient } = this.props;
|
||||||
|
|
||||||
|
_setPrivateMessageRecipient(_participant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageMenuButton));
|
|
@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
|
||||||
import {
|
import {
|
||||||
MuteButton,
|
MuteButton,
|
||||||
KickButton,
|
KickButton,
|
||||||
|
PrivateMessageMenuButton,
|
||||||
RemoteControlButton,
|
RemoteControlButton,
|
||||||
RemoteVideoMenu,
|
RemoteVideoMenu,
|
||||||
VolumeSlider
|
VolumeSlider
|
||||||
|
@ -188,6 +189,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buttons.push(
|
||||||
|
<PrivateMessageMenuButton
|
||||||
|
key = 'privateMessage'
|
||||||
|
participantID = { participantID } />
|
||||||
|
);
|
||||||
|
|
||||||
if (onVolumeChange) {
|
if (onVolumeChange) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<VolumeSlider
|
<VolumeSlider
|
||||||
|
|
|
@ -8,6 +8,7 @@ export { default as MuteButton } from './MuteButton';
|
||||||
export {
|
export {
|
||||||
default as MuteRemoteParticipantDialog
|
default as MuteRemoteParticipantDialog
|
||||||
} from './MuteRemoteParticipantDialog';
|
} from './MuteRemoteParticipantDialog';
|
||||||
|
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
||||||
export {
|
export {
|
||||||
REMOTE_CONTROL_MENU_STATES,
|
REMOTE_CONTROL_MENU_STATES,
|
||||||
default as RemoteControlButton
|
default as RemoteControlButton
|
||||||
|
|
Loading…
Reference in New Issue