Преглед на файлове

feat(conference) add end conference

Add the ability (for moderators) to end the meeting for everyone.
factor2
wfleischer преди 2 години
родител
ревизия
09efaecc41
No account linked to committer's email address
променени са 28 файла, в които са добавени 723 реда и са изтрити 42 реда
  1. 14
    0
      css/_toolbars.scss
  2. 2
    0
      css/_variables.scss
  3. 5
    0
      doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
  4. 2
    1
      lang/main-ar.json
  5. 2
    1
      lang/main-ca.json
  6. 2
    1
      lang/main-de.json
  7. 2
    1
      lang/main-hsb.json
  8. 2
    1
      lang/main-it.json
  9. 2
    1
      lang/main-pt.json
  10. 4
    1
      lang/main.json
  11. 36
    1
      react/features/base/conference/actions.js
  12. 2
    5
      react/features/base/conference/middleware.any.js
  13. 2
    2
      react/features/conference/components/native/carmode/EndMeetingButton.tsx
  14. 10
    0
      react/features/toolbox/actionTypes.ts
  15. 17
    0
      react/features/toolbox/actions.web.js
  16. 2
    9
      react/features/toolbox/components/HangupButton.js
  17. 83
    0
      react/features/toolbox/components/native/HangupMenu.tsx
  18. 36
    0
      react/features/toolbox/components/native/HangupMenuButton.tsx
  19. 19
    4
      react/features/toolbox/components/native/Toolbox.js
  20. 11
    0
      react/features/toolbox/components/native/styles.js
  21. 38
    0
      react/features/toolbox/components/web/EndConferenceButton.tsx
  22. 130
    0
      react/features/toolbox/components/web/HangupMenuButton.tsx
  23. 75
    0
      react/features/toolbox/components/web/HangupToggleButton.tsx
  24. 35
    0
      react/features/toolbox/components/web/LeaveConferenceButton.tsx
  25. 89
    9
      react/features/toolbox/components/web/Toolbox.tsx
  26. 14
    0
      react/features/toolbox/reducer.js
  27. 84
    0
      resources/prosody-plugins/mod_end_conference.lua
  28. 3
    5
      resources/prosody-plugins/mod_muc_breakout_rooms.lua

+ 14
- 0
css/_toolbars.scss Целия файл

@@ -134,6 +134,20 @@
134 134
     }
135 135
 }
136 136
 
137
+.hangup-menu-button {
138
+    background-color: $hangupMenuButtonColor;
139
+
140
+    @media (hover: hover) and (pointer: fine) {
141
+        &:hover {
142
+            background-color: $hangupMenuButtonHoverColor;
143
+        }
144
+    }
145
+
146
+    svg {
147
+        fill: #fff;
148
+    }
149
+}
150
+
137 151
 .profile-button-avatar {
138 152
     align-items: center;
139 153
 }

+ 2
- 0
css/_variables.scss Целия файл

@@ -6,6 +6,8 @@
6 6
 $baseFontFamily: -apple-system, BlinkMacSystemFont, 'open_sanslight', 'Helvetica Neue', Helvetica, Arial, sans-serif;
7 7
 $hangupColor:#DD3849;
8 8
 $hangupHoverColor: #F25363;
9
+$hangupMenuButtonColor:#0056E0;;
10
+$hangupMenuButtonHoverColor: #246FE5;
9 11
 $hangupFontSize: 2em;
10 12
 
