mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-30 02:20:20 +00:00
Compare commits
37 Commits
release-89
...
debug-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
417c38ab9e | ||
|
|
33a4245a1f | ||
|
|
2eb07cb79f | ||
|
|
63e4c41d92 | ||
|
|
2c6ccd7d6b | ||
|
|
4ce27eeb1a | ||
|
|
11453dcc79 | ||
|
|
3375ee49bd | ||
|
|
e45df58cfb | ||
|
|
7d628960d7 | ||
|
|
cf13b8f0ba | ||
|
|
e106109090 | ||
|
|
fc170891cb | ||
|
|
775cc52f66 | ||
|
|
3baede6ff1 | ||
|
|
9462a9ce36 | ||
|
|
4713062200 | ||
|
|
65eb2a2899 | ||
|
|
f0452d05b9 | ||
|
|
1102f4205a | ||
|
|
447def54c8 | ||
|
|
481b9a6e58 | ||
|
|
fb3bc3c367 | ||
|
|
9fa5489154 | ||
|
|
9499bf29ed | ||
|
|
f605b5c487 | ||
|
|
88fba5acab | ||
|
|
7bc79bc144 | ||
|
|
3e469019b5 | ||
|
|
d324935501 | ||
|
|
cd11cf6f65 | ||
|
|
5db3d529f4 | ||
|
|
c7d2c9c204 | ||
|
|
9832c7a226 | ||
|
|
12ee929499 | ||
|
|
b9ed42613b | ||
|
|
0d572b3bfb |
2
.github/workflows/ci-lua.yml
vendored
2
.github/workflows/ci-lua.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
name: Luacheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
|
||||
- name: Install luarocks
|
||||
run: sudo apt-get --install-recommends -y install luarocks
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -7,8 +7,8 @@ jobs:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -42,8 +42,8 @@ jobs:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -59,8 +59,8 @@ jobs:
|
||||
name: Build mobile bundle (Android)
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -74,8 +74,8 @@ jobs:
|
||||
name: Build mobile bundle (iOS)
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -116,8 +116,8 @@ jobs:
|
||||
rm -rf /host/usr/share/dotnet
|
||||
rm -rf /host/usr/share/swift
|
||||
df -h /
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -137,8 +137,8 @@ jobs:
|
||||
name: Build mobile SDK (iOS)
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
@@ -160,7 +160,7 @@ jobs:
|
||||
-workspace ios/jitsi-meet.xcworkspace \
|
||||
-scheme JitsiMeetSDK
|
||||
- name: setup-cocoapods
|
||||
uses: ruby/setup-ruby@v1
|
||||
uses: ruby/setup-ruby@4fc31e1c823882afd7ef55985266a526c589de90 #v1.282.0
|
||||
with:
|
||||
ruby-version: '3.4'
|
||||
bundler-cache: true
|
||||
@@ -187,8 +187,8 @@ jobs:
|
||||
name: Test Debian packages build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f #v6.1.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 #v10.1.0
|
||||
with:
|
||||
stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
stale-pr-message: 'This PR has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.'
|
||||
|
||||
@@ -264,4 +264,4 @@ react/features/sample/
|
||||
- [Jitsi Handbook](https://jitsi.github.io/handbook/) - Comprehensive documentation
|
||||
- [Community Forum](https://community.jitsi.org/) - Ask questions and get support
|
||||
- [Architecture Guide](https://jitsi.github.io/handbook/docs/architecture) - System overview
|
||||
- [Contributing Guidelines](https://jitsi.github.io/handbook/docs/dev-guide/contributing) - Detailed contribution process
|
||||
- [Contributing Guidelines](https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-contributing/) - Detailed contribution process
|
||||
|
||||
@@ -30,6 +30,9 @@ import com.facebook.react.ReactRootView;
|
||||
|
||||
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public class JitsiMeetView extends FrameLayout {
|
||||
|
||||
@@ -84,6 +87,10 @@ public class JitsiMeetView extends FrameLayout {
|
||||
result.putInt(key, (int)bValue);
|
||||
} else if (valueType.contentEquals("Bundle")) {
|
||||
result.putBundle(key, mergeProps((Bundle)aValue, (Bundle)bValue));
|
||||
} else if (valueType.contentEquals("String[]")) {
|
||||
// Convert String[] to ArrayList<String> for React Native bridge compatibility
|
||||
String[] stringArray = (String[]) bValue;
|
||||
result.putStringArrayList(key, new ArrayList<>(Arrays.asList(stringArray)));
|
||||
} else {
|
||||
throw new RuntimeException("Unsupported type: " + valueType);
|
||||
}
|
||||
|
||||
@@ -927,6 +927,9 @@ var config = {
|
||||
// [ 'microphone', 'camera' ]
|
||||
// ],
|
||||
|
||||
// Enable reduced UI on web.
|
||||
// reducedUIEnabled: true,
|
||||
|
||||
// Overrides the buttons displayed in the main toolbar for reduced UI.
|
||||
// When there isn't an override for a certain configuration the default jitsi-meet configuration will be used.
|
||||
// The order of the buttons in the array is preserved.
|
||||
|
||||
@@ -1407,7 +1407,7 @@ PODS:
|
||||
- Yoga
|
||||
- react-native-performance (5.1.2):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (5.5.2):
|
||||
- react-native-safe-area-context (5.6.1):
|
||||
- React-Core
|
||||
- react-native-slider (4.5.6):
|
||||
- DoubleConversion
|
||||
@@ -2271,7 +2271,7 @@ SPEC CHECKSUMS:
|
||||
react-native-orientation-locker: dbd3f6ddbe9e62389cb0807dc2af63f6c36dec36
|
||||
react-native-pager-view: 11662c698c8f11d39e05891316d2a144fa00adc4
|
||||
react-native-performance: 125a96c145e29918b55b45ce25cbba54f1e24dcd
|
||||
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
|
||||
react-native-safe-area-context: 2243039f43d10cb1ea30ec5ac57fc6d1448413f4
|
||||
react-native-slider: 1205801a8d29b28cacc14eef08cb120015cdafcb
|
||||
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
|
||||
react-native-webrtc: e8f0ce746353adc2744a2b933645e1aeb41eaa74
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"it": "Italiano",
|
||||
"ja": "日本語",
|
||||
"kab": "Taqbaylit",
|
||||
"kk": "Қазақша",
|
||||
"ko": "한국어",
|
||||
"lt": "Lietuvių",
|
||||
"lv": "Latviešu",
|
||||
|
||||
1895
lang/main-da.json
1895
lang/main-da.json
File diff suppressed because it is too large
Load Diff
@@ -126,8 +126,16 @@
|
||||
"messagebox": "Digita un messaggio",
|
||||
"newMessages": "Nuovi messaggi",
|
||||
"nickname": {
|
||||
"featureChat": "la chat",
|
||||
"featureClosedCaptions": "i sottotitoli",
|
||||
"featureFileSharing": "la condivisione file",
|
||||
"featurePolls": "i sondaggi",
|
||||
"popover": "Scegli un nickname",
|
||||
"title": "Inserisci un nickname per usare la chat",
|
||||
"titleWith1Features": "Inserisci un nickname per usare {{feature1}}",
|
||||
"titleWith2Features": "Inserisci un nickname per usare {{feature1}} e {{feature2}}",
|
||||
"titleWith3Features": "Inserisci un nickname per usare {{feature1}}, {{feature2}} e {{feature3}}",
|
||||
"titleWith4Features": "Inserisci un nickname per usare {{feature1}}, {{feature2}}, {{feature3}} e {{feature4}}",
|
||||
"titleWithCC": "Inserisci un nickname per usare la chat e i sottotitoli",
|
||||
"titleWithPolls": "Inserisci un nickname per usare la chat e i sondaggi",
|
||||
"titleWithPollsAndCC": "Inserisci un nickname per usare la chat, i sondaggi e i sottotitoli",
|
||||
@@ -159,7 +167,7 @@
|
||||
"installExtensionText": "Installa un'estensione per integrare Google Calendar e Office 365"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che l'organizzatore lo attiverà",
|
||||
"emptyState": "Il contenuto dei sottotitoli sarà disponibile una volta che un moderatore lo attiverà",
|
||||
"startClosedCaptionsButton": "Attiva sottotitoli"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
@@ -283,9 +291,9 @@
|
||||
"Submit": "Invia",
|
||||
"Understand": "Accetto, mantieni microfono e videocamera disattivati per ora",
|
||||
"UnderstandAndUnmute": "Accetto, riattiva microfono e videocamera",
|
||||
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun organizzatore si è ancora collegato. Si prega di attendere.",
|
||||
"WaitingForHostButton": "Attendi l'organizzatore",
|
||||
"WaitingForHostTitle": "In attesa dell'organizzatore…",
|
||||
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun moderatore si è ancora collegato. Si prega di attendere.",
|
||||
"WaitingForHostButton": "Attendi un moderatore",
|
||||
"WaitingForHostTitle": "In attesa di un moderatore…",
|
||||
"Yes": "Sì",
|
||||
"accessibilityLabel": {
|
||||
"Cancel": "Annulla (chiudi dialogo)",
|
||||
@@ -348,8 +356,8 @@
|
||||
"error": "Errore",
|
||||
"errorRoomCreationRestriction": "Hai provato ad accedere alla riunione troppo presto, torna tra un po'.",
|
||||
"gracefulShutdown": "Il nostro servizio è al momento inattivo per manutenzione. Si prega di riprovare più tardi.",
|
||||
"grantModeratorDialog": "Vuoi rendere relatore questo partecipante?",
|
||||
"grantModeratorTitle": "Fai diventare relatore",
|
||||
"grantModeratorDialog": "Desideri di concedere i permessi da moderatore a {{participantName}}?",
|
||||
"grantModeratorTitle": "Concedi permessi da moderatore",
|
||||
"hide": "Nascondi",
|
||||
"hideShareAudioHelper": "Non mostrare più questa finestra",
|
||||
"incorrectPassword": "Nome utente o password errati",
|
||||
@@ -727,12 +735,12 @@
|
||||
"emailField": "Inserisci il tuo indirizzo email",
|
||||
"enableDialogPasswordField": "Imposta password (opzionale)",
|
||||
"enableDialogSubmit": "Attiva",
|
||||
"enableDialogText": "La sala d'attesa ti permette di proteggere la riunione concedendo l'accesso solo alle persone autorizzate da un organizzatore.",
|
||||
"enableDialogText": "La sala d'attesa ti permette di proteggere la riunione concedendo l'accesso solo alle persone autorizzate da un moderatore.",
|
||||
"enterPasswordButton": "Inserisci password riunione",
|
||||
"enterPasswordTitle": "Inserisci la password per entrare nella riunione",
|
||||
"errorMissingPassword": "Inserisci la password della riunione",
|
||||
"invalidPassword": "Password errata",
|
||||
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un organizzatore.",
|
||||
"joinRejectedMessage": "La tua richiesta d'accesso è stata respinta da un moderatore.",
|
||||
"joinRejectedTitle": "Richiesta d'accesso respinta.",
|
||||
"joinTitle": "Entra nella riunione",
|
||||
"joinWithPasswordMessage": "Tentativo di accesso con password in corso, si prega di attendere…",
|
||||
@@ -754,7 +762,7 @@
|
||||
"passwordJoinButton": "Entra",
|
||||
"title": "Sala d'attesa",
|
||||
"toggleLabel": "Attiva sala d'attesa",
|
||||
"waitForModerator": "La riunione non è ancora iniziata, perché non è arrivato alcun organizzatore. Se vuoi diventarlo autenticati, altrimenti attendi."
|
||||
"waitForModerator": "La riunione non è ancora iniziata, perché non è arrivato alcun moderatore. Se vuoi diventarlo autenticati, altrimenti si prega di attendere."
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -772,11 +780,11 @@
|
||||
"me": "Io",
|
||||
"messages": {
|
||||
"engaged": "Registrazione avviata.",
|
||||
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione all'organizzatore.",
|
||||
"finishedModerator": "La registrazione della sessione {{token}} è terminata. La registrazione della traccia è stata salvata. Chiedi ai partecipanti di inviare le loro registrazioni.",
|
||||
"notModerator": "Non sei un relatore. Non puoi avviare o interrompere la registrazione."
|
||||
"finished": "La registrazione della sessione {{token}} è terminata. Invia il file della registrazione al moderatore.",
|
||||
"finishedModerator": "La registrazione della sessione {{token}} è terminata. La registrazione della traccia locale è stata salvata. Si prega di chiedere ai partecipanti di inviare le loro registrazioni.",
|
||||
"notModerator": "Non sei un moderatore. Non puoi avviare o interrompere la registrazione."
|
||||
},
|
||||
"moderator": "Relatore",
|
||||
"moderator": "Moderatore",
|
||||
"no": "No",
|
||||
"participant": "Partecipante",
|
||||
"participantStats": "Statistiche partecipante",
|
||||
@@ -824,7 +832,7 @@
|
||||
"focusFail": "{{component}} non disponibile - nuovo tentativo tra {{ms}} sec",
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "Notifiche",
|
||||
"hostAskedUnmute": "Il relatore ti chiede di intervenire.",
|
||||
"hostAskedUnmute": "Il moderatore ti chiede di intervenire.",
|
||||
"invalidTenant": "Nome non valido",
|
||||
"invalidTenantHyphenDescription": "Il nome che hai scelto non è valido (inizia o finisce con '-').",
|
||||
"invalidTenantLengthDescription": "Il nome che hai scelto è troppo lungo.",
|
||||
@@ -846,17 +854,17 @@
|
||||
"localRecordingStopped": "{{name}} ha smesso di registrare.",
|
||||
"me": "Io",
|
||||
"moderationInEffectCSDescription": "Alza la mano, se vuoi condividere lo schermo.",
|
||||
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal relatore",
|
||||
"moderationInEffectCSTitle": "La condivisione schermo è stata bloccata dal moderatore",
|
||||
"moderationInEffectDescription": "Alza la mano, se vuoi prendere la parola.",
|
||||
"moderationInEffectTitle": "Il tuo microfono è stato spento dal relatore",
|
||||
"moderationInEffectTitle": "Il tuo microfono è stato spento dal moderatore",
|
||||
"moderationInEffectVideoDescription": "Alza la mano, se vuoi avviare la tua videocamera.",
|
||||
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dal relatore",
|
||||
"moderationRequestFromModerator": "Il relatore vorrebbe che tu accendessi il microfono",
|
||||
"moderationInEffectVideoTitle": "La tua videocamera è stata spenta dal moderatore",
|
||||
"moderationRequestFromModerator": "L'organizzatore ti chiede di accendere il microfono",
|
||||
"moderationRequestFromParticipant": "Vuole parlare",
|
||||
"moderationStartedTitle": "Moderazione in corso",
|
||||
"moderationStoppedTitle": "Moderazione interrotta",
|
||||
"moderationToggleDescription": "da {{participantDisplayName}}",
|
||||
"moderator": "Ora sei un relatore!",
|
||||
"moderator": "Ora sei un moderatore!",
|
||||
"muted": "Hai iniziato la conversazione con il microfono disattivato.",
|
||||
"mutedRemotelyDescription": "Puoi sempre attivare il microfono quando vuoi parlare. Spegni il microfono quando hai finito, per evitare rumori di fondo nella riunione.",
|
||||
"mutedRemotelyTitle": "{{participantDisplayName}} ti ha spento il microfono",
|
||||
@@ -1162,7 +1170,7 @@
|
||||
},
|
||||
"security": {
|
||||
"about": "Puoi aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
|
||||
"aboutReadOnly": "Gli organizzatori possono aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
|
||||
"aboutReadOnly": "I moderatori possono aggiungere una $t(lockRoomPassword) alla riunione. I partecipanti dovranno inserire la $t(lockRoomPassword) per accedere alla riunione.",
|
||||
"insecureRoomNameWarningNative": "Il nome della riunione è troppo semplice. Partecipanti non desiderati potrebbero entrare nella riunione. {{recommendAction}} Per saperne di più sulla sicurezza della tua riunione ",
|
||||
"insecureRoomNameWarningWeb": "Il nome della riunione è troppo semplice. Partecipanti non desiderati potrebbero entrare nella riunione. {{recommendAction}} Per saperne di più sulla sicurezza della tua riunione <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">clicca qui</a>.",
|
||||
"title": "Impostazioni di sicurezza",
|
||||
@@ -1182,7 +1190,7 @@
|
||||
"signedIn": "Accesso agli eventi del calendario per {{email}} in corso. Clicca sul pulsante Disconnetti per interrompere l’accesso agli eventi sul calendario.",
|
||||
"title": "Calendario"
|
||||
},
|
||||
"chatWithPermissions": "La chat richiede il permesso",
|
||||
"chatWithPermissions": "Disattiva la chat per i partecipanti",
|
||||
"desktopShareFramerate": "Frequenza di aggiornamento condivisone schermo",
|
||||
"desktopShareHighFpsWarning": "Una frequenza di aggiornamento della condivisione dello schermo più alta può influire sulla tua connessione. Devi riavviare la condivisione schermo, per applicare le modifiche.",
|
||||
"desktopShareWarning": "Devi riavviare la condivisione schermo, per applicare le modifiche.",
|
||||
@@ -1195,8 +1203,8 @@
|
||||
"loggedIn": "Connesso come {{name}}",
|
||||
"maxStageParticipants": "Numero massimo di partecipanti che possono essere messi in evidenza nella schermata principale",
|
||||
"microphones": "Microfoni",
|
||||
"moderator": "Relatore",
|
||||
"moderatorOptions": "Opzioni relatore",
|
||||
"moderator": "Moderatore",
|
||||
"moderatorOptions": "Opzioni moderatore",
|
||||
"more": "Generali",
|
||||
"name": "Nome",
|
||||
"noDevice": "Nessuno",
|
||||
@@ -1325,7 +1333,7 @@
|
||||
"feedback": "Lascia un feedback",
|
||||
"fullScreen": "Attiva modalità a schermo intero",
|
||||
"giphy": "Mostra menu GIPHY",
|
||||
"grantModerator": "Concedi permessi da relatore",
|
||||
"grantModerator": "Concedi permessi da moderatore",
|
||||
"hangup": "Lascia la riunione",
|
||||
"heading": "Barra degli strumenti",
|
||||
"help": "Aiuto",
|
||||
@@ -1350,7 +1358,7 @@
|
||||
"muteEveryoneElsesVideoStream": "Spegni la videocamera a tutti gli altri",
|
||||
"muteEveryonesVideoStream": "Spegni la videocamera a tutti",
|
||||
"muteGUMPending": "Connessione del microfono in corso",
|
||||
"noiseSuppression": "Cancellazione del rumore (BETA)",
|
||||
"noiseSuppression": "Cancellazione del rumore",
|
||||
"openChat": "Apri chat",
|
||||
"participants": "Apri pannello partecipanti. {{participantsCount}} partecipanti",
|
||||
"pip": "Attiva modalità Picture-in-Picture",
|
||||
@@ -1415,20 +1423,21 @@
|
||||
"closeParticipantsPane": "Chiudi pannello partecipanti",
|
||||
"closeReactionsMenu": "Chiudi menu reazioni",
|
||||
"closedCaptions": "Sottotitoli",
|
||||
"disableNoiseSuppression": "Disattiva cancellazione del rumore (BETA)",
|
||||
"disableNoiseSuppression": "Disattiva cancellazione del rumore",
|
||||
"disableReactionSounds": "Puoi disattivare i suoni delle reazioni in questa riunione",
|
||||
"documentClose": "Chiudi documento condiviso",
|
||||
"documentOpen": "Apri documento condiviso",
|
||||
"download": "Scarica le nostre app",
|
||||
"e2ee": "Crittografia End-to-End",
|
||||
"embedMeeting": "Incorpora riunione",
|
||||
"enableNoiseSuppression": "Attiva cancellazione del rumore (BETA)",
|
||||
"enableNoiseSuppression": "Attiva cancellazione del rumore",
|
||||
"endConference": "Termina la riunione per tutti",
|
||||
"enterFullScreen": "Mostra a schermo intero",
|
||||
"enterTileView": "Mostra vista a mosaico",
|
||||
"exitFullScreen": "Esci dalla modalità a schermo intero",
|
||||
"exitTileView": "Esci dalla vista a mosaico",
|
||||
"feedback": "Lascia un feedback",
|
||||
"fileSharing": "Condivisione file",
|
||||
"giphy": "Menu GIPHY",
|
||||
"hangup": "Lascia la riunione",
|
||||
"help": "Aiuto",
|
||||
@@ -1457,13 +1466,14 @@
|
||||
"noAudioSignalDialInDesc": "Puoi anche chiamare usando:",
|
||||
"noAudioSignalDialInLinkDesc": "Numeri di telefono",
|
||||
"noAudioSignalTitle": "Nessun suono rilevato dal tuo microfono!",
|
||||
"noiseSuppression": "Cancellazione del rumore (BETA)",
|
||||
"noiseSuppression": "Cancellazione del rumore",
|
||||
"noisyAudioInputDesc": "Sembra che il tuo microfono faccia rumore, si prega di spegnerlo o cambiarlo.",
|
||||
"noisyAudioInputTitle": "Il tuo microfono sembra fare rumore!",
|
||||
"openChat": "Apri chat",
|
||||
"openReactionsMenu": "Apri il menu reazioni",
|
||||
"participants": "Partecipanti",
|
||||
"pip": "Abilita modalità Picture-in-Picture",
|
||||
"polls": "Sondaggi",
|
||||
"privateMessage": "Invia un messaggio privato",
|
||||
"profile": "Modifica profilo",
|
||||
"raiseHand": "Alza la mano",
|
||||
@@ -1565,11 +1575,11 @@
|
||||
"domuteVideo": "Disattiva videocamera",
|
||||
"domuteVideoOfOthers": "Disattiva videocamera a tutti gli altri",
|
||||
"flip": "Specchia",
|
||||
"grantModerator": "Concedi permessi da relatore",
|
||||
"grantModerator": "Concedi permessi da moderatore",
|
||||
"hideSelfView": "Nascondi la tua immagine",
|
||||
"kick": "Espelli",
|
||||
"mirrorVideo": "Specchia il tuo video",
|
||||
"moderator": "Relatore",
|
||||
"moderator": "Moderatore",
|
||||
"mute": "Il partecipante ha il microfono spento",
|
||||
"muted": "Microfono spento",
|
||||
"pinToStage": "Metti in primo piano",
|
||||
@@ -1616,7 +1626,7 @@
|
||||
"description": "Adesso sei uno spettatore in questa riunione.",
|
||||
"raiseHand": "Alza la mano",
|
||||
"title": "Ingresso nella riunione in corso",
|
||||
"wishToSpeak": "Se vuoi parlare, si prega di alzare la mano sotto e aspettare l'autorizzazione del relatore."
|
||||
"wishToSpeak": "Per parlare si prega di alzare la mano sotto e aspettare l'autorizzazione del moderatore."
|
||||
},
|
||||
"labelTooltip": "Numero di spettatori: {{count}}",
|
||||
"notification": {
|
||||
@@ -1626,7 +1636,7 @@
|
||||
"noVisitorLobby": "Non puoi partecipare se la sala d'attesa è attiva per la riunione.",
|
||||
"notAllowedPromotion": "Un partecipante deve autorizzare la tua richiesta prima.",
|
||||
"requestToJoin": "Mano alzata",
|
||||
"requestToJoinDescription": "La tua richiesta è stata inviata ai relatori. Tieni duro!",
|
||||
"requestToJoinDescription": "La tua richiesta è stata inviata ai moderatori. Tieni duro!",
|
||||
"title": "Sei uno spettatore nella riunione"
|
||||
},
|
||||
"waitingMessage": "Ti unirai alla riunione quando inizierà!"
|
||||
@@ -1667,7 +1677,7 @@
|
||||
"mobileDownLoadLinkAndroid": "Scarica applicazione per Android",
|
||||
"mobileDownLoadLinkFDroid": "Scarica applicazione da F-Droid",
|
||||
"mobileDownLoadLinkIos": "Scarica applicazione per iOS",
|
||||
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">prepara un URL di riunione</a> in anticipo, quando sei l'unico organizzatore.",
|
||||
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">prepara un URL di riunione</a> in anticipo, quando sei l'unico moderatore.",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "Recenti",
|
||||
"recentListDelete": "Cancella",
|
||||
|
||||
1703
lang/main-kk.json
Normal file
1703
lang/main-kk.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -114,6 +114,10 @@
|
||||
"error": "Erro: a sua mensagem não foi enviada. Motivo: {{error}}",
|
||||
"everyone": "Todos",
|
||||
"fieldPlaceHolder": "Aa",
|
||||
"fileAccessibleTitle": "{{user}} carregou um ficheiro",
|
||||
"fileAccessibleTitleMe": "carreguei um arquivo",
|
||||
"fileDeleted": "Um ficheiro foi eliminado",
|
||||
"guestsChatIndicator": "(convidado)",
|
||||
"lobbyChatMessageTo": "Mensagem de chat na sala de espera para {{recipient}}",
|
||||
"message": "Mensagem",
|
||||
"messageAccessibleTitle": "{{user}} disse:",
|
||||
@@ -122,8 +126,16 @@
|
||||
"messagebox": "Escreva uma mensagem",
|
||||
"newMessages": "Novas mensagens",
|
||||
"nickname": {
|
||||
"featureChat": "chat",
|
||||
"featureClosedCaptions": "legendas ocultas",
|
||||
"featureFileSharing": "partilha de ficheiros",
|
||||
"featurePolls": "sondagens",
|
||||
"popover": "Escolha um apelido",
|
||||
"title": "Introduza um apelido para usar o chat",
|
||||
"titleWith1Features": "Insira um apelido para usar {{feature1}}",
|
||||
"titleWith2Features": "Insira um apelido para usar {{feature1}} e {{feature2}}",
|
||||
"titleWith3Features": "Insira um apelido para usar {{feature1}}, {{feature2}} e {{feature3}}",
|
||||
"titleWith4Features": "Insira um apelido para usar {{feature1}}, {{feature2}}, {{feature3}} e {{feature4}}",
|
||||
"titleWithCC": "Insira um apelido para usar o chat e as legendas ocultas",
|
||||
"titleWithPolls": "Digite um apelido para usar o chat e as sondagens",
|
||||
"titleWithPollsAndCC": "Insira um apelido para utilizar o chat, as sondagens e as legendas ocultas",
|
||||
@@ -279,7 +291,6 @@
|
||||
"Submit": "Submeter",
|
||||
"Understand": "Entendo, mantenha-me em silêncio por enquanto.",
|
||||
"UnderstandAndUnmute": "Entendo, por favor, desative o silêncio.",
|
||||
"WaitForHostMsg": "A conferência ainda não começou porque ainda não chegaram moderadores. Se quiser ser um moderador, inicie a sessão. Caso contrário, aguarde.",
|
||||
"WaitForHostNoAuthMsg": "A conferência ainda não começou porque ainda não chegaram os moderadores. Por favor, aguarde.",
|
||||
"WaitingForHostButton": "Esperar pelo moderador",
|
||||
"WaitingForHostTitle": "À espera de um moderador…",
|
||||
@@ -522,6 +533,7 @@
|
||||
"tokenAuthFailedWithReasons": "Lamentamos, mas não está autorizado a participar nesta chamada. Razões possíveis: {{reason}}",
|
||||
"tokenAuthUnsupported": "O URL de token não é suportado.",
|
||||
"transcribing": "Transcrição",
|
||||
"unauthenticatedAccessDisabled": "Esta chamada requer autenticação. Por favor, inicie sessão para prosseguir.",
|
||||
"unlockRoom": "Retirar reunião $t(lockRoomPassword)",
|
||||
"user": "Utilizador",
|
||||
"userIdentifier": "Identificador do utilizador",
|
||||
@@ -566,13 +578,17 @@
|
||||
"downloadFailedDescription": "Por favor, tente novamente.",
|
||||
"downloadFailedTitle": "Falha no descarregar",
|
||||
"downloadFile": "Descarregar",
|
||||
"downloadStarted": "O download do ficheiro foi iniciado",
|
||||
"dragAndDrop": "Arraste e solte os ficheiros aqui ou em qualquer lugar do ecrã",
|
||||
"fileAlreadyUploaded": "O ficheiro já foi carregado para esta reunião.",
|
||||
"fileRemovedByOther": "O seu ficheiro '{{ fileName }}' foi removido",
|
||||
"fileTooLargeDescription": "Certifique-se de que o ficheiro não exceda {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "O ficheiro selecionado é muito grande",
|
||||
"fileUploadProgress": "Progresso do envio do ficheiro",
|
||||
"fileUploadedSuccessfully": "Ficheiro carregado com sucesso",
|
||||
"newFileNotification": "{{ participantName }} partilhou '{{ fileName }}'",
|
||||
"removeFile": "Remover",
|
||||
"removeFileSuccess": "Ficheiro removido com sucesso",
|
||||
"uploadFailedDescription": "Por favor, tente novamente.",
|
||||
"uploadFailedTitle": "Falha ao carregar",
|
||||
"uploadFile": "Partilhar ficheiro"
|
||||
@@ -745,7 +761,8 @@
|
||||
"notificationTitle": "Sala de espera",
|
||||
"passwordJoinButton": "Solicitar",
|
||||
"title": "Sala de espera",
|
||||
"toggleLabel": "Ativar sala de espera"
|
||||
"toggleLabel": "Ativar sala de espera",
|
||||
"waitForModerator": "A conferência ainda não começou porque não chegou nenhum moderador. Se deseja tornar-se um moderador, faça login. Caso contrário, aguarde."
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -862,6 +879,7 @@
|
||||
"oldElectronClientDescription1": "Parece estar a utilizar uma versão antiga do cliente Jitsi Meet que tem vulnerabilidades de segurança conhecidas. Por favor, certifique-se de que actualiza a nossa ",
|
||||
"oldElectronClientDescription2": "compilação mais recente",
|
||||
"oldElectronClientDescription3": " agora!",
|
||||
"openChat": "Abrir chat",
|
||||
"participantWantsToJoin": "Deseja juntar-se à reunião",
|
||||
"participantsWantToJoin": "Desejam juntar-se à reunião",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
|
||||
@@ -960,6 +978,9 @@
|
||||
"by": "Por {{ name }}",
|
||||
"closeButton": "Fechar sondagem",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "Enviar sondagem"
|
||||
},
|
||||
"addOption": "Adicionar opção",
|
||||
"answerPlaceholder": "Opção {{index}}",
|
||||
"cancel": "Cancelar",
|
||||
@@ -968,8 +989,7 @@
|
||||
"pollQuestion": "Pergunta de Sondagem",
|
||||
"questionPlaceholder": "Faça uma pergunta",
|
||||
"removeOption": "Remover opção",
|
||||
"save": "Guardar",
|
||||
"send": "Enviar"
|
||||
"save": "Guardar"
|
||||
},
|
||||
"errors": {
|
||||
"notUniqueOption": "As opções devem ser únicas"
|
||||
@@ -1338,7 +1358,7 @@
|
||||
"muteEveryoneElsesVideo": "Parar o vídeo de todos os outros",
|
||||
"muteEveryonesVideo": "Parar o vídeo de todos",
|
||||
"muteGUMPending": "A ligar o seu microfone",
|
||||
"noiseSuppression": "Supressão extra de ruído (BETA)",
|
||||
"noiseSuppression": "Supressão extra de ruído",
|
||||
"openChat": "Abrir chat",
|
||||
"participants": "Abrir painel de participantes. {{participantsCount}} participantes",
|
||||
"pip": "Mudar para o modo Picture-in-Picture",
|
||||
@@ -1376,6 +1396,20 @@
|
||||
"videounmute": "Iniciar câmara"
|
||||
},
|
||||
"addPeople": "Adicione pessoas à sua chamada",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "Cancelamento de eco acústico"
|
||||
},
|
||||
"agc": {
|
||||
"label": "Controlo automático de ganho"
|
||||
},
|
||||
"ns": {
|
||||
"label": "Supressão de ruído"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "Estéreo"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "Desativar modo de largura de banda baixa",
|
||||
"audioOnlyOn": "Ativar modo de largura de banda baixa",
|
||||
"audioRoute": "Selecionar o dispositivo de som",
|
||||
@@ -1389,20 +1423,21 @@
|
||||
"closeParticipantsPane": "Fechar painel de participantes",
|
||||
"closeReactionsMenu": "Fechar menu de reações",
|
||||
"closedCaptions": "Legendas ocultas",
|
||||
"disableNoiseSuppression": "Desativar supressão de ruído extra (BETA)",
|
||||
"disableNoiseSuppression": "Desativar supressão de ruído extra",
|
||||
"disableReactionSounds": "Pode desactivar os sons de reacção para esta reunião",
|
||||
"documentClose": "Fechar documento partilhado",
|
||||
"documentOpen": "Abrir documento partilhado",
|
||||
"download": "Descarregar as nossas aplicações",
|
||||
"e2ee": "Criptografia ponta a ponta",
|
||||
"embedMeeting": "Incorporar reunião",
|
||||
"enableNoiseSuppression": "Ativar supressão extra de ruído (BETA)",
|
||||
"enableNoiseSuppression": "Ativar supressão extra de ruído",
|
||||
"endConference": "Terminar reunião para todos",
|
||||
"enterFullScreen": "Ver em ecrã completo",
|
||||
"enterTileView": "Ver em quadrícula",
|
||||
"exitFullScreen": "Sair de ecrã completo",
|
||||
"exitTileView": "Sair de quadrícula",
|
||||
"feedback": "Deixar comentários",
|
||||
"fileSharing": "Partilha de ficheiros",
|
||||
"giphy": "Ativar/Desativar o menu GIPHY",
|
||||
"hangup": "Sair da reunião",
|
||||
"help": "Ajuda",
|
||||
@@ -1431,13 +1466,14 @@
|
||||
"noAudioSignalDialInDesc": "Também pode marcar usando:",
|
||||
"noAudioSignalDialInLinkDesc": "Números de marcação",
|
||||
"noAudioSignalTitle": "Não há nenhuma entrada vinda do seu microfone!",
|
||||
"noiseSuppression": "Supressão extra de ruído (BETA)",
|
||||
"noiseSuppression": "Supressão extra de ruído",
|
||||
"noisyAudioInputDesc": "Parece que o seu microfone está a fazer barulho, por favor considere silenciar ou mudar de dispositivo.",
|
||||
"noisyAudioInputTitle": "Seu microfone parece estar barulhento!",
|
||||
"openChat": "Abrir chat",
|
||||
"openReactionsMenu": "Abrir menu de reações",
|
||||
"participants": "Participantes",
|
||||
"pip": "Entrar no modo Picture-in-Picture",
|
||||
"polls": "Sondagens",
|
||||
"privateMessage": "Enviar mensagem privada",
|
||||
"profile": "Editar o seu perfil",
|
||||
"raiseHand": "Levantar a mão",
|
||||
@@ -1447,6 +1483,7 @@
|
||||
"reactionHeart": "Enviar reação com coração",
|
||||
"reactionLaugh": "Enviar reação de risos",
|
||||
"reactionLike": "Enviar reação de aprovado",
|
||||
"reactionLove": "Enviar reação de amor",
|
||||
"reactionSilence": "Enviar reação de silêncio",
|
||||
"reactionSurprised": "Enviar reação de surpreendido",
|
||||
"reactions": "Reações",
|
||||
@@ -1598,6 +1635,8 @@
|
||||
"noMainParticipantsTitle": "Esta reunião ainda não começou.",
|
||||
"noVisitorLobby": "Não é possível aderir enquanto houver uma sala de espera activada para a reunião.",
|
||||
"notAllowedPromotion": "É necessário que um participante autorize primeiro o seu pedido.",
|
||||
"requestToJoin": "Mão levantada",
|
||||
"requestToJoinDescription": "A sua solicitação foi enviada aos moderadores. Aguarde um momento!",
|
||||
"title": "É um espectador na reunião"
|
||||
},
|
||||
"waitingMessage": "Participará na reunião assim que esta estiver em direto!"
|
||||
|
||||
@@ -790,7 +790,7 @@ function initCommands() {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -812,7 +812,7 @@ function initCommands() {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
APP.store.dispatch(setRequestingSubtitles(false, false, null, true));
|
||||
}
|
||||
|
||||
if (mode === 'local') {
|
||||
|
||||
24
package-lock.json
generated
24
package-lock.json
generated
@@ -66,7 +66,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -91,7 +91,7 @@
|
||||
"react-native-pager-view": "6.8.1",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.1.2",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "4.11.1",
|
||||
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
|
||||
"react-native-splash-view": "0.0.18",
|
||||
@@ -18360,8 +18360,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
@@ -22002,9 +22002,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-safe-area-context": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
|
||||
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw==",
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
|
||||
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
@@ -39717,8 +39717,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-V0fp3/ZJRjHZgRlpzfCcdIF4nZ+WWWkAxKuWVTMHXfcZj07aQUHTiwIukgRludtaj9RcyNcqCVC0PrGBeeUzTw==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-wNfil8xxSjcrT3oNA5Lil0qETqR6W2XxpXNewYYjFiM2kSuWgH8fcVxgcQYzvGH+sOqEjdUEk1V81oNo/rB6tQ==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "^2.6.7",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
@@ -42378,9 +42378,9 @@
|
||||
"integrity": "sha512-l5JOJphNzox9a9icL3T6O/gEqZuqWqcbejW04WPa10m0UanBdIYrNkPFl48B3ivWw3MabpjB6GiDYv7old9/fw=="
|
||||
},
|
||||
"react-native-safe-area-context": {
|
||||
"version": "5.5.2",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.5.2.tgz",
|
||||
"integrity": "sha512-t4YVbHa9uAGf+pHMabGrb0uHrD5ogAusSu842oikJ3YKXcYp6iB4PTGl0EZNkUIR3pCnw/CXKn42OCfhsS0JIw=="
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz",
|
||||
"integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA=="
|
||||
},
|
||||
"react-native-screens": {
|
||||
"version": "4.11.1",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2118.0.0+67fd2c84/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2124.0.0+80df84a1/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -97,7 +97,7 @@
|
||||
"react-native-pager-view": "6.8.1",
|
||||
"react-native-paper": "5.10.3",
|
||||
"react-native-performance": "5.1.2",
|
||||
"react-native-safe-area-context": "5.5.2",
|
||||
"react-native-safe-area-context": "5.6.1",
|
||||
"react-native-screens": "4.11.1",
|
||||
"react-native-sound": "https://github.com/jitsi/react-native-sound.git#ea13c97b5c2a4ff5e0d9bacbd9ff5e4457fe2c3c",
|
||||
"react-native-splash-view": "0.0.18",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { setRoom } from '../base/conference/actions';
|
||||
import { setRoom } from '../base/conference/actions.native';
|
||||
import { getConferenceState } from '../base/conference/functions';
|
||||
import {
|
||||
configWillLoad,
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
} from '../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
import { clearNotifications } from '../notifications/actions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../prejoin/functions.native';
|
||||
|
||||
import { maybeRedirectToTokenAuthUrl } from './actions.any';
|
||||
import { addTrackStateToURL, getDefaultURL } from './functions.native';
|
||||
|
||||
@@ -81,7 +81,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
break;
|
||||
}
|
||||
case LOCAL_PARTICIPANT_MODERATION_NOTIFICATION: {
|
||||
let descriptionKey;
|
||||
let titleKey;
|
||||
let uid = '';
|
||||
const localParticipant = getLocalParticipant(getState);
|
||||
@@ -111,8 +110,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
!raisedHand && dispatch(raiseHand(true));
|
||||
dispatch(hideNotification(uid));
|
||||
}) ],
|
||||
descriptionKey,
|
||||
sticky: true,
|
||||
titleKey,
|
||||
uid
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
@@ -271,7 +268,6 @@ StateListenerRegistry.register(
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.hostAskedUnmute',
|
||||
sticky: true,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
uid: ASKED_TO_UNMUTE_NOTIFICATION_ID
|
||||
|
||||
@@ -93,10 +93,17 @@ export function commonUserJoinedHandling(
|
||||
if (!user.isHidden()) {
|
||||
const isReplacing = user?.isReplacing();
|
||||
const isPromoted = conference?.getMetadataHandler().getMetadata()?.visitors?.promoted?.[id];
|
||||
const userIdentity = user.getIdentity()?.user;
|
||||
|
||||
// Map identity from JWT context to userContext for external API
|
||||
const userContext = userIdentity ? {
|
||||
id: userIdentity.id,
|
||||
name: userIdentity.name
|
||||
} : undefined;
|
||||
|
||||
// the identity and avatar come from jwt and never change in the presence
|
||||
dispatch(participantJoined({
|
||||
avatarURL: user.getIdentity()?.user?.avatar,
|
||||
avatarURL: userIdentity?.avatar,
|
||||
botType: user.getBotType(),
|
||||
conference,
|
||||
id,
|
||||
@@ -105,7 +112,8 @@ export function commonUserJoinedHandling(
|
||||
role: user.getRole(),
|
||||
isPromoted,
|
||||
isReplacing,
|
||||
sources: user.getSources()
|
||||
sources: user.getSources(),
|
||||
userContext
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +559,7 @@ export interface IConfig {
|
||||
skipConsentInMeeting?: boolean;
|
||||
suggestRecording?: boolean;
|
||||
};
|
||||
reducedUIEnabled?: boolean;
|
||||
reducedUImainToolbarButtons?: Array<string>;
|
||||
remoteVideoMenu?: {
|
||||
disableDemote?: boolean;
|
||||
|
||||
@@ -215,6 +215,7 @@ export default [
|
||||
'recordings.showPrejoinWarning',
|
||||
'recordings.showRecordingLink',
|
||||
'recordings.suggestRecording',
|
||||
'reducedUIEnabled',
|
||||
'reducedUImainToolbarButtons',
|
||||
'replaceParticipant',
|
||||
'resolution',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getCustomerDetails } from '../../jaas/actions.any';
|
||||
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { conferenceWillLeave } from '../conference/actions.native';
|
||||
import { setJWT } from '../jwt/actions';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
@@ -58,5 +59,8 @@ export function connect(id?: string, password?: string) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hangup(_requestFeedback = false) {
|
||||
return (dispatch: IStore['dispatch']) => dispatch(appNavigate(undefined));
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
dispatch(conferenceWillLeave());
|
||||
dispatch(appNavigate(undefined));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import i18next from './i18next';
|
||||
*
|
||||
* @param {string} language - The language e.g. 'en', 'fr'.
|
||||
* @param {string} url - The url of the translation bundle.
|
||||
* @param {string} ns - The namespace of the translation bundle.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function changeLanguageBundle(language: string, url: string) {
|
||||
export async function changeLanguageBundle(language: string, url: string, ns = 'main') {
|
||||
const res = await fetch(url);
|
||||
const bundle = await res.json();
|
||||
|
||||
i18next.addResourceBundle(language, 'main', bundle, true, true);
|
||||
i18next.addResourceBundle(language, ns, bundle, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -58,6 +58,14 @@ export const DEFAULT_LANGUAGE = 'en';
|
||||
*/
|
||||
export const TRANSLATION_LANGUAGES_HEAD: Array<string> = [ DEFAULT_LANGUAGE ];
|
||||
|
||||
/**
|
||||
* The available/supported i18n namespaces.
|
||||
*
|
||||
* @public
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
export const SUPPORTED_NS = [ 'main', 'languages', 'countries', 'translation-languages' ];
|
||||
|
||||
/**
|
||||
* The options to initialize i18next with.
|
||||
*
|
||||
@@ -81,7 +89,7 @@ const options: i18next.InitOptions = {
|
||||
escapeValue: false // not needed for react as it escapes by default
|
||||
},
|
||||
load: 'all',
|
||||
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
|
||||
ns: SUPPORTED_NS,
|
||||
react: {
|
||||
// re-render when a new resource bundle is added
|
||||
// @ts-expect-error. Fixed in i18next 19.6.1.
|
||||
|
||||
@@ -4,7 +4,7 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
|
||||
import { I18NEXT_INITIALIZED, LANGUAGE_CHANGED } from './actionTypes';
|
||||
import { changeLanguageBundle } from './functions';
|
||||
import i18next from './i18next';
|
||||
import i18next, { SUPPORTED_NS } from './i18next';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
@@ -19,9 +19,10 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case LANGUAGE_CHANGED:
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const { language } = i18next;
|
||||
const { labels } = action.type === SET_DYNAMIC_BRANDING_DATA
|
||||
const data = action.type === SET_DYNAMIC_BRANDING_DATA
|
||||
? action.value
|
||||
: store.getState()['features/dynamic-branding'];
|
||||
const labels = data?.labels;
|
||||
|
||||
if (language && labels?.[language]) {
|
||||
changeLanguageBundle(language, labels[language])
|
||||
@@ -30,6 +31,17 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
});
|
||||
}
|
||||
|
||||
SUPPORTED_NS.forEach(ns => {
|
||||
const nsLabels = data?.[`labels-${ns}`];
|
||||
|
||||
if (language && nsLabels?.[language]) {
|
||||
changeLanguageBundle(language, nsLabels[language], ns)
|
||||
.catch(err => {
|
||||
logger.log(`Error setting dynamic language bundle for ${ns}`, err);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Update transcription language, if applicable.
|
||||
if (action.type === SET_DYNAMIC_BRANDING_DATA) {
|
||||
const { defaultTranscriptionLanguage } = action.value;
|
||||
|
||||
@@ -10,6 +10,10 @@ import { parseURLParams } from '../util/parseURLParams';
|
||||
import { JWT_VALIDATION_ERRORS, MEET_FEATURES } from './constants';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Note that this is just client-side code and it intentionally does not verify the signature of the JWT.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the JSON Web Token (JWT), if any, defined by a specific
|
||||
* {@link URL}.
|
||||
|
||||
@@ -17,7 +17,7 @@ interface IProps {
|
||||
/**
|
||||
* The children component(s) of the Modal, to be rendered.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Additional style to be appended to the KeyboardAvoidingView content container.
|
||||
@@ -63,7 +63,7 @@ const JitsiScreen = ({
|
||||
footerComponent,
|
||||
hasBottomTextInput = false,
|
||||
hasExtraHeaderHeight = false,
|
||||
safeAreaInsets = [ 'left', 'right' ],
|
||||
safeAreaInsets = [ 'bottom', 'left', 'right' ],
|
||||
style
|
||||
}: IProps) => {
|
||||
const renderContent = () => (
|
||||
@@ -78,8 +78,8 @@ const JitsiScreen = ({
|
||||
edges = { safeAreaInsets }
|
||||
style = { styles.safeArea }>
|
||||
{ children }
|
||||
{ footerComponent?.() }
|
||||
</SafeAreaView>
|
||||
{ footerComponent?.() }
|
||||
</JitsiKeyboardAvoidingView>
|
||||
);
|
||||
|
||||
|
||||
@@ -818,7 +818,7 @@ export const addPeopleFeatureControl = (stateful: IStateful) => {
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispatch: IStore['dispatch']) => {
|
||||
export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean | undefined, dispatch: IStore['dispatch']) => {
|
||||
if (addPeopleFeatureEnabled) {
|
||||
dispatch(toggleShareDialog(false));
|
||||
} else {
|
||||
|
||||
@@ -606,13 +606,21 @@ function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConferen
|
||||
function _localParticipantJoined({ getState, dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
const settings = getState()['features/base/settings'];
|
||||
const state = getState();
|
||||
const settings = state['features/base/settings'];
|
||||
const jwtUser = state['features/base/jwt']?.user;
|
||||
|
||||
const userContext = jwtUser ? {
|
||||
id: jwtUser.id,
|
||||
name: jwtUser.name
|
||||
} : undefined;
|
||||
|
||||
dispatch(localParticipantJoined({
|
||||
avatarURL: settings.avatarURL,
|
||||
email: settings.email,
|
||||
name: settings.displayName,
|
||||
id: ''
|
||||
id: '',
|
||||
userContext
|
||||
}));
|
||||
|
||||
return result;
|
||||
|
||||
@@ -627,7 +627,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
|
||||
pinned,
|
||||
presence,
|
||||
role,
|
||||
sources
|
||||
sources,
|
||||
userContext
|
||||
} = participant;
|
||||
let { conference, id } = participant;
|
||||
|
||||
@@ -659,7 +660,8 @@ function _participantJoined({ participant }: { participant: IParticipant; }) {
|
||||
pinned: pinned || false,
|
||||
presence,
|
||||
role: role || PARTICIPANT_ROLE.NONE,
|
||||
sources
|
||||
sources,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ export interface IParticipant {
|
||||
role?: string;
|
||||
sources?: Map<string, Map<string, ISourceInfo>>;
|
||||
supportsRemoteControl?: boolean;
|
||||
userContext?: IUserContext;
|
||||
}
|
||||
|
||||
export interface IUserContext {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ILocalParticipant extends IParticipant {
|
||||
|
||||
@@ -43,6 +43,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const state = getState();
|
||||
const { reducedUIEnabled = true } = state['features/base/config'];
|
||||
const { isOpen: isChatOpen, width } = state['features/chat'];
|
||||
|
||||
if (isChatOpen) {
|
||||
@@ -51,7 +52,7 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
|
||||
availableWidth -= getParticipantsPaneWidth(state);
|
||||
|
||||
dispatch(setReducedUI(availableWidth, clientHeight));
|
||||
reducedUIEnabled && dispatch(setReducedUI(availableWidth, clientHeight));
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
@@ -112,7 +113,7 @@ export function setReducedUI(width: number, height: number) {
|
||||
const threshold = navigator.product === 'ReactNative'
|
||||
? REDUCED_UI_THRESHOLD
|
||||
: WEB_REDUCED_UI_THRESHOLD;
|
||||
const reducedUI = Math.min(width, height) < threshold;
|
||||
const reducedUI = Math.max(width, height) < threshold;
|
||||
|
||||
if (reducedUI !== getState()['features/base/responsive-ui'].reducedUI) {
|
||||
return dispatch({
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
TRACK_UPDATED,
|
||||
TRACK_WILL_CREATE
|
||||
} from './actionTypes';
|
||||
import { toggleScreensharing } from './actions';
|
||||
import {
|
||||
createLocalTracksF,
|
||||
getCameraFacingMode,
|
||||
@@ -385,8 +386,22 @@ export function trackAdded(track: any) {
|
||||
const mediaType = track.getVideoType() === VIDEO_TYPE.DESKTOP
|
||||
? MEDIA_TYPE.SCREENSHARE
|
||||
: track.getType();
|
||||
|
||||
let isReceivingData, noDataFromSourceNotificationInfo, participantId;
|
||||
|
||||
// Make screen share toggle off listen to MediaStreamTrack "ended" event
|
||||
// when it's terminated via Android status bar chip.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
const mediaStreamTrack = track?.getTrack?.();
|
||||
|
||||
if (mediaType === MEDIA_TYPE.SCREENSHARE) {
|
||||
const onEnded = () => dispatch(toggleScreensharing(false));
|
||||
|
||||
mediaStreamTrack.addEventListener('ended', onEnded);
|
||||
track._onEnded = onEnded;
|
||||
}
|
||||
}
|
||||
|
||||
if (local) {
|
||||
// Reset the no data from src notification state when we change the track, as it's context is set
|
||||
// on a per device basis.
|
||||
@@ -568,6 +583,16 @@ export function trackRemoved(track: any): {
|
||||
track.removeAllListeners(JitsiTrackEvents.TRACK_VIDEOTYPE_CHANGED);
|
||||
track.removeAllListeners(JitsiTrackEvents.NO_DATA_FROM_SOURCE);
|
||||
|
||||
// Remove MediaStreamTrack "ended" event.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
const mediaStreamTrack = track?.getTrack?.();
|
||||
|
||||
if (track._onEnded) {
|
||||
mediaStreamTrack.removeEventListener('ended', track._onEnded);
|
||||
delete track._onEnded;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: TRACK_REMOVED,
|
||||
track: {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
|
||||
button: {
|
||||
marginBottom: BaseTheme.spacing[4],
|
||||
marginHorizontal: BaseTheme.spacing[2]
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
collapsibleList: {
|
||||
|
||||
@@ -44,6 +44,16 @@ export const getMainRoom = (stateful: IStateful) => {
|
||||
* @returns {IRoomsInfo} The rooms info.
|
||||
*/
|
||||
export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const state = toState(stateful);
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
const jwtUser = state['features/base/jwt']?.user;
|
||||
const localUserContext = jwtUser ? {
|
||||
id: jwtUser.id,
|
||||
name: jwtUser.name
|
||||
} : {
|
||||
id: localParticipant?.jwtId,
|
||||
name: localParticipant?.name
|
||||
};
|
||||
const breakoutRooms = getBreakoutRooms(stateful);
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
@@ -57,7 +67,6 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const conferenceParticipants = conference?.getParticipants()
|
||||
.filter((participant: IJitsiParticipant) => !participant.isHidden());
|
||||
|
||||
const localParticipant = getLocalParticipant(stateful);
|
||||
let localParticipantInfo;
|
||||
|
||||
if (localParticipant) {
|
||||
@@ -65,7 +74,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
role: localParticipant.role,
|
||||
displayName: localParticipant.name,
|
||||
avatarUrl: localParticipant.loadableAvatarUrl,
|
||||
id: localParticipant.id
|
||||
id: localParticipant.id,
|
||||
userContext: localUserContext
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +96,8 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
role: participantItem.getRole(),
|
||||
displayName: participantItem.getDisplayName(),
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: participantItem.getId()
|
||||
id: participantItem.getId(),
|
||||
userContext: storeParticipant?.userContext
|
||||
} as IRoomInfoParticipant;
|
||||
}) ]
|
||||
: [ localParticipantInfo ]
|
||||
@@ -110,13 +121,18 @@ export const getRoomsInfo = (stateful: IStateful) => {
|
||||
const storeParticipant = getParticipantById(stateful,
|
||||
ids.length > 1 ? ids[1] : participantItem.jid);
|
||||
|
||||
// Check if this is the local participant
|
||||
const isLocal = storeParticipant?.id === localParticipant?.id;
|
||||
const userContext = isLocal ? localUserContext : (storeParticipant?.userContext || participantItem.userContext);
|
||||
|
||||
return {
|
||||
jid: participantItem?.jid,
|
||||
role: participantItem?.role,
|
||||
displayName: participantItem?.displayName,
|
||||
avatarUrl: storeParticipant?.loadableAvatarUrl,
|
||||
id: storeParticipant ? storeParticipant.id
|
||||
: participantLongId
|
||||
: participantLongId,
|
||||
userContext
|
||||
} as IRoomInfoParticipant;
|
||||
}) : []
|
||||
} as IRoomInfo;
|
||||
|
||||
@@ -44,19 +44,59 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
switch (type) {
|
||||
case UPDATE_BREAKOUT_ROOMS: {
|
||||
// edit name if it was overwritten
|
||||
// Enrich participants with userContext from Redux store
|
||||
if (!action.updatedNames) {
|
||||
const { overwrittenNameList } = getState()['features/base/participants'];
|
||||
const state = getState();
|
||||
const { overwrittenNameList, local: localParticipant } = state['features/base/participants'];
|
||||
const jwtUser = state['features/base/jwt']?.user;
|
||||
const localUserContext = jwtUser ? {
|
||||
id: jwtUser.id,
|
||||
name: jwtUser.name
|
||||
} : {
|
||||
id: localParticipant?.jwtId,
|
||||
name: localParticipant?.name
|
||||
};
|
||||
|
||||
if (Object.keys(overwrittenNameList).length > 0) {
|
||||
const newRooms: IRooms = {};
|
||||
// Get existing userContext cache
|
||||
const existingCache = state['features/breakout-rooms'].userContextCache || {};
|
||||
const newCache = { ...existingCache };
|
||||
|
||||
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
|
||||
let participants = r?.participants || {};
|
||||
let jid;
|
||||
const newRooms: IRooms = {};
|
||||
|
||||
Object.entries(action.rooms as IRooms).forEach(([ key, r ]) => {
|
||||
let participants = r?.participants || {};
|
||||
|
||||
// Add userContext to each participant
|
||||
const enhancedParticipants: typeof participants = {};
|
||||
|
||||
for (const [ participantJid, participantData ] of Object.entries(participants)) {
|
||||
const ids = participantJid.split('/');
|
||||
const participantId = ids.length > 1 ? ids[1] : participantData.jid;
|
||||
const storeParticipant = getParticipantById(state, participantId);
|
||||
const isLocal = storeParticipant?.id === localParticipant?.id;
|
||||
|
||||
// Try to get userContext from: local, store, cache, or incoming data
|
||||
const userContext = isLocal
|
||||
? localUserContext
|
||||
: (storeParticipant?.userContext || newCache[participantId] || participantData.userContext);
|
||||
|
||||
// Update cache if we have userContext
|
||||
if (userContext && participantId) {
|
||||
newCache[participantId] = userContext;
|
||||
}
|
||||
|
||||
enhancedParticipants[participantJid] = {
|
||||
...participantData,
|
||||
userContext
|
||||
};
|
||||
}
|
||||
|
||||
participants = enhancedParticipants;
|
||||
|
||||
// Apply overwritten display names
|
||||
if (Object.keys(overwrittenNameList).length > 0) {
|
||||
for (const id of Object.keys(overwrittenNameList)) {
|
||||
jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
|
||||
const jid = Object.keys(participants).find(p => p.slice(p.indexOf('/') + 1) === id);
|
||||
|
||||
if (jid) {
|
||||
participants = {
|
||||
@@ -68,15 +108,16 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newRooms[key] = {
|
||||
...r,
|
||||
participants
|
||||
};
|
||||
});
|
||||
newRooms[key] = {
|
||||
...r,
|
||||
participants
|
||||
};
|
||||
});
|
||||
|
||||
action.rooms = newRooms;
|
||||
}
|
||||
action.rooms = newRooms;
|
||||
action.userContextCache = newCache;
|
||||
}
|
||||
|
||||
// edit the chat history to match names for participants in breakout rooms
|
||||
|
||||
@@ -10,12 +10,19 @@ import { IRooms } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
rooms: {},
|
||||
roomCounter: 0
|
||||
roomCounter: 0,
|
||||
userContextCache: {}
|
||||
};
|
||||
|
||||
export interface IBreakoutRoomsState {
|
||||
roomCounter: number;
|
||||
rooms: IRooms;
|
||||
userContextCache: {
|
||||
[participantId: string]: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,12 +36,13 @@ ReducerRegistry.register<IBreakoutRoomsState>(FEATURE_KEY, (state = DEFAULT_STAT
|
||||
roomCounter: action.roomCounter
|
||||
};
|
||||
case UPDATE_BREAKOUT_ROOMS: {
|
||||
const { roomCounter, rooms } = action;
|
||||
const { roomCounter, rooms, userContextCache } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
roomCounter,
|
||||
rooms
|
||||
rooms,
|
||||
userContextCache: userContextCache || state.userContextCache
|
||||
};
|
||||
}
|
||||
case _RESET_BREAKOUT_ROOMS: {
|
||||
|
||||
@@ -8,6 +8,10 @@ export interface IRoom {
|
||||
displayName: string;
|
||||
jid: string;
|
||||
role: string;
|
||||
userContext?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -33,4 +37,8 @@ export interface IRoomInfoParticipant {
|
||||
id: string;
|
||||
jid: string;
|
||||
role: string;
|
||||
userContext?: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function openChat(participant?: IParticipant | undefined | Object, disabl
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
} else {
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
navigate(screen.conference.chatTabs.main);
|
||||
}
|
||||
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
|
||||
111
react/features/chat/components/AbstractClosedCaptions.tsx
Normal file
111
react/features/chat/components/AbstractClosedCaptions.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { ComponentType, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../app/types';
|
||||
import { openDialog } from '../../base/dialog/actions';
|
||||
import { IMessageGroup, groupMessagesBySender } from '../../base/util/messageGrouping';
|
||||
// @ts-ignore
|
||||
import { StartRecordingDialog } from '../../recording/components/Recording';
|
||||
import { setRequestingSubtitles } from '../../subtitles/actions.any';
|
||||
import { canStartSubtitles } from '../../subtitles/functions.any';
|
||||
import { ISubtitle } from '../../subtitles/types';
|
||||
import { isTranscribing } from '../../transcribing/functions';
|
||||
|
||||
export type AbstractProps = {
|
||||
canStartSubtitles: boolean;
|
||||
filteredSubtitles: ISubtitle[];
|
||||
groupedSubtitles: IMessageGroup<ISubtitle>[];
|
||||
isButtonPressed: boolean;
|
||||
isTranscribing: boolean;
|
||||
startClosedCaptions: () => void;
|
||||
};
|
||||
|
||||
const AbstractClosedCaptions = (Component: ComponentType<AbstractProps>) => () => {
|
||||
const dispatch = useDispatch();
|
||||
const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory);
|
||||
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
|
||||
const selectedLanguage = language?.replace('translation-languages:', '');
|
||||
const _isTranscribing = useSelector(isTranscribing);
|
||||
const _canStartSubtitles = useSelector(canStartSubtitles);
|
||||
const [ isButtonPressed, setButtonPressed ] = useState(false);
|
||||
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
const filteredSubtitles = useMemo(() => {
|
||||
// First, create a map of transcription messages by message ID
|
||||
const transcriptionMessages = new Map(
|
||||
subtitles
|
||||
.filter(s => s.isTranscription)
|
||||
.map(s => [ s.id, s ])
|
||||
);
|
||||
|
||||
if (!selectedLanguage) {
|
||||
// When no language is selected, show all original transcriptions
|
||||
return Array.from(transcriptionMessages.values());
|
||||
}
|
||||
|
||||
// Then, create a map of translation messages by message ID
|
||||
const translationMessages = new Map(
|
||||
subtitles
|
||||
.filter(s => !s.isTranscription && s.language === selectedLanguage)
|
||||
.map(s => [ s.id, s ])
|
||||
);
|
||||
|
||||
// When a language is selected, for each transcription message:
|
||||
// 1. Use its translation if available
|
||||
// 2. Fall back to the original transcription if no translation exists
|
||||
return Array.from(transcriptionMessages.values())
|
||||
.filter((m: ISubtitle) => !m.interim)
|
||||
.map(m => translationMessages.get(m.id) ?? m);
|
||||
}, [ subtitles, selectedLanguage ]);
|
||||
|
||||
const groupedSubtitles = useMemo(() =>
|
||||
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
|
||||
|
||||
const startClosedCaptions = useCallback(() => {
|
||||
if (isAsyncTranscriptionEnabled) {
|
||||
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, {
|
||||
recordAudioAndVideo: false
|
||||
}));
|
||||
} else {
|
||||
if (isButtonPressed) {
|
||||
return;
|
||||
}
|
||||
dispatch(setRequestingSubtitles(true, false, null));
|
||||
setButtonPressed(true);
|
||||
}
|
||||
|
||||
}, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
}, [ subtitlesError, isButtonPressed, isAsyncTranscriptionEnabled ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!_isTranscribing && isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
}, [ _isTranscribing, isButtonPressed, isAsyncTranscriptionEnabled ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
}, [ isButtonPressed, isAsyncTranscriptionEnabled ]);
|
||||
|
||||
return (
|
||||
<Component
|
||||
canStartSubtitles = { _canStartSubtitles }
|
||||
filteredSubtitles = { filteredSubtitles }
|
||||
groupedSubtitles = { groupedSubtitles }
|
||||
isButtonPressed = { isButtonPressed }
|
||||
isTranscribing = { _isTranscribing }
|
||||
startClosedCaptions = { startClosedCaptions } />
|
||||
);
|
||||
};
|
||||
|
||||
export default AbstractClosedCaptions;
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import { Route, useIsFocused } from '@react-navigation/native';
|
||||
import React, { Component, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { StyleType } from '../../../base/styles/functions.native';
|
||||
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
|
||||
import { getUnreadPollCount } from '../../../polls/functions';
|
||||
import { pollsStyles } from '../../../polls/components/native/styles';
|
||||
import { closeChat, sendMessage } from '../../actions.native';
|
||||
import { getUnreadFilesCount } from '../../functions';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
import ChatInputBar from './ChatInputBar';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* The number of unread file messages.
|
||||
*/
|
||||
_unreadFilesCount: number;
|
||||
|
||||
/**
|
||||
* The number of unread messages.
|
||||
*/
|
||||
_unreadMessagesCount: number;
|
||||
|
||||
/**
|
||||
* The number of unread polls.
|
||||
*/
|
||||
_unreadPollsCount: number;
|
||||
|
||||
/**
|
||||
* Default prop for navigating between screen components(React Navigation).
|
||||
*/
|
||||
@@ -62,6 +52,7 @@ class Chat extends Component<IProps> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onSendMessage = this._onSendMessage.bind(this);
|
||||
this._renderFooter = this._renderFooter.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,14 +67,10 @@ class Chat extends Component<IProps> {
|
||||
return (
|
||||
<JitsiScreen
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
footerComponent = { () =>
|
||||
<ChatInputBar onSend = { this._onSendMessage } />
|
||||
}
|
||||
footerComponent = { this._renderFooter }
|
||||
hasBottomTextInput = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { styles.chatContainer }>
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
{/* @ts-ignore */}
|
||||
<MessageContainer messages = { _messages } />
|
||||
<MessageRecipient privateMessageRecipient = { privateMessageRecipient } />
|
||||
@@ -91,6 +78,16 @@ class Chat extends Component<IProps> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the footer component.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Element<*>}
|
||||
*/
|
||||
_renderFooter() {
|
||||
return <ChatInputBar onSend = { this._onSendMessage } />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
*
|
||||
@@ -113,9 +110,7 @@ class Chat extends Component<IProps> {
|
||||
* @private
|
||||
* @returns {{
|
||||
* _messages: Array<Object>,
|
||||
* _unreadMessagesCount: number,
|
||||
* _unreadPollsCount: number,
|
||||
* _unreadFilesCount: number
|
||||
* _unreadMessagesCount: number
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
@@ -123,34 +118,34 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
|
||||
return {
|
||||
_messages: messages,
|
||||
_unreadMessagesCount: unreadMessagesCount,
|
||||
_unreadPollsCount: getUnreadPollCount(state),
|
||||
_unreadFilesCount: getUnreadFilesCount(state)
|
||||
_unreadMessagesCount: unreadMessagesCount
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)((props: IProps) => {
|
||||
const { _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount, dispatch, navigation, t } = props;
|
||||
const totalUnread = _unreadMessagesCount + _unreadPollsCount + _unreadFilesCount;
|
||||
const unreadMessagesNr = totalUnread > 0;
|
||||
const { _unreadMessagesCount, dispatch, navigation, t } = props;
|
||||
|
||||
const isChatTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CHAT);
|
||||
|
||||
const isFocused = useIsFocused();
|
||||
|
||||
const activeUnreadMessagesNr = !isChatTabFocused && _unreadMessagesCount > 0;
|
||||
|
||||
useEffect(() => {
|
||||
navigation?.setOptions({
|
||||
tabBarLabel: () => (
|
||||
<TabBarLabelCounter
|
||||
activeUnreadNr = { unreadMessagesNr }
|
||||
activeUnreadNr = { activeUnreadMessagesNr }
|
||||
isFocused = { isFocused }
|
||||
label = { t('chat.tabs.chat') }
|
||||
unreadCount = { totalUnread } />
|
||||
unreadCount = { _unreadMessagesCount } />
|
||||
)
|
||||
});
|
||||
|
||||
return () => {
|
||||
isFocused && dispatch(closeChat());
|
||||
};
|
||||
}, [ isFocused, _unreadMessagesCount, _unreadPollsCount, _unreadFilesCount ]);
|
||||
}, [ isFocused, _unreadMessagesCount ]);
|
||||
|
||||
return (
|
||||
<Chat { ...props } />
|
||||
|
||||
@@ -43,7 +43,7 @@ class ChatButton extends AbstractButton<IProps> {
|
||||
override _handleClick() {
|
||||
this.props._isPollsDisabled
|
||||
? navigate(screen.conference.chat)
|
||||
: navigate(screen.conference.chatandpolls.main);
|
||||
: navigate(screen.conference.chatTabs.main);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,6 @@ import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconSend } from '../../../base/icons/svg';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import IconButton from '../../../base/ui/components/native/IconButton';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
@@ -85,14 +84,6 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
let inputBarStyles;
|
||||
|
||||
if (this.props.aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
inputBarStyles = styles.inputBarWide;
|
||||
} else {
|
||||
inputBarStyles = styles.inputBarNarrow;
|
||||
}
|
||||
|
||||
if (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) {
|
||||
return (
|
||||
<View
|
||||
@@ -109,7 +100,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
<View
|
||||
id = 'chat-input'
|
||||
style = { [
|
||||
inputBarStyles,
|
||||
styles.inputBar,
|
||||
this.state.addPadding ? styles.extraBarPadding : null
|
||||
] as ViewStyle[] }>
|
||||
<Input
|
||||
@@ -129,6 +120,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
id = { this.props.t('chat.sendButton') }
|
||||
onPress = { this._onSubmit }
|
||||
src = { IconSend }
|
||||
style = { styles.sendButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
</View>
|
||||
);
|
||||
|
||||
130
react/features/chat/components/native/ClosedCaptions.tsx
Normal file
130
react/features/chat/components/native/ClosedCaptions.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowRight, IconSubtitles } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { StyleType } from '../../../base/styles/functions.any';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
|
||||
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions';
|
||||
|
||||
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
|
||||
import { closedCaptionsStyles } from './styles';
|
||||
|
||||
/**
|
||||
* Component that displays the closed captions interface.
|
||||
*
|
||||
* @returns {JSX.Element} - The ClosedCaptions component.
|
||||
*/
|
||||
const ClosedCaptions = ({
|
||||
canStartSubtitles,
|
||||
filteredSubtitles,
|
||||
groupedSubtitles,
|
||||
isButtonPressed,
|
||||
isTranscribing,
|
||||
startClosedCaptions
|
||||
}: AbstractProps): JSX.Element => {
|
||||
const navigation = useNavigation();
|
||||
const { t } = useTranslation();
|
||||
const isCCTabFocused = useSelector((state: IReduxState) => state['features/chat'].focusedTab === ChatTabs.CLOSED_CAPTIONS);
|
||||
const selectedLanguage = useSelector((state: IReduxState) => state['features/subtitles']._language);
|
||||
const navigateToLanguageSelect = useCallback(() => {
|
||||
navigate(screen.conference.subtitles);
|
||||
}, [ navigation, screen ]);
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
useEffect(() => {
|
||||
navigation?.setOptions({
|
||||
tabBarLabel: () => (
|
||||
<TabBarLabelCounter
|
||||
isFocused = { isCCTabFocused }
|
||||
label = { t('chat.tabs.closedCaptions') } />
|
||||
)
|
||||
});
|
||||
}, [ isCCTabFocused, navigation, t ]);
|
||||
|
||||
const getContentContainerStyle = () => {
|
||||
if (isTranscribing) {
|
||||
return closedCaptionsStyles.transcribingContainer as StyleType;
|
||||
}
|
||||
|
||||
return closedCaptionsStyles.emptyContentContainer as StyleType;
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (!isTranscribing) {
|
||||
if (canStartSubtitles) {
|
||||
return (
|
||||
<View style = { closedCaptionsStyles.emptyContent as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = { t('closedCaptionsTab.startClosedCaptionsButton') }
|
||||
disabled = { isButtonPressed }
|
||||
labelKey = 'closedCaptionsTab.startClosedCaptionsButton'
|
||||
onClick = { startClosedCaptions }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { closedCaptionsStyles.emptyContent as ViewStyle }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon03 }
|
||||
size = { 100 }
|
||||
src = { IconSubtitles } />
|
||||
<Text style = { [ closedCaptionsStyles.emptyStateText, { marginTop: BaseTheme.spacing[3] } ] }>
|
||||
{ t('closedCaptionsTab.emptyState') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
// Hide the "Translate to" option when asyncTranscription is enabled
|
||||
!isAsyncTranscriptionEnabled && <View style = { closedCaptionsStyles.languageButtonContainer as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t('transcribing.translateTo') }:</Text>
|
||||
<TouchableHighlight onPress = { navigateToLanguageSelect }>
|
||||
<View style = { closedCaptionsStyles.languageButtonContent as ViewStyle }>
|
||||
<Text style = { closedCaptionsStyles.languageButtonText }>{ t(selectedLanguage ?? 'transcribing.subtitlesOff') }</Text>
|
||||
<Icon
|
||||
size = { 24 }
|
||||
src = { IconArrowRight } />
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
}
|
||||
<View style = { closedCaptionsStyles.messagesContainer as ViewStyle }>
|
||||
<SubtitlesMessagesContainer
|
||||
groups = { groupedSubtitles }
|
||||
messages = { filteredSubtitles } />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
contentContainerStyle = { getContentContainerStyle() }
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { closedCaptionsStyles.container as StyleType }>
|
||||
{ renderContent() }
|
||||
</JitsiScreen>
|
||||
);
|
||||
};
|
||||
|
||||
export default AbstractClosedCaptions(ClosedCaptions);
|
||||
@@ -44,19 +44,23 @@ class MessageContainer extends Component<IProps, any> {
|
||||
*/
|
||||
override render() {
|
||||
const data = this._getMessagesGroupedBySender();
|
||||
const noMessages = data.length === 0;
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
ListEmptyComponent = { this._renderListEmptyComponent }
|
||||
bounces = { false }
|
||||
data = { data }
|
||||
|
||||
// @ts-ignore
|
||||
contentContainerStyle = { noMessages && styles.emptyListContentContainer }
|
||||
data = { data }
|
||||
// Workaround for RN bug:
|
||||
// https://github.com/facebook/react-native/issues/21196
|
||||
inverted = { Boolean(data.length) }
|
||||
keyExtractor = { this._keyExtractor }
|
||||
keyboardShouldPersistTaps = 'handled'
|
||||
renderItem = { this._renderMessageGroup } />
|
||||
renderItem = { this._renderMessageGroup }
|
||||
style = { noMessages && styles.emptyListStyle } />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ class PrivateMessageButton extends AbstractButton<IProps, any> {
|
||||
? navigate(screen.conference.chat, {
|
||||
privateMessageRecipient: this.props._participant
|
||||
})
|
||||
: navigate(screen.conference.chatandpolls.main, {
|
||||
screen: screen.conference.chatandpolls.tab.chat,
|
||||
: navigate(screen.conference.chatTabs.main, {
|
||||
screen: screen.conference.chatTabs.tab.chat,
|
||||
params: {
|
||||
privateMessageRecipient: this.props._participant
|
||||
}
|
||||
|
||||
45
react/features/chat/components/native/SubtitleMessage.tsx
Normal file
45
react/features/chat/components/native/SubtitleMessage.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { Text, View, ViewStyle } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { getParticipantDisplayName } from '../../../base/participants/functions';
|
||||
import { ISubtitle } from '../../../subtitles/types';
|
||||
|
||||
import { closedCaptionsStyles } from './styles';
|
||||
|
||||
|
||||
interface IProps extends ISubtitle {
|
||||
showDisplayName: boolean;
|
||||
}
|
||||
|
||||
export default function SubtitleMessage({ participantId, text, timestamp, interim, showDisplayName }: IProps) {
|
||||
const participantName = useSelector((state: IReduxState) =>
|
||||
getParticipantDisplayName(state, participantId));
|
||||
|
||||
const containerStyle: ViewStyle[] = [
|
||||
closedCaptionsStyles.subtitleMessageContainer as ViewStyle
|
||||
];
|
||||
|
||||
if (interim) {
|
||||
containerStyle.push(closedCaptionsStyles.subtitleMessageInterim as ViewStyle);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { containerStyle }>
|
||||
<View style = { closedCaptionsStyles.subtitleMessageContent as ViewStyle }>
|
||||
{
|
||||
showDisplayName && (
|
||||
<Text style = { closedCaptionsStyles.subtitleMessageHeader }>
|
||||
{ participantName }
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
<Text style = { closedCaptionsStyles.subtitleMessageText }>{ text }</Text>
|
||||
<Text style = { closedCaptionsStyles.subtitleMessageTimestamp }>
|
||||
{ new Date(timestamp).toLocaleTimeString() }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
40
react/features/chat/components/native/SubtitlesGroup.tsx
Normal file
40
react/features/chat/components/native/SubtitlesGroup.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { View, ViewStyle } from 'react-native';
|
||||
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { ISubtitle } from '../../../subtitles/types';
|
||||
|
||||
import SubtitleMessage from './SubtitleMessage';
|
||||
import { closedCaptionsStyles } from './styles';
|
||||
|
||||
|
||||
interface IProps {
|
||||
messages: ISubtitle[];
|
||||
senderId: string;
|
||||
}
|
||||
|
||||
export function SubtitlesGroup({ messages, senderId }: IProps) {
|
||||
if (!messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style = { closedCaptionsStyles.subtitlesGroupContainer as ViewStyle }>
|
||||
<View style = { closedCaptionsStyles.subtitlesGroupAvatar as ViewStyle }>
|
||||
<Avatar
|
||||
participantId = { senderId }
|
||||
size = { 32 } />
|
||||
</View>
|
||||
<View style = { closedCaptionsStyles.subtitlesGroupMessagesContainer as ViewStyle }>
|
||||
{
|
||||
messages.map((message, index) => (
|
||||
<SubtitleMessage
|
||||
key = { `${message.timestamp}-${message.id}` }
|
||||
showDisplayName = { index === 0 }
|
||||
{ ...message } />
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { NativeScrollEvent, NativeSyntheticEvent, ScrollView, View, ViewStyle } from 'react-native';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { ISubtitle } from '../../../subtitles/types';
|
||||
|
||||
import { SubtitlesGroup } from './SubtitlesGroup';
|
||||
import { closedCaptionsStyles } from './styles';
|
||||
|
||||
/**
|
||||
* The threshold value used to determine if the user is at the bottom of the scroll view.
|
||||
*/
|
||||
const SCROLL_THRESHOLD = 50;
|
||||
|
||||
interface IProps {
|
||||
groups: Array<{
|
||||
messages: ISubtitle[];
|
||||
senderId: string;
|
||||
}>;
|
||||
messages: ISubtitle[];
|
||||
}
|
||||
|
||||
export function SubtitlesMessagesContainer({ messages, groups }: IProps) {
|
||||
const [ hasNewMessages, setHasNewMessages ] = useState(false);
|
||||
const [ isScrolledToBottom, setIsScrolledToBottom ] = useState(true);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const previousMessages = useRef(messages);
|
||||
|
||||
const scrollToBottom = useCallback((withAnimation: boolean) => {
|
||||
scrollViewRef.current?.scrollToEnd({ animated: withAnimation });
|
||||
}, []);
|
||||
|
||||
const handleNewMessagesClick = useCallback(() => {
|
||||
scrollToBottom(true);
|
||||
}, [ scrollToBottom ]);
|
||||
|
||||
const handleScroll = useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
|
||||
const isAtBottom = contentOffset.y + layoutMeasurement.height >= contentSize.height - SCROLL_THRESHOLD;
|
||||
|
||||
setIsScrolledToBottom(isAtBottom);
|
||||
if (isAtBottom) {
|
||||
setHasNewMessages(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom(false);
|
||||
}, [ scrollToBottom ]);
|
||||
|
||||
useEffect(() => {
|
||||
const newMessages = messages.filter(message => !previousMessages.current.includes(message));
|
||||
|
||||
if (newMessages.length > 0) {
|
||||
if (isScrolledToBottom) {
|
||||
scrollToBottom(false);
|
||||
} else {
|
||||
setHasNewMessages(true);
|
||||
}
|
||||
}
|
||||
|
||||
previousMessages.current = messages;
|
||||
}, [ messages, scrollToBottom ]);
|
||||
|
||||
return (
|
||||
<View style = { closedCaptionsStyles.subtitlesMessagesContainer as ViewStyle }>
|
||||
<ScrollView
|
||||
contentContainerStyle = { closedCaptionsStyles.subtitlesMessagesList as ViewStyle }
|
||||
onScroll = { handleScroll }
|
||||
ref = { scrollViewRef }
|
||||
scrollEventThrottle = { 16 }>
|
||||
{
|
||||
groups.map(group => (
|
||||
<SubtitlesGroup
|
||||
key = { `${group.senderId}-${group.messages[0]?.timestamp}` }
|
||||
messages = { group.messages }
|
||||
senderId = { group.senderId } />
|
||||
))
|
||||
}
|
||||
</ScrollView>
|
||||
{
|
||||
!isScrolledToBottom && hasNewMessages && (
|
||||
<View style = { closedCaptionsStyles.newMessagesButtonContainer as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'chat.newMessages'
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
icon = { () => (
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon04 }
|
||||
size = { 20 }
|
||||
src = { IconArrowDown } />
|
||||
) }
|
||||
labelKey = 'chat.newMessages'
|
||||
onClick = { handleNewMessagesClick }
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -14,12 +14,6 @@ const recipientContainer = {
|
||||
padding: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
const inputBar = {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the feature chat.
|
||||
*
|
||||
@@ -44,7 +38,8 @@ export default {
|
||||
},
|
||||
|
||||
emptyComponentText: {
|
||||
color: BaseTheme.palette.text03,
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
@@ -113,13 +108,22 @@ export default {
|
||||
},
|
||||
|
||||
emptyComponentWrapper: {
|
||||
alignSelf: 'center',
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: BoxModel.padding,
|
||||
paddingTop: '8%',
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
emptyListStyle: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
emptyListContentContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
disabledSendWrapper: {
|
||||
alignSelf: 'center',
|
||||
flex: 0,
|
||||
@@ -133,23 +137,23 @@ export default {
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
extraBarPadding: {
|
||||
paddingBottom: 30
|
||||
paddingBottom: BaseTheme.spacing[8]
|
||||
},
|
||||
|
||||
inputBarNarrow: {
|
||||
...inputBar,
|
||||
height: 112,
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
inputBar: {
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
inputBarWide: {
|
||||
...inputBar,
|
||||
height: 88,
|
||||
marginHorizontal: BaseTheme.spacing[9]
|
||||
sendButton: {
|
||||
marginRight: BaseTheme.spacing[5],
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
customInputContainer: {
|
||||
width: '75%'
|
||||
marginLeft: BaseTheme.spacing[5],
|
||||
flex: 1
|
||||
},
|
||||
|
||||
messageBubble: {
|
||||
@@ -205,11 +209,6 @@ export default {
|
||||
fontSize: 13
|
||||
},
|
||||
|
||||
chatContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
tabContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center'
|
||||
@@ -270,3 +269,128 @@ export default {
|
||||
flex: 1
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Styles for the ClosedCaptions component.
|
||||
*/
|
||||
export const closedCaptionsStyles = {
|
||||
container: {
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
emptyContentContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
|
||||
emptyContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
padding: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
emptyStateText: {
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
textAlign: 'center',
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
transcribingContainer: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
languageButtonContainer: {
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
padding: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
languageButtonText: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginHorizontal: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
languageButtonContent: {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
subtitleMessageContainer: {
|
||||
backgroundColor: BaseTheme.palette.ui02,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
padding: BaseTheme.spacing[2],
|
||||
maxWidth: '100%',
|
||||
marginTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
subtitleMessageContent: {
|
||||
maxWidth: '100%',
|
||||
flex: 1
|
||||
},
|
||||
|
||||
subtitleMessageHeader: {
|
||||
...BaseTheme.typography.labelBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
marginBottom: BaseTheme.spacing[1],
|
||||
maxWidth: 130
|
||||
},
|
||||
|
||||
subtitleMessageText: {
|
||||
...BaseTheme.typography.bodyShortRegular,
|
||||
color: BaseTheme.palette.text01
|
||||
},
|
||||
|
||||
subtitleMessageTimestamp: {
|
||||
...BaseTheme.typography.labelRegular,
|
||||
color: BaseTheme.palette.text03,
|
||||
marginTop: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
subtitleMessageInterim: {
|
||||
opacity: 0.7
|
||||
},
|
||||
|
||||
subtitlesGroupContainer: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
subtitlesGroupAvatar: {
|
||||
marginBottom: BaseTheme.spacing[10],
|
||||
marginRight: BaseTheme.spacing[2],
|
||||
alignSelf: 'flex-start',
|
||||
width: 32
|
||||
},
|
||||
|
||||
subtitlesGroupMessagesContainer: {
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
maxWidth: '100%'
|
||||
},
|
||||
|
||||
subtitlesMessagesContainer: {
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
height: '100%'
|
||||
},
|
||||
|
||||
subtitlesMessagesList: {
|
||||
padding: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
newMessagesButtonContainer: {
|
||||
position: 'absolute',
|
||||
bottom: BaseTheme.spacing[3],
|
||||
alignSelf: 'center'
|
||||
},
|
||||
|
||||
messagesContainer: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconSubtitles } from '../../../base/icons/svg';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { groupMessagesBySender } from '../../../base/util/messageGrouping';
|
||||
import { StartRecordingDialog } from '../../../recording/components/Recording';
|
||||
import { setRequestingSubtitles } from '../../../subtitles/actions.any';
|
||||
import LanguageSelector from '../../../subtitles/components/web/LanguageSelector';
|
||||
import { canStartSubtitles } from '../../../subtitles/functions.any';
|
||||
import { ISubtitle } from '../../../subtitles/types';
|
||||
import { isTranscribing } from '../../../transcribing/functions';
|
||||
// @ts-ignore
|
||||
import AbstractClosedCaptions, { AbstractProps } from '../AbstractClosedCaptions';
|
||||
|
||||
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
|
||||
|
||||
@@ -79,72 +72,19 @@ const useStyles = makeStyles()(theme => {
|
||||
*
|
||||
* @returns {JSX.Element} - The ClosedCaptionsTab component.
|
||||
*/
|
||||
export default function ClosedCaptionsTab() {
|
||||
const ClosedCaptionsTab = ({
|
||||
canStartSubtitles,
|
||||
filteredSubtitles,
|
||||
groupedSubtitles,
|
||||
isButtonPressed,
|
||||
isTranscribing,
|
||||
startClosedCaptions
|
||||
}: AbstractProps): JSX.Element => {
|
||||
const { classes, theme } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const subtitles = useSelector((state: IReduxState) => state['features/subtitles'].subtitlesHistory);
|
||||
const language = useSelector((state: IReduxState) => state['features/subtitles']._language);
|
||||
const selectedLanguage = language?.replace('translation-languages:', '');
|
||||
const _isTranscribing = useSelector(isTranscribing);
|
||||
const _canStartSubtitles = useSelector(canStartSubtitles);
|
||||
const [ isButtonPressed, setButtonPressed ] = useState(false);
|
||||
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
const filteredSubtitles = useMemo(() => {
|
||||
// First, create a map of transcription messages by message ID
|
||||
const transcriptionMessages = new Map(
|
||||
subtitles
|
||||
.filter(s => s.isTranscription)
|
||||
.map(s => [ s.id, s ])
|
||||
);
|
||||
|
||||
if (!selectedLanguage) {
|
||||
// When no language is selected, show all original transcriptions
|
||||
return Array.from(transcriptionMessages.values());
|
||||
}
|
||||
|
||||
// Then, create a map of translation messages by message ID
|
||||
const translationMessages = new Map(
|
||||
subtitles
|
||||
.filter(s => !s.isTranscription && s.language === selectedLanguage)
|
||||
.map(s => [ s.id, s ])
|
||||
);
|
||||
|
||||
// When a language is selected, for each transcription message:
|
||||
// 1. Use its translation if available
|
||||
// 2. Fall back to the original transcription if no translation exists
|
||||
return Array.from(transcriptionMessages.values())
|
||||
.filter((m: ISubtitle) => !m.interim)
|
||||
.map(m => translationMessages.get(m.id) ?? m);
|
||||
}, [ subtitles, selectedLanguage ]);
|
||||
|
||||
const groupedSubtitles = useMemo(() =>
|
||||
groupMessagesBySender(filteredSubtitles), [ filteredSubtitles ]);
|
||||
|
||||
const startClosedCaptions = useCallback(() => {
|
||||
if (isAsyncTranscriptionEnabled) {
|
||||
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog, {
|
||||
recordAudioAndVideo: false
|
||||
}));
|
||||
} else {
|
||||
if (isButtonPressed) {
|
||||
return;
|
||||
}
|
||||
dispatch(setRequestingSubtitles(true, false, null));
|
||||
setButtonPressed(true);
|
||||
}
|
||||
|
||||
}, [ isAsyncTranscriptionEnabled, dispatch, isButtonPressed, openDialog, setButtonPressed ]);
|
||||
|
||||
if (subtitlesError && isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
if (!_isTranscribing) {
|
||||
if (_canStartSubtitles) {
|
||||
if (!isTranscribing) {
|
||||
if (canStartSubtitles) {
|
||||
return (
|
||||
<div className = { classes.emptyContent }>
|
||||
<Button
|
||||
@@ -159,10 +99,6 @@ export default function ClosedCaptionsTab() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.emptyContent }>
|
||||
<Icon
|
||||
@@ -176,10 +112,6 @@ export default function ClosedCaptionsTab() {
|
||||
);
|
||||
}
|
||||
|
||||
if (isButtonPressed && !isAsyncTranscriptionEnabled) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<LanguageSelector />
|
||||
@@ -190,4 +122,6 @@ export default function ClosedCaptionsTab() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AbstractClosedCaptions(ClosedCaptionsTab);
|
||||
|
||||
@@ -21,10 +21,9 @@ const titleBarSafeView = {
|
||||
export default {
|
||||
|
||||
bottomContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bottom: BaseTheme.spacing[8]
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -101,7 +100,7 @@ export default {
|
||||
},
|
||||
|
||||
soundDeviceButton: {
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
marginVertical: BaseTheme.spacing[3],
|
||||
width: 240
|
||||
},
|
||||
|
||||
@@ -162,7 +161,6 @@ export default {
|
||||
videoStoppedLabel: {
|
||||
...BaseTheme.typography.bodyShortRegularLarge,
|
||||
color: BaseTheme.palette.text01,
|
||||
marginBottom: BaseTheme.spacing[3],
|
||||
textAlign: 'center',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
@@ -257,6 +257,9 @@ class Conference extends AbstractConference<IProps, any> {
|
||||
id = 'videospace'
|
||||
onTouchStart = { this._onVideospaceTouchStart }>
|
||||
<LargeVideo />
|
||||
<StageFilmstrip />
|
||||
<ScreenshareFilmstrip />
|
||||
<MainFilmstrip />
|
||||
</div>
|
||||
<span
|
||||
aria-level = { 1 }
|
||||
|
||||
@@ -212,7 +212,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const state = store.getState();
|
||||
const { defaultRemoteDisplayName } = state['features/base/config'];
|
||||
const { participant } = action;
|
||||
const { fakeParticipant, id, local, name } = participant;
|
||||
const { fakeParticipant, id, local, name, userContext } = participant;
|
||||
|
||||
// The version of external api outside of middleware did not emit
|
||||
// the local participant being created.
|
||||
@@ -225,7 +225,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
APP.API.notifyUserJoined(id, {
|
||||
displayName: name,
|
||||
formattedDisplayName: appendSuffix(
|
||||
name || defaultRemoteDisplayName)
|
||||
name || defaultRemoteDisplayName),
|
||||
userContext
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,11 @@ export interface IProps extends WithTranslation {
|
||||
*/
|
||||
_maxTopPanelHeight: number;
|
||||
|
||||
/**
|
||||
* Whethere reduced UI feature is enabled or not.
|
||||
*/
|
||||
_reducedUI: boolean;
|
||||
|
||||
/**
|
||||
* The participants in the call.
|
||||
*/
|
||||
@@ -546,6 +551,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
_filmstripDisabled,
|
||||
_localScreenShareId,
|
||||
_mainFilmstripVisible,
|
||||
_reducedUI,
|
||||
_resizableFilmstrip,
|
||||
_topPanelFilmstrip,
|
||||
_topPanelMaxHeight,
|
||||
@@ -589,6 +595,13 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
}
|
||||
}
|
||||
|
||||
// FIX: Until we move AudioTracksContainer to a more global place,
|
||||
// we apply this css hot fix to hide Filmstrip but keep AudioTracksContainer in the DOM,
|
||||
// so we don't have audio problems when reduced UI is enabled.
|
||||
if (_reducedUI) {
|
||||
filmstripStyle.display = 'none';
|
||||
}
|
||||
|
||||
let toolbar: React.ReactNode = null;
|
||||
|
||||
if (!this.props._iAmRecorder && this.props._isFilmstripButtonEnabled
|
||||
@@ -1120,6 +1133,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const _isVerticalFilmstrip = _currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW
|
||||
|| (filmstripType === FILMSTRIP_TYPE.MAIN && _currentLayout === LAYOUTS.STAGE_FILMSTRIP_VIEW);
|
||||
const { reducedUI } = state['features/base/responsive-ui'];
|
||||
|
||||
return {
|
||||
_className: className,
|
||||
@@ -1137,6 +1151,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_mainFilmstripVisible: notDisabled,
|
||||
_maxFilmstripWidth: videoSpaceWidth - MIN_STAGE_VIEW_WIDTH,
|
||||
_maxTopPanelHeight: clientHeight - MIN_STAGE_VIEW_HEIGHT,
|
||||
_reducedUI: reducedUI,
|
||||
_remoteParticipantsLength: _remoteParticipants?.length ?? 0,
|
||||
_topPanelHeight: topPanelHeight.current,
|
||||
_topPanelMaxHeight: topPanelHeight.current || TOP_FILMSTRIP_HEIGHT,
|
||||
|
||||
@@ -24,8 +24,6 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: 56,
|
||||
justifyContent: 'center',
|
||||
marginBottom: BaseTheme.spacing[0],
|
||||
paddingBottom: BaseTheme.spacing[4],
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { WithTranslation } from 'react-i18next';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
FlatList,
|
||||
SafeAreaView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle
|
||||
@@ -513,7 +512,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
*/
|
||||
_renderShareMeetingButton() {
|
||||
return (
|
||||
<SafeAreaView
|
||||
<View
|
||||
style = { [
|
||||
styles.bottomBar as ViewStyle,
|
||||
this.state.bottomPadding ? styles.extraBarPadding : null
|
||||
@@ -524,7 +523,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
src = { IconShare }
|
||||
style = { styles.shareIcon } />
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_LEFT,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_PASSWORD
|
||||
} from '../base/conference/actionTypes';
|
||||
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
|
||||
@@ -69,11 +70,13 @@ ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE,
|
||||
}
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_LEAVE:
|
||||
return {
|
||||
...state,
|
||||
isDisplayNameRequiredError: false,
|
||||
knocking: false,
|
||||
passwordJoinFailed: false
|
||||
lobbyVisible: false,
|
||||
passwordJoinFailed: false,
|
||||
};
|
||||
case KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED:
|
||||
return _knockingParticipantArrivedOrUpdated(action.participant, state);
|
||||
|
||||
@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -603,7 +603,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(false, false, null));
|
||||
store.dispatch(setRequestingSubtitles(false, false, null, true));
|
||||
}
|
||||
|
||||
if (![ JitsiRecordingConstants.mode.FILE, JitsiRecordingConstants.mode.STREAM ].includes(mode)) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { StyleProp, Text, TextStyle, View } from 'react-native';
|
||||
import { navigationStyles } from './styles';
|
||||
|
||||
interface ITabBarLabelCounterProps {
|
||||
activeUnreadNr: boolean;
|
||||
activeUnreadNr?: boolean;
|
||||
isFocused: boolean;
|
||||
label: string;
|
||||
unreadCount?: number;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getClientHeight,
|
||||
getClientWidth
|
||||
} from '../../../../../base/modal/components/functions';
|
||||
import { setFocusedTab } from '../../../../../chat/actions.any';
|
||||
import Chat from '../../../../../chat/components/native/Chat';
|
||||
import { ChatTabs } from '../../../../../chat/constants';
|
||||
import { getFocusedTab } from '../../../../../chat/functions';
|
||||
import { resetUnreadPollsCount } from '../../../../../polls/actions';
|
||||
import PollsPane from '../../../../../polls/components/native/PollsPane';
|
||||
import { screen } from '../../../routes';
|
||||
import { chatTabBarOptions } from '../../../screenOptions';
|
||||
|
||||
const ChatTab = createMaterialTopTabNavigator();
|
||||
|
||||
const ChatAndPolls = () => {
|
||||
const clientHeight = useSelector(getClientHeight);
|
||||
const clientWidth = useSelector(getClientWidth);
|
||||
const dispatch = useDispatch();
|
||||
const currentFocusedTab = useSelector(getFocusedTab);
|
||||
const initialRouteName = currentFocusedTab === ChatTabs.POLLS
|
||||
? screen.conference.chatandpolls.tab.polls
|
||||
: screen.conference.chatandpolls.tab.chat;
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ChatTab.Navigator
|
||||
backBehavior = 'none'
|
||||
initialLayout = {{
|
||||
height: clientHeight,
|
||||
width: clientWidth
|
||||
}}
|
||||
initialRouteName = { initialRouteName }
|
||||
screenOptions = { chatTabBarOptions }>
|
||||
<ChatTab.Screen
|
||||
component = { Chat }
|
||||
listeners = {{
|
||||
tabPress: () => {
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
}
|
||||
}}
|
||||
name = { screen.conference.chatandpolls.tab.chat } />
|
||||
<ChatTab.Screen
|
||||
component = { PollsPane }
|
||||
listeners = {{
|
||||
tabPress: () => {
|
||||
dispatch(setFocusedTab(ChatTabs.POLLS));
|
||||
dispatch(resetUnreadPollsCount);
|
||||
}
|
||||
}}
|
||||
name = { screen.conference.chatandpolls.tab.polls } />
|
||||
</ChatTab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatAndPolls;
|
||||
@@ -0,0 +1,88 @@
|
||||
/* eslint-disable lines-around-comment */
|
||||
|
||||
import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import {
|
||||
getClientHeight,
|
||||
getClientWidth
|
||||
} from '../../../../../base/modal/components/functions.native';
|
||||
import { setFocusedTab } from '../../../../../chat/actions.native';
|
||||
import Chat from '../../../../../chat/components/native/Chat';
|
||||
import ClosedCaptions from '../../../../../chat/components/native/ClosedCaptions';
|
||||
import { ChatTabs } from '../../../../../chat/constants';
|
||||
import { getFocusedTab, isChatDisabled } from '../../../../../chat/functions';
|
||||
import { arePollsDisabled } from '../../../../../conference/functions.native';
|
||||
import { resetUnreadPollsCount } from '../../../../../polls/actions';
|
||||
import PollsPane from '../../../../../polls/components/native/PollsPane';
|
||||
import { isCCTabEnabled } from '../../../../../subtitles/functions.any';
|
||||
import { screen } from '../../../routes';
|
||||
import { chatTabBarOptions } from '../../../screenOptions';
|
||||
|
||||
const ChatTab = createMaterialTopTabNavigator();
|
||||
|
||||
const ChatNavigator = () => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const clientHeight = useSelector(getClientHeight);
|
||||
const clientWidth = useSelector(getClientWidth);
|
||||
const currentFocusedTab = useSelector(getFocusedTab);
|
||||
const isPollsTabDisabled = useSelector(arePollsDisabled);
|
||||
const isChatTabDisabled = useSelector(isChatDisabled);
|
||||
const isCCTabDisabled = !useSelector(isCCTabEnabled);
|
||||
|
||||
const initialRouteName
|
||||
= currentFocusedTab === ChatTabs.POLLS ? screen.conference.chatTabs.tab.polls
|
||||
: currentFocusedTab === ChatTabs.CLOSED_CAPTIONS ? screen.conference.chatTabs.tab.closedCaptions
|
||||
: screen.conference.chatTabs.tab.chat;
|
||||
|
||||
return (
|
||||
// @ts-ignore
|
||||
<ChatTab.Navigator
|
||||
backBehavior = 'none'
|
||||
initialLayout = {{
|
||||
height: clientHeight,
|
||||
width: clientWidth
|
||||
}}
|
||||
initialRouteName = { initialRouteName }
|
||||
screenOptions = { chatTabBarOptions }>
|
||||
{
|
||||
!isChatTabDisabled
|
||||
&& <ChatTab.Screen
|
||||
component = { Chat }
|
||||
listeners = {{
|
||||
tabPress: () => {
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
}
|
||||
}}
|
||||
name = { screen.conference.chatTabs.tab.chat } />
|
||||
}
|
||||
{
|
||||
!isPollsTabDisabled
|
||||
&& <ChatTab.Screen
|
||||
component = { PollsPane }
|
||||
listeners = {{
|
||||
tabPress: () => {
|
||||
dispatch(setFocusedTab(ChatTabs.POLLS));
|
||||
dispatch(resetUnreadPollsCount);
|
||||
}
|
||||
}}
|
||||
name = { screen.conference.chatTabs.tab.polls } />
|
||||
}
|
||||
{
|
||||
!isCCTabDisabled
|
||||
&& <ChatTab.Screen
|
||||
component = { ClosedCaptions }
|
||||
listeners = {{
|
||||
tabPress: () => {
|
||||
dispatch(setFocusedTab(ChatTabs.CLOSED_CAPTIONS));
|
||||
}
|
||||
}}
|
||||
name = { screen.conference.chatTabs.tab.closedCaptions } />
|
||||
}
|
||||
</ChatTab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatNavigator;
|
||||
@@ -43,6 +43,7 @@ import SpeakerStats
|
||||
import LanguageSelectorDialog
|
||||
// @ts-ignore
|
||||
from '../../../../../subtitles/components/native/LanguageSelectorDialog';
|
||||
import { isCCTabEnabled } from '../../../../../subtitles/functions.any';
|
||||
import Whiteboard from '../../../../../whiteboard/components/native/Whiteboard';
|
||||
// @ts-ignore
|
||||
import { screen } from '../../../routes';
|
||||
@@ -68,7 +69,7 @@ import {
|
||||
// @ts-ignore
|
||||
} from '../../../screenOptions';
|
||||
// @ts-ignore
|
||||
import ChatAndPollsNavigator from '../../chat/components/ChatAndPollsNavigator';
|
||||
import ChatNavigator from '../../chat/components/ChatNavigator';
|
||||
// @ts-ignore
|
||||
import LobbyNavigationContainer from '../../lobby/components/LobbyNavigationContainer';
|
||||
// @ts-ignore
|
||||
@@ -84,20 +85,32 @@ const ConferenceStack = createStackNavigator();
|
||||
|
||||
const ConferenceNavigationContainer = () => {
|
||||
const isPollsDisabled = useSelector(arePollsDisabled);
|
||||
const _isPollsEnabled = !isPollsDisabled;
|
||||
const _isCCTabEnabled = useSelector(isCCTabEnabled);
|
||||
const { t } = useTranslation();
|
||||
|
||||
let ChatScreen;
|
||||
let chatScreenName;
|
||||
let chatTitleString;
|
||||
let chatTitle;
|
||||
|
||||
if (isPollsDisabled) {
|
||||
ChatScreen = Chat;
|
||||
chatScreenName = screen.conference.chat;
|
||||
chatTitleString = 'chat.title';
|
||||
} else {
|
||||
ChatScreen = ChatAndPollsNavigator;
|
||||
chatScreenName = screen.conference.chatandpolls.main;
|
||||
chatTitleString = 'chat.titleWithPolls';
|
||||
ChatScreen = ChatNavigator;
|
||||
chatScreenName = screen.conference.chatTabs.main;
|
||||
}
|
||||
|
||||
if (_isPollsEnabled || _isCCTabEnabled) {
|
||||
const features = [
|
||||
_isPollsEnabled ? t('chat.titleWithPolls') : '',
|
||||
_isCCTabEnabled ? t('chat.titleWithCC') : ''
|
||||
].filter(Boolean);
|
||||
|
||||
chatTitle = `${t('chat.titleWithFeatures')} ${features.join(' and ')}`;
|
||||
} else {
|
||||
chatTitle = t('chat.title');
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<NavigationContainer
|
||||
@@ -117,7 +130,7 @@ const ConferenceNavigationContainer = () => {
|
||||
name = { chatScreenName }
|
||||
options = {{
|
||||
...chatScreenOptions,
|
||||
title: t(chatTitleString)
|
||||
title: chatTitle
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { ParticipantsPane }
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
welcomeScreenOptions
|
||||
} from '../../../screenOptions';
|
||||
import {
|
||||
goBack,
|
||||
settingsNavigationContainerRef
|
||||
} from '../SettingsNavigationContainerRef';
|
||||
|
||||
@@ -51,6 +52,7 @@ const SettingsNavigationContainer = ({ isInWelcomePage }: IProps) => {
|
||||
|
||||
const LanguageSelectScreen = useCallback(() =>
|
||||
(<LanguageSelectView
|
||||
goBack = { goBack }
|
||||
isInWelcomePage = { isInWelcomePage } />)
|
||||
, []);
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ export const screen = {
|
||||
breakoutRooms: 'Breakout Rooms',
|
||||
carmode: 'Car Mode',
|
||||
chat: 'Chat',
|
||||
chatandpolls: {
|
||||
main: 'Chat and Polls',
|
||||
chatTabs: {
|
||||
main: 'Chat',
|
||||
tab: {
|
||||
chat: 'Chat',
|
||||
polls: 'Polls'
|
||||
polls: 'Polls',
|
||||
closedCaptions: 'CC'
|
||||
}
|
||||
},
|
||||
container: 'Conference container',
|
||||
|
||||
@@ -13,7 +13,6 @@ export interface INotificationProps {
|
||||
hideErrorSupportLink?: boolean;
|
||||
icon?: string;
|
||||
maxLines?: number;
|
||||
sticky?: boolean;
|
||||
title?: string;
|
||||
titleArguments?: {
|
||||
[key: string]: string | number;
|
||||
|
||||
@@ -27,7 +27,7 @@ const LobbyParticipantList = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<View style = { styles.meetingListContainer }>
|
||||
<View style = { styles.listDetails as ViewStyle } >
|
||||
<Text style = { styles.lobbyListDescription as TextStyle }>
|
||||
{ title }
|
||||
@@ -50,7 +50,7 @@ const LobbyParticipantList = () => {
|
||||
participant = { p } />)
|
||||
)
|
||||
}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import React, { useCallback } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
@@ -10,14 +9,18 @@ import {
|
||||
hasRaisedHand,
|
||||
isParticipantModerator
|
||||
} from '../../../base/participants/functions';
|
||||
import { FakeParticipant, IParticipant } from '../../../base/participants/types';
|
||||
import { FakeParticipant } from '../../../base/participants/types';
|
||||
import {
|
||||
isParticipantAudioMuted,
|
||||
isParticipantVideoMuted
|
||||
} from '../../../base/tracks/functions.native';
|
||||
import { showContextMenuDetails, showSharedVideoMenu } from '../../actions.native';
|
||||
import type { MediaState } from '../../constants';
|
||||
import { getParticipantAudioMediaState, getParticipantVideoMediaState } from '../../functions';
|
||||
import {
|
||||
getParticipantAudioMediaState,
|
||||
getParticipantVideoMediaState,
|
||||
participantMatchesSearch
|
||||
} from '../../functions';
|
||||
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
|
||||
@@ -59,9 +62,9 @@ interface IProps {
|
||||
_localVideoOwner: boolean;
|
||||
|
||||
/**
|
||||
* The participant ID.
|
||||
* Whether or not the participant name matches the search string.
|
||||
*/
|
||||
_participantID: string;
|
||||
_matchesSearch: boolean;
|
||||
|
||||
/**
|
||||
* True if the participant have raised hand.
|
||||
@@ -74,85 +77,55 @@ interface IProps {
|
||||
_videoMediaState: MediaState;
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
* The participant ID.
|
||||
*/
|
||||
dispatch: IStore['dispatch'];
|
||||
participantID: string;
|
||||
|
||||
/**
|
||||
* The participant.
|
||||
* Name of the participant we search for.
|
||||
*/
|
||||
participant?: IParticipant;
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the MeetingParticipantItem component.
|
||||
*/
|
||||
class MeetingParticipantItem extends PureComponent<IProps> {
|
||||
|
||||
/**
|
||||
* Creates new MeetingParticipantItem instance.
|
||||
*
|
||||
* @param {IProps} props - The props of the component.
|
||||
*/
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this._onPress = this._onPress.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles MeetingParticipantItem press events.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onPress() {
|
||||
const {
|
||||
_fakeParticipant,
|
||||
_local,
|
||||
_localVideoOwner,
|
||||
_participantID,
|
||||
dispatch
|
||||
} = this.props;
|
||||
|
||||
const MeetingParticipantItem = ({
|
||||
_audioMediaState,
|
||||
_disableModeratorIndicator,
|
||||
_displayName,
|
||||
_fakeParticipant,
|
||||
_isModerator,
|
||||
_local,
|
||||
_localVideoOwner,
|
||||
_matchesSearch,
|
||||
_raisedHand,
|
||||
_videoMediaState,
|
||||
participantID
|
||||
}: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const onPress = useCallback(() => {
|
||||
if (_fakeParticipant && _localVideoOwner) {
|
||||
dispatch(showSharedVideoMenu(_participantID));
|
||||
dispatch(showSharedVideoMenu(participantID));
|
||||
} else if (!_fakeParticipant) {
|
||||
dispatch(showContextMenuDetails(_participantID, _local));
|
||||
dispatch(showContextMenuDetails(participantID, _local));
|
||||
} // else no-op
|
||||
}, [ dispatch ]);
|
||||
|
||||
if (!_matchesSearch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_audioMediaState,
|
||||
_disableModeratorIndicator,
|
||||
_displayName,
|
||||
_isModerator,
|
||||
_local,
|
||||
_participantID,
|
||||
_raisedHand,
|
||||
_videoMediaState
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ParticipantItem
|
||||
audioMediaState = { _audioMediaState }
|
||||
disableModeratorIndicator = { _disableModeratorIndicator }
|
||||
displayName = { _displayName }
|
||||
isModerator = { _isModerator }
|
||||
local = { _local }
|
||||
onPress = { this._onPress }
|
||||
participantID = { _participantID }
|
||||
raisedHand = { _raisedHand }
|
||||
videoMediaState = { _videoMediaState } />
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ParticipantItem
|
||||
audioMediaState = { _audioMediaState }
|
||||
disableModeratorIndicator = { _disableModeratorIndicator }
|
||||
displayName = { _displayName }
|
||||
isModerator = { _isModerator }
|
||||
local = { _local }
|
||||
onPress = { onPress }
|
||||
participantID = { participantID }
|
||||
raisedHand = { _raisedHand }
|
||||
videoMediaState = { _videoMediaState } />
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
@@ -163,8 +136,9 @@ class MeetingParticipantItem extends PureComponent<IProps> {
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { participant } = ownProps;
|
||||
const { participantID, searchString } = ownProps;
|
||||
const { ownerId } = state['features/shared-video'];
|
||||
const participant = getParticipantById(state, participantID);
|
||||
const localParticipantId = getLocalParticipant(state)?.id;
|
||||
const _isAudioMuted = isParticipantAudioMuted(participant, state);
|
||||
const _isVideoMuted = isParticipantVideoMuted(participant, state);
|
||||
@@ -173,23 +147,24 @@ function mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { disableModeratorIndicator } = state['features/base/config'];
|
||||
const raisedHand = hasRaisedHand(participant?.local
|
||||
? participant
|
||||
: getParticipantById(state, participant?.id)
|
||||
: getParticipantById(state, participantID)
|
||||
);
|
||||
const _matchesSearch = participantMatchesSearch(participant, searchString);
|
||||
|
||||
return {
|
||||
_audioMediaState: audioMediaState,
|
||||
_disableModeratorIndicator: disableModeratorIndicator,
|
||||
_displayName: getParticipantDisplayName(state, participant?.id),
|
||||
_displayName: getParticipantDisplayName(state, participantID),
|
||||
_fakeParticipant: participant?.fakeParticipant,
|
||||
_isAudioMuted,
|
||||
_isModerator: isParticipantModerator(participant),
|
||||
_local: Boolean(participant?.local),
|
||||
_localVideoOwner: Boolean(ownerId === localParticipantId),
|
||||
_participantID: participant?.id,
|
||||
_matchesSearch,
|
||||
_raisedHand: raisedHand,
|
||||
_videoMediaState: videoMediaState
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default translate(connect(mapStateToProps)(MeetingParticipantItem));
|
||||
// @ts-ignore
|
||||
export default connect(mapStateToProps)(MeetingParticipantItem);
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlatList, Text, TextStyle, View } from 'react-native';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconAddUser } from '../../../base/icons/svg';
|
||||
import {
|
||||
addPeopleFeatureControl,
|
||||
getLocalParticipant,
|
||||
getParticipantCountWithFake,
|
||||
getRemoteParticipants,
|
||||
getParticipantById,
|
||||
isScreenShareParticipant,
|
||||
setShareDialogVisiblity
|
||||
} from '../../../base/participants/functions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
@@ -23,57 +22,70 @@ import {
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
import { getInviteOthersControl } from '../../../share-room/functions';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
|
||||
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
|
||||
|
||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||
import styles from './styles';
|
||||
|
||||
interface IProps {
|
||||
currentRoom?: {
|
||||
jid: string;
|
||||
name: string;
|
||||
};
|
||||
iconColor: string;
|
||||
isAddPeopleFeatureEnabled?: boolean | undefined;
|
||||
isShareDialogVisible: boolean;
|
||||
participantsCount?: number;
|
||||
showInviteButton?: boolean;
|
||||
sortedParticipantIds?: Array<string>;
|
||||
visitorsCount?: number | undefined;
|
||||
}
|
||||
|
||||
|
||||
const MeetingParticipantList = ({
|
||||
currentRoom,
|
||||
iconColor,
|
||||
isAddPeopleFeatureEnabled,
|
||||
isShareDialogVisible,
|
||||
participantsCount,
|
||||
showInviteButton,
|
||||
sortedParticipantIds = [],
|
||||
visitorsCount
|
||||
}: IProps): any => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [ searchString, setSearchString ] = useState('');
|
||||
|
||||
const MeetingParticipantList = () => {
|
||||
const currentRoomId = useSelector(getCurrentRoomId);
|
||||
const currentRoom = useSelector(getBreakoutRooms)[currentRoomId];
|
||||
const dispatch = useDispatch();
|
||||
const inviteOthersControl = useSelector(getInviteOthersControl);
|
||||
const isAddPeopleFeatureEnabled = useSelector(addPeopleFeatureControl);
|
||||
const keyExtractor
|
||||
= useCallback((e: undefined, i: number) => i.toString(), []);
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const _iAmVisitor = useSelector(iAmVisitor);
|
||||
|
||||
const keyExtractor = useCallback((e: undefined, i: number) => i.toString(), []);
|
||||
const onInvite = useCallback(() => {
|
||||
setShareDialogVisiblity(isAddPeopleFeatureEnabled, dispatch);
|
||||
dispatch(doInvitePeople());
|
||||
}, [ dispatch ]);
|
||||
const [ searchString, setSearchString ] = useState('');
|
||||
const onSearchStringChange = useCallback((text: string) =>
|
||||
setSearchString(text), []);
|
||||
const participantsCount = useSelector(getParticipantCountWithFake);
|
||||
const remoteParticipants = useSelector(getRemoteParticipants);
|
||||
const renderParticipant = ({ item/* , index, separators */ }: any) => {
|
||||
const participant = item === localParticipant?.id
|
||||
? localParticipant : remoteParticipants.get(item);
|
||||
|
||||
if (participantMatchesSearch(participant, searchString)) {
|
||||
return (
|
||||
<MeetingParticipantItem
|
||||
key = { item }
|
||||
participant = { participant } />
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
const showInviteButton = useSelector(shouldRenderInviteButton);
|
||||
const sortedRemoteParticipants = useSelector(
|
||||
(state: IReduxState) => state['features/filmstrip'].remoteParticipants);
|
||||
const { t } = useTranslation();
|
||||
const title = currentRoom?.name
|
||||
? `${currentRoom.name} (${participantsCount})`
|
||||
: t('participantsPane.headings.participantsList',
|
||||
{ count: participantsCount });
|
||||
const { color, shareDialogVisible } = inviteOthersControl;
|
||||
const visitorsLabelText = visitorsCount && visitorsCount > 0
|
||||
? t('participantsPane.headings.visitors', { count: visitorsCount })
|
||||
: undefined;
|
||||
|
||||
const renderParticipant = ({ item }: any) => (
|
||||
<MeetingParticipantItem
|
||||
key = { item }
|
||||
participantID = { item }
|
||||
searchString = { searchString } />
|
||||
);
|
||||
|
||||
return (
|
||||
<View style = { styles.meetingListContainer }>
|
||||
<Text style = { styles.visitorsLabel as TextStyle }>
|
||||
{ visitorsLabelText }
|
||||
</Text>
|
||||
<Text
|
||||
style = { styles.meetingListDescription as TextStyle }>
|
||||
{ title }
|
||||
@@ -82,12 +94,12 @@ const MeetingParticipantList = () => {
|
||||
showInviteButton
|
||||
&& <Button
|
||||
accessibilityLabel = 'participantsPane.actions.invite'
|
||||
disabled = { shareDialogVisible }
|
||||
disabled = { isShareDialogVisible }
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-bind, no-confusing-arrow
|
||||
icon = { () => (
|
||||
<Icon
|
||||
color = { color }
|
||||
color = { iconColor }
|
||||
size = { 20 }
|
||||
src = { IconAddUser } />
|
||||
) }
|
||||
@@ -105,10 +117,7 @@ const MeetingParticipantList = () => {
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
<FlatList
|
||||
data = { _iAmVisitor
|
||||
? [ ...sortedRemoteParticipants ]
|
||||
: [ localParticipant?.id, ...sortedRemoteParticipants ] as Array<any>
|
||||
}
|
||||
data = { sortedParticipantIds as Array<any> }
|
||||
keyExtractor = { keyExtractor }
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
@@ -118,4 +127,48 @@ const MeetingParticipantList = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default MeetingParticipantList;
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated props for this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
let sortedParticipantIds: any = getSortedParticipantIds(state);
|
||||
|
||||
const _iAmVisitor = iAmVisitor(state);
|
||||
|
||||
sortedParticipantIds = sortedParticipantIds.filter((id: any) => {
|
||||
const participant = getParticipantById(state, id);
|
||||
|
||||
if (_iAmVisitor && participant?.local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isScreenShareParticipant(participant);
|
||||
});
|
||||
|
||||
const currentRoomId = getCurrentRoomId(state);
|
||||
const currentRoom = getBreakoutRooms(state)[currentRoomId];
|
||||
const inviteOthersControl = getInviteOthersControl(state);
|
||||
const { color, shareDialogVisible } = inviteOthersControl;
|
||||
const isAddPeopleFeatureEnabled = addPeopleFeatureControl(state);
|
||||
const participantsCount = sortedParticipantIds.length;
|
||||
const showInviteButton = shouldRenderInviteButton(state);
|
||||
const visitorsCount = state['features/visitors']?.count || 0;
|
||||
|
||||
return {
|
||||
currentRoom,
|
||||
iconColor: color,
|
||||
inviteOthersControl,
|
||||
isAddPeopleFeatureEnabled,
|
||||
isShareDialogVisible: shareDialogVisible,
|
||||
participantsCount,
|
||||
showInviteButton,
|
||||
sortedParticipantIds,
|
||||
visitorsCount
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(_mapStateToProps)(MeetingParticipantList);
|
||||
|
||||
@@ -94,8 +94,6 @@ function ParticipantItem({
|
||||
}: IProps) {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const participantNameContainerStyles
|
||||
= isKnockingParticipant ? styles.lobbyParticipantNameContainer : styles.participantNameContainer;
|
||||
|
||||
return (
|
||||
<View style = { styles.participantContainer as StyleProp<ViewStyle> } >
|
||||
@@ -106,12 +104,8 @@ function ParticipantItem({
|
||||
displayName = { displayName }
|
||||
participantId = { participantID }
|
||||
size = { 32 } />
|
||||
<View
|
||||
style = { [
|
||||
styles.participantDetailsContainer,
|
||||
raisedHand && styles.participantDetailsContainerRaisedHand
|
||||
] as StyleProp<ViewStyle> }>
|
||||
<View style = { participantNameContainerStyles as StyleProp<ViewStyle> }>
|
||||
<View>
|
||||
<View style = { styles.participantNameContainer as StyleProp<ViewStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.participantName as StyleProp<TextStyle> }>
|
||||
@@ -128,13 +122,11 @@ function ParticipantItem({
|
||||
</View>
|
||||
{
|
||||
!isKnockingParticipant
|
||||
&& <>
|
||||
{ raisedHand && <RaisedHandIndicator /> }
|
||||
<View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
</>
|
||||
&& <View style = { styles.participantStatesContainer as StyleProp<ViewStyle> }>
|
||||
<View>{ raisedHand && <RaisedHandIndicator /> }</View>
|
||||
<View style = { styles.participantStateVideo }>{ VideoStateIcons[videoMediaState] }</View>
|
||||
<View>{ AudioStateIcons[audioMediaState] }</View>
|
||||
</View>
|
||||
}
|
||||
</TouchableOpacity>
|
||||
{ !local && children }
|
||||
|
||||
@@ -12,11 +12,6 @@ import VisitorsList from './VisitorsList';
|
||||
import styles from './styles';
|
||||
|
||||
|
||||
/**
|
||||
* Participants pane.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
const ParticipantsPane = () => {
|
||||
const isLocalModerator = useSelector(isLocalParticipantModerator);
|
||||
const keyExtractor
|
||||
|
||||
@@ -6,12 +6,8 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
const participantListDescription = {
|
||||
...BaseTheme.typography.heading6,
|
||||
color: BaseTheme.palette.text01,
|
||||
fontSize: 15,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: BaseTheme.spacing[2],
|
||||
paddingVertical: BaseTheme.spacing[2],
|
||||
position: 'relative',
|
||||
width: '70%'
|
||||
paddingVertical: BaseTheme.spacing[2]
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,13 +48,6 @@ const contextMenuItem = {
|
||||
marginLeft: BaseTheme.spacing[3]
|
||||
};
|
||||
|
||||
const participantNameContainer = {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3]
|
||||
};
|
||||
|
||||
/**
|
||||
* The styles of the native components of the feature {@code participants}.
|
||||
*/
|
||||
@@ -91,8 +80,7 @@ export default {
|
||||
flexDirection: 'row',
|
||||
height: BaseTheme.spacing[9],
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
participantContent: {
|
||||
@@ -106,24 +94,11 @@ export default {
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
participantDetailsContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '73%'
|
||||
},
|
||||
|
||||
participantDetailsContainerRaisedHand: {
|
||||
width: '65%'
|
||||
},
|
||||
|
||||
participantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
lobbyParticipantNameContainer: {
|
||||
...participantNameContainer,
|
||||
width: '40%'
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
paddingLeft: BaseTheme.spacing[3],
|
||||
},
|
||||
|
||||
participantName: {
|
||||
@@ -139,19 +114,19 @@ export default {
|
||||
},
|
||||
|
||||
participantStatesContainer: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
marginLeft: 'auto',
|
||||
width: '15%'
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
participantStateVideo: {
|
||||
paddingRight: BaseTheme.spacing[3]
|
||||
marginRight: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
raisedHandIndicator: {
|
||||
backgroundColor: BaseTheme.palette.warning02,
|
||||
borderRadius: BaseTheme.shape.borderRadius / 2,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
height: BaseTheme.spacing[4],
|
||||
width: BaseTheme.spacing[4],
|
||||
marginLeft: 'auto',
|
||||
@@ -166,7 +141,7 @@ export default {
|
||||
|
||||
buttonAdmit: {
|
||||
position: 'absolute',
|
||||
right: 16
|
||||
right: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
buttonReject: {
|
||||
@@ -201,20 +176,14 @@ export default {
|
||||
},
|
||||
|
||||
participantsPaneFooterContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
height: 128,
|
||||
left: 0,
|
||||
paddingHorizontal: BaseTheme.spacing[4],
|
||||
right: 0
|
||||
alignSelf: 'stretch',
|
||||
marginHorizontal: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
participantsPaneFooter: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-end',
|
||||
paddingBottom: BaseTheme.spacing[3],
|
||||
width: '100%'
|
||||
marginLeft: 'auto'
|
||||
},
|
||||
|
||||
inviteButton: {
|
||||
@@ -275,13 +244,11 @@ export default {
|
||||
},
|
||||
|
||||
inputContainer: {
|
||||
marginLeft: BaseTheme.spacing[3],
|
||||
marginRight: BaseTheme.spacing[3],
|
||||
marginBottom: BaseTheme.spacing[4]
|
||||
marginHorizontal: BaseTheme.spacing[3],
|
||||
marginBottom: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
centerInput: {
|
||||
paddingRight: BaseTheme.spacing[3],
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../b
|
||||
import { isButtonEnabled, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
|
||||
import { useParticipantDrawer } from '../../hooks';
|
||||
import { useParticipantDrawer } from '../../hooks.web';
|
||||
import RenameButton from '../breakout-rooms/components/web/RenameButton';
|
||||
|
||||
import { InviteButton } from './InviteButton';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FlatList, Platform, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { FlatList, SafeAreaView, TextInput, View, ViewStyle } from 'react-native';
|
||||
import { Divider } from 'react-native-paper';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
@@ -97,9 +97,6 @@ const PollCreate = (props: AbstractProps) => {
|
||||
type = { TERTIARY } />
|
||||
);
|
||||
|
||||
const pollCreateButtonsContainerStyles = Platform.OS === 'android'
|
||||
? pollsStyles.pollCreateButtonsContainerAndroid : pollsStyles.pollCreateButtonsContainerIos;
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
const renderListItem = ({ index }: { index: number; }) => {
|
||||
|
||||
@@ -160,7 +157,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
), [ question ]);
|
||||
|
||||
return (
|
||||
<View style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<SafeAreaView style = { pollsStyles.pollCreateContainer as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateSubContainer as ViewStyle }>
|
||||
<FlatList
|
||||
ListHeaderComponent = { renderListHeaderComponent }
|
||||
@@ -169,7 +166,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { answerListRef }
|
||||
renderItem = { renderListItem } />
|
||||
<View style = { pollCreateButtonsContainerStyles as ViewStyle }>
|
||||
<View style = { pollsStyles.pollCreateButtonsContainer as ViewStyle }>
|
||||
<Button
|
||||
accessibilityLabel = 'polls.create.addOption'
|
||||
disabled = { answers.length >= ANSWERS_LIMIT }
|
||||
@@ -207,7 +204,7 @@ const PollCreate = (props: AbstractProps) => {
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,32 +38,36 @@ const PollsList = ({ setCreateMode }: IPollListProps) => {
|
||||
scrollToBottom();
|
||||
}, [ polls ]);
|
||||
|
||||
const renderEmptyComponent = useCallback(() => (
|
||||
<View style = { pollsStyles.noPollContent as ViewStyle }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon03 }
|
||||
size = { 100 }
|
||||
src = { IconMessage } />
|
||||
<Text
|
||||
id = 'no-polls-text'
|
||||
style = { pollsStyles.noPollText as TextStyle } >
|
||||
{
|
||||
t('polls.results.empty')
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
), [ t ]);
|
||||
|
||||
const noPolls = listPolls.length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
listPolls.length === 0
|
||||
&& <View style = { pollsStyles.noPollContent as ViewStyle }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon03 }
|
||||
size = { 160 }
|
||||
src = { IconMessage } />
|
||||
<Text
|
||||
id = 'no-polls-text'
|
||||
style = { pollsStyles.noPollText as TextStyle } >
|
||||
{
|
||||
t('polls.results.empty')
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
<FlatList
|
||||
data = { listPolls }
|
||||
extraData = { listPolls }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { flatlistRef }
|
||||
renderItem = { renderItem } />
|
||||
</>
|
||||
<FlatList
|
||||
ListEmptyComponent = { renderEmptyComponent }
|
||||
// @ts-ignore
|
||||
contentContainerStyle = { noPolls && pollsStyles.emptyListContentContainer as ViewStyle }
|
||||
data = { listPolls }
|
||||
extraData = { listPolls }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
keyExtractor = { (item, index) => index.toString() }
|
||||
ref = { flatlistRef }
|
||||
renderItem = { renderItem }
|
||||
style = { noPolls && pollsStyles.emptyListStyle as ViewStyle } />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Platform } from 'react-native';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
@@ -42,28 +41,27 @@ const PollsPane = (props: AbstractProps) => {
|
||||
|
||||
}, [ isPollsTabFocused, unreadPollsCount ]);
|
||||
|
||||
const createPollButtonStyles = Platform.OS === 'android'
|
||||
? pollsStyles.createPollButtonAndroid : pollsStyles.createPollButtonIos;
|
||||
const renderCreateAPollBtn = useCallback(() => (
|
||||
!createMode && !isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
id = { t('polls.create.create') }
|
||||
labelKey = 'polls.create.create'
|
||||
onClick = { onCreate }
|
||||
style = { pollsStyles.createPollButton }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
), [ createMode, isCreatePollsDisabled ]);
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
contentContainerStyle = { pollsStyles.pollPane as StyleType }
|
||||
disableForcedKeyboardDismiss = { true }
|
||||
footerComponent = { renderCreateAPollBtn }
|
||||
hasExtraHeaderHeight = { true }
|
||||
style = { pollsStyles.pollPaneContainer as StyleType }>
|
||||
{
|
||||
createMode
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <>
|
||||
<PollsList setCreateMode = { setCreateMode } />
|
||||
{!isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
id = { t('polls.create.create') }
|
||||
labelKey = 'polls.create.create'
|
||||
onClick = { onCreate }
|
||||
style = { createPollButtonStyles }
|
||||
type = { BUTTON_TYPES.PRIMARY } />}
|
||||
</>
|
||||
: <PollsList setCreateMode = { setCreateMode } />
|
||||
}
|
||||
</JitsiScreen>
|
||||
);
|
||||
|
||||
@@ -117,21 +117,26 @@ export const resultsStyles = createStyleSheet({
|
||||
|
||||
export const pollsStyles = createStyleSheet({
|
||||
|
||||
emptyListStyle: {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
emptyListContentContainer: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
noPollContent: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: '25%'
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
noPollText: {
|
||||
flex: 1,
|
||||
color: BaseTheme.palette.text03,
|
||||
...BaseTheme.typography.bodyLongBold,
|
||||
color: BaseTheme.palette.text02,
|
||||
textAlign: 'center',
|
||||
maxWidth: '70%'
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
pollItemContainer: {
|
||||
@@ -153,13 +158,7 @@ export const pollsStyles = createStyleSheet({
|
||||
marginTop: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerAndroid: {
|
||||
marginBottom: BaseTheme.spacing[8],
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
pollCreateButtonsContainerIos: {
|
||||
marginBottom: BaseTheme.spacing[5],
|
||||
pollCreateButtonsContainer: {
|
||||
marginHorizontal: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
@@ -205,14 +204,8 @@ export const pollsStyles = createStyleSheet({
|
||||
color: BaseTheme.palette.action01
|
||||
},
|
||||
|
||||
createPollButtonIos: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
},
|
||||
|
||||
createPollButtonAndroid: {
|
||||
marginHorizontal: 20,
|
||||
marginVertical: BaseTheme.spacing[5]
|
||||
createPollButton: {
|
||||
marginHorizontal: BaseTheme.spacing[5],
|
||||
},
|
||||
|
||||
pollPane: {
|
||||
|
||||
@@ -41,7 +41,8 @@ const useStyles = makeStyles<{ deviceStatusType?: string; }>()((theme, { deviceS
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
borderRadius: '100%',
|
||||
backgroundColor: deviceStatusType === 'ok' ? theme.palette.success01 : ColorPalette.darkGrey
|
||||
backgroundColor: deviceStatusType === 'ok' ? theme.palette.success01 : ColorPalette.darkGrey,
|
||||
flexShrink: 0
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { ComponentType, PureComponent } from 'react';
|
||||
import { TouchableWithoutFeedback } from 'react-native';
|
||||
import { Edge, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { TouchableWithoutFeedback, View } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
@@ -9,7 +8,6 @@ import { hideDialog } from '../../../base/dialog/actions';
|
||||
import { isDialogOpen } from '../../../base/dialog/functions';
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { StyleType } from '../../../base/styles/functions.native';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
|
||||
import ReactionMenu from './ReactionMenu';
|
||||
|
||||
@@ -38,11 +36,6 @@ interface IProps {
|
||||
*/
|
||||
_styles: StyleType;
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the Toolbox is visible.
|
||||
*/
|
||||
_toolboxVisible: boolean;
|
||||
|
||||
/**
|
||||
* The width of the screen.
|
||||
*/
|
||||
@@ -87,13 +80,12 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { _height, _participantCount, _styles, _toolboxVisible, _width } = this.props;
|
||||
const { _height, _participantCount, _styles, _width } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
onPress = { this._onCancel }>
|
||||
<SafeAreaView
|
||||
edges = { [ 'bottom', 'left', 'right', !_toolboxVisible && 'top' ].filter(Boolean) as Edge[] }
|
||||
<View
|
||||
style = { [
|
||||
_styles,
|
||||
{
|
||||
@@ -103,7 +95,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
<ReactionMenu
|
||||
onCancel = { this._onCancel }
|
||||
overflowMenu = { false } />
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
@@ -138,8 +130,7 @@ function _mapStateToProps(state: IReduxState) {
|
||||
_styles: ColorSchemeRegistry.get(state, 'Toolbox').reactionDialog,
|
||||
_width: state['features/base/responsive-ui'].clientWidth,
|
||||
_height: state['features/base/responsive-ui'].clientHeight,
|
||||
_participantCount: getParticipantCount(state),
|
||||
_toolboxVisible: isToolboxVisible(state)
|
||||
_participantCount: getParticipantCount(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -37,16 +37,25 @@ export function grantRecordingConsent() {
|
||||
|
||||
/**
|
||||
* Grants recording consent, unmutes audio/video, and closes the dialog.
|
||||
* Restores the mute state that existed before the consent dialog was shown,
|
||||
* respecting the user's intentional mute choices from prejoin or initial settings.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function grantRecordingConsentAndUnmute() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
// Read the prejoin mute state from the dialog props stored in Redux
|
||||
const dialogProps = getState()['features/base/dialog'].componentProps as any;
|
||||
const audioMuted = dialogProps?.audioWasMuted ?? false;
|
||||
const videoMuted = dialogProps?.videoWasMuted ?? false;
|
||||
|
||||
batch(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
dispatch(setAudioMuted(false, true));
|
||||
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
|
||||
// Restore to the mute state before consent was requested.
|
||||
dispatch(setAudioMuted(audioMuted, false));
|
||||
dispatch(setVideoMuted(videoMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
|
||||
dispatch(hideDialog());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -415,7 +415,7 @@ class AbstractStartRecordingDialog extends Component<IProps, IState> {
|
||||
|
||||
if (this.state.selectedRecordingService === RECORDING_TYPES.JITSI_REC_SERVICE
|
||||
&& this.state.shouldRecordTranscription) {
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage));
|
||||
dispatch(setRequestingSubtitles(true, _displaySubtitles, _subtitlesLanguage, true));
|
||||
} else {
|
||||
_conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: this.state.shouldRecordTranscription
|
||||
|
||||
@@ -108,7 +108,8 @@ export default class AbstractStopRecordingDialog<P extends IProps>
|
||||
}
|
||||
|
||||
// TODO: this should be an action in transcribing. -saghul
|
||||
this.props.dispatch(setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage));
|
||||
this.props.dispatch(
|
||||
setRequestingSubtitles(Boolean(_displaySubtitles), _displaySubtitles, _subtitlesLanguage, true));
|
||||
|
||||
this.props._conference?.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
|
||||
@@ -180,8 +180,8 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
||||
this.selfRecording.withVideo = Boolean(videoTrack);
|
||||
const localTracks: MediaStreamTrack[] = [];
|
||||
|
||||
audioTrack && localTracks.push(audioTrack);
|
||||
videoTrack && localTracks.push(videoTrack);
|
||||
audioTrack && localTracks.push(audioTrack.clone());
|
||||
videoTrack && localTracks.push(videoTrack.clone());
|
||||
this.stream = new MediaStream(localTracks);
|
||||
} else {
|
||||
if (supportsCaptureHandle) {
|
||||
@@ -282,6 +282,9 @@ const LocalRecordingManager: ILocalRecordingManager = {
|
||||
this.audioContext = undefined;
|
||||
this.audioDestination = undefined;
|
||||
this.startTime = undefined;
|
||||
this.stream = undefined;
|
||||
this.selfRecording.on = false;
|
||||
this.selfRecording.withVideo = false;
|
||||
|
||||
if (this.writableStream) {
|
||||
try {
|
||||
|
||||
@@ -8,20 +8,21 @@ import ConfirmDialog from '../../../../base/dialog/components/native/ConfirmDial
|
||||
import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../../../../base/media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../../../../base/media/constants';
|
||||
import Link from '../../../../base/react/components/native/Link';
|
||||
import { IRecordingConsentDialogProps } from '../../../reducer';
|
||||
import styles from '../styles.native';
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
*
|
||||
* @param {IRecordingConsentDialogProps} props - The component props.
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
export default function RecordingConsentDialog() {
|
||||
export default function RecordingConsentDialog({ audioWasMuted = false, videoWasMuted = false }: IRecordingConsentDialogProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { recordings } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const { consentLearnMoreLink } = recordings ?? {};
|
||||
|
||||
|
||||
const consent = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
@@ -32,11 +33,13 @@ export default function RecordingConsentDialog() {
|
||||
const consentAndUnmute = useCallback(() => {
|
||||
dispatch(setAudioUnmutePermissions(false, true));
|
||||
dispatch(setVideoUnmutePermissions(false, true));
|
||||
dispatch(setAudioMuted(false, true));
|
||||
dispatch(setVideoMuted(false, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
|
||||
// Restore to the mute state before consent was requested.
|
||||
dispatch(setAudioMuted(audioWasMuted, false));
|
||||
dispatch(setVideoMuted(videoWasMuted, VIDEO_MUTISM_AUTHORITY.USER, false));
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
}, [ audioWasMuted, videoWasMuted ]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
|
||||
@@ -10,6 +10,7 @@ import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../../
|
||||
|
||||
/**
|
||||
* Component that renders the dialog for explicit consent for recordings.
|
||||
* The prejoin mute state is read from Redux by the action creator.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
|
||||
@@ -421,12 +421,21 @@ function _showExplicitConsentDialog(recorderSession: any, dispatch: IStore['disp
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture the current mute state BEFORE forcing mute for consent
|
||||
// This preserves the user's intentional mute choices from prejoin or initial settings
|
||||
const state = getState();
|
||||
const audioWasMuted = state['features/base/media'].audio.muted;
|
||||
const videoWasMuted = state['features/base/media'].video.muted;
|
||||
|
||||
batch(() => {
|
||||
dispatch(markConsentRequested(recorderSession.getID()));
|
||||
dispatch(setAudioUnmutePermissions(true, true));
|
||||
dispatch(setVideoUnmutePermissions(true, true));
|
||||
dispatch(setAudioMuted(true));
|
||||
dispatch(setVideoMuted(true));
|
||||
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog));
|
||||
dispatch(openDialog('RecordingConsentDialog', RecordingConsentDialog, {
|
||||
audioWasMuted,
|
||||
videoWasMuted
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,6 +42,14 @@ export interface IRecordingState {
|
||||
wasStartRecordingSuggested?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Props for the RecordingConsentDialog component.
|
||||
*/
|
||||
export interface IRecordingConsentDialogProps {
|
||||
audioWasMuted?: boolean;
|
||||
videoWasMuted?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the Redux store this feature stores its state in.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import React, { useCallback, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { GestureResponderEvent, ScrollView, Text, TouchableHighlight, View, ViewStyle } from 'react-native';
|
||||
import { Edge } from 'react-native-safe-area-context';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
@@ -11,12 +11,13 @@ import { IconArrowLeft } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import BaseThemeNative from '../../../base/ui/components/BaseTheme.native';
|
||||
import HeaderNavigationButton from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import { goBack, navigate } from '../../../mobile/navigation/components/settings/SettingsNavigationContainerRef';
|
||||
import { screen } from '../../../mobile/navigation/routes';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; }) => {
|
||||
const LanguageSelectView = ({ goBack, isInWelcomePage }: {
|
||||
goBack?: (e?: GestureResponderEvent | React.MouseEvent) => void;
|
||||
isInWelcomePage?: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation();
|
||||
const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
|
||||
@@ -25,7 +26,7 @@ const LanguageSelectView = ({ isInWelcomePage }: { isInWelcomePage?: boolean; })
|
||||
const setLanguage = useCallback(language => () => {
|
||||
i18next.changeLanguage(language);
|
||||
conference?.setTranscriptionLanguage(language);
|
||||
navigate(screen.settings.main);
|
||||
goBack?.();
|
||||
}, [ conference, i18next ]);
|
||||
|
||||
const headerLeft = () => (
|
||||
|
||||
@@ -83,7 +83,7 @@ export function toggleRequestingSubtitles() {
|
||||
* @param {boolean} enabled - The new state of the subtitles.
|
||||
* @param {boolean} displaySubtitles - Whether to display subtitles or not.
|
||||
* @param {string} language - The language of the subtitles.
|
||||
* @param {boolean} backendRecordingOn - Whether backend recording is on.
|
||||
* @param {boolean} forceBackendRecordingOn - Whether to force that backend recording is on.
|
||||
* @returns {{
|
||||
* type: SET_REQUESTING_SUBTITLES,
|
||||
* backendRecordingOn: boolean,
|
||||
@@ -95,11 +95,13 @@ export function toggleRequestingSubtitles() {
|
||||
export function setRequestingSubtitles(
|
||||
enabled: boolean,
|
||||
displaySubtitles = true,
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`) {
|
||||
language: string | null = `translation-languages:${DEFAULT_LANGUAGE}`,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
return {
|
||||
type: SET_REQUESTING_SUBTITLES,
|
||||
displaySubtitles,
|
||||
enabled,
|
||||
forceBackendRecordingOn,
|
||||
language
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ const useStyles = makeStyles()(theme => {
|
||||
* Uses the same language options as LanguageSelectorDialog and
|
||||
* updates the subtitles language preference in Redux.
|
||||
*
|
||||
* @param {IProps} props - The component props.
|
||||
* @returns {JSX.Element} - The rendered component.
|
||||
*/
|
||||
function LanguageSelector() {
|
||||
@@ -51,6 +50,13 @@ function LanguageSelector() {
|
||||
state,
|
||||
selectedLanguage?.replace('translation-languages:', '')
|
||||
));
|
||||
const isAsyncTranscriptionEnabled = useSelector((state: IReduxState) =>
|
||||
state['features/base/conference'].conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription);
|
||||
|
||||
// Hide the "Translate to" option when asyncTranscription is enabled
|
||||
if (isAsyncTranscriptionEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps available languages to Select component options format.
|
||||
|
||||
@@ -98,7 +98,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case SET_REQUESTING_SUBTITLES:
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language);
|
||||
_requestingSubtitlesChange(store, action.enabled, action.language, action.forceBackendRecordingOn);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -344,13 +344,16 @@ function _getPrimaryLanguageCode(language: string) {
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {boolean} enabled - Whether subtitles should be enabled or not.
|
||||
* @param {string} language - The language to use for translation.
|
||||
* @param {boolean} forceBackendRecordingOn - Whether to force backend recording is on or not. This is used only when
|
||||
* we start recording, stopping is based on whether isTranscribingEnabled is already set.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _requestingSubtitlesChange(
|
||||
{ dispatch, getState }: IStore,
|
||||
enabled: boolean,
|
||||
language?: string | null) {
|
||||
language?: string | null,
|
||||
forceBackendRecordingOn: boolean = false) {
|
||||
const state = getState();
|
||||
const { conference } = state['features/base/conference'];
|
||||
const backendRecordingOn = conference?.getMetadataHandler()?.getMetadata()?.asyncTranscription;
|
||||
@@ -375,7 +378,9 @@ function _requestingSubtitlesChange(
|
||||
}));
|
||||
dispatch(setSubtitlesError(true));
|
||||
});
|
||||
} else {
|
||||
}
|
||||
|
||||
if (backendRecordingOn || forceBackendRecordingOn) {
|
||||
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
@@ -388,7 +393,7 @@ function _requestingSubtitlesChange(
|
||||
language.replace('translation-languages:', ''));
|
||||
}
|
||||
|
||||
if (!enabled && backendRecordingOn
|
||||
if (!enabled && (backendRecordingOn || forceBackendRecordingOn)
|
||||
&& conference?.getMetadataHandler()?.getMetadata()[RECORDING_METADATA_ID]?.isTranscribingEnabled) {
|
||||
conference?.getMetadataHandler()?.setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: false
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"cs": "cs-CZ",
|
||||
"da": "da-DK",
|
||||
"de": "de-DE",
|
||||
"dsb": "dsb-DE",
|
||||
"el": "el-GR",
|
||||
"enGB": "en-GB",
|
||||
"es": "es-ES",
|
||||
|
||||
@@ -76,3 +76,13 @@ local function anonymous(self, message)
|
||||
end
|
||||
|
||||
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
|
||||
|
||||
module:hook("pre-resource-unbind", function (e)
|
||||
local error, session = e.error, e.session;
|
||||
|
||||
prosody.events.fire_event('jitsi-pre-session-unbind', {
|
||||
jid = session.full_jid,
|
||||
session = session,
|
||||
error = error
|
||||
});
|
||||
end, 11);
|
||||
|
||||
@@ -61,6 +61,16 @@ end
|
||||
module:hook_global("bosh-session", init_session);
|
||||
module:hook_global("websocket-session", init_session);
|
||||
|
||||
module:hook("pre-resource-unbind", function (e)
|
||||
local error, session = e.error, e.session;
|
||||
|
||||
prosody.events.fire_event('jitsi-pre-session-unbind', {
|
||||
jid = session.full_jid,
|
||||
session = session,
|
||||
error = error
|
||||
});
|
||||
end, 11);
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return nil, "Password based auth not supported";
|
||||
end
|
||||
|
||||
@@ -13,6 +13,7 @@ local inspect = require 'inspect';
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
local ban_check_error_count = module:measure("muc_auth_ban_check_error", "rate")
|
||||
|
||||
-- we will cache banned tokens to avoid extra requests
|
||||
-- on destroying session, websocket retries 2 more times before giving up
|
||||
@@ -68,6 +69,10 @@ local function shouldAllow(session)
|
||||
|
||||
cache:set(token, socket.gettime());
|
||||
end
|
||||
else
|
||||
ban_check_error_count();
|
||||
module:log("warn", "Error code:%s contacting url:%s response:%s request:%s content:%s",
|
||||
code, ACCESS_MANAGER_URL, response, request, content);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ end
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local jid_host = require 'util.jid'.host;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_resource = require 'util.jid'.resource;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
local uuid_gen = require 'util.uuid'.generate;
|
||||
@@ -65,12 +67,16 @@ local main_muc_service;
|
||||
-- Maps a breakout room jid to the main room jid
|
||||
local main_rooms_map = {};
|
||||
|
||||
-- Maps a full room JID to a bare connection jid for a participant that's changing rooms.
|
||||
local cache = require 'util.cache';
|
||||
local switching_room_cache = cache.new(1000);
|
||||
|
||||
-- Utility functions
|
||||
|
||||
function get_main_room_jid(room_jid)
|
||||
local _, host = jid_split(room_jid);
|
||||
|
||||
return
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and room_jid
|
||||
or main_rooms_map[room_jid];
|
||||
@@ -391,7 +397,7 @@ function on_breakout_room_pre_create(event)
|
||||
end
|
||||
|
||||
function on_occupant_joined(event)
|
||||
local room = event.room;
|
||||
local occupant, room = event.occupant, event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
@@ -409,6 +415,9 @@ function on_occupant_joined(event)
|
||||
main_room.close_timer:stop();
|
||||
main_room.close_timer = nil;
|
||||
end
|
||||
|
||||
-- clear any switching state for this occupant, we always store main room / resource
|
||||
switching_room_cache:set(main_room_jid..'/'..jid_resource(occupant.nick), nil);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -447,6 +456,11 @@ function on_occupant_pre_leave(event)
|
||||
prosody.events.fire_event('jitsi-breakout-occupant-leaving', {
|
||||
room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session;
|
||||
});
|
||||
|
||||
local presence_status = stanza:get_child_text('status');
|
||||
if presence_status == 'switch_room' then
|
||||
switching_room_cache:set(main_room.jid..'/'..jid_resource(occupant.nick), jid_bare(occupant.jid));
|
||||
end
|
||||
end
|
||||
|
||||
function on_occupant_left(event)
|
||||
@@ -518,6 +532,86 @@ function on_main_room_destroyed(event)
|
||||
end
|
||||
end
|
||||
|
||||
-- Checks for a conflict with a JID in the switching_room_cache. In case of a conflict sends an error and returns true (the join is not allowed).
|
||||
-- in switching_room_cache is the same as the jid that is sending the stanza, if that is the case we can allow
|
||||
-- the join to proceed by returning false. If there is no match we send an error and return true
|
||||
-- which should halt the join.
|
||||
-- @param jid - The jid to check, this is the jid requested to join breakout or main room
|
||||
-- @param from_bare_jid - The real jid of the occupant trying to join
|
||||
-- @param room - The room being joined
|
||||
-- @param stanza - The presence stanza
|
||||
-- @param origin - The session origin to send error if needed
|
||||
function check_switching_state(jid, from_bare_jid, room, stanza, origin)
|
||||
local switching_session_jid = switching_room_cache:get(jid);
|
||||
|
||||
if switching_session_jid and switching_session_jid ~= from_bare_jid then
|
||||
local reply = st.error_reply(stanza, "cancel", "conflict", nil, room.jid):up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function check_for_existing_occupant_in_room(room, requested_resource, bare_jid, stanza, origin)
|
||||
local dest_occupant = room:get_occupant_by_nick(room.jid..'/'..requested_resource);
|
||||
if dest_occupant ~= nil and bare_jid ~= jid_bare(dest_occupant.bare_jid) then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'conflict', nil, room.jid):up());
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- This is a request to join or change jid in main or breakout room. We need to check whether the requested jid does not
|
||||
-- conflict with a jid which is currently in switching state or already in another room.
|
||||
function on_occupant_pre_join_or_change(e)
|
||||
local room, stanza, origin = e.room, e.stanza, e.origin;
|
||||
local requested_jid = stanza.attr.to;
|
||||
|
||||
local main_room = get_main_room(room.jid);
|
||||
|
||||
-- case where the room can be destroyed while someone is switching to it
|
||||
if not main_room then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'service-unavailable'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local main_room_requested_jid = main_room.jid..'/'..jid_resource(requested_jid);
|
||||
local bare_jid = jid_bare(stanza.attr.from);
|
||||
|
||||
-- we always store main room jid with resource in switching cache
|
||||
if check_switching_state(main_room_requested_jid, bare_jid, room, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
|
||||
if main_room == room then
|
||||
-- this is the main room we need to check all its breakout rooms
|
||||
for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
if check_for_existing_occupant_in_room(
|
||||
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
-- this is a breakout room let's check the main room
|
||||
if check_for_existing_occupant_in_room(main_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- now let's check the rest of the breakout rooms
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
if check_for_existing_occupant_in_room(
|
||||
breakout_room, jid_resource(requested_jid), bare_jid, stanza, origin) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Module operations
|
||||
|
||||
@@ -544,6 +638,8 @@ function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
|
||||
host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave);
|
||||
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
|
||||
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
@@ -641,6 +737,24 @@ process_host_module(breakout_rooms_muc_component_config, function(host_module, h
|
||||
end
|
||||
end);
|
||||
|
||||
-- clears switching_room_cache on resource unbind (disconnect one way or another)
|
||||
local function handle_pre_resource_unbind(event)
|
||||
local participant_bare_jid = jid_bare(event.jid);
|
||||
|
||||
-- check switching_room_cache for anyone that is switching rooms but got disconnected so we can clean up the map
|
||||
local keysToRemove = {};
|
||||
for key, value in switching_room_cache:items() do
|
||||
if value == participant_bare_jid then
|
||||
table.insert(keysToRemove, key)
|
||||
end
|
||||
end
|
||||
|
||||
for _, key in ipairs(keysToRemove) do
|
||||
switching_room_cache:set(key, nil);
|
||||
end
|
||||
end
|
||||
module:hook_global('jitsi-pre-session-unbind', handle_pre_resource_unbind);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
@@ -650,6 +764,18 @@ function process_main_muc_loaded(main_muc, host_module)
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-destroyed', on_main_room_destroyed, 1); -- prosody handles it at 0
|
||||
|
||||
host_module:hook('muc-occupant-pre-leave', function(event)
|
||||
local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza;
|
||||
|
||||
local presence_status = stanza:get_child_text('status');
|
||||
if presence_status == 'switch_room' then
|
||||
switching_room_cache:set(occupant.nick, jid_bare(occupant.jid));
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-pre-join', on_occupant_pre_join_or_change);
|
||||
host_module:hook('muc-occupant-pre-change', on_occupant_pre_join_or_change);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
|
||||
@@ -74,17 +74,7 @@ local function verify_user(session, stanza)
|
||||
or allowlist:contains(user_bare_jid)
|
||||
|
||||
-- allow main participants in visitor mode
|
||||
or session.type == 's2sin'
|
||||
|
||||
-- Let Jigasi or transcriber pass throw
|
||||
or util.is_sip_jigasi(stanza)
|
||||
or util.is_transcriber_jigasi(stanza)
|
||||
|
||||
-- is jibri
|
||||
or util.is_jibri(user_jid)
|
||||
|
||||
-- Let Sip Jibri pass through
|
||||
or util.is_sip_jibri_join(stanza) then
|
||||
or session.type == 's2sin' then
|
||||
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -2,6 +2,7 @@ import BasePageObject from './BasePageObject';
|
||||
|
||||
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
|
||||
const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
|
||||
const AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectVideoTitle';
|
||||
const JOIN_MULTIPLE_TEST_ID = 'notify.connectedThreePlusMembers';
|
||||
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
|
||||
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
|
||||
@@ -57,6 +58,13 @@ export default class Notifications extends BasePageObject {
|
||||
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the video muted notification.
|
||||
*/
|
||||
async closeAVModerationVideoMutedNotification(skipNonExisting = false) {
|
||||
return this.closeNotification(AV_MODERATION_VIDEO_MUTED_NOTIFICATION_ID, skipNonExisting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the ask to unmute notification.
|
||||
*/
|
||||
@@ -106,14 +114,22 @@ export default class Notifications extends BasePageObject {
|
||||
* @private
|
||||
*/
|
||||
private async closeNotification(testId: string, skipNonExisting = false) {
|
||||
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
|
||||
const closeButton = this.participant.driver.$(`[data-testid="${testId}"] #close-notification`);
|
||||
|
||||
if (skipNonExisting && !await closeButton.isExisting()) {
|
||||
return Promise.resolve();
|
||||
try {
|
||||
if (skipNonExisting && !await closeButton.isExisting()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
} catch (e) {
|
||||
console.error(`Error closing notification ${testId}`, e);
|
||||
|
||||
if (!skipNonExisting) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
await closeButton.moveTo();
|
||||
await closeButton.click();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ export default class PasswordDialog extends BaseDialog {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.waitForClickable({ timeout: 2000 });
|
||||
await passwordInput.waitForStable();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export async function unmuteAudioAndCheck(testee: Participant, observer: Partici
|
||||
* @param observer
|
||||
*/
|
||||
export async function unmuteVideoAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getNotifications().closeAskToUnmuteNotification(true);
|
||||
await testee.getNotifications().closeAVModerationVideoMutedNotification(true);
|
||||
await testee.getToolbar().clickVideoUnmuteButton();
|
||||
|
||||
await testee.getParticipantsPane().assertVideoMuteIconIsDisplayed(testee, true);
|
||||
|
||||
@@ -122,12 +122,12 @@ describe('Audio/video moderation', () => {
|
||||
await moderatorParticipantsPane.assertVideoMuteIconIsDisplayed(moderator);
|
||||
await nonModeratorParticipantsPane.assertVideoMuteIconIsDisplayed(nonModerator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await moderatorParticipantsPane.askToUnmute(nonModerator, false);
|
||||
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(nonModerator);
|
||||
await nonModerator.getNotifications().waitForAskToUnmuteNotification();
|
||||
await unmuteVideoAndCheck(nonModerator, p1);
|
||||
|
||||
await moderatorParticipantsPane.clickContextMenuButton();
|
||||
@@ -190,6 +190,10 @@ describe('Audio/video moderation', () => {
|
||||
// stop video and check
|
||||
await p1.getFilmstrip().muteVideo(p2);
|
||||
|
||||
// close and open participants pane to make sure the context menu disappears
|
||||
await p1.getParticipantsPane().close();
|
||||
await p1.getParticipantsPane().open();
|
||||
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
|
||||
@@ -256,11 +260,12 @@ async function unmuteByModerator(
|
||||
await moderator.getNotifications().waitForRaisedHandNotification();
|
||||
|
||||
// ask participant to unmute
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await moderatorParticipantsPane.askToUnmute(participant, false);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
|
||||
await unmuteAudioAndCheck(participant, moderator);
|
||||
|
||||
await moderatorParticipantsPane.allowVideo(participant);
|
||||
await participant.getNotifications().waitForAskToUnmuteNotification();
|
||||
await unmuteVideoAndCheck(participant, moderator);
|
||||
|
||||
if (stopModeration) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user