Просмотр исходного кода

feat(conference) add end conference

Add the ability (for moderators) to end the meeting for everyone.
factor2
wfleischer 2 лет назад
Родитель
Сommit
09efaecc41
Аккаунт пользователя с таким Email не найден
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
     }
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
 .profile-button-avatar {
151
 .profile-button-avatar {
138
     align-items: center;
152
     align-items: center;
139
 }
153
 }

+ 2
- 0
css/_variables.scss Просмотреть файл

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

+ 5
- 0
doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example Просмотреть файл

52
     av_moderation_component = "avmoderation.jitmeet.example.com"
52
     av_moderation_component = "avmoderation.jitmeet.example.com"
53
     speakerstats_component = "speakerstats.jitmeet.example.com"
53
     speakerstats_component = "speakerstats.jitmeet.example.com"
54
     conference_duration_component = "conferenceduration.jitmeet.example.com"
54
     conference_duration_component = "conferenceduration.jitmeet.example.com"
55
+    end_conference_component = "endconference.jitmeet.example.com"
55
     -- we need bosh
56
     -- we need bosh
56
     modules_enabled = {
57
     modules_enabled = {
57
         "bosh";
58
         "bosh";
60
         "speakerstats";
61
         "speakerstats";
61
         "external_services";
62
         "external_services";
62
         "conference_duration";
63
         "conference_duration";
64
+        "end_conference";
63
         "muc_lobby_rooms";
65
         "muc_lobby_rooms";
64
         "muc_breakout_rooms";
66
         "muc_breakout_rooms";
65
         "av_moderation";
67
         "av_moderation";
123
 Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
125
 Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
124
     muc_component = "conference.jitmeet.example.com"
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
 Component "avmoderation.jitmeet.example.com" "av_moderation_component"
131
 Component "avmoderation.jitmeet.example.com" "av_moderation_component"
127
     muc_component = "conference.jitmeet.example.com"
132
     muc_component = "conference.jitmeet.example.com"
128
 
133
 

+ 2
- 1
lang/main-ar.json Просмотреть файл

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

+ 2
- 1
lang/main-ca.json Просмотреть файл

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

+ 2
- 1
lang/main-de.json Просмотреть файл

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

+ 2
- 1
lang/main-hsb.json Просмотреть файл

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

+ 2
- 1
lang/main-it.json Просмотреть файл

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

+ 2
- 1
lang/main-pt.json Просмотреть файл

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

+ 4
- 1
lang/main.json Просмотреть файл

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

+ 36
- 1
react/features/base/conference/actions.js Просмотреть файл

6
     createStartMutedConfigurationEvent,
6
     createStartMutedConfigurationEvent,
7
     sendAnalytics
7
     sendAnalytics
8
 } from '../../analytics';
8
 } from '../../analytics';
9
+import { appNavigate } from '../../app/actions';
9
 import { endpointMessageReceived } from '../../subtitles';
10
 import { endpointMessageReceived } from '../../subtitles';
10
 import { getReplaceParticipant } from '../config/functions';
11
 import { getReplaceParticipant } from '../config/functions';
11
-import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
12
+import { JITSI_CONNECTION_CONFERENCE_KEY, disconnect } from '../connection';
12
 import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
13
 import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
13
 import {
14
 import {
14
     MEDIA_TYPE,
15
     MEDIA_TYPE,
27
     participantRoleChanged,
28
     participantRoleChanged,
28
     participantUpdated
29
     participantUpdated
29
 } from '../participants';
30
 } from '../participants';
31
+import { toState } from '../redux';
30
 import {
32
 import {
31
     destroyLocalTracks,
33
     destroyLocalTracks,
32
     getLocalTracks,
34
     getLocalTracks,
75
     commonUserLeftHandling,
77
     commonUserLeftHandling,
76
     getConferenceOptions,
78
     getConferenceOptions,
77
     getCurrentConference,
79
     getCurrentConference,
80
+    getConferenceState,
78
     sendLocalParticipant
81
     sendLocalParticipant
79
 } from './functions';
82
 } from './functions';
80
 import logger from './logger';
83
 import logger from './logger';
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
  * Signals that we've been kicked out of the conference.
604
  * Signals that we've been kicked out of the conference.
589
  *
605
  *
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
  * Signals that the lock state of a specific JitsiConference changed.