11 13
 /**

+ 5
- 0
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example Целия файл

@@ -52,6 +52,7 @@ VirtualHost "jitmeet.example.com"
52 52
     av_moderation_component = "avmoderation.jitmeet.example.com"
53 53
     speakerstats_component = "speakerstats.jitmeet.example.com"
54 54
     conference_duration_component = "conferenceduration.jitmeet.example.com"
55
+    end_conference_component = "endconference.jitmeet.example.com"
55 56
     -- we need bosh
56 57
     modules_enabled = {
57 58
         "bosh";
@@ -60,6 +61,7 @@ VirtualHost "jitmeet.example.com"
60 61
         "speakerstats";
61 62
         "external_services";
62 63
         "conference_duration";
64
+        "end_conference";
63 65
         "muc_lobby_rooms";
64 66
         "muc_breakout_rooms";
65 67
         "av_moderation";
@@ -123,6 +125,9 @@ Component "speakerstats.jitmeet.example.com" "speakerstats_component"
123 125
 Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
124 126
     muc_component = "conference.jitmeet.example.com"
125 127
 
128
+Component "endconference.jitmeet.example.com" "end_conference"
129
+    muc_component = "conference.jitmeet.example.com"
130
+
126 131
 Component "avmoderation.jitmeet.example.com" "av_moderation_component"
127 132
     muc_component = "conference.jitmeet.example.com"
128 133
 

+ 2
- 1
lang/main-ar.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": "اترك الاجتماع",
83 82
             "selectSoundDevice": "حدد جهاز الصوت"
84 83
         },
85 84
         "labels": {
@@ -1072,6 +1071,7 @@
1072 1071
             "invite": "ادعُ آخرين",
1073 1072
             "kick": "اطرد مشاركًا",
1074 1073
             "laugh": "يضحك",
1074
+            "leaveConference": "اترك الاجتماع",
1075 1075
             "like": "رفع الإبهام متمنيا النجاح",
1076 1076
             "linkToSalesforce": "ارتباط إلى Salesforce",
1077 1077
             "lobbyButton": "فعِّل/عطِّل وضع غرفة الانتظار",
@@ -1146,6 +1146,7 @@
1146 1146
         "joinBreakoutRoom": "انضم إلى غرفة الجانبية",
1147 1147
         "laugh": "يضحك",
1148 1148
         "leaveBreakoutRoom": "اترك إلى غرفة الجانبية",
1149
+        "leaveConference": "اترك الاجتماع",
1149 1150
         "like": "رفع الإبهام متمنيا النجاح",
1150 1151
         "linkToSalesforce": "ارتباط إلى Salesforce",
1151 1152
         "lobbyButtonDisable": "عطِّل وضع غرفة الانتظار",

+ 2
- 1
lang/main-ca.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": "Abandona la reunió",
83 82
             "selectSoundDevice": "Seleccioneu l'aparell d'àudio"
84 83
         },
85 84
         "labels": {
@@ -1039,6 +1038,7 @@
1039 1038
             "invite": "Convida-hi persones",
1040 1039
             "kick": "Expulsa el participant",
1041 1040
             "laugh": "Riure",
1041
+            "leaveConference": "Abandona la reunió",
1042 1042
             "like": "Polzes amunt",
1043 1043
             "linkToSalesforce": "Enllaç a Salesforce",
1044 1044
             "lobbyButton": "Activa o desactiva la sala d'espera",
@@ -1111,6 +1111,7 @@
1111 1111
         "joinBreakoutRoom": "Entra a la sala de descans",
1112 1112
         "laugh": "Riure",
1113 1113
         "leaveBreakoutRoom": "Surt de la sala de descans",
1114
+        "leaveConference": "Abandona la reunió",
1114 1115
         "like": "Polzes amunt",
1115 1116
         "linkToSalesforce": "Enllaç a Salesforce",
1116 1117
         "lobbyButtonDisable": "Desactiva el mode de sala d'espera",

+ 2
- 1
lang/main-de.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": "Konferenz verlassen",
83 82
             "selectSoundDevice": "Audiogerät auswählen"
84 83
         },
85 84
         "labels": {
@@ -1066,6 +1065,7 @@
1066 1065
             "invite": "Person einladen",
1067 1066
             "kick": "Person entfernen",
1068 1067
             "laugh": "Lachen",
1068
+            "leaveConference": "Konferenz verlassen",
1069 1069
             "like": "Daumen nach oben",
1070 1070
             "linkToSalesforce": "Mit Salesforce verlinken",
1071 1071
             "lobbyButton": "Lobbymodus ein-/ausschalten",
@@ -1140,6 +1140,7 @@
1140 1140
         "joinBreakoutRoom": "In Breakout-Raum wechseln",
1141 1141
         "laugh": "Lachen",
1142 1142
         "leaveBreakoutRoom": "Breakout-Raum verlassen",
1143
+        "leaveConference": "Konferenz verlassen",
1143 1144
         "like": "Daumen hoch",
1144 1145
         "linkToSalesforce": "Mit Salesforce verknüpfen",
1145 1146
         "lobbyButtonDisable": "Lobbymodus deaktivieren",

+ 2
- 1
lang/main-hsb.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": "konferencu wopušćić",
83 82
             "selectSoundDevice": "nastroj za zwuk wuzwolić"
84 83
         },
85 84
         "labels": {
@@ -1045,6 +1044,7 @@
1045 1044
             "invite": "wobdźělnika přeprosyć",
1046 1045
             "kick": " wobdźělnika wuzamknyć",
1047 1046
             "laugh": "so smjeć",
1047
+            "leaveConference": "konferencu wopušćić",
1048 1048
             "like": "palc horje",
1049 1049
             "linkToSalesforce": "ze Salesforce zwjazać",
1050 1050
             "lobbyButton": "lobby-modus zapnyć/hasnyć",
@@ -1117,6 +1117,7 @@
1117 1117
         "joinBreakoutRoom": "do breakout rumnosće měnić",
1118 1118
         "laugh": "so smjeć",
1119 1119
         "leaveBreakoutRoom": "breakout rumnosć wopusćić",
1120
+        "leaveConference": "konferencu wopušćić",
1120 1121
         "like": "palc horje",
1121 1122
         "linkToSalesforce": "ze Salesforce zwjazać",
1122 1123
         "lobbyButtonDisable": "lobby-modus deaktiwěrować",

+ 2
- 1
lang/main-it.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": " Lascia riunione",
83 82
             "selectSoundDevice": "Scegli audio"
84 83
         },
85 84
         "labels": {
@@ -1045,6 +1044,7 @@
1045 1044
             "invite": "Invita partecipanti",
1046 1045
             "kick": "Espelli partecipante",
1047 1046
             "laugh": "Ridi",
1047
+            "leaveConference": " Lascia riunione",
1048 1048
             "like": "Mi piace",
1049 1049
             "linkToSalesforce": "Collega a Salesforce",
1050 1050
             "lobbyButton": "Attiva/Disattiva sala d'attesa",
@@ -1117,6 +1117,7 @@
1117 1117
         "joinBreakoutRoom": "Entra in sottogruppo",
1118 1118
         "laugh": "Ridi",
1119 1119
         "leaveBreakoutRoom": "Lascia breakout room",
1120
+        "leaveConference": " Lascia riunione",
1120 1121
         "like": "Mi piace",
1121 1122
         "linkToSalesforce": "Collega a Salesforce",
1122 1123
         "lobbyButtonDisable": "Disabilita sala d'attesa",

+ 2
- 1
lang/main-pt.json Целия файл

@@ -79,7 +79,6 @@
79 79
     },
80 80
     "carmode": {
81 81
         "actions": {
82
-            "leaveMeeting": " Deixar a reunião",
83 82
             "selectSoundDevice": "Seleccionar dispositivo de som"
84 83
         },
85 84
         "labels": {
@@ -1076,6 +1075,7 @@
1076 1075
             "invite": "Convidar pessoas",
1077 1076
             "kick": "Remover participante",
1078 1077
             "laugh": "Risos",
1078
+            "leaveConference": "Deixar a reunião",
1079 1079
             "like": "Aprovado",
1080 1080
             "linkToSalesforce": "Link para a Salesforce",
1081 1081
             "lobbyButton": "Ativar/desativar sala de espera",
@@ -1150,6 +1150,7 @@
1150 1150
         "joinBreakoutRoom": "Entrar na sala",
1151 1151
         "laugh": "Risos",
1152 1152
         "leaveBreakoutRoom": "Sair da sala",
1153
+        "leaveConference": "Deixar a reunião",
1153 1154
         "like": "Aprovado",
1154 1155
         "linkToSalesforce": "Link para a Salesforce",
1155 1156
         "lobbyButtonDisable": "Desativar sala de espera",

+ 4
- 1
lang/main.json Целия файл

@@ -78,7 +78,6 @@
78 78
     },
79 79
     "carmode": {
80 80
         "actions": {
81
-            "leaveMeeting": " Leave meeting",
82 81
             "selectSoundDevice": "Select sound device"
83 82
         },
84 83
         "labels": {
@@ -1064,6 +1063,7 @@
1064 1063
             "document": "Toggle shared document",
1065 1064
             "download": "Download our apps",
1066 1065
             "embedMeeting": "Embed meeting",
1066
+            "endConference": "End meeting for all",
1067 1067
             "expand": "Expand",
1068 1068
             "feedback": "Leave feedback",
1069 1069
             "fullScreen": "Toggle full screen",
@@ -1074,6 +1074,7 @@
1074 1074
             "invite": "Invite people",
1075 1075
             "kick": "Kick participant",
1076 1076
             "laugh": "Laugh",
1077
+            "leaveConference": "Leave meeting",
1077 1078
             "like": "Thumbs Up",
1078 1079
             "linkToSalesforce": "Link to Salesforce",
1079 1080
             "lobbyButton": "Enable/disable lobby mode",
@@ -1136,6 +1137,7 @@
1136 1137
         "download": "Download our apps",
1137 1138
         "e2ee": "End-to-End Encryption",
1138 1139
         "embedMeeting": "Embed meeting",
1140
+        "endConference": "End meeting for all",
1139 1141
         "enterFullScreen": "View full screen",
1140 1142
         "enterTileView": "Enter tile view",
1141 1143
         "exitFullScreen": "Exit full screen",
@@ -1148,6 +1150,7 @@
1148 1150
         "joinBreakoutRoom": "Join breakout room",
1149 1151
         "laugh": "Laugh",
1150 1152
         "leaveBreakoutRoom": "Leave breakout room",
1153
+        "leaveConference": "Leave meeting",
1151 1154
         "like": "Thumbs Up",
1152 1155
         "linkToSalesforce": "Link to Salesforce",
1153 1156
         "lobbyButtonDisable": "Disable lobby mode",

+ 36
- 1
react/features/base/conference/actions.js Целия файл

@@ -6,9 +6,10 @@ import {
6 6
     createStartMutedConfigurationEvent,
7 7
     sendAnalytics
8 8
 } from '../../analytics';
9
+import { appNavigate } from '../../app/actions';
9 10
 import { endpointMessageReceived } from '../../subtitles';
10 11
 import { getReplaceParticipant } from '../config/functions';
11
-import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
12
+import { JITSI_CONNECTION_CONFERENCE_KEY, disconnect } from '../connection';
12 13
 import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
13 14
 import {
14 15
     MEDIA_TYPE,
@@ -27,6 +28,7 @@ import {
27 28
     participantRoleChanged,
28 29
     participantUpdated
29 30
 } from '../participants';
31
+import { toState } from '../redux';
30 32
 import {
31 33
     destroyLocalTracks,
32 34
     getLocalTracks,
@@ -75,6 +77,7 @@ import {
75 77
     commonUserLeftHandling,
76 78
     getConferenceOptions,
77 79
     getCurrentConference,
80
+    getConferenceState,
78 81
     sendLocalParticipant
79 82
 } from './functions';
80 83
 import logger from './logger';
@@ -584,6 +587,19 @@ export function dataChannelOpened() {
584 587
     };
585 588
 }
586 589
 
590
+/**
591
+ * Action to end a conference for all participants.
592
+ *
593
+ * @returns {Function}
594
+ */
595
+export function endConference() {
596
+    return async (dispatch: Dispatch<any>, getState: Function) => {
597
+        const { conference } = getConferenceState(toState(getState));
598
+
599
+        conference?.end();
600
+    };
601
+}
602
+
587 603
 /**
588 604
  * Signals that we've been kicked out of the conference.
589 605
  *
@@ -605,6 +621,25 @@ export function kickedOut(conference: Object, participant: Object) {
605 621
     };
606 622
 }
607 623
 
624
+
625
+/**
626
+ * Action to leave a conference.
627
+ *
628
+ * @returns {Function}
629
+ */
630
+export function leaveConference() {
631
+    return async (dispatch: Dispatch<any>) => {
632
+
633
+        // FIXME: these should be unified.
634
+        if (navigator.product === 'ReactNative') {
635
+            dispatch(appNavigate(undefined));
636
+        } else {
637
+            dispatch(disconnect(true));
638
+        }
639
+    };
640
+}
641
+
642
+
608 643
 /**
609 644
  * Signals that the lock state of a specific JitsiConference changed.
610 645
  *

+ 2
- 5
react/features/base/conference/middleware.any.js Целия файл

@@ -11,10 +11,7 @@ import {
11 11
 import { reloadNow } from '../../app/actions';
12 12
 import { removeLobbyChatParticipant } from '../../chat/actions.any';
13 13
 import { openDisplayNamePrompt } from '../../display-name';
14
-import {
15
-    NOTIFICATION_TIMEOUT_TYPE,
16
-    showErrorNotification
17
-} from '../../notifications';
14
+import { NOTIFICATION_TIMEOUT_TYPE, showErrorNotification, showWarningNotification } from '../../notifications';
18 15
 import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, connectionDisconnected } from '../connection';
19 16
 import { validateJwt } from '../jwt';
20 17
 import { JitsiConferenceErrors } from '../lib-jitsi-meet';
@@ -132,7 +129,7 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
132 129
     case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
133 130
         const [ reason ] = error.params;
134 131
 
135
-        dispatch(showErrorNotification({
132
+        dispatch(showWarningNotification({
136 133
             description: reason,
137 134
             titleKey: 'dialog.sessTerminated'
138 135
         }, NOTIFICATION_TIMEOUT_TYPE.LONG));

+ 2
- 2
react/features/conference/components/native/carmode/EndMeetingButton.tsx Целия файл

@@ -29,9 +29,9 @@ const EndMeetingButton = () : JSX.Element => {
29 29
 
30 30
     return (
31 31
         <Button
32
-            accessibilityLabel = 'carmode.actions.leaveMeeting'
32
+            accessibilityLabel = 'toolbar.accessibilityLabel.leaveConference'
33 33
             icon = { EndMeetingIcon }
34
-            labelKey = 'carmode.actions.leaveMeeting'
34
+            labelKey = 'toolbar.leaveConference'
35 35
             onClick = { onSelect }
36 36
             style = { styles.endMeetingButton }
37 37
             type = { BUTTON_TYPES.DESTRUCTIVE } />

+ 10
- 0
react/features/toolbox/actionTypes.ts Целия файл

@@ -29,6 +29,16 @@ export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
29 29
  */
