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 {
|
||||
background-color: $chatHeaderBackgroundColor;
|
||||
height: 70px;
|
||||
|
@ -196,6 +217,11 @@
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.privatemessagenotice {
|
||||
color: $defaultWarningColor;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.smiley {
|
||||
|
@ -228,6 +254,7 @@
|
|||
.smileys-panel {
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, .6) !important;
|
||||
height: auto;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
|
@ -312,6 +339,16 @@
|
|||
|
||||
.chatmessage-wrapper {
|
||||
max-width: 100%;
|
||||
|
||||
.replywrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
.toolbox-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessage {
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
min-width: 75px;
|
||||
text-align: left;
|
||||
padding: 0px;
|
||||
width: 150px;
|
||||
width: 180px;
|
||||
white-space: nowrap;
|
||||
|
||||
&__item {
|
||||
|
@ -87,6 +87,7 @@
|
|||
display: inline-block;
|
||||
min-width: 20px;
|
||||
height: 100%;
|
||||
padding-right: 10px;
|
||||
|
||||
> * {
|
||||
@include absoluteAligning();
|
||||
|
|
|
@ -28,6 +28,7 @@ $defaultColor: #F1F1F1;
|
|||
$defaultSideBarFontColor: #44A5FF;
|
||||
$defaultSemiDarkColor: #ACACAC;
|
||||
$defaultDarkColor: #2b3d5c;
|
||||
$defaultWarningColor: rgb(215, 121, 118);
|
||||
|
||||
/**
|
||||
* Toolbar
|
||||
|
|
|
@ -48,11 +48,14 @@
|
|||
"chat": {
|
||||
"error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
|
||||
"messagebox": "Type a message",
|
||||
"messageTo": "Private message to {{recipient}}",
|
||||
"nickname": {
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat"
|
||||
},
|
||||
"title": "Chat"
|
||||
"privateNotice": "Private message to {{recipient}}",
|
||||
"title": "Chat",
|
||||
"you": "you"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"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. ",
|
||||
"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.",
|
||||
"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",
|
||||
"sessTerminated": "Call terminated",
|
||||
"Share": "Share",
|
||||
|
@ -571,6 +578,7 @@
|
|||
"moreActionsMenu": "More actions menu",
|
||||
"mute": "Toggle mute audio",
|
||||
"pip": "Toggle Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Toggle raise hand",
|
||||
"recording": "Toggle recording",
|
||||
|
@ -611,6 +619,7 @@
|
|||
"mute": "Mute / Unmute",
|
||||
"openChat": "Open chat",
|
||||
"pip": "Enter Picture-in-Picture mode",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower 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 IconMenuThumb } from './thumb-menu.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 IconMicrophone } from './microphone.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 IconRemoteControlStart } from './play.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 IconRoomLock } from './security.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',
|
||||
red: '#D00000',
|
||||
transparent: 'rgba(0, 0, 0, 0)',
|
||||
warning: 'rgb(215, 121, 118)',
|
||||
white: '#FFFFFF',
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,11 +30,23 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
|
|||
*
|
||||
* {
|
||||
* type: SEND_MESSAGE,
|
||||
* ignorePrivacy: boolean,
|
||||
* message: string
|
||||
* }
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
SEND_MESSAGE,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
TOGGLE_CHAT
|
||||
} from './actionTypes';
|
||||
|
||||
|
@ -53,18 +54,37 @@ export function clearMessages() {
|
|||
* Sends a chat message to everyone in the conference.
|
||||
*
|
||||
* @param {string} message - The chat message to send out.
|
||||
* @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
|
||||
* @returns {{
|
||||
* type: SEND_MESSAGE,
|
||||
* ignorePrivacy: boolean,
|
||||
* message: string
|
||||
* }}
|
||||
*/
|
||||
export function sendMessage(message: string) {
|
||||
export function sendMessage(message: string, ignorePrivacy: boolean = false) {
|
||||
return {
|
||||
type: SEND_MESSAGE,
|
||||
ignorePrivacy,
|
||||
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.
|
||||
*
|
||||
|
|
|
@ -56,4 +56,17 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
|
|||
return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
|
||||
.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
|
||||
|
||||
export * from './native';
|
||||
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export * from './web';
|
||||
|
||||
export { default as PrivateMessageButton } from './PrivateMessageButton';
|
||||
|
|
|
@ -16,6 +16,7 @@ import AbstractChat, {
|
|||
|
||||
import ChatInputBar from './ChatInputBar';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
|
@ -53,6 +54,7 @@ class Chat extends AbstractChat<Props> {
|
|||
onPressBack = { this._onClose } />
|
||||
<SafeAreaView style = { styles.backdrop }>
|
||||
<MessageContainer messages = { this.props._messages } />
|
||||
<MessageRecipient />
|
||||
<ChatInputBar onSend = { this.props._onSendMessage } />
|
||||
</SafeAreaView>
|
||||
</KeyboardAvoidingView>
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
|
|||
import { replaceNonUnicodeEmojis } from '../../functions';
|
||||
|
||||
import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
|
||||
import PrivateMessageButton from '../PrivateMessageButton';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
|
@ -57,14 +58,26 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
<View style = { styles.messageWrapper } >
|
||||
{ this._renderAvatar() }
|
||||
<View style = { detailsWrapperStyle }>
|
||||
<View style = { textWrapperStyle } >
|
||||
{
|
||||
this.props.showDisplayName
|
||||
&& this._renderDisplayName()
|
||||
}
|
||||
<Linkify linkStyle = { styles.chatLink }>
|
||||
{ replaceNonUnicodeEmojis(messageText) }
|
||||
</Linkify>
|
||||
<View style = { styles.replyWrapper }>
|
||||
<View style = { textWrapperStyle } >
|
||||
{
|
||||
this.props.showDisplayName
|
||||
&& this._renderDisplayName()
|
||||
}
|
||||
<Linkify linkStyle = { styles.chatLink }>
|
||||
{ replaceNonUnicodeEmojis(messageText) }
|
||||
</Linkify>
|
||||
{
|
||||
message.privateMessage
|
||||
&& this._renderPrivateNotice()
|
||||
}
|
||||
</View>
|
||||
{ message.privateMessage && !localMessage
|
||||
&& <PrivateMessageButton
|
||||
participantID = { message.id }
|
||||
reply = { true }
|
||||
showLabel = { false }
|
||||
toggledStyles = { styles.replyStyles } /> }
|
||||
</View>
|
||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||
</View>
|
||||
|
@ -74,6 +87,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
|
||||
_getFormattedTimestamp: () => string;
|
||||
|
||||
_getPrivateNoticeMessage: () => string;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -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 ChatButton } from './ChatButton';
|
||||
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';
|
||||
|
|
|
@ -80,6 +80,23 @@ export default {
|
|||
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.
|
||||
*/
|
||||
|
@ -115,6 +132,25 @@ export default {
|
|||
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: {
|
||||
color: ColorPalette.darkGrey,
|
||||
fontSize: 22
|
||||
|
|
|
@ -14,6 +14,7 @@ import AbstractChat, {
|
|||
import ChatInput from './ChatInput';
|
||||
import DisplayNameForm from './DisplayNameForm';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
|
||||
/**
|
||||
* React Component for holding the chat feature in a side panel that slides in
|
||||
|
@ -116,7 +117,10 @@ class Chat extends AbstractChat<Props> {
|
|||
<MessageContainer
|
||||
messages = { this.props._messages }
|
||||
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 { connect } from '../../../base/redux';
|
||||
|
||||
import { sendMessage } from '../../actions';
|
||||
|
||||
import SmileysPanel from './SmileysPanel';
|
||||
|
||||
/**
|
||||
|
@ -28,6 +26,11 @@ type Props = {
|
|||
*/
|
||||
onResize: ?Function,
|
||||
|
||||
/**
|
||||
* Callback to invoke on message send.
|
||||
*/
|
||||
onSend: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
|
@ -163,7 +166,7 @@ class ChatInput extends Component<Props, State> {
|
|||
const trimmed = this.state.message.trim();
|
||||
|
||||
if (trimmed) {
|
||||
this.props.dispatch(sendMessage(trimmed));
|
||||
this.props.onSend(trimmed);
|
||||
|
||||
this.setState({ message: '' });
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
|
|||
import AbstractChatMessage, {
|
||||
type Props
|
||||
} from '../AbstractChatMessage';
|
||||
import PrivateMessageButton from '../PrivateMessageButton';
|
||||
|
||||
/**
|
||||
* Renders a single chat message.
|
||||
|
@ -45,11 +46,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
|
||||
return (
|
||||
<div className = 'chatmessage-wrapper'>
|
||||
<div className = 'chatmessage'>
|
||||
{ this.props.showDisplayName && this._renderDisplayName() }
|
||||
<div className = 'usermessage'>
|
||||
{ processedMessage }
|
||||
<div className = 'replywrapper'>
|
||||
<div className = 'chatmessage'>
|
||||
{ this.props.showDisplayName && this._renderDisplayName() }
|
||||
<div className = 'usermessage'>
|
||||
{ processedMessage }
|
||||
</div>
|
||||
{ message.privateMessage && this._renderPrivateNotice() }
|
||||
</div>
|
||||
{ message.privateMessage && message.messageType !== 'local'
|
||||
&& <PrivateMessageButton
|
||||
participantID = { message.id }
|
||||
reply = { true }
|
||||
showLabel = { false } /> }
|
||||
</div>
|
||||
{ this.props.showTimestamp && this._renderTimestamp() }
|
||||
</div>
|
||||
|
@ -58,6 +67,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
|
|||
|
||||
_getFormattedTimestamp: () => string;
|
||||
|
||||
_getPrivateNoticeMessage: () => string;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
|
@ -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 ChatCounter } from './ChatCounter';
|
||||
export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';
|
||||
|
|
|
@ -5,8 +5,10 @@ import {
|
|||
CONFERENCE_JOINED,
|
||||
getCurrentConference
|
||||
} from '../base/conference';
|
||||
import { openDialog } from '../base/dialog';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName
|
||||
} from '../base/participants';
|
||||
|
@ -14,14 +16,23 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
|
|||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
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 { ChatPrivacyDialog } from './components';
|
||||
import { INCOMING_MSG_SOUND_ID } from './constants';
|
||||
import { INCOMING_MSG_SOUND_FILE } from './sounds';
|
||||
|
||||
declare var APP: 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.
|
||||
*
|
||||
|
@ -29,14 +40,16 @@ declare var interfaceConfig : Object;
|
|||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
const { dispatch } = store;
|
||||
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
store.dispatch(
|
||||
dispatch(
|
||||
registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||
dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
|
@ -44,16 +57,43 @@ MiddlewareRegistry.register(store => next => action => {
|
|||
break;
|
||||
|
||||
case SEND_MESSAGE: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
const state = store.getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifySendingChatMessage(action.message);
|
||||
// There may be cases when we intend to send a private message but we forget to set the
|
||||
// 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;
|
||||
}
|
||||
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT: {
|
||||
_maybeFocusField();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
@ -112,44 +152,183 @@ function _addChatMsgListener(conference, { dispatch, getState }) {
|
|||
conference.on(
|
||||
JitsiConferenceEvents.MESSAGE_RECEIVED,
|
||||
(id, message, timestamp, nick) => {
|
||||
// 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 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,
|
||||
_handleReceivedMessage({
|
||||
dispatch,
|
||||
getState
|
||||
}, {
|
||||
id,
|
||||
messageType: participant.local ? 'local' : 'remote',
|
||||
message,
|
||||
timestamp: millisecondsTimestamp
|
||||
}));
|
||||
nick,
|
||||
privateMessage: false,
|
||||
timestamp
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
// Logic for web only:
|
||||
|
||||
APP.API.notifyReceivedChatMessage({
|
||||
body: message,
|
||||
id,
|
||||
nick: displayName,
|
||||
ts: timestamp
|
||||
});
|
||||
|
||||
dispatch(showToolbox(4000));
|
||||
}
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
|
||||
(id, message, timestamp) => {
|
||||
_handleReceivedMessage({
|
||||
dispatch,
|
||||
getState
|
||||
}, {
|
||||
id,
|
||||
message,
|
||||
privateMessage: true,
|
||||
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 { ADD_MESSAGE, CLEAR_MESSAGES, TOGGLE_CHAT } from './actionTypes';
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
CLEAR_MESSAGES,
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
TOGGLE_CHAT
|
||||
} from './actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
isOpen: false,
|
||||
lastReadMessage: undefined,
|
||||
messages: []
|
||||
messages: [],
|
||||
privateMessageRecipient: undefined
|
||||
};
|
||||
|
||||
ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
||||
|
@ -19,6 +25,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
|||
id: action.id,
|
||||
messageType: action.messageType,
|
||||
message: action.message,
|
||||
privateMessage: action.privateMessage,
|
||||
recipient: action.recipient,
|
||||
timestamp: action.timestamp
|
||||
};
|
||||
|
||||
|
@ -48,12 +56,20 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
|
|||
messages: []
|
||||
};
|
||||
|
||||
case SET_PRIVATE_MESSAGE_RECIPIENT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: Boolean(action.participant) || state.isOpen,
|
||||
privateMessageRecipient: action.participant
|
||||
};
|
||||
|
||||
case TOGGLE_CHAT:
|
||||
return {
|
||||
...state,
|
||||
isOpen: !state.isOpen,
|
||||
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 { connect } from '../../../base/redux';
|
||||
import { StyleType } from '../../../base/styles';
|
||||
import { PrivateMessageButton } from '../../../chat';
|
||||
|
||||
import { hideRemoteVideoMenu } from '../../actions';
|
||||
|
||||
|
@ -95,6 +96,7 @@ class RemoteVideoMenu extends Component<Props> {
|
|||
<MuteButton { ...buttonProps } />
|
||||
<KickButton { ...buttonProps } />
|
||||
<PinButton { ...buttonProps } />
|
||||
<PrivateMessageButton { ...buttonProps } />
|
||||
</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 {
|
||||
MuteButton,
|
||||
KickButton,
|
||||
PrivateMessageMenuButton,
|
||||
RemoteControlButton,
|
||||
RemoteVideoMenu,
|
||||
VolumeSlider
|
||||
|
@ -188,6 +189,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
|
|||
);
|
||||
}
|
||||
|
||||
buttons.push(
|
||||
<PrivateMessageMenuButton
|
||||
key = 'privateMessage'
|
||||
participantID = { participantID } />
|
||||
);
|
||||
|
||||
if (onVolumeChange) {
|
||||
buttons.push(
|
||||
<VolumeSlider
|
||||
|
|
|
@ -8,6 +8,7 @@ export { default as MuteButton } from './MuteButton';
|
|||
export {
|
||||
default as MuteRemoteParticipantDialog
|
||||
} from './MuteRemoteParticipantDialog';
|
||||
export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
|
||||
export {
|
||||
REMOTE_CONTROL_MENU_STATES,
|
||||
default as RemoteControlButton
|
||||
|
|
Loading…
Reference in New Issue