mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-29 20:57:49 +00:00
Compare commits
33 Commits
8865
...
debug-brek
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff6f0ee949 | ||
|
|
b2f7b3be6c | ||
|
|
29fd5df16a | ||
|
|
f324122d93 | ||
|
|
82d4628976 | ||
|
|
8ab02d598c | ||
|
|
5b23072bd0 | ||
|
|
7b4cc552fb | ||
|
|
eb188ff02a | ||
|
|
b40c24db70 | ||
|
|
a855f76377 | ||
|
|
c481e7ede4 | ||
|
|
17b4c2156a | ||
|
|
ca6579e032 | ||
|
|
f3e99624e9 | ||
|
|
3b5c2d9b0b | ||
|
|
bab9ddbb57 | ||
|
|
8d4193ce1e | ||
|
|
7d2cf3dbf2 | ||
|
|
9309d61c00 | ||
|
|
4089702060 | ||
|
|
bf2254c753 | ||
|
|
641b52c51d | ||
|
|
083037d152 | ||
|
|
a3200a172f | ||
|
|
ebff46971d | ||
|
|
657aefefc2 | ||
|
|
683d6eb208 | ||
|
|
a62fa3f833 | ||
|
|
88f1ef27c5 | ||
|
|
95ecf73c71 | ||
|
|
a96908dd7c | ||
|
|
a103b0e5bd |
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#17A0DB</color>
|
||||
<color name="navigationBarColor">#161618</color>
|
||||
<color name="navigationBarColor">#040404</color>
|
||||
<color name="statusBarColor">#040404</color>
|
||||
</resources>
|
||||
@@ -3,6 +3,7 @@
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
<item name="android:statusBarColor">@color/statusBarColor</item>
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data android:name="org.jitsi.meet.sdk.JitsiInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
|
||||
@@ -135,7 +135,12 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
JitsiMeetActivityDelegate.onHostResume(this);
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
addTopBottomInsets(getWindow(),findViewById(android.R.id.content));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
addTopBottomInsets(getWindow(), findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
registerForBroadcastMessages();
|
||||
|
||||
5
android/sdk/src/main/res/values/colors.xml
Normal file
5
android/sdk/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="navigationBarColor">#040404</color>
|
||||
<color name="statusBarColor">#040404</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,6 @@
|
||||
<resources>
|
||||
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar"/>
|
||||
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
<item name="android:statusBarColor">@color/statusBarColor</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -805,7 +805,9 @@ var config = {
|
||||
// // By setting preCallTestEnabled, you enable the pre-call test in the prejoin page.
|
||||
// // ICE server credentials need to be provided over the preCallTestICEUrl
|
||||
// preCallTestEnabled: false,
|
||||
// preCallTestICEUrl: ''
|
||||
// preCallTestICEUrl: '',
|
||||
// // Shows the hangup button in the lobby screen.
|
||||
// showHangUp: true,
|
||||
// },
|
||||
|
||||
// When 'true', the user cannot edit the display name.
|
||||
@@ -1605,6 +1607,10 @@ var config = {
|
||||
// audio: true,
|
||||
// video: true
|
||||
// },
|
||||
// // Hides the visitor count for visitors.
|
||||
// // hideVisitorCountForVisitors: false,
|
||||
// // Whether to show the join meeting dialog when joining as a visitor.
|
||||
// // showJoinMeetingDialog: true,
|
||||
// },
|
||||
// The default type of desktop sharing sources that will be used in the electron app.
|
||||
// desktopSharingSources: ['screen', 'window'],
|
||||
|
||||
@@ -1473,7 +1473,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-webrtc (124.0.4):
|
||||
- react-native-webrtc (124.0.7):
|
||||
- JitsiWebRTC (~> 124.0.0)
|
||||
- React-Core
|
||||
- react-native-webview (13.13.5):
|
||||
@@ -2274,7 +2274,7 @@ SPEC CHECKSUMS:
|
||||
react-native-safe-area-context: 0f7bf11598f9a61b7ceac8dc3f59ef98697e99e1
|
||||
react-native-slider: 1205801a8d29b28cacc14eef08cb120015cdafcb
|
||||
react-native-video: eb861d67a71dfef1bbf6086a811af5f338b13781
|
||||
react-native-webrtc: 2261a482150195092246fe70b3aff976f2e11ec5
|
||||
react-native-webrtc: e8f0ce746353adc2744a2b933645e1aeb41eaa74
|
||||
react-native-webview: 079eca50edf657503318b66687dadfb903731aa8
|
||||
react-native-worklets-core: b59cf88762c8fb6132d8796babd4cec15217d6f0
|
||||
React-nativeconfig: ecf4dc92c40b97e2b3f0c619938f78bfd6507b08
|
||||
|
||||
@@ -109,9 +109,12 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "Skicka chattmeddelande är inaktiverat",
|
||||
"enter": "Delta i mötet",
|
||||
"error": "Fel: ditt meddelande skickades inte. Orsak: {{error}}",
|
||||
"everyone": "Alla",
|
||||
"fieldPlaceHolder": "Skriv ditt meddelande här",
|
||||
"guestsChatIndicator": "(gäst)",
|
||||
"lobbyChatMessageTo": "Skicka meddelande",
|
||||
"message": "Meddelande",
|
||||
"messageAccessibleTitle": "{{user}} Säger:",
|
||||
@@ -122,7 +125,10 @@
|
||||
"nickname": {
|
||||
"popover": "Välj ett namn",
|
||||
"title": "Skriv in ett namn för att börja använda chatten",
|
||||
"titleWithPolls": "Skriv in ett namn för att börja använda chatten"
|
||||
"titleWithCC": "Skriv in ett namn för att börja använda chatten och för undertexter",
|
||||
"titleWithPolls": "Skriv in ett namn för att börja använda chatten och omröstningar",
|
||||
"titleWithPollsAndCC": "Skriv in ett namn för att börja använda chatten, omröstningar och undertexter",
|
||||
"titleWithPollsAndCCAndFileSharing": "Skriv in ett namn för att börja använda chatten, omröstningar, undertexter och fildelning"
|
||||
},
|
||||
"noMessagesMessage": "Det finns ännu inga meddelanden i mötet. Påbörja en konversation!",
|
||||
"privateNotice": "Privat meddelande till {{recipient}}",
|
||||
@@ -131,11 +137,16 @@
|
||||
"systemDisplayName": "",
|
||||
"tabs": {
|
||||
"chat": "Chatt",
|
||||
"closedCaptions": "Undertexter",
|
||||
"fileSharing": "Fildelning",
|
||||
"polls": "Omröstningar"
|
||||
},
|
||||
"title": "Chatt",
|
||||
"titleWithPolls": "Chatt",
|
||||
"you": "du"
|
||||
"titleWithCC": "Undertexter",
|
||||
"titleWithFeatures": "Chatt och",
|
||||
"titleWithFileSharing": "Filer",
|
||||
"titleWithPolls": "Omröstningar",
|
||||
"you": "dig"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"buttonText": "Installera Chrome-tillägg",
|
||||
@@ -144,6 +155,10 @@
|
||||
"dontShowAgain": "Visa inte det här igen",
|
||||
"installExtensionText": "Installera tillägget för integration med Google Kalender och Office 365"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "Undertexter kommer vara tillgängliga när en moderator startar de",
|
||||
"startClosedCaptionsButton": "Starta undertexter"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "Ansluter till mötet…"
|
||||
},
|
||||
@@ -263,7 +278,8 @@
|
||||
"Remove": "Ta bort",
|
||||
"Share": "Dela",
|
||||
"Submit": "Skicka",
|
||||
"WaitForHostMsg": "Konferensen har inte börjat än. Autentisera konferensen om du är värd. Vänta annars på att värden startar konferensen.",
|
||||
"Understand": "Jag förstår, låt min mikrofon vara avstängd tillsvidare",
|
||||
"UnderstandAndUnmute": "Jag förstår, starta min mikrofon",
|
||||
"WaitForHostNoAuthMsg": "Konferensen har ännu inte startat eftersom ingen värd har anlänt ännu. Vänligen vänta.",
|
||||
"WaitingForHostButton": "Vänta på värd",
|
||||
"WaitingForHostTitle": "Väntar på värden…",
|
||||
@@ -285,6 +301,12 @@
|
||||
"alreadySharedVideoTitle": "Endast en delad video åt gången tillåts",
|
||||
"applicationWindow": "Applikationsfönster",
|
||||
"authenticationRequired": "Autentisering krävs",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "Ta ett foto och skicka de via din mobila enhet",
|
||||
"ok": "Starta kamera",
|
||||
"reject": "Inte nu",
|
||||
"title": "Ta ett foto"
|
||||
},
|
||||
"cameraConstraintFailedError": "Din kamera uppfyller inte kraven för användning.",
|
||||
"cameraNotFoundError": "Hittar ingen kamera.",
|
||||
"cameraNotSendingData": "Vi saknar åtkomst till kameran. Kontrollera om ett annat program använder enheten, välj en annan enhet från inställningsmenyn eller försök att starta om programmet.",
|
||||
@@ -299,6 +321,7 @@
|
||||
"conferenceReloadMsg": "Vi försöker fixa problemet. Återansluter om {{seconds}} sekunder…",
|
||||
"conferenceReloadTitle": "Något gick snett.",
|
||||
"confirm": "Bekräfta",
|
||||
"confirmBack": "Bakåt",
|
||||
"confirmNo": "Nej",
|
||||
"confirmYes": "Ja",
|
||||
"connectError": "Ojdå! Något gick fel och vi kunde inte ansluta till konferensen.",
|
||||
@@ -331,11 +354,12 @@
|
||||
"internalError": "Ett fel uppstod. Fel: {{error}}",
|
||||
"internalErrorTitle": "Internt fel",
|
||||
"kickMessage": "Du kan kontakta {{participantDisplayName}} för mer information.",
|
||||
"kickParticipantButton": "Ta bort från mötet",
|
||||
"kickParticipantButton": "Ta bort deltagaren från mötet",
|
||||
"kickParticipantDialog": "Vill du ta bort denna deltagaren från mötet?",
|
||||
"kickParticipantTitle": "Tysta deltagaren?",
|
||||
"kickSystemTitle": "Du har blivit borttagen från mötet",
|
||||
"kickTitle": "{{participantDisplayName}} tog bort dig från mötet",
|
||||
"learnMore": "Läs mer",
|
||||
"linkMeeting": "Länka möte",
|
||||
"linkMeetingTitle": "Länka möte till Salesforce",
|
||||
"liveStreaming": "Streama",
|
||||
@@ -358,22 +382,34 @@
|
||||
"micTimeoutError": "Time out, kunde ej starta ljud enhet",
|
||||
"micUnknownError": "Av okänd anledning kan inte din mikrofon användas.",
|
||||
"moderationAudioLabel": "Tillåt deltagarna att slå på ljudet för sig själva",
|
||||
"moderationDesktopLabel": "Tillåt deltagare att skärmdela",
|
||||
"moderationVideoLabel": "Tillåt deltagarna att starta sin video",
|
||||
"muteEveryoneDialog": "Är du säker på att du vill tysta alla? Du kan inte slå på mikrofonen åt dem, men de kan själva slå på sin egen mikrofon när som helst.",
|
||||
"muteEveryoneDialogModerationOn": "Deltagarna kan när som helst begära att få prata.",
|
||||
"muteEveryoneElseDialog": "När någon tystats kan du inte slå på mikrofonen, men de kan själva slå på sin egen mikrofon när som helst.",
|
||||
"muteEveryoneElseTitle": "Tysta alla utom {{whom}}?",
|
||||
"muteEveryoneElsesDesktopDialog": "När delningen är stoppad kommer du inte kunna starta den igen. Men de kan dela med dig igen.",
|
||||
"muteEveryoneElsesDesktopTitle": "Stoppa allas skärmdelning utom {{whom}}?",
|
||||
"muteEveryoneElsesVideoDialog": "När kameran är inaktiverad kan den inte aktiveras igen. Däremot kan övriga deltagare aktivera sina kameror.",
|
||||
"muteEveryoneElsesVideoTitle": "Inaktivera allas kameror förutom {{whom}}",
|
||||
"muteEveryoneSelf": "Dig själv",
|
||||
"muteEveryoneStartMuted": "Alla börjar tystade",
|
||||
"muteEveryoneTitle": "Tysta alla?",
|
||||
"muteEveryonesDesktopDialog": "Deltagarna kan dela sin skärm när som helst. Du kan inte starta deras skärmdelning åt dem.",
|
||||
"muteEveryonesDesktopDialogModerationOn": "Deltagarna kan skicka en förfrågan om att starta skärmdelningen.",
|
||||
"muteEveryonesDesktopTitle": "Stoppa alla deltagares skärmdelning?",
|
||||
"muteEveryonesVideoDialog": "Är du säker du vill inaktivera allas kameror. Du kommer inte att kunna aktivera dessa igen. Däremot kommer deltagarna att kunna aktivera sin egen kamera när som.",
|
||||
"muteEveryonesVideoDialogModerationOn": "Deltagarna kan när som helst begära att få aktivera sin kamera.",
|
||||
"muteEveryonesVideoDialogOk": "Inaktivera",
|
||||
"muteEveryonesVideoTitle": "Inaktiveras allas kameror",
|
||||
"muteParticipantBody": "Du kan inte aktivera deras mikrofoner, men de kan göra det själva.",
|
||||
"muteParticipantButton": "Tysta",
|
||||
"muteParticipantsDesktopBody": "Du kommer inte kunna starta deltagarnas skärmdelning. Men deltagare kan starta skärmdelning.",
|
||||
"muteParticipantsDesktopBodyModerationOn": "Du kommer inte kunna starta deltagarnas skärmdelning.Deltagare kommer inte kunna starta deras skärmdelning.",
|
||||
"muteParticipantsDesktopButton": "Stoppa skärmdelning",
|
||||
"muteParticipantsDesktopDialog": "Är du säker på att du vill stänga av deltagarens skärmdelning? Du kommer inte kunna slå igång den igen, utan deltagaren måste starta de igen.",
|
||||
"muteParticipantsDesktopDialogModerationOn": "Är du säker på att du vill stänga av deltagarens skärmdelning? Du kommer inte kunna slå igång den igen, deltagaren kommer inte heller kunna starta den igen.",
|
||||
"muteParticipantsDesktopTitle": "Avaktivera skärmdelning för deltagaren?",
|
||||
"muteParticipantsVideoBody": "Du kommer inte att kunna aktivera kameran igen. Däremot kan deltagaren kunna aktivera sin egen kamera när som.",
|
||||
"muteParticipantsVideoBodyModerationOn": "Du och deltagarna kommer inte att kunna aktivera kameran igen.",
|
||||
"muteParticipantsVideoButton": "Inaktivera kamera",
|
||||
@@ -393,6 +429,10 @@
|
||||
"recentlyUsedObjects": "Dina senaste använda objekt",
|
||||
"recording": "Inspelning",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Ej möjligt medan livestreaming pågår.",
|
||||
"recordingInProgressDescription": "Mötet spelas in och analyseras av AI{{learnMore}}. Ditt ljud och din bild har stängts av, om du väljer att starta kamera eller mikrofon så accepterar du att bli inspelad.",
|
||||
"recordingInProgressDescriptionFirstHalf": "Mötet spelas in och analyseras av AI",
|
||||
"recordingInProgressDescriptionSecondHalf": "Ditt ljud och din bild har stängts av, om du väljer att starta kamera eller mikrofon så accepterar du att bli inspelad.",
|
||||
"recordingInProgressTitle": "Inspelning pågår",
|
||||
"rejoinNow": "Återanslut nu",
|
||||
"remoteControlAllowedMessage": "{{user}} godkände din begäran om fjärrstyrning.",
|
||||
"remoteControlDeniedMessage": "{{user}} avböjde din begäran om fjärrstyrning.",
|
||||
@@ -482,6 +522,7 @@
|
||||
"tokenAuthFailedWithReasons": "Förlåt, du har inte tillåtelse att gå med i det här samtalet. Troliga anledingar: {{reason}}",
|
||||
"tokenAuthUnsupported": "Token URL är inte tillåten",
|
||||
"transcribing": "Transkriberar",
|
||||
"unauthenticatedAccessDisabled": "Detta samtalet kräver identifiering. Logga in för att fortsätta.",
|
||||
"unlockRoom": "Ta bort möte $t(lockRoomPassword)",
|
||||
"user": "Användare",
|
||||
"userIdentifier": "Användar-ID",
|
||||
@@ -522,6 +563,25 @@
|
||||
"veryBad": "Mycket dåligt",
|
||||
"veryGood": "Mycket bra"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "Försök igen.",
|
||||
"downloadFailedTitle": "Nedladdning misslyckades",
|
||||
"downloadFile": "Ladda ner",
|
||||
"downloadStarted": "Nedladdning har startat",
|
||||
"dragAndDrop": "Dra och släpp filen här eller någonstans på skärmen",
|
||||
"fileAlreadyUploaded": "Filen har redan laddats upp till mötet.",
|
||||
"fileRemovedByOther": "Filen vid namn '{{ fileName }}' har tagits bort",
|
||||
"fileTooLargeDescription": "Se till att filstorleken inte överskrider {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Den valda filen är för stor",
|
||||
"fileUploadProgress": "Överföring pågår",
|
||||
"fileUploadedSuccessfully": "Överföring lyckades",
|
||||
"newFileNotification": "{{ participantName }} delade '{{ fileName }}'",
|
||||
"removeFile": "Ta bort",
|
||||
"removeFileSuccess": "Filen togs bort",
|
||||
"uploadFailedDescription": "Snälla försök igen.",
|
||||
"uploadFailedTitle": "Överföring misslyckades",
|
||||
"uploadFile": "Dela fil"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Videominiatyrer"
|
||||
@@ -690,7 +750,8 @@
|
||||
"notificationTitle": "Väntrum",
|
||||
"passwordJoinButton": "Anslut",
|
||||
"title": "Lobby",
|
||||
"toggleLabel": "Aktivera väntrum"
|
||||
"toggleLabel": "Aktivera väntrum",
|
||||
"waitForModerator": "Konferensen har annu inte startat för ingen moderator anslutit. Om du vill bli moderator var vänlig logga in. Annars, var god dröj."
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -733,7 +794,10 @@
|
||||
"me": "jag",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "Säkerhetsproblem!",
|
||||
"allowAction": "Tillåt",
|
||||
"allowAll": "Tillåt",
|
||||
"allowAudio": "Tillåt ljud",
|
||||
"allowDesktop": "Tillåt skärmdelning",
|
||||
"allowVideo": "Tillåt kamera",
|
||||
"allowedUnmute": "Du kan slå på mikrofonen, starta kameran eller dela din skärm.",
|
||||
"audioUnmuteBlockedDescription": "Aktivering av mikrofonen har tillfälligt blockerats på grund av systembegränsningar.",
|
||||
"audioUnmuteBlockedTitle": "Mikrofonen har blockerats!",
|
||||
@@ -746,8 +810,10 @@
|
||||
"dataChannelClosedDescription": "Bryggkanalen har kopplats bort och därmed är videokvaliteten begränsad till sin lägsta inställning",
|
||||
"dataChannelClosedDescriptionWithAudio": "Bryggkanalen har kopplats bort och därmed kan abvrott i ljud och bild uppstå.",
|
||||
"dataChannelClosedWithAudio": "Ljud och bild kvalite kan vara försämrad.",
|
||||
"desktopMutedRemotelyTitle": "Din skärmdelning har avslutats av {{participantDisplayName}}",
|
||||
"disabledIframe": "Inbäddning är endast avsedd för demonstrationsändamål, så det här samtalet kommer att kopplas ner om {{timeout}} minuter.",
|
||||
"disabledIframeSecondary": "Bädda in {{domain}} är bara till för demo, så detta samtal kommer att kopplas bort inom {{timeout}} minuter. Var god använd <a href='{{jaasDomain}}' rel='nooper noreferrer' target='_blank'>Jitsi som tjänst</a> för att bädda in i produktion.",
|
||||
"disabledIframeSecondaryNative": "Inbäddning {{domain}} är endast avsedd för demonstrationsändamål, så det här samtalet kommer att kopplas ner om {{timeout}} minuter.",
|
||||
"disabledIframeSecondaryWeb": "Bädda in {{domain}} är bara till för demo, så detta samtal kommer att kopplas bort inom {{timeout}} minuter. Var god använd <a href='{{jaasDomain}}' rel='nooper noreferrer' target='_blank'>Jitsi som tjänst</a> för att bädda in i produktion.",
|
||||
"disconnected": "frånkopplad",
|
||||
"displayNotifications": "Visa aviseringar för",
|
||||
"dontRemindMe": "Påminn mig inte",
|
||||
@@ -802,6 +868,7 @@
|
||||
"oldElectronClientDescription1": "Den version av Jitsi meet som används är gammal och har säkerhetsluckor. Var god uppdatera till den senaste versionen.",
|
||||
"oldElectronClientDescription2": "senast build",
|
||||
"oldElectronClientDescription3": "nu!",
|
||||
"openChat": "Öppna chatt",
|
||||
"participantWantsToJoin": "Vill vara med på mötet",
|
||||
"participantsWantToJoin": "Vill vara med på mötet",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) togs bort av en annan deltagare",
|
||||
@@ -825,6 +892,8 @@
|
||||
"suggestRecordingDescription": "Vill du starta en inspelning?",
|
||||
"suggestRecordingTitle": "Spela in detta mötet",
|
||||
"unmute": "Slå på mikrofonen",
|
||||
"unmuteScreen": "Starta skärmdelning",
|
||||
"unmuteVideo": "Starta kamera",
|
||||
"videoMutedRemotelyDescription": "Du kan alltid slå på den igen.",
|
||||
"videoMutedRemotelyTitle": "Din kamera har inaktiverats av {{participantDisplayName}}!",
|
||||
"videoUnmuteBlockedDescription": "Aktivering av kameran och delning av skrivbord har tillfälligt blockerats på grund av systembegränsningar.",
|
||||
@@ -843,11 +912,14 @@
|
||||
"admit": "Godkänn",
|
||||
"admitAll": "Godkänn alla",
|
||||
"allow": "Låt deltagarna:",
|
||||
"allowDesktop": "Tillåt skärmdelning",
|
||||
"allowVideo": "Tillåt kamera",
|
||||
"askDesktop": "Skicka skärmdelningsförfrågan",
|
||||
"askUnmute": "Be om att aktivera ljud",
|
||||
"audioModeration": "Slå på ljudet för sig själva",
|
||||
"blockEveryoneMicCamera": "Inaktivera allas mikrofon och kamera",
|
||||
"breakoutRooms": "Grupprum",
|
||||
"desktopModeration": "Starta skärmdelning",
|
||||
"goLive": "Gå live",
|
||||
"invite": "Bjud in någon",
|
||||
"lowerAllHands": "Ta ner allas händer",
|
||||
@@ -859,6 +931,8 @@
|
||||
"muteAll": "Stäng av allt ljud",
|
||||
"muteEveryoneElse": "Inaktivera ljud för alla deltagare",
|
||||
"reject": "Avvisa",
|
||||
"stopDesktop": "Stoppa skärmdelning",
|
||||
"stopEveryonesDesktop": "Stoppa allas skärmdelning",
|
||||
"stopEveryonesVideo": "Inaktivera allas video",
|
||||
"stopVideo": "Inaktivera video",
|
||||
"unblockEveryoneMicCamera": "Aktivera allas mikrofon och kamera",
|
||||
@@ -868,12 +942,15 @@
|
||||
"headings": {
|
||||
"lobby": "Väntrum ({{count}})",
|
||||
"participantsList": "Mötesdeltagare ({{count}})",
|
||||
"viewerRequests": "Deltagar förfrågningar {{count}}",
|
||||
"visitorInQueue": "(väntande {{count}})",
|
||||
"visitorRequests": "(förfrågningar {{count}})",
|
||||
"visitors": "Gäster ({{count}})",
|
||||
"visitorsList": "Gäster ({{count}})",
|
||||
"waitingLobby": "Väntar i väntrum ({{count}})"
|
||||
},
|
||||
"search": "Sök efter deltagare",
|
||||
"searchDescription": "Börja skriv för att filtrera bland deltagare",
|
||||
"title": "Deltagare"
|
||||
},
|
||||
"passwordDigitsOnly": "Ange max {{number}} siffror",
|
||||
@@ -890,6 +967,9 @@
|
||||
"by": "Av {{ name }}",
|
||||
"closeButton": "Stäng omröstning",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "Skicka omröstning"
|
||||
},
|
||||
"addOption": "Lägg till alternativ",
|
||||
"answerPlaceholder": "Alternativ",
|
||||
"cancel": "Avbryt",
|
||||
@@ -898,8 +978,7 @@
|
||||
"pollQuestion": "Fråga för omröstning",
|
||||
"questionPlaceholder": "Ställ en fråga",
|
||||
"removeOption": "Ta bort alternativ",
|
||||
"save": "Spara",
|
||||
"send": "Skicka"
|
||||
"save": "Spara"
|
||||
},
|
||||
"errors": {
|
||||
"notUniqueOption": "Alternativ måste vara unika"
|
||||
@@ -1100,6 +1179,7 @@
|
||||
"signedIn": "Hämtar kalenderhändelser från {{email}}. Tryck på knappen nedan för att sluta hämta kalenderhändelser.",
|
||||
"title": "Kalender"
|
||||
},
|
||||
"chatWithPermissions": "Chatt kräver högre rättigheter",
|
||||
"desktopShareFramerate": "Bildfrekvens för skrivbordsdelning",
|
||||
"desktopShareHighFpsWarning": "En högre bildhastighet för skrivbordsdelning kan påverka din bandbredd. Du måste starta om skärmdelningen för att de nya inställningarna ska träda i kraft.",
|
||||
"desktopShareWarning": "Du måste starta om skärmdelningen för att de nya inställningarna ska träda i kraft.",
|
||||
@@ -1129,6 +1209,7 @@
|
||||
"selectMic": "Mikrofon",
|
||||
"selfView": "Självvy",
|
||||
"shortcuts": "Genvägar",
|
||||
"showSubtitlesOnStage": "Vissa undertexter på scenen",
|
||||
"speakers": "Högtalare",
|
||||
"startAudioMuted": "Alla börjar tystade",
|
||||
"startReactionsMuted": "Stäng av reaktionsljud för alla",
|
||||
@@ -1188,6 +1269,7 @@
|
||||
"neutral": "Neutral",
|
||||
"sad": "Ledsen",
|
||||
"search": "Sök",
|
||||
"searchDescription": "Börja skriv för att filtrera bland deltagare",
|
||||
"searchHint": "Sök deltagare",
|
||||
"seconds": "{{count}} s",
|
||||
"speakerStats": "Talarstatistik",
|
||||
@@ -1224,6 +1306,7 @@
|
||||
"closeChat": "Stäng chatten",
|
||||
"closeMoreActions": "Stäng menyn för fler åtgärder",
|
||||
"closeParticipantsPane": "Stäng deltagarfönstret",
|
||||
"closedCaptions": "Undertexter",
|
||||
"collapse": "Minimera",
|
||||
"document": "Växla delat dokument",
|
||||
"documentClose": "Stäng delat dokument",
|
||||
@@ -1245,7 +1328,7 @@
|
||||
"help": "Hjälp",
|
||||
"hideWhiteboard": "Dölj whiteboard",
|
||||
"invite": "Bjud in personer",
|
||||
"kick": "Ta bort deltagare",
|
||||
"kick": "Ta bort deltagare från möte",
|
||||
"laugh": "Skratta",
|
||||
"leaveConference": "Lämna mötet",
|
||||
"like": "Tummen upp",
|
||||
@@ -1302,6 +1385,20 @@
|
||||
"videounmute": "Starta kameran"
|
||||
},
|
||||
"addPeople": "Lägg till personer i samtal",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "Akustisk eko hantering"
|
||||
},
|
||||
"agc": {
|
||||
"label": "Automatisk gain kontroll"
|
||||
},
|
||||
"ns": {
|
||||
"label": "Brusreducering"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "Stereo"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "Avsluta ljudläget",
|
||||
"audioOnlyOn": "Starta ljudläget",
|
||||
"audioRoute": "Välj ljudenhet",
|
||||
@@ -1314,6 +1411,7 @@
|
||||
"closeChat": "Stäng chatt",
|
||||
"closeParticipantsPane": "Stäng deltagarrutan",
|
||||
"closeReactionsMenu": "Stäng meny för reaktioner",
|
||||
"closedCaptions": "Undertexter",
|
||||
"disableNoiseSuppression": "Inaktivera brusreducering",
|
||||
"disableReactionSounds": "Du kan inaktivera reaktionsljud för det här mötet",
|
||||
"documentClose": "Stäng delat dokument",
|
||||
@@ -1372,6 +1470,7 @@
|
||||
"reactionHeart": "Skicka kärlek",
|
||||
"reactionLaugh": "Skratta",
|
||||
"reactionLike": "Skicka tummen upp",
|
||||
"reactionLove": "Skicka kärleksreaktion",
|
||||
"reactionSilence": "Skicka tyst reaktion",
|
||||
"reactionSurprised": "Skicka reaktionen överaskad",
|
||||
"reactions": "Reaktioner",
|
||||
@@ -1404,14 +1503,18 @@
|
||||
"ccButtonTooltip": "Aktivera / Inaktivera undertexter",
|
||||
"expandedLabel": "Transkribering är aktiverad",
|
||||
"failed": "Transkribiering misslyckades",
|
||||
"labelToolTip": "Mötet transkriberas",
|
||||
"labelTooltip": "Mötet transkriberas",
|
||||
"labelTooltipExtra": "Transkriberingen kommer finnas tillgänglig senare.",
|
||||
"openClosedCaptions": "Öppna undertexter",
|
||||
"original": "Original",
|
||||
"sourceLanguageDesc": "För närvarande är mötesspråket inställt på <b>{{sourceLanguage}}</b>. <br/> Du kan ändra det från ",
|
||||
"sourceLanguageHere": "här",
|
||||
"start": "Börja visa undertexter",
|
||||
"stop": "Sluta visa undertexter",
|
||||
"subtitles": "Undertexter",
|
||||
"subtitlesOff": "Av",
|
||||
"tr": "TR"
|
||||
"tr": "TR",
|
||||
"translateTo": "Översätt till"
|
||||
},
|
||||
"unpinParticipant": "Lossa deltagare",
|
||||
"userMedia": {
|
||||
@@ -1453,6 +1556,8 @@
|
||||
"connectionInfo": "Anslutningsinformation",
|
||||
"demote": "Gör till besökare",
|
||||
"domute": "Tysta",
|
||||
"domuteDesktop": "Stoppa skärmdelning",
|
||||
"domuteDesktopOfOthers": "Stoppa skärmdelning för alla andra",
|
||||
"domuteOthers": "Inaktivera ljud för alla andra",
|
||||
"domuteVideo": "Inaktivera kamera",
|
||||
"domuteVideoOfOthers": "Inaktivera kamera för alla andra",
|
||||
@@ -1517,6 +1622,8 @@
|
||||
"noMainParticipantsTitle": "Det är mötet har inte startat än",
|
||||
"noVisitorLobby": "Du kan inte gå med medans lobby är påslagen för mötet.",
|
||||
"notAllowedPromotion": "En deltagare behöver godkänna din förfrågan först.",
|
||||
"requestToJoin": "Håller upp handen",
|
||||
"requestToJoinDescription": "Din begäran skickades till en moderator. Var god dröj!",
|
||||
"title": "Du är en besökare i mötet"
|
||||
},
|
||||
"waitingMessage": "Du kommer att komma in i mötet när det är live!"
|
||||
|
||||
@@ -114,6 +114,9 @@
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"everyone": "Everyone",
|
||||
"fieldPlaceHolder": "Aa",
|
||||
"fileAccessibleTitle": "{{user}} uploaded a file",
|
||||
"fileAccessibleTitleMe": "me uploaded a file",
|
||||
"fileDeleted": "A file was deleted",
|
||||
"guestsChatIndicator": "(guest)",
|
||||
"lobbyChatMessageTo": "Lobby chat message to {{recipient}}",
|
||||
"message": "Message",
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -100,7 +100,7 @@
|
||||
"react-native-tab-view": "3.5.2",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.13.0",
|
||||
"react-native-webrtc": "124.0.4",
|
||||
"react-native-webrtc": "124.0.7",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-worklets-core": "https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
@@ -22471,9 +22471,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "124.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.4.tgz",
|
||||
"integrity": "sha512-ZbhSz1f+kc1v5VE0B84+v6ujIWTHa2fIuocrYzGUIFab7E5izmct7PNHb9dzzs0xhBGqh4c2rUa49jbL+P/e2w==",
|
||||
"version": "124.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz",
|
||||
"integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
"debug": "4.3.4",
|
||||
@@ -42684,9 +42685,9 @@
|
||||
"integrity": "sha512-eY6jgLcmYKAAlAZhsZbp8wfCVrGu7jmUYTTspn8udN8j4jqr4Fq90ROOM/QegGkwNs4waclL0IkzGuq61kT4DQ=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "124.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.4.tgz",
|
||||
"integrity": "sha512-ZbhSz1f+kc1v5VE0B84+v6ujIWTHa2fIuocrYzGUIFab7E5izmct7PNHb9dzzs0xhBGqh4c2rUa49jbL+P/e2w==",
|
||||
"version": "124.0.7",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-124.0.7.tgz",
|
||||
"integrity": "sha512-gnXPdbUS8IkKHq9WNaWptW/yy5s6nMyI6cNn90LXdobPVCgYSk6NA2uUGdT4c4J14BRgaFA95F+cR28tUPkMVA==",
|
||||
"requires": {
|
||||
"base64-js": "1.5.1",
|
||||
"debug": "4.3.4",
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
"react-native-tab-view": "3.5.2",
|
||||
"react-native-url-polyfill": "2.0.0",
|
||||
"react-native-video": "6.13.0",
|
||||
"react-native-webrtc": "124.0.4",
|
||||
"react-native-webrtc": "124.0.7",
|
||||
"react-native-webview": "13.13.5",
|
||||
"react-native-worklets-core": "https://github.com/jitsi/react-native-worklets-core.git#8c5dfab2a5907305da8971696a781b60f0f9cb18",
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
import * as amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default amplitude;
|
||||
|
||||
|
||||
@@ -524,6 +524,7 @@ export interface IConfig {
|
||||
hideExtraJoinButtons?: Array<string>;
|
||||
preCallTestEnabled?: boolean;
|
||||
preCallTestICEUrl?: string;
|
||||
showHangUp?: boolean;
|
||||
};
|
||||
raisedHands?: {
|
||||
disableLowerHandByModerator?: boolean;
|
||||
@@ -653,7 +654,9 @@ export interface IConfig {
|
||||
audio?: boolean;
|
||||
video?: boolean;
|
||||
};
|
||||
hideVisitorCountForVisitors?: boolean;
|
||||
queueService: string;
|
||||
showJoinMeetingDialog?: boolean;
|
||||
};
|
||||
watchRTCConfigParams?: IWatchRTCConfiguration;
|
||||
webhookProxyUrl?: string;
|
||||
|
||||
@@ -238,6 +238,8 @@ export default [
|
||||
'useTurnUdp',
|
||||
'videoQuality',
|
||||
'visitors.enableMediaOnPromote',
|
||||
'visitors.hideVisitorCountForVisitors',
|
||||
'visitors.showJoinMeetingDialog',
|
||||
'watchRTCConfigParams.allowBrowserLogCollection',
|
||||
'watchRTCConfigParams.collectionInterval',
|
||||
'watchRTCConfigParams.console',
|
||||
|
||||
@@ -62,65 +62,6 @@ const AVATAR_CHECKER_FUNCTIONS = [
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
|
||||
* dominant speaker is visible always on the vertical filmstrip in stage layout.
|
||||
*
|
||||
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
speakersList
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
let activeSpeakers = new Map(speakersList);
|
||||
|
||||
// Do not re-sort the active speakers if dominant speaker is currently visible.
|
||||
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
return activeSpeakers;
|
||||
}
|
||||
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
|
||||
|
||||
if (activeSpeakers.has(dominantSpeaker ?? '')) {
|
||||
activeSpeakers.delete(dominantSpeaker ?? '');
|
||||
}
|
||||
|
||||
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
|
||||
// alphabetically sorted.
|
||||
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
|
||||
const updatedSpeakers = Array.from(activeSpeakers);
|
||||
|
||||
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
|
||||
activeSpeakers = new Map(updatedSpeakers);
|
||||
}
|
||||
|
||||
// Remove screenshares from the count.
|
||||
if (sortedRemoteVirtualScreenshareParticipants) {
|
||||
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
|
||||
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
|
||||
|
||||
activeSpeakers.delete(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fake participants from the count.
|
||||
if (fakeParticipants) {
|
||||
availableSlotsForActiveSpeakers -= fakeParticipants.size;
|
||||
}
|
||||
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
|
||||
|
||||
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
|
||||
|
||||
return new Map(truncatedSpeakersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
@@ -827,17 +768,30 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
* Checks if private chat is enabled for the given participant or local participant.
|
||||
*
|
||||
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @param {boolean} [checkSelf=false] - Whether to check for local participant's ability to send messages.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
|
||||
export function isPrivateChatEnabled(
|
||||
participant: IParticipant | IVisitorChatParticipant | undefined,
|
||||
state: IReduxState,
|
||||
checkSelf: boolean = false
|
||||
): boolean {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
|
||||
// If checking self capability (if the local participant can send messages) ignore the local participant blocking rule
|
||||
const isLocal = !isVisitorChatParticipant(participant) && participant?.local;
|
||||
|
||||
if (isLocal && !checkSelf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if private chat is disabled globally
|
||||
if (disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -857,3 +811,15 @@ export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatPar
|
||||
|
||||
return !disablePrivateChat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the local participant (can they send private messages).
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if the local participant can send private messages, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabledSelf(state: IReduxState): boolean {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return isPrivateChatEnabled(localParticipant, state, true);
|
||||
}
|
||||
|
||||
@@ -76,12 +76,12 @@ const DEFAULT_STATE = {
|
||||
numberOfParticipantsNotSupportingE2EE: 0,
|
||||
overwrittenNameList: {},
|
||||
pinnedParticipant: undefined,
|
||||
previousSpeakers: new Set<string>(),
|
||||
raisedHandsQueue: [],
|
||||
remote: new Map(),
|
||||
remoteVideoSources: new Set<string>(),
|
||||
sortedRemoteVirtualScreenshareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: new Map()
|
||||
sortedRemoteParticipants: new Map()
|
||||
};
|
||||
|
||||
export interface IParticipantsState {
|
||||
@@ -94,12 +94,12 @@ export interface IParticipantsState {
|
||||
numberOfParticipantsNotSupportingE2EE: number;
|
||||
overwrittenNameList: { [id: string]: string; };
|
||||
pinnedParticipant?: string;
|
||||
previousSpeakers: Set<string>;
|
||||
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
|
||||
remote: Map<string, IParticipant>;
|
||||
remoteVideoSources: Set<string>;
|
||||
sortedRemoteParticipants: Map<string, string>;
|
||||
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
|
||||
speakersList: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,22 +156,10 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
const { participant } = action;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const newSpeakers = [ id, ...previousSpeakers ];
|
||||
const sortedSpeakersList: Array<Array<string>> = [];
|
||||
|
||||
for (const speaker of newSpeakers) {
|
||||
if (speaker !== local?.id) {
|
||||
const remoteParticipant = state.remote.get(speaker);
|
||||
|
||||
remoteParticipant
|
||||
&& sortedSpeakersList.push(
|
||||
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the remote speaker list sorted alphabetically.
|
||||
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
// Build chronologically ordered Set of remote speakers (excluding local)
|
||||
const previousSpeakersSet: Set<string>
|
||||
= new Set(previousSpeakers.filter((speaker: string) => speaker !== local?.id));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
@@ -181,8 +169,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id, // @ts-ignore
|
||||
speakersList: new Map(sortedSpeakersList)
|
||||
dominantSpeaker: id,
|
||||
previousSpeakers: previousSpeakersSet
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,7 +423,7 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.speakersList.has(id) && state.speakersList.delete(id);
|
||||
state.previousSpeakers.delete(id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
|
||||
@@ -282,15 +282,18 @@ const PreMeetingScreen = ({
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState, ownProps: Partial<IProps>) {
|
||||
const { hiddenPremeetingButtons } = state['features/base/config'];
|
||||
const { hiddenPremeetingButtons, prejoinConfig } = state['features/base/config'];
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const { showHangUp = true } = getLobbyConfig(state);
|
||||
const { knocking } = state['features/lobby'];
|
||||
const { showHangUp: showHangUpLobby = true } = getLobbyConfig(state);
|
||||
const { showHangUp: showHangUpPrejoin = true } = prejoinConfig || {};
|
||||
const premeetingButtons = (ownProps.thirdParty
|
||||
? THIRD_PARTY_PREJOIN_BUTTONS
|
||||
: PREMEETING_BUTTONS).filter((b: any) => !(hiddenPremeetingButtons || []).includes(b));
|
||||
|
||||
if (showHangUp && knocking && !premeetingButtons.includes('hangup')) {
|
||||
const shouldShowHangUp = knocking ? showHangUpLobby : showHangUpPrejoin;
|
||||
|
||||
if (shouldShowHangUp && !premeetingButtons.includes('hangup')) {
|
||||
premeetingButtons.push('hangup');
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class Message extends Component<IProps> {
|
||||
const content: any[] = [];
|
||||
const { gifEnabled } = this.props;
|
||||
|
||||
// check if the message is a GIF
|
||||
// Check if the message is a GIF
|
||||
if (gifEnabled && isGifMessage(text)) {
|
||||
const url = extractGifURL(text);
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getRemoteParticipants } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, getRemoteParticipants, isPrivateChatEnabledSelf } from '../../../base/participants/functions';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import Tabs from '../../../base/ui/components/web/Tabs';
|
||||
import { arePollsDisabled } from '../../../conference/functions.any';
|
||||
@@ -242,6 +242,7 @@ const Chat = ({
|
||||
} = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const privateMessageRecipient = useSelector((state: IReduxState) => state['features/chat'].privateMessageRecipient);
|
||||
const participants = useSelector(getRemoteParticipants);
|
||||
const isPrivateChatAllowed = useSelector((state: IReduxState) => isPrivateChatEnabledSelf(state));
|
||||
|
||||
const options = useMemo(() => {
|
||||
const o = Array.from(participants?.values() || [])
|
||||
@@ -431,12 +432,14 @@ const Chat = ({
|
||||
<MessageContainer
|
||||
messages = { _messages } />
|
||||
<MessageRecipient />
|
||||
<Select
|
||||
containerClassName = { cx(classes.privateMessageRecipientsList) }
|
||||
id = 'select-chat-recipient'
|
||||
onChange = { onSelectedRecipientChange }
|
||||
options = { options }
|
||||
value = { privateMessageRecipient?.id || OPTION_GROUPCHAT } />
|
||||
{isPrivateChatAllowed && (
|
||||
<Select
|
||||
containerClassName = { cx(classes.privateMessageRecipientsList) }
|
||||
id = 'select-chat-recipient'
|
||||
onChange = { onSelectedRecipientChange }
|
||||
options = { options }
|
||||
value = { privateMessageRecipient?.id || OPTION_GROUPCHAT } />
|
||||
)}
|
||||
<ChatInput
|
||||
onSend = { onSendMessage } />
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,10 @@ import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } f
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import Message from '../../../base/react/components/web/Message';
|
||||
import { MESSAGE_TYPE_LOCAL } from '../../constants';
|
||||
import { getDisplayNameSuffix, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage } from '../../functions';
|
||||
import { getDisplayNameSuffix, getFormattedTimestamp, getMessageText, getPrivateNoticeMessage, isFileMessage } from '../../functions';
|
||||
import { IChatMessageProps } from '../../types';
|
||||
|
||||
import FileMessage from './FileMessage';
|
||||
import MessageMenu from './MessageMenu';
|
||||
import ReactButton from './ReactButton';
|
||||
|
||||
@@ -48,6 +49,22 @@ const useStyles = makeStyles()((theme: Theme) => {
|
||||
marginTop: '4px',
|
||||
boxSizing: 'border-box' as const,
|
||||
|
||||
'&.file': {
|
||||
display: 'flex',
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
|
||||
'& $replyWrapper': {
|
||||
width: '100%',
|
||||
minWidth: 0
|
||||
},
|
||||
|
||||
'& $messageContent': {
|
||||
width: '100%',
|
||||
minWidth: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&.privatemessage': {
|
||||
backgroundColor: theme.palette.support05
|
||||
},
|
||||
@@ -117,8 +134,7 @@ const useStyles = makeStyles()((theme: Theme) => {
|
||||
},
|
||||
messageContent: {
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
flex: 1
|
||||
overflow: 'hidden'
|
||||
},
|
||||
optionsButtonContainer: {
|
||||
display: 'flex',
|
||||
@@ -344,6 +360,7 @@ const ChatMessage = ({
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
enablePrivateChat = { Boolean(enablePrivateChat) }
|
||||
isFileMessage = { isFileMessage(message) }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
@@ -356,19 +373,31 @@ const ChatMessage = ({
|
||||
classes.chatMessage,
|
||||
className,
|
||||
message.privateMessage && 'privatemessage',
|
||||
message.lobbyChat && !knocking && 'lobbymessage'
|
||||
message.lobbyChat && !knocking && 'lobbymessage',
|
||||
isFileMessage(message) && 'file'
|
||||
) }>
|
||||
<div className = { classes.replyWrapper }>
|
||||
<div className = { cx('messagecontent', classes.messageContent) }>
|
||||
{showDisplayName && _renderDisplayName()}
|
||||
<div className = { cx('usermessage', classes.userMessage) }>
|
||||
<Message
|
||||
screenReaderHelpText = { message.displayName === message.recipient
|
||||
? t<string>('chat.messageAccessibleTitleMe')
|
||||
: t<string>('chat.messageAccessibleTitle', {
|
||||
user: message.displayName
|
||||
}) }
|
||||
text = { getMessageText(message) } />
|
||||
{isFileMessage(message) ? (
|
||||
<FileMessage
|
||||
message = { message }
|
||||
screenReaderHelpText = { message.messageType === MESSAGE_TYPE_LOCAL
|
||||
? t<string>('chat.fileAccessibleTitleMe')
|
||||
: t<string>('chat.fileAccessibleTitle', {
|
||||
user: message.displayName
|
||||
})
|
||||
} />
|
||||
) : (
|
||||
<Message
|
||||
screenReaderHelpText = { message.messageType === MESSAGE_TYPE_LOCAL
|
||||
? t<string>('chat.messageAccessibleTitleMe')
|
||||
: t<string>('chat.messageAccessibleTitle', {
|
||||
user: message.displayName
|
||||
}) }
|
||||
text = { getMessageText(message) } />
|
||||
)}
|
||||
{(message.privateMessage || (message.lobbyChat && !knocking))
|
||||
&& _renderPrivateNotice()}
|
||||
<div className = { classes.chatMessageFooter }>
|
||||
@@ -400,6 +429,7 @@ const ChatMessage = ({
|
||||
{isHovered && <MessageMenu
|
||||
displayName = { message.displayName }
|
||||
enablePrivateChat = { Boolean(enablePrivateChat) }
|
||||
isFileMessage = { isFileMessage(message) }
|
||||
isFromVisitor = { message.isFromVisitor }
|
||||
isLobbyMessage = { message.lobbyChat }
|
||||
message = { message.message }
|
||||
|
||||
@@ -27,7 +27,7 @@ const useStyles = makeStyles()(theme => {
|
||||
flexDirection: 'column',
|
||||
maxWidth: '100%',
|
||||
|
||||
'&.remote': {
|
||||
'&.remote, &.file': {
|
||||
maxWidth: 'calc(100% - 40px)' // 100% - avatar and margin
|
||||
}
|
||||
},
|
||||
|
||||
143
react/features/chat/components/web/FileMessage.tsx
Normal file
143
react/features/chat/components/web/FileMessage.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { downloadFile, removeFile } from '../../../file-sharing/actions';
|
||||
import FileItem from '../../../file-sharing/components/web/FileItem';
|
||||
import { isFileUploadingEnabled } from '../../../file-sharing/functions.any';
|
||||
import { IMessage } from '../../types';
|
||||
|
||||
/**
|
||||
* Props for the FileMessage component.
|
||||
*/
|
||||
interface IFileMessageProps {
|
||||
|
||||
/**
|
||||
* Additional CSS class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The message containing file metadata.
|
||||
*/
|
||||
message: IMessage;
|
||||
|
||||
/**
|
||||
* Screen reader help text for accessibility.
|
||||
*/
|
||||
screenReaderHelpText?: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
fileMessageContainer: {
|
||||
margin: `${theme.spacing(1)} 0`,
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
padding: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
// Override FileItem styles for compact chat display
|
||||
'& .fileItem': {
|
||||
padding: theme.spacing(2), // Reduced from 3 (24px → 16px)
|
||||
gap: theme.spacing(2), // Reduced from 3
|
||||
|
||||
// Add background to button container to hide text underneath in chat context
|
||||
'& > div:last-child': {
|
||||
backgroundColor: theme.palette.ui02,
|
||||
paddingLeft: theme.spacing(2)
|
||||
},
|
||||
|
||||
'&:hover > div:last-child': {
|
||||
backgroundColor: theme.palette.ui03
|
||||
}
|
||||
},
|
||||
|
||||
'& .fileName': {
|
||||
...theme.typography.bodyShortRegular // Match message text font
|
||||
},
|
||||
|
||||
'& .fileSize': {
|
||||
...theme.typography.labelRegular // Keep smaller for metadata
|
||||
}
|
||||
},
|
||||
deletedFileMessage: {
|
||||
...theme.typography.bodyShortRegular,
|
||||
fontStyle: 'italic',
|
||||
color: theme.palette.text02,
|
||||
padding: theme.spacing(1, 0)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying file messages in chat.
|
||||
*
|
||||
* @param {IFileMessageProps} props - The component props.
|
||||
* @returns {JSX.Element | null} The FileMessage component or null if no file metadata.
|
||||
*/
|
||||
const FileMessage = ({ className = '', message, screenReaderHelpText }: IFileMessageProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const isUploadEnabled = useSelector(isFileUploadingEnabled);
|
||||
|
||||
/**
|
||||
* Handles the download action for a file.
|
||||
*
|
||||
* @param {string} fileId - The ID of the file to download.
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleDownload = useCallback((fileId: string) => {
|
||||
dispatch(downloadFile(fileId));
|
||||
}, [ dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles the remove action for a file.
|
||||
*
|
||||
* @param {string} fileId - The ID of the file to remove.
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleRemove = useCallback((fileId: string) => {
|
||||
dispatch(removeFile(fileId));
|
||||
}, [ dispatch ]);
|
||||
|
||||
if (!message.fileMetadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If the file has been deleted, show a deletion message instead of the file item.
|
||||
if (message.fileMetadata.isDeleted) {
|
||||
return (
|
||||
<div className = { cx(classes.fileMessageContainer, className) }>
|
||||
<div className = { classes.deletedFileMessage }>
|
||||
{t('chat.fileDeleted')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { cx(classes.fileMessageContainer, className) }>
|
||||
{screenReaderHelpText && (
|
||||
<span className = 'sr-only'>
|
||||
{screenReaderHelpText}
|
||||
</span>
|
||||
)}
|
||||
<FileItem
|
||||
actionsVisible = { true }
|
||||
className = 'fileItem'
|
||||
file = { message.fileMetadata }
|
||||
iconSize = { 40 }
|
||||
onDownload = { handleDownload }
|
||||
onRemove = { handleRemove }
|
||||
showAuthor = { false }
|
||||
showRemoveButton = { isUploadEnabled }
|
||||
showTimestamp = { false } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileMessage;
|
||||
@@ -17,6 +17,7 @@ export interface IProps {
|
||||
className?: string;
|
||||
displayName?: string;
|
||||
enablePrivateChat: boolean;
|
||||
isFileMessage?: boolean;
|
||||
isFromVisitor?: boolean;
|
||||
isLobbyMessage: boolean;
|
||||
message: string;
|
||||
@@ -60,7 +61,7 @@ const useStyles = makeStyles()(theme => {
|
||||
};
|
||||
});
|
||||
|
||||
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName }: IProps) => {
|
||||
const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, enablePrivateChat, displayName, isFileMessage }: IProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { classes, cx } = useStyles();
|
||||
const { t } = useTranslation();
|
||||
@@ -72,6 +73,11 @@ const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, en
|
||||
|
||||
const participant = useSelector((state: IReduxState) => getParticipantById(state, participantId));
|
||||
|
||||
// If no menu items will be shown, don't render the menu button.
|
||||
if (!enablePrivateChat && isFileMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleMenuClick = useCallback(() => {
|
||||
setIsPopoverOpen(true);
|
||||
}, []);
|
||||
@@ -137,11 +143,13 @@ const MessageMenu = ({ message, participantId, isFromVisitor, isLobbyMessage, en
|
||||
{t('Private Message')}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className = { classes.menuItem }
|
||||
onClick = { handleCopyClick }>
|
||||
{t('Copy')}
|
||||
</div>
|
||||
{!isFileMessage && (
|
||||
<div
|
||||
className = { classes.menuItem }
|
||||
onClick = { handleCopyClick }>
|
||||
{t('Copy')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -293,3 +293,13 @@ export function getDisplayNameSuffix(message: IMessage): string {
|
||||
|
||||
return suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message is a file message by verifying the presence of file metadata.
|
||||
*
|
||||
* @param {IMessage} message - The message to check.
|
||||
* @returns {boolean} True if the message contains file metadata, false otherwise.
|
||||
*/
|
||||
export function isFileMessage(message: IMessage): boolean {
|
||||
return Boolean(message?.fileMetadata);
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
|
||||
const newMessage: IMessage = {
|
||||
displayName: action.displayName,
|
||||
error: action.error,
|
||||
fileMetadata: action.fileMetadata,
|
||||
isFromGuest: Boolean(action.isFromGuest),
|
||||
isFromVisitor: Boolean(action.isFromVisitor),
|
||||
participantId: action.participantId,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { IFileMetadata } from '../file-sharing/types';
|
||||
|
||||
export interface IMessage {
|
||||
displayName: string;
|
||||
error?: Object;
|
||||
fileMetadata?: IFileMetadata;
|
||||
isFromGuest?: boolean;
|
||||
isFromVisitor?: boolean;
|
||||
isReaction: boolean;
|
||||
|
||||
369
react/features/file-sharing/components/web/FileItem.tsx
Normal file
369
react/features/file-sharing/components/web/FileItem.tsx
Normal file
@@ -0,0 +1,369 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconDownload, IconTrash } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import {
|
||||
formatFileSize,
|
||||
formatTimestamp,
|
||||
getFileIcon
|
||||
} from '../../functions.any';
|
||||
import { IFileMetadata } from '../../types';
|
||||
|
||||
/**
|
||||
* Props for the FileItem component.
|
||||
*/
|
||||
interface IFileItemProps {
|
||||
|
||||
/**
|
||||
* Whether to show action buttons (download, remove).
|
||||
*/
|
||||
actionsVisible?: boolean;
|
||||
|
||||
/**
|
||||
* Additional CSS class name.
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* The file metadata to display.
|
||||
*/
|
||||
file: IFileMetadata;
|
||||
|
||||
/**
|
||||
* Size of the file icon in pixels (default: 64).
|
||||
*/
|
||||
iconSize?: number;
|
||||
|
||||
/**
|
||||
* Callback function when download button is clicked.
|
||||
*/
|
||||
onDownload?: (fileId: string) => void;
|
||||
|
||||
/**
|
||||
* Callback function when remove button is clicked.
|
||||
*/
|
||||
onRemove?: (fileId: string) => void;
|
||||
|
||||
/**
|
||||
* Whether to show the author/uploader information.
|
||||
*/
|
||||
showAuthor?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the remove button.
|
||||
*/
|
||||
showRemoveButton?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to show the timestamp.
|
||||
*/
|
||||
showTimestamp?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: theme.spacing(2),
|
||||
position: 'absolute',
|
||||
right: theme.spacing(3),
|
||||
top: 0
|
||||
},
|
||||
|
||||
fileIconContainer: {
|
||||
display: 'flex',
|
||||
flexShrink: 0,
|
||||
margin: 'auto'
|
||||
},
|
||||
|
||||
fileItem: {
|
||||
backgroundColor: theme.palette.ui02,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(3),
|
||||
justifyContent: 'space-between',
|
||||
maxWidth: '100%',
|
||||
minWidth: 0,
|
||||
padding: theme.spacing(3),
|
||||
position: 'relative',
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s'
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&.focused .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&.focused .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
fileItemDetails: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
minWidth: 0,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
|
||||
fileName: {
|
||||
...theme.typography.labelBold,
|
||||
gap: theme.spacing(1),
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileAuthorParticipant: {
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
gap: theme.spacing(1)
|
||||
},
|
||||
|
||||
fileAuthorParticipantName: {
|
||||
...theme.typography.labelBold,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileSize: {
|
||||
...theme.typography.labelRegular
|
||||
},
|
||||
|
||||
fileTimestamp: {
|
||||
...theme.typography.labelRegular,
|
||||
display: 'flex',
|
||||
lineHeight: '1.2rem',
|
||||
marginTop: theme.spacing(1),
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
iconButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: '8px',
|
||||
padding: 0,
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
},
|
||||
|
||||
progressBar: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 4,
|
||||
overflow: 'hidden',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
progressFill: {
|
||||
backgroundColor: theme.palette.action01,
|
||||
height: '100%',
|
||||
transition: 'width 0.3s ease'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying file information in a consistent way across the application.
|
||||
*
|
||||
* @param {IFileItemProps} props - The component props.
|
||||
* @returns {JSX.Element} The FileItem component.
|
||||
*/
|
||||
const FileItem = ({
|
||||
actionsVisible = true,
|
||||
className = '',
|
||||
file,
|
||||
iconSize = 64,
|
||||
onDownload,
|
||||
onRemove,
|
||||
showAuthor = true,
|
||||
showRemoveButton = false,
|
||||
showTimestamp = true
|
||||
}: IFileItemProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const [ isFocused, setIsFocused ] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const isUploading = (file.progress ?? 100) < 100;
|
||||
|
||||
/**
|
||||
* Handles the download button click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleDownload = useCallback(() => {
|
||||
onDownload?.(file.fileId);
|
||||
}, [ onDownload, file.fileId ]);
|
||||
|
||||
/**
|
||||
* Handles the remove button click.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleRemove = useCallback(() => {
|
||||
onRemove?.(file.fileId);
|
||||
}, [ onRemove, file.fileId ]);
|
||||
|
||||
/**
|
||||
* Handles blur event to remove focus state.
|
||||
*
|
||||
* @param {React.FocusEvent} e - The blur event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleBlur = useCallback((e: React.FocusEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handles focus event to set focus state.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
setIsFocused(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className = { cx(classes.fileItem, isFocused && 'focused', className) }
|
||||
key = { file.fileId }
|
||||
onBlur = { handleBlur }
|
||||
onFocus = { handleFocus }
|
||||
tabIndex = { -1 }
|
||||
title = { file.fileName }>
|
||||
{
|
||||
!isUploading && (
|
||||
<>
|
||||
<div className = { classes.fileIconContainer }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { iconSize }
|
||||
src = { getFileIcon(file.fileType) } />
|
||||
</div>
|
||||
<div className = { classes.fileItemDetails }>
|
||||
<div className = { cx(classes.fileName, 'fileName') }>
|
||||
{ file.fileName }
|
||||
</div>
|
||||
<div className = { cx(classes.fileSize, 'fileSize') }>
|
||||
{ formatFileSize(file.fileSize) }
|
||||
</div>
|
||||
{
|
||||
showAuthor && (
|
||||
<div className = { classes.fileAuthorParticipant }>
|
||||
<Avatar
|
||||
displayName = { file.authorParticipantName }
|
||||
participantId = { file.authorParticipantId }
|
||||
size = { 16 } />
|
||||
<div className = { classes.fileAuthorParticipantName }>
|
||||
{ file.authorParticipantName }
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{
|
||||
showTimestamp && (
|
||||
<div className = { `${classes.fileTimestamp} timestampVisibility` }>
|
||||
<pre>
|
||||
{ formatTimestamp(file.timestamp) }
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
actionsVisible && (
|
||||
<div className = { `${classes.buttonContainer} actionIconVisibility` }>
|
||||
{
|
||||
onDownload && (
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.downloadFile')} ${file.fileName}` }
|
||||
className = { classes.iconButton }
|
||||
onClick = { handleDownload }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showRemoveButton && onRemove && (
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.removeFile')} ${file.fileName}` }
|
||||
className = { classes.iconButton }
|
||||
onClick = { handleRemove }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
isUploading && (
|
||||
<div
|
||||
aria-label = { t('fileSharing.fileUploadProgress') }
|
||||
aria-valuemax = { 100 }
|
||||
aria-valuemin = { 0 }
|
||||
aria-valuenow = { file.progress }
|
||||
className = { classes.progressBar }
|
||||
role = 'progressbar'>
|
||||
<div
|
||||
className = { classes.progressFill }
|
||||
style = {{ width: `${file.progress}%` }} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileItem;
|
||||
@@ -4,34 +4,21 @@ import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloudUpload, IconDownload, IconTrash } from '../../../base/icons/svg';
|
||||
import { IconCloudUpload } from '../../../base/icons/svg';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { downloadFile, removeFile } from '../../actions';
|
||||
import {
|
||||
formatFileSize,
|
||||
formatTimestamp,
|
||||
getFileIcon,
|
||||
isFileUploadingEnabled,
|
||||
processFiles
|
||||
} from '../../functions.any';
|
||||
|
||||
import FileItem from './FileItem';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
buttonContainer: {
|
||||
alignItems: 'center',
|
||||
bottom: 0,
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
gap: theme.spacing(2),
|
||||
position: 'absolute',
|
||||
right: theme.spacing(3),
|
||||
top: 0
|
||||
},
|
||||
|
||||
container: {
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
@@ -64,60 +51,6 @@ const useStyles = makeStyles()(theme => {
|
||||
}
|
||||
},
|
||||
|
||||
fileIconContainer: {
|
||||
display: 'flex',
|
||||
margin: 'auto'
|
||||
},
|
||||
|
||||
fileItem: {
|
||||
backgroundColor: theme.palette.ui02,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: theme.spacing(3),
|
||||
justifyContent: 'space-between',
|
||||
padding: theme.spacing(3),
|
||||
position: 'relative',
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s'
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
|
||||
'& .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'& .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
'&.focused .actionIconVisibility': {
|
||||
opacity: 1
|
||||
},
|
||||
|
||||
'&.focused .timestampVisibility': {
|
||||
opacity: 0
|
||||
}
|
||||
},
|
||||
|
||||
fileItemDetails: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 2,
|
||||
gap: theme.spacing(1),
|
||||
justifyContent: 'center',
|
||||
minWidth: 0
|
||||
},
|
||||
|
||||
fileList: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
@@ -129,40 +62,13 @@ const useStyles = makeStyles()(theme => {
|
||||
marginTop: 0,
|
||||
overflowY: 'auto',
|
||||
padding: 0,
|
||||
zIndex: 1
|
||||
},
|
||||
zIndex: 1,
|
||||
|
||||
fileName: {
|
||||
...theme.typography.labelBold,
|
||||
gap: theme.spacing(1),
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileAuthorParticipant: {
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
gap: theme.spacing(1)
|
||||
},
|
||||
|
||||
fileAuthorParticipantName: {
|
||||
...theme.typography.labelBold,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
|
||||
fileSize: {
|
||||
...theme.typography.labelRegular
|
||||
},
|
||||
|
||||
fileTimestamp: {
|
||||
...theme.typography.labelRegular,
|
||||
display: 'flex',
|
||||
lineHeight: '1.2rem',
|
||||
marginTop: theme.spacing(1),
|
||||
textAlign: 'center',
|
||||
'& > li': {
|
||||
listStyleType: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}
|
||||
},
|
||||
|
||||
hiddenInput: {
|
||||
@@ -184,20 +90,6 @@ const useStyles = makeStyles()(theme => {
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
progressBar: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
height: 4,
|
||||
overflow: 'hidden',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
progressFill: {
|
||||
backgroundColor: theme.palette.action01,
|
||||
height: '100%',
|
||||
transition: 'width 0.3s ease'
|
||||
},
|
||||
|
||||
uploadButton: {
|
||||
bottom: theme.spacing(4),
|
||||
cursor: 'pointer',
|
||||
@@ -210,33 +102,6 @@ const useStyles = makeStyles()(theme => {
|
||||
|
||||
uploadIcon: {
|
||||
margin: '0 auto'
|
||||
},
|
||||
|
||||
actionIcon: {
|
||||
background: 'transparent',
|
||||
border: 0,
|
||||
cursor: 'pointer',
|
||||
padding: theme.spacing(1),
|
||||
visibility: 'hidden',
|
||||
'&:focus': {
|
||||
outline: `2px solid ${theme.palette.action01}`
|
||||
}
|
||||
},
|
||||
|
||||
iconButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
marginLeft: '8px',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: `2px solid ${theme.palette.action01}`,
|
||||
borderRadius: '4px'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -244,7 +109,6 @@ const useStyles = makeStyles()(theme => {
|
||||
const FileSharing = () => {
|
||||
const { classes } = useStyles();
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
const [ isFocused, setIsFocused ] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const uploadButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { t } = useTranslation();
|
||||
@@ -349,92 +213,12 @@ const FileSharing = () => {
|
||||
<ul className = { classes.fileList }>
|
||||
{
|
||||
sortedFiles.map(file => (
|
||||
<li
|
||||
className = { `${classes.fileItem} ${isFocused ? 'focused' : ''}` }
|
||||
key = { file.fileId }
|
||||
// Only remove focus when leaving the whole fileItem, not just moving between its buttons
|
||||
onBlur = { e => !e.currentTarget.contains(e.relatedTarget as Node) && setIsFocused(false) }
|
||||
onFocus = { () => setIsFocused(true) }
|
||||
tabIndex = { -1 }
|
||||
title = { file.fileName }>
|
||||
{
|
||||
(file.progress ?? 100) === 100 && (
|
||||
<>
|
||||
<div className = { classes.fileIconContainer }>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 64 }
|
||||
src = { getFileIcon(file.fileType) } />
|
||||
</div>
|
||||
<div className = { classes.fileItemDetails }>
|
||||
<div className = { classes.fileName }>
|
||||
{ file.fileName }
|
||||
</div>
|
||||
<div className = { classes.fileSize }>
|
||||
{ formatFileSize(file.fileSize) }
|
||||
</div>
|
||||
<div className = { classes.fileAuthorParticipant }>
|
||||
<Avatar
|
||||
displayName = { file.authorParticipantName }
|
||||
participantId = { file.authorParticipantId }
|
||||
size = { 16 } />
|
||||
<div className = { classes.fileAuthorParticipantName }>
|
||||
{ file.authorParticipantName }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className = { `${classes.fileTimestamp} timestampVisibility` }>
|
||||
<pre>
|
||||
{ formatTimestamp(file.timestamp) }
|
||||
</pre>
|
||||
</div>
|
||||
<div className = { `${classes.buttonContainer} actionIconVisibility` }>
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.downloadFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(downloadFile(file.fileId)) }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
</button>
|
||||
|
||||
{
|
||||
isUploadEnabled && (
|
||||
<button
|
||||
aria-label = { `${t('fileSharing.removeFile')} ${file.fileName}` }
|
||||
className = { `${classes.iconButton}` }
|
||||
onClick = { () => dispatch(removeFile(file.fileId)) }
|
||||
type = 'button'>
|
||||
<Icon
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
size = { 24 }
|
||||
src = { IconTrash } />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
(file.progress ?? 100) < 100 && (
|
||||
<>
|
||||
<div
|
||||
aria-label = { t('fileSharing.fileUploadProgress') }
|
||||
aria-valuemax = { 100 }
|
||||
aria-valuemin = { 0 }
|
||||
aria-valuenow = { file.progress }
|
||||
className = { classes.progressBar }
|
||||
role = 'progressbar'>
|
||||
<div
|
||||
className = { classes.progressFill }
|
||||
style = {{ width: `${file.progress}%` }} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<li key = { file.fileId }>
|
||||
<FileItem
|
||||
file = { file }
|
||||
onDownload = { fileId => dispatch(downloadFile(fileId)) }
|
||||
onRemove = { fileId => dispatch(removeFile(fileId)) }
|
||||
showRemoveButton = { isUploadEnabled } />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -6,12 +6,21 @@ import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { ChatTabs } from '../chat/constants';
|
||||
import { addMessage, editMessage } from '../chat/actions.any';
|
||||
import { ChatTabs, MESSAGE_TYPE_LOCAL, MESSAGE_TYPE_REMOTE } from '../chat/constants';
|
||||
import { showErrorNotification, showNotification, showSuccessNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
import { I_AM_VISITOR_MODE } from '../visitors/actionTypes';
|
||||
|
||||
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
|
||||
import {
|
||||
ADD_FILE,
|
||||
DOWNLOAD_FILE,
|
||||
REMOVE_FILE,
|
||||
UPDATE_FILE_UPLOAD_PROGRESS,
|
||||
UPLOAD_FILES,
|
||||
_FILE_LIST_RECEIVED,
|
||||
_FILE_REMOVED
|
||||
} from './actionTypes';
|
||||
import { addFile, removeFile, updateFileProgress } from './actions';
|
||||
import { getFileExtension } from './functions.any';
|
||||
import logger from './logger';
|
||||
@@ -110,6 +119,44 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
case UPDATE_FILE_UPLOAD_PROGRESS:
|
||||
case ADD_FILE: {
|
||||
const result = next(action);
|
||||
const state = store.getState();
|
||||
const { files } = state['features/file-sharing'];
|
||||
const file = action.type === ADD_FILE ? action.file as IFileMetadata : files.get(action.fileId);
|
||||
|
||||
if (!file) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const isLocalFile = localParticipant?.id === file.authorParticipantId;
|
||||
|
||||
// Only dispatch chat message for fully uploaded files (progress === 100).
|
||||
// Files that are still uploading have progress < 100, so we skip creating the message.
|
||||
// Once upload completes, for the local participant the file is broadcast with progress: 100 and the message
|
||||
// is created. Remote participants receive the file metadata only once the file is successfully uploaded and
|
||||
// the progress field will be undefined.
|
||||
if (file.progress === 100 || !isLocalFile) {
|
||||
store.dispatch(addMessage({
|
||||
displayName: file.authorParticipantName,
|
||||
fileMetadata: file,
|
||||
hasRead: isLocalFile,
|
||||
isReaction: false,
|
||||
lobbyChat: false,
|
||||
message: '', // Empty message as the file metadata contains all info
|
||||
messageId: file.fileId,
|
||||
messageType: isLocalFile ? MESSAGE_TYPE_LOCAL : MESSAGE_TYPE_REMOTE,
|
||||
participantId: file.authorParticipantId,
|
||||
privateMessage: false,
|
||||
timestamp: file.timestamp
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case REMOVE_FILE: {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
@@ -196,6 +243,27 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
case _FILE_REMOVED: {
|
||||
const result = next(action);
|
||||
const state = store.getState();
|
||||
const { messages } = state['features/chat'];
|
||||
|
||||
// Find the message corresponding to this file and mark it as deleted.
|
||||
const fileMessage = messages.find(msg => msg.messageId === action.fileId);
|
||||
|
||||
if (fileMessage?.fileMetadata) {
|
||||
// Replace the file metadata with just the isDeleted flag to avoid keeping unnecessary data.
|
||||
store.dispatch(editMessage({
|
||||
...fileMessage,
|
||||
fileMetadata: {
|
||||
isDeleted: true
|
||||
} as any
|
||||
}));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface IFileMetadata {
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
isDeleted?: boolean;
|
||||
progress?: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import {
|
||||
getActiveSpeakersToBeDisplayed,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { getParticipantById, getVirtualScreenshareParticipantOwnerId } from '../base/participants/functions';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
import { isFilmstripScrollVisible } from './functions';
|
||||
|
||||
/**
|
||||
* Returns an array containing the first `count` items from a set.
|
||||
*
|
||||
* @param {Set<T>} set - The set from which to take items.
|
||||
* @param {number} count - The number of items to take.
|
||||
* @returns {T[]} An array containing the taken items.
|
||||
* @private
|
||||
*/
|
||||
function _takeFirstN<T>(set: Set<T>, count: number): T[] {
|
||||
const result: T[] = [];
|
||||
|
||||
for (const item of set) {
|
||||
if (result.length >= count) break;
|
||||
result.push(item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the reorderd list of the remote participants.
|
||||
*
|
||||
@@ -16,7 +32,7 @@ import { isFilmstripScrollVisible } from './functions';
|
||||
* @returns {void}
|
||||
* @private
|
||||
*/
|
||||
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string) {
|
||||
export function updateRemoteParticipants(store: IStore, force?: boolean, participantId?: string): void {
|
||||
const state = store.getState();
|
||||
let reorderedParticipants = [];
|
||||
const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants'];
|
||||
@@ -33,14 +49,26 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
}
|
||||
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
previousSpeakers,
|
||||
sortedRemoteParticipants
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
const remoteParticipants = new Map(sortedRemoteParticipants);
|
||||
const screenShareParticipants = sortedRemoteVirtualScreenshareParticipants
|
||||
? [ ...sortedRemoteVirtualScreenshareParticipants.keys() ] : [];
|
||||
const sharedVideos = fakeParticipants ? Array.from(fakeParticipants.keys()) : [];
|
||||
const speakers = getActiveSpeakersToBeDisplayed(state);
|
||||
const speakers: Set<string> = new Set();
|
||||
const dominant = dominantSpeaker ? getParticipantById(state, dominantSpeaker) : undefined;
|
||||
const config = state['features/base/config'];
|
||||
const defaultRemoteDisplayName = config?.defaultRemoteDisplayName ?? 'Fellow Jitster';
|
||||
|
||||
// Generate the remote active speakers list.
|
||||
if (dominant && !dominant.local) {
|
||||
speakers.add(dominant.id);
|
||||
}
|
||||
previousSpeakers.forEach(id => speakers.add(id));
|
||||
|
||||
for (const screenshare of screenShareParticipants) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
@@ -57,7 +85,16 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
remoteParticipants.delete(speaker);
|
||||
}
|
||||
|
||||
// Always update the order of the thubmnails.
|
||||
// Calculate the number of slots available for active speakers and then sort them alphabetically to ensure
|
||||
// consistent order.
|
||||
const numberOfActiveSpeakerSlots
|
||||
= visibleRemoteParticipants.size - (screenShareParticipants.length * 2) - sharedVideos.length;
|
||||
const activeSpeakersDisplayed = _takeFirstN(speakers, numberOfActiveSpeakerSlots)
|
||||
.sort((a: string, b: string) => {
|
||||
return (getParticipantById(state, a)?.name ?? defaultRemoteDisplayName)
|
||||
.localeCompare(getParticipantById(state, b)?.name ?? defaultRemoteDisplayName);
|
||||
});
|
||||
|
||||
const participantsWithScreenShare = screenShareParticipants.reduce<string[]>((acc, screenshare) => {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare);
|
||||
|
||||
@@ -67,10 +104,11 @@ export function updateRemoteParticipants(store: IStore, force?: boolean, partici
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Always update the order of the thumbnails.
|
||||
reorderedParticipants = [
|
||||
...participantsWithScreenShare,
|
||||
...sharedVideos,
|
||||
...Array.from(speakers.keys()),
|
||||
...activeSpeakersDisplayed,
|
||||
...Array.from(remoteParticipants.keys())
|
||||
];
|
||||
|
||||
|
||||
@@ -38,9 +38,10 @@ export const getKeyboardKey = (e: KeyboardEvent): string => {
|
||||
// If alt is pressed a different char can be returned so this takes
|
||||
// the char from the code. It also prefixes with a colon to differentiate
|
||||
// alt combo from simple keypress.
|
||||
if (altKey) {
|
||||
const replacedKey = code.replace('Key', '');
|
||||
|
||||
const replacedKey = code.replace('Key', '');
|
||||
|
||||
if (altKey) {
|
||||
return `:${replacedKey}`;
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ export const getKeyboardKey = (e: KeyboardEvent): string => {
|
||||
return `-${key}`;
|
||||
}
|
||||
|
||||
if (code.startsWith('Key')) {
|
||||
return replacedKey;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,18 +161,26 @@ function _electParticipantInLargeVideo(state: IReduxState) {
|
||||
}
|
||||
}
|
||||
|
||||
// Next, pick the dominant speaker (other than self).
|
||||
// Next, pick the dominant speaker or the last active speaker if the dominant speaker is local.
|
||||
participant = getDominantSpeakerParticipant(state);
|
||||
if (participant && !participant.local) {
|
||||
// Return the screensharing participant id associated with this endpoint if multi-stream is enabled and
|
||||
// auto_pin_latest_screen_share setting is disabled.
|
||||
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, participant.id);
|
||||
let speakerId: string | undefined;
|
||||
|
||||
return screenshareParticipant?.id ?? participant.id;
|
||||
if (participant?.local) {
|
||||
const { previousSpeakers } = state['features/base/participants'];
|
||||
|
||||
if (previousSpeakers?.size) {
|
||||
speakerId = previousSpeakers.keys().next().value;
|
||||
}
|
||||
} else if (participant) {
|
||||
speakerId = participant.id;
|
||||
}
|
||||
|
||||
// In case this is the local participant.
|
||||
participant = undefined;
|
||||
// Return the screensharing participant id associated with this endpoint.
|
||||
if (speakerId) {
|
||||
const screenshareParticipant = getVirtualScreenshareParticipantByOwnerId(state, speakerId);
|
||||
|
||||
return screenshareParticipant?.id ?? speakerId;
|
||||
}
|
||||
|
||||
// Next, pick the most recent participant with video.
|
||||
const lastVisibleRemoteParticipant = _electLastVisibleRemoteParticipant(state);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
@@ -35,6 +35,16 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
_aspectRatio: Symbol;
|
||||
|
||||
/**
|
||||
* The current height of the screen.
|
||||
*/
|
||||
_clientHeight: number;
|
||||
|
||||
/**
|
||||
* The current width of the screen.
|
||||
*/
|
||||
_clientWidth: number;
|
||||
|
||||
/**
|
||||
* The room name.
|
||||
*/
|
||||
@@ -63,33 +73,33 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _aspectRatio, _roomName } = this.props;
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
const { _aspectRatio, _clientHeight, _clientWidth, _roomName } = this.props;
|
||||
const isTablet = Math.min(_clientWidth, _clientHeight) >= 768;
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapper;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
} else {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapperWide;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainerWide;
|
||||
let contentContainerStyles = preJoinStyles.contentContainer;
|
||||
let largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
|
||||
|
||||
if (isTablet && _aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
// @ts-ignore
|
||||
contentContainerStyles = preJoinStyles.contentContainerWide;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainerWide;
|
||||
}
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
style = { preJoinStyles.contentWrapper }>
|
||||
<BrandingImageBackground />
|
||||
<View style = { largeVideoContainerStyles as ViewStyle }>
|
||||
<View style = { preJoinStyles.displayRoomNameBackdrop as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { preJoinStyles.preJoinRoomName }>
|
||||
{ _roomName }
|
||||
</Text>
|
||||
<View style = { preJoinStyles.conferenceInfo as ViewStyle }>
|
||||
<View style = { preJoinStyles.displayRoomNameBackdrop }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { preJoinStyles.preJoinRoomName }>
|
||||
{ _roomName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<LargeVideo />
|
||||
</View>
|
||||
@@ -304,6 +314,8 @@ function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_clientHeight: state['features/base/responsive-ui'].clientHeight,
|
||||
_clientWidth: state['features/base/responsive-ui'].clientWidth,
|
||||
_roomName: getConferenceName(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ export default {
|
||||
},
|
||||
|
||||
passwordJoinButtons: {
|
||||
top: 40
|
||||
top: BaseTheme.spacing[7]
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
bottom: BaseTheme.spacing[0],
|
||||
display: 'flex',
|
||||
height: 388,
|
||||
justifyContent: 'center',
|
||||
@@ -45,46 +45,6 @@ export default {
|
||||
marginBottom: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
// KnockingParticipantList
|
||||
|
||||
knockingParticipantList: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
},
|
||||
|
||||
|
||||
knockingParticipantListDetails: {
|
||||
flex: 1,
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
knockingParticipantListEntry: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
knockingParticipantListText: {
|
||||
color: 'white'
|
||||
},
|
||||
|
||||
lobbyButtonAdmit: {
|
||||
position: 'absolute',
|
||||
right: 184,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonChat: {
|
||||
position: 'absolute',
|
||||
right: 104,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonReject: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyTitle: {
|
||||
...BaseTheme.typography.heading5,
|
||||
color: BaseTheme.palette.text01,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { NavigationContainer, Theme } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
|
||||
import Prejoin from '../../../prejoin/components/native/Prejoin';
|
||||
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions.native';
|
||||
import VisitorsQueue from '../../../visitors/components/native/VisitorsQueue';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
@@ -70,11 +69,6 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
|
||||
onReady = { onReady }
|
||||
ref = { rootNavigationRef }
|
||||
theme = { navigationContainerTheme as Theme }>
|
||||
<StatusBar
|
||||
animated = { true }
|
||||
backgroundColor = 'transparent'
|
||||
barStyle = { 'light-content' }
|
||||
translucent = { true } />
|
||||
<RootStack.Navigator
|
||||
initialRouteName = { initialRouteName }>
|
||||
{
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getFieldValue } from '../../../base/react/functions';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
@@ -51,9 +51,10 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFocused = useIsFocused();
|
||||
const { t } = useTranslation();
|
||||
const aspectRatio = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']?.aspectRatio
|
||||
const { aspectRatio, clientHeight, clientWidth } = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']
|
||||
);
|
||||
const isTablet = Math.min(clientWidth, clientHeight) >= 768;
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const isDisplayNameMandatory = useSelector((state: IReduxState) => isDisplayNameRequired(state));
|
||||
const isDisplayNameVisible
|
||||
@@ -61,7 +62,8 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const isDisplayNameReadonly = useSelector(isNameReadOnly);
|
||||
const roomName = useSelector((state: IReduxState) => getConferenceName(state));
|
||||
const roomNameEnabled = useSelector((state: IReduxState) => isRoomNameEnabled(state));
|
||||
const { showHangUp = true } = useSelector((state: IReduxState) => getLobbyConfig(state));
|
||||
const { showHangUp: showHangUpLobby = true } = useSelector((state: IReduxState) => getLobbyConfig(state));
|
||||
const { showHangUp: showHangUpPrejoin = true } = useSelector((state: IReduxState) => state['features/base/config'].prejoinConfig || {});
|
||||
const { knocking } = useSelector((state: IReduxState) => state['features/lobby']);
|
||||
const participantName = localParticipant?.name;
|
||||
const [ displayName, setDisplayName ]
|
||||
@@ -146,16 +148,11 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
let contentContainerStyles = styles.contentContainer;
|
||||
let largeVideoContainerStyles = styles.largeVideoContainer;
|
||||
|
||||
if (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = styles.contentWrapper;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
largeVideoContainerStyles = styles.largeVideoContainer;
|
||||
} else {
|
||||
contentWrapperStyles = styles.contentWrapperWide;
|
||||
if (isTablet && aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
// @ts-ignore
|
||||
contentContainerStyles = styles.contentContainerWide;
|
||||
largeVideoContainerStyles = styles.largeVideoContainerWide;
|
||||
}
|
||||
@@ -164,7 +161,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
style = { styles.contentWrapper }>
|
||||
<BrandingImageBackground />
|
||||
{
|
||||
isFocused
|
||||
@@ -190,7 +187,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
<VideoMuteButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
{
|
||||
showHangUp && knocking
|
||||
(knocking ? showHangUpLobby : showHangUpPrejoin)
|
||||
&& <HangupButton
|
||||
styles = { styles.buttonStylesBorderless } />
|
||||
}
|
||||
@@ -206,7 +203,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
}
|
||||
{
|
||||
showDisplayNameError && (
|
||||
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
|
||||
<View style = { styles.errorContainer as StyleProp<ViewStyle> }>
|
||||
<Text style = { styles.error as StyleProp<TextStyle> }>
|
||||
{ t('prejoin.errorMissingName') }
|
||||
</Text>
|
||||
|
||||
@@ -86,12 +86,14 @@ const UnsafeRoomWarning: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProp
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { styles.unsafeRoomWarningContainer } >
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
<View style = { styles.conferenceInfo as ViewStyle }>
|
||||
<View style = { styles.displayRoomNameBackdrop as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style = { unsafeRoomContentContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.warningIconWrapper as StyleProp<ViewStyle> }>
|
||||
|
||||
@@ -10,14 +10,14 @@ export const preJoinStyles = {
|
||||
buttonStylesBorderless: {
|
||||
iconStyle: {
|
||||
color: BaseTheme.palette.icon01,
|
||||
fontSize: 24
|
||||
fontSize: BaseTheme.spacing[4]
|
||||
},
|
||||
style: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: BaseTheme.spacing[3],
|
||||
height: 24,
|
||||
width: 24
|
||||
height: BaseTheme.spacing[4],
|
||||
width: BaseTheme.spacing[4]
|
||||
},
|
||||
underlayColor: 'transparent'
|
||||
},
|
||||
@@ -27,13 +27,8 @@ export const preJoinStyles = {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
contentWrapperWide: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
largeVideoContainer: {
|
||||
height: '60%'
|
||||
height: '50%'
|
||||
},
|
||||
|
||||
largeVideoContainerWide: {
|
||||
@@ -46,9 +41,9 @@ export const preJoinStyles = {
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
bottom: BaseTheme.spacing[0],
|
||||
display: 'flex',
|
||||
height: 280,
|
||||
height: '50%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
@@ -115,6 +110,7 @@ export const preJoinStyles = {
|
||||
maxWidth: 273,
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
displayRoomNameBackdrop: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
@@ -122,6 +118,7 @@ export const preJoinStyles = {
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
recordingWarning: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -130,9 +127,11 @@ export const preJoinStyles = {
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
width: 'auto'
|
||||
},
|
||||
|
||||
recordingWarningText: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
|
||||
unsafeRoomWarningContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
@@ -142,6 +141,7 @@ export const preJoinStyles = {
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
},
|
||||
|
||||
unsafeRoomContentContainer: {
|
||||
justifySelf: 'center',
|
||||
height: '100%',
|
||||
@@ -167,6 +167,7 @@ export const preJoinStyles = {
|
||||
textAlign: 'center',
|
||||
marginBottom: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
warningIconWrapper: {
|
||||
backgroundColor: BaseTheme.palette.warning01,
|
||||
borderRadius: BaseTheme.shape.circleRadius,
|
||||
@@ -175,8 +176,9 @@ export const preJoinStyles = {
|
||||
zIndex: 0
|
||||
|
||||
},
|
||||
|
||||
warningIcon: {
|
||||
color: BaseTheme.palette.ui01,
|
||||
fontSize: 40
|
||||
fontSize: BaseTheme.spacing[7]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,7 +45,15 @@ export function iAmVisitor(stateful: IStateful) {
|
||||
* @returns {number} - The number of visitors.
|
||||
*/
|
||||
export function getVisitorsCount(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].count ?? 0;
|
||||
const state = toState(stateful);
|
||||
const { hideVisitorCountForVisitors } = state['features/base/config'].visitors || {};
|
||||
const isVisitor = state['features/visitors'].iAmVisitor;
|
||||
|
||||
if (isVisitor && hideVisitorCountForVisitors) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return state['features/visitors'].count ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -64,6 +64,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
if (getState()['features/visitors'].iAmVisitor) {
|
||||
|
||||
const { demoteActorDisplayName } = getState()['features/visitors'];
|
||||
const { showJoinMeetingDialog = true } = getState()['features/base/config'].visitors || {};
|
||||
|
||||
if (demoteActorDisplayName) {
|
||||
const notificationParams: INotificationProps = {
|
||||
@@ -78,7 +79,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
dispatch(setVisitorDemoteActor(undefined));
|
||||
});
|
||||
} else {
|
||||
} else if (showJoinMeetingDialog) {
|
||||
dispatch(openDialog(JoinMeetingDialog));
|
||||
}
|
||||
|
||||
|
||||
@@ -69,8 +69,22 @@ local function verify_user(session, stanza)
|
||||
local user_bare_jid = jid_bare(user_jid);
|
||||
local _, user_domain = jid_split(user_jid);
|
||||
|
||||
-- allowlist for participants
|
||||
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) then
|
||||
-- allowlist for participants, jigasi (sip & transcriber), jibri (recorder & sip)
|
||||
if allowlist:contains(user_domain)
|
||||
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
|
||||
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
@@ -261,9 +261,9 @@ export class Participant {
|
||||
await this.switchToIFrame();
|
||||
}
|
||||
|
||||
if (!options.skipWaitToJoin) {
|
||||
await this.waitForMucJoinedOrError();
|
||||
}
|
||||
// if (!options.skipWaitToJoin) {
|
||||
// await this.waitForMucJoinedOrError();
|
||||
// }
|
||||
|
||||
await this.postLoadProcess();
|
||||
|
||||
@@ -306,7 +306,7 @@ export class Participant {
|
||||
}
|
||||
}, this._name, driver.sessionId, LOG_PREFIX));
|
||||
|
||||
await Promise.all(parallel);
|
||||
// await Promise.all(parallel);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,6 +579,50 @@ export class Participant {
|
||||
return new LargeVideo(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a specific participant to be displayed on large video.
|
||||
*
|
||||
* @param {string} expectedEndpointId - The endpoint ID of the participant expected on large video.
|
||||
* @param {string} timeoutMsg - Optional custom timeout message.
|
||||
* @param {number} timeout - Optional timeout in milliseconds (default: 30000).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForParticipantOnLargeVideo(
|
||||
expectedEndpointId: string,
|
||||
timeoutMsg?: string,
|
||||
timeout: number = 30_000): Promise<void> {
|
||||
await this.driver.waitUntil(
|
||||
async () => await this.getLargeVideo().getResource() === expectedEndpointId,
|
||||
{
|
||||
timeout,
|
||||
timeoutMsg: timeoutMsg || `Expected ${expectedEndpointId} on large video for ${this.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for any one of the specified participants to be displayed on large video.
|
||||
*
|
||||
* @param {string[]} expectedEndpointIds - Array of endpoint IDs, any one of which is expected on large video.
|
||||
* @param {string} timeoutMsg - Optional custom timeout message.
|
||||
* @param {number} timeout - Optional timeout in milliseconds (default: 30000).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForAnyParticipantOnLargeVideo(
|
||||
expectedEndpointIds: string[],
|
||||
timeoutMsg?: string,
|
||||
timeout: number = 30_000): Promise<void> {
|
||||
await this.driver.waitUntil(
|
||||
async () => {
|
||||
const largeVideoResource = await this.getLargeVideo().getResource();
|
||||
|
||||
return expectedEndpointIds.includes(largeVideoResource);
|
||||
},
|
||||
{
|
||||
timeout,
|
||||
timeoutMsg: timeoutMsg || `Expected one of [${expectedEndpointIds.join(', ')}] on large video for ${this.name}`
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the videoQuality Dialog.
|
||||
*
|
||||
|
||||
@@ -34,11 +34,11 @@ export async function ensureOneParticipant(
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(options?: IJoinOptions): Promise<void> {
|
||||
await ensureOneParticipant(options);
|
||||
// await ensureOneParticipant(options);
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
joinParticipant({ name: P2 }, options),
|
||||
// joinParticipant({ name: P2 }, options),
|
||||
joinParticipant({ name: P3 }, options)
|
||||
]);
|
||||
|
||||
@@ -46,16 +46,16 @@ export async function ensureThreeParticipants(options?: IJoinOptions): Promise<v
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
ctx.p1.waitForIceConnected(),
|
||||
ctx.p2.waitForIceConnected(),
|
||||
ctx.p3.waitForIceConnected()
|
||||
]);
|
||||
await Promise.all([
|
||||
ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
]);
|
||||
// await Promise.all([
|
||||
// ctx.p1.waitForIceConnected(),
|
||||
// ctx.p2.waitForIceConnected(),
|
||||
// ctx.p3.waitForIceConnected()
|
||||
// ]);
|
||||
// await Promise.all([
|
||||
// ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)),
|
||||
// ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)),
|
||||
// ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)),
|
||||
// ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,6 +173,9 @@ async function joinParticipant( // eslint-disable-line max-params
|
||||
}
|
||||
|
||||
if (await p.isInMuc()) {
|
||||
|
||||
console.log('return because it is in muc', p.name);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { config as testsConfig } from './TestsConfig';
|
||||
const https = require('https');
|
||||
|
||||
export function generateRoomName(testName: string) {
|
||||
// XXX why chose between 1 and 40 and then always pad with an extra 0?
|
||||
const rand = (Math.floor(Math.random() * 40) + 1).toString().padStart(3, '0');
|
||||
const rand = (Math.floor(Math.random() * 400) + 1).toString();
|
||||
let roomName = `${testName}-${rand}`;
|
||||
|
||||
if (testsConfig.roomName.prefix) {
|
||||
|
||||
@@ -119,7 +119,7 @@ class BreakoutRoom extends BasePageObject {
|
||||
|
||||
await listItem.click();
|
||||
|
||||
const button = listItem.$(`aria/${MORE_LABEL}`);
|
||||
const button = listItem.$(`button[title="${MORE_LABEL}"]`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
@@ -140,7 +140,36 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
return await this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`).length;
|
||||
// 2025-10-21T16:28:20.737Z INFO webdriver: BIDI RESULT {"id":48,"result":{"nodes":[{"sharedId":"f.CC595D3B0EA529D11DA54EA54EC8E1A9.d.AC4B929B2F37714F6A7D1AFFBDAC2B04.e.103","type":"node","value":{"attributes":{"class":"css-169oaf-participantsPane","id":"participants-pane"},"childNodeCount":3,"localName":"div","namespaceURI":"http://www.w3.org/1999/xhtml","nodeType":1,"shadowRoot":null}}]},"type":"success"}
|
||||
//
|
||||
//
|
||||
//
|
||||
// start query 2025-10-21T16:28:28.875Z
|
||||
// end query 2025-10-21T16:28:28.875Z
|
||||
// 2025-10-21T16:28:20.738Z INFO webdriver: BIDI COMMAND script.callFunction {"functionDeclaration":"<Function1111[function anonymous(\n) {\nreturn (/* __wdio script__ */function anonymous(\n) {\nconsole.log(\"start query\", new Date().toISOString()); const res = document.querySelectorAll(`#participants-pane .breakout-room-container`).length; console.log(\"end query\",new Date().toISOString()); return res;\n}/* __wdio script end__ */).apply(this, arguments);\n} bytes] anonymous>","awaitPromise":true,"arguments":[],"target":{"context":"CC595D3B0EA529D11DA54EA54EC8E1A9"}}
|
||||
// 2025-10-21T16:28:29.080Z INFO webdriver: BIDI RESULT {"id":49,"result":{"realm":"4934028136633545550.-854534272777004590","result":{"type":"number","value":2},"type":"success"},"type":"success"}
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
// 2025-10-21T16:28:29.086Z INFO webdriver: BIDI COMMAND browsingContext.locateNodes {"locator":{"type":"css","value":"#participants-pane .breakout-room-container"},"context":"CC595D3B0EA529D11DA54EA54EC8E1A9"}
|
||||
|
||||
|
||||
// [8546:62532126:1021/140916.958361:INFO:CONSOLE:12] "start query 2025-10-21T19:09:16.958Z", source: (12)
|
||||
// [8546:62532126:1021/140916.958526:INFO:CONSOLE:12] "end query 2025-10-21T19:09:16.958Z", source: (12)
|
||||
|
||||
|
||||
|
||||
const st = Date.now();
|
||||
|
||||
console.log(`${new Date().toISOString()} took: start query`);
|
||||
// const b = await this.participant.driver.$$(`#participants-pane .${BREAKOUT_ROOMS_CLASS}`).length;
|
||||
const b = await this.participant.driver.execute(
|
||||
'console.log("start query", new Date().toISOString()); const res = document.querySelectorAll(`#participants-pane .breakout-room-container`).length; console.log("end query",new Date().toISOString()); return res;');
|
||||
|
||||
console.log(`${new Date().toISOString()} took: `, Date.now() - st, 'ms to get breakout rooms count: ', b, this.participant.name, this.participant.driver.sessionId);
|
||||
|
||||
return b;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,7 +182,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
|
||||
const addBreakoutButton = this.participant.driver.$(`button=${ADD_BREAKOUT_ROOM}`);
|
||||
|
||||
await addBreakoutButton.waitForDisplayed();
|
||||
await addBreakoutButton.click();
|
||||
@@ -163,7 +192,10 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
* Returns all breakout rooms.
|
||||
*/
|
||||
async getRooms(): Promise<BreakoutRoom[]> {
|
||||
const rooms = this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`);
|
||||
const st = Date.now();
|
||||
const rooms = this.participant.driver.$$(`#participants-pane .${BREAKOUT_ROOMS_CLASS}`);
|
||||
|
||||
console.log('took: ', Date.now() - st, 'ms to get breakout rooms');
|
||||
|
||||
return rooms.map(async room => new BreakoutRoom(
|
||||
this.participant, await room.$('span').getText(), await room.getAttribute('data-testid')));
|
||||
@@ -179,7 +211,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.open();
|
||||
}
|
||||
|
||||
const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
|
||||
const leaveButton = this.participant.driver.$(`button=${LEAVE_ROOM_LABEL}`);
|
||||
|
||||
await leaveButton.isClickable();
|
||||
await leaveButton.click();
|
||||
@@ -189,7 +221,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
* Auto assign participants to breakout rooms.
|
||||
*/
|
||||
async autoAssignToBreakoutRooms() {
|
||||
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
|
||||
const button = this.participant.driver.$(`button=${AUTO_ASSIGN_LABEL}`);
|
||||
|
||||
await button.waitForClickable();
|
||||
await button.click();
|
||||
@@ -204,7 +236,7 @@ export default class BreakoutRooms extends BasePageObject {
|
||||
await participantsPane.selectParticipant(participant);
|
||||
await participantsPane.openParticipantContextMenu(participant);
|
||||
|
||||
const sendButton = this.participant.driver.$(`aria/${roomName}`);
|
||||
const sendButton = this.participant.driver.$(`button=${roomName}`);
|
||||
|
||||
await sendButton.waitForClickable();
|
||||
await sendButton.click();
|
||||
|
||||
@@ -40,6 +40,7 @@ export default class PasswordDialog extends BaseDialog {
|
||||
const passwordInput = this.participant.driver.$(INPUT_KEY_XPATH);
|
||||
|
||||
await passwordInput.waitForExist();
|
||||
await passwordInput.waitForClickable();
|
||||
await passwordInput.click();
|
||||
await passwordInput.clearValue();
|
||||
|
||||
|
||||
@@ -58,15 +58,16 @@ describe('JaaS CHAT_UPLOADED webhook.', () => {
|
||||
expect(uploadedChat.meetingFqn).toBe(fqn);
|
||||
expect(uploadedChat.messageType).toBe('CHAT');
|
||||
|
||||
const messages = uploadedChat.messages;
|
||||
const messages: any[] = uploadedChat.messages;
|
||||
|
||||
expect(messages).toBeDefined();
|
||||
expect(messages.length).toBe(3);
|
||||
expect(messages[0].content).toBe('foo');
|
||||
expect(messages[0].name).toBe('p1');
|
||||
expect(messages[1].content).toBe('bar');
|
||||
expect(messages[1].name).toBe('p2');
|
||||
expect(messages[2].content).toBe('baz');
|
||||
expect(messages[2].name).toBe('p1');
|
||||
console.log(JSON.stringify(messages));
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'foo')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p2' && m.content === 'bar')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'baz')).toBe(true);
|
||||
messages.forEach(m => {
|
||||
expect(typeof m.timestamp).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,6 +66,7 @@ describe('Dial-in', () => {
|
||||
await p1.getFilmstrip().kickParticipant(endpointId);
|
||||
|
||||
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
|
||||
|
||||
await p1.hangup();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -98,9 +98,16 @@ describe('XMPP login and MUC join', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// it('without sending a conference-request', async () => {
|
||||
// console.log('Joining a MUC without sending a conference-request');
|
||||
// // TODO verify failure
|
||||
// //expect(await joinMuc(ctx.roomName, 'p1', token)).toBe(true);
|
||||
// });
|
||||
it('without sending a conference-request', async () => {
|
||||
console.log('Joining a MUC without sending a conference-request');
|
||||
const p = await joinJaasMuc({
|
||||
token: t({ room: `${ctx.roomName}-no-cf` })
|
||||
}, {
|
||||
configOverwrite: {
|
||||
disableFocus: true // this effectively disables the sending of a conference-request
|
||||
}
|
||||
});
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
|
||||
it('test visitor tokens', async () => {
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
const moderatorToken = t({ room, displayName: 'Mo de Rator', moderator: true });
|
||||
const moderator = await joinJaasMuc({ name: 'p1', token: moderatorToken });
|
||||
|
||||
@@ -90,6 +91,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await moderator.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(moderator);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token before any visitors
|
||||
const participantToken = t({ room, displayName: 'Parti Cipant' });
|
||||
const participant = await joinJaasMuc({ name: 'p2', token: participantToken });
|
||||
@@ -99,6 +101,7 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await participant.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a visitor token
|
||||
const visitorToken = t({ room, displayName: 'Visi Tor', visitor: true });
|
||||
const visitor = await joinJaasMuc({ name: 'p3', token: visitorToken });
|
||||
@@ -108,9 +111,11 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await visitor.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await participant.hangup();
|
||||
await verifyLeftWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token after visitors -> visitor
|
||||
const participantToken2 = t({ room, displayName: 'Visi Tor 2' });
|
||||
const visitor2 = await joinJaasMuc({ name: 'p2', token: participantToken2 });
|
||||
@@ -120,12 +125,15 @@ describe('Visitors triggered by visitor tokens', () => {
|
||||
expect(await visitor2.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor.hangup();
|
||||
await verifyLeftWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor2.hangup();
|
||||
await verifyLeftWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await moderator.hangup();
|
||||
await verifyLeftWebhook(moderator);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,16 @@ describe('Active speaker', () => {
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Exit tile view to enter stage view (default for 3 participants is tile view)
|
||||
await p1.getToolbar().clickExitTileViewButton();
|
||||
await p1.waitForTileViewDisplayed(true);
|
||||
|
||||
await p2.getToolbar().clickExitTileViewButton();
|
||||
await p2.waitForTileViewDisplayed(true);
|
||||
|
||||
await p3.getToolbar().clickExitTileViewButton();
|
||||
await p3.waitForTileViewDisplayed(true);
|
||||
|
||||
await muteAudioAndCheck(p1, p2);
|
||||
await muteAudioAndCheck(p2, p1);
|
||||
await muteAudioAndCheck(p3, p1);
|
||||
@@ -31,6 +41,110 @@ describe('Active speaker', () => {
|
||||
await assertOneDominantSpeaker(p2);
|
||||
await assertOneDominantSpeaker(p3);
|
||||
});
|
||||
|
||||
it('testLocalDominantSpeakerShowsLastRemote', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// p2 speaks first
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not displayed as dominant speaker on P1');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p3 speaks second
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P3 not displayed as dominant speaker on P1');
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// Now p1 starts screenshare and becomes dominant speaker
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// p1 should see p3 (last remote dominant speaker) on stage
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P1 should see P3 (last remote speaker) on stage when local is dominant');
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
});
|
||||
|
||||
it('testAlternatingLocalAndRemoteDominantSpeaker', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// Test alternating pattern: p2 -> p1 (local) -> p3 -> p1 (local) -> p2
|
||||
// This tests that local dominant speaker consistently shows last remote speaker
|
||||
|
||||
// p2 speaks
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not shown on P1');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks - should still show p2
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P1 local dominant should show P2 (last remote)');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p3 speaks
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P3 not shown on P1');
|
||||
await p3.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks again - should now show p3
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p3EndpointId, 'P1 local dominant should show P3 (last remote)');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p2 speaks again
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P2 not shown on P1 after second speak');
|
||||
await p2.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// p1 (local) speaks - should show p2 again
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(p2EndpointId, 'P1 local dominant should show P2 (last remote) again');
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
});
|
||||
|
||||
it('testDominantSpeakerWithSimultaneousSpeakers', async () => {
|
||||
await ensureThreeParticipants();
|
||||
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p2EndpointId = await p2.getEndpointId();
|
||||
const p3EndpointId = await p3.getEndpointId();
|
||||
|
||||
// Multiple participants unmute simultaneously (this is the scenario we're testing)
|
||||
await p2.getToolbar().clickAudioUnmuteButton();
|
||||
await p3.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// Wait for one to become dominant (bridge will pick one)
|
||||
await p1.waitForAnyParticipantOnLargeVideo(
|
||||
[ p2EndpointId, p3EndpointId ],
|
||||
'No dominant speaker detected when multiple speak simultaneously');
|
||||
|
||||
// Get which one was selected as dominant
|
||||
const firstDominant = await p1.getLargeVideo().getResource();
|
||||
|
||||
// Mute both
|
||||
await Promise.all([
|
||||
p2.getToolbar().clickAudioMuteButton(),
|
||||
p3.getToolbar().clickAudioMuteButton()
|
||||
]);
|
||||
|
||||
// Now p1 becomes local dominant speaker
|
||||
// Should show the last remote dominant speaker (firstDominant)
|
||||
await p1.getToolbar().clickAudioUnmuteButton();
|
||||
await p1.waitForParticipantOnLargeVideo(
|
||||
firstDominant,
|
||||
'Local dominant should show last remote dominant after simultaneous speakers');
|
||||
|
||||
await p1.getToolbar().clickAudioMuteButton();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -63,14 +177,12 @@ async function testActiveSpeaker(
|
||||
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
|
||||
|
||||
// Verify that the user is now an active speaker from otherParticipant1's perspective
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
async () => await otherParticipant1.getFilmstrip().isDominantSpeaker(speakerEndpoint),
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: `${activeSpeaker.name} is not selected as active speaker.`
|
||||
});
|
||||
await otherParticipant1.waitForParticipantOnLargeVideo(speakerEndpoint,
|
||||
`Active speaker ${activeSpeaker.name} not displayed on large video.`,
|
||||
30_000); // 30 seconds
|
||||
await otherParticipant2.waitForParticipantOnLargeVideo(speakerEndpoint,
|
||||
`Active speaker ${activeSpeaker.name} not displayed on large video.`,
|
||||
30_000); // 30 seconds
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VIDEO_CODEC } from '../../../react/features/video-quality/constants';
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import {
|
||||
ensureOneParticipant,
|
||||
@@ -39,8 +40,10 @@ describe('Codec selection', () => {
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
const p1ExpectedCodec = p1.driver.isFirefox ? VP8 : VP9;
|
||||
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(p1ExpectedCodec);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP8);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, p1ExpectedCodec),
|
||||
waitForCodec(p2, VP8)
|
||||
]);
|
||||
});
|
||||
|
||||
it('asymmetric codecs with AV1', async () => {
|
||||
@@ -62,16 +65,13 @@ describe('Codec selection', () => {
|
||||
// Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences.
|
||||
// Except on Firefox because it doesn't support VP9 encode.
|
||||
const p1ExpectedCodec = p1.driver.isFirefox ? VP8 : VP9;
|
||||
const p3ExpectedCodec = (p1.driver.isFirefox && majorVersion < 136) ? VP9 : AV1;
|
||||
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(p1ExpectedCodec);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP8);
|
||||
|
||||
// If there is a Firefox ep in the call, all other eps will switch to VP9.
|
||||
if (p1.driver.isFirefox && majorVersion < 136) {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP9);
|
||||
} else {
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(AV1);
|
||||
}
|
||||
await Promise.all([
|
||||
waitForCodec(p1, p1ExpectedCodec),
|
||||
waitForCodec(p2, VP8),
|
||||
waitForCodec(p3, p3ExpectedCodec)
|
||||
]);
|
||||
});
|
||||
|
||||
it('codec switch over', async () => {
|
||||
@@ -92,8 +92,10 @@ describe('Codec selection', () => {
|
||||
}
|
||||
|
||||
// Check if p1 and p2 are encoding in VP9 which is the default codec.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP9);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP9);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP9),
|
||||
waitForCodec(p2, VP9)
|
||||
]);
|
||||
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
@@ -105,27 +107,28 @@ describe('Codec selection', () => {
|
||||
});
|
||||
const { p3 } = ctx;
|
||||
|
||||
// Check if all three participants are encoding in VP8 now.
|
||||
expect(await p1.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP8);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP8);
|
||||
expect(await p3.execute(() => JitsiMeetJS.app.testing.getLocalCameraEncoding())).toBe(VP8);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP8),
|
||||
waitForCodec(p2, VP8),
|
||||
waitForCodec(p3, VP8)
|
||||
]);
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Check of p1 and p2 have switched to VP9.
|
||||
await p1.driver.waitUntil(
|
||||
() => p1.execute(c => JitsiMeetJS.app.testing.getLocalCameraEncoding() === c, VP9),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p1 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
await p2.driver.waitUntil(
|
||||
() => p2.execute(c => JitsiMeetJS.app.testing.getLocalCameraEncoding() === c, VP9),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: 'p2 did not switch back to VP9'
|
||||
}
|
||||
);
|
||||
await Promise.all([
|
||||
waitForCodec(p1, VP9),
|
||||
waitForCodec(p2, VP9)
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForCodec(p: Participant, codec: string) {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(c => JitsiMeetJS.app.testing.getLocalCameraEncoding() === c, codec),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `${p.name} failed to use VP8`
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ describe('Desktop sharing', () => {
|
||||
await ensureTwoParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2 } = ctx;
|
||||
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
// Check if a remote screen share tile is created on p1.
|
||||
@@ -53,7 +55,13 @@ describe('Desktop sharing', () => {
|
||||
it('p2p to jvb switch', async () => {
|
||||
await ctx.p2.getToolbar().clickDesktopSharingButton();
|
||||
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p1, p2, p3 } = ctx;
|
||||
|
||||
// Check if a remote screen share tile is created on all participants.
|
||||
@@ -72,6 +80,10 @@ describe('Desktop sharing', () => {
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Check if a remote screen share tile is created on p1 and p2 after switching back to p2p.
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
await checkForScreensharingTile(p2, p2);
|
||||
@@ -86,7 +98,14 @@ describe('Desktop sharing', () => {
|
||||
await checkForScreensharingTile(p1, p1);
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
@@ -108,6 +127,10 @@ describe('Desktop sharing', () => {
|
||||
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Start share on both p1 and p2.
|
||||
await p1.getToolbar().clickDesktopSharingButton();
|
||||
await p2.getToolbar().clickDesktopSharingButton();
|
||||
@@ -117,7 +140,14 @@ describe('Desktop sharing', () => {
|
||||
await checkForScreensharingTile(p2, p1);
|
||||
|
||||
// Add p3 back to the conference and check if p1 and p2's shares are visible on p3.
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await checkForScreensharingTile(p1, p3);
|
||||
await checkForScreensharingTile(p2, p3);
|
||||
@@ -137,6 +167,7 @@ describe('Desktop sharing', () => {
|
||||
await ensureOneParticipant({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
@@ -151,7 +182,14 @@ describe('Desktop sharing', () => {
|
||||
await p1.getToolbar().clickStopDesktopSharingButton();
|
||||
|
||||
// Call switches to jvb.
|
||||
await ensureThreeParticipants();
|
||||
await ensureThreeParticipants({
|
||||
configOverwrite: {
|
||||
p2p: {
|
||||
backToP2PDelay: 3,
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
});
|
||||
const { p2, p3 } = ctx;
|
||||
|
||||
// p1 starts share again when call switches to jvb.
|
||||
@@ -167,6 +205,10 @@ describe('Desktop sharing', () => {
|
||||
// p3 leaves the call.
|
||||
await p3.hangup();
|
||||
|
||||
// Wait for p1 and p2 to switch back to p2p.
|
||||
await p1.waitForP2PIceConnected();
|
||||
await p2.waitForP2PIceConnected();
|
||||
|
||||
// Make sure p2 see's p1's share after the call switches back to p2p.
|
||||
await checkForScreensharingTile(p1, p2);
|
||||
expect(await p2.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true);
|
||||
|
||||
@@ -175,11 +175,6 @@ describe('Start muted', () => {
|
||||
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);
|
||||
await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1);
|
||||
|
||||
await Promise.all([
|
||||
p1.getLargeVideo().waitForSwitchTo(await p2.getEndpointId()),
|
||||
p2.getLargeVideo().waitForSwitchTo(await p1.getEndpointId())
|
||||
]);
|
||||
|
||||
await unmuteVideoAndCheck(p2, p1);
|
||||
await p1.getLargeVideo().assertPlaying();
|
||||
});
|
||||
|
||||
@@ -26,148 +26,171 @@ describe('Breakout rooms', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('add breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
// it('add breakout room', async () => {
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // there should be no breakout rooms initially, list is sent with a small delay
|
||||
// await p1.driver.pause(2000);
|
||||
// expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
//
|
||||
// // add one breakout room
|
||||
// await p1BreakoutRooms.addBreakoutRoom();
|
||||
//
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'No breakout room added for p1'
|
||||
// });
|
||||
//
|
||||
//
|
||||
// // second participant should also see one breakout room
|
||||
// await p2.driver.waitUntil(
|
||||
// async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'No breakout room seen by p2'
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('join breakout room', async () => {
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // there should be one breakout room
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'No breakout room seen by p1'
|
||||
// });
|
||||
//
|
||||
// const roomsList = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// expect(roomsList.length).toBe(1);
|
||||
//
|
||||
// // join the room
|
||||
// await roomsList[0].joinRoom();
|
||||
//
|
||||
// // the participant should see the main room as the only breakout room
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].name === MAIN_ROOM_NAME;
|
||||
// }, {
|
||||
// timeout: 5000,
|
||||
// timeoutMsg: 'P1 did not join breakout room'
|
||||
// });
|
||||
//
|
||||
// // the second participant should see one participant in the breakout room
|
||||
// await p2.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p2.getBreakoutRooms().getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'P2 is not seeing p1 in the breakout room'
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('leave breakout room', async () => {
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // leave room
|
||||
// await p1BreakoutRooms.leaveBreakoutRoom();
|
||||
//
|
||||
// // there should be one breakout room and that should not be the main room
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].name !== MAIN_ROOM_NAME;
|
||||
// }, {
|
||||
// timeout: 5000,
|
||||
// timeoutMsg: 'P1 did not leave breakout room'
|
||||
// });
|
||||
//
|
||||
// // the second participant should see no participants in the breakout room
|
||||
// await p2.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p2.getBreakoutRooms().getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 0;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'P2 is seeing p1 in the breakout room'
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('remove breakout room', async () => {
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // remove the room
|
||||
// await (await p1BreakoutRooms.getRooms())[0].removeRoom();
|
||||
//
|
||||
// // there should be no breakout rooms
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => await p1BreakoutRooms.getRoomsCount() === 0, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'Breakout room was not removed for p1'
|
||||
// });
|
||||
//
|
||||
// // the second participant should also see no breakout rooms
|
||||
// await p2.driver.waitUntil(
|
||||
// async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'Breakout room was not removed for p2'
|
||||
// });
|
||||
// });
|
||||
|
||||
// there should be no breakout rooms initially, list is sent with a small delay
|
||||
await p1.driver.pause(2000);
|
||||
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
// const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
//
|
||||
// async function waitForCondition(conditionFn, options = {}) {
|
||||
// const {
|
||||
// timeout = 5000,
|
||||
// interval = 500,
|
||||
// timeoutMsg = 'Condition not met within timeout'
|
||||
// } = options;
|
||||
//
|
||||
// const startTime = Date.now();
|
||||
//
|
||||
// while (Date.now() - startTime < timeout) {
|
||||
// if (await conditionFn()) {
|
||||
// return; // Condition met - pass!
|
||||
// }
|
||||
// await delay(interval);
|
||||
// }
|
||||
//
|
||||
// // Timeout occurred - throw error
|
||||
// throw new Error(timeoutMsg);
|
||||
// }
|
||||
|
||||
// add one breakout room
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
|
||||
// second participant should also see one breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room seen by p2'
|
||||
});
|
||||
});
|
||||
|
||||
it('join breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be one breakout room
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room seen by p1'
|
||||
});
|
||||
|
||||
const roomsList = await p1BreakoutRooms.getRooms();
|
||||
|
||||
expect(roomsList.length).toBe(1);
|
||||
|
||||
// join the room
|
||||
await roomsList[0].joinRoom();
|
||||
|
||||
// the participant should see the main room as the only breakout room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name === MAIN_ROOM_NAME;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not join breakout room'
|
||||
});
|
||||
|
||||
// the second participant should see one participant in the breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2.getBreakoutRooms().getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('leave breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// leave room
|
||||
await p1BreakoutRooms.leaveBreakoutRoom();
|
||||
|
||||
// there should be one breakout room and that should not be the main room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name !== MAIN_ROOM_NAME;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not leave breakout room'
|
||||
});
|
||||
|
||||
// the second participant should see no participants in the breakout room
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2.getBreakoutRooms().getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is seeing p1 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('remove breakout room', async () => {
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// remove the room
|
||||
await (await p1BreakoutRooms.getRooms())[0].removeRoom();
|
||||
|
||||
// there should be no breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Breakout room was not removed for p1'
|
||||
});
|
||||
|
||||
// the second participant should also see no breakout rooms
|
||||
await p2.driver.waitUntil(
|
||||
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'Breakout room was not removed for p2'
|
||||
});
|
||||
});
|
||||
|
||||
it('auto assign', async () => {
|
||||
await ensureThreeParticipants();
|
||||
@@ -179,6 +202,14 @@ describe('Breakout rooms', () => {
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
// there should be two breakout rooms
|
||||
// await waitForCondition(
|
||||
// async () => await p1BreakoutRooms.getRoomsCount() === 2,
|
||||
// {
|
||||
// timeout: 3000,
|
||||
// interval: 500,
|
||||
// timeoutMsg: 'Breakout room was not created by p1'
|
||||
// }
|
||||
// );
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
|
||||
timeout: 3000,
|
||||
@@ -206,6 +237,29 @@ describe('Breakout rooms', () => {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
|
||||
});
|
||||
console.log('stopppppp');
|
||||
await browser.debug()
|
||||
|
||||
// await waitForCondition(
|
||||
// async () => {
|
||||
// if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
// },
|
||||
// {
|
||||
// timeout: 5000,
|
||||
// interval: 500,
|
||||
// timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
|
||||
// }
|
||||
// );
|
||||
|
||||
// the second participant should see one participant in the main room
|
||||
const p2BreakoutRooms = p2.getBreakoutRooms();
|
||||
@@ -228,259 +282,311 @@ describe('Breakout rooms', () => {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P2 is not seeing p1 in the main room'
|
||||
});
|
||||
// await waitForCondition(
|
||||
// async () => {
|
||||
// if (await p2BreakoutRooms.getRoomsCount() !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p2BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1 && list[1].participantCount === 1
|
||||
// && (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
|
||||
// },
|
||||
// {
|
||||
// timeout: 3000,
|
||||
// interval: 500,
|
||||
// timeoutMsg: 'P2 is not seeing p1 in the main room'
|
||||
// }
|
||||
// );
|
||||
});
|
||||
|
||||
it('close breakout room', async () => {
|
||||
const { p1, p2, p3 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be two non-empty breakout rooms
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P1 is not seeing two breakout rooms'
|
||||
});
|
||||
|
||||
// close the first room
|
||||
await (await p1BreakoutRooms.getRooms())[0].closeRoom();
|
||||
|
||||
// there should be two rooms and first one should be empty
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0 || list[1].participantCount === 0;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing an empty breakout room'
|
||||
});
|
||||
|
||||
// there should be two participants in the main room, either p2 or p3 got moved to the main room
|
||||
const checkParticipants = (p: Participant) =>
|
||||
p.driver.waitUntil(
|
||||
async () => {
|
||||
const isInBreakoutRoom = await p.isInBreakoutRoom();
|
||||
const breakoutRooms = p.getBreakoutRooms();
|
||||
|
||||
if (isInBreakoutRoom) {
|
||||
if (await breakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await breakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list.every(r => { // eslint-disable-line arrow-body-style
|
||||
return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
|
||||
});
|
||||
}
|
||||
|
||||
if (await breakoutRooms.getRoomsCount() !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const list = await breakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount + list[1].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
|
||||
});
|
||||
|
||||
await checkParticipants(p2);
|
||||
await checkParticipants(p3);
|
||||
});
|
||||
|
||||
it('send participants to breakout room', async () => {
|
||||
await hangupAllParticipants();
|
||||
|
||||
// because the participants rejoin so fast, the meeting is not properly ended,
|
||||
// so the previous breakout rooms would still be there.
|
||||
// To avoid this issue we use a different meeting
|
||||
// Respect room name suffix as it is important in multi-shard testing
|
||||
ctx.roomName += `new-${ctx.roomName}`;
|
||||
|
||||
await ensureTwoParticipants();
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be no breakout rooms
|
||||
expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
|
||||
// add one breakout room
|
||||
await p1BreakoutRooms.addBreakoutRoom();
|
||||
|
||||
// there should be one empty room
|
||||
await p1.driver.waitUntil(
|
||||
async () => await p1BreakoutRooms.getRoomsCount() === 1
|
||||
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'No breakout room added for p1'
|
||||
});
|
||||
|
||||
// send the second participant to the first breakout room
|
||||
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
|
||||
|
||||
// there should be one room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
});
|
||||
});
|
||||
|
||||
it('collapse breakout room', async () => {
|
||||
const { p1 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// there should be one breakout room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
});
|
||||
|
||||
// get id of the breakout room participant
|
||||
const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
|
||||
const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
|
||||
async el => {
|
||||
const id = await el.getAttribute('id');
|
||||
|
||||
return id !== '' && id !== null;
|
||||
}) as ChainablePromiseElement;
|
||||
|
||||
const pId = await breakoutRoomItem.getAttribute('id');
|
||||
const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
|
||||
|
||||
expect(await breakoutParticipant.isDisplayed()).toBe(true);
|
||||
|
||||
// collapse the first
|
||||
await (await p1BreakoutRooms.getRooms())[0].collapse();
|
||||
|
||||
// the participant should not be visible
|
||||
expect(await breakoutParticipant.isDisplayed()).toBe(false);
|
||||
|
||||
// the collapsed room should still have one participant
|
||||
expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
|
||||
});
|
||||
|
||||
it('rename breakout room', async () => {
|
||||
const myNewRoomName = `breakout-${crypto.randomUUID()}`;
|
||||
const { p1, p2 } = ctx;
|
||||
const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
|
||||
// let's rename breakout room and see it in local and remote
|
||||
await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].name === myNewRoomName;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room was not renamed for p1'
|
||||
});
|
||||
|
||||
await checkSubject(p2, myNewRoomName);
|
||||
|
||||
const p2BreakoutRooms = p2.getBreakoutRooms();
|
||||
|
||||
// leave room
|
||||
await p2BreakoutRooms.leaveBreakoutRoom();
|
||||
|
||||
// there should be one empty room
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 0;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room not found or not empty for p1'
|
||||
});
|
||||
|
||||
await p2.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p2BreakoutRooms.getRooms();
|
||||
|
||||
return list?.length === 1;
|
||||
}, {
|
||||
timeout: 3000,
|
||||
timeoutMsg: 'The breakout room not seen by p2'
|
||||
});
|
||||
|
||||
expect((await p2BreakoutRooms.getRooms())[0].name).toBe(myNewRoomName);
|
||||
|
||||
// send the second participant to the first breakout room
|
||||
await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
|
||||
|
||||
// there should be one room with one participant
|
||||
await p1.driver.waitUntil(
|
||||
async () => {
|
||||
const list = await p1BreakoutRooms.getRooms();
|
||||
|
||||
if (list?.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return list[0].participantCount === 1;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'The breakout room was not rename for p1'
|
||||
});
|
||||
|
||||
await checkSubject(p2, myNewRoomName);
|
||||
});
|
||||
// it('close breakout room', async () => {
|
||||
// const { p1, p2, p3 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // there should be two non-empty breakout rooms
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// if (await p1BreakoutRooms.getRoomsCount() !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1 && list[1].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'P1 is not seeing two breakout rooms'
|
||||
// });
|
||||
//
|
||||
// // close the first room
|
||||
// await (await p1BreakoutRooms.getRooms())[0].closeRoom();
|
||||
//
|
||||
// // there should be two rooms and first one should be empty
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const c1 = await p1BreakoutRooms.getRoomsCount();
|
||||
//
|
||||
// if (c1 !== 2) {
|
||||
// console.log('c1 for p1:', c1);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// console.log('list len for p1:', list?.length);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const b = list[0].participantCount === 0 || list[1].participantCount === 0;
|
||||
//
|
||||
// console.log('p1:', list[0].participantCount, list[1].participantCount, b);
|
||||
//
|
||||
// return b;
|
||||
// }, {
|
||||
// timeout: 10000,
|
||||
// timeoutMsg: 'P1 is not seeing an empty breakout room'
|
||||
// });
|
||||
//
|
||||
// // there should be two participants in the main room, either p2 or p3 got moved to the main room
|
||||
// const checkParticipants = (p: Participant) =>
|
||||
// p.driver.waitUntil(
|
||||
// async () => {
|
||||
// const isInBreakoutRoom = await p.isInBreakoutRoom();
|
||||
// const breakoutRooms = p.getBreakoutRooms();
|
||||
//
|
||||
// console.log('isInBreakoutRoom:', isInBreakoutRoom, 'p:', p.name);
|
||||
//
|
||||
// if (isInBreakoutRoom) {
|
||||
// const c = await breakoutRooms.getRoomsCount();
|
||||
//
|
||||
// if (c !== 2) {
|
||||
// console.log(`Breakout room count is ${c}`);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await breakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// console.log(`Breakout room count is ${c}`);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list.every(r => { // eslint-disable-line arrow-body-style
|
||||
// console.log(`Breakout room count is ${r.name} p: ${r.participantCount}`);
|
||||
//
|
||||
// return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// const c2 = await breakoutRooms.getRoomsCount();
|
||||
//
|
||||
// if (await breakoutRooms.getRoomsCount() !== 2) {
|
||||
// console.log(`Breakout room count c2 ${c2}`);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// const list = await breakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 2) {
|
||||
// console.log(`Breakout room count len2 ${list?.length}`);
|
||||
//
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// console.log('oooo ', list[0].participantCount + list[1].participantCount);
|
||||
//
|
||||
// return list[0].participantCount + list[1].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
|
||||
// });
|
||||
//
|
||||
// console.log('before check p2');
|
||||
// await checkParticipants(p2);
|
||||
// console.log('after check p2 and before p3');
|
||||
// await checkParticipants(p3);
|
||||
// console.log('before after p3');
|
||||
// });
|
||||
//
|
||||
// it('send participants to breakout room', async () => {
|
||||
// await hangupAllParticipants();
|
||||
//
|
||||
// // because the participants rejoin so fast, the meeting is not properly ended,
|
||||
// // so the previous breakout rooms would still be there.
|
||||
// // To avoid this issue we use a different meeting
|
||||
// // Respect room name suffix as it is important in multi-shard testing
|
||||
// ctx.roomName += `new-${ctx.roomName}`;
|
||||
//
|
||||
// await ensureTwoParticipants();
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // there should be no breakout rooms
|
||||
// expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
|
||||
//
|
||||
// // add one breakout room
|
||||
// await p1BreakoutRooms.addBreakoutRoom();
|
||||
//
|
||||
// // there should be one empty room
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => await p1BreakoutRooms.getRoomsCount() === 1
|
||||
// && (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'No breakout room added for p1'
|
||||
// });
|
||||
//
|
||||
// // send the second participant to the first breakout room
|
||||
// await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
|
||||
//
|
||||
// // there should be one room with one participant
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 5000,
|
||||
// timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// it('collapse breakout room', async () => {
|
||||
// const { p1 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // there should be one breakout room with one participant
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'P1 is not seeing p2 in the breakout room'
|
||||
// });
|
||||
//
|
||||
// // get id of the breakout room participant
|
||||
// const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
|
||||
// const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
|
||||
// async el => {
|
||||
// const id = await el.getAttribute('id');
|
||||
//
|
||||
// return id !== '' && id !== null;
|
||||
// }) as ChainablePromiseElement;
|
||||
//
|
||||
// const pId = await breakoutRoomItem.getAttribute('id');
|
||||
// const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
|
||||
//
|
||||
// expect(await breakoutParticipant.isDisplayed()).toBe(true);
|
||||
//
|
||||
// // collapse the first
|
||||
// await (await p1BreakoutRooms.getRooms())[0].collapse();
|
||||
//
|
||||
// // the participant should not be visible
|
||||
// expect(await breakoutParticipant.isDisplayed()).toBe(false);
|
||||
//
|
||||
// // the collapsed room should still have one participant
|
||||
// expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
|
||||
// });
|
||||
//
|
||||
// it('rename breakout room', async () => {
|
||||
// const myNewRoomName = `breakout-${crypto.randomUUID()}`;
|
||||
// const { p1, p2 } = ctx;
|
||||
// const p1BreakoutRooms = p1.getBreakoutRooms();
|
||||
//
|
||||
// // let's rename breakout room and see it in local and remote
|
||||
// await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
|
||||
//
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].name === myNewRoomName;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'The breakout room was not renamed for p1'
|
||||
// });
|
||||
//
|
||||
// await checkSubject(p2, myNewRoomName);
|
||||
//
|
||||
// const p2BreakoutRooms = p2.getBreakoutRooms();
|
||||
//
|
||||
// // leave room
|
||||
// await p2BreakoutRooms.leaveBreakoutRoom();
|
||||
//
|
||||
// // there should be one empty room
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 0;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'The breakout room not found or not empty for p1'
|
||||
// });
|
||||
//
|
||||
// await p2.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p2BreakoutRooms.getRooms();
|
||||
//
|
||||
// return list?.length === 1;
|
||||
// }, {
|
||||
// timeout: 3000,
|
||||
// timeoutMsg: 'The breakout room not seen by p2'
|
||||
// });
|
||||
//
|
||||
// expect((await p2BreakoutRooms.getRooms())[0].name).toBe(myNewRoomName);
|
||||
//
|
||||
// // send the second participant to the first breakout room
|
||||
// await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
|
||||
//
|
||||
// // there should be one room with one participant
|
||||
// await p1.driver.waitUntil(
|
||||
// async () => {
|
||||
// const list = await p1BreakoutRooms.getRooms();
|
||||
//
|
||||
// if (list?.length !== 1) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return list[0].participantCount === 1;
|
||||
// }, {
|
||||
// timeout: 5000,
|
||||
// timeoutMsg: 'The breakout room was not rename for p1'
|
||||
// });
|
||||
//
|
||||
// await checkSubject(p2, myNewRoomName);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -50,6 +50,7 @@ describe('Dial-in', () => {
|
||||
it('open/close invite dialog', async () => {
|
||||
await ctx.p1.getInviteDialog().open();
|
||||
await ctx.p1.getInviteDialog().clickCloseButton();
|
||||
await ctx.p1.getInviteDialog().waitTillOpen(true);
|
||||
});
|
||||
|
||||
it('dial-in displayed', async function() {
|
||||
|
||||
@@ -53,6 +53,9 @@ if (process.env.VIDEO_CAPTURE_FILE) {
|
||||
chromeArgs.push(`--use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
|
||||
}
|
||||
|
||||
// chromeArgs.push('--enable-logging');
|
||||
// chromeArgs.push('--v=99');
|
||||
|
||||
const chromePreferences = {
|
||||
'intl.accept_languages': 'en-US'
|
||||
};
|
||||
@@ -224,9 +227,9 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
initLogger(bInstance, `${instance}-${cid}-${testName}`, TEST_RESULTS_DIR);
|
||||
|
||||
// setup keepalive
|
||||
keepAlive.push(setInterval(async () => {
|
||||
await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
|
||||
}, 20_000));
|
||||
// keepAlive.push(setInterval(async () => {
|
||||
// await bInstance.execute(() => console.log(`${new Date().toISOString()} keep-alive`));
|
||||
// }, 20_000));
|
||||
|
||||
if (bInstance.isFirefox) {
|
||||
return;
|
||||
@@ -341,6 +344,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${new Date().toISOString()} ---=== Start test ${test.title} ===---`);
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.title} ===---`);
|
||||
});
|
||||
@@ -355,10 +359,21 @@ export const config: WebdriverIO.MultiremoteConfig = {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async afterTest(test, context, { error }) {
|
||||
console.log(`${new Date().toISOString()}---=== End test ${test.title} ===---`);
|
||||
multiremotebrowser.instances.forEach((instance: string) =>
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.title} ===---`));
|
||||
|
||||
if (error) {
|
||||
|
||||
// skip all remaining tests in the suite
|
||||
ctx.skipSuiteTests = `Test "${test.title}" has failed.`;
|
||||
|
||||
// make sure all browsers are at the main app in iframe (if used), so we collect debug info
|
||||
await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
|
||||
// @ts-ignore
|
||||
await ctx[instance].switchToIFrame();
|
||||
}));
|
||||
|
||||
const allProcessing: Promise<any>[] = [];
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
|
||||
@@ -29,6 +29,9 @@ const mergedConfig = merge(defaultConfig, {
|
||||
'specs/media/desktopSharing.spec.ts',
|
||||
'specs/media/lastN.spec.ts',
|
||||
|
||||
// fails randomly for failed downloading asset and page stays in incomplete state
|
||||
'specs/misc/urlNormalisation.spec.ts',
|
||||
|
||||
// when unmuting a participant, we see the presence in debug logs immediately,
|
||||
// but for 15 seconds it is not received/processed by the client
|
||||
// (also the menu disappears after clicking one of the moderation options, does not happen manually)
|
||||
|
||||
Reference in New Issue
Block a user