644
  * Signals that the lock state of a specific JitsiConference changed.
610
  *
645
  *

+ 2
- 5
react/features/base/conference/middleware.any.js Просмотреть файл

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

+ 2
- 2
react/features/conference/components/native/carmode/EndMeetingButton.tsx Просмотреть файл

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

+ 10
- 0
react/features/toolbox/actionTypes.ts Просмотреть файл

29
  */
29
  */
30
 export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
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
  * The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
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
     CLEAR_TOOLBOX_TIMEOUT,
9
     CLEAR_TOOLBOX_TIMEOUT,
10
     FULL_SCREEN_CHANGED,
10
     FULL_SCREEN_CHANGED,
11
     SET_FULL_SCREEN,
11
     SET_FULL_SCREEN,
12
+    SET_HANGUP_MENU_VISIBLE,
12
     SET_OVERFLOW_DRAWER,
13
     SET_OVERFLOW_DRAWER,
13
     SET_OVERFLOW_MENU_VISIBLE,
14
     SET_OVERFLOW_MENU_VISIBLE,
14
     SET_TOOLBAR_HOVERED,
15
     SET_TOOLBAR_HOVERED,
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
  * Shows/hides the overflow menu.
209
  * Shows/hides the overflow menu.
193
  *
210
  *

+ 2
- 9
react/features/toolbox/components/HangupButton.js Просмотреть файл

3
 import _ from 'lodash';
3
 import _ from 'lodash';
4
 
4
 
5
 import { createToolbarEvent, sendAnalytics } from '../../analytics';
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
 import { translate } from '../../base/i18n';
7
 import { translate } from '../../base/i18n';
9
 import { connect } from '../../base/redux';
8
 import { connect } from '../../base/redux';
10
 import { AbstractHangupButton } from '../../base/toolbox/components';
9
 import { AbstractHangupButton } from '../../base/toolbox/components';
44
 
43
 