30 30
 export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
31 31
 
32
+/**
33
+ * The type of the (redux) action which shows/hides the hangup menu.
34
+ *
35
+ * {
36
+ *     type: SET_HANGUP_MENU_VISIBLE,
37
+ *     visible: boolean
38
+ * }
39
+ */
40
+export const SET_HANGUP_MENU_VISIBLE = 'SET_HANGUP_MENU_VISIBLE';
41
+
32 42
 /**
33 43
  * The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
34 44
  */

+ 17
- 0
react/features/toolbox/actions.web.js Целия файл

@@ -9,6 +9,7 @@ import {
9 9
     CLEAR_TOOLBOX_TIMEOUT,
10 10
     FULL_SCREEN_CHANGED,
11 11
     SET_FULL_SCREEN,
12
+    SET_HANGUP_MENU_VISIBLE,
12 13
     SET_OVERFLOW_DRAWER,
13 14
     SET_OVERFLOW_MENU_VISIBLE,
14 15
     SET_TOOLBAR_HOVERED,
@@ -188,6 +189,22 @@ export function clearToolboxTimeout(): Object {
188 189
     };
189 190
 }
190 191
 
192
+/**
193
+ * Shows/hides the hangup menu.
194
+ *
195
+ * @param {boolean} visible - True to show it or false to hide it.
196
+ * @returns {{
197
+ *     type: SET_HANGUP_MENU_VISIBLE,
198
+ *     visible: boolean
199
+ * }}
200
+ */
201
+export function setHangupMenuVisible(visible: boolean): Object {
202
+    return {
203
+        type: SET_HANGUP_MENU_VISIBLE,
204
+        visible
205
+    };
206
+}
207
+
191 208
 /**
192 209
  * Shows/hides the overflow menu.
193 210
  *

+ 2
- 9
react/features/toolbox/components/HangupButton.js Целия файл

@@ -3,8 +3,7 @@
3 3
 import _ from 'lodash';
4 4
 
5 5
 import { createToolbarEvent, sendAnalytics } from '../../analytics';
6
-import { appNavigate } from '../../app/actions';
7
-import { disconnect } from '../../base/connection';
6
+import { leaveConference } from '../../base/conference/actions';
8 7
 import { translate } from '../../base/i18n';
9 8
 import { connect } from '../../base/redux';
10 9
 import { AbstractHangupButton } from '../../base/toolbox/components';
@@ -44,13 +43,7 @@ class HangupButton extends AbstractHangupButton<Props, *> {
44 43
 
45 44
         this._hangup = _.once(() => {
46 45
             sendAnalytics(createToolbarEvent('hangup'));
47
-
48
-            // FIXME: these should be unified.
49
-            if (navigator.product === 'ReactNative') {
50
-                this.props.dispatch(appNavigate(undefined));
51
-            } else {
52
-                this.props.dispatch(disconnect(true));
53
-            }
46
+            this.props.dispatch(leaveConference());
54 47
         });
55 48
     }
56 49
 

+ 83
- 0
react/features/toolbox/components/native/HangupMenu.tsx Целия файл

@@ -0,0 +1,83 @@
1
+/* eslint-disable lines-around-comment */
2
+import React, { useCallback } from 'react';
3
+import { View } from 'react-native';
4
+import { useDispatch, useSelector } from 'react-redux';
5
+
6
+// @ts-ignore
7
+import { createBreakoutRoomsEvent, createToolbarEvent, sendAnalytics } from '../../../analytics';
8
+// @ts-ignore
9
+import { appNavigate } from '../../../app/actions';
10
+// @ts-ignore
11
+import { ColorSchemeRegistry } from '../../../base/color-scheme';
12
+// @ts-ignore
13
+import { endConference } from '../../../base/conference';
14
+// @ts-ignore
15
+import { hideSheet } from '../../../base/dialog';
16
+// @ts-ignore
17
+import BottomSheet from '../../../base/dialog/components/native/BottomSheet';
18
+// @ts-ignore
19
+import { getLocalParticipant, PARTICIPANT_ROLE } from '../../../base/participants';
20
+import Button from '../../../base/ui/components/native/Button';
21
+import { BUTTON_TYPES } from '../../../base/ui/constants';
22
+// @ts-ignore
23
+import { moveToRoom } from '../../../breakout-rooms/actions';
24
+// @ts-ignore
25
+import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
26
+
27
+/**
28
+ * Menu presenting options to leave a room or meeting and to end meeting.
29
+ *
30
+ * @returns {JSX.Element} - The hangup menu.
31
+ */
32
+function HangupMenu() {
33
+    const dispatch = useDispatch();
34
+    const _styles = useSelector(state => ColorSchemeRegistry.get(state, 'Toolbox'));
35
+    const inBreakoutRoom = useSelector(isInBreakoutRoom);
36
+    const isModerator = useSelector(state => getLocalParticipant(state).role === PARTICIPANT_ROLE.MODERATOR);
37
+    const { DESTRUCTIVE, SECONDARY } = BUTTON_TYPES;
38
+
39
+    const handleEndConference = useCallback(() => {
40
+        dispatch(hideSheet());
41
+        sendAnalytics(createToolbarEvent('endmeeting'));
42
+        dispatch(endConference());
43
+    }, [ hideSheet ]);
44
+
45
+    const handleLeaveConference = useCallback(() => {
46
+        dispatch(hideSheet());
47
+        sendAnalytics(createToolbarEvent('hangup'));
48
+        dispatch(appNavigate(undefined));
49
+    }, [ hideSheet ]);
50
+
51
+    const handleLeaveBreakoutRoom = useCallback(() => {
52
+        dispatch(hideSheet());
53
+        sendAnalytics(createBreakoutRoomsEvent('leave'));
54
+        dispatch(moveToRoom());
55
+    }, [ hideSheet ]);
56
+
57
+    return (
58
+        <BottomSheet>
59
+            <View style = { _styles.hangupMenuContainer }>
60
+                { isModerator && <Button
61
+                    accessibilityLabel = 'toolbar.endConference'
62
+                    labelKey = 'toolbar.endConference'
63
+                    onClick = { handleEndConference }
64
+                    style = { _styles.hangupButton }
65
+                    type = { DESTRUCTIVE } /> }
66
+                <Button
67
+                    accessibilityLabel = 'toolbar.leaveConference'
68
+                    labelKey = 'toolbar.leaveConference'
69
+                    onClick = { handleLeaveConference }
70
+                    style = { _styles.hangupButton }
71
+                    type = { SECONDARY } />
72
+                { inBreakoutRoom && <Button
73
+                    accessibilityLabel = 'breakoutRooms.actions.leaveBreakoutRoom'
74
+                    labelKey = 'breakoutRooms.actions.leaveBreakoutRoom'
75
+                    onClick = { handleLeaveBreakoutRoom }
76
+                    style = { _styles.hangupButton }
77
+                    type = { SECONDARY } /> }
78
+            </View>
79
+        </BottomSheet>
80
+    );
81
+}
82
+
83
+export default HangupMenu;

