Compare commits

...

27 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
6d05ac519a TEST - don't merge 2026-03-27 09:17:02 +01:00
damencho
e9daf4395e fix(prosody): Drops not needed config for jigasi-invite module. 2026-03-26 16:35:33 -05:00
damencho
2245e4e747 fix(prosody): Adds some nil checks. 2026-03-26 16:35:33 -05:00
damencho
43132a7eba chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/releases/tag/v2143.0.0+733e66c6
2026-03-26 14:37:40 -05:00
damencho
4f3dc195cf fix(recording): On missing session id, send stop. 2026-03-26 14:37:40 -05:00
damencho
a80a732ec4 fix(webpack-dev-server-proxy): Allow all hosts.
Avoids errors when accessed via a website (the test-lab).
2026-03-26 14:37:40 -05:00
Daniel Nylander
04757f103e lang: Complete Swedish (sv) translation
* l10n: Complete Swedish (sv) translation — add 26 missing strings

Add Swedish translations for 26 missing UI strings:
- File sharing labels (upload, delete notifications)
- Nickname dialog for chat/polls/file sharing features
- Login error messages
- Screen sharing system stop dialog
- Virtual background limit messages
- Toolbar labels (copilot, file sharing, polls)

This brings Swedish translation to 100% coverage (1492/1492 keys).

* l10n(sv): Fix lint + quality improvements

- Add trailing newline (fixes CI Lint check)
- Fix 33 strings: denna/detta/dessa → den här/det här/de här
- Remove 'vänligen' (overly formal)
- Fix English leftover in demoteParticipantDialog
- Fix permission error messages

* l10n(sv): Fix untranslated string (searchResultsTryAgain)

* Fix Swedish translation JSON formatting

- Properly sort and format translation keys per project standards
- Fixes CI lint failure in PR #17206

---------

