Browse Source

feat: private messages

master
Bettenbuk Zoltan 5 years ago
parent
commit
42271b1b89
34 changed files with 987 additions and 63 deletions
  1. 37
    0
      css/_chat.scss
  2. 2
    1
      css/_popup_menu.scss
  3. 1
    0
      css/_variables.scss
  4. 10
    1
      lang/main.json
  5. 2
    0
      react/features/base/icons/svg/index.js
  6. 1
    0
      react/features/base/icons/svg/message.svg
  7. 1
    0
      react/features/base/icons/svg/reply.svg
  8. 1
    0
      react/features/base/styles/components/styles/ColorPalette.js
  9. 12
    0
      react/features/chat/actionTypes.js
  10. 21
    1
      react/features/chat/actions.js
  11. 13
    0
      react/features/chat/components/AbstractChatMessage.js
  12. 116
    0
      react/features/chat/components/AbstractChatPrivacyDialog.js
  13. 61
    0
      react/features/chat/components/AbstractMessageRecipient.js
  14. 101
    0
      react/features/chat/components/PrivateMessageButton.js
  15. 1
    0
      react/features/chat/components/index.native.js
  16. 2
    0
      react/features/chat/components/index.web.js
  17. 2
    0
      react/features/chat/components/native/Chat.js
  18. 36
    8
      react/features/chat/components/native/ChatMessage.js
  19. 37
    0
      react/features/chat/components/native/ChatPrivacyDialog.js
  20. 52
    0
      react/features/chat/components/native/MessageRecipient.js
  21. 1
    0
      react/features/chat/components/native/index.js
  22. 36
    0
      react/features/chat/components/native/styles.js
  23. 5
    1
      react/features/chat/components/web/Chat.js
  24. 6
    3
      react/features/chat/components/web/ChatInput.js
  25. 28
    4
      react/features/chat/components/web/ChatMessage.js
  26. 42
    0
      react/features/chat/components/web/ChatPrivacyDialog.js
  27. 48
    0
      react/features/chat/components/web/MessageRecipient.js
  28. 1
    0
      react/features/chat/components/web/index.js
  29. 220
    41
      react/features/chat/middleware.js
  30. 19
    3
      react/features/chat/reducer.js
  31. 2
    0
      react/features/remote-video-menu/components/native/RemoteVideoMenu.js
  32. 62
    0
      react/features/remote-video-menu/components/web/PrivateMessageMenuButton.js
  33. 7
    0
      react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js
  34. 1
    0
      react/features/remote-video-menu/components/web/index.js

+ 37
- 0
css/_chat.scss View File

@@ -80,6 +80,27 @@
80 80
     }
81 81
 }
82 82
 
83
+#chat-recipient {
84
+    align-items: center;
85
+    background-color: $defaultWarningColor;
86
+    display: flex;
87
+    flex-direction: row;
88
+    padding: 10px;
89
+
90
+    span {
91
+        color: white;
92
+        display: flex;
93
+        flex: 1;
94
+    }
95
+
96
+    div {
97
+        svg {
98
+            cursor: pointer;
99
+            fill: white
100
+        }
101
+    }
102
+}
103
+
83 104
 .chat-header {
84 105
     background-color: $chatHeaderBackgroundColor;
85 106
     height: 70px;
@@ -196,6 +217,11 @@
196 217
             padding: 0;
197 218
         }
198 219
     }
220
+
221
+    .privatemessagenotice {
222
+        color: $defaultWarningColor;
223
+        font-style: italic;
224
+    }
199 225
 }
200 226
 