+ 36
- 0
react/features/toolbox/components/native/HangupMenuButton.tsx Целия файл

@@ -0,0 +1,36 @@
1
+/* eslint-disable lines-around-comment */
2
+import React, { useCallback } from 'react';
3
+import { useDispatch } from 'react-redux';
4
+
5
+// @ts-ignore
6
+import { openSheet } from '../../../base/dialog';
7
+// @ts-ignore
8
+import { IconHangup } from '../../../base/icons';
9
+import IconButton from '../../../base/react/components/native/IconButton';
10
+import { BUTTON_TYPES } from '../../../base/ui/constants';
11
+
12
+import HangupMenu from './HangupMenu';
13
+
14
+
15
+/**
16
+ * Button for showing the hangup menu.
17
+ *
18
+ * @returns {JSX.Element} - The hangup menu button.
19
+ */
20
+const HangupMenuButton = () : JSX.Element => {
21
+    const dispatch = useDispatch();
22
+
23
+    const onSelect = useCallback(() => {
24
+        dispatch(openSheet(HangupMenu));
25
+    }, [ dispatch ]);
26
+
27
+    return (
28
+        <IconButton
29
+            accessibilityLabel = 'toolbar.accessibilityLabel.hangup'
30
+            onPress = { onSelect }
31
+            src = { IconHangup }
32
+            type = { BUTTON_TYPES.PRIMARY } />
33
+    );
34
+};
35
+
36
+export default HangupMenuButton;

+ 19
- 4
react/features/toolbox/components/native/Toolbox.js Целия файл

@@ -18,6 +18,7 @@ import AudioMuteButton from '../AudioMuteButton';
18 18
 import HangupButton from '../HangupButton';
19 19
 import VideoMuteButton from '../VideoMuteButton';
20 20
 
21
+import HangupMenuButton from './HangupMenuButton';
21 22
 import OverflowMenuButton from './OverflowMenuButton';
22 23
 import RaiseHandButton from './RaiseHandButton';
23 24
 import styles from './styles';
@@ -27,6 +28,11 @@ import styles from './styles';
27 28
  */