45
         this._hangup = _.once(() => {
44
         this._hangup = _.once(() => {
46
             sendAnalytics(createToolbarEvent('hangup'));
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 Просмотреть файл

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 Просмотреть файл

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

+ 11
- 0
react/features/toolbox/components/native/styles.js Просмотреть файл

133
         backgroundColor: BaseTheme.palette.ui13
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
     hangupButtonStyles: {
147
     hangupButtonStyles: {
137
         iconStyle: whiteToolbarButtonIcon,
148
         iconStyle: whiteToolbarButtonIcon,
138
         style: {
149
         style: {

+ 38
- 0
react/features/toolbox/components/web/EndConferenceButton.tsx Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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 Просмотреть файл

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
 import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants';
108
 import { VIRTUAL_BACKGROUND_TYPE } from '../../../virtual-background/constants';
109
 import {
109
 import {
110
     setFullScreen,
110
     setFullScreen,
111
+    setHangupMenuVisible,
111
     setOverflowMenuVisible,
112
     setOverflowMenuVisible,
112
     setToolbarHovered,
113
     setToolbarHovered,
113
     showToolbox
114
     showToolbox
127
 import AudioSettingsButton from './AudioSettingsButton';
128
 import AudioSettingsButton from './AudioSettingsButton';
128
 // @ts-ignore
129
 // @ts-ignore
129
 import DockIframeButton from './DockIframeButton';
130
 import DockIframeButton from './DockIframeButton';
131
+import { EndConferenceButton } from './EndConferenceButton';
130
 // @ts-ignore
132
 // @ts-ignore
131
 import FullscreenButton from './FullscreenButton';
133
 import FullscreenButton from './FullscreenButton';
134
+import HangupMenuButton from './HangupMenuButton';
135
+import { LeaveConferenceButton } from './LeaveConferenceButton';
132
 // @ts-ignore
136
 // @ts-ignore
133
 import LinkToSalesforceButton from './LinkToSalesforceButton';
137
 import LinkToSalesforceButton from './LinkToSalesforceButton';
134
 // @ts-ignore
138
 // @ts-ignore
199
      */
203
      */
200
     _disabled: boolean,
204
     _disabled: boolean,
201
 
205
 
206
+    /**
207
+     * Whether the end conference feature is supported.
208
+     */
209
+    _endConferenceSupported: boolean,
210
+
202
     /**
211
     /**
203
      * Whether or not call feedback can be sent.
212
      * Whether or not call feedback can be sent.
204
      */
213
      */
214
      */
223
      */
215
     _gifsEnabled: boolean,
224
     _gifsEnabled: boolean,
216
 
225
 
226
+    /**
227
+     * Whether the hangup menu is visible.
228
+     */
229
+    _hangupMenuVisible: boolean,
230
+
217
     /**
231
     /**
218
      * Whether the app has Salesforce integration.
232
      * Whether the app has Salesforce integration.
219
      */
233
      */
220
     _hasSalesforce: boolean,
234
     _hasSalesforce: boolean,
221
 
235
 
222
-    /**
236
+     /**
223
      * Whether or not the app is running in an ios mobile browser.
237
      * Whether or not the app is running in an ios mobile browser.
224
      */
238
      */
225
     _isIosMobile: boolean,
239
     _isIosMobile: boolean,
335
             right: 'auto',
349
             right: 'auto',
336
             maxHeight: 'inherit',
350
             maxHeight: 'inherit',
337
             margin: 0
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
         // Bind event handlers so they are only bound once per instance.
380
         // Bind event handlers so they are only bound once per instance.
358
         this._onMouseOut = this._onMouseOut.bind(this);
381
         this._onMouseOut = this._onMouseOut.bind(this);
359
         this._onMouseOver = this._onMouseOver.bind(this);
382
         this._onMouseOver = this._onMouseOver.bind(this);
383
+        this._onSetHangupVisible = this._onSetHangupVisible.bind(this);
360
         this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
384
         this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
361
         this._onTabIn = this._onTabIn.bind(this);
385
         this._onTabIn = this._onTabIn.bind(this);
362
 
386
 
482
      * @inheritdoc
506
      * @inheritdoc
483
      */
507
      */
484
     componentDidUpdate(prevProps: Props) {
508
     componentDidUpdate(prevProps: Props) {
485
-        const { _dialog, dispatch } = this.props;
509
+        const { _dialog, _visible, dispatch } = this.props;
486
 
510
 
487
 
511
 
488
         if (prevProps._overflowMenuVisible
512
         if (prevProps._overflowMenuVisible
491
             this._onSetOverflowVisible(false);
515
             this._onSetOverflowVisible(false);
492
             dispatch(setToolbarHovered(false));
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
     }
565
     }
536
 
566
 
537
     /**
567
     /**
538
-     * Key handler for overflow menu.
568
+     * Key handler for overflow/hangup menus.
539
      *
569
      *
540
      * @param {KeyboardEvent} e - Esc key click to close the popup.
570
      * @param {KeyboardEvent} e - Esc key click to close the popup.
541
      * @returns {void}
571
      * @returns {void}
543
     _onEscKey(e: React.KeyboardEvent) {
573
     _onEscKey(e: React.KeyboardEvent) {
544
         if (e.key === 'Escape') {
574
         if (e.key === 'Escape') {
545
             e.stopPropagation();
575
             e.stopPropagation();
576
+            this._closeHangupMenuIfOpen();
546
             this._closeOverflowMenuIfOpen();
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
      * Closes the overflow menu if opened.
594
      * Closes the overflow menu if opened.
552
      *
595
      *
1012
         this.props.dispatch(setToolbarHovered(true));
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
      * Sets the visibility of the overflow menu.
1072
      * Sets the visibility of the overflow menu.
1017
      *
1073
      *
1307
      */
1363
      */
1308
     _renderToolboxContent() {
1364
     _renderToolboxContent() {
1309
         const {
1365
         const {
1366
+            _endConferenceSupported,
1367
+            _hangupMenuVisible,
1310
             _isMobile,
1368
             _isMobile,
1311
             _overflowDrawer,
1369
             _overflowDrawer,
1312
             _overflowMenuVisible,
1370
             _overflowMenuVisible,
1383
                             </OverflowMenuButton>
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
                     </div>
1468
                     </div>
1393
                 </div>
1469
                 </div>
1394
             </div>
1470
             </div>
1407
  */
1483
  */
1408
 function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1484
 function _mapStateToProps(state: any, ownProps: Partial<Props>) {
1409
     const { conference } = state['features/base/conference'];
1485
     const { conference } = state['features/base/conference'];
1486
+    const endConferenceSupported = conference?.isEndConferenceSupported();
1410
     const {
1487
     const {
1411
         buttonsWithNotifyClick,
1488
         buttonsWithNotifyClick,
1412
         callStatsID,
1489
         callStatsID,
1416
     } = state['features/base/config'];
1493
     } = state['features/base/config'];
1417
     const {
1494
     const {
1418
         fullScreen,
1495
         fullScreen,
1496
+        hangupMenuVisible,
1419
         overflowMenuVisible,
1497
         overflowMenuVisible,
1420
         overflowDrawer
1498
         overflowDrawer
1421
     } = state['features/toolbox'];
1499
     } = state['features/toolbox'];
1434
         _desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
1512
         _desktopSharingButtonDisabled: isDesktopShareButtonDisabled(state),
1435
         _dialog: Boolean(state['features/base/dialog'].component),
1513
         _dialog: Boolean(state['features/base/dialog'].component),
1436
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1514
         _disabled: Boolean(iAmRecorder || iAmSipGateway),
1515
+        _endConferenceSupported: Boolean(endConferenceSupported),
1437
         _feedbackConfigured: Boolean(callStatsID),
1516
         _feedbackConfigured: Boolean(callStatsID),
1438
         _fullScreen: fullScreen,
1517
         _fullScreen: fullScreen,
1439
         _gifsEnabled: isGifEnabled(state),
1518
         _gifsEnabled: isGifEnabled(state),
1442
         _isMobile: isMobileBrowser(),
1521
         _isMobile: isMobileBrowser(),
1443
         _isVpaasMeeting: isVpaasMeeting(state),
1522
         _isVpaasMeeting: isVpaasMeeting(state),
1444
         _hasSalesforce: isSalesforceEnabled(state),
1523
         _hasSalesforce: isSalesforceEnabled(state),
1524
+        _hangupMenuVisible: hangupMenuVisible,
1445
         _localParticipantID: localParticipant?.id,
1525
         _localParticipantID: localParticipant?.id,
1446
         _localVideo: localVideo,
1526
         _localVideo: localVideo,
1447
         _multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state),
1527
         _multiStreamModeEnabled: getMultipleVideoSendingSupportFeatureFlag(state),

+ 14
- 0
react/features/toolbox/reducer.js Просмотреть файл

5
 import {
5
 import {
6
     CLEAR_TOOLBOX_TIMEOUT,
6
     CLEAR_TOOLBOX_TIMEOUT,
7
     FULL_SCREEN_CHANGED,
7
     FULL_SCREEN_CHANGED,
8
+    SET_HANGUP_MENU_VISIBLE,
8
     SET_OVERFLOW_DRAWER,
9
     SET_OVERFLOW_DRAWER,
9
     SET_OVERFLOW_MENU_VISIBLE,
10
     SET_OVERFLOW_MENU_VISIBLE,
10
     SET_TOOLBAR_HOVERED,
11
     SET_TOOLBAR_HOVERED,
26
      */
27
      */
27
     enabled: true,
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
      * The indicator which determines whether a Toolbar in the Toolbox is
38
      * The indicator which determines whether a Toolbar in the Toolbox is
31
      * hovered.
39
      * hovered.
81
                 fullScreen: action.fullScreen
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
         case SET_OVERFLOW_DRAWER:
98
         case SET_OVERFLOW_DRAWER:
85
             return {
99
             return {
86
                 ...state,
100
                 ...state,

+ 84
- 0
resources/prosody-plugins/mod_end_conference.lua Просмотреть файл

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
 function destroy_breakout_room(room_jid, message)
213
 function destroy_breakout_room(room_jid, message)
214
     local main_room, main_room_jid = get_main_room(room_jid);
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
         return;
217
         return;
218
     end
218
     end
219
 
219
 
221
 
221
 
222
     if breakout_room then
222
     if breakout_room then
223
         message = message or 'Breakout room removed.';
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
     end
225
     end
226
     if main_room then
226
     if main_room then
227
         if main_room._data.breakout_rooms then
227
         if main_room._data.breakout_rooms then
418
         return;
418
         return;
419
     end
419
     end
420
 
420
 
421
-    local message = 'Conference ended.';
422
-
423
     for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
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
     end
423
     end
426
 end
424
 end
427
 
425
 

Загрузка…
Отмена
Сохранить