Co-authored-by: Daniel Nylander <daniel@danielnylander.se>
2026-03-26 09:25:40 -05:00
Vishal Malyan
768be97fa4 feat(ci) optimize Android SDK build by caching Gradle dependencies 2026-03-26 15:16:35 +01:00
Philip Örnfeldt
d67096ee8e lang(sv): Small change of wording
Signed-off-by: Örnfeldt Philip (66140321) <philip.ornfeldt@forsakringskassan.se>
2026-03-26 06:57:05 -05:00
Kevin Caballero
783527ac53 lang: add missing translation in main es 2026-03-26 06:56:22 -05:00
Дамян Минков
fc289dd5ae fix(tests): Report worker crash when session cleanup times out. (#17211)
* fix(tests): Report worker crash when session cleanup times out.

When the WebDriver session DELETE request times out, the worker exits
with code 1 before the JUnit reporter can flush, leaving a zero-byte
XML file that is invisible to the report generator. The onWorkerEnd
hook now detects this and writes a failure entry so the crash shows
up in the test report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* squash: Fix lint errors.

* fix(tests): Use error instead of failure for worker crash XML.

The previous fix incorrectly used <failure> (test assertion failed) for
the zero-byte XML fallback. Since the test may have actually passed and
only the session cleanup timed out, use <error> (infrastructure problem)
with a message clarifying the result is unknown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* squash: Updates message.

* squash: Bumps connection timeout.

* squash: Bumps webdriver.io dependencies.

* squash:Use junitReportBuilder to build the report.

* Also add an allure report when a worker returns non-zero.

* squash: fix dependency version.

* fix(tests): Increase allure report generation timeout and improve error messages.

5 seconds was too tight for CI with a full test suite of allure results.
Also distinguish between timeout and non-zero exit code in the error message
to make failures easier to diagnose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Boris Grozev <boris@jitsi.org>
2026-03-25 16:03:56 -05:00
Stephan Paternotte
e84877e91f lang: Update Dutch translations 2026-03-25 11:50:24 -05:00
Mihaela Dumitru
b24e615ded fix(ui) adjust svg fills and backgrounds (#17062) 2026-03-25 08:54:37 +02:00
Mihaela Dumitru
5ad6332632 feat(external-api): send full user context (#17147) 2026-03-24 17:43:36 +02:00
Calinteodor
8006bd05c6 Revert "feat(deep-linking): use same action for join in app if scheme url is undefined" 2026-03-24 13:15:11 +02:00
mishra
048791c858 fix(react-native-sdk): Export JitsiMeeting component
- Change main entry point from index.tsx to dist/index.js
- Add types field pointing to dist/index.d.ts
- Create tsconfig.json for TypeScript compilation
- Add build script: tsc -p tsconfig.json
- Update prepare hook to auto-compile on npm publish
- Add files array to control npm package contents
- Add .npmignore to exclude build artifacts

Resolves #16443 where JitsiMeeting could not be imported from @jitsi/react-native-sdk
2026-03-24 12:32:33 +02:00
Nishant kumar
ad82e557e0 * fix(invite): use URLSearchParams for decoding dial-in room name 2026-03-23 15:22:04 -05:00
Jaya Allamsetty
15511f86be chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2137.0.0+084a5a9c...v2140.0.0+fe26afb0
2026-03-23 13:49:36 -04:00
Naman Jain
7699cfdac5 feat(translation): added missing hindi translations 2026-03-20 14:31:16 -04:00
Calin-Teodor
16fcf27fb7 feat(deep-linking): use same action for join in app if scheme url is undefined 2026-03-19 15:51:24 +02:00
Jaya Allamsetty
94243c797c feat(tracks) Adds UI notification when SS is killed by macOS.
* feat(tracks) Adds UI notification when SS is killed by macOS.
2026-03-17 16:13:56 -04:00
damencho
6564ba52a2 feat(tests): Adds filesharing tests. 2026-03-13 14:32:07 -05:00
damencho
a77ac20db9 fix(file-sharing): Disable dragging on prejoin.
Updates docs to reflect implementation.
2026-03-13 14:32:07 -05:00
mishraditi
6907db1127 fix(settings) Refreshes audio input levels when microphone permission is regranted. 2026-03-13 11:28:47 -04:00
mishraditi
78931d4f0d fix(settings) Fixes missing previews after device permissions are granted. 2026-03-13 10:25:22 -04:00
Jaya Allamsetty
b19d76fbdf chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2135.0.0+17e2281c...v2137.0.0+084a5a9c
2026-03-12 16:54:14 -04:00
Nishant kumar
0676ca5e62 fix(invite): Use decoded room name for dial-in info page url
* fix(invite): normalize room before building dial-in URL to avoid double encoding

* refactor(invite): use getNormalizedRoomName for room normalization
2026-03-12 13:05:47 -05:00
43 changed files with 2181 additions and 2183 deletions

View File

@@ -135,6 +135,13 @@ jobs:
node -v
npm -v
- run: npm install
- name: Cache Gradle dependencies
uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57
with:
path: /root/.gradle
key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle.properties', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: |
cd android
./gradlew :sdk:clean

View File

@@ -1,4 +1,4 @@
# <p align="center">Jitsi Meet</p>
# <p align="center">Jitsi Meet</p>
Jitsi Meet is a set of Open Source projects which empower users to use and deploy
video conferencing platforms with state-of-the-art video quality and features.

View File

@@ -26,7 +26,7 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />

View File

@@ -883,6 +883,7 @@
"or": "o",
"premeeting": "Pre-reunión",
"proceedAnyway": "Continuar de todos modos",
"recordingWarning": "Otros participantes pueden estar grabando esta llamada",
"screenSharingError": "Error al compartir pantalla:",
"startWithPhone": "Iniciar con audio de llamada telefónica",
"unsafeRoomConsent": "Comprendo los riesgos, quiero unirme a la reunión",

File diff suppressed because it is too large Load Diff

View File

@@ -469,6 +469,8 @@
"screenSharingFailed": "Oeps! Er is iets misgegaan, de schermdeling kon niet worden gestart!",
"screenSharingFailedTitle": "Schermdeling mislukt!",
"screenSharingPermissionDeniedError": "Oeps! Er is iets misgegaan met uw toegangsrechten voor schermdeling. Herlaad en probeer opnieuw.",
"screenshareStoppedDiskSpace": "Dit gebeurt als u de zwevende werkbalk van macOS hebt gebruikt om het delen van schermen te stoppen. Het kan ook te wijten zijn aan een lage schijfruimte.",
"screenshareStoppedTitle": "Scherm delen gestopt via systeem",
"searchInSalesforce": "Zoeken in Salesforce",
"searchResults": "Zoekresultaten({{count}})",
"searchResultsDetailsError": "Er ging iets mis bij het ophalen van eigenaargegevens.",

View File

@@ -114,6 +114,9 @@
"error": "Fel: ditt meddelande skickades inte. Orsak: {{error}}",
"everyone": "Alla",
"fieldPlaceHolder": "Skriv ditt meddelande här",
"fileAccessibleTitle": "{{user}} laddade upp en fil",
"fileAccessibleTitleMe": "jag laddade upp en fil",
"fileDeleted": "En fil raderades",
"guestsChatIndicator": "(gäst)",
"lobbyChatMessageTo": "Skicka meddelande",
"message": "Meddelande",
@@ -123,8 +126,16 @@
"messagebox": "Skriv ett meddelande",
"newMessages": "Nytt meddelande",
"nickname": {
"featureChat": "chatt",
"featureClosedCaptions": "textning",
"featureFileSharing": "fildelning",
"featurePolls": "omröstningar",
"popover": "Välj ett namn",
"title": "Skriv in ett namn för att börja använda chatten",
"titleWith1Features": "Ange ett smeknamn för att använda {{feature1}}",
"titleWith2Features": "Ange ett smeknamn för att använda {{feature1}} och {{feature2}}",
"titleWith3Features": "Ange ett smeknamn för att använda {{feature1}}, {{feature2}} och {{feature3}}",
"titleWith4Features": "Ange ett smeknamn för att använda {{feature1}}, {{feature2}}, {{feature3}} och {{feature4}}",
"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",
@@ -216,6 +227,9 @@
"video_ssrc": "Video SSRC:",
"yes": "Ja"
},
"customPanel": {
"close": "Stäng"
},
"dateUtils": {
"earlier": "Tidigare",
"today": "Idag",
@@ -230,10 +244,10 @@
"downloadMobileApp": "Ladda ner mobilappen",
"ifDoNotHaveApp": "Om du inte har appen än:",
"ifHaveApp": "Om du redan har appen:",
"joinInApp": "Delta i detta möte med din app",
"joinInApp": "Delta i det här mötet med din app",
"joinInAppNew": "Delta i appen",
"joinInBrowser": "Delta på webben",
"launchMeetingLabel": "Hur vill du delta i detta möte?",
"launchMeetingLabel": "Hur vill du delta i det här mötet?",
"launchWebButton": "Starta på webben",
"noDesktopApp": "",
"noMobileApp": "Har du inte appen?",
@@ -280,7 +294,7 @@
"Submit": "Skicka",
"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.",
"WaitForHostNoAuthMsg": "Konferensen har ännu inte startat eftersom ingen värd har anlänt ännu. Var god vänta.",
"WaitingForHostButton": "Vänta på värd",
"WaitingForHostTitle": "Väntar på värden…",
"Yes": "Ja",
@@ -330,7 +344,7 @@
"contactSupport": "Kontakta kundtjänst",
"copied": "Kopierad",
"copy": "Kopiera",
"demoteParticipantDialog": "Are you sure you want to move this participant to viewer? Är du säker på att du vill flytta denna deltagaren till tittare",
"demoteParticipantDialog": "Är du säker på att du vill flytta den här deltagaren till tittare?",
"demoteParticipantTitle": "Flytta till tittare",
"dismiss": "Förkasta",
"displayNameRequired": "Hej, vad heter du?",
@@ -344,18 +358,18 @@
"enterDisplayName": "Ange namn",
"error": "Fel",
"errorRoomCreationRestriction": "Du försökte gå med för snabbt, kom tillbaka om en stund.",
"gracefulShutdown": "Vår tjänst är för tillfället nedstängd för underhåll. Vänligen försök senare.",
"grantModeratorDialog": "Är du säker du vill göra denna deltagare till en moderator?",
"gracefulShutdown": "Vår tjänst är för tillfället nedstängd för underhåll. Försök senare.",
"grantModeratorDialog": "Är du säker på att du vill göra den här deltagaren till moderator?",
"grantModeratorTitle": "Godkänn moderator",
"hide": "Dölj",
"hideShareAudioHelper": "Visa inte denna dialog igen ",
"hideShareAudioHelper": "Visa inte den här dialogen igen ",
"incorrectPassword": "Fel användarnamn eller lösenord",
"incorrectRoomLockPassword": "Felaktigt lösenord",
"internalError": "Ett fel uppstod. Fel: {{error}}",
"internalErrorTitle": "Internt fel",
"kickMessage": "Du kan kontakta {{participantDisplayName}} för mer information.",
"kickParticipantButton": "Ta bort deltagaren från mötet",
"kickParticipantDialog": "Vill du ta bort denna deltagaren från mötet?",
"kickParticipantDialog": "Vill du ta bort den här 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",
@@ -369,6 +383,9 @@
"lockRoom": "Lägg till möte $t(lockRoomPasswordUppercase)",
"lockTitle": "Låsning misslyckades",
"login": "Logga in",
"loginFailed": "Inloggningen misslyckades.",
"loginOnResume": "Din autentiseringssession har gått ut. Du måste logga in igen för att fortsätta mötet.",
"loginPopupBlocked": "Inloggningsfönstret blockerades av din webbläsare.",
"loginQuestion": "Är du säker på att du vill logga in och lämna mötet",
"logoutQuestion": "Är du säker på att du vill logga ut och lämna konferensen?",
"logoutTitle": "Logga ut",
@@ -413,18 +430,18 @@
"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",
"muteParticipantsVideoDialog": "Är du säker du vill inaktivera denna deltagares kamera. Du kommer inte att kunna aktivera den igen. Däremot kan deltagaren kunna aktivera sin egen kamera när som.",
"muteParticipantsVideoDialog": "Är du säker du vill inaktivera den här deltagarens kamera. Du kommer inte att kunna aktivera den igen. Däremot kan deltagaren kunna aktivera sin egen kamera när som.",
"muteParticipantsVideoDialogModerationOn": "Är du säker på att du vill inaktivera den här deltagarens kamera? Du kommer inte att kunna aktivera kameran igen och inte de heller.",
"muteParticipantsVideoTitle": "Inaktivera denna deltagares kamera?",
"muteParticipantsVideoTitle": "Inaktivera den här deltagarens kamera?",
"noDropboxToken": "Ingen giltig dropbox tecken",
"password": "Lösenord",
"passwordLabel": "Mötet har låsts av en deltagare. Ange $t(lockRoomPassword) för att gå med.",
"passwordNotSupported": "Att sätta ett $t(lockRoomPassword) för mötesrummet stöds ej.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) stöds inte",
"passwordRequired": "$t(lockRoomPasswordUppercase) krävs",
"permissionCameraRequiredError": "Tillåtelse krävs för att delta med kamera i denna möte. Var god skaffa detta i \"inställningar\".",
"permissionCameraRequiredError": "Behörighet krävs för att delta med kamera i det här mötet. Ändra detta i \"Inställningar\".",
"permissionErrorTitle": "Tillåtelse krävs",
"permissionMicRequiredError": "Tillåtelse krävs för att delta med mikrofon i denna möte. Var god skaffa detta i \"inställningar\".",
"permissionMicRequiredError": "Behörighet krävs för att delta med mikrofon i det här mötet. Ändra detta i \"Inställningar\".",
"readMore": "Mer",
"recentlyUsedObjects": "Dina senaste använda objekt",
"recording": "Inspelning",
@@ -452,12 +469,14 @@
"screenSharingFailed": "Oops! Något gick fel, skärmdelning kunde ej startas.",
"screenSharingFailedTitle": "Skärmdelning misslyckades!",
"screenSharingPermissionDeniedError": "Något är fel med åtkomstinställningarna för skärmdelningen. Ladda om sidan och försök igen.",
"screenshareStoppedDiskSpace": "Det här händer om du använde macOS flytande verktygsfält för att stoppa skärmdelningen. Det har inte stöd för att starta den igen.",
"screenshareStoppedTitle": "Skärmdelningen stoppades via systemet",
"searchInSalesforce": "Sök i Salesforce",
"searchResults": "Sökresultat ({{count}})",
"searchResultsDetailsError": "Något gick fel när ägardata hämtades.",
"searchResultsError": "Något gick fel när data hämtades.",
"searchResultsNotFound": "Inga sökresultat hittades.",
"searchResultsTryAgain": "Try using alternative keywords.",
"searchResultsTryAgain": "Försök med andra sökord.",
"sendPrivateMessage": "Du har fått ett privat meddelande. Tänkte du svara på det privat, eller vill du skicka ditt meddelande till alla deltagare?",
"sendPrivateMessageCancel": "Skicka till alla deltagare",
"sendPrivateMessageOk": "Skicka privat",
@@ -520,7 +539,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.",
"unauthenticatedAccessDisabled": "Det här 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",
@@ -543,7 +562,7 @@
"title": "Delade dokument"
},
"e2ee": {
"labelToolTip": "Ljud- och videokommunikation för detta samtal är krypterad från dator till dator"
"labelToolTip": "Ljud- och videokommunikation för det här samtalet är krypterad från dator till dator"
},
"embedMeeting": {
"title": "Bädda in möte"
@@ -576,6 +595,7 @@
"newFileNotification": "{{ participantName }} delade '{{ fileName }}'",
"removeFile": "Ta bort",
"removeFileSuccess": "Filen togs bort",
"uploadDisabled": "Inte tillåtet att ladda upp filer. Be en moderator om behörighet för den åtgärden.",
"uploadFailedDescription": "Snälla försök igen.",
"uploadFailedTitle": "Överföring misslyckades",
"uploadFile": "Dela fil"
@@ -603,7 +623,7 @@
"conferenceURL": "Länk:",
"copyNumber": "Kopiera nummer",
"country": "Land",
"dialANumber": "Om du vill gå med i mötet ringer du något av dessa nummer och fyller sedan i PIN-koden.",
"dialANumber": "Om du vill gå med i mötet ringer du något av de här numren och fyller sedan i PIN-koden.",
"dialInConferenceID": "PIN-kod:",
"dialInNotSupported": "Tyvärr stöds inte inringning just nu.",
"dialInNumber": "Inringning:",
@@ -635,14 +655,14 @@
"sipAudioOnly": "SIP endast ljud address",
"title": "Dela",
"tooltip": "Dela länk och information om inringning för mötet",
"upgradeOptions": "Vänligen kontrollera om uppgraderingsalternativen är på",
"upgradeOptions": "Kontrollera om uppgraderingsalternativen är på",
"whiteboardError": "Problem att ladda whiteboard, var god försök senare."
},
"inlineDialogFailure": {
"msg": "Vi slirade lite.",
"retry": "Försök igen",
"support": "Support",
"supportMsg": "Om detta fortsätter hända kontakta"
"supportMsg": "Om det här fortsätter att hända, kontakta"
},
"inviteDialog": {
"alertText": "Det gick inte att bjuda in alla deltagare.",
@@ -709,13 +729,14 @@
"streamIdHelp": "Vad är det här?",
"title": "Direktsändning",
"unavailableTitle": "Livesändning otillgänglig",
"youTubeGoLiveWarning": "Kom ihåg att klicka på \"Gå live\" i YouTube Studio om autostart/autostopp är inaktiverade.",
"youtubeTerms": "Tjänstevillkor för YouTube"
},
"lobby": {
"backToKnockModeButton": "Tillbaka till väntrum",
"chat": "Chatt",
"dialogTitle": "Väntrum",
"disableDialogContent": "Väntrumsläge är för närvarande aktiverat. Denna funktion säkerställer att oönskade deltagare inte kan gå med i ditt möte. Vill du inaktivera det?",
"disableDialogContent": "Väntrumsläge är för närvarande aktiverat. Den här funktionen säkerställer att oönskade deltagare inte kan gå med i ditt möte. Vill du inaktivera det?",
"disableDialogSubmit": "Inaktivera",
"emailField": "Skriv in din mailadress",
"enableDialogPasswordField": "Ange lösenord (valfritt)",
@@ -809,7 +830,7 @@
"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.",
"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.",
"disabledIframeSecondaryWeb": "Bädda in {{domain}} är bara till för demo, så det här samtalet 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",
@@ -817,7 +838,7 @@
"focusFail": "{{component}} inte tillgänglig försöker igen om {{ms}} sek",
"gifsMenu": "GIPHY",
"groupTitle": "Notifieringar",
"hostAskedUnmute": "Värden vill att du ska stänga av ljudet",
"hostAskedUnmute": "Värden vill att du ska starta din mikrofon",
"invalidTenant": "Ogiltig tenant",
"invalidTenantHyphenDescription": "Tenant du använder har ogiltiga tecken (startar eller slutar med '-').",
"invalidTenantLengthDescription": "Tenant du använder är för lång",
@@ -832,7 +853,7 @@
"linkToSalesforce": "Länk till Salesforce",
"linkToSalesforceDescription": "Du kan länka mötessammanfattningen till ett Salesforce-objekt.",
"linkToSalesforceError": "Det gick inte att länka mötet till Salesforce",
"linkToSalesforceKey": "Länka detta möte",
"linkToSalesforceKey": "Länka det här mötet",
"linkToSalesforceProgress": "Länkar möte till Salesforce…",
"linkToSalesforceSuccess": "Mötet länkades till Salesforce",
"localRecordingStarted": "{{name}} har påbörjat en lokal inspelning.",
@@ -858,7 +879,7 @@
"newDeviceAudioTitle": "Ny ljudenhet hittad",
"newDeviceCameraTitle": "Ny kamera hittad",
"nextToSpeak": "Du är näst i kö för att prata",
"noiseSuppressionDesktopAudioDescription": "Brusreducering kan inte aktiveras när du delar skrivbordsljud, vänligen inaktivera det och försök igen.",
"noiseSuppressionDesktopAudioDescription": "Brusreducering kan inte aktiveras när du delar skrivbordsljud, inaktivera det och försök igen.",
"noiseSuppressionFailedTitle": "Det gick inte att starta brusreducering",
"noiseSuppressionStereoDescription": "Brusreducering i stereoljud stöds för närvarande inte.",
"oldElectronClientDescription1": "Den version av Jitsi meet som används är gammal och har säkerhetsluckor. Var god uppdatera till den senaste versionen.",
@@ -886,7 +907,7 @@
"suboptimalExperienceTitle": "Webbläsarvarning",
"suggestRecordingAction": "Starta",
"suggestRecordingDescription": "Vill du starta en inspelning?",
"suggestRecordingTitle": "Spela in detta mötet",
"suggestRecordingTitle": "Spela in det här mötett",
"unmute": "Slå på mikrofonen",
"unmuteScreen": "Starta skärmdelning",
"unmuteVideo": "Starta kamera",
@@ -981,7 +1002,7 @@
},
"notification": {
"description": "Öppna fliken omröstningar för att rösta",
"title": "En ny omröstning har blivit tillagd till detta möte"
"title": "En ny omröstning har blivit tillagd till det här mötet"
},
"results": {
"changeVote": "Ändra din röst",
@@ -1014,7 +1035,7 @@
"audioHighQuality": "Vi förväntar oss att ditt ljud har utmärkt kvalitet.",
"audioLowNoVideo": "Vi förväntar oss att din ljudkvalitet är låg och ingen video.",
"goodQuality": "Grymt bra! Din mediekvalitet kommer att bli bra.",
"noMediaConnectivity": "Vi kunde inte hitta ett sätt att upprätta mediaanslutning för detta test. Detta orsakas vanligtvis av en brandvägg eller NAT.",
"noMediaConnectivity": "Vi kunde inte upprätta mediaanslutning för det här testet. Det orsakas vanligtvis av en brandvägg eller NAT.",
"noVideo": "Vi förväntar oss att din video kommer ha låg kvalitet eller inte fungera.",
"testFailed": "Anslutningstestet stötte på oväntade problem, men det behöver inte påverka din upplevelse.",
"undetectable": "Om du fortfarande inte kan ringa i webbläsaren rekommenderar vi att du ser till att dina högtalare, mikrofon och kamera är korrekt inställda, att du har beviljat din webbläsare rättigheter att använda din mikrofon och kamera och att din webbläsarversion är uppdaterad.",
@@ -1028,7 +1049,7 @@
"dialInMeeting": "Ring in till mötet",
"dialInPin": "Ring in till mötet och ange PIN-kod:",
"dialing": "Ringer",
"doNotShow": "Visa inte denna ruta igen",
"doNotShow": "Visa inte den här rutan igen",
"errorDialOut": "Kunde inte ringa ut",
"errorDialOutDisconnected": "Kunde inte ringa ut. Kopplar ner",
"errorDialOutFailed": "Kunde inte ringa ut. Samtal misslyckades",
@@ -1082,7 +1103,7 @@
"raisedHandsLabel": "Antal uppräckta händer",
"record": {
"already": {
"linked": "Mötet är redan länkat till detta Salesforce-objekt."
"linked": "Mötet är redan länkat till det här Salesforce-objekt."
},
"type": {
"account": "Konto",
@@ -1121,7 +1142,7 @@
"localRecordingVideoWarning": "För att spela in din video måste du ha den på när du startar inspelningen",
"localRecordingWarning": "Se till att du väljer den aktuella fliken för att kunna använda rätt video och ljud.",
"loggedIn": "Inloggad som {{userName}}",
"noMicPermission": "Mikrofonspåret kunde inte skapas. Vänligen ge tillstånd att använda mikrofonen.",
"noMicPermission": "Mikrofonspåret kunde inte skapas. Ge tillstånd att använda mikrofonen.",
"noStreams": "Ingen ljud- eller videoström upptäcktes.",
"off": "Inspelningen avslutades",
"offBy": "{{name}} avslutade inspelningen",
@@ -1246,7 +1267,7 @@
"version": "Version"
},
"share": {
"dialInfoText": "\n\n=====\n\nVill du istället ringa in via telefon?\n\n{{defaultDialInNumber}} Klicka på den här länken för att se telefonnumret för detta möte\n{{dialInfoPageUrl}}",
"dialInfoText": "\n\n=====\n\nVill du istället ringa in via telefon?\n\n{{defaultDialInNumber}} Klicka på den här länken för att se telefonnumret för det här mötet\n{{dialInfoPageUrl}}",
"mainText": "Klicka på länken för att delta i mötet:\n{{roomUrl}}"
},
"speaker": "Talare",
@@ -1298,6 +1319,7 @@
"chat": "Öppna eller stäng chattfönster",
"clap": "Applådera",
"closeChat": "Stäng chatten",
"closeCustomPanel": "Stäng",
"closeMoreActions": "Stäng menyn för fler åtgärder",
"closeParticipantsPane": "Stäng deltagarfönstret",
"closedCaptions": "Undertexter",
@@ -1403,9 +1425,11 @@
"chat": "Öppna / stäng chatten",
"clap": "Klappa",
"closeChat": "Stäng chatt",
"closeCustomPanel": "Stäng",
"closeParticipantsPane": "Stäng deltagarrutan",
"closeReactionsMenu": "Stäng meny för reaktioner",
"closedCaptions": "Undertexter",
"copilot": "Copilot",
"disableNoiseSuppression": "Inaktivera brusreducering",
"disableReactionSounds": "Du kan inaktivera reaktionsljud för det här mötet",
"documentClose": "Stäng delat dokument",
@@ -1420,6 +1444,7 @@
"exitFullScreen": "Stäng fullskärm",
"exitTileView": "Stäng panelvy",
"feedback": "Lämna återkoppling",
"fileSharing": "Fildelning",
"giphy": "Växla GIPHY-menyn",
"hangup": "Lämna",
"help": "Hjälp",
@@ -1455,6 +1480,7 @@
"openReactionsMenu": "Öppna meny för reaktioner",
"participants": "Deltagare",
"pip": "Öppna bild-i-bild-läge",
"polls": "Omröstningar",
"privateMessage": "Skicka privat meddelande",
"profile": "Redigera din profil",
"raiseHand": "Räck upp / ta ner din hand",
@@ -1581,6 +1607,7 @@
"addBackground": "Lägg till bakgrund",
"apply": "Tillämpa",
"backgroundEffectError": "Det gick inte att tillämpa bakgrundseffekt.",
"backgroundLimitReached": "Gränsen för anpassade bakgrunder har nåtts",
"blur": "Oskärpa",
"deleteImage": "Ta bort bild",
"desktopShare": "Dela skrivbord",
@@ -1593,7 +1620,8 @@
"image6": "Skog",
"image7": "Soluppgång",
"none": "Ingen",
"pleaseWait": "Vänligen vänta…",
"oldestBackgroundRemoved": "Den äldsta anpassade bakgrunden har tagits bort för att lägga till den nya.",
"pleaseWait": "Var god vänta…",
"removeBackground": "Ta bort bakgrunden",
"slightBlur": "Lätt oskärpa",
"title": "Virtuella bakgrunder",
@@ -1665,7 +1693,7 @@
"recentListEmpty": "Inga tidigare möten. Chatta med ditt team och hitta alla tidigare möten där.",
"recentMeetings": "Dina senaste möten",
"reducedUIText": "Välkommen till {{app}}!",
"roomNameAllowedChars": "Mötesnamn kan inte innehålla dessa tecken: ?, &,:, ', \",%, #.",
"roomNameAllowedChars": "Mötesnamn kan inte innehålla de här tecknen: ?, &,:, ', \",%, #.",
"roomname": "Skriv in rumsnamn",
"roomnameHint": "Ange namnet eller URL:en till mötesrummet du vill ansluta till. Du kan hitta på ett nytt namn, berätta då för de andra du tänker möta så de anger samma namn.",
"sendFeedback": "Ge återkoppling",

View File

@@ -469,6 +469,8 @@
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
"screenSharingFailedTitle": "Screen sharing failed!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing permissions. Please reload and try again.",
"screenshareStoppedDiskSpace": "This happens if you used the macOS's floating toolbar to stop screen sharing. It may also be due to low disk space.",
"screenshareStoppedTitle": "Screen sharing stopped via system",
"searchInSalesforce": "Search in Salesforce",
"searchResults": "Search results({{count}})",
"searchResultsDetailsError": "Something went wrong while retrieving owner data.",

View File

@@ -843,12 +843,8 @@ function initCommands() {
const activeSession = getActiveSession(state, mode);
if (activeSession && activeSession.id) {
APP.store.dispatch(toggleScreenshotCaptureSummary(false));
conference.stopRecording(activeSession.id);
} else {
logger.error('No recording or streaming session found');
}
APP.store.dispatch(toggleScreenshotCaptureSummary(false));
conference.stopRecording(activeSession?.id);
},
'initiate-private-chat': participantId => {
const state = APP.store.getState();

2752
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -72,7 +72,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2135.0.0+17e2281c/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2143.0.0+733e66c6/lib-jitsi-meet.tgz",
"lodash-es": "4.17.23",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -164,12 +164,12 @@
"@types/w3c-image-capture": "1.0.6",
"@types/w3c-web-hid": "1.0.3",
"@types/zxcvbn": "4.4.1",
"@wdio/allure-reporter": "9.23.2",
"@wdio/cli": "9.23.2",
"@wdio/globals": "9.23.0",
"@wdio/junit-reporter": "9.23.2",
"@wdio/local-runner": "9.23.2",
"@wdio/mocha-framework": "9.23.2",
"@wdio/allure-reporter": "9.27.0",
"@wdio/cli": "9.27.0",
"@wdio/globals": "9.27.0",
"@wdio/junit-reporter": "9.27.0",
"@wdio/local-runner": "9.27.0",
"@wdio/mocha-framework": "9.27.0",
"babel-loader": "9.1.0",
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
@@ -184,6 +184,7 @@
"eslint-plugin-typescript-sort-keys": "3.3.0",
"jetifier": "1.6.4",
"jsonwebtoken": "9.0.2",
"junit-report-builder": "5.1.1",
"patch-package": "6.4.7",
"pretty": "2.0.0",
"process": "0.11.10",
@@ -193,7 +194,7 @@
"ts-loader": "9.4.2",
"typescript": "5.7.2",
"unorm": "1.6.0",
"webdriverio": "9.22.0",
"webdriverio": "9.27.0",
"webpack": "5.105.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "5.1.4",

View File

@@ -1 +1,6 @@
*.tgz
tsconfig.json
.npmrc
.git
.gitignore
node_modules

View File

@@ -2,7 +2,8 @@
"name": "@jitsi/react-native-sdk",
"version": "0.0.0",
"description": "React Native SDK for Jitsi Meet.",
"main": "index.tsx",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "Apache-2.0",
"author": "",
"homepage": "https://jitsi.org",
@@ -92,9 +93,27 @@
"@babel/plugin-proposal-optional-chaining": "0.0.0"
},
"scripts": {
"build": "tsc -p tsconfig.json",
"postinstall": "node sdk_instructions.js",
"prepare": "node prepare_sdk.js"
"prepare": "node prepare_sdk.js && npm run build"
},
"files": [
"dist",
"android",
"ios",
"index.tsx",
"jitsi-meet-rnsdk.podspec",
"prepare_sdk.js",
"sdk_instructions.js",
"update_dependencies.js",
"update_sdk_dependencies.js",
"README.md",
"images",
"sounds",
"lang",
"modules",
"react"
],
"bugs": {
"url": "https://github.com/jitsi/jitsi-meet/issues"
},

View File

@@ -0,0 +1,17 @@
{
"extends": "../tsconfig.native.json",
"compilerOptions": {
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"emitDeclarationOnly": false
},
"include": [
"index.tsx"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -95,12 +95,6 @@ export function commonUserJoinedHandling(
const isPromoted = conference?.getMetadataHandler().getMetadata()?.visitors?.promoted?.[id];
const userIdentity = user.getIdentity()?.user;
// Map identity from JWT context to userContext for external API
const userContext = userIdentity ? {
id: userIdentity.id,
name: userIdentity.name
} : undefined;
// the identity and avatar come from jwt and never change in the presence
dispatch(participantJoined({
avatarURL: userIdentity?.avatar,
@@ -113,7 +107,7 @@ export function commonUserJoinedHandling(
isPromoted,
isReplacing,
sources: user.getSources(),
userContext
userContext: userIdentity
}));
}
}

View File

@@ -619,17 +619,12 @@ function _localParticipantJoined({ getState, dispatch }: IStore, next: Function,
const settings = state['features/base/settings'];
const jwtUser = state['features/base/jwt']?.user;
const userContext = jwtUser ? {
id: jwtUser.id,
name: jwtUser.name
} : undefined;
dispatch(localParticipantJoined({
avatarURL: settings.avatarURL,
email: settings.email,
name: settings.displayName,
id: '',
userContext
userContext: jwtUser
}));
return result;

View File

@@ -45,6 +45,7 @@ export interface IParticipant {
}
export interface IUserContext {
[key: string]: any;
id?: string;
name?: string;
}

View File

@@ -1,10 +1,11 @@
import { createTrackMutedEvent } from '../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../analytics/functions';
import { IStore } from '../../app/types';
import { showErrorNotification, showNotification } from '../../notifications/actions';
import { showErrorNotification, showNotification, showWarningNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT, NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { getCurrentConference } from '../conference/functions';
import { IJitsiConference } from '../conference/reducer';
import { isMacOS } from '../environment/environment';
import { JitsiTrackErrors, JitsiTrackEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setScreenshareMuted, setVideoMuted } from '../media/actions';
import {
@@ -439,6 +440,12 @@ export function trackAdded(track: any) {
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
() => {
logger.debug(`Local track stopped: ${track}, removing it from the conference`);
if (mediaType === MEDIA_TYPE.SCREENSHARE && isMacOS()) {
dispatch(showWarningNotification({
descriptionKey: 'dialog.screenshareStoppedDiskSpace',
titleKey: 'dialog.screenshareStoppedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
dispatch({
type: TRACK_STOPPED,
track: {

View File

@@ -214,7 +214,7 @@ export const colorMap = {
// Visitors
visitorsCountBadge: 'warning02', // Visitors count badge background
visitorsCountText: 'uiBackground', // Visitors count badge text
visitorsCountIcon: 'icon04', // Visitors count icon
visitorsCountIcon: 'iconSvgFill', // Visitors count icon
visitorsQueueBackground: 'ui01', // Visitors queue panel background
visitorsQueueText: 'text01', // Visitors queue text
visitorsArrowBackground: 'ui02', // Visitors arrow container background
@@ -355,7 +355,7 @@ export const colorMap = {
dialInSecondaryText: 'text02', // Dial-in summary secondary text
// Reactions
reactionsMenuBackground: 'ui01', // Reactions menu background
reactionsMenuBackground: '#242528', // Reactions menu background
reactionsMenuBorder: 'ui02', // Reactions menu border
reactionsMenuButtonToggled: 'surface01', // Reactions menu button toggled state background
reactionsMenuBoxShadow1: 'ui09', // Reactions menu box shadow primary
@@ -411,7 +411,7 @@ export const colorMap = {
fileSharingItemBorder: 'ui02', // File sharing item hover/border
// Gifs
gifsBackground: 'ui01', // GIFs panel background
gifsBackground: '#242528', // GIFs panel background
gifsText: 'text01', // GIFs panel text
// Whiteboard

View File

@@ -19,6 +19,7 @@ export interface IBreakoutRoomsState {
rooms: IRooms;
userContextCache: {
[participantId: string]: {
[key: string]: any;
id?: string;
name?: string;
};

View File

@@ -9,6 +9,7 @@ export interface IRoom {
jid: string;
role: string;
userContext?: {
[key: string]: any;
id?: string;
name?: string;
};
@@ -38,6 +39,7 @@ export interface IRoomInfoParticipant {
jid: string;
role: string;
userContext?: {
[key: string]: any;
id?: string;
name?: string;
};

View File

@@ -483,6 +483,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
const { isOpen: isChatOpen } = useSelector((state: IReduxState) => state['features/chat']);
const isFileUploadEnabled = useSelector(isFileUploadingEnabled);
const isOnPrejoin = useSelector(isPrejoinPageVisible);
const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -500,7 +501,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
e.preventDefault();
e.stopPropagation();
if (!isFileUploadEnabled) {
if (!isFileUploadEnabled || isOnPrejoin) {
return;
}
@@ -510,7 +511,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
}
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
}
}, [ isChatOpen, isDragging, isFileUploadEnabled ]);
}, [ isChatOpen, isDragging, isFileUploadEnabled, isOnPrejoin ]);
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault();
@@ -528,6 +529,7 @@ export default reactReduxConnect(_mapStateToProps)(translate(props => {
return (
<div
data-testid = 'conference-drag-zone'
onDragEnter = { handleDragEnter }
onDragLeave = { handleDragLeave }
onDragOver = { handleDragOver }

View File

@@ -235,6 +235,13 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
!== this.props.selectedAudioInputId) {
this._createAudioInputTrack(this.props.selectedAudioInputId);
}
if (!prevProps.hasAudioPermission && this.props.hasAudioPermission) {
this._createAudioInputTrack(this.props.selectedAudioInputId)
?.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
}
/**

View File

@@ -194,6 +194,13 @@ class VideoDeviceSelection extends AbstractDialogTab<IProps, IState> {
!== this.props.selectedVideoInputId) {
this._createVideoInputTrack(this.props.selectedVideoInputId);
}
if (!prevProps.hasVideoPermission && this.props.hasVideoPermission) {
this._createVideoInputTrack(this.props.selectedVideoInputId)
?.then(() => {
this.props.dispatch(getAvailableDevices());
});
}
}
/**

View File

@@ -32,6 +32,7 @@ const useStyles = makeStyles()(theme => {
marginBottom: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
backgroundColor: theme.palette.gifsBackground,
'& div:focus': {
border: '1px solid red !important',
@@ -54,7 +55,7 @@ const useStyles = makeStyles()(theme => {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
color: theme.palette.gifsText,
marginTop: theme.spacing(1)
},
@@ -62,7 +63,8 @@ const useStyles = makeStyles()(theme => {
padding: theme.spacing(3),
width: '100%',
boxSizing: 'border-box',
height: '100%'
height: '100%',
backgroundColor: theme.palette.gifsBackground
},
overflowMenu: {

View File

@@ -4,7 +4,6 @@ import BaseApp from '../../../../base/app/components/BaseApp';
import { isMobileBrowser } from '../../../../base/environment/utils';
import GlobalStyles from '../../../../base/ui/components/GlobalStyles.web';
import JitsiThemeProvider from '../../../../base/ui/components/JitsiThemeProvider.web';
import { parseURLParams } from '../../../../base/util/parseURLParams';
import { DIAL_IN_INFO_PAGE_PATH_NAME } from '../../../constants';
import NoRoomError from '../../dial-in-info-page/NoRoomError.web';
@@ -25,7 +24,8 @@ export default class DialInSummaryApp extends BaseApp<any> {
await super.componentDidMount();
// @ts-ignore
const { room } = parseURLParams(window.location, true, 'search');
const params = new URLSearchParams(window.location.search);
const room = params.get('room') || '';
const { href } = window.location;
const ix = href.indexOf(DIAL_IN_INFO_PAGE_PATH_NAME);
const url = (ix > 0 ? href.substring(0, ix) : href) + room;
@@ -36,7 +36,7 @@ export default class DialInSummaryApp extends BaseApp<any> {
? <DialInSummary
className = 'dial-in-page'
clickableNumbers = { isMobileBrowser() }
room = { decodeURIComponent(room) }
room = { room }
scrollable = { true }
showTitle = { true }
url = { url } />

View File

@@ -17,6 +17,7 @@ import { parseURLParams } from '../base/util/parseURLParams';
import {
StatusCode,
appendURLParam,
getNormalizedRoomName,
parseURIString
} from '../base/util/uri';
import { isVpaasMeeting } from '../jaas/functions';
@@ -729,7 +730,7 @@ export function getDialInfoPageURL(state: IReduxState, roomName?: string) {
const conferenceName = roomName ?? getRoomName(state);
const { locationURL } = state['features/base/connection'];
const { href = '' } = locationURL ?? {};
const room = _decodeRoomURI(conferenceName ?? '');
const room = getNormalizedRoomName(conferenceName) ?? '';
const url = didPageUrl || `${href.substring(0, href.lastIndexOf('/'))}/${DIAL_IN_INFO_PAGE_PATH_NAME}`;

View File

@@ -614,13 +614,7 @@ function _registerForNativeEvents(store: IStore) {
const activeSession = getActiveSession(state, mode);
if (!activeSession?.id) {
logger.error('No recording or streaming session found');
return;
}
conference.stopRecording(activeSession.id);
conference.stopRecording(activeSession?.id);
});
eventEmitter.addListener(ExternalAPI.OVERWRITE_CONFIG, ({ config }: any) => {

View File

@@ -57,9 +57,7 @@ export default class AbstractStopLiveStreamDialog extends Component<IProps> {
const { _session } = this.props;
if (_session) {
this.props._conference?.stopRecording(_session.id);
}
this.props._conference?.stopRecording(_session?.id);
return true;
}

View File

@@ -102,8 +102,8 @@ export default class AbstractStopRecordingDialog<P extends IProps>
if (localRecordingVideoStop) {
dispatch(setVideoMuted(true));
}
} else if (_fileRecordingSession) {
_conference?.stopRecording(_fileRecordingSession.id);
} else {
_conference?.stopRecording(_fileRecordingSession?.id);
this._toggleScreenshotCapture();
}

View File

@@ -87,30 +87,6 @@ paths:
$ref: '#/components/schemas/AddDocumentResponse'
example:
fileId: e393a7e5-e790-4f43-836e-d27238201904
delete:
tags:
- Document sharing history
summary: Deletes documents for a given session, user and customer
operationId: deleteDocumentsForSessionCustomerAndUser
parameters:
- name: sessionId
in: path
required: true
schema:
type: string
- name: user-id
in: query
required: true
schema:
type: string
- name: customer-id
in: query
required: true
schema:
type: string
responses:
'204':
description: No Content
/v1/documents/sessions/{sessionId}/files/{fileId}:
get:
tags:
@@ -141,7 +117,7 @@ paths:
tags:
- Document sharing history
summary: Delete a file by sessionId and fileId
description: Delete a file by sessionId and fileId allowed by all moderators
description: Delete a file by sessionId and fileId allowed by the "file-upload" feature
operationId: deleteFile
parameters:
- name: sessionId

View File

@@ -2,7 +2,7 @@
-- jwt is used to validate access
-- Copyright (C) 2023-present 8x8, Inc.
local jid_split = require "util.jid".split;
local jid = require "util.jid";
local hashes = require "util.hashes";
local random = require "util.random";
local st = require("util.stanza");
@@ -18,7 +18,6 @@ local muc_domain = module:get_option_string("muc_internal_domain_base", 'interna
local jigasi_brewery_room_jid = module:get_option_string("muc_jigasi_brewery_jid", 'jigasibrewery@' .. muc_domain);
local jigasi_bare_jid = module:get_option_string("muc_jigasi_jid", "jigasi@auth." .. muc_domain_base);
local focus_jid = module:get_option_string("muc_jicofo_brewery_jid", jigasi_brewery_room_jid .. "/focus");
local main_muc_service;
@@ -43,9 +42,9 @@ local function invite_jigasi(conference, phone_no)
--select least stressed Jigasi
local least_stressed_value = math.huge;
local least_stressed_jigasi_jid;
local least_stressed_jigasi_occupant;
for occupant_jid, occupant in jigasi_brewery_room:each_occupant() do
local _, _, resource = jid_split(occupant_jid);
local _, _, resource = jid.split(occupant_jid);
if resource ~= 'focus' then
local occ = occupant:get_presence();
local stats_child = occ:get_child("stats", "http://jitsi.org/protocol/colibri")
@@ -63,7 +62,7 @@ local function invite_jigasi(conference, phone_no)
local stress_level = tonumber(stats_tag.attr.value);
module:log("debug", "Stressed level %s %s ", stress_level, occupant_jid)
if stress_level < least_stressed_value then
least_stressed_jigasi_jid = occupant_jid
least_stressed_jigasi_occupant = occupant;
least_stressed_value = stress_level
end
end
@@ -71,18 +70,15 @@ local function invite_jigasi(conference, phone_no)
end
end
end
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_jid, least_stressed_value)
if not least_stressed_jigasi_jid then
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_occupant.jid, least_stressed_value)
if not least_stressed_jigasi_occupant then
module:log("error", "Cannot invite jigasi from room %s", jigasi_brewery_room.jid)
return 404, 'Jigasi not found'
end
-- invite Jigasi to join the conference
local _, _, jigasi_res = jid_split(least_stressed_jigasi_jid)
local jigasi_full_jid = jigasi_bare_jid .. "/" .. jigasi_res;
local stanza_id = hashes.sha256(random.bytes(8), true);
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = jigasi_full_jid, from = focus_jid, id = stanza_id })
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = least_stressed_jigasi_occupant.jid, from = focus_jid, id = stanza_id })
:tag("dial", { xmlns = "urn:xmpp:rayo:1", from = "fromnumber", to = phone_no })
:tag("header", { xmlns = "urn:xmpp:rayo:1", name = "JvbRoomName", value = conference })

View File

@@ -141,6 +141,11 @@ end
local main_room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
local occupant_jid = stanza.attr.from;
if not main_room then
module:log('warn', 'No main room found for %s %s', session.jitsi_web_query_room, session.jitsi_web_query_prefix);
return;
end
occupant = main_room:get_occupant_by_real_jid(occupant_jid);
if main_room._data.breakout_rooms_active and not occupant then
@@ -148,10 +153,13 @@ end
-- not in main room, let's check breakout rooms
for breakout_room_jid, subject in pairs(main_room._data.breakout_rooms or {}) do
local breakout_room = get_room_from_jid(breakout_room_jid);
occupant = breakout_room:get_occupant_by_real_jid(occupant_jid);
if occupant then
room = breakout_room;
break;
if breakout_room then
occupant = breakout_room:get_occupant_by_real_jid(occupant_jid);
if occupant then
room = breakout_room;
break;
end
end
end
else

View File

@@ -279,7 +279,9 @@ function Util:process_and_verify_token(session)
-- We're fetching an public key from an ASAP server
local dotFirst = session.auth_token:find("%.");
if not dotFirst then return false, "not-allowed", "Invalid token" end
local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
local headerPartEncoded = basexx.from_url64(session.auth_token:sub(1,dotFirst-1));
if not headerPartEncoded then return false, "not-allowed", "Invalid token" end
local header, err = json_safe.decode(headerPartEncoded);
if err then
return false, "not-allowed", "bad token format";
end

View File

@@ -8,6 +8,7 @@ import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import BreakoutRooms from '../pageobjects/BreakoutRooms';
import ChatPanel from '../pageobjects/ChatPanel';
import FileSharingPanel from '../pageobjects/FileSharingPanel';
import Filmstrip from '../pageobjects/Filmstrip';
import IframeAPI from '../pageobjects/IframeAPI';
import InviteDialog from '../pageobjects/InviteDialog';
@@ -534,6 +535,13 @@ export class Participant {
return new ChatPanel(this);
}
/**
* Returns the file sharing panel for this participant.
*/
getFileSharingPanel(): FileSharingPanel {
return new FileSharingPanel(this);
}
/**
* Returns the BreakoutRooms for this participant.
*

View File

@@ -22,6 +22,7 @@ const defaultExpectations = {
enabled: true
},
jaas: {
fileSharingEnabled: true,
liveStreamingEnabled: true,
recordingEnabled: true,
transcriptionEnabled: true,

View File

@@ -10,6 +10,11 @@ export type ITokenOptions = {
* The duration for which the token is valid, e.g. "1h" for one hour.
*/
exp?: string;
/**
* Additional JWT features to include (merged with the defaults).
* Keys are feature names (e.g. 'file-upload'), values are boolean or string.
*/
features?: Record<string, boolean | string>;
/**
* The key ID to use for the token.
* If not provided, the kid configured with environment variables will be used (see env.example).
@@ -72,7 +77,8 @@ export function generatePayload(options: ITokenOptions): any {
'transcription': 'true',
'recording': 'true',
'sip-outbound-call': true,
'livestreaming': true
'livestreaming': true,
...options.features
},
},
'room': options.room || '*'

View File

@@ -0,0 +1,172 @@
import BasePageObject from './BasePageObject';
const FILE_SHARING_TAB_ID = 'file_sharing-tab';
const FILE_SHARING_PANEL_ID = 'file_sharing-tab-panel';
const UPLOAD_BUTTON_LABEL = 'Share file';
const DOWNLOAD_BUTTON_LABEL = 'Download';
const REMOVE_BUTTON_LABEL = 'Remove';
/**
* Page object for the file sharing panel.
*/
export default class FileSharingPanel extends BasePageObject {
/**
* Opens the chat sidebar and navigates to the file sharing tab.
*/
async open(): Promise<void> {
if (!await this.participant.driver.$('#sideToolbarContainer').isExisting()) {
await this.participant.getToolbar().clickChatButton();
}
await this.participant.driver.$(`#${FILE_SHARING_TAB_ID}`).click();
}
/**
* Returns whether the file sharing panel is the currently active tab.
*/
async isActive(): Promise<boolean> {
return this.participant.execute(() => {
// @ts-ignore
const state = APP?.store?.getState?.();
return state?.['features/chat']?.focusedTab === 'file_sharing-tab';
});
}
/**
* Returns whether the upload button is enabled (not disabled).
*/
async isUploadButtonEnabled(): Promise<boolean> {
const btn = this.participant.driver.$(`[aria-label="${UPLOAD_BUTTON_LABEL}"]`);
return !await btn.getAttribute('disabled');
}
/**
* Uploads a file via the hidden file input.
*
* @param {string} localFilePath - Local path to the file to upload.
*/
async uploadFile(localFilePath: string): Promise<void> {
const remotePath = await this.participant.driver.uploadFile(localFilePath);
const input = this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} input[type="file"]`);
await input.addValue(remotePath);
}
/**
* Waits until a file with the given name appears in the file list.
*
* @param {string} fileName - The file name to wait for.
* @param {number} timeout - Timeout in milliseconds.
*/
async waitForFile(fileName: string, timeout = 15_000): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`)
.waitForExist({ timeout, timeoutMsg: `File "${fileName}" did not appear within ${timeout}ms` });
}
/**
* Waits until a file with the given name disappears from the file list.
*
* @param {string} fileName - The file name to wait for.
* @param {number} timeout - Timeout in milliseconds.
*/
async waitForFileGone(fileName: string, timeout = 15_000): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`)
.waitForExist({ timeout, reverse: true, timeoutMsg: `File "${fileName}" still present after ${timeout}ms` });
}
/**
* Returns whether a file with the given name exists in the file list.
*
* @param {string} fileName - The file name to check.
*/
async hasFile(fileName: string): Promise<boolean> {
return this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`).isExisting();
}
/**
* Clicks the download button for the given file. Hovers over the file item first to make action buttons visible.
*
* @param {string} fileName - The file name to download.
*/
async downloadFile(fileName: string): Promise<void> {
await this.hoverOverFileItem(fileName);
await this.participant.driver
.$(`button[aria-label="${DOWNLOAD_BUTTON_LABEL} ${fileName}"]`)
.click();
}
/**
* Clicks the remove button for the given file. Hovers over the file item first to make action buttons visible.
*
* @param {string} fileName - The file name to remove.
*/
async removeFile(fileName: string): Promise<void> {
await this.hoverOverFileItem(fileName);
await this.participant.driver
.$(`button[aria-label="${REMOVE_BUTTON_LABEL} ${fileName}"]`)
.click();
}
/**
* Returns whether the remove button exists in the DOM for the given file.
* The remove button is only rendered when the participant has the 'file-upload' JWT feature.
*
* @param {string} fileName - The file name.
*/
canRemoveFile(fileName: string) {
return this.participant.driver
.$(`button[aria-label="${REMOVE_BUTTON_LABEL} ${fileName}"]`)
.isExisting();
}
/**
* Returns whether the download button exists in the DOM for the given file.
*
* @param {string} fileName - The file name.
*/
canDownloadFile(fileName: string) {
return this.participant.driver
.$(`button[aria-label="${DOWNLOAD_BUTTON_LABEL} ${fileName}"]`)
.isExisting();
}
/**
* Simulates dragging a file into the conference area and dispatches drag events.
* A dragenter event sets the React isDragging state, then after a short delay a
* dragover event triggers the file sharing tab to open.
*/
async simulateDragIntoConference(): Promise<void> {
await this.participant.driver.executeAsync((done: () => void) => {
const el = document.querySelector('[data-testid="conference-drag-zone"]') ?? document.body;
el.dispatchEvent(new DragEvent('dragenter', {
bubbles: true,
cancelable: true
}));
setTimeout(() => {
el.dispatchEvent(new DragEvent('dragover', {
bubbles: true,
cancelable: true
}));
done();
}, 200);
});
}
/**
* Returns whether the chat sidebar is currently open.
*/
isChatOpen() {
return this.participant.driver.$('#sideToolbarContainer').isExisting();
}
/**
* Hovers over the file item element to make its action buttons visible.
*
* @param {string} fileName - The file name whose item should be hovered.
*/
private async hoverOverFileItem(fileName: string): Promise<void> {
await this.participant.driver.$(`#${FILE_SHARING_PANEL_ID} [title="${fileName}"]`).moveTo();
}
}

View File

@@ -0,0 +1 @@
This is a test file for file sharing upload tests.

View File

@@ -0,0 +1,169 @@
import { expect } from '@wdio/globals';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
usesBrowsers: [ 'p1', 'p2' ]
});
const TEST_FILE_PATH = 'tests/resources/test-upload.txt';
const TEST_FILE_NAME = 'test-upload.txt';
describe('File sharing', () => {
let p1: Participant, p2: Participant;
let fileSharingEnabled: boolean;
it('setup', async () => {
const room = ctx.roomName;
p1 = await joinJaasMuc({
name: 'p1',
token: t({ room, features: { 'file-upload': 'true' } })
});
fileSharingEnabled = await p1.execute(
() => Boolean((window as any).config?.fileSharing?.enabled && (window as any).config?.fileSharing?.apiUrl)
);
expect(fileSharingEnabled).toBe(expectations.jaas.fileSharingEnabled);
});
it('upload button enabled with file-upload feature', async () => {
if (!fileSharingEnabled) {
return;
}
const panel = p1.getFileSharingPanel();
await panel.open();
expect(await panel.isUploadButtonEnabled()).toBe(true);
});
it('upload button disabled without file-upload feature', async () => {
if (!fileSharingEnabled) {
return;
}
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
const panel = p2.getFileSharingPanel();
await panel.open();
expect(await panel.isUploadButtonEnabled()).toBe(false);
});
it('user with file-upload can delete files uploaded by another participant', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 (with file-upload) uploads a file
const p1Panel = p1.getFileSharingPanel();
await p1Panel.open();
await p1Panel.uploadFile(TEST_FILE_PATH);
await p1Panel.waitForFile(TEST_FILE_NAME);
// p2 (joined earlier without file-upload) rejoins with file-upload to test deletion of p1's file
p2 = await joinJaasMuc({
name: 'p2',
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
});
const p2Panel = p2.getFileSharingPanel();
await p2Panel.open();
await p2Panel.waitForFile(TEST_FILE_NAME);
// p2 should be able to see and use the remove button for p1's file
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(true);
await p2Panel.removeFile(TEST_FILE_NAME);
await p1Panel.waitForFileGone(TEST_FILE_NAME);
});
it('user without file-upload can download but not delete files', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 (with file-upload) uploads a fresh file
const p1Panel = p1.getFileSharingPanel();
await p1Panel.open();
await p1Panel.uploadFile(TEST_FILE_PATH);
await p1Panel.waitForFile(TEST_FILE_NAME);
// p2 rejoins without file-upload
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
const p2Panel = p2.getFileSharingPanel();
await p2Panel.open();
await p2Panel.waitForFile(TEST_FILE_NAME);
// Download button should be present, remove button should not
expect(await p2Panel.canDownloadFile(TEST_FILE_NAME)).toBe(true);
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(false);
});
it('dragging into conference opens file sharing tab', async () => {
if (!fileSharingEnabled) {
return;
}
// p1 already has file-upload feature
const panel = p1.getFileSharingPanel();
// Ensure chat is closed before starting
if (await panel.isChatOpen()) {
await p1.getToolbar().clickCloseChatButton();
}
await panel.simulateDragIntoConference();
await p1.driver.waitUntil(
() => panel.isChatOpen(),
{ timeout: 3000, timeoutMsg: 'Chat did not open after drag' }
);
expect(await panel.isActive()).toBe(true);
});
it('dragging on pre-join screen does not open file sharing', async () => {
if (!fileSharingEnabled) {
return;
}
// Join with iFrame API, file-upload feature, and pre-join screen enabled — but do NOT click join
p1 = await joinJaasMuc(
{
name: 'p1',
iFrameApi: true,
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
},
{
configOverwrite: { prejoinConfig: { enabled: true } },
skipPrejoinButtonClick: true,
skipWaitToJoin: true
}
);
// Wait for pre-join screen to appear
await p1.getPreJoinScreen().waitForLoading();
// Simulate drag while on pre-join screen
const panel = p1.getFileSharingPanel();
await panel.simulateDragIntoConference();
// Chat/file sharing should NOT have opened
expect(await panel.isChatOpen()).toBe(false);
});
});

View File

@@ -3,6 +3,8 @@ import { multiremotebrowser } from '@wdio/globals';
import { Buffer } from 'buffer';
import fs from 'fs';
import { glob } from 'glob';
import junitReportBuilder from 'junit-report-builder';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import process from 'node:process';
import pretty from 'pretty';
@@ -146,7 +148,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 15_000,
connectionRetryTimeout: 30_000,
// Default request retries count
connectionRetryCount: 3,
@@ -468,6 +470,60 @@ export const config: WebdriverIO.MultiremoteConfig = {
});
},
/**
* Gets executed after a worker process has exited.
* If the worker crashed (e.g. session DELETE timeout), the JUnit reporter never flushes,
* leaving a zero-byte XML file. This hook detects that and writes a failure entry so the
* report generator has something to show.
*/
onWorkerEnd(cid, exitCode, workerSpecs) {
if (exitCode === 0) {
return;
}
const xmlPath = path.join(TEST_RESULTS_DIR, `results-${cid}.xml`);
try {
if (fs.statSync(xmlPath).size > 0) {
return;
}
} catch {
// file doesn't exist yet — fall through and create it
}
const specName = workerSpecs?.[0] ? path.basename(workerSpecs[0], '.spec.ts') : 'unknown';
const dirMatch = workerSpecs?.[0]?.match(/\/tests\/specs\/([^/]+)\//);
const dir = dirMatch ? dirMatch[1] : 'unknown';
const message = `Worker exited with code ${exitCode} before results were written. Test result is unknown - tests may have passed.`;
const b = junitReportBuilder.newBuilder();
b.testSuite().name(specName).testCase()
.name('Test runner crashed')
.className(specName)
.error(message);
b.writeTo(xmlPath);
const allureResult = {
uuid: randomUUID(),
name: 'Test runner crashed',
status: 'broken',
statusDetails: { message },
stage: 'finished',
steps: [],
attachments: [],
parameters: [],
labels: [
{ name: 'parentSuite', value: dir },
{ name: 'suite', value: specName }
],
links: []
};
const allurePath = path.join(TEST_RESULTS_DIR, 'allure-results', `${allureResult.uuid}-result.json`);
fs.writeFileSync(allurePath, JSON.stringify(allureResult));
console.log(`[onWorkerEnd] Wrote error XML and allure result for crashed worker ${cid} (spec: ${specName})`);
},
/**
* Gets executed after all workers have shut down and the process is about to exit.
* An error thrown in the `onComplete` hook will result in the test run failing.
@@ -503,7 +559,6 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
});
const reportError = new Error('Could not generate Allure report');
const generation = allure([
'generate', `${TEST_RESULTS_DIR}/allure-results`,
'--clean', '--single-file',
@@ -512,15 +567,15 @@ export const config: WebdriverIO.MultiremoteConfig = {
return new Promise<void>((resolve, reject) => {
const generationTimeout = setTimeout(
() => reject(reportError),
5000);
() => reject(new Error('Could not generate Allure report: timed out after 60s')),
60_000);
// @ts-ignore
generation.on('exit', eCode => {
clearTimeout(generationTimeout);
if (eCode !== 0) {
return reject(reportError);
return reject(new Error(`Could not generate Allure report: allure exited with code ${eCode}`));
}
console.log('Allure report successfully generated');

View File

@@ -7,5 +7,5 @@ import { merge } from 'lodash-es';
import { config as defaultConfig } from './wdio.conf.ts';
export const config = merge(defaultConfig, {
baseUrl: 'https://127.0.0.1:8080/torture'
baseUrl: 'https://localhost:8080/torture'
}, { clone: false });

View File

@@ -257,6 +257,7 @@ function getDevServerConfig() {
warnings: false
}
},
allowedHosts: 'all',
host: 'localhost',
hot: true,
proxy: [