28 29
 type Props = {
29 30
 
31
+    /**
32
+     * Whether the end conference feature is supported.
33
+     */
34
+    _endConferenceSupported: boolean,
35
+
30 36
     /**
31 37
      * Whether or not the reactions feature is enabled.
32 38
      */
@@ -55,14 +61,14 @@ type Props = {
55 61
  * @returns {React$Element}.
56 62
  */
57 63
 function Toolbox(props: Props) {
58
-    const { _reactionsEnabled, _styles, _visible, _width } = props;
64
+    const { _endConferenceSupported, _reactionsEnabled, _styles, _visible, _width } = props;
59 65
 
60 66
     if (!_visible) {
61 67
         return null;
62 68
     }
63 69
 
64 70
     const bottomEdge = Platform.OS === 'ios' && _visible;
65
-    const { buttonStylesBorderless, hangupButtonStyles, toggledButtonStyles } = _styles;
71
+    const { buttonStylesBorderless, hangupButtonStyles, hangupMenuButtonStyles, toggledButtonStyles } = _styles;
66 72
     const additionalButtons = getMovableButtons(_width);
67 73
     const backgroundToggledStyle = {
68 74
         ...toggledButtonStyles,
@@ -110,8 +116,13 @@ function Toolbox(props: Props) {
110 116
                 <OverflowMenuButton
111 117
                     styles = { buttonStylesBorderless }
112 118
                     toggledStyles = { toggledButtonStyles } />
113
-                <HangupButton
114
-                    styles = { hangupButtonStyles } />
119
+                { _endConferenceSupported
120
+                    ? <HangupMenuButton
121
+                        styles = { hangupMenuButtonStyles }
122
+                        toggledStyles = { toggledButtonStyles } />
123
+                    : <HangupButton
124
+                        styles = { hangupButtonStyles } />
125
+                }
115 126
             </SafeAreaView>
116 127
         </View>
117 128
     );
@@ -127,7 +138,11 @@ function Toolbox(props: Props) {
127 138
  * @returns {Props}
128 139
  */
129 140
 function _mapStateToProps(state: Object): Object {
141
+    const { conference } = state['features/base/conference'];
142
+    const endConferenceSupported = conference?.isEndConferenceSupported();
143
+
130 144
     return {
145
+        _endConferenceSupported: Boolean(endConferenceSupported),
131 146
         _styles: ColorSchemeRegistry.get(state, 'Toolbox'),
132 147
         _visible: isToolboxVisible(state),
133 148
         _width: state['features/base/responsive-ui'].clientWidth,

+ 11
- 0
react/features/toolbox/components/native/styles.js Целия файл

@@ -133,6 +133,17 @@ ColorSchemeRegistry.register('Toolbox', {
133 133
         backgroundColor: BaseTheme.palette.ui13
134 134
     },
135 135
 
136
+    hangupMenuContainer: {
137
+        marginHorizontal: BaseTheme.spacing[2],
138
+        marginVertical: BaseTheme.spacing[2]
139
+    },
140
+
141
+    hangupButton: {
142
+        flex: 1,
143
+        marginHorizontal: BaseTheme.spacing[2],
144
+        marginVertical: BaseTheme.spacing[2]
145
+    },
146
+
136 147
     hangupButtonStyles: {
137 148
         iconStyle: whiteToolbarButtonIcon,
138 149
         style: {

+ 38
- 0
react/features/toolbox/components/web/EndConferenceButton.tsx Целия файл

@@ -0,0 +1,38 @@
1
+/* eslint-disable lines-around-comment */
2
+import React, { useCallback } from 'react';
3
+import { useTranslation } from 'react-i18next';
4
+import { useDispatch, useSelector } from 'react-redux';
5
+
6
+// @ts-ignore
7
+import { endConference } from '../../../base/conference';
8
+// @ts-ignore
9
+import { isLocalParticipantModerator } from '../../../base/participants';
10
+import Button from '../../../base/ui/components/web/Button';
11
+import { BUTTON_TYPES } from '../../../base/ui/constants';
12
+// @ts-ignore
13
+import { isInBreakoutRoom } from '../../../breakout-rooms/functions';
14
+
15
+/**
16
+ * Button to end the conference for all participants.
17
+ *
18
+ * @returns {JSX.Element} - The end conference button.
19
+ */
20
+export const EndConferenceButton = () => {
21
+    const { t } = useTranslation();
22
+    const dispatch = useDispatch();
23
+    const _isLocalParticipantModerator = useSelector(isLocalParticipantModerator);
24
+    const _isInBreakoutRoom = useSelector(isInBreakoutRoom);
25
+
26
+    const onEndConference = useCallback(() => {
27
+        dispatch(endConference());
28
+    }, [ dispatch ]);
29
+
30
+    return (<>
31
+        { !_isInBreakoutRoom && _isLocalParticipantModerator && <Button
32
+            accessibilityLabel = { t('toolbar.accessibilityLabel.endConference') }
33
+            fullWidth = { true }
34
+            label = { t('toolbar.endConference') }
35
+            onClick = { onEndConference }
36
+            type = { BUTTON_TYPES.DESTRUCTIVE } /> }
37
+    </>);
38
+};

+ 130
- 0
react/features/toolbox/components/web/HangupMenuButton.tsx Целия файл

@@ -0,0 +1,130 @@
1
+/* eslint-disable lines-around-comment */
2
+import InlineDialog from '@atlaskit/inline-dialog';
3
+import React, { Component } from 'react';
4
+
5
+// @ts-ignore
6
+import { createToolbarEvent, sendAnalytics } from '../../../analytics';
7
+// @ts-ignore
8
+import { translate } from '../../../base/i18n';
9
+
10
+import HangupToggleButton from './HangupToggleButton';
11
+
12
+/**
13
+ * The type of the React {@code Component} props of {@link HangupMenuButton}.
14
+ */
15
+type Props = {
16
+
17
+    /**
18
+     * ID of the menu that is controlled by this button.
19
+     */
20
+    ariaControls: String,
21
+
22
+    /**
23
+     * A child React Element to display within {@code InlineDialog}.
24
+     */
25
+    children: React.ReactNode,
26
+
27
+    /**
28
+     * Whether or not the HangupMenu popover should display.
29
+     */
30
+    isOpen: boolean,
31
+
32
+    /**
33
+     * Callback to change the visibility of the hangup menu.
34
+     */
35
+    onVisibilityChange: Function,
36
+
37
+    /**
38
+     * Invoked to obtain translated strings.
39
+     */
40
+    t: Function,
41
+};
42
+
43
+/**
44
+ * A React {@code Component} for opening or closing the {@code HangupMenu}.
45
+ *
46
+ * @augments Component
47
+ */
48
+class HangupMenuButton extends Component<Props> {
49
+    /**
50
+     * Initializes a new {@code HangupMenuButton} instance.
51
+     *
52
+     * @param {Object} props - The read-only properties with which the new
53
+     * instance is to be initialized.
54
+     */
55
+    constructor(props: Props) {
56
+        super(props);
57
+
58
+        // Bind event handlers so they are only bound once per instance.
59
+        this._onCloseDialog = this._onCloseDialog.bind(this);
60
+        this._toggleDialogVisibility
61
+            = this._toggleDialogVisibility.bind(this);
62
+        this._onEscClick = this._onEscClick.bind(this);
63
+    }
64
+
65
+    /**
66
+     * Click handler for the more actions entries.
67
+     *
68
+     * @param {KeyboardEvent} event - Esc key click to close the popup.
69
+     * @returns {void}
70
+     */
71
+    _onEscClick(event: KeyboardEvent) {
72
+        if (event.key === 'Escape' && this.props.isOpen) {
73
+            event.preventDefault();
74
+            event.stopPropagation();
75
+            this._onCloseDialog();
76
+        }
77
+    }
78
+
79
+    /**
80
+     * Implements React's {@link Component#render()}.
81
+     *
82
+     * @inheritdoc
83
+     * @returns {ReactElement}
84
+     */
85
+    render() {
86
+        const { children, isOpen } = this.props;
87
+
88
+        return (
89
+            <div className = 'toolbox-button-wth-dialog context-menu'>
90
+                <InlineDialog
91
+                    content = { children }
92
+                    isOpen = { isOpen }
93
+                    onClose = { this._onCloseDialog }
94
+                    placement = 'top-end'>
95
+                    <HangupToggleButton
96
+                        customClass = 'hangup-menu-button'
97
+                        handleClick = { this._toggleDialogVisibility }
98
+                        isOpen = { isOpen }
99
+                        onKeyDown = { this._onEscClick } />
100
+                </InlineDialog>
101
+            </div>
102
+        );
103
+    }
104
+
105
+    /**
106
+     * Callback invoked when {@code InlineDialog} signals that it should be
107
+     * close.
108
+     *
109
+     * @private
110
+     * @returns {void}
111
+     */
112
+    _onCloseDialog() {
113
+        this.props.onVisibilityChange(false);
114
+    }
115
+
116
+    /**
117
+     * Callback invoked to signal that an event has occurred that should change
118
+     * the visibility of the {@code InlineDialog} component.
119
+     *
120
+     * @private
121
+     * @returns {void}
122
+     */
123
+    _toggleDialogVisibility() {
124
+        sendAnalytics(createToolbarEvent('hangup'));
125
+
126
+        this.props.onVisibilityChange(!this.props.isOpen);
127
+    }
128
+}
129
+
130
+export default translate(HangupMenuButton);

+ 75
- 0
react/features/toolbox/components/web/HangupToggleButton.tsx Целия файл

@@ -0,0 +1,75 @@
1
+/* eslint-disable lines-around-comment */
2
+// @ts-ignore
3
+import { translate } from '../../../base/i18n';
4
+// @ts-ignore
5
+import { IconClose, IconHangup } from '../../../base/icons';
6
+// @ts-ignore
7
+import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
8
+
9
+/**
10
+ * The type of the React {@code Component} props of {@link HangupToggleButton}.
11
+ */
12
+type Props = AbstractButtonProps & {
13
+
14
+    /**
15
+     * Whether the more options menu is open.
16
+     */
17
+    isOpen: boolean,
18
+
19
+    /**
20
+     * External handler for key down action.
21
+     */
22
+    onKeyDown: Function,
23
+};
24
+
25
+/**
26
+ * Implementation of a button for toggling the hangup menu.
27
+ */
28
+class HangupToggleButton extends AbstractButton<Props, any, any> {
29
+    accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
30
+    icon = IconHangup;
31
+    label = 'toolbar.hangup';
32
+    toggledIcon = IconClose;
33
+    toggledLabel = 'toolbar.hangup';
34
+    props: Props;
35
+
36
+    /**
37
+     * Retrieves tooltip dynamically.
38
+     */
39
+    get tooltip() {
40
+        return 'toolbar.hangup';
41
+    }
42
+
43
+    /**
44
+     * Required by linter due to AbstractButton overwritten prop being writable.
45
+     *
46
+     * @param {string} _value - The value.
47
+     */
48
+    set tooltip(_value) {
49
+        // Unused.
50
+    }
51
+
52
+    /**
53
+     * Indicates whether this button is in toggled state or not.
54
+     *
55
+     * @override
56
+     * @protected
57
+     * @returns {boolean}
58
+     */
59
+    _isToggled() {
60
+        return this.props.isOpen;
61
+    }
62
+
63
+    /**
64
+     * Indicates whether a key was pressed.
65
+     *
66
+     * @override
67
+     * @protected
68
+     * @returns {boolean}
69
+     */
70
+    _onKeyDown() {
71
+        this.props.onKeyDown();
72
+    }
73
+}
74
+
75
+export default translate(HangupToggleButton);

+ 35
- 0
react/features/toolbox/components/web/LeaveConferenceButton.tsx Целия файл

@@ -0,0 +1,35 @@
1
+/* eslint-disable lines-around-comment */
2
+import React, { useCallback } from 'react';
3
+import { useTranslation } from 'react-i18next';
4
+import { useDispatch } from 'react-redux';
5
+
6
+// @ts-ignore
7
+import { createToolbarEvent, sendAnalytics } from '../../../analytics';
8
+// @ts-ignore
9
+import { leaveConference } from '../../../base/conference/actions';
10
+import Button from '../../../base/ui/components/web/Button';
11
+import { BUTTON_TYPES } from '../../../base/ui/constants';
12
+
13
+/**
14
+ * Button to leave the conference.
15
+ *
16
+ * @returns {JSX.Element} - The leave conference button.
17
+ */
18
+export const LeaveConferenceButton = () => {
19
+    const { t } = useTranslation();
20
+    const dispatch = useDispatch();
21
+
22
+    const onLeaveConference = useCallback(() => {
23
+        sendAnalytics(createToolbarEvent('hangup'));
24
+        dispatch(leaveConference());
25
+    }, [ dispatch ]);
26
+
27
+    return (
28
+        <Button
29
+            accessibilityLabel = { t('toolbar.accessibilityLabel.leaveConference') }
30
+            fullWidth = { true }
31
+            label = { t('toolbar.leaveConference') }
32
+            onClick = { onLeaveConference }
33
+            type = { BUTTON_TYPES.SECONDARY } />
34
+    );
35
+};

+ 89
- 9
react/features/toolbox/components/web/Toolbox.tsx Целия файл

@@ -108,6 +108,7 @@ import { VideoBackgroundButton, toggleBackgroundEffect } from '../../../virtual-
108 108
 import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants';
109 109
 import {
110 110
     setFullScreen,
111
+    setHangupMenuVisible,
111 112
     setOverflowMenuVisible,
112 113
     setToolbarHovered,
113 114
     showToolbox
@@ -127,8 +128,11 @@ import HelpButton from '../HelpButton';
127 128
 import AudioSettingsButton from './AudioSettingsButton';
128 129
 // @ts-ignore
129 130
 import DockIframeButton from './DockIframeButton';
131
+import { EndConferenceButton } from './EndConferenceButton';
130 132
 // @ts-ignore
131 133
 import FullscreenButton from './FullscreenButton';
134
+import HangupMenuButton from './HangupMenuButton';
135
+import { LeaveConferenceButton } from './LeaveConferenceButton';
132 136
 // @ts-ignore
133 137
 import LinkToSalesforceButton from './LinkToSalesforceButton';
134 138
 // @ts-ignore
@@ -199,6 +203,11 @@ interface Props extends WithTranslation {
199 203
      */
200 204
     _disabled: boolean,
201 205
 
206
+    /**
207
+     * Whether the end conference feature is supported.
208
+     */
209
+    _endConferenceSupported: boolean,
210
+
202 211
     /**
203 212
      * Whether or not call feedback can be sent.
204 213
      */
@@ -214,12 +223,17 @@ interface Props extends WithTranslation {
214 223
      */
215 224
     _gifsEnabled: boolean,
216 225
 
226
+    /**
227
+     * Whether the hangup menu is visible.
228
+     */
229
+    _hangupMenuVisible: boolean,
230
+
217 231
     /**
218 232
      * Whether the app has Salesforce integration.
219 233
      */
220 234
     _hasSalesforce: boolean,
221 235
 
222
-    /**
236
+     /**
223 237
      * Whether or not the app is running in an ios mobile browser.
224 238
      */
225 239
     _isIosMobile: boolean,
@@ -335,6 +349,15 @@ const styles = () => {
335 349
             right: 'auto',
336 350
             maxHeight: 'inherit',
337 351
             margin: 0
352
+        },
353
+        hangupMenu: {
354
+            position: 'relative',
355
+            right: 'auto',
356
+            display: 'flex',
357
+            flexDirection: 'column',
358
+            rowGap: '8px',
359
+            margin: 0,
360
+            padding: '16px'
338 361
         }
339 362
     };
340 363
 };
@@ -357,6 +380,7 @@ class Toolbox extends Component<Props> {
357 380
         // Bind event handlers so they are only bound once per instance.
358 381
         this._onMouseOut = this._onMouseOut.bind(this);
359 382
         this._onMouseOver = this._onMouseOver.bind(this);
383
+        this._onSetHangupVisible = this._onSetHangupVisible.bind(this);
360 384
         this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
361 385
         this._onTabIn = this._onTabIn.bind(this);
362 386
 
@@ -482,7 +506,7 @@ class Toolbox extends Component<Props> {
482 506
      * @inheritdoc
483 507
      */
484 508
     componentDidUpdate(prevProps: Props) {
485
-        const { _dialog, dispatch } = this.props;
509
+        const { _dialog, _visible, dispatch } = this.props;
486 510
 
487 511
 
488 512
         if (prevProps._overflowMenuVisible
@@ -491,6 +515,12 @@ class Toolbox extends Component<Props> {
491 515
             this._onSetOverflowVisible(false);
492 516
             dispatch(setToolbarHovered(false));
493 517
         }
518
+        if (prevProps._hangupMenuVisible
519
+            && prevProps._visible
520
+            && !_visible) {
521
+            this._onSetHangupVisible(false);
522
+            dispatch(setToolbarHovered(false));
523
+        }
494 524
     }
495 525
 
496 526
     /**
@@ -535,7 +565,7 @@ class Toolbox extends Component<Props> {
535 565
     }
536 566
 
537 567
     /**
538
-     * Key handler for overflow menu.
568
+     * Key handler for overflow/hangup menus.
539 569
      *
540 570
      * @param {KeyboardEvent} e - Esc key click to close the popup.
541 571
      * @returns {void}
@@ -543,10 +573,23 @@ class Toolbox extends Component<Props> {
543 573
     _onEscKey(e: React.KeyboardEvent) {
544 574
         if (e.key === 'Escape') {
545 575
             e.stopPropagation();
576
+            this._closeHangupMenuIfOpen();
546 577
             this._closeOverflowMenuIfOpen();
547 578
         }
548 579
     }
549 580
 
581
+    /**
582
+     * Closes the hangup menu if opened.
583
+     *
584
+     * @private
585
+     * @returns {void}
586
+     */
587
+    _closeHangupMenuIfOpen() {
588
+        const { dispatch, _hangupMenuVisible } = this.props;
589
+
590
+        _hangupMenuVisible && dispatch(setHangupMenuVisible(false));
591
+    }
592
+
550 593
     /**
551 594
      * Closes the overflow menu if opened.
552 595
      *
@@ -1012,6 +1055,19 @@ class Toolbox extends Component<Props> {
1012 1055
         this.props.dispatch(setToolbarHovered(true));
1013 1056
     }
1014 1057
 
1058
+    /**
1059
+     * Sets the visibility of the hangup menu.
1060
+     *
1061
+     * @param {boolean} visible - Whether or not the hangup menu should be
1062
+     * displayed.
1063
+     * @private
1064
+     * @returns {void}
1065
+     */
1066
+    _onSetHangupVisible(visible: boolean) {
1067
+        this.props.dispatch(setHangupMenuVisible(visible));
1068
+        this.props.dispatch(setToolbarHovered(visible));
1069
+    }
1070
+
1015 1071
     /**
1016 1072
      * Sets the visibility of the overflow menu.
1017 1073
      *
@@ -1307,6 +1363,8 @@ class Toolbox extends Component<Props> {
1307 1363
      */
1308 1364
     _renderToolboxContent() {
1309 1365
         const {
1366
+            _endConferenceSupported,
1367
+            _hangupMenuVisible,
1310 1368
             _isMobile,
1311 1369
             _overflowDrawer,
1312 1370
             _overflowMenuVisible,
@@ -1383,12 +1441,30 @@ class Toolbox extends Component<Props> {
1383 1441
                             </OverflowMenuButton>
1384 1442
                         )}
1385 1443
 
1386
-                        <HangupButton
1387
-                            buttonKey = 'hangup'
1388
-                            customClass = 'hangup-button'
1389
-                            key = 'hangup-button'
1390
-                            notifyMode = { this._getButtonNotifyMode('hangup') }
1391
-                            visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
1444
+                        { isToolbarButtonEnabled('hangup', _toolbarButtons) && (
1445
+                            _endConferenceSupported
1446
+                                ? <HangupMenuButton
1447
+                                    ariaControls = 'hangup-menu'
1448
+                                    isOpen = { _hangupMenuVisible }
1449
+                                    key = 'hangup-menu'
1450
+                                    onVisibilityChange = { this._onSetHangupVisible }>
1451
+                                    <ContextMenu
1452
+                                        accessibilityLabel = { t(toolbarAccLabel) }
1453
+                                        className = { classes.hangupMenu }
1454
+                                        hidden = { false }
1455
+                                        inDrawer = { _overflowDrawer }
1456
+                                        onKeyDown = { this._onEscKey }>
1457
+                                        <EndConferenceButton />
1458
+                                        <LeaveConferenceButton />
1459
+                                    </ContextMenu>
1460
+                                </HangupMenuButton>
1461
+                                : <HangupButton
1462
+                                    buttonKey = 'hangup'
1463
+                                    customClass = 'hangup-button'
1464
+                                    key = 'hangup-button'
1465
+                                    notifyMode = { this._getButtonNotifyMode('hangup') }
1466
+                                    visible = { isToolbarButtonEnabled('hangup', _toolbarButtons) } />
1467
+                        )}
1392 1468
                     </div>
1393 1469
                 </div>
1394 1470
             </div>
@@ -1407,6 +1483,7 @@ class Toolbox extends Component<Props> {
1407 1483
  */
1408 1484
 function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1409 1485
     const { conference } = state['features/base/conference'];
1486
+    const endConferenceSupported = conference?.isEndConferenceSupported();
1410 1487
     const {
1411 1488
         buttonsWithNotifyClick,
1412 1489
         callStatsID,
@@ -1416,6 +1493,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1416 1493
     } = state['features/base/config'];
1417 1494
     const {
1418 1495
         fullScreen,
1496
+        hangupMenuVisible,
1419 1497
         overflowMenuVisible,
1420 1498
         overflowDrawer
1421 1499
     } = state['features/toolbox'];
@@ -1434,6 +1512,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1434 1512
         _desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
1435 1513
         _dialog: Boolean(state['features/base/dialog'].component),
1436 1514
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1515
+        _endConferenceSupported: Boolean(endConferenceSupported),
1437 1516
         _feedbackConfigured: Boolean(callStatsID),
1438 1517
         _fullScreen: fullScreen,
1439 1518
         _gifsEnabled: isGifEnabled(state),
@@ -1442,6 +1521,7 @@ function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1442 1521
         _isMobile: isMobileBrowser(),
1443 1522
         _isVpaasMeeting: isVpaasMeeting(state),
1444 1523
         _hasSalesforce: isSalesforceEnabled(state),
1524
+        _hangupMenuVisible: hangupMenuVisible,
1445 1525
         _localParticipantID: localParticipant?.id,
1446 1526
         _localVideo: localVideo,
1447 1527
         _multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state),

+ 14
- 0
react/features/toolbox/reducer.js Целия файл

@@ -5,6 +5,7 @@ import { ReducerRegistry, set } from '../base/redux';
5 5
 import {
6 6
     CLEAR_TOOLBOX_TIMEOUT,
7 7
     FULL_SCREEN_CHANGED,
8
+    SET_HANGUP_MENU_VISIBLE,
8 9
     SET_OVERFLOW_DRAWER,
9 10
     SET_OVERFLOW_MENU_VISIBLE,
10 11
     SET_TOOLBAR_HOVERED,
@@ -26,6 +27,13 @@ const INITIAL_STATE = {
26 27
      */
27 28
     enabled: true,
28 29
 
30
+    /**
31
+     * The indicator which determines whether the hangup menu is visible.
32
+     *
33
+     * @type {boolean}
34
+     */
35
+    hangupMenuVisible: false,
36
+
29 37
     /**
30 38
      * The indicator which determines whether a Toolbar in the Toolbox is
31 39
      * hovered.
@@ -81,6 +89,12 @@ ReducerRegistry.register(
81 89
                 fullScreen: action.fullScreen
82 90
             };
83 91
 
92
+        case SET_HANGUP_MENU_VISIBLE:
93
+            return {
94
+                ...state,
95
+                hangupMenuVisible: action.visible
96
+            };
97
+
84 98
         case SET_OVERFLOW_DRAWER:
85 99
             return {
86 100
                 ...state,

+ 84
- 0
resources/prosody-plugins/mod_end_conference.lua Целия файл

@@ -0,0 +1,84 @@
1
+-- This module is added under the main virtual host domain
2
+--
3
+-- VirtualHost "jitmeet.example.com"
4
+--     modules_enabled = {
5
+--         "end_conference"
6
+--     }
7
+--     end_conference_component = "endconference.jitmeet.example.com"
8
+--
9
+-- Component "endconference.jitmeet.example.com" "end_conference"
10
+--     muc_component = muc.jitmeet.example.com
11
+--
12
+local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
13
+
14
+local END_CONFERENCE_REASON = 'The meeting has been terminated';
15
+
16
+local end_conference_component = module:get_option_string('end_conference_component', 'endconference.'..module.host);
17
+if end_conference_component == nil then
18
+    log('error', 'No end_conference_component specified.');
19
+    return;
20
+end
21
+
22
+-- Advertise end conference so client can pick up the address and use it
23
+module:add_identity('component', 'end_conference', end_conference_component);
24
+
25
+module:depends("jitsi_session");
26
+
27
+local muc_component_host = module:get_option_string('muc_component');
28
+if muc_component_host == nil then
29
+    log('error', 'No muc_component specified. No muc to operate on!');
30
+    return;
31
+end
32
+
33
+module:log('info', 'Starting end_conference for %s', muc_component_host);
34
+
35
+-- receives messages from clients to the component to end a conference
36
+function on_message(event)
37
+    local session = event.origin;
38
+
39
+    -- Check the type of the incoming stanza to avoid loops:
40
+    if event.stanza.attr.type == 'error' then
41
+        return; -- We do not want to reply to these, so leave.
42
+    end
43
+
44
+    if not session or not session.jitsi_web_query_room then
45
+        return false;
46
+    end
47
+
48
+    local moderation_command = event.stanza:get_child('end_conference');
49
+
50
+    if moderation_command then
51
+        -- get room name with tenant and find room
52
+        local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
53
+
54
+        if not room then
55
+            module:log('warn', 'No room found found for %s/%s',
56
+                    session.jitsi_web_query_prefix, session.jitsi_web_query_room);
57
+            return false;
58
+        end
59
+
60
+        -- check that the participant requesting is a moderator and is an occupant in the room
61
+        local from = event.stanza.attr.from;
62
+        local occupant = room:get_occupant_by_real_jid(from);
63
+        if not occupant then
64
+            log('warn', 'No occupant %s found for %s', from, room.jid);
65
+            return false;
66
+        end
67
+        if occupant.role ~= 'moderator' then
68
+            log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
69
+            return false;
70
+        end
71
+
72
+        -- destroy the room
73
+        room:destroy(nil, END_CONFERENCE_REASON);
74
+        log('info', 'Room %s destroyed by occupant %s', room.jid, from);
75
+        return true;
76
+    end
77
+
78
+    -- return error
79
+    return false
80
+end
81
+
82
+
83
+-- we will receive messages from the clients
84
+module:hook('message/host', on_message);

+ 3
- 5
resources/prosody-plugins/mod_muc_breakout_rooms.lua Целия файл

@@ -213,7 +213,7 @@ end
213 213
 function destroy_breakout_room(room_jid, message)
214 214
     local main_room, main_room_jid = get_main_room(room_jid);
215 215
 
216
-    if room_jid == main_room_jid or not main_room then
216
+    if room_jid == main_room_jid then
217 217
         return;
218 218
     end
219 219
 
@@ -221,7 +221,7 @@ function destroy_breakout_room(room_jid, message)
221 221
 
222 222
     if breakout_room then
223 223
         message = message or 'Breakout room removed.';
224
-        breakout_room:destroy(main_room_jid, message);
224
+        breakout_room:destroy(main_room and main_room_jid or nil, message);
225 225
     end
226 226
     if main_room then
227 227
         if main_room._data.breakout_rooms then
@@ -418,10 +418,8 @@ function on_main_room_destroyed(event)
418 418
         return;
419 419
     end
420 420
 
421
-    local message = 'Conference ended.';
422
-
423 421
     for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
424
-        destroy_breakout_room(breakout_room_jid, message)
422
+        destroy_breakout_room(breakout_room_jid, event.reason)
425 423
     end
426 424
 end
427 425
 

Loading…
Отказ
Запис