201 227
 .smiley {
@@ -228,6 +254,7 @@
228 254
 .smileys-panel {
229 255
     bottom: 100%;
230 256
     box-sizing: border-box;
257
+    background-color: rgba(0, 0, 0, .6) !important;
231 258
     height: auto;
232 259
     max-height: 0;
233 260
     overflow: hidden;
@@ -312,6 +339,16 @@
312 339
 
313 340
     .chatmessage-wrapper {
314 341
         max-width: 100%;
342
+
343
+        .replywrapper {
344
+            display: flex;
345
+            flex-direction: row;
346
+            align-items: center;
347
+
348
+            .toolbox-icon {
349
+                cursor: pointer;
350
+            }
351
+        }
315 352
     }
316 353
 
317 354
     .chatmessage {

+ 2
- 1
css/_popup_menu.scss View File

@@ -6,7 +6,7 @@
6 6
     min-width: 75px;
7 7
     text-align: left;
8 8
     padding: 0px;
9
-    width: 150px;
9
+    width: 180px;
10 10
     white-space: nowrap;
11 11
 
12 12
     &__item {
@@ -87,6 +87,7 @@
87 87
         display: inline-block;
88 88
         min-width: 20px;
89 89
         height: 100%;
90
+        padding-right: 10px;
90 91
 
91 92
         > * {
92 93
             @include absoluteAligning();

+ 1
- 0
css/_variables.scss View File

@@ -28,6 +28,7 @@ $defaultColor: #F1F1F1;
28 28
 $defaultSideBarFontColor: #44A5FF;
29 29
 $defaultSemiDarkColor: #ACACAC;
30 30
 $defaultDarkColor: #2b3d5c;
31
+$defaultWarningColor: rgb(215, 121, 118);
31 32
 
32 33
 /**
33 34
  * Toolbar

+ 10
- 1
lang/main.json View File

@@ -48,11 +48,14 @@
48 48
     "chat": {
49 49
         "error": "Error: your message \"{{originalText}}\" was not sent. Reason: {{error}}",
50 50
         "messagebox": "Type a message",
51
+        "messageTo": "Private message to {{recipient}}",
51 52
         "nickname": {
52 53
             "popover": "Choose a nickname",
53 54
             "title": "Enter a nickname to use chat"
54 55
         },
55
-        "title": "Chat"
56
+        "privateNotice": "Private message to {{recipient}}",
57
+        "title": "Chat",
58
+        "you": "you"
56 59
     },
57 60
     "connectingOverlay": {
58 61
         "joiningRoom": "Connecting you to your meeting..."
@@ -235,6 +238,10 @@
235 238
         "screenSharingFirefoxPermissionDeniedError": "Something went wrong while we were trying to share your screen. Please make sure that you have given us permission to do so. ",
236 239
         "screenSharingFirefoxPermissionDeniedTitle": "Oops! We weren’t able to start screen sharing!",
237 240
         "screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing extension permissions. Please reload and try again.",
241
+        "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?",
242
+        "sendPrivateMessageCancel": "Send to the group",
243
+        "sendPrivateMessageOk": "Send privately",
244
+        "sendPrivateMessageTitle": "Send privately?",
238 245
         "serviceUnavailable": "Service unavailable",
239 246
         "sessTerminated": "Call terminated",
240 247
         "Share": "Share",
@@ -571,6 +578,7 @@
571 578
             "moreActionsMenu": "More actions menu",
572 579
             "mute": "Toggle mute audio",
573 580
             "pip": "Toggle Picture-in-Picture mode",
581
+            "privateMessage": "Send private message",
574 582
             "profile": "Edit your profile",
575 583
             "raiseHand": "Toggle raise hand",
576 584
             "recording": "Toggle recording",
@@ -611,6 +619,7 @@
611 619
         "mute": "Mute / Unmute",
612 620
         "openChat": "Open chat",
613 621
         "pip": "Enter Picture-in-Picture mode",
622
+        "privateMessage": "Send private message",
614 623
         "profile": "Edit your profile",
615 624
         "raiseHand": "Raise / Lower your hand",
616 625
         "raiseYourHand": "Raise your hand",

+ 2
- 0
react/features/base/icons/svg/index.js View File

@@ -36,6 +36,7 @@ export { default as IconMenu } from './menu.svg';
36 36
 export { default as IconMenuDown } from './menu-down.svg';
37 37
 export { default as IconMenuThumb } from './thumb-menu.svg';
38 38
 export { default as IconMenuUp } from './menu-up.svg';
39
+export { default as IconMessage } from './message.svg';
39 40
 export { default as IconMicDisabled } from './mic-disabled.svg';
40 41
 export { default as IconMicrophone } from './microphone.svg';
41 42
 export { default as IconModerator } from './star.svg';
@@ -48,6 +49,7 @@ export { default as IconRaisedHand } from './raised-hand.svg';
48 49
 export { default as IconRec } from './rec.svg';
49 50
 export { default as IconRemoteControlStart } from './play.svg';
50 51
 export { default as IconRemoteControlStop } from './stop.svg';
52
+export { default as IconReply } from './reply.svg';
51 53
 export { default as IconRestore } from './restore.svg';
52 54
 export { default as IconRoomLock } from './security.svg';
53 55
 export { default as IconRoomUnlock } from './security-locked.svg';

+ 1
- 0
react/features/base/icons/svg/message.svg View File

@@ -0,0 +1 @@
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>

+ 1
- 0
react/features/base/icons/svg/reply.svg View File

@@ -0,0 +1 @@
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>

+ 1
- 0
react/features/base/styles/components/styles/ColorPalette.js View File

@@ -28,6 +28,7 @@ export const ColorPalette = {
28 28
     overflowMenuItemUnderlay: '#EEEEEE',
29 29
     red: '#D00000',
30 30
     transparent: 'rgba(0, 0, 0, 0)',
31
+    warning: 'rgb(215, 121, 118)',
31 32
     white: '#FFFFFF',
32 33
 
33 34
     /**

+ 12
- 0
react/features/chat/actionTypes.js View File

@@ -30,11 +30,23 @@ export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
30 30
  *
31 31
  * {
32 32
  *     type: SEND_MESSAGE,
33
+ *     ignorePrivacy: boolean,
33 34
  *     message: string
34 35
  * }
35 36
  */
36 37
 export const SEND_MESSAGE = 'SEND_MESSAGE';
37 38
 
39
+/**
40
+ * The type of action which signals the initiation of sending of as private message to the
41
+ * supplied recipient.
42
+ *
43
+ * {
44
+ *     participant: Participant,
45
+ *     type: SET_PRIVATE_MESSAGE_RECIPIENT
46
+ * }
47
+ */
48
+export const SET_PRIVATE_MESSAGE_RECIPIENT = 'SET_PRIVATE_MESSAGE_RECIPIENT';
49
+
38 50
 /**
39 51
  * The type of the action which signals to toggle the display of the chat panel.
40 52
  *

+ 21
- 1
react/features/chat/actions.js View File

@@ -4,6 +4,7 @@ import {
4 4
     ADD_MESSAGE,
5 5
     CLEAR_MESSAGES,
6 6
     SEND_MESSAGE,
7
+    SET_PRIVATE_MESSAGE_RECIPIENT,
7 8
     TOGGLE_CHAT
8 9
 } from './actionTypes';
9 10
 
@@ -53,18 +54,37 @@ export function clearMessages() {
53 54
  * Sends a chat message to everyone in the conference.
54 55
  *
55 56
  * @param {string} message - The chat message to send out.
57
+ * @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
56 58
  * @returns {{
57 59
  *     type: SEND_MESSAGE,
60
+ *     ignorePrivacy: boolean,
58 61
  *     message: string
59 62
  * }}
60 63
  */
61
-export function sendMessage(message: string) {
64
+export function sendMessage(message: string, ignorePrivacy: boolean = false) {
62 65
     return {
63 66
         type: SEND_MESSAGE,
67
+        ignorePrivacy,
64 68
         message
65 69
     };
66 70
 }
67 71
 
72
+/**
73
+ * Initiates the sending of a private message to the supplied participant.
74
+ *
75
+ * @param {Participant} participant - The participant to set the recipient to.
76
+ * @returns {{
77
+ *     participant: Participant,
78
+ *     type: SET_PRIVATE_MESSAGE_RECIPIENT
79
+ * }}
80
+ */
81
+export function setPrivateMessageRecipient(participant: Object) {
82
+    return {
83
+        participant,
84
+        type: SET_PRIVATE_MESSAGE_RECIPIENT
85
+    };
86
+}
87
+
68 88
 /**
69 89
  * Toggles display of the chat side panel.
70 90
  *

+ 13
- 0
react/features/chat/components/AbstractChatMessage.js View File

@@ -56,4 +56,17 @@ export default class AbstractChatMessage<P: Props> extends PureComponent<P> {
56 56
         return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
57 57
             .format(TIMESTAMP_FORMAT);
58 58
     }
59
+
60
+    /**
61
+     * Returns the message that is displayed as a notice for private messages.
62
+     *
63
+     * @returns {string}
64
+     */
65
+    _getPrivateNoticeMessage() {
66
+        const { message, t } = this.props;
67
+
68
+        return t('chat.privateNotice', {
69
+            recipient: message.messageType === 'local' ? message.recipient : t('chat.you')
70
+        });
71
+    }
59 72
 }

+ 116
- 0
react/features/chat/components/AbstractChatPrivacyDialog.js View File

@@ -0,0 +1,116 @@
1
+// @flow
2
+
3
+import { PureComponent } from 'react';
4
+
5
+import { sendMessage, setPrivateMessageRecipient } from '../actions';
6
+import { getParticipantById } from '../../base/participants';
7
+
8
+type Props = {
9
+
10
+    /**
11
+     * The message that is about to be sent.
12
+     */
13
+    message: Object,
14
+
15
+    /**
16
+     * The ID of the participant that we think the message may be intended to.
17
+     */
18
+    participantID: string,
19
+
20
+    /**
21
+     * Function to be used to translate i18n keys.
22
+     */
23
+    t: Function,
24
+
25
+    /**
26
+     * Prop to be invoked on sending the message.
27
+     */
28
+    _onSendMessage: Function,
29
+
30
+    /**
31
+     * Prop to be invoked when the user wants to set a private recipient.
32
+     */
33
+    _onSetMessageRecipient: Function,
34
+
35
+    /**
36
+     * The participant retreived from Redux by the participanrID prop.
37
+     */
38
+    _participant: Object
39
+};
40
+
41
+/**
42
+ * Abstract class for the dialog displayed to avoid mis-sending private messages.
43
+ */
44
+export class AbstractChatPrivacyDialog extends PureComponent<Props> {
45
+    /**
46
+     * Instantiates a new instance.
47
+     *
48
+     * @inheritdoc
49
+     */
50
+    constructor(props: Props) {
51
+        super(props);
52
+
53
+        this._onSendGroupMessage = this._onSendGroupMessage.bind(this);
54
+        this._onSendPrivateMessage = this._onSendPrivateMessage.bind(this);
55
+    }
56
+
57
+    _onSendGroupMessage: () => boolean;
58
+
59
+    /**
60
+     * Callback to be invoked for cancel action (user wants to send a group message).
61
+     *
62
+     * @returns {boolean}
63
+     */
64
+    _onSendGroupMessage() {
65
+        this.props._onSendMessage(this.props.message);
66
+
67
+        return true;
68
+    }
69
+
70
+    _onSendPrivateMessage: () => boolean;
71
+
72
+    /**
73
+     * Callback to be invoked for submit action (user wants to send a private message).
74
+     *
75
+     * @returns {void}
76
+     */
77
+    _onSendPrivateMessage() {
78
+        const { message, _onSendMessage, _onSetMessageRecipient, _participant } = this.props;
79
+
80
+        _onSetMessageRecipient(_participant);
81
+        _onSendMessage(message);
82
+
83
+        return true;
84
+    }
85
+}
86
+
87
+/**
88
+ * Maps part of the props of this component to Redux actions.
89
+ *
90
+ * @param {Function} dispatch - The Redux dispatch function.
91
+ * @returns {Props}
92
+ */
93
+export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
94
+    return {
95
+        _onSendMessage: (message: Object) => {
96
+            dispatch(sendMessage(message, true));
97
+        },
98
+
99
+        _onSetMessageRecipient: participant => {
100
+            dispatch(setPrivateMessageRecipient(participant));
101
+        }
102
+    };
103
+}
104
+
105
+/**
106
+ * Maps part of the Redux store to the props of this component.
107
+ *
108
+ * @param {Object} state - The Redux state.
109
+ * @param {Props} ownProps - The own props of the component.
110
+ * @returns {Props}
111
+ */
112
+export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
113
+    return {
114
+        _participant: getParticipantById(state, ownProps.participantID)
115
+    };
116
+}

+ 61
- 0
react/features/chat/components/AbstractMessageRecipient.js View File

@@ -0,0 +1,61 @@
1
+// @flow
2
+
3
+import { PureComponent } from 'react';
4
+
5
+import { getParticipantDisplayName } from '../../base/participants';
6
+
7
+import { setPrivateMessageRecipient } from '../actions';
8
+
9
+type Props = {
10
+
11
+    /**
12
+     * Function used to translate i18n labels.
13
+     */
14
+    t: Function,
15
+
16
+    /**
17
+     * Function to remove the recipent setting of the chat window.
18
+     */
19
+    _onRemovePrivateMessageRecipient: Function,
20
+
21
+    /**
22
+     * The name of the message recipient, if any.
23
+     */
24
+    _privateMessageRecipient: ?string
25
+};
26
+
27
+/**
28
+ * Abstract class for the {@code MessageRecipient} component.
29
+ */
30
+export default class AbstractMessageRecipient extends PureComponent<Props> {
31
+
32
+}
33
+
34
+/**
35
+ * Maps part of the props of this component to Redux actions.
36
+ *
37
+ * @param {Function} dispatch - The Redux dispatch function.
38
+ * @returns {Props}
39
+ */
40
+export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
41
+    return {
42
+        _onRemovePrivateMessageRecipient: () => {
43
+            dispatch(setPrivateMessageRecipient());
44
+        }
45
+    };
46
+}
47
+
48
+/**
49
+ * Maps part of the Redux store to the props of this component.
50
+ *
51
+ * @param {Object} state - The Redux state.
52
+ * @returns {Props}
53
+ */
54
+export function _mapStateToProps(state: Object): $Shape<Props> {
55
+    const { privateMessageRecipient } = state['features/chat'];
56
+
57
+    return {
58
+        _privateMessageRecipient:
59
+            privateMessageRecipient ? getParticipantDisplayName(state, privateMessageRecipient.id) : undefined
60
+    };
61
+}

+ 101
- 0
react/features/chat/components/PrivateMessageButton.js View File

@@ -0,0 +1,101 @@
1
+// @flow
2
+
3
+import { AbstractButton, type AbstractButtonProps } from '../../base/toolbox';
4
+
5
+import { translate } from '../../base/i18n';
6
+import { IconMessage, IconReply } from '../../base/icons';
7
+import { getParticipantById } from '../../base/participants';
8
+import { connect } from '../../base/redux';
9
+
10
+import { setPrivateMessageRecipient } from '../actions';
11
+
12
+export type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * The ID of the participant that the message is to be sent.
16
+     */
17
+    participantID: string,
18
+
19
+    /**
20
+     * True if the button is rendered as a reply button.
21
+     */
22
+    reply: boolean,
23
+
24
+    /**
25
+     * Function to be used to translate i18n labels.
26
+     */
27
+    t: Function,
28
+
29
+    /**
30
+     * The participant object retreived from Redux.
31
+     */
32
+    _participant: Object,
33
+
34
+    /**
35
+     * Function to dispatch the result of the participant selection to send a private message.
36
+     */
37
+    _setPrivateMessageRecipient: Function
38
+};
39
+
40
+/**
41
+ * Class to render a button that initiates the sending of a private message through chet.
42
+ */
43
+class PrivateMessageButton extends AbstractButton<Props, any> {
44
+    accessibilityLabel = 'toolbar.accessibilityLabel.privateMessage';
45
+    icon = IconMessage;
46
+    label = 'toolbar.privateMessage';
47
+    toggledIcon = IconReply;
48
+
49
+    /**
50
+     * Handles clicking / pressing the button, and kicks the participant.
51
+     *
52
+     * @private
53
+     * @returns {void}
54
+     */
55
+    _handleClick() {
56
+        const { _participant, _setPrivateMessageRecipient } = this.props;
57
+
58
+        _setPrivateMessageRecipient(_participant);
59
+    }
60
+
61
+    /**
62
+     * Helper function to be implemented by subclasses, which must return a
63
+     * {@code boolean} value indicating if this button is toggled or not.
64
+     *
65
+     * @protected
66
+     * @returns {boolean}
67
+     */
68
+    _isToggled() {
69
+        return this.props.reply;
70
+    }
71
+
72
+}
73
+
74
+/**
75
+ * Maps part of the props of this component to Redux actions.
76
+ *
77
+ * @param {Function} dispatch - The Redux dispatch function.
78
+ * @returns {Props}
79
+ */
80
+export function _mapDispatchToProps(dispatch: Function): $Shape<Props> {
81
+    return {
82
+        _setPrivateMessageRecipient: participant => {
83
+            dispatch(setPrivateMessageRecipient(participant));
84
+        }
85
+    };
86
+}
87
+
88
+/**
89
+ * Maps part of the Redux store to the props of this component.
90
+ *
91
+ * @param {Object} state - The Redux state.
92
+ * @param {Props} ownProps - The own props of the component.
93
+ * @returns {Props}
94
+ */
95
+export function _mapStateToProps(state: Object, ownProps: Props): $Shape<Props> {
96
+    return {
97
+        _participant: getParticipantById(state, ownProps.participantID)
98
+    };
99
+}
100
+
101
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageButton));

+ 1
- 0
react/features/chat/components/index.native.js View File

@@ -1,3 +1,4 @@
1 1
 // @flow
2 2
 
3 3
 export * from './native';
4
+export { default as PrivateMessageButton } from './PrivateMessageButton';

+ 2
- 0
react/features/chat/components/index.web.js View File

@@ -1,3 +1,5 @@
1 1
 // @flow
2 2
 
3 3
 export * from './web';
4
+
5
+export { default as PrivateMessageButton } from './PrivateMessageButton';

+ 2
- 0
react/features/chat/components/native/Chat.js View File

@@ -16,6 +16,7 @@ import AbstractChat, {
16 16
 
17 17
 import ChatInputBar from './ChatInputBar';
18 18
 import MessageContainer from './MessageContainer';
19
+import MessageRecipient from './MessageRecipient';
19 20
 import styles from './styles';
20 21
 
21 22
 /**
@@ -53,6 +54,7 @@ class Chat extends AbstractChat<Props> {
53 54
                         onPressBack = { this._onClose } />
54 55
                     <SafeAreaView style = { styles.backdrop }>
55 56
                         <MessageContainer messages = { this.props._messages } />
57
+                        <MessageRecipient />
56 58
                         <ChatInputBar onSend = { this.props._onSendMessage } />
57 59
                     </SafeAreaView>
58 60
                 </KeyboardAvoidingView>

+ 36
- 8
react/features/chat/components/native/ChatMessage.js View File

@@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
10 10
 import { replaceNonUnicodeEmojis } from '../../functions';
11 11
 
12 12
 import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
13
+import PrivateMessageButton from '../PrivateMessageButton';
13 14
 
14 15
 import styles from './styles';
15 16
 
@@ -57,14 +58,26 @@ class ChatMessage extends AbstractChatMessage<Props> {
57 58
             <View style = { styles.messageWrapper } >
58 59
                 { this._renderAvatar() }
59 60
                 <View style = { detailsWrapperStyle }>
60
-                    <View style = { textWrapperStyle } >
61
-                        {
62
-                            this.props.showDisplayName
63
-                                && this._renderDisplayName()
64
-                        }
65
-                        <Linkify linkStyle = { styles.chatLink }>
66
-                            { replaceNonUnicodeEmojis(messageText) }
67
-                        </Linkify>
61
+                    <View style = { styles.replyWrapper }>
62
+                        <View style = { textWrapperStyle } >
63
+                            {
64
+                                this.props.showDisplayName
65
+                                    && this._renderDisplayName()
66
+                            }
67
+                            <Linkify linkStyle = { styles.chatLink }>
68
+                                { replaceNonUnicodeEmojis(messageText) }
69
+                            </Linkify>
70
+                            {
71
+                                message.privateMessage
72
+                                    && this._renderPrivateNotice()
73
+                            }
74
+                        </View>
75
+                        { message.privateMessage && !localMessage
76
+                            && <PrivateMessageButton
77
+                                participantID = { message.id }
78
+                                reply = { true }
79
+                                showLabel = { false }
80
+                                toggledStyles = { styles.replyStyles } /> }
68 81
                     </View>
69 82
                     { this.props.showTimestamp && this._renderTimestamp() }
70 83
                 </View>
@@ -74,6 +87,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
74 87
 
75 88
     _getFormattedTimestamp: () => string;
76 89
 
90
+    _getPrivateNoticeMessage: () => string;
91
+
77 92
     /**
78 93
      * Renders the avatar of the sender.
79 94
      *
@@ -106,6 +121,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
106 121
         );
107 122
     }
108 123
 
124
+    /**
125
+     * Renders the message privacy notice.
126
+     *
127
+     * @returns {React$Element<*>}
128
+     */
129
+    _renderPrivateNotice() {
130
+        return (
131
+            <Text style = { styles.privateNotice }>
132
+                { this._getPrivateNoticeMessage() }
133
+            </Text>
134
+        );
135
+    }
136
+
109 137
     /**
110 138
      * Renders the time at which the message was sent.
111 139
      *

+ 37
- 0
react/features/chat/components/native/ChatPrivacyDialog.js View File

@@ -0,0 +1,37 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { ConfirmDialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+
9
+import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
10
+
11
+/**
12
+ * Implements a component for the dialog displayed to avoid mis-sending private messages.
13
+ */
14
+class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
15
+    /**
16
+     * Implements React's {@link Component#render()}.
17
+     *
18
+     * @inheritdoc
19
+     * @returns {ReactElement}
20
+     */
21
+    render() {
22
+        return (
23
+            <ConfirmDialog
24
+                cancelKey = 'dialog.sendPrivateMessageCancel'
25
+                contentKey = 'dialog.sendPrivateMessage'
26
+                okKey = 'dialog.sendPrivateMessageOk'
27
+                onCancel = { this._onSendGroupMessage }
28
+                onSubmit = { this._onSendPrivateMessage } />
29
+        );
30
+    }
31
+
32
+    _onSendGroupMessage: () => boolean;
33
+
34
+    _onSendPrivateMessage: () => boolean;
35
+}
36
+
37
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

+ 52
- 0
react/features/chat/components/native/MessageRecipient.js View File

@@ -0,0 +1,52 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import { Text, TouchableHighlight, View } from 'react-native';
5
+
6
+import { translate } from '../../../base/i18n';
7
+import { Icon, IconCancelSelection } from '../../../base/icons';
8
+import { connect } from '../../../base/redux';
9
+
10
+import AbstractMessageRecipient, {
11
+    _mapDispatchToProps,
12
+    _mapStateToProps
13
+} from '../AbstractMessageRecipient';
14
+
15
+import styles from './styles';
16
+
17
+/**
18
+ * Class to implement the displaying of the recipient of the next message.
19
+ */
20
+class MessageRecipient extends AbstractMessageRecipient {
21
+    /**
22
+     * Implements {@code PureComponent#render}.
23
+     *
24
+     * @inheritdoc
25
+     */
26
+    render() {
27
+        const { _privateMessageRecipient } = this.props;
28
+
29
+        if (!_privateMessageRecipient) {
30
+            return null;
31
+        }
32
+
33
+        const { t } = this.props;
34
+
35
+        return (
36
+            <View style = { styles.messageRecipientContainer }>
37
+                <Text style = { styles.messageRecipientText }>
38
+                    { t('chat.messageTo', {
39
+                        recipient: _privateMessageRecipient
40
+                    }) }
41
+                </Text>
42
+                <TouchableHighlight onPress = { this.props._onRemovePrivateMessageRecipient }>
43
+                    <Icon
44
+                        src = { IconCancelSelection }
45
+                        style = { styles.messageRecipientCancelIcon } />
46
+                </TouchableHighlight>
47
+            </View>
48
+        );
49
+    }
50
+}
51
+
52
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

+ 1
- 0
react/features/chat/components/native/index.js View File

@@ -2,3 +2,4 @@
2 2
 
3 3
 export { default as Chat } from './Chat';
4 4
 export { default as ChatButton } from './ChatButton';
5
+export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

+ 36
- 0
react/features/chat/components/native/styles.js View File

@@ -80,6 +80,23 @@ export default {
80 80
         flex: 1
81 81
     },
82 82
 
83
+    messageRecipientCancelIcon: {
84
+        color: ColorPalette.white,
85
+        fontSize: 18
86
+    },
87
+
88
+    messageRecipientContainer: {
89
+        alignItems: 'center',
90
+        backgroundColor: ColorPalette.warning,
91
+        flexDirection: 'row',
92
+        padding: BoxModel.padding
93
+    },
94
+
95
+    messageRecipientText: {
96
+        color: ColorPalette.white,
97
+        flex: 1
98
+    },
99
+
83 100
     /**
84 101
      * The message text itself.
85 102
      */
@@ -115,6 +132,25 @@ export default {
115 132
         borderTopRightRadius: 0
116 133
     },
117 134
 
135
+    replyWrapper: {
136
+        alignItems: 'center',
137
+        flexDirection: 'row'
138
+    },
139
+
140
+    replyStyles: {
141
+        iconStyle: {
142
+            color: 'rgb(118, 136, 152)',
143
+            fontSize: 22,
144
+            margin: BoxModel.margin / 2
145
+        }
146
+    },
147
+
148
+    privateNotice: {
149
+        color: ColorPalette.warning,
150
+        fontSize: 13,
151
+        fontStyle: 'italic'
152
+    },
153
+
118 154
     sendButtonIcon: {
119 155
         color: ColorPalette.darkGrey,
120 156
         fontSize: 22

+ 5
- 1
react/features/chat/components/web/Chat.js View File

@@ -14,6 +14,7 @@ import AbstractChat, {
14 14
 import ChatInput from './ChatInput';
15 15
 import DisplayNameForm from './DisplayNameForm';
16 16
 import MessageContainer from './MessageContainer';
17
+import MessageRecipient from './MessageRecipient';
17 18
 
18 19
 /**
19 20
  * React Component for holding the chat feature in a side panel that slides in
@@ -116,7 +117,10 @@ class Chat extends AbstractChat<Props> {
116 117
                 <MessageContainer
117 118
                     messages = { this.props._messages }
118 119
                     ref = { this._messageContainerRef } />
119
-                <ChatInput onResize = { this._onChatInputResize } />
120
+                <MessageRecipient />
121
+                <ChatInput
122
+                    onResize = { this._onChatInputResize }
123
+                    onSend = { this.props._onSendMessage } />
120 124
             </>
121 125
         );
122 126
     }

+ 6
- 3
react/features/chat/components/web/ChatInput.js View File

@@ -8,8 +8,6 @@ import type { Dispatch } from 'redux';
8 8
 import { translate } from '../../../base/i18n';
9 9
 import { connect } from '../../../base/redux';
10 10
 
11
-import { sendMessage } from '../../actions';
12
-
13 11
 import SmileysPanel from './SmileysPanel';
14 12
 
15 13
 /**
@@ -28,6 +26,11 @@ type Props = {
28 26
      */
29 27
     onResize: ?Function,
30 28
 
29
+    /**
30
+     * Callback to invoke on message send.
31
+     */
32
+    onSend: Function,
33
+
31 34
     /**
32 35
      * Invoked to obtain translated strings.
33 36
      */
@@ -163,7 +166,7 @@ class ChatInput extends Component<Props, State> {
163 166
             const trimmed = this.state.message.trim();
164 167
 
165 168
             if (trimmed) {
166
-                this.props.dispatch(sendMessage(trimmed));
169
+                this.props.onSend(trimmed);
167 170
 
168 171
                 this.setState({ message: '' });
169 172
             }

+ 28
- 4
react/features/chat/components/web/ChatMessage.js View File

@@ -10,6 +10,7 @@ import { Linkify } from '../../../base/react';
10 10
 import AbstractChatMessage, {
11 11
     type Props
12 12
 } from '../AbstractChatMessage';
13
+import PrivateMessageButton from '../PrivateMessageButton';
13 14
 
14 15
 /**
15 16
  * Renders a single chat message.
@@ -45,11 +46,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
45 46
 
46 47
         return (
47 48
             <div className = 'chatmessage-wrapper'>
48
-                <div className = 'chatmessage'>
49
-                    { this.props.showDisplayName && this._renderDisplayName() }
50
-                    <div className = 'usermessage'>
51
-                        { processedMessage }
49
+                <div className = 'replywrapper'>
50
+                    <div className = 'chatmessage'>
51
+                        { this.props.showDisplayName && this._renderDisplayName() }
52
+                        <div className = 'usermessage'>
53
+                            { processedMessage }
54
+                        </div>
55
+                        { message.privateMessage && this._renderPrivateNotice() }
52 56
                     </div>
57
+                    { message.privateMessage && message.messageType !== 'local'
58
+                    && <PrivateMessageButton
59
+                        participantID = { message.id }
60
+                        reply = { true }
61
+                        showLabel = { false } /> }
53 62
                 </div>
54 63
                 { this.props.showTimestamp && this._renderTimestamp() }
55 64
             </div>
@@ -58,6 +67,8 @@ class ChatMessage extends AbstractChatMessage<Props> {
58 67
 
59 68
     _getFormattedTimestamp: () => string;
60 69
 
70
+    _getPrivateNoticeMessage: () => string;
71
+
61 72
     /**
62 73
      * Renders the display name of the sender.
63 74
      *
@@ -71,6 +82,19 @@ class ChatMessage extends AbstractChatMessage<Props> {
71 82
         );
72 83
     }
73 84
 
85
+    /**
86
+     * Renders the message privacy notice.
87
+     *
88
+     * @returns {React$Element<*>}
89
+     */
90
+    _renderPrivateNotice() {
91
+        return (
92
+            <div className = 'privatemessagenotice'>
93
+                { this._getPrivateNoticeMessage() }
94
+            </div>
95
+        );
96
+    }
97
+
74 98
     /**
75 99
      * Renders the time at which the message was sent.
76 100
      *

+ 42
- 0
react/features/chat/components/web/ChatPrivacyDialog.js View File

@@ -0,0 +1,42 @@
1
+/* @flow */
2
+
3
+import React from 'react';
4
+
5
+import { Dialog } from '../../../base/dialog';
6
+import { translate } from '../../../base/i18n';
7
+import { connect } from '../../../base/redux';
8
+
9
+import { AbstractChatPrivacyDialog, _mapDispatchToProps, _mapStateToProps } from '../AbstractChatPrivacyDialog';
10
+
11
+/**
12
+ * Implements a component for the dialog displayed to avoid mis-sending private messages.
13
+ */
14
+class ChatPrivacyDialog extends AbstractChatPrivacyDialog {
15
+    /**
16
+     * Implements React's {@link Component#render()}.
17
+     *
18
+     * @inheritdoc
19
+     * @returns {ReactElement}
20
+     */
21
+    render() {
22
+        return (
23
+            <Dialog
24
+                cancelKey = 'dialog.sendPrivateMessageCancel'
25
+                okKey = 'dialog.sendPrivateMessageOk'
26
+                onCancel = { this._onSendGroupMessage }
27
+                onSubmit = { this._onSendPrivateMessage }
28
+                titleKey = 'dialog.sendPrivateMessageTitle'
29
+                width = 'small'>
30
+                <div>
31
+                    { this.props.t('dialog.sendPrivateMessage') }
32
+                </div>
33
+            </Dialog>
34
+        );
35
+    }
36
+
37
+    _onSendGroupMessage: () => boolean;
38
+
39
+    _onSendPrivateMessage: () => boolean;
40
+}
41
+
42
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(ChatPrivacyDialog));

+ 48
- 0
react/features/chat/components/web/MessageRecipient.js View File

@@ -0,0 +1,48 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { Icon, IconCancelSelection } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+
9
+import AbstractMessageRecipient, {
10
+    _mapDispatchToProps,
11
+    _mapStateToProps
12
+} from '../AbstractMessageRecipient';
13
+
14
+/**
15
+ * Class to implement the displaying of the recipient of the next message.
16
+ */
17
+class MessageRecipient extends AbstractMessageRecipient {
18
+    /**
19
+     * Implements {@code PureComponent#render}.
20
+     *
21
+     * @inheritdoc
22
+     */
23
+    render() {
24
+        const { _privateMessageRecipient } = this.props;
25
+
26
+        if (!_privateMessageRecipient) {
27
+            return null;
28
+        }
29
+
30
+        const { t } = this.props;
31
+
32
+        return (
33
+            <div id = 'chat-recipient'>
34
+                <span>
35
+                    { t('chat.messageTo', {
36
+                        recipient: _privateMessageRecipient
37
+                    }) }
38
+                </span>
39
+                <div onClick = { this.props._onRemovePrivateMessageRecipient }>
40
+                    <Icon
41
+                        src = { IconCancelSelection } />
42
+                </div>
43
+            </div>
44
+        );
45
+    }
46
+}
47
+
48
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(MessageRecipient));

+ 1
- 0
react/features/chat/components/web/index.js View File

@@ -2,3 +2,4 @@
2 2
 
3 3
 export { default as Chat } from './Chat';
4 4
 export { default as ChatCounter } from './ChatCounter';
5
+export { default as ChatPrivacyDialog } from './ChatPrivacyDialog';

+ 220
- 41
react/features/chat/middleware.js View File

@@ -5,8 +5,10 @@ import {
5 5
     CONFERENCE_JOINED,
6 6
     getCurrentConference
7 7
 } from '../base/conference';
8
+import { openDialog } from '../base/dialog';
8 9
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
9 10
 import {
11
+    getLocalParticipant,
10 12
     getParticipantById,
11 13
     getParticipantDisplayName
12 14
 } from '../base/participants';
@@ -14,14 +16,23 @@ import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
14 16
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
15 17
 import { isButtonEnabled, showToolbox } from '../toolbox';
16 18
 
17
-import { SEND_MESSAGE } from './actionTypes';
19
+import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
18 20
 import { addMessage, clearMessages, toggleChat } from './actions';
21
+import { ChatPrivacyDialog } from './components';
19 22
 import { INCOMING_MSG_SOUND_ID } from './constants';
20 23
 import { INCOMING_MSG_SOUND_FILE } from './sounds';
21 24
 
22 25
 declare var APP: Object;
23 26
 declare var interfaceConfig : Object;
24 27
 
28
+/**
29
+ * Timeout for when to show the privacy notice after a private message was received.
30
+ *
31
+ * E.g. if this value is 20 secs (20000ms), then we show the privacy notice when sending a non private
32
+ * message after we have received a private message in the last 20 seconds.
33
+ */
34
+const PRIVACY_NOTICE_TIMEOUT = 20 * 1000;
35
+
25 36
 /**
26 37
  * Implements the middleware of the chat feature.
27 38
  *
@@ -29,14 +40,16 @@ declare var interfaceConfig : Object;
29 40
  * @returns {Function}
30 41
  */
31 42
 MiddlewareRegistry.register(store => next => action => {
43
+    const { dispatch } = store;
44
+
32 45
     switch (action.type) {
33 46
     case APP_WILL_MOUNT:
34
-        store.dispatch(
47
+        dispatch(
35 48
                 registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
36 49
         break;
37 50
 
38 51
     case APP_WILL_UNMOUNT:
39
-        store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
52
+        dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
40 53
         break;
41 54
 
42 55
     case CONFERENCE_JOINED:
@@ -44,16 +57,43 @@ MiddlewareRegistry.register(store => next => action => {
44 57
         break;
45 58
 
46 59
     case SEND_MESSAGE: {
47
-        const { conference } = store.getState()['features/base/conference'];
60
+        const state = store.getState();
61
+        const { conference } = state['features/base/conference'];
48 62
 
49 63
         if (conference) {
50
-            if (typeof APP !== 'undefined') {
51
-                APP.API.notifySendingChatMessage(action.message);
64
+            // There may be cases when we intend to send a private message but we forget to set the
65
+            // recipient. This logic tries to mitigate this risk.
66
+            const shouldSendPrivateMessageTo = _shouldSendPrivateMessageTo(state, action);
67
+
68
+            if (shouldSendPrivateMessageTo) {
69
+                dispatch(openDialog(ChatPrivacyDialog, {
70
+                    message: action.message,
71
+                    participantID: shouldSendPrivateMessageTo
72
+                }));
73
+            } else {
74
+                // Sending the message if privacy notice doesn't need to be shown.
75
+
76
+                if (typeof APP !== 'undefined') {
77
+                    APP.API.notifySendingChatMessage(action.message);
78
+                }
79
+
80
+                const { privateMessageRecipient } = state['features/chat'];
81
+
82
+                if (privateMessageRecipient) {
83
+                    conference.sendPrivateTextMessage(privateMessageRecipient.id, action.message);
84
+                    _persistSentPrivateMessage(store, privateMessageRecipient.id, action.message);
85
+                } else {
86
+                    conference.sendTextMessage(action.message);
87
+                }
52 88
             }
53
-            conference.sendTextMessage(action.message);
54 89
         }
55 90
         break;
56 91
     }
92
+
93
+    case SET_PRIVATE_MESSAGE_RECIPIENT: {
94
+        _maybeFocusField();
95
+        break;
96
+    }
57 97
     }
58 98
 
59 99
     return next(action);
@@ -112,44 +152,183 @@ function _addChatMsgListener(conference, { dispatch, getState }) {
112 152
     conference.on(
113 153
         JitsiConferenceEvents.MESSAGE_RECEIVED,
114 154
         (id, message, timestamp, nick) => {
115
-            // Logic for all platforms:
116
-            const state = getState();
117
-            const { isOpen: isChatOpen } = state['features/chat'];
118
-
119
-            if (!isChatOpen) {
120
-                dispatch(playSound(INCOMING_MSG_SOUND_ID));
121
-            }
155
+            _handleReceivedMessage({
156
+                dispatch,
157
+                getState
158
+            }, {
159
+                id,
160
+                message,
161
+                nick,
162
+                privateMessage: false,
163
+                timestamp
164
+            });
165
+        }
166
+    );
122 167
 
123
-            // Provide a default for for the case when a message is being
124
-            // backfilled for a participant that has left the conference.
125
-            const participant = getParticipantById(state, id) || {};
126
-            const displayName = participant.name || nick || getParticipantDisplayName(state, id);
127
-            const hasRead = participant.local || isChatOpen;
128
-            const timestampToDate = timestamp
129
-                ? new Date(timestamp) : new Date();
130
-            const millisecondsTimestamp = timestampToDate.getTime();
131
-
132
-            dispatch(addMessage({
133
-                displayName,
134
-                hasRead,
168
+    conference.on(
169
+        JitsiConferenceEvents.PRIVATE_MESSAGE_RECEIVED,
170
+        (id, message, timestamp) => {
171
+            _handleReceivedMessage({
172
+                dispatch,
173
+                getState
174
+            }, {
135 175
                 id,
136
-                messageType: participant.local ? 'local' : 'remote',
137 176
                 message,
138
-                timestamp: millisecondsTimestamp
139
-            }));
177
+                privateMessage: true,
178
+                timestamp,
179
+                nick: undefined
180
+            });
181
+        }
182
+    );
183
+}
184
+
185
+/**
186
+ * Function to handle an incoming chat message.
187
+ *
188
+ * @param {Store} store - The Redux store.
189
+ * @param {Object} message - The message object.
190
+ * @returns {void}
191
+ */
192
+function _handleReceivedMessage({ dispatch, getState }, { id, message, nick, privateMessage, timestamp }) {
193
+    // Logic for all platforms:
194
+    const state = getState();
195
+    const { isOpen: isChatOpen } = state['features/chat'];
140 196
 
141
-            if (typeof APP !== 'undefined') {
142
-                // Logic for web only:
197
+    if (!isChatOpen) {
198
+        dispatch(playSound(INCOMING_MSG_SOUND_ID));
199
+    }
143 200
 
144
-                APP.API.notifyReceivedChatMessage({
145
-                    body: message,
146
-                    id,
147
-                    nick: displayName,
148
-                    ts: timestamp
149
-                });
201
+    // Provide a default for for the case when a message is being
202
+    // backfilled for a participant that has left the conference.
203
+    const participant = getParticipantById(state, id) || {};
204
+    const localParticipant = getLocalParticipant(getState);
205
+    const displayName = participant.name || nick || getParticipantDisplayName(state, id);
206
+    const hasRead = participant.local || isChatOpen;
207
+    const timestampToDate = timestamp
208
+        ? new Date(timestamp) : new Date();
209
+    const millisecondsTimestamp = timestampToDate.getTime();
150 210
 
151
-                dispatch(showToolbox(4000));
152
-            }
153
-        }
154
-    );
211
+    dispatch(addMessage({
212
+        displayName,
213
+        hasRead,
214
+        id,
215
+        messageType: participant.local ? 'local' : 'remote',
216
+        message,
217
+        privateMessage,
218
+        recipient: getParticipantDisplayName(state, localParticipant.id),
219
+        timestamp: millisecondsTimestamp
220
+    }));
221
+
222
+    if (typeof APP !== 'undefined') {
223
+        // Logic for web only:
224
+
225
+        APP.API.notifyReceivedChatMessage({
226
+            body: message,
227
+            id,
228
+            nick: displayName,
229
+            ts: timestamp
230
+        });
231
+
232
+        dispatch(showToolbox(4000));
233
+    }
234
+}
235
+
236
+/**
237
+ * Focuses the chat text field on web after the message recipient was updated, if needed.
238
+ *
239
+ * @returns {void}
240
+ */
241
+function _maybeFocusField() {
242
+    if (navigator.product !== 'ReactNative') {
243
+        const textField = document.getElementById('usermsg');
244
+
245
+        textField && textField.focus();
246
+    }
247
+}
248
+
249
+/**
250
+ * Persists the sent private messages as if they were received over the muc.
251
+ *
252
+ * This is required as we rely on the fact that we receive all messages from the muc that we send
253
+ * (as they are sent to everybody), but we don't receive the private messages we send to another participant.
254
+ * But those messages should be in the store as well, otherwise they don't appear in the chat window.
255
+ *
256
+ * @param {Store} store - The Redux store.
257
+ * @param {string} recipientID - The ID of the recipient the private message was sent to.
258
+ * @param {string} message - The sent message.
259
+ * @returns {void}
260
+ */
261
+function _persistSentPrivateMessage({ dispatch, getState }, recipientID, message) {
262
+    const localParticipant = getLocalParticipant(getState);
263
+    const displayName = getParticipantDisplayName(getState, localParticipant.id);
264
+
265
+    dispatch(addMessage({
266
+        displayName,
267
+        hasRead: true,
268
+        id: localParticipant.id,
269
+        messageType: 'local',
270
+        message,
271
+        privateMessage: true,
272
+        recipient: getParticipantDisplayName(getState, recipientID),
273
+        timestamp: Date.now()
274
+    }));
275
+}
276
+
277
+/**
278
+ * Returns the ID of the participant who we may have wanted to send the message
279
+ * that we're about to send.
280
+ *
281
+ * @param {Object} state - The Redux state.
282
+ * @param {Object} action - The action being dispatched now.
283
+ * @returns {string?}
284
+ */
285
+function _shouldSendPrivateMessageTo(state, action): ?string {
286
+    if (action.ignorePrivacy) {
287
+        // Shortcut: this is only true, if we already displayed the notice, so no need to show it again.
288
+        return undefined;
289
+    }
290
+
291
+    const { messages, privateMessageRecipient } = state['features/chat'];
292
+
293
+    if (privateMessageRecipient) {
294
+        // We're already sending a private message, no need to warn about privacy.
295
+        return undefined;
296
+    }
297
+
298
+    if (!messages.length) {
299
+        // No messages yet, no need to warn for privacy.
300
+        return undefined;
301
+    }
302
+
303
+    // Platforms sort messages differently
304
+    const lastMessage = navigator.product === 'ReactNative'
305
+        ? messages[0] : messages[messages.length - 1];
306
+
307
+    if (lastMessage.messageType === 'local') {
308
+        // The sender is probably aware of any private messages as already sent
309
+        // a message since then. Doesn't make sense to display the notice now.
310
+        return undefined;
311
+    }
312
+
313
+    if (lastMessage.privateMessage) {
314
+        // We show the notice if the last received message was private.
315
+        return lastMessage.id;
316
+    }
317
+
318
+    // But messages may come rapidly, we want to protect our users from mis-sending a message
319
+    // even when there was a reasonable recently received private message.
320
+    const now = Date.now();
321
+    const recentPrivateMessages = messages.filter(
322
+        message =>
323
+            message.messageType !== 'local'
324
+            && message.privateMessage
325
+            && message.timestamp + PRIVACY_NOTICE_TIMEOUT > now);
326
+    const recentPrivateMessage = navigator.product === 'ReactNative'
327
+        ? recentPrivateMessages[0] : recentPrivateMessages[recentPrivateMessages.length - 1];
328
+
329
+    if (recentPrivateMessage) {
330
+        return recentPrivateMessage.id;
331
+    }
332
+
333
+    return undefined;
155 334
 }

+ 19
- 3
react/features/chat/reducer.js View File

@@ -2,12 +2,18 @@
2 2
 
3 3
 import { ReducerRegistry } from '../base/redux';
4 4
 
5
-import { ADD_MESSAGE, CLEAR_MESSAGES, TOGGLE_CHAT } from './actionTypes';
5
+import {
6
+    ADD_MESSAGE,
7
+    CLEAR_MESSAGES,
8
+    SET_PRIVATE_MESSAGE_RECIPIENT,
9
+    TOGGLE_CHAT
10
+} from './actionTypes';
6 11
 
7 12
 const DEFAULT_STATE = {
8 13
     isOpen: false,
9 14
     lastReadMessage: undefined,
10
-    messages: []
15
+    messages: [],
16
+    privateMessageRecipient: undefined
11 17
 };
12 18
 
13 19
 ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
@@ -19,6 +25,8 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
19 25
             id: action.id,
20 26
             messageType: action.messageType,
21 27
             message: action.message,
28
+            privateMessage: action.privateMessage,
29
+            recipient: action.recipient,
22 30
             timestamp: action.timestamp
23 31
         };
24 32
 
@@ -48,12 +56,20 @@ ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
48 56
             messages: []
49 57
         };
50 58
 
59
+    case SET_PRIVATE_MESSAGE_RECIPIENT:
60
+        return {
61
+            ...state,
62
+            isOpen: Boolean(action.participant) || state.isOpen,
63
+            privateMessageRecipient: action.participant
64
+        };
65
+
51 66
     case TOGGLE_CHAT:
52 67
         return {
53 68
             ...state,
54 69
             isOpen: !state.isOpen,
55 70
             lastReadMessage: state.messages[
56
-                navigator.product === 'ReactNative' ? 0 : state.messages.length - 1]
71
+                navigator.product === 'ReactNative' ? 0 : state.messages.length - 1],
72
+            privateMessageRecipient: state.isOpen ? undefined : state.privateMessageRecipient
57 73
         };
58 74
     }
59 75
 

+ 2
- 0
react/features/remote-video-menu/components/native/RemoteVideoMenu.js View File

@@ -9,6 +9,7 @@ import { BottomSheet, isDialogOpen } from '../../../base/dialog';
9 9
 import { getParticipantDisplayName } from '../../../base/participants';
10 10
 import { connect } from '../../../base/redux';
11 11
 import { StyleType } from '../../../base/styles';
12
+import { PrivateMessageButton } from '../../../chat';
12 13
 
13 14
 import { hideRemoteVideoMenu } from '../../actions';
14 15
 
@@ -95,6 +96,7 @@ class RemoteVideoMenu extends Component<Props> {
95 96
                 <MuteButton { ...buttonProps } />
96 97
                 <KickButton { ...buttonProps } />
97 98
                 <PinButton { ...buttonProps } />
99
+                <PrivateMessageButton { ...buttonProps } />
98 100
             </BottomSheet>
99 101
         );
100 102
     }

+ 62
- 0
react/features/remote-video-menu/components/web/PrivateMessageMenuButton.js View File

@@ -0,0 +1,62 @@
1
+// @flow
2
+
3
+import React, { Component } from 'react';
4
+
5
+import { translate } from '../../../base/i18n';
6
+import { IconMessage } from '../../../base/icons';
7
+import { connect } from '../../../base/redux';
8
+import { _mapDispatchToProps, _mapStateToProps, type Props } from '../../../chat/components/PrivateMessageButton';
9
+
10
+import RemoteVideoMenuButton from './RemoteVideoMenuButton';
11
+
12
+/**
13
+ * A custom implementation of the PrivateMessageButton specialized for
14
+ * the web version of the remote video menu. When the web platform starts to use
15
+ * the {@code AbstractButton} component for the remote video menu, we can get rid
16
+ * of this component and use the generic button in the chat feature.
17
+ */
18
+class PrivateMessageMenuButton extends Component<Props> {
19
+    /**
20
+     * Instantiates a new Component instance.
21
+     *
22
+     * @inheritdoc
23
+     */
24
+    constructor(props: Props) {
25
+        super(props);
26
+
27
+        this._onClick = this._onClick.bind(this);
28
+    }
29
+
30
+    /**
31
+     * Implements React's {@link Component#render()}.
32
+     *
33
+     * @inheritdoc
34
+     * @returns {ReactElement}
35
+     */
36
+    render() {
37
+        const { participantID, t } = this.props;
38
+
39
+        return (
40
+            <RemoteVideoMenuButton
41
+                buttonText = { t('toolbar.privateMessage') }
42
+                icon = { IconMessage }
43
+                id = { `privmsglink_${participantID}` }
44
+                onClick = { this._onClick } />
45
+        );
46
+    }
47
+
48
+    _onClick: () => void;
49
+
50
+    /**
51
+     * Callback to be invoked on pressing the button.
52
+     *
53
+     * @returns {void}
54
+     */
55
+    _onClick() {
56
+        const { _participant, _setPrivateMessageRecipient } = this.props;
57
+
58
+        _setPrivateMessageRecipient(_participant);
59
+    }
60
+}
61
+
62
+export default translate(connect(_mapStateToProps, _mapDispatchToProps)(PrivateMessageMenuButton));

+ 7
- 0
react/features/remote-video-menu/components/web/RemoteVideoMenuTriggerButton.js View File

@@ -8,6 +8,7 @@ import { Popover } from '../../../base/popover';
8 8
 import {
9 9
     MuteButton,
10 10
     KickButton,
11
+    PrivateMessageMenuButton,
11 12
     RemoteControlButton,
12 13
     RemoteVideoMenu,
13 14
     VolumeSlider
@@ -188,6 +189,12 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
188 189
             );
189 190
         }
190 191
 
192
+        buttons.push(
193
+            <PrivateMessageMenuButton
194
+                key = 'privateMessage'
195
+                participantID = { participantID } />
196
+        );
197
+
191 198
         if (onVolumeChange) {
192 199
             buttons.push(
193 200
                 <VolumeSlider

+ 1
- 0
react/features/remote-video-menu/components/web/index.js View File

@@ -8,6 +8,7 @@ export { default as MuteButton } from './MuteButton';
8 8
 export {
9 9
     default as MuteRemoteParticipantDialog
10 10
 } from './MuteRemoteParticipantDialog';
11
+export { default as PrivateMessageMenuButton } from './PrivateMessageMenuButton';
11 12
 export {
12 13
     REMOTE_CONTROL_MENU_STATES,
13 14
     default as RemoteControlButton

Loading…
Cancel
Save