Compare commits

...

33 Commits

Author SHA1 Message Date
damencho
ff6f0ee949 debug 2025-10-21 14:48:16 -05:00
Hannes
b2f7b3be6c fix(chat): don't show private chat picker if disabled (#16556)
* fix: 🐛 don't show private chat picker if disabled

* style: 🚨

* refactor: ♻️ combine function
2025-10-21 07:57:55 -05:00
Jaya Allamsetty
29fd5df16a fix(tests) Increase backToP2PDelay to 3 secs.
Setting it to 1 sec was causing p2p connections to be created when it was not needed.
2025-10-17 11:27:10 -05:00
damencho
f324122d93 feat(tests): Fail early and gather debug logs for iframe tests. 2025-10-16 16:00:39 -05:00
Jaya Allamsetty
82d4628976 fix(large-video)pin prev speaker on stage when local user is dominant speaker. (#16511)
* fix(large-video)pin prev speaker on stage when local user is dominant speaker.
Also fix the case where the dominant speaker sometimes doesn't show up in the filmstrip when SS is pinned to stage.

* fix(test) Add more tests for activeSpeaker

* fix(test) Do not check for large-video if there is no remote video track.

* fix(filmstrip) Get updated display name for calc speaker order
2025-10-16 15:35:15 -04:00
damencho
8ab02d598c fix(visitors): Fix room token verification.
When allowUnauthenticatedAccess is enabled we want to allow main prosody participants without verifying their token.
2025-10-16 11:22:30 -05:00
damencho
5b23072bd0 feat(tests): Make sure dial-in user hangups.
Avoid closing browser and leaving it to the timeout of the connection.
2025-10-16 11:22:22 -05:00
damencho
7b4cc552fb feat(tests): Increase the randomness of the room name. 2025-10-16 11:22:22 -05:00
damencho
eb188ff02a fix(tests): Wait dialog elements to be clickable.
To avoid trying to click too quickly while animations are still rendered. Avoids: Can't call elementClick on element with selector "//input[@name="lockKey"]" because element wasn't found
2025-10-16 11:22:22 -05:00
damencho
b40c24db70 fix(tests): Visitors tests to avoid reordered webhook events. 2025-10-16 11:22:22 -05:00
Jaya Allamsetty
a855f76377 fix(tests) Wait for p2p switch before checking for SS 2025-10-16 11:20:16 -04:00
Calin-Teodor
c481e7ede4 chore(android/sdk): check also for sdk version that the app targets 2025-10-16 17:30:50 +03:00
Calinteodor
17b4c2156a chore(android): update colors for status bar and navigation bar (#16557)
*Update Android navigation color and add status bar color for both app and sdk.
2025-10-16 14:33:21 +03:00
Calin-Teodor
ca6579e032 feat(prejoin): fix room name backdrop and button sizes 2025-10-16 11:19:26 +03:00
damencho
f3e99624e9 fix(tests): Fix p2p enable flag in desktop sharing. 2025-10-15 15:32:20 -05:00
Saúl Ibarra Corretgé
3b5c2d9b0b fix(android) fix initializer not running
We want to merge our initializer with any others an app migh add.

Fixes: https://github.com/jitsi/jitsi-meet/issues/16505
Fixes: https://github.com/jitsi/jitsi-meet/issues/16527
2025-10-15 16:49:22 +03:00
Calinteodor
bab9ddbb57 feat(prejoin/lobby): style adjustments (#16550)
* UI adjustments, for prejoin and lobby, around device screen orientation.
2025-10-15 16:20:24 +03:00
Calinteodor
8d4193ce1e chore(android): insets navigation adjustments (#16543)
* Adjustments around top and bottom insets, especially for Android >= 15 where edge-to-edge is forced.
2025-10-15 16:12:09 +03:00
damencho
7d2cf3dbf2 fix: Disable urlNormalisation on FF. 2025-10-15 08:10:10 -05:00
bgrozev
9309d61c00 test: Fix jaas chat test again (typo). (#16548) 2025-10-14 17:08:02 -05:00
damencho
4089702060 feat(token_verification): Pass through recorder and transcriber into meetings. 2025-10-14 15:51:15 -05:00
damencho
bf2254c753 feat(keyboard-shortcuts): Adds support for any keyboard layout. 2025-10-14 13:06:45 -05:00
Mihaela Dumitru
641b52c51d feat(visitors): add showJoinMeetingDialog config option (#16540) 2025-10-14 19:36:36 +03:00
bgrozev
083037d152 test: Do not assert message order (they can race). (#16544) 2025-10-14 11:29:21 -05:00
Philip Örnfeldt
a3200a172f lang: Updated swedish translation 2025-10-14 10:44:14 -05:00
bgrozev
ebff46971d test: Add a test for joining a MUC without a conference request. (#16537) 2025-10-14 10:24:06 -05:00
bgrozev
657aefefc2 test: Use timeout for expected codec changes. (#16539)
I suspect some intermittent test failures are caused by not waiting for
the codec change to complete. Might be exacerbated by
ensureThreeParticipants only waiting for 1 remote stream, which means
it the "ensureTwo(); ensureThree()" call may return before p2 sees p3.
2025-10-14 09:20:30 -05:00
Mihaela Dumitru
683d6eb208 feat(visitors): add hideVisitorCountForVisitors config option (#16541) 2025-10-14 17:06:14 +03:00
Hristo Terezov
a62fa3f833 feat(chat): Display file uploads as inline chat messages
Integrates file sharing into the chat interface so uploaded files appear as messages in the chat timeline alongside text
messages.

Changes:
- Created FileMessage component for inline file display in chat
- Extracted FileItem component for reusable file UI across chat and file sharing tab
- Show "A file was deleted" placeholder instead of removing message when file deleted
- Hide message menu (3-dot) when no actions are available for file messages
- Add button backgrounds in chat context to hide text on hover
- Fix timing: local participant only sees file message after upload completes (progress: 100%)

Technical implementation:
- Added fileMetadata field to IMessage interface
- Added isDeleted flag to IFileMetadata for soft-delete state
- Middleware dispatches addMessage when files uploaded (ADD_FILE action)
- Middleware uses editMessage when files deleted to preserve chat history
- Minimal state retention (only isDeleted flag) for deleted files

This provides a unified messaging experience where file sharing is part of the conversation flow.
2025-10-14 08:45:51 -05:00
bgrozev
88f1ef27c5 fix: Fix dial-in test (wait until the dialog is closed). (#16538) 2025-10-13 11:49:08 -05:00
Mihaela Dumitru
95ecf73c71 feat(prejoin): add showHangUp config option to prejoinConfig (#16531) 2025-10-13 10:46:04 +03:00
Calin-Teodor
a96908dd7c fix(analytics/amplitude/native): package does nott have a def export 2025-10-10 14:17:11 +03:00
Saúl Ibarra Corretgé
a103b0e5bd chore(deps) update react-native-webrtc
- Fixes a crash in iOS 26 simulator
- Avoid NPE on Android
2025-10-09 22:54:47 +02:00
60 changed files with 1908 additions and 980 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="navigationBarColor">#040404</color>
<color name="statusBarColor">#040404</color>
</resources>

View File

@@ -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>

View File

@@ -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'],

View File

@@ -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

View File

@@ -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!"

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -1,4 +1,4 @@
import amplitude from '@amplitude/analytics-react-native';
import * as amplitude from '@amplitude/analytics-react-native';
export default amplitude;

View File

@@ -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;

View File

@@ -238,6 +238,8 @@ export default [
'useTurnUdp',
'videoQuality',
'visitors.enableMediaOnPromote',
'visitors.hideVisitorCountForVisitors',
'visitors.showJoinMeetingDialog',
'watchRTCConfigParams.allowBrowserLogCollection',
'watchRTCConfigParams.collectionInterval',
'watchRTCConfigParams.console',

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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');
}

View File

@@ -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);

View File

@@ -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>

View File

@@ -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 }

View File

@@ -27,7 +27,7 @@ const useStyles = makeStyles()(theme => {
flexDirection: 'column',
maxWidth: '100%',
'&.remote': {
'&.remote, &.file': {
maxWidth: 'calc(100% - 40px)' // 100% - avatar and margin
}
},

View 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;

View File

@@ -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>
);

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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;

View 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;

View File

@@ -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>
))
}

View File

@@ -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);

View File

@@ -7,6 +7,7 @@ export interface IFileMetadata {
fileName: string;
fileSize: number;
fileType: string;
isDeleted?: boolean;
progress?: number;
timestamp: number;
}

View File

@@ -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())
];

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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)
};
}

View File

@@ -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,

View File

@@ -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 }>
{

View File

@@ -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>

View File

@@ -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> }>

View File

@@ -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]
}
};

View File

@@ -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;
}
/**

View File

@@ -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));
}

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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();

View File

@@ -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');
});
});
});

View File

@@ -66,6 +66,7 @@ describe('Dial-in', () => {
await p1.getFilmstrip().kickParticipant(endpointId);
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
await p1.hangup();
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});

View File

@@ -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');

View File

@@ -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`
}
);
}

View File

@@ -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);

View File

@@ -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();
});

View File

@@ -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);
// });
});

View File

@@ -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() {

View File

@@ -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) => {

View File

@@ -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)