浏览代码

feat: private messages

master
Bettenbuk Zoltan 6 年前
父节点
当前提交
42271b1b89
共有 34 个文件被更改,包括 987 次插入63 次删除
  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 查看文件

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
 .chat-header {
104
 .chat-header {
84
     background-color: $chatHeaderBackgroundColor;
105
     background-color: $chatHeaderBackgroundColor;
85
     height: 70px;
106
     height: 70px;
196
             padding: 0;
217
             padding: 0;
197
         }
218
         }
198
     }
219
     }
220
+
221
+    .privatemessagenotice {
222
+        color: $defaultWarningColor;
223
+        font-style: italic;
224
+    }
199
 }
225
 }
200
 
226
 
201
 .smiley {
227
 .smiley {
228
 .smileys-panel {
254
 .smileys-panel {
229
     bottom: 100%;
255
     bottom: 100%;
230
     box-sizing: border-box;
256
     box-sizing: border-box;
257
+    background-color: rgba(0, 0, 0, .6) !important;
231
     height: auto;
258
     height: auto;
232
     max-height: 0;
259
     max-height: 0;
233
     overflow: hidden;
260
     overflow: hidden;
312
 
339
 
313
     .chatmessage-wrapper {
340
     .chatmessage-wrapper {
314
         max-width: 100%;
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
     .chatmessage {
354
     .chatmessage {

+ 2
- 1
css/_popup_menu.scss 查看文件

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

+ 1
- 0
css/_variables.scss 查看文件

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

+ 10
- 1
lang/main.json 查看文件

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

+ 2
- 0
react/features/base/icons/svg/index.js 查看文件

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

+ 1
- 0
react/features/base/icons/svg/message.svg 查看文件

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 查看文件

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 查看文件

28
     overflowMenuItemUnderlay: '#EEEEEE',
28
     overflowMenuItemUnderlay: '#EEEEEE',
29
     red: '#D00000',
29
     red: '#D00000',
30
     transparent: 'rgba(0, 0, 0, 0)',
30
     transparent: 'rgba(0, 0, 0, 0)',
31
+    warning: 'rgb(215, 121, 118)',
31
     white: '#FFFFFF',
32
     white: '#FFFFFF',
32
 
33
 
33
     /**
34
     /**

+ 12
- 0
react/features/chat/actionTypes.js 查看文件

30
  *
30
  *
31
  * {
31
  * {
32
  *     type: SEND_MESSAGE,
32
  *     type: SEND_MESSAGE,
33
+ *     ignorePrivacy: boolean,
33
  *     message: string
34
  *     message: string
34
  * }
35
  * }
35
  */
36
  */
36
 export const SEND_MESSAGE = 'SEND_MESSAGE';
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
  * The type of the action which signals to toggle the display of the chat panel.
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 查看文件

4
     ADD_MESSAGE,
4
     ADD_MESSAGE,
5
     CLEAR_MESSAGES,
5
     CLEAR_MESSAGES,
6
     SEND_MESSAGE,
6
     SEND_MESSAGE,
7
+    SET_PRIVATE_MESSAGE_RECIPIENT,
7
     TOGGLE_CHAT
8
     TOGGLE_CHAT
8
 } from './actionTypes';
9
 } from './actionTypes';
9
 
10
 
53
  * Sends a chat message to everyone in the conference.
54
  * Sends a chat message to everyone in the conference.
54
  *
55
  *
55
  * @param {string} message - The chat message to send out.
56
  * @param {string} message - The chat message to send out.
57
+ * @param {boolean} ignorePrivacy - True if the privacy notification should be ignored.
56
  * @returns {{
58
  * @returns {{
57
  *     type: SEND_MESSAGE,
59
  *     type: SEND_MESSAGE,
60
+ *     ignorePrivacy: boolean,
58
  *     message: string
61
  *     message: string
59
  * }}
62
  * }}
60
  */
63
  */
61
-export function sendMessage(message: string) {
64
+export function sendMessage(message: string, ignorePrivacy: boolean = false) {
62
     return {
65
     return {
63
         type: SEND_MESSAGE,
66
         type: SEND_MESSAGE,
67
+        ignorePrivacy,
64
         message
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
  * Toggles display of the chat side panel.
89
  * Toggles display of the chat side panel.
70
  *
90
  *

+ 13
- 0
react/features/chat/components/AbstractChatMessage.js 查看文件

56
         return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
56
         return getLocalizedDateFormatter(new Date(this.props.message.timestamp))
57
             .format(TIMESTAMP_FORMAT);
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 查看文件

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 查看文件

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 查看文件

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 查看文件

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

+ 2
- 0
react/features/chat/components/index.web.js 查看文件

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

+ 2
- 0
react/features/chat/components/native/Chat.js 查看文件

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

+ 36
- 8
react/features/chat/components/native/ChatMessage.js 查看文件

10
 import { replaceNonUnicodeEmojis } from '../../functions';
10
 import { replaceNonUnicodeEmojis } from '../../functions';
11
 
11
 
12
 import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
12
 import AbstractChatMessage, { type Props } from '../AbstractChatMessage';
13
+import PrivateMessageButton from '../PrivateMessageButton';
13
 
14
 
14
 import styles from './styles';
15
 import styles from './styles';
15
 
16
 
57
             <View style = { styles.messageWrapper } >
58
             <View style = { styles.messageWrapper } >
58
                 { this._renderAvatar() }
59
                 { this._renderAvatar() }
59
                 <View style = { detailsWrapperStyle }>
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
                     </View>
81
                     </View>
69
                     { this.props.showTimestamp && this._renderTimestamp() }
82
                     { this.props.showTimestamp && this._renderTimestamp() }
70
                 </View>
83
                 </View>
74
 
87
 
75
     _getFormattedTimestamp: () => string;
88
     _getFormattedTimestamp: () => string;
76
 
89
 
90
+    _getPrivateNoticeMessage: () => string;
91
+
77
     /**
92
     /**
78
      * Renders the avatar of the sender.
93
      * Renders the avatar of the sender.
79
      *
94
      *
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
      * Renders the time at which the message was sent.
138
      * Renders the time at which the message was sent.
111
      *
139
      *

+ 37
- 0
react/features/chat/components/native/ChatPrivacyDialog.js 查看文件

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 查看文件

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 查看文件

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

+ 36
- 0
react/features/chat/components/native/styles.js 查看文件

80
         flex: 1
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
      * The message text itself.
101
      * The message text itself.
85
      */
102
      */
115
         borderTopRightRadius: 0
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
     sendButtonIcon: {
154
     sendButtonIcon: {
119
         color: ColorPalette.darkGrey,
155
         color: ColorPalette.darkGrey,
120
         fontSize: 22
156
         fontSize: 22

+ 5
- 1
react/features/chat/components/web/Chat.js 查看文件

14
 import ChatInput from './ChatInput';
14
 import ChatInput from './ChatInput';
15
 import DisplayNameForm from './DisplayNameForm';
15
 import DisplayNameForm from './DisplayNameForm';
16
 import MessageContainer from './MessageContainer';
16
 import MessageContainer from './MessageContainer';
17
+import MessageRecipient from './MessageRecipient';
17
 
18
 
18
 /**
19
 /**
19
  * React Component for holding the chat feature in a side panel that slides in
20
  * React Component for holding the chat feature in a side panel that slides in
116
                 <MessageContainer
117
                 <MessageContainer
117
                     messages = { this.props._messages }
118
                     messages = { this.props._messages }
118
                     ref = { this._messageContainerRef } />
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 查看文件

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

+ 28
- 4
react/features/chat/components/web/ChatMessage.js 查看文件

10
 import AbstractChatMessage, {
10
 import AbstractChatMessage, {
11
     type Props
11
     type Props
12
 } from '../AbstractChatMessage';
12
 } from '../AbstractChatMessage';
13
+import PrivateMessageButton from '../PrivateMessageButton';
13
 
14
 
14
 /**
15
 /**
15
  * Renders a single chat message.
16
  * Renders a single chat message.
45
 
46
 
46
         return (
47
         return (
47
             <div className = 'chatmessage-wrapper'>
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
                     </div>
56
                     </div>
57
+                    { message.privateMessage && message.messageType !== 'local'
58
+                    && <PrivateMessageButton
59
+                        participantID = { message.id }
60
+                        reply = { true }
61
+                        showLabel = { false } /> }
53
                 </div>
62
                 </div>
54
                 { this.props.showTimestamp && this._renderTimestamp() }
63
                 { this.props.showTimestamp && this._renderTimestamp() }
55
             </div>
64
             </div>
58
 
67
 
59
     _getFormattedTimestamp: () => string;
68
     _getFormattedTimestamp: () => string;
60
 
69
 
70
+    _getPrivateNoticeMessage: () => string;
71
+
61
     /**
72
     /**
62
      * Renders the display name of the sender.
73
      * Renders the display name of the sender.
63
      *
74
      *
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
      * Renders the time at which the message was sent.
99
      * Renders the time at which the message was sent.
76
      *
100
      *

+ 42
- 0
react/features/chat/components/web/ChatPrivacyDialog.js 查看文件

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 查看文件

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 查看文件

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

+ 220
- 41
react/features/chat/middleware.js 查看文件

5
     CONFERENCE_JOINED,
5
     CONFERENCE_JOINED,
6
     getCurrentConference
6
     getCurrentConference
7
 } from '../base/conference';
7
 } from '../base/conference';
8
+import { openDialog } from '../base/dialog';
8
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
9
 import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
9
 import {
10
 import {
11
+    getLocalParticipant,
10
     getParticipantById,
12
     getParticipantById,
11
     getParticipantDisplayName
13
     getParticipantDisplayName
12
 } from '../base/participants';
14
 } from '../base/participants';
14
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
16
 import { playSound, registerSound, unregisterSound } from '../base/sounds';
15
 import { isButtonEnabled, showToolbox } from '../toolbox';
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
 import { addMessage, clearMessages, toggleChat } from './actions';
20
 import { addMessage, clearMessages, toggleChat } from './actions';
21
+import { ChatPrivacyDialog } from './components';
19
 import { INCOMING_MSG_SOUND_ID } from './constants';
22
 import { INCOMING_MSG_SOUND_ID } from './constants';
20
 import { INCOMING_MSG_SOUND_FILE } from './sounds';
23
 import { INCOMING_MSG_SOUND_FILE } from './sounds';
21
 
24
 
22
 declare var APP: Object;
25
 declare var APP: Object;
23
 declare var interfaceConfig : Object;
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
  * Implements the middleware of the chat feature.
37
  * Implements the middleware of the chat feature.
27
  *
38
  *
29
  * @returns {Function}
40
  * @returns {Function}
30
  */
41
  */
31
 MiddlewareRegistry.register(store => next => action => {
42
 MiddlewareRegistry.register(store => next => action => {
43
+    const { dispatch } = store;
44
+
32
     switch (action.type) {
45
     switch (action.type) {
33
     case APP_WILL_MOUNT:
46
     case APP_WILL_MOUNT:
34
-        store.dispatch(
47
+        dispatch(
35
                 registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
48
                 registerSound(INCOMING_MSG_SOUND_ID, INCOMING_MSG_SOUND_FILE));
36
         break;
49
         break;
37
 
50
 
38
     case APP_WILL_UNMOUNT:
51
     case APP_WILL_UNMOUNT:
39
-        store.dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
52
+        dispatch(unregisterSound(INCOMING_MSG_SOUND_ID));
40
         break;
53
         break;
41
 
54
 
42
     case CONFERENCE_JOINED:
55
     case CONFERENCE_JOINED:
44
         break;
57
         break;
45
 
58
 
46
     case SEND_MESSAGE: {
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
         if (conference) {
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
         break;
90
         break;
56
     }
91
     }
92
+
93
+    case SET_PRIVATE_MESSAGE_RECIPIENT: {
94
+        _maybeFocusField();
95
+        break;
96
+    }
57
     }
97
     }
58
 
98
 
59
     return next(action);
99
     return next(action);
112
     conference.on(
152
     conference.on(
113
         JitsiConferenceEvents.MESSAGE_RECEIVED,
153
         JitsiConferenceEvents.MESSAGE_RECEIVED,
114
         (id, message, timestamp, nick) => {
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
                 id,
175
                 id,
136
-                messageType: participant.local ? 'local' : 'remote',
137
                 message,
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 查看文件

2
 
2
 
3
 import { ReducerRegistry } from '../base/redux';
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
 const DEFAULT_STATE = {
12
 const DEFAULT_STATE = {
8
     isOpen: false,
13
     isOpen: false,
9
     lastReadMessage: undefined,
14
     lastReadMessage: undefined,
10
-    messages: []
15
+    messages: [],
16
+    privateMessageRecipient: undefined
11
 };
17
 };
12
 
18
 
13
 ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
19
 ReducerRegistry.register('features/chat', (state = DEFAULT_STATE, action) => {
19
             id: action.id,
25
             id: action.id,
20
             messageType: action.messageType,
26
             messageType: action.messageType,
21
             message: action.message,
27
             message: action.message,
28
+            privateMessage: action.privateMessage,
29
+            recipient: action.recipient,
22
             timestamp: action.timestamp
30
             timestamp: action.timestamp
23
         };
31
         };
24
 
32
 
48
             messages: []
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
     case TOGGLE_CHAT:
66
     case TOGGLE_CHAT:
52
         return {
67
         return {
53
             ...state,
68
             ...state,
54
             isOpen: !state.isOpen,
69
             isOpen: !state.isOpen,
55
             lastReadMessage: state.messages[
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 查看文件

9
 import { getParticipantDisplayName } from '../../../base/participants';
9
 import { getParticipantDisplayName } from '../../../base/participants';
10
 import { connect } from '../../../base/redux';
10
 import { connect } from '../../../base/redux';
11
 import { StyleType } from '../../../base/styles';
11
 import { StyleType } from '../../../base/styles';
12
+import { PrivateMessageButton } from '../../../chat';
12
 
13
 
13
 import { hideRemoteVideoMenu } from '../../actions';
14
 import { hideRemoteVideoMenu } from '../../actions';
14
 
15
 
95
                 <MuteButton { ...buttonProps } />
96
                 <MuteButton { ...buttonProps } />
96
                 <KickButton { ...buttonProps } />
97
                 <KickButton { ...buttonProps } />
97
                 <PinButton { ...buttonProps } />
98
                 <PinButton { ...buttonProps } />
99
+                <PrivateMessageButton { ...buttonProps } />
98
             </BottomSheet>
100
             </BottomSheet>
99
         );
101
         );
100
     }
102
     }

+ 62
- 0
react/features/remote-video-menu/components/web/PrivateMessageMenuButton.js 查看文件

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 查看文件

8
 import {
8
 import {
9
     MuteButton,
9
     MuteButton,
10
     KickButton,
10
     KickButton,
11
+    PrivateMessageMenuButton,
11
     RemoteControlButton,
12
     RemoteControlButton,
12
     RemoteVideoMenu,
13
     RemoteVideoMenu,
13
     VolumeSlider
14
     VolumeSlider
188
             );
189
             );
189
         }
190
         }
190
 
191
 
192
+        buttons.push(
193
+            <PrivateMessageMenuButton
194
+                key = 'privateMessage'
195
+                participantID = { participantID } />
196
+        );
197
+
191
         if (onVolumeChange) {
198
         if (onVolumeChange) {
192
             buttons.push(
199
             buttons.push(
193
                 <VolumeSlider
200
                 <VolumeSlider

+ 1
- 0
react/features/remote-video-menu/components/web/index.js 查看文件

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

正在加载...
取消
保存