Compare commits

..

36 Commits

Author SHA1 Message Date
Mihaela Dumitru
c0fc232a0f feat(visitors): add showJoinMeetingDialog config option (#16540) 2025-11-17 16:05:41 -06:00
Hristo Terezov
0fbadd716a Revert "fix(large-video): Prevents unnecessary updates when container is hidden"
This reverts commit 6deb0a6385.
2025-11-11 08:39:53 -06:00
damencho
dbe927666e fix(tests): Give some time for recording events to be received. 2025-11-07 11:18:14 -06:00
damencho
d6f5f2fec0 fix: Disable urlNormalisation on FF. 2025-11-07 11:07:09 -06:00
Jaya Allamsetty
5c1b191dc4 chore(deps) Update lib-jitsi-meet branch. 2025-11-06 11:49:14 -05:00
damencho
45d4039b53 feat(prosody): Adds a module to cleanup room with just service components in it. 2025-11-04 16:47:36 -06:00
damencho
93871d4ec9 fix(persistent_lobby): Avoids calling destroy twice. 2025-11-04 16:47:28 -06:00
Jaya Allamsetty
c5acef3824 fix(video-layout) Possibly fixes auto-pinning of SS in a large call.
When a user joins a very large call with SS, sometime SS is not auto-pinned to stage. This may happen when lot of participant joins are processed at the same time and therefore the state for remoteScreenShares may not get updated in time. Added extra logging to help debug if this issue reproduces.
2025-11-04 13:46:10 -05:00
damencho
615fcad3a3 fix(lobby): Hide login button if authenticated(jwt is available). 2025-11-03 14:30:28 -06:00
Дамян Минков
2e201ede28 * fix(lobby): Updates metadata on destroy lobby room and let in participants on empty main.
* fix(lobby): Updates metadata on destroy lobby room.

* fix(visitors): Let people join lobby when main room is empty but with lobby.
2025-10-30 16:08:57 -05:00
bgrozev
e1c99d3220 feat: Filter transcription results. (#16606) 2025-10-30 15:40:09 -05:00
bgrozev
7a8bd1b348 feat(tests): Do not require WebhooksProxy for jaas dial-in test (#16595)
* Add a requireWebhooksProxy test property.

* test: Make the jaas dial-in test use but not require WH proxy.
2025-10-28 14:15:27 -05:00
Дамян Минков
8d1a2694a6 feat(metadata): Adds lobbyEnabled and visitorsEnabled to metadata. (#16583) 2025-10-28 12:16:45 -05:00
Kerinlin
f93509b460 fix(i18n): Fix Chinese language issues and hyphenated locale persistence
- Fix missing Chinese translations in main-zh-CN.json and main-zh-TW.json
- Fix language selection not persisting for hyphenated locales (zh-CN, zh-TW, es-US, fr-CA, pt-BR)
- Update normalizeCurrentLanguage to check exact match before normalization
2025-10-28 12:16:38 -05:00
damencho
307fd0d9e1 fix(tests): More tests fixes.
fix(tests): Fix hangup by avoiding not defined error.

ReferenceError: APP is not defined.

fix(tests): Fix reference to APP in debug logs retrieval

Avoids `ReferenceError: APP is not defined` the failure may happen after hangup where APP is not defined.

Minor test fixes (#16577)

* test: Increase password dialog timeout.
* test: Try to fix hangup().

feat(tests): Drop more aria selectors.

fix(tests): Send logs to browser to keep correct order.

feat(tests): Uses memory logs on failure and stops logging during conference.

feat(tests): Avoids using aria selector for breakout rooms.

It is reported to be slow at times.

fix(tests) Increase backToP2PDelay to 3 secs.
Setting it to 1 sec was causing p2p connections to be created when it was not needed.

feat(tests): Fail early and gather debug logs for iframe tests.
2025-10-28 12:16:06 -05:00
damencho
ad028e3361 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:29:05 -05:00
bgrozev
8df5a4a519 feat(tests): Various tests fixes, expectations.
feat(tests): Make sure dial-in user hangups.

Avoid closing browser and leaving it to the timeout of the connection.

feat(tests): Increase the randomness of the room name.

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

fix(tests): Visitors tests to avoid reordered webhook events.

fix(tests) Wait for p2p switch before checking for SS

fix(tests): Fix p2p enable flag in desktop sharing.

test: Fix jaas chat test again (typo). (#16548)

test: Do not assert message order (they can race). (#16544)

test: Add a test for joining a MUC without a conference request. (#16537)

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.

fix: Fix dial-in test (wait until the dialog is closed). (#16538)

feat: Whitelist the disableFocus config option. (#16526)

Reorganize tests by feature, minor test updates (#16518)

* test: Move lockRoom under moderation/.

* ref: Cleanup lockRoom test.

* test: Move lockRoomDigitsOnly to ui/.

* test: Add a setPasswordAvailable expectation.

* ref: Move the lobby test to moderation/.

* test: Move tests to media/.

* test: Add a useTenant expectation.

* test: Move mute to media/.

* test: Move audioOnly to media/.

* test: Move startMuted to media/.

* test: Move codecSelection to media/.

* ref: Simplify, log the "actual" codec value.

* test: Move stopVideo to media/.

* test: Move videoLayout to ui/.

* test: Move chatPanel to ui/.

* test: Move switchVideo to media/pinning.spec.ts.

* test: Move audioVideoModeration to media/.

* test: Move displayName to ui/.

* test: Move preJoin to ui/.

* test: Move endConference to ui/.

* test: Move selfView to ui/.

* test: Move oneOnOne to ui/.

* test: Move tileView to ui/.

* test: Move singlePort and udp to misc/connectivity.spec.ts.

* test: Move avatars to misc/.

* test: Move polls to misc/.

* test: Move breakoutRooms to misc/.

* test: Move followMe to misc/.

* test: Move invite to dial/dialInUi.spec.ts.

* test: Move dialInAudio to dial/dialIn.spec.ts.

* test: Only log expectations in the main wdio process.

* test: Move fakeDialInAudio to dial/.

* test: Move subject to misc/.

* test: Check for subject set remotely.

* test: Remove references to "2way", "3way".

* test: Consolidate all dial-in tests in one file.

* test: Move dialIn to misc/.

* test: Adjust test titles.

* Remove waitForAudioFromDialInParticipant test.
2025-10-16 11:28:24 -05:00
damencho
b6110a5120 feat(token_verification): Pass through recorder and transcriber into meetings. 2025-10-14 15:53:05 -05:00
damencho
698aa8db3e fix(persistent_lobby): Fix main room lookup.
The change about keeping jid was introduced in 5580301.
2025-10-08 14:42:10 -05:00
damencho
50bad7bbca fix(shot_lived_token): Handles case with empty string for tenant. 2025-10-08 10:29:51 -05:00
Mihaela Dumitru
9d4e6c2d0d fix(recording) prevent recording consent dialog for visitors (#16452) 2025-10-08 16:09:55 +03:00
JPL
f3e1fbfdce Update jitsi-meet-rnsdk.podspec
This PR addresses a sporadic issue where cp would fail with a "directory not found" error during file operations. Replaced cp with ditto, which handles directory copying more reliably on macOS and resolves the random failures observed.
2025-10-08 11:15:59 +03:00
damencho
6287c14dd3 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2100.0.0+0d2e5fef...v2101.0.0+8061f52a
2025-10-07 14:09:51 -04:00
bgrozev
8a7ee9bae5 test: Add skip reason to report (#16515)
* test: Add description, skip reason to Allure report.
* test: Adds reasons for skipped tests.
* test: Add more URL normalization test cases.
* ref: Move urlNormalization test to misc/.
2025-10-07 12:28:39 -05:00
Calinteodor
38677dbe0a dep(react-native-immersive-mode): remove everything related to this (#16513)
* Remove anything related to immersive mode on Android. Right now we use top and bottom insets.
2025-10-07 13:50:39 +03:00
bgrozev
1900c42098 test: Fix token for moderation test. (#16510) 2025-10-06 12:40:51 -05:00
Hristo Terezov
6deb0a6385 fix(large-video): Prevents unnecessary updates when container is hidden
Large video was being updated through scheduleLargeVideoUpdate even when
the large video container was hidden via CSS. This occurred in multiple
layout modes: tile view, stage filmstrip (with 2+ participants), and
etherpad editing. These updates caused expensive operations including
setting video streams, managing track listeners, updating avatars, and
running show/hide animations - all wasted CPU cycles since the container
wasn't visible.

The fix introduces a centralized shouldHideLargeVideo() function that checks
all cases where the large video container is hidden. This function is used in
selectParticipantInLargeVideo() to guard to not update the participant id.
A state listener monitors transitions from hidden to visible states and ensures
the large video participant id is properly updated when the container becomes
visible again and set to undefined when large video is hidden.

This improves performance by eliminating unnecessary video element manipulation
and handler execution across all layout modes where large video is not displayed.
2025-10-06 12:00:40 -05:00
Calinteodor
ce567955f0 chore(android): remove api check for setting top bottom insets (#16509)
* chore(android): remove api check for setting top bottom insets
2025-10-06 18:23:20 +03:00
Mihaela Dumitru
9d2f1ce8e0 fix(ui): improve tab badge styling (#16507) 2025-10-06 13:40:41 +03:00
Jaya Allamsetty
841ab8c052 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v2099.0.0+89536686...v2100.0.0+0d2e5fef
2025-10-04 07:31:05 -04:00
yanas
3e4f45dc7b Update main-fr.json 2025-10-03 15:25:55 -05:00
damencho
19cff49ab1 feat(conference): Adds option to silently reconnect.
Uses the end_meeting endpoint to trigger a silent reconnect by destroying the current conference and passing its jid.
2025-10-03 14:58:56 -05:00
Mihaela Dumitru
a06c3fe715 feat(file-sharing): show count badges for unread files and notify on uploads/removals (#16484) 2025-10-03 01:52:24 +03:00
Дамян Минков
5580301ef7 fix(prosody): Avoid using stale room instances. (#16492)
* fix(prosody): Avoid using stale room instances.

In very rare cases a participant can request a room and jicofo join there, but the participant don't show up (waiting for host) so jicofo leaves and in the mean time if someone tries to use the room instance just before and after the room is being destroyed, strange things can occur like web connected and joined to a stale room where nothing is received exchanged compared to the live meeting room.

* squash: Revert meeting-id one, will fix it in the problem place where there is an async call.

* squash: Change to a simple check.
2025-10-02 16:50:01 -05:00
damencho
69b0ac4686 fix(tests): Fixes randomly failing start muted test. 2025-10-02 17:18:57 -04:00
bgrozev
9f7eb6b657 test: Add configurable test expectations. (#16496)
* Add a sample "expectations" config.
* feat: Add configurable expectation for dial in.
* Add JaaS unauthenticatedJoins expectation.
* test: Move grantModerator to moderation/, add expectation.
* test: Move kick test to moderation/, fix p2p enabled case.
* test: Add a test case for non-moderator kick.
2025-10-02 15:49:03 -05:00
140 changed files with 1920 additions and 1324 deletions

View File

@@ -73,7 +73,6 @@ dependencies {
}
implementation project(':react-native-gesture-handler')
implementation project(':react-native-get-random-values')
implementation project(':react-native-immersive-mode')
implementation project(':react-native-keep-awake')
implementation project(':react-native-orientation-locker')
implementation project(':react-native-pager-view')

View File

@@ -96,9 +96,6 @@ public class JitsiMeetActivity extends AppCompatActivity
public static void addTopBottomInsets(@NonNull Window w, @NonNull View v) {
// Only apply if edge-to-edge is supported (API 30+) or enforced (API 35+)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return;
View decorView = w.getDecorView();
decorView.post(() -> {

View File

@@ -101,7 +101,6 @@ class ReactInstanceManagerHolder {
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
new org.linusu.RNGetRandomValuesPackage(),
new com.rnimmersivemode.RNImmersiveModePackage(),
new com.swmansion.rnscreens.RNScreensPackage(),
new com.zmxv.RNSound.RNSoundPackage(),
new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),

View File

@@ -24,8 +24,6 @@ include ':react-native-giphy'
project(':react-native-giphy').projectDir = new File(rootProject.projectDir, '../node_modules/@giphy/react-native-sdk/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-google-signin/google-signin/android')
include ':react-native-immersive-mode'
project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/@sayem314/react-native-keep-awake/android')
include ':react-native-orientation-locker'

View File

@@ -1375,7 +1375,7 @@ export default {
}
APP.store.dispatch(updateRemoteParticipantFeatures(user));
logger.log(`USER ${id} connected:`, user);
logger.log(`USER ${id} connected`);
APP.UI.addUser(user);
});

View File

@@ -120,7 +120,7 @@
"messageAccessibleTitle": "{{user}} dit: ",
"messageAccessibleTitleMe": "Je dis: ",
"messageTo": "Message privé à {{recipient}}",
"messagebox": "Saisissez un message",
"messagebox": "Envoyer un message",
"newMessages": "Nouveaux messages",
"nickname": {
"popover": "Choisissez un pseudonyme",

View File

@@ -112,7 +112,12 @@
"disabled": "聊天已禁用",
"enter": "加入会议室",
"error": "错误:你的消息未发送。原因:{{error}}",
"everyone": "所有人",
"fieldPlaceHolder": "在这里输入你的信息",
"fileAccessibleTitle": "{{user}}上传了一个文件",
"fileAccessibleTitleMe": "我上传了一个文件",
"fileDeleted": "文件已被删除",
"guestsChatIndicator": "(访客)",
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
"message": "信息",
"messageAccessibleTitle": "{{user}}",
@@ -300,6 +305,12 @@
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
"applicationWindow": "应用程序窗口",
"authenticationRequired": "需要身份验证",
"cameraCaptureDialog": {
"description": "使用手机摄像头拍照并发送",
"ok": "打开相机",
"reject": "暂不使用",
"title": "拍照"
},
"cameraConstraintFailedError": "你的摄像头未满足某些必要条件。",
"cameraNotFoundError": "找不到摄像头",
"cameraNotSendingData": "我们无法访问你的摄像头,请检查是否有其他应用程序正在使用此设备,从设置菜单中选择另一个设备,或尝试重新加载应用程序。",
@@ -375,22 +386,34 @@
"micTimeoutError": "无法开启音频设备,发生超时!",
"micUnknownError": "由于未知原因,无法使用麦克风。",
"moderationAudioLabel": "允许参会者自己解除静音",
"moderationDesktopLabel": "允许非主持人共享屏幕",
"moderationVideoLabel": "允许参会者自己开启视频",
"muteEveryoneDialog": "参会者可以在任何时候解除自己的静音。",
"muteEveryoneDialogModerationOn": "参会者可以在任何时候请求发言。",
"muteEveryoneElseDialog": "静音后,你将无法为其解除静音,但是他们可以随时解除自己的静音。",
"muteEveryoneElseTitle": "除了{{whom}}以外的将所有人静音?",
"muteEveryoneElsesDesktopDialog": "一旦停止共享,你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的屏幕共享?",
"muteEveryoneElsesVideoDialog": "一旦关闭,你将无法重新开启他们的摄像头,但他们随时可以重新开启。",
"muteEveryoneElsesVideoTitle": "除了{{whom}}以外,关闭所有人的摄像头?",
"muteEveryoneSelf": "你自己",
"muteEveryoneStartMuted": "现在所有人都已静音",
"muteEveryoneTitle": "静音所有人?",
"muteEveryonesDesktopDialog": "参会者可以随时共享他们的屏幕",
"muteEveryonesDesktopDialogModerationOn": "参会者可以随时请求共享他们的屏幕",
"muteEveryonesDesktopTitle": "停止所有人的屏幕共享?",
"muteEveryonesVideoDialog": "参会者可以随时开启他们的摄像头",
"muteEveryonesVideoDialogModerationOn": "参会者可以随时请求开启他们的摄像头",
"muteEveryonesVideoDialogOk": "关闭",
"muteEveryonesVideoTitle": "关闭所有人的摄像头?",
"muteParticipantBody": "你将无法为他们解除静音,但是他们可以随时解除自己的静音。",
"muteParticipantButton": "静音",
"muteParticipantsDesktopBody": "你无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteParticipantsDesktopBodyModerationOn": "你和他们都无法重新开启屏幕共享",
"muteParticipantsDesktopButton": "停止屏幕共享",
"muteParticipantsDesktopDialog": "你确定要停止这个参会者的屏幕共享吗?你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
"muteParticipantsDesktopDialogModerationOn": "你确定要停止这个参会者的屏幕共享吗?你和他们都无法重新开启屏幕共享。",
"muteParticipantsDesktopTitle": "停止这个参会者的屏幕共享?",
"muteParticipantsVideoBody": "你无法重新开启摄像头,但他们随时可以重新开启。",
"muteParticipantsVideoBodyModerationOn": "你和他们都无法重新开启摄像头",
"muteParticipantsVideoButton": "关闭摄像头",
@@ -503,6 +526,7 @@
"tokenAuthFailedWithReasons": "抱歉,你无法加入此通话,原因:",
"tokenAuthUnsupported": "Token地址不支持",
"transcribing": "转录中",
"unauthenticatedAccessDisabled": "此会议需要身份验证,请先登录后继续。",
"unlockRoom": "移除会议$t(lockRoomPassword)",
"user": "用户",
"userIdentifier": "用户ID",
@@ -547,11 +571,17 @@
"downloadFailedDescription": "请稍后重试",
"downloadFailedTitle": "下载失败",
"downloadFile": "下载",
"downloadStarted": "文件下载已开始",
"dragAndDrop": "拖拽文件到此处上传",
"fileAlreadyUploaded": "文件已上传至本次会议",
"fileRemovedByOther": "你的文件{{fileName}}已被移除",
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
"fileTooLargeTitle": "文件太大",
"fileUploadProgress": "文件上传进度",
"fileUploadedSuccessfully": "文件上传成功",
"newFileNotification": "{{participantName}}分享了{{fileName}}",
"removeFile": "移除",
"removeFileSuccess": "文件移除成功",
"uploadFailedDescription": "请稍后重试",
"uploadFailedTitle": "上传失败",
"uploadFile": "文件共享"
@@ -724,7 +754,8 @@
"notificationTitle": "等候室",
"passwordJoinButton": "加入",
"title": "等候室",
"toggleLabel": "开启等候室模式"
"toggleLabel": "开启等候室模式",
"waitForModerator": "会议尚未开始,暂无主持人入会。如需成为主持人请先登录,或耐心等待会议开始。"
},
"localRecording": {
"clientState": {
@@ -767,8 +798,10 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAll": "允许全部",
"allowAudio": "允许开启麦克风",
"allowBoth": "允许音视频",
"allowDesktop": "允许屏幕共享",
"allowVideo": "允许开启摄像头",
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
@@ -782,6 +815,7 @@
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
"dataChannelClosedWithAudio": "音视频质量可能受影响",
"desktopMutedRemotelyTitle": "你的屏幕共享已被{{participantDisplayName}}停止",
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>",
@@ -839,6 +873,7 @@
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端请确保您更新到我们的",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": "",
"openChat": "打开聊天",
"participantWantsToJoin": "想要加入会议",
"participantsWantToJoin": "想要加入会议",
"passwordRemovedRemotely": "其他参会者移除了$t(lockRoomPasswordUppercase)",
@@ -862,6 +897,7 @@
"suggestRecordingDescription": "是否需要录制本次会议?",
"suggestRecordingTitle": "录制会议",
"unmute": "解除静音",
"unmuteScreen": "开始屏幕共享",
"unmuteVideo": "开启摄像头",
"videoMutedRemotelyDescription": "你可随时重新开启视频",
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
@@ -881,11 +917,14 @@
"admit": "同意加入",
"admitAll": "全部同意加入",
"allow": "允许参会者:",
"allowDesktop": "允许屏幕共享",
"allowVideo": "允许开启摄像头",
"askDesktop": "请求共享屏幕",
"askUnmute": "请求取消静音",
"audioModeration": "自行解除静音",
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
"breakoutRooms": "分组讨论室",
"desktopModeration": "开始屏幕共享",
"goLive": "开始直播",
"invite": "邀请其他人",
"lowerAllHands": "取消全部举手",
@@ -897,6 +936,8 @@
"muteAll": "全体静音",
"muteEveryoneElse": "静音其他人",
"reject": "拒绝",
"stopDesktop": "停止屏幕共享",
"stopEveryonesDesktop": "停止所有人的屏幕共享",
"stopEveryonesVideo": "关闭所有人摄像头",
"stopVideo": "关闭摄像头",
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
@@ -906,9 +947,11 @@
"headings": {
"lobby": "等候室(({{count}}人)",
"participantsList": "会议参会者({{count}}人)",
"viewerRequests": "观众请求({{count}}人)",
"visitorInQueue": "(排队中:{{count}}人)",
"visitorRequests": "(请求加入:{{count}}人)",
"visitors": "观众(({{count}}人)",
"visitorsList": "观众({{count}}人)",
"waitingLobby": "在等候室等待({{count}}人)"
},
"search": "搜索参会者",
@@ -929,6 +972,9 @@
"by": "由{{ name }}发起",
"closeButton": "结束投票",
"create": {
"accessibilityLabel": {
"send": "发送投票"
},
"addOption": "添加选项",
"answerPlaceholder": "选项{{index}}",
"cancel": "取消",
@@ -1345,6 +1391,20 @@
"videounmute": "打开摄像头"
},
"addPeople": "添加成员到通话中",
"advancedAudioSettings": {
"aec": {
"label": "回声消除"
},
"agc": {
"label": "自动增益控制"
},
"ns": {
"label": "降噪"
},
"stereo": {
"label": "立体声"
}
},
"audioOnlyOff": "关闭省流模式",
"audioOnlyOn": "开启省流模式",
"audioRoute": "选择音频设备",
@@ -1416,6 +1476,7 @@
"reactionHeart": "发送爱心",
"reactionLaugh": "发送大笑",
"reactionLike": "发送点赞",
"reactionLove": "发送爱心",
"reactionSilence": "发送沉默",
"reactionSurprised": "发送惊讶",
"reactions": "互动表情",
@@ -1501,6 +1562,8 @@
"connectionInfo": "连接信息",
"demote": "设为观众",
"domute": "静音",
"domuteDesktop": "停止屏幕共享",
"domuteDesktopOfOthers": "停止屏幕共享给其他人",
"domuteOthers": "静音其他人",
"domuteVideo": "关闭摄像头",
"domuteVideoOfOthers": "关闭其他人摄像头",
@@ -1565,6 +1628,8 @@
"noMainParticipantsTitle": "会议尚未开始",
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
"requestToJoin": "举手请求",
"requestToJoinDescription": "你的请求已发送给主持人,请稍候!",
"title": "你当前为会议观众"
},
"waitingMessage": "会议开始后将自动加入"

View File

@@ -112,7 +112,12 @@
"disabled": "聊天訊息已停用",
"enter": "加入聊天室",
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
"everyone": "所有人",
"fieldPlaceHolder": "在此輸入您的訊息",
"fileAccessibleTitle": "{{user}}上傳了一個檔案",
"fileAccessibleTitleMe": "我上傳了一個檔案",
"fileDeleted": "檔案已被刪除",
"guestsChatIndicator": "(訪客)",
"lobbyChatMessageTo": "大廳聊天訊息傳送至 {{recipient}}",
"message": "訊息",
"messageAccessibleTitle": "{{user}}",
@@ -300,6 +305,12 @@
"alreadySharedVideoTitle": "同一時間只允許一位影像分享",
"applicationWindow": "應用程式視窗",
"authenticationRequired": "需要驗證",
"cameraCaptureDialog": {
"description": "使用手機攝影機拍照並傳送",
"ok": "開啟相機",
"reject": "暫不使用",
"title": "拍照"
},
"cameraConstraintFailedError": "您的網路攝影機不符合要求。",
"cameraNotFoundError": "找不到網路攝影機。",
"cameraNotSendingData": "我們無法存取您的網路攝影機,請檢查是否有其他應用程式正在使用這個裝置,並從裝置選單裡選擇其他設備或者重新載入。",
@@ -375,22 +386,34 @@
"micTimeoutError": "無法啟動音訊裝置,連線逾時!",
"micUnknownError": "不明原因造成麥克風無法使用。",
"moderationAudioLabel": "允許與會者自我解除靜音",
"moderationDesktopLabel": "允許非主持人共享螢幕",
"moderationVideoLabel": "允許與會者開啟視訊",
"muteEveryoneDialog": "與會者可以隨時解除自己的靜音狀態。",
"muteEveryoneDialogModerationOn": "與會者可以隨時請求發言。",
"muteEveryoneElseDialog": "靜音後,您就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
"muteEveryoneElsesDesktopDialog": "一旦停止共享,您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的螢幕共享?",
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的網路攝影機,但對方隨時能重新開啟自己的網路攝影機。",
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的網路攝影機?",
"muteEveryoneSelf": "您自己",
"muteEveryoneStartMuted": "現在所有人皆已靜音",
"muteEveryoneTitle": "要將所有人靜音嗎?",
"muteEveryonesDesktopDialog": "與會者可以隨時共享他們的螢幕",
"muteEveryonesDesktopDialogModerationOn": "與會者可以隨時請求共享他們的螢幕",
"muteEveryonesDesktopTitle": "停止所有人的螢幕共享?",
"muteEveryonesVideoDialog": "與會者隨時可以重新開啟自己的網路攝影機。",
"muteEveryonesVideoDialogModerationOn": "與會者可以隨時傳送開啟視訊請求。",
"muteEveryonesVideoDialogOk": "停用",
"muteEveryonesVideoTitle": "要關閉所有人的網路攝影機嗎?",
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
"muteParticipantButton": "靜音",
"muteParticipantsDesktopBody": "您無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteParticipantsDesktopBodyModerationOn": "您和他們都無法重新開啟螢幕共享",
"muteParticipantsDesktopButton": "停止螢幕分享",
"muteParticipantsDesktopDialog": "您確定要停止這位與會者的螢幕共享嗎?您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
"muteParticipantsDesktopDialogModerationOn": "您確定要停止這位與會者的螢幕共享嗎?您和他們都無法重新開啟螢幕共享。",
"muteParticipantsDesktopTitle": "停止這位與會者的螢幕共享?",
"muteParticipantsVideoBody": "您無法重新開啟,只有對方能自己重新開啟。",
"muteParticipantsVideoBodyModerationOn": "您和他都無法再將視訊重新開啟。",
"muteParticipantsVideoButton": "停用網路攝影機",
@@ -503,6 +526,7 @@
"tokenAuthFailedWithReasons": "抱歉,您無法參加這個通話,可能原因:{{reason}}",
"tokenAuthUnsupported": "不支援權杖網址。",
"transcribing": "轉錄中",
"unauthenticatedAccessDisabled": "此會議需要身份驗證,請先登入後繼續。",
"unlockRoom": "移除會議 $t(lockRoomPassword)",
"user": "使用者",
"userIdentifier": "使用者 ID",
@@ -547,11 +571,17 @@
"downloadFailedDescription": "請重試",
"downloadFailedTitle": "下載失敗",
"downloadFile": "下載",
"downloadStarted": "檔案下載已開始",
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
"fileAlreadyUploaded": "檔案已上傳至此會議",
"fileRemovedByOther": "您的檔案「{{fileName}}」已被移除",
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
"fileTooLargeTitle": "檔案過大",
"fileUploadProgress": "檔案上傳進度",
"fileUploadedSuccessfully": "檔案上傳成功",
"newFileNotification": "{{participantName}}分享了「{{fileName}}」",
"removeFile": "移除",
"removeFileSuccess": "檔案移除成功",
"uploadFailedDescription": "請重試",
"uploadFailedTitle": "上傳失敗",
"uploadFile": "分享檔案"
@@ -724,7 +754,8 @@
"notificationTitle": "大廳",
"passwordJoinButton": "加入",
"title": "大廳",
"toggleLabel": "啟用大廳模式"
"toggleLabel": "啟用大廳模式",
"waitForModerator": "會議尚未開始,暫無主持人入會。如需成為主持人請先登入,或耐心等待會議開始。"
},
"localRecording": {
"clientState": {
@@ -767,8 +798,10 @@
"me": "我",
"notify": {
"OldElectronAPPTitle": "安全漏洞!",
"allowAll": "允許全部",
"allowAudio": "允許音訊",
"allowBoth": "允許音訊與視訊",
"allowDesktop": "允許螢幕分享",
"allowVideo": "允許視訊",
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
@@ -782,6 +815,7 @@
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
"desktopMutedRemotelyTitle": "您的螢幕分享已被{{participantDisplayName}}停止",
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
"disabledIframeSecondaryNative": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷。",
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
@@ -839,6 +873,7 @@
"oldElectronClientDescription1": "您似乎正在使用存在已知安全漏洞的過時 Jitsi Meet 用戶端,請盡快更新至我們的",
"oldElectronClientDescription2": "最新版本",
"oldElectronClientDescription3": "",
"openChat": "開啟聊天",
"participantWantsToJoin": "希望加入會議",
"participantsWantToJoin": "希望加入會議",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他與會者移除",
@@ -862,6 +897,7 @@
"suggestRecordingDescription": "是否要開始錄製這場會議?",
"suggestRecordingTitle": "錄製此會議",
"unmute": "取消靜音",
"unmuteScreen": "開始螢幕分享",
"unmuteVideo": "啟用視訊",
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
@@ -881,11 +917,14 @@
"admit": "準許",
"admitAll": "準許所有人",
"allow": "允許與會者能夠:",
"allowDesktop": "允許螢幕分享",
"allowVideo": "允許視訊",
"askDesktop": "請求共享螢幕",
"askUnmute": "要求解除靜音",
"audioModeration": "自我解除靜音",
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
"breakoutRooms": "分組討論室",
"desktopModeration": "開始螢幕分享",
"goLive": "開始直播",
"invite": "邀請他人",
"lowerAllHands": "全部取消舉手",
@@ -897,6 +936,8 @@
"muteAll": "靜音所有人",
"muteEveryoneElse": "靜音其他人",
"reject": "拒絕",
"stopDesktop": "停止螢幕分享",
"stopEveryonesDesktop": "停止所有人的螢幕分享",
"stopEveryonesVideo": "停用所有人的視訊",
"stopVideo": "停用視訊",
"unblockEveryoneMicCamera": "解除封鎖所有人的麥克風及網路攝影機",
@@ -906,9 +947,11 @@
"headings": {
"lobby": "大廳({{count}} 人)",
"participantsList": "會議與會者({{count}} 人)",
"viewerRequests": "觀眾請求({{count}}人)",
"visitorInQueue": "{{count}} 人等候中)",
"visitorRequests": "{{count}} 人申請",
"visitors": "訪客({{count}} 人)",
"visitorsList": "觀眾({{count}}人)",
"waitingLobby": "於大廳等候({{count}} 人)"
},
"search": "搜尋與會者",
@@ -929,6 +972,9 @@
"by": "由 {{ name }}",
"closeButton": "結束投票",
"create": {
"accessibilityLabel": {
"send": "傳送投票"
},
"addOption": "新增選項",
"answerPlaceholder": "選項 {{index}}",
"cancel": "取消",
@@ -1345,6 +1391,20 @@
"videounmute": "啟用網路攝影機"
},
"addPeople": "新增人員到您的通話中",
"advancedAudioSettings": {
"aec": {
"label": "回聲消除"
},
"agc": {
"label": "自動增益控制"
},
"ns": {
"label": "降噪"
},
"stereo": {
"label": "立體聲"
}
},
"audioOnlyOff": "停用低頻寬模式",
"audioOnlyOn": "啟用低頻寬模式",
"audioRoute": "選擇音訊裝置",
@@ -1416,6 +1476,7 @@
"reactionHeart": "傳送愛心反應",
"reactionLaugh": "傳送大笑反應",
"reactionLike": "傳送比讚反應",
"reactionLove": "傳送愛心",
"reactionSilence": "傳送沉默反應",
"reactionSurprised": "傳送驚訝反應",
"reactions": "反應",
@@ -1501,6 +1562,8 @@
"connectionInfo": "連線資訊",
"demote": "轉為訪客",
"domute": "靜音",
"domuteDesktop": "停止螢幕分享",
"domuteDesktopOfOthers": "停止螢幕分享給其他人",
"domuteOthers": "靜音其他人",
"domuteVideo": "停用網路攝影機",
"domuteVideoOfOthers": "停用其他人的網路攝影機",
@@ -1565,6 +1628,8 @@
"noMainParticipantsTitle": "會議尚未開始",
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
"notAllowedPromotion": "需由與會者同意您的申請",
"requestToJoin": "舉手請求",
"requestToJoinDescription": "您的請求已傳送給主持人,請稍候!",
"title": "您是會議中的訪客"
},
"waitingMessage": "會議開始後您將自動加入!"

View File

@@ -570,10 +570,12 @@
"downloadStarted": "File download started",
"dragAndDrop": "Drag and drop files here or anywhere on screen",
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
"fileRemovedByOther": "Your file '{{ fileName }}' was removed",
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
"fileTooLargeTitle": "The selected file is too large",
"fileUploadProgress": "File upload progress",
"fileUploadedSuccessfully": "File uploaded successfully",
"newFileNotification": "{{ participantName }} shared '{{ fileName }}'",
"removeFile": "Remove",
"removeFileSuccess": "File removed successfully",
"uploadFailedDescription": "Please try again.",

22
package-lock.json generated
View File

@@ -66,7 +66,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8860",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -87,7 +87,6 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",
@@ -18260,8 +18259,7 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#0af202368649360ede8dcc1ca4d055910c6b9337",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.4.6",
@@ -21870,14 +21868,6 @@
"react-native": ">=0.56"
}
},
"node_modules/react-native-immersive-mode": {
"version": "2.0.2",
"resolved": "git+ssh://git@github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"license": "MIT",
"peerDependencies": {
"react-native": ">=0.60.5"
}
},
"node_modules/react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",
@@ -39715,8 +39705,8 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#0af202368649360ede8dcc1ca4d055910c6b9337",
"from": "lib-jitsi-meet@https://github.com/jitsi/lib-jitsi-meet#release-8860",
"requires": {
"@jitsi/js-utils": "2.4.6",
"@jitsi/logger": "2.1.1",
@@ -42365,10 +42355,6 @@
"fast-base64-decode": "^1.0.0"
}
},
"react-native-immersive-mode": {
"version": "git+ssh://git@github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"from": "react-native-immersive-mode@https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c"
},
"react-native-is-edge-to-edge": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz",

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/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet#release-8860",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -93,7 +93,6 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.24.0",
"react-native-get-random-values": "1.11.0",
"react-native-immersive-mode": "https://github.com/jitsi/react-native-immersive-mode.git#38cc9001db24618bc0c61800f81e889bcfb6ff2c",
"react-native-orientation-locker": "https://github.com/jitsi/react-native-orientation-locker.git#fe095651d819cf134624f786b61fc8667862178a",
"react-native-pager-view": "6.8.1",
"react-native-paper": "5.10.3",

View File

@@ -29,7 +29,7 @@ Pod::Spec.new do |s|
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds/"
TARGET_PATH=$(dirname "${CONFIGURATION_BUILD_DIR}")
PROJECT_NAME=$(basename $(dirname $(dirname "${PROJECT_DIR}"))).app
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
ditto "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}/sounds"
',
}
end

View File

@@ -73,7 +73,6 @@
"react-native-device-info": "0.0.0",
"react-native-get-random-values": "0.0.0",
"react-native-gesture-handler": "0.0.0",
"react-native-immersive-mode": "0.0.0",
"react-native-pager-view": "0.0.0",
"react-native-performance": "0.0.0",
"react-native-orientation-locker": "0.0.0",

View File

@@ -4,7 +4,6 @@ import '../mobile/audio-mode/middleware';
import '../mobile/background/middleware';
import '../mobile/call-integration/middleware';
import '../mobile/external-api/middleware';
import '../mobile/full-screen/middleware';
import '../mobile/navigation/middleware';
import '../mobile/permissions/middleware';
import '../mobile/proximity/middleware';

View File

@@ -2,7 +2,6 @@ import '../mobile/audio-mode/reducer';
import '../mobile/background/reducer';
import '../mobile/call-integration/reducer';
import '../mobile/external-api/reducer';
import '../mobile/full-screen/reducer';
import '../mobile/watchos/reducer';
import '../share-room/reducer';

View File

@@ -52,7 +52,6 @@ import { IMobileAudioModeState } from '../mobile/audio-mode/reducer';
import { IMobileBackgroundState } from '../mobile/background/reducer';
import { ICallIntegrationState } from '../mobile/call-integration/reducer';
import { IMobileExternalApiState } from '../mobile/external-api/reducer';
import { IFullScreenState } from '../mobile/full-screen/reducer';
import { IMobileWatchOSState } from '../mobile/watchos/reducer';
import { INoAudioSignalState } from '../no-audio-signal/reducer';
import { INoiseDetectionState } from '../noise-detection/reducer';
@@ -132,7 +131,6 @@ export interface IReduxState {
'features/file-sharing': IFileSharingState;
'features/filmstrip': IFilmstripState;
'features/follow-me': IFollowMeState;
'features/full-screen': IFullScreenState;
'features/gifs': IGifsState;
'features/google-api': IGoogleApiState;
'features/invite': IInviteState;

View File

@@ -7,6 +7,8 @@ import { showNotification } from '../../notifications/actions';
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
import { determineTranscriptionLanguage } from '../../transcribing/functions';
import { IStateful } from '../app/types';
import { connect } from '../connection/actions';
import { disconnect } from '../connection/actions.any';
import { JitsiTrackErrors } from '../lib-jitsi-meet';
import { setAudioMuted, setVideoMuted } from '../media/actions';
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
@@ -22,7 +24,7 @@ import {
safeDecodeURIComponent
} from '../util/uri';
import { setObfuscatedRoom } from './actions';
import { conferenceWillInit, setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -618,3 +620,34 @@ export function updateTrackMuteState(stateful: IStateful, dispatch: IStore['disp
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
}
/**
* Processes the "destroyed" event of a conference and if the destroyed conference is the current one,
* it silently reconnects to the same room.
*
* @param {Object|Function} stateful - Either the whole Redux state object or the Redux store's {@code getState} method.
* @param {Function} dispatch - Redux dispatch function.
* @param {Array} params - The parameters for the destroy event.
*
* @returns {boolean} - True if the destroyed conference was the current one, and we are reconnecting, false otherwise.
*/
export function processDestroyConferenceEvent(stateful: IStateful, dispatch: IStore['dispatch'], params: Array<any>) {
const [ jid ] = params;
const conference = getCurrentConference(stateful);
// if the jid of the room is the same as the current conference, we are being
// notified that the current conference has been destroyed, and we need to reconnect
if (conference?.room?.roomjid === jid) {
dispatch(disconnect(true, false))
.then(() => {
dispatch(conferenceWillInit());
logger.info('Dispatching silent re-connect.');
return dispatch(connect());
});
return true;
}
return false;
}

View File

@@ -6,8 +6,8 @@ import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { CONFERENCE_FAILED } from './actionTypes';
import { conferenceLeft } from './actions.native';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import './middleware.any';
import { processDestroyConferenceEvent } from './functions';
MiddlewareRegistry.register(store => next => action => {
const { dispatch } = store;
@@ -23,6 +23,10 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
if (processDestroyConferenceEvent(state, dispatch, error.params)) {
break;
}
if (!notifyOnConferenceDestruction) {
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));

View File

@@ -24,8 +24,8 @@ import {
KICKED_OUT
} from './actionTypes';
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
import { processDestroyConferenceEvent } from './functions';
import logger from './logger';
import './middleware.any';
let screenLock: WakeLockSentinel | undefined;
@@ -127,6 +127,11 @@ MiddlewareRegistry.register(store => next => action => {
const state = getState();
const { notifyOnConferenceDestruction = true } = state['features/base/config'];
const [ reason ] = action.error.params;
if (processDestroyConferenceEvent(state, dispatch, action.error.params)) {
break;
}
const titlekey = Object.keys(TRIGGER_READY_TO_CLOSE_REASONS)[
Object.values(TRIGGER_READY_TO_CLOSE_REASONS).indexOf(reason)
];

View File

@@ -654,6 +654,7 @@ export interface IConfig {
video?: boolean;
};
queueService: string;
showJoinMeetingDialog?: boolean;
};
watchRTCConfigParams?: IWatchRTCConfiguration;
webhookProxyUrl?: string;

View File

@@ -99,6 +99,7 @@ export default [
'disabledNotifications',
'disabledSounds',
'disableFilmstripAutohiding',
'disableFocus',
'disableInitialGUM',
'disableInviteFunctions',
'disableIncomingMessageSound',
@@ -237,6 +238,7 @@ export default [
'useTurnUdp',
'videoQuality',
'visitors.enableMediaOnPromote',
'visitors.showJoinMeetingDialog',
'watchRTCConfigParams.allowBrowserLogCollection',
'watchRTCConfigParams.collectionInterval',
'watchRTCConfigParams.console',

View File

@@ -385,10 +385,10 @@ function _propertiesUpdate(properties: object) {
* Closes connection.
*
* @param {boolean} isRedirect - Indicates if the action has been dispatched as part of visitor promotion.
*
* @param {boolean} shouldLeave - Indicates whether to call JitsiConference.leave().
* @returns {Function}
*/
export function disconnect(isRedirect?: boolean) {
export function disconnect(isRedirect?: boolean, shouldLeave = true) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
@@ -407,20 +407,26 @@ export function disconnect(isRedirect?: boolean) {
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_, isRedirect));
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
if (!shouldLeave) {
// we are skipping JitsiConference.leave(), but will still dispatch the normal leave flow events
dispatch(conferenceLeft(conference_));
promise = Promise.resolve();
} else {
promise
= conference_.leave()
.catch((error: Error) => {
logger.warn(
'JitsiConference.leave() rejected with:',
error);
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
// The library lib-jitsi-meet failed to make the
// JitsiConference leave. Which may be because
// JitsiConference thinks it has already left.
// Regardless of the failure reason, continue in
// jitsi-meet as if the leave has succeeded.
dispatch(conferenceLeft(conference_));
});
}
} else {
promise = Promise.resolve();
}

View File

@@ -78,12 +78,6 @@ export const CHAT_ENABLED = 'chat.enabled';
*/
export const FILMSTRIP_ENABLED = 'filmstrip.enabled';
/**
* Flag indicating if fullscreen (immersive) mode should be enabled.
* Default: enabled (true).
*/
export const FULLSCREEN_ENABLED = 'fullscreen.enabled';
/**
* Flag indicating if the Help button should be enabled.
* Default: enabled (true).

View File

@@ -80,7 +80,7 @@ const options: i18next.InitOptions = {
interpolation: {
escapeValue: false // not needed for react as it escapes by default
},
load: 'languageOnly',
load: 'all',
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
react: {
// re-render when a new resource bundle is added

View File

@@ -186,6 +186,9 @@ function _initLogging({ dispatch, getState }: IStore,
Logger.addGlobalTransport(debugLogCollector);
JitsiMeetJS.addGlobalLogTransport(debugLogCollector);
debugLogCollector.start();
Logger.removeGlobalTransport(console);
JitsiMeetJS.removeGlobalLogTransport(console);
}
} else if (logCollector && loggingConfig.disableLogCollector) {
Logger.removeGlobalTransport(logCollector);

View File

@@ -27,6 +27,7 @@ import {
isRemoteScreenshareParticipant,
isScreenShareParticipant
} from './functions';
import logger from './logger';
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
/**
@@ -364,6 +365,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant joined', id);
}
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
@@ -448,6 +451,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
sortedRemoteVirtualScreenshareParticipants.delete(id);
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
logger.debug('Remote screenshare participant left', id);
}
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {

View File

@@ -21,6 +21,7 @@ import {
getRemoteScreensharesBasedOnPresence,
getVirtualScreenshareParticipantOwnerId
} from './functions';
import logger from './logger';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -69,14 +70,19 @@ function _createOrRemoveVirtualParticipants(
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
})));
removedScreenshareSourceNames.forEach(id => {
logger.debug('Dispatching participantLeft for virtual screenshare', id);
dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
}));
});
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
addedScreenshareSourceNames.forEach(id => {
logger.debug('Creating virtual screenshare participant', id);
dispatch(createVirtualScreenshareParticipant(id, false, conference));
});
}
}

View File

@@ -1,6 +1,5 @@
import { IReduxState, IStore } from '../../app/types';
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
import { VIDEO_CODEC } from '../../video-quality/constants';
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
import {
@@ -57,75 +56,12 @@ export function isLargeVideoReceived({ getState }: IStore): boolean {
}
/**
* Returns whether the local video track is encoded in AV1.
* Returns the local video track's codec.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
* @returns {string?} The local video track's codec.
*/
export function isLocalCameraEncodingAv1({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.AV1) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in H.264.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingH264({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.H264) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in VP8.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingVp8({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP8) {
return true;
}
return false;
}
/**
* Returns whether the local video track is encoded in VP9.
*
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function isLocalCameraEncodingVp9({ getState }: IStore): boolean {
const state = getState();
const tracks = state['features/base/tracks'];
const localtrack = getLocalVideoTrack(tracks);
if (localtrack?.codec?.toLowerCase() === VIDEO_CODEC.VP9) {
return true;
}
return false;
export function getLocalCameraEncoding({ getState }: IStore): string | undefined {
return getLocalVideoTrack(getState()['features/base/tracks'])?.codec?.toLowerCase();
}
/**

View File

@@ -8,12 +8,9 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
import { setConnectionState } from './actions';
import {
getLocalCameraEncoding,
getRemoteVideoType,
isLargeVideoReceived,
isLocalCameraEncodingAv1,
isLocalCameraEncodingH264,
isLocalCameraEncodingVp8,
isLocalCameraEncodingVp9,
isRemoteVideoReceived,
isTestModeEnabled
} from './functions';
@@ -90,10 +87,7 @@ function _bindTortureHelpers(store: IStore) {
getJitsiMeetGlobalNS().testing = {
getRemoteVideoType: getRemoteVideoType.bind(null, store),
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
isLocalCameraEncodingAv1: isLocalCameraEncodingAv1.bind(null, store),
isLocalCameraEncodingH264: isLocalCameraEncodingH264.bind(null, store),
isLocalCameraEncodingVp8: isLocalCameraEncodingVp8.bind(null, store),
isLocalCameraEncodingVp9: isLocalCameraEncodingVp9.bind(null, store),
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
};
}

View File

@@ -71,11 +71,16 @@ const useStyles = makeStyles()(theme => {
badge: {
...theme.typography.labelBold,
color: theme.palette.text04,
padding: `0 ${theme.spacing(1)}`,
borderRadius: '100%',
alignItems: 'center',
backgroundColor: theme.palette.warning01,
marginLeft: theme.spacing(2)
borderRadius: theme.spacing(2),
color: theme.palette.text04,
display: 'inline-flex',
height: theme.spacing(3),
justifyContent: 'center',
marginLeft: theme.spacing(2),
minWidth: theme.spacing(2),
padding: `0 ${theme.spacing(1)}`
},
icon: {

View File

@@ -7,7 +7,9 @@ import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import { TabBarLabelCounter } from '../../../mobile/navigation/components/TabBarLabelCounter';
import { getUnreadPollCount } from '../../../polls/functions';
import { closeChat, sendMessage } from '../../actions.native';
import { getUnreadFilesCount } from '../../functions';
import { IChatProps as AbstractProps } from '../../types';
import ChatInputBar from './ChatInputBar';
@@ -17,6 +19,21 @@ import styles from './styles';
interface IProps extends AbstractProps {
/**
* The number of unread file messages.
*/
_nbUnreadFiles: number;
/**
* The number of unread messages.
*/
_nbUnreadMessages: number;
/**
* The number of unread polls.
*/
_nbUnreadPolls: number;
/**
* Default prop for navigating between screen components(React Navigation).
*/
@@ -96,7 +113,9 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -104,13 +123,16 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: getUnreadPollCount(state),
_nbUnreadFiles: getUnreadFilesCount(state)
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
const { _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles, dispatch, navigation, t } = props;
const totalUnread = _nbUnreadMessages + _nbUnreadPolls + _nbUnreadFiles;
const unreadMessagesNr = totalUnread > 0;
const isFocused = useIsFocused();
@@ -121,14 +143,14 @@ export default translate(connect(_mapStateToProps)((props: IProps) => {
activeUnreadNr = { unreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
nbUnread = { _nbUnreadMessages } />
nbUnread = { totalUnread } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages ]);
}, [ isFocused, _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles ]);
return (
<Chat { ...props } />

View File

@@ -10,7 +10,7 @@ import { arePollsDisabled } from '../../../conference/functions.any';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -70,9 +70,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
_isPollsDisabled: arePollsDisabled(state),
// The toggled icon should also be available for new polls
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state),
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state) || getUnreadFilesCount(state),
visible
};
}

View File

@@ -73,6 +73,11 @@ interface IProps extends AbstractProps {
*/
_isResizing: boolean;
/**
* Number of unread file sharing messages.
*/
_nbUnreadFiles: number;
/**
* Number of unread poll messages.
*/
@@ -218,6 +223,7 @@ const Chat = ({
_messages,
_nbUnreadMessages,
_nbUnreadPolls,
_nbUnreadFiles,
_showNamePrompt,
_width,
dispatch,
@@ -512,7 +518,7 @@ const Chat = ({
if (_isFileSharingTabEnabled) {
tabs.push({
accessibilityLabel: t('chat.tabs.fileSharing'),
countBadge: undefined,
countBadge: _focusedTab !== ChatTabs.FILE_SHARING && _nbUnreadFiles > 0 ? _nbUnreadFiles : undefined,
id: ChatTabs.FILE_SHARING,
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
icon: IconShareDoc,
@@ -586,13 +592,14 @@ const Chat = ({
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number,
* _showNamePrompt: boolean,
* _width: number,
* _isResizing: boolean
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat'];
const { isOpen, focusedTab, messages, nbUnreadMessages, nbUnreadFiles, width, isResizing } = state['features/chat'];
const { nbUnreadPolls } = state['features/polls'];
const _localParticipant = getLocalParticipant(state);
@@ -606,6 +613,7 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: nbUnreadPolls,
_nbUnreadFiles: nbUnreadFiles,
_showNamePrompt: !_localParticipant?.name,
_width: width?.current || CHAT_SIZE,
_isResizing: isResizing

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { getUnreadPollCount } from '../../../polls/functions';
import { getUnreadCount } from '../../functions';
import { getUnreadCount, getUnreadFilesCount } from '../../functions';
/**
* The type of the React {@code Component} props of {@link ChatCounter}.
@@ -65,7 +65,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_count: getUnreadCount(state) + getUnreadPollCount(state),
_count: getUnreadCount(state) + getUnreadPollCount(state) + getUnreadFilesCount(state),
_isOpen: isOpen
};

View File

@@ -131,6 +131,16 @@ export function getUnreadCount(state: IReduxState) {
return messagesCount - (lastReadIndex + 1) - reactionMessages;
}
/**
* Gets the unread files count.
*
* @param {IReduxState} state - The redux state.
* @returns {number} The number of unread files.
*/
export function getUnreadFilesCount(state: IReduxState): number {
return state['features/chat']?.nbUnreadFiles || 0;
}
/**
* Get whether the chat smileys are disabled or not.
*

View File

@@ -1,6 +1,7 @@
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
import { ILocalParticipant, IParticipant } from '../base/participants/types';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, _FILE_LIST_RECEIVED } from '../file-sharing/actionTypes';
import { IVisitorChatParticipant } from '../visitors/types';
import {
@@ -30,6 +31,7 @@ const DEFAULT_STATE = {
notifyPrivateRecipientsChangedTimestamp: undefined,
reactions: {},
nbUnreadMessages: 0,
nbUnreadFiles: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
@@ -53,6 +55,7 @@ export interface IChatState {
name: string;
} | ILocalParticipant;
messages: IMessage[];
nbUnreadFiles: number;
nbUnreadMessages: number;
notifyPrivateRecipientsChangedTimestamp?: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
@@ -235,7 +238,8 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages,
nbUnreadFiles: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.nbUnreadFiles
};
case SET_CHAT_WIDTH: {
@@ -271,6 +275,23 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
...state,
notifyPrivateRecipientsChangedTimestamp: action.payload
};
case ADD_FILE:
return {
...state,
nbUnreadFiles: action.shouldIncrementUnread ? state.nbUnreadFiles + 1 : state.nbUnreadFiles
};
case _FILE_LIST_RECEIVED: {
const remoteFilesCount = Object.values(action.files).filter(
(file: any) => file.authorParticipantId !== action.localParticipantId
).length;
return {
...state,
nbUnreadFiles: remoteFilesCount
};
}
}
return state;

View File

@@ -3,9 +3,7 @@ import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
Platform,
SafeAreaView,
StatusBar,
View,
ViewStyle
} from 'react-native';
@@ -16,8 +14,6 @@ import { appNavigate } from '../../../app/actions.native';
import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { isDisplayNameVisible } from '../../../base/config/functions.native';
import { FULLSCREEN_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import Container from '../../../base/react/components/native/Container';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import TintedView from '../../../base/react/components/native/TintedView';
@@ -96,11 +92,6 @@ interface IProps extends AbstractProps {
*/
_filmstripVisible: boolean;
/**
* The indicator which determines whether fullscreen (immersive) mode is enabled.
*/
_fullscreenEnabled: boolean;
/**
* The indicator which determines if the display name is visible.
*/
@@ -277,7 +268,6 @@ class Conference extends AbstractConference<IProps, State> {
override render() {
const {
_brandingStyles,
_fullscreenEnabled
} = this.props;
return (
@@ -287,13 +277,6 @@ class Conference extends AbstractConference<IProps, State> {
_brandingStyles
] }>
<BrandingImageBackground />
{
Platform.OS === 'android'
&& <StatusBar
barStyle = 'light-content'
hidden = { _fullscreenEnabled }
translucent = { _fullscreenEnabled } />
}
{ this._renderContent() }
</Container>
);
@@ -590,7 +573,6 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
_calendarEnabled: isCalendarEnabled(state),
_connecting: isConnecting(state),
_filmstripVisible: isFilmstripVisible(state),
_fullscreenEnabled: getFeatureFlag(state, FULLSCREEN_ENABLED, true),
_isDisplayNameVisible: isDisplayNameVisible(state),
_isParticipantsPaneOpen: isOpen,
_largeVideoParticipantId: state['features/large-video'].participantId,

View File

@@ -39,12 +39,14 @@ export function updateFileProgress(fileId: string, progress: number) {
* Add a file.
*
* @param {IFileMetadata} file - The file to add to the state.
* @param {boolean} shouldIncrementUnread - Whether to increment the unread count.
* @returns {Object}
*/
export function addFile(file: IFileMetadata) {
export function addFile(file: IFileMetadata, shouldIncrementUnread = false) {
return {
type: ADD_FILE,
file
file,
shouldIncrementUnread
};
}

View File

@@ -6,8 +6,10 @@ 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 { 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 { addFile, removeFile, updateFileProgress } from './actions';
@@ -23,12 +25,40 @@ import { downloadFile } from './utils';
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch }, previousConference) => {
(conference, { dispatch, getState }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
dispatch(addFile(file));
const state = getState();
const localParticipant = getLocalParticipant(state);
const isRemoteFile = file.authorParticipantId !== localParticipant?.id;
const { isOpen, focusedTab } = state['features/chat'];
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
dispatch(addFile(file, isRemoteFile && !isFileSharingTabVisible));
if (isRemoteFile && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.newFileNotification',
titleArguments: { participantName: file.authorParticipantName, fileName: file.fileName }
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
}
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
const { files } = state['features/file-sharing'];
const { isOpen, focusedTab } = state['features/chat'];
const removedFile = files.get(fileId);
const isFileSharingTabVisible = isOpen && focusedTab === ChatTabs.FILE_SHARING;
if (removedFile && removedFile.authorParticipantId === localParticipant?.id && !isFileSharingTabVisible) {
dispatch(showNotification({
titleKey: 'fileSharing.fileRemovedByOther',
titleArguments: { fileName: removedFile.fileName },
appearance: NOTIFICATION_TYPE.WARNING
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
}
dispatch({
type: _FILE_REMOVED,
fileId
@@ -36,9 +66,13 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
dispatch({
type: _FILE_LIST_RECEIVED,
files
files,
localParticipantId: localParticipant?.id
});
});
}
@@ -52,6 +86,17 @@ StateListenerRegistry.register(
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case I_AM_VISITOR_MODE: {
if (!action.iAmVisitor) {
const state = store.getState();
const conference = getCurrentConference(state);
conference?.getFileSharing()?.requestFileList?.();
}
return next(action);
}
case UPLOAD_FILES: {
const state = store.getState();
const conference = getCurrentConference(state);

View File

@@ -1,6 +1,11 @@
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { ADD_FILE, UPDATE_FILE_UPLOAD_PROGRESS, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
import {
ADD_FILE,
UPDATE_FILE_UPLOAD_PROGRESS,
_FILE_LIST_RECEIVED,
_FILE_REMOVED
} from './actionTypes';
import { IFileMetadata } from './types';
export interface IFileSharingState {
@@ -20,6 +25,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.set(action.file.fileId, action.file);
return {
...state,
files: newFiles
};
}
@@ -30,6 +36,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.delete(action.fileId);
return {
...state,
files: newFiles
};
}
@@ -43,12 +50,14 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
}
return {
...state,
files: newFiles
};
}
case _FILE_LIST_RECEIVED: {
return {
...state,
files: new Map(Object.entries(action.files))
};
}

View File

@@ -467,7 +467,7 @@ export function _mapStateToProps(state: IReduxState) {
_knocking: knocking,
_lobbyChatMessages: messages,
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
_login: showModeratorLogin,
_login: showModeratorLogin && !state['features/base/jwt'].jwt,
_hangUp: showHangUp,
_isLobbyChatActive: isLobbyChatActive,
_meetingName: getConferenceName(state),

View File

@@ -1,12 +0,0 @@
/**
* The type of (redux) action to set the react-native-immersive's change event
* subscription.
*
* {
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: Function
* }
*
* @protected
*/
export const _SET_IMMERSIVE_SUBSCRIPTION = '_SET_IMMERSIVE_SUBSCRIPTION';

View File

@@ -1,21 +0,0 @@
import { NativeEventSubscription } from 'react-native';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
/**
* Sets the change event listener to be used with react-native-immersive's API.
*
* @param {Function} subscription - The function to be used with
* react-native-immersive's API as the change event listener.
* @protected
* @returns {{
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: ?NativeEventSubscription
* }}
*/
export function _setImmersiveSubscription(subscription?: NativeEventSubscription) {
return {
type: _SET_IMMERSIVE_SUBSCRIPTION,
subscription
};
}

View File

@@ -1,22 +0,0 @@
import { IReduxState } from '../../app/types';
import { getCurrentConference } from '../../base/conference/functions';
import { isAnyDialogOpen } from '../../base/dialog/functions';
import { FULLSCREEN_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import { isLocalVideoTrackDesktop } from '../../base/tracks/functions.any';
/**
* Checks whether full-screen state should be used or not.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - Whether full-screen state should be used or not.
*/
export function shouldUseFullScreen(state: IReduxState) {
const { enabled: audioOnly } = state['features/base/audio-only'];
const conference = getCurrentConference(state);
const dialogOpen = isAnyDialogOpen(state);
const fullscreenEnabled = getFeatureFlag(state, FULLSCREEN_ENABLED, true);
const isDesktopSharing = isLocalVideoTrackDesktop(state);
return conference ? !audioOnly && !dialogOpen && !isDesktopSharing && fullscreenEnabled : false;
}

View File

@@ -1,3 +0,0 @@
import { getLogger } from '../../base/logging/functions';
export default getLogger('mobile-app:full-screen');

View File

@@ -1,102 +0,0 @@
import ImmersiveMode from 'react-native-immersive-mode';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
import { _setImmersiveSubscription } from './actions';
import { shouldUseFullScreen } from './functions';
import logger from './logger';
type BarVisibilityType = {
navigationBottomBar: boolean;
statusBar: boolean;
};
type ImmersiveListener = (visibility: BarVisibilityType) => void;
/**
* Middleware that captures conference actions and activates or deactivates the
* full screen mode. On iOS it hides the status bar, and on Android it uses the
* immersive mode:
* https://developer.android.com/training/system-ui/immersive.html
* In immersive mode the status and navigation bars are hidden and thus the
* entire screen will be covered by our application.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case APP_WILL_MOUNT: {
_setImmersiveListener(store, _onImmersiveChange.bind(undefined, store));
break;
}
case APP_WILL_UNMOUNT:
_setImmersiveListener(store, undefined);
break;
}
return next(action);
});
StateListenerRegistry.register(
/* selector */ shouldUseFullScreen,
/* listener */ fullScreen => _setFullScreen(fullScreen)
);
/**
* Handler for Immersive mode changes. This will be called when Android's
* immersive mode changes. This can happen without us wanting, so re-evaluate if
* immersive mode is desired and reactivate it if needed.
*
* @param {Object} store - The redux store.
* @private
* @returns {void}
*/
function _onImmersiveChange({ getState }: IStore) {
const state = getState();
const { appState } = state['features/mobile/background'];
if (appState === 'active') {
_setFullScreen(shouldUseFullScreen(state));
}
}
/**
* Activates/deactivates the full screen mode. On iOS it will hide the status
* bar, and on Android it will turn immersive mode on.
*
* @param {boolean} fullScreen - True to set full screen mode, false to
* deactivate it.
* @private
* @returns {void}
*/
function _setFullScreen(fullScreen: boolean) {
logger.info(`Setting full-screen mode: ${fullScreen}`);
ImmersiveMode.fullLayout(fullScreen);
ImmersiveMode.setBarMode(fullScreen ? 'Full' : 'Normal');
}
/**
* Notifies the feature filmstrip that the action
* {@link _SET_IMMERSIVE_LISTENER} is being dispatched within a specific redux
* store.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Function} listener - Listener for immersive state.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setImmersiveListener({ dispatch, getState }: IStore, listener?: ImmersiveListener) {
const { subscription } = getState()['features/full-screen'];
subscription?.remove();
dispatch(_setImmersiveSubscription(listener ? ImmersiveMode.addEventListener(listener) : undefined));
}

View File

@@ -1,21 +0,0 @@
import { NativeEventSubscription } from 'react-native';
import ReducerRegistry from '../../base/redux/ReducerRegistry';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
export interface IFullScreenState {
subscription?: NativeEventSubscription;
}
ReducerRegistry.register<IFullScreenState>('features/full-screen', (state = {}, action): IFullScreenState => {
switch (action.type) {
case _SET_IMMERSIVE_SUBSCRIPTION:
return {
...state,
subscription: action.subscription
};
}
return state;
});

View File

@@ -13,6 +13,7 @@ import { isInBreakoutRoom as isInBreakoutRoomF } from '../breakout-rooms/functio
import { isEnabled as isDropboxEnabled } from '../dropbox/functions';
import { extractFqnFromPath } from '../dynamic-branding/functions.any';
import { canAddTranscriber, isRecorderTranscriptionsRunning } from '../transcribing/functions';
import { iAmVisitor } from '../visitors/functions';
import LocalRecordingManager from './components/Recording/LocalRecordingManager';
import {
@@ -444,10 +445,11 @@ export function shouldRequireRecordingConsent(recorderSession: any, state: IRedu
= state['features/dynamic-branding'] || {};
const { conference } = state['features/base/conference'] || {};
const { requireConsent, skipConsentInMeeting } = state['features/base/config'].recordings || {};
const _iAmVisitor = iAmVisitor(state);
const { iAmRecorder, testing: { showSpotConsentDialog = false } = {} } = state['features/base/config'];
const { consentRequested } = state['features/recording'];
if (iAmRecorder) {
if (iAmRecorder || _iAmVisitor) {
return false;
}

View File

@@ -96,6 +96,11 @@ function normalizeCurrentLanguage(language: string) {
return;
}
// First check if the language code exists as-is (e.g., 'zh-CN', 'fr-CA')
if (LANGUAGES.includes(language)) {
return language;
}
const [ country, lang ] = language.split('-');
const jitsiNormalized = `${country}${lang ?? ''}`;

View File

@@ -11,6 +11,7 @@ import { getReceiverVideoQualityLevel } from '../video-quality/functions';
import { getMinHeightForQualityLvlMap } from '../video-quality/selector';
import { LAYOUTS } from './constants';
import logger from './logger';
/**
* A selector for retrieving the current automatic pinning setting.
@@ -113,39 +114,49 @@ export function shouldDisplayTileView(state: IReduxState) {
* Private helper to automatically pin the latest screen share stream or unpin
* if there are no more screen share streams.
*
* @param {Array<string>} screenShares - Array containing the list of all the screen sharing endpoints
* @param {Array<string>} previousScreenShares - Array containing the list of all the screen sharing endpoints
* before the update was triggered (including the ones that have been removed from redux because of the update).
* @param {Array<string>} currentScreenShares - Array containing the current list of screen sharing endpoints.
* @param {Store} store - The redux store.
* @returns {void}
*/
export function updateAutoPinnedParticipant(
screenShares: Array<string>, { dispatch, getState }: IStore) {
const state = getState();
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
previousScreenShares: Array<string>,
currentScreenShares: Array<string>,
{ dispatch, getState }: IStore) {
const pinned = getPinnedParticipant(getState);
// if the pinned participant is shared video or some other fake participant we want to skip auto-pinning
if (pinned?.fakeParticipant && pinned.fakeParticipant !== FakeParticipant.RemoteScreenShare) {
logger.debug('Skipping auto-pin: pinned participant is non-screenshare virtual participant', pinned.id);
return;
}
// Unpin the screen share when the screen sharing participant leaves. Switch to tile view if no other
// participant was pinned before screen share was auto-pinned, pin the previously pinned participant otherwise.
if (!remoteScreenShares?.length) {
if (!currentScreenShares?.length) {
let participantId = null;
if (pinned && !screenShares.find(share => share === pinned.id)) {
if (pinned && !previousScreenShares.find((share: string) => share === pinned.id)) {
participantId = pinned.id;
}
logger.debug('No more screenshares, unpinning or restoring previous pin', participantId);
dispatch(pinParticipant(participantId));
return;
}
const latestScreenShareParticipantId = remoteScreenShares[remoteScreenShares.length - 1];
const latestScreenShareParticipantId = currentScreenShares[currentScreenShares.length - 1];
if (latestScreenShareParticipantId) {
dispatch(pinParticipant(latestScreenShareParticipantId));
const alreadyPinned = pinned?.id === latestScreenShareParticipantId;
if (!alreadyPinned) {
logger.debug(`Auto pinning latest screen share participant: ${latestScreenShareParticipantId}`);
dispatch(pinParticipant(latestScreenShareParticipantId));
}
}
}

View File

@@ -12,7 +12,7 @@ import { isFollowMeActive } from '../follow-me/functions';
import { SET_TILE_VIEW } from './actionTypes';
import { setTileView } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
import logger from './logger';
import './subscriber';
let previousTileViewEnabled: boolean | undefined;
@@ -27,13 +27,21 @@ MiddlewareRegistry.register(store => next => action => {
// we want to extract the leaving participant and check its type before actually the participant being removed.
let shouldUpdateAutoPin = false;
let oldScreenShares: Array<string> = [];
switch (action.type) {
case PARTICIPANT_LEFT: {
if (!getAutoPinSetting() || isFollowMeActive(store)) {
logger.debug('Auto pinning is disabled or Follow Me is active, skipping auto pinning.');
break;
}
shouldUpdateAutoPin = Boolean(getParticipantById(store.getState(), action.participant.id)?.fakeParticipant);
if (shouldUpdateAutoPin) {
// Capture the old screenshare list before the reducer runs
oldScreenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
}
break;
}
}
@@ -75,9 +83,9 @@ MiddlewareRegistry.register(store => next => action => {
}
if (shouldUpdateAutoPin) {
const screenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
const newScreenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
updateAutoPinnedParticipant(screenShares, store);
updateAutoPinnedParticipant(oldScreenShares, newScreenShares, store);
}
return result;

View File

@@ -4,6 +4,7 @@ import { isFollowMeActive } from '../follow-me/functions';
import { virtualScreenshareParticipantsUpdated } from './actions';
import { getAutoPinSetting, updateAutoPinnedParticipant } from './functions';
import logger from './logger';
StateListenerRegistry.register(
/* selector */ state => state['features/base/participants'].sortedRemoteVirtualScreenshareParticipants,
@@ -22,14 +23,18 @@ StateListenerRegistry.register(
knownSharingParticipantIds.forEach(participantId => {
if (!newScreenSharesOrder.includes(participantId)) {
newScreenSharesOrder.push(participantId);
logger.debug('Adding new screenshare to list', participantId);
}
});
if (!equals(oldScreenSharesOrder, newScreenSharesOrder)) {
logger.debug('Screenshare order changed, dispatching update');
store.dispatch(virtualScreenshareParticipantsUpdated(newScreenSharesOrder));
if (getAutoPinSetting() && !isFollowMeActive(store)) {
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
updateAutoPinnedParticipant(oldScreenSharesOrder, newScreenSharesOrder, store);
} else {
logger.debug('Auto pinning is disabled or Follow Me is active, skipping auto pinning of screenshare.');
}
}
});

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

@@ -0,0 +1,58 @@
-- Module to be enabled under main muc component
-- Clean up the room in case it is empty and has only jibri and jigasi-transcriber left in the meeting
local util = module:require 'util';
local is_admin = util.is_admin;
local is_transcriber = util.is_transcriber;
local is_jibri = util.is_jibri;
local EMPTY_TIMEOUT = module:get_option_number('services_empty_meeting_timeout', 20);
module:hook('muc-occupant-joined', function (event)
local room = event.room;
local occupant = event.occupant;
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
return;
end
-- clear the timer when someone joins
if not is_jibri(occupant.jid) and not is_transcriber(occupant.jid) and room.empty_destroy_timer then
room.empty_destroy_timer:stop();
room.empty_destroy_timer = nil;
end
end, -100); -- make sure we are last in the chain
module:hook('muc-occupant-left', function (event)
local occupant, room = event.occupant, event.room;
if is_admin(occupant.bare_jid) or is_jibri(occupant.jid) or is_transcriber(occupant.jid) then
return;
end
for _, o in room:each_occupant() do
if not is_jibri(o.jid) and not is_transcriber(o.jid)
and not is_admin(o.bare_jid) then
-- not empty
return;
end
end
-- seems the room only has jibri and transcriber, add a timeout to destroy the room
room.empty_destroy_timer = module:add_timer(EMPTY_TIMEOUT, function()
room:destroy(nil, 'Empty room with recording and/or transcribing.');
module:log('info',
'the conference terminated %s as being empty for %s seconds with recording/transcribing enabled',
room.jid, EMPTY_TIMEOUT);
end)
end, -100); -- the last thing to execute
module:hook('muc-room-destroyed', function (event)
local room = event.room;
if room.empty_destroy_timer then
room.empty_destroy_timer:stop();
room.empty_destroy_timer = nil;
end
end);

View File

@@ -6,6 +6,7 @@ module:set_global();
local util = module:require "util";
local async_handler_wrapper = util.async_handler_wrapper;
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
local room_jid_match_rewrite = util.room_jid_match_rewrite;
local get_room_from_jid = util.get_room_from_jid;
local starts_with = util.starts_with;
@@ -17,6 +18,8 @@ local parse = neturl.parseQuery;
local token_util;
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
local muc_domain = muc_domain_prefix..'.'..muc_domain_base;
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
@@ -47,6 +50,7 @@ function handle_terminate_meeting (event)
end
local params = parse(event.request.url.query);
local conference = params["conference"];
local silent_reconnect = params['silent-reconnect'];
local room_jid;
if conference then
@@ -78,8 +82,13 @@ function handle_terminate_meeting (event)
module:log("warn", "Room not found")
return { status_code = 404 };
else
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
if silent_reconnect == 'true' then
module:log('info', 'Setting silent_reconnect on room %s', room.jid);
room:destroy(internal_room_jid_match_rewrite(room.jid), 'The meeting has been terminated silently')
else
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
end
end
event_count_success()
return { status_code = 200 };

View File

@@ -48,6 +48,7 @@ local NOTIFY_LOBBY_ACCESS_DENIED = 'LOBBY-ACCESS-DENIED';
local util = module:require "util";
local ends_with = util.ends_with;
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local presence_check_status = util.presence_check_status;
local process_host_module = util.process_host_module;
@@ -105,7 +106,7 @@ end
-- Sends a json message notifying that the jid was granted/denied access in lobby
-- the message from is the actor that did the operation
function notify_lobby_access(room, actor, jid, display_name, granted)
function notify_lobby_access(room_jid, actor, jid, display_name, granted)
local notify_json = {
value = jid,
name = display_name
@@ -116,6 +117,12 @@ function notify_lobby_access(room, actor, jid, display_name, granted)
notify_json.event = NOTIFY_LOBBY_ACCESS_DENIED;
end
local room = get_room_from_jid(room_jid);
if not room then
module:log('error', 'Room not found for %s', room_jid)
return;
end
broadcast_json_msg(room, actor, notify_json);
end
@@ -227,7 +234,7 @@ function attach_lobby_room(room, actor)
-- avoid lobby destroy while it is enabled
new_room:set_persistent(true);
module:log("info","Lobby room jid = %s created from:%s", lobby_room_jid, actor);
new_room.main_room = room;
new_room.main_room_jid = room.jid;
room._data.lobbyroom = new_room.jid;
room:save(true);
return true
@@ -245,6 +252,13 @@ function destroy_lobby_room(room, newjid, message)
if lobby_room_obj then
lobby_room_obj:set_persistent(false);
lobby_room_obj:destroy(newjid, message);
module:log('info', 'Lobby room destroyed %s', lobby_room_obj.jid)
if room.jitsiMetadata then
room.jitsiMetadata.lobbyEnabled = false;
module:context(main_muc_component_config):fire_event('room-metadata-changed', { room = room; });
end
end
room._data.lobbyroom = nil;
room._data.lobby_extra_reason = nil;
@@ -412,13 +426,18 @@ function process_lobby_muc_loaded(lobby_muc, host_module)
local room_mt = lobby_muc_service.room_mt;
-- we base affiliations (roles) in lobby muc component to be based on the roles in the main muc
room_mt.get_affiliation = function(room, jid)
if not room.main_room then
if not room.main_room_jid then
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
return 'none';
end
-- moderators in main room are moderators here
local role = room.main_room.get_affiliation(room.main_room, jid);
local main_room = get_room_from_jid(room.main_room_jid);
if not main_room then
module:log('error', 'Main room not found for %s!', room.main_room_jid);
return 'none';
end
local role = main_room.get_affiliation(main_room, jid);
if role then
return role;
end
@@ -433,7 +452,7 @@ function process_lobby_muc_loaded(lobby_muc, host_module)
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
-- we need to notify in the main room
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
notify_lobby_access(room.main_room_jid, actor, occupant.nick, display_name, false);
end
end);
end
@@ -474,9 +493,16 @@ process_host_module(main_muc_component_config, function(host_module, host)
module:fire_event('jitsi-lobby-enabled', { room = room; });
event.status_codes['104'] = true;
notify_lobby_enabled(room, actor, true);
-- let's set it in the metadata and fire the event
if not room.jitsiMetadata then
room.jitsiMetadata = {};
end
room.jitsiMetadata.lobbyEnabled = true;
host_module:fire_event('room-metadata-changed', { room = room; });
end
elseif room._data.lobbyroom then
destroy_lobby_room(room, room.jid);
destroy_lobby_room(room, room.jid, nil);
module:fire_event('jitsi-lobby-disabled', { room = room; });
notify_lobby_enabled(room, actor, false);
end
@@ -589,7 +615,7 @@ process_host_module(main_muc_component_config, function(host_module, host)
local display_name = occupant:get_presence():get_child_text(
'nick', 'http://jabber.org/protocol/nick');
notify_lobby_access(room, from, occupant.nick, display_name, true);
notify_lobby_access(room.jid, from, occupant.nick, display_name, true);
end
end
end
@@ -635,6 +661,13 @@ function handle_create_lobby(event)
room._data.lobby_extra_reason = event.reason;
room._data.lobby_skip_display_name_check = event.skip_display_name_check;
-- set in metadata without firing room-metadata-changed,
-- as this is a backend call and the caller will take care of that
if not room.jitsiMetadata then
room.jitsiMetadata = {};
end
room.jitsiMetadata.lobbyEnabled = true;
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
room:broadcast_message(
st.message({ type='groupchat', from=room.jid })

View File

@@ -10,6 +10,8 @@ local is_healthcheck_room = main_util.is_healthcheck_room;
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
local presence_check_status = main_util.presence_check_status;
local extract_subdomain = main_util.extract_subdomain;
local util = module:require 'util';
local is_transcriber = util.is_transcriber;
local QUEUE_MAX_SIZE = 500;
@@ -158,7 +160,7 @@ module:hook("muc-occupant-groupchat", function(event)
event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
end, 45); -- prosody check is prio 50, we want to run after it
module:hook('message/bare', function(event)
local function filterTranscriptionResult(event)
local stanza = event.stanza;
if stanza.attr.type ~= 'groupchat' then
@@ -190,12 +192,6 @@ module:hook('message/bare', function(event)
return;
end
-- TODO: add optimization by moving type and certain fields like is_interim as attribute on 'json-message'
-- using string find is roughly 70x faster than json decode for checking the value
if string.find(json_message, '"is_interim":true', 1, true) then
return;
end
local msg_obj, error = json.decode(json_message);
if error then
@@ -203,6 +199,18 @@ module:hook('message/bare', function(event)
return true;
end
if msg_obj.type == 'transcription-result' then
if not is_transcriber(stanza.attr.from) then
module:log('warn', 'Filtering transcription-result message from non-transcriber: %s', stanza.attr.from);
-- Do not fire the event, and do not forward the message
return true
end
if msg_obj.is_interim then
-- Do not fire the event, but forward the message
return
end
end
if msg_obj.transcript ~= nil then
local transcription = msg_obj;
@@ -240,4 +248,7 @@ module:hook('message/bare', function(event)
room = room, occupant = occupant, message = msg_obj,
origin = event.origin,
stanza = stanza, raw_message = json_message });
end);
end
module:hook('message/bare', filterTranscriptionResult);
module:hook('jitsi-visitor-groupchat-pre-route', filterTranscriptionResult);

View File

@@ -108,7 +108,13 @@ module:hook("muc-occupant-pre-join", function (event)
if not room.join_rate_queue_timer then
timer.add_task(1, function ()
local status, result = pcall(timer_process_queue_elements,
if room.destroying then
-- if room was destroyed in the mean time, ignore
return;
end
local status, result = pcall(
timer_process_queue_elements,
join_rate_per_conference,
room.join_rate_presence_queue,
function(ev)

View File

@@ -11,6 +11,7 @@
module:depends('room_destroy');
local util = module:require "util";
local get_room_from_jid = util.get_room_from_jid;
local is_healthcheck_room = util.is_healthcheck_room;
local main_muc_component_host = module:get_option_string('main_muc');
local lobby_muc_component_host = module:get_option_string('lobby_muc');
@@ -110,7 +111,8 @@ run_when_component_loaded(main_muc_component_host, function(host_module, host_na
-- Check if room should be destroyed when someone leaves the main room
local main_room = event.room;
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room)
or main_room.destroying then
return;
end
@@ -144,9 +146,13 @@ run_when_component_loaded(lobby_muc_component_host, function(host_module, host_n
lobby_module:hook("muc-occupant-left", function(event)
-- Check if room should be destroyed when someone leaves the lobby
local lobby_room = event.room;
local main_room = lobby_room.main_room;
if not lobby_room.main_room_jid then
return;
end
local main_room = get_room_from_jid(lobby_room.main_room_jid);
if not main_room or is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
return;

View File

@@ -54,13 +54,18 @@ function generateToken(session, audience, room, occupant)
local exp = t + options.ttl_seconds;
local presence = occupant:get_presence(session.full_jid);
local _, _, id = extract_subdomain(jid.node(room.jid));
local sub = module.host;
if session.jitsi_web_query_prefix and session.jitsi_web_query_prefix ~= '' then
sub = session.jitsi_web_query_prefix;
end
local payload = {
iss = options.issuer,
aud = audience,
nbf = t,
exp = exp,
sub = session.jitsi_web_query_prefix or module.host,
sub = sub,
context = {
group = session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id,
user = session.jitsi_meet_context_user or {

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

@@ -565,6 +565,10 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
elseif room._data.participants then
-- This is non jaas room which has a list of participants allowed to participate in the main room
-- but this occupant is not one of them and the room is either not live or has no participants joined
if room:get_members_only() then
-- if there is a lobby, let's pass it through it will wait for the main participant
return;
end
session.log('warn',
'Deny user join in the main not live meeting, not in the list of main participants');
session.send(st.error_reply(
@@ -700,6 +704,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
return;
end
if always_visitors_enabled then
if not room.jitsiMetadata then
room.jitsiMetadata = {};
end
room.jitsiMetadata.visitorsEnabled = true;
end
go_live(room);
end);
end

View File

@@ -191,8 +191,8 @@ export class Participant {
* @param {string} message - The message to log.
* @returns {void}
*/
log(message: string): void {
logInfo(this.driver, message);
async log(message: string): Promise<void> {
await logInfo(this.driver, message);
}
/**
@@ -678,32 +678,34 @@ export class Participant {
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
*/
async hangup() {
const current = await this.driver.getUrl();
console.log('Hanging up');
if ((await this.driver.getUrl()).endsWith('/base.html')) {
console.log('Already hung up');
// already hangup
if (current.endsWith('/base.html')) {
return;
}
// do a hangup, to make sure unavailable presence is sent
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
// @ts-ignore
await this.execute(() => window.APP?.conference?.hangup());
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
// Wait until _room is unset, which is one of the last things hangup() does.
await this.driver.waitUntil(
async () => {
const u = await this.driver.getUrl();
// trying to debug some failures of reporting not leaving, where we see the close page in screenshot
console.log(`initialUrl: ${current} currentUrl: ${u}`);
return current !== u;
try {
// @ts-ignore
return await this.driver.execute(() => window.APP?.conference?._room === undefined);
} catch (e) {
// There seems to be a race condition with hangup() causing the page to change, and execute()
// might fail with a Bidi error. Retry.
return false;
}
},
{
timeout: 8000,
timeoutMsg: `${this.name} did not leave the muc in 8s initialUrl: ${current}`
timeoutMsg: `${this.name} failed to hang up`
}
);
console.log('Hung up');
await this.driver.url('/base.html')

View File

@@ -2,44 +2,26 @@
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
*/
export type ITestProperties = {
/**
* A more detailed description of what the test does, to be included in the Allure report.
*/
description?: string;
/** The test requires the webhook proxy to be available. */
requireWebhookProxy: boolean;
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
useJaas: boolean;
/** The test requires the webhook proxy. */
/** The test uses the webhook proxy if available. */
useWebhookProxy: boolean;
usesBrowsers?: string[];
usesBrowsers: string[];
};
const defaultProperties: ITestProperties = {
useWebhookProxy: false,
requireWebhookProxy: false,
useJaas: false,
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
usesBrowsers: [ 'p1' ]
};
function getDefaultProperties(filename: string): ITestProperties {
const properties = { ...defaultProperties };
properties.usesBrowsers = getDefaultBrowsers(filename);
return properties;
}
function getDefaultBrowsers(filename: string): string[] {
if (filename.includes('/alone/')) {
return [ 'p1' ];
}
if (filename.includes('/2way/')) {
return [ 'p1', 'p2' ];
}
if (filename.includes('/3way/')) {
return [ 'p1', 'p2', 'p3' ];
}
if (filename.includes('/4way/')) {
return [ 'p1', 'p2', 'p3', 'p4' ];
}
// Tests outside /alone/, /2way/, /3way/, /4way/ will default to p1 only.
return [ 'p1' ];
}
/**
* Maps a test filename to its registered properties.
@@ -59,7 +41,7 @@ export function setTestProperties(filename: string, properties: Partial<ITestPro
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
}
testProperties[filename] = { ...getDefaultProperties(filename), ...properties };
testProperties[filename] = { ...defaultProperties, ...properties };
}
let testFilesLoaded = false;
@@ -97,7 +79,7 @@ export function loadTestFiles(files: string[]): void {
require(file);
if (!testProperties[file]) {
// If no properties were set, apply defaults
setTestProperties(file, getDefaultProperties(file));
setTestProperties(file, { ...defaultProperties });
}
} catch (error) {
console.warn(`Warning: Could not analyze ${file}:`, (error as Error).message);
@@ -126,5 +108,11 @@ export function loadTestFiles(files: string[]): void {
* @returns Promise<ITestProperties> - The test properties with defaults applied
*/
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
return testProperties[testFilePath] || getDefaultProperties(testFilePath);
const properties = testProperties[testFilePath] || { ...defaultProperties };
if (properties.requireWebhookProxy && !properties.useWebhookProxy) {
properties.useWebhookProxy = true;
}
return properties;
}

View File

@@ -4,18 +4,7 @@
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
/** Whether to expect the environment to automatically elect a new moderator when the existing moderator leaves. */
autoModerator: (() => {
if (typeof process.env.AUTO_MODERATOR !== 'undefined') {
return process.env.AUTO_MODERATOR?.trim() === 'true';
}
// If not explicitly configured, fallback to recognizing whether we're running against one of the JaaS
// environments which are known to have the setting disabled.
return !Boolean(
process.env.JWT_PRIVATE_KEY_PATH && process.env.JWT_KID?.startsWith('vpaas-magic-cookie-')
);
})(),
expectationsFile: process.env.EXPECTATIONS?.trim(),
jaas: {
customerId: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {

View File

@@ -45,13 +45,11 @@ export function getLogs(driver: WebdriverIO.Browser) {
}
/**
* Logs a message in the logfile.
*
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
* @param {string} message - The message to add.
* @returns {void}
* Appends value to the log file.
* @param {WebdriverIO.Browser} driver - The driver which log file is requested.
* @param {string} value - The content to add to the file.
*/
export function logInfo(driver: WebdriverIO.Browser, message: string) {
export function saveLogs(driver: WebdriverIO.Browser, value: string) {
// @ts-ignore
if (!driver.logFile) {
return;
@@ -59,9 +57,27 @@ export function logInfo(driver: WebdriverIO.Browser, message: string) {
try {
// @ts-ignore
fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
fs.appendFileSync(driver.logFile, value);
} catch (err) {
console.error(err);
}
}
/**
* Logs a message in the logfile.
*
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
* @param {string} message - The message to add.
* @returns {void}
*/
export async function logInfo(driver: WebdriverIO.Browser, message: string) {
// @ts-ignore
if (!driver.logFile) {
return;
}
return driver.execute((prefix, msg) =>
console.log(`${new Date().toISOString()} ${prefix} ${msg}\n`),
LOG_PREFIX, message);
}

View File

@@ -0,0 +1,57 @@
import fs from 'fs';
import { merge } from 'lodash-es';
import { config } from './TestsConfig';
const defaultExpectations = {
dialIn: {
/*
* The dial-in functionality is enabled.
* true -> assert the config is enabled, the UI elements are displayed, and the feature works.
* false -> assert the config is disabled and the UI elements are not displayed.
* null -> if the config is enabled, assert the UI elements are displayed and the feature works.
*/
enabled: null,
},
jaas: {
/**
* Whether the jaas account is configured with the account-level setting to allow unauthenticated users to join.
*/
unauthenticatedJoins: false
},
moderation: {
// Everyone is a moderator.
allModerators: false,
// When a moderator leaves, another one is elected.
autoModerator: true,
// The first to join is a moderator.
firstModerator: true,
// The grantOwner function is available.
grantModerator: true,
// Whether the ability to set a password is available (there's a backend options which makes moderators unable
// to set a room password unless they also happen to have a token (any valid token?))
setPasswordAvailable: true
},
// We can create conferences under any tenant.
useTenant: true
};
let overrides: any = {};
if (config.expectationsFile) {
try {
const str = fs.readFileSync(config.expectationsFile, 'utf8');
// Remove comments and multiline comments.
overrides = JSON.parse(str.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, ''));
} catch (e) {
console.error('Error reading expectations file', e);
}
console.log('Loaded expectations from', config.expectationsFile);
}
export const expectations = merge(defaultExpectations, overrides);
if (!process.env.WDIO_WORKER_ID) {
console.log('Expectations:', expectations);
}

View File

@@ -38,3 +38,13 @@ export async function joinMuc(
roomName: joinOptions?.roomName || ctx.roomName,
});
}
/**
* Wait until all participants have ICE connected and have sent and received data (their PC stats are ready).
* @param participants
*/
export async function waitForMedia(participants: Participant[]) {
await Promise.all(participants.map(p =>
p.waitForIceConnected().then(() => p.waitForSendReceiveData())
));
}

View File

@@ -18,8 +18,9 @@ export type IContext = {
roomName: string;
/**
* A flag that tests can set, which signals to the framework that the (rest of the) test suite should be skipped.
* A string value indicates the reason for the skipped (to be included in the Allure report).
*/
skipSuiteTests: boolean;
skipSuiteTests: boolean | string;
/**
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
* set up the context appropriately.

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();
@@ -153,7 +153,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();
@@ -179,7 +179,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 +189,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,8 +204,9 @@ 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.$(`div=${roomName}`);
await sendButton.scrollIntoView();
await sendButton.waitForClickable();
await sendButton.click();
}

View File

@@ -49,7 +49,7 @@ export default class ChatPanel extends BasePageObject {
}
async clickCreatePollButton() {
await this.participant.driver.$('aria/Create a poll').click();
await this.participant.driver.$('button=Create a poll').click();
}
/**
@@ -95,7 +95,7 @@ export default class ChatPanel extends BasePageObject {
* Clicks the "Add option" button.
*/
async clickAddOptionButton() {
await this.participant.driver.$('aria/Add option').click();
await this.participant.driver.$('button=Add option').click();
}
/**
@@ -141,35 +141,35 @@ export default class ChatPanel extends BasePageObject {
* Clicks the "Save" button.
*/
async clickSavePollButton() {
await this.participant.driver.$('aria/Save').click();
await this.participant.driver.$('button=Save').click();
}
/**
* Clicks the "Edit" button.
*/
async clickEditPollButton() {
await this.participant.driver.$('aria/Edit').click();
await this.participant.driver.$('button=Edit').click();
}
/**
* Clicks the "Skip" button.
*/
async clickSkipPollButton() {
await this.participant.driver.$('aria/Skip').click();
await this.participant.driver.$('button=Skip').click();
}
/**
* Clicks the "Send" button.
*/
async clickSendPollButton() {
await this.participant.driver.$('aria/Send poll').click();
await this.participant.driver.$('button=Send').click();
}
/**
* Waits for the "Send" button to be visible.
*/
async waitForSendButton() {
await this.participant.driver.$('aria/Send poll').waitForExist({
await this.participant.driver.$('button=Send').waitForExist({
timeout: 1000,
timeoutMsg: 'Send button not visible'
});
@@ -185,7 +185,7 @@ export default class ChatPanel extends BasePageObject {
(id, ix) => document.getElementById(`poll-answer-checkbox-${id}-${ix}`)?.click(),
pollId, index);
await this.participant.driver.$('aria/Submit').click();
await this.participant.driver.$('button=Submit').click();
}
/**

View File

@@ -5,7 +5,7 @@ import BasePageObject from './BasePageObject';
const LOCAL_VIDEO_XPATH = '//span[@id="localVideoContainer"]';
const LOCAL_VIDEO_MENU_TRIGGER = '#local-video-menu-trigger';
const LOCAL_USER_CONTROLS = 'aria/Local user controls';
const LOCAL_USER_CONTROLS = 'button[title="Local user controls"]';
const HIDE_SELF_VIEW_BUTTON_XPATH = '//div[contains(@class, "popover")]//div[@id="hideselfviewButton"]';
/**

View File

@@ -51,7 +51,7 @@ export default class InviteDialog extends BaseDialog {
const fullText = await elem.getText();
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
await this.participant.log(`Extracted text in invite dialog: ${fullText}`);
return fullText.split(':')[1].trim();
}

View File

@@ -106,17 +106,12 @@ export default class Notifications extends BasePageObject {
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
if (skipNonExisting && !await notification.isExisting()) {
if (skipNonExisting && !await closeButton.isExisting()) {
return Promise.resolve();
}
await notification.waitForExist();
await notification.waitForStable();
const closeButton = notification.$('#close-notification');
await closeButton.moveTo();
await closeButton.click();
}

View File

@@ -214,7 +214,7 @@ export default class ParticipantsPane extends BasePageObject {
await this.open();
}
const inviteButton = this.participant.driver.$(`aria/${INVITE}`);
const inviteButton = this.participant.driver.$(`button=${INVITE}`);
await inviteButton.waitForDisplayed();
await inviteButton.click();
@@ -262,7 +262,7 @@ export default class ParticipantsPane extends BasePageObject {
.substring('participant-item-'.length);
const moreOptionsButton
= this.participant.driver.$(`aria/More moderation options ${participantNameToReject}`);
= this.participant.driver.$(`button[title="More moderation options ${participantNameToReject}"]`);
await moreOptionsButton.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({ timeout: 2000 });
await passwordInput.click();
await passwordInput.clearValue();

View File

@@ -33,7 +33,7 @@ export default class Toolbar extends BasePageObject {
* @private
*/
private getButton(accessibilityCSSSelector: string) {
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
return this.participant.driver.$(`[aria-label="${accessibilityCSSSelector}"]`);
}
/**
@@ -55,8 +55,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickAudioMuteButton(): Promise<void> {
this.participant.log('Clicking on: Audio Mute Button');
async clickAudioMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Mute Button');
return this.audioMuteBtn.click();
}
@@ -66,8 +66,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickAudioUnmuteButton(): Promise<void> {
this.participant.log('Clicking on: Audio Unmute Button');
async clickAudioUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Unmute Button');
return this.audioUnMuteBtn.click();
}
@@ -91,8 +91,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickVideoMuteButton(): Promise<void> {
this.participant.log('Clicking on: Video Mute Button');
async clickVideoMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Mute Button');
return this.videoMuteBtn.click();
}
@@ -102,8 +102,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickVideoUnmuteButton(): Promise<void> {
this.participant.log('Clicking on: Video Unmute Button');
async clickVideoUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Unmute Button');
return this.videoUnMuteBtn.click();
}
@@ -113,8 +113,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickCloseParticipantsPaneButton(): Promise<void> {
this.participant.log('Clicking on: Close Participants pane Button');
async clickCloseParticipantsPaneButton(): Promise<void> {
await this.participant.log('Clicking on: Close Participants pane Button');
return this.getButton(CLOSE_PARTICIPANTS_PANE).click();
}
@@ -124,8 +124,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
clickParticipantsPaneButton(): Promise<void> {
this.participant.log('Clicking on: Participants pane Button');
async clickParticipantsPaneButton(): Promise<void> {
await this.participant.log('Clicking on: Participants pane Button');
// Special case for participants pane button, as it contains the number of participants and its label
// is changing
@@ -150,8 +150,8 @@ export default class Toolbar extends BasePageObject {
/**
* Clicks on the raise hand button that enables participants will to speak.
*/
clickRaiseHandButton(): Promise<void> {
this.participant.log('Clicking on: Raise hand Button');
async clickRaiseHandButton(): Promise<void> {
await this.participant.log('Clicking on: Raise hand Button');
return this.getButton(RAISE_HAND).click();
}
@@ -159,8 +159,8 @@ export default class Toolbar extends BasePageObject {
/**
* Clicks on the chat button that opens chat panel.
*/
clickChatButton(): Promise<void> {
this.participant.log('Clicking on: Chat Button');
async clickChatButton(): Promise<void> {
await this.participant.log('Clicking on: Chat Button');
return this.getButton(CHAT).click();
}
@@ -168,8 +168,8 @@ export default class Toolbar extends BasePageObject {
/**
* Clicks on the chat button that closes chat panel.
*/
clickCloseChatButton(): Promise<void> {
this.participant.log('Clicking on: Close Chat Button');
async clickCloseChatButton(): Promise<void> {
await this.participant.log('Clicking on: Close Chat Button');
return this.getButton(CLOSE_CHAT).click();
}
@@ -205,8 +205,8 @@ export default class Toolbar extends BasePageObject {
/**
* Clicks on the hangup button that ends the conference.
*/
clickHangupButton(): Promise<void> {
this.participant.log('Clicking on: Hangup Button');
async clickHangupButton(): Promise<void> {
await this.participant.log('Clicking on: Hangup Button');
return this.getButton(HANGUP).click();
}
@@ -237,7 +237,7 @@ export default class Toolbar extends BasePageObject {
// so let's move focus away before clicking the button
await this.participant.driver.$('#overflow-context-menu').moveTo();
this.participant.log(`Clicking on: ${accessibilityLabel}`);
await this.participant.log(`Clicking on: ${accessibilityLabel}`);
await this.getButton(accessibilityLabel).click();
await this.closeOverflowMenu();
@@ -248,7 +248,7 @@ export default class Toolbar extends BasePageObject {
* @private
*/
private async isOverflowMenuOpen() {
return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
return await this.participant.driver.$$(`[aria-label="${OVERFLOW_MENU}"]`).length > 0;
}
/**

View File

@@ -8,7 +8,7 @@ export default class Visitors extends BasePageObject {
* Returns the visitors dialog element if any.
*/
hasVisitorsDialog() {
return this.participant.driver.$('aria/Joining meeting');
return this.participant.driver.$('div=Joining meeting');
}
/**

View File

@@ -1,52 +0,0 @@
import process from 'node:process';
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Fake Dial-In', () => {
it('join participant', async () => {
// we execute fake dial in only if the real dial in is not enabled
// check rest url is not configured
if (process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant();
// check dial-in is enabled, so skip
if (await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});
it('open invite dialog', async () => {
await ctx.p1.getInviteDialog().open();
await ctx.p1.getInviteDialog().clickCloseButton();
});
it('invite second participant', async () => {
if (!await ctx.p1.isInMuc()) {
// local participant did not join abort
return;
}
await ensureTwoParticipants();
});
it('wait for audio from second participant', async () => {
const { p1 } = ctx;
if (!await p1.isInMuc()) {
// local participant did not join abort
return;
}
await waitForAudioFromDialInParticipant(p1);
await cleanup(p1);
});
});

View File

@@ -1,42 +0,0 @@
import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
describe('Grant moderator', () => {
it('joining the meeting', async () => {
await ensureOneParticipant();
if (await ctx.p1.execute(() => typeof APP.conference._room.grantOwner !== 'function')) {
ctx.skipSuiteTests = true;
return;
}
await ensureTwoParticipants();
});
it('grant moderator and validate', async () => {
const { p1, p2 } = ctx;
if (!await p1.isModerator()) {
ctx.skipSuiteTests = true;
return;
}
if (await p2.isModerator()) {
ctx.skipSuiteTests = true;
return;
}
await p1.getFilmstrip().grantModerator(p2);
await p2.driver.waitUntil(
() => p2.isModerator(),
{
timeout: 3000,
timeoutMsg: 'p2 did not become moderator'
}
);
});
});

View File

@@ -1,44 +0,0 @@
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Kick', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants();
if (!await ctx.p1.isModerator()) {
ctx.skipSuiteTests = true;
}
});
it('kick and check', () => kickParticipant2AndCheck());
it('kick p2p and check', async () => {
await ensureTwoParticipants({
configOverwrite: {
p2p: {
enabled: true
}
}
});
await kickParticipant2AndCheck();
});
});
/**
* Kicks the second participant and checks that the participant is removed from the conference and that dialog is open.
*/
async function kickParticipant2AndCheck() {
const { p1, p2 } = ctx;
await p1.getFilmstrip().kickParticipant(await p2.getEndpointId());
await p1.waitForParticipants(0);
// check that the kicked participant sees the kick reason dialog
// let's wait for this to appear at least 2 seconds
await p2.driver.waitUntil(
async () => p2.isLeaveReasonDialogOpen(), {
timeout: 2000,
timeoutMsg: 'No leave reason dialog shown for p2'
});
}

View File

@@ -1,29 +0,0 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('Single port', () => {
it('joining the meeting', () => ensureTwoParticipants());
it('test', async () => {
const { p1, p2 } = ctx;
const port1 = await getRemotePort(p1);
const port2 = await getRemotePort(p2);
expect(Number.isInteger(port1)).toBe(true);
expect(Number.isInteger(port2)).toBe(true);
expect(port1).toBe(port2);
});
});
/**
* Get the remote port of the participant.
* @param participant
*/
async function getRemotePort(participant: Participant) {
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.ip);
const parts = data.split(':');
return parts.length > 1 ? parseInt(parts[1], 10) : '';
}

View File

@@ -1,32 +0,0 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
const MY_TEST_SUBJECT = 'My Test Subject';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
describe('Subject', () => {
it('joining the meeting', () => ensureTwoParticipants({
configOverwrite: {
subject: MY_TEST_SUBJECT
}
}));
it('check', async () => {
await checkSubject(ctx.p1, MY_TEST_SUBJECT);
await checkSubject(ctx.p2, MY_TEST_SUBJECT);
});
});
/**
* Check was subject set.
*
* @param participant
* @param subject
*/
async function checkSubject(participant: Participant, subject: string) {
const localTile = participant.driver.$(SUBJECT_XPATH);
await localTile.moveTo();
expect(await localTile.getText()).toBe(subject);
}

View File

@@ -1,26 +0,0 @@
import type { Participant } from '../../helpers/Participant';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('UDP', () => {
it('joining the meeting', () => ensureTwoParticipants());
it('check', async () => {
const { p1, p2 } = ctx;
// just in case wait 1500, this is the interval we use for `config.pcStatsInterval`
await p1.driver.pause(1500);
expect(await getProtocol(p1)).toBe('udp');
expect(await getProtocol(p2)).toBe('udp');
});
});
/**
* Get the remote port of the participant.
* @param participant
*/
async function getProtocol(participant: Participant) {
const data = await participant.execute(() => APP?.conference?.getStats()?.transport[0]?.type);
return data.toLowerCase();
}

View File

@@ -1,41 +0,0 @@
import { multiremotebrowser } from '@wdio/globals';
import { config } from '../../helpers/TestsConfig';
import { ensureTwoParticipants } from '../../helpers/participants';
describe('URL Normalisation', () => {
it('joining the meeting', async () => {
// if we are running with token this becomes ugly to match the URL
if (config.jwt.preconfiguredToken) {
ctx.skipSuiteTests = true;
return;
}
// a hack to extract the baseUrl that the test will use
const baseUrl = multiremotebrowser.getInstance('p1').options.baseUrl;
if (!baseUrl) {
throw new Error('baseUrl is not set');
}
await ensureTwoParticipants({
tenant: 'tenant@example.com',
roomName: `${ctx.roomName}@example.com`
});
});
it('check', async () => {
const currentUrlStr = await ctx.p1.driver.getUrl();
const currentUrl = new URL(currentUrlStr);
const path = currentUrl.pathname;
const parts = path.split('/');
expect(parts[1]).toBe('tenantexample.com');
// @ts-ignore
expect(parts[2]).toBe(`${ctx.roomName}example.com`);
});
});

View File

@@ -1,69 +0,0 @@
import process from 'node:process';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import { cleanup, dialIn, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
describe('Dial-In', () => {
it('join participant', async () => {
// check rest url is configured
if (!process.env.DIAL_IN_REST_URL) {
ctx.skipSuiteTests = true;
return;
}
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant();
expect(await ctx.p1.isInMuc()).toBe(true);
if (!await isDialInEnabled(ctx.p1)) {
ctx.skipSuiteTests = true;
}
});
it('retrieve pin', async () => {
let dialInPin: string;
try {
dialInPin = await ctx.p1.getDialInPin();
} catch (e) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = true;
throw e;
}
if (dialInPin.length === 0) {
console.error('dial-in.test.no-pin');
ctx.skipSuiteTests = true;
throw new Error('no pin');
}
expect(dialInPin.length >= 8).toBe(true);
});
it('invite dial-in participant', async () => {
await dialIn(await ctx.p1.getDialInPin());
});
it('wait for audio from dial-in participant', async () => {
const { p1 } = ctx;
if (!await p1.isInMuc()) {
// local participant did not join abort
return;
}
await waitForAudioFromDialInParticipant(p1);
await cleanup(p1);
});
});

View File

@@ -1,41 +0,0 @@
import { Participant } from '../../helpers/Participant';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { ensureOneParticipant } from '../../helpers/participants';
import { assertDialInDisplayed, assertUrlDisplayed, isDialInEnabled, verifyMoreNumbersPage } from '../helpers/DialIn';
describe('Invite', () => {
let p1: Participant;
let dialInEnabled: boolean;
it('setup', async () => {
// This is a temporary hack to avoid failing when running against a jaas env. The same cases are covered in
// jaas/dial/dialin.spec.ts.
if (testsConfig.jaas.enabled) {
ctx.skipSuiteTests = true;
return;
}
await ensureOneParticipant();
p1 = ctx.p1;
dialInEnabled = await isDialInEnabled(p1);
});
it('url displayed', () => assertUrlDisplayed(p1));
it('dial-in displayed', async () => {
if (!dialInEnabled) {
return;
}
await assertDialInDisplayed(p1);
});
it('view more numbers page', async () => {
if (!dialInEnabled) {
return;
}
await verifyMoreNumbersPage(p1);
});
});

View File

@@ -1,46 +0,0 @@
import { ensureOneParticipant } from '../../helpers/participants';
/**
* Tests that the digits only password feature works.
*
* 1. Lock the room with a string (shouldn't work)
* 2. Lock the room with a valid numeric password (should work)
*/
describe('Lock Room with Digits only', () => {
it('join participant', () => ensureOneParticipant({
configOverwrite: {
roomPasswordNumberOfDigits: 5
}
}));
it('lock room with digits only', async () => {
const { p1 } = ctx;
expect(await p1.execute(
() => APP.store.getState()['features/base/config'].roomPasswordNumberOfDigits === 5)).toBe(true);
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(false);
// Set a non-numeric password.
await p1SecurityDialog.addPassword('AAAAA');
expect(await p1SecurityDialog.isLocked()).toBe(false);
await p1SecurityDialog.clickCloseButton();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await p1SecurityDialog.addPassword('12345');
await p1SecurityDialog.clickCloseButton();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
expect(await p1SecurityDialog.isLocked()).toBe(true);
});
});

View File

@@ -95,14 +95,14 @@ export async function assertUrlDisplayed(p: Participant) {
await inviteDialog.waitTillOpen(true);
}
export async function assertDialInDisplayed(p: Participant) {
export async function assertDialInDisplayed(p: Participant, displayed: boolean = false) {
const inviteDialog = p.getInviteDialog();
await inviteDialog.open();
await inviteDialog.waitTillOpen();
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
expect((await inviteDialog.getDialInNumber()).length > 0).toBe(displayed);
expect((await inviteDialog.getPinNumber()).length > 0).toBe(displayed);
}
export async function verifyMoreNumbersPage(p: Participant) {

View File

@@ -9,7 +9,7 @@ setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('iFrame API for Chat', () => {
describe('Chat', () => {
let p1: Participant, p2: Participant;
it('setup', async () => {
@@ -17,8 +17,7 @@ describe('iFrame API for Chat', () => {
p2 = await joinMuc({ name: 'p2', iFrameApi: true });
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
return;
}

View File

@@ -15,8 +15,7 @@ describe('Participants presence', () => {
const { p1, p2 } = ctx;
if (await p1.execute(() => config.disableIframeAPI)) {
// skip the test if iframeAPI is disabled
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'The environment has the iFrame API disabled.';
return;
}

View File

@@ -8,8 +8,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { fetchJson } from '../../helpers/utils';
setTestProperties(__filename, {
requireWebhookProxy: true,
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});
@@ -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

@@ -2,6 +2,7 @@ import type { Participant } from '../../../helpers/Participant';
import { setTestProperties } from '../../../helpers/TestProperties';
import { config as testsConfig } from '../../../helpers/TestsConfig';
import WebhookProxy from '../../../helpers/WebhookProxy';
import { expectations } from '../../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import {
assertDialInDisplayed, assertUrlDisplayed,
@@ -24,24 +25,33 @@ describe('Dial-in', () => {
const room = ctx.roomName;
if (!process.env.DIAL_IN_REST_URL) {
console.log('Dial-in test is disabled, set DIAL_IN_REST_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
return;
}
p1 = await joinJaasMuc({ name: 'p1', token: t({ room, moderator: true }) });
webhooksProxy = ctx.webhooksProxy;
if (!webhooksProxy) {
console.error('WebhooksProxy is not available, will not verify webhooks.');
}
expect(await p1.isInMuc()).toBe(true);
expect(await isDialInEnabled(p1)).toBe(true);
if (expectations.dialIn.enabled !== null) {
expect(await isDialInEnabled(p1)).toBe(expectations.dialIn.enabled);
}
expect(customerId).toBeDefined();
});
it ('Invite UI', async () => {
await assertUrlDisplayed(p1);
await assertDialInDisplayed(p1);
await verifyMoreNumbersPage(p1);
if (expectations.dialIn.enabled !== null) {
await assertDialInDisplayed(p1, expectations.dialIn.enabled);
}
if (expectations.dialIn.enabled === true) {
// TODO: assert the page is NOT shown when the expectation is false.
await verifyMoreNumbersPage(p1);
}
});
it('dial-in', async () => {
@@ -52,13 +62,21 @@ describe('Dial-in', () => {
await dialIn(dialInPin);
await waitForMedia(p1);
const startedPayload
= await verifyStartedWebhooks(webhooksProxy, 'in', 'DIAL_IN_STARTED', customerId);
let startedPayload: any;
if (webhooksProxy) {
startedPayload
= await verifyStartedWebhooks(webhooksProxy, 'in', 'DIAL_IN_STARTED', customerId);
}
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
await p1.getFilmstrip().kickParticipant(endpointId);
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
if (webhooksProxy) {
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
}
await p1.hangup();
});
});

View File

@@ -7,8 +7,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
requireWebhookProxy: true,
useJaas: true
});
describe('Dial-out', () => {
@@ -20,8 +20,7 @@ describe('Dial-out', () => {
const room = ctx.roomName;
if (!dialOutUrl) {
console.log('Dial-out test is disabled, set DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'DIAL_OUT_URL is not set.';
return;
}

View File

@@ -7,8 +7,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
import { waitForMedia } from './util';
setTestProperties(__filename, {
requireWebhookProxy: true,
useJaas: true,
useWebhookProxy: true
});
@@ -22,15 +22,13 @@ describe('SIP jibri invite', () => {
if (true) {
// This is temporary until we figure out how to fix it and configure it properly.
console.log('SIP jibri test is disabled.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'This test is disabled as the code doesn\'t work anymore.';
return;
}
if (!dialOutUrl) {
console.log('SIP jibri test is disabled, set SIP_JIBRI_DIAL_OUT_URL to enable.');
ctx.skipSuiteTests = true;
ctx.skipSuiteTests = 'SIP_JIBRI_DIAL_OUT_URL is not set.';
return;
}

View File

@@ -1,4 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../../pageobjects/Notifications';
@@ -6,7 +7,7 @@ setTestProperties(__filename, {
useJaas: true
});
describe('XMPP login and MUC join test', () => {
describe('XMPP login and MUC join', () => {
it('with a valid token (wildcard room)', async () => {
console.log('Joining a MUC with a valid token (wildcard room)');
const p = await joinJaasMuc({ token: t({ room: '*' }) });
@@ -86,16 +87,27 @@ describe('XMPP login and MUC join test', () => {
console.log('Joining a MUC without a token');
const p = await joinJaasMuc();
expect(Boolean(await p.isInMuc())).toBe(false);
if (expectations.jaas.unauthenticatedJoins) {
expect(Boolean(await p.isInMuc())).toBe(true);
} else {
expect(Boolean(await p.isInMuc())).toBe(false);
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
expect(errorText).toContain('not allowed to join');
expect(errorText).toContain('not allowed to 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

@@ -2,12 +2,12 @@ import { setTestProperties } from '../../helpers/TestProperties';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
requireWebhookProxy: true,
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
describe('MaxOccupants limit enforcement', () => {
describe('MaxOccupants', () => {
it('test maxOccupants limit', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
maxOccupants: 2

View File

@@ -3,8 +3,8 @@ import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
import { IToken } from '../../helpers/token';
setTestProperties(__filename, {
requireWebhookProxy: true,
useJaas: true,
useWebhookProxy: true,
usesBrowsers: [ 'p1', 'p2' ]
});

View File

@@ -2,13 +2,13 @@ import { setTestProperties } from '../../helpers/TestProperties';
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
setTestProperties(__filename, {
useJaas: true,
useWebhookProxy: true
requireWebhookProxy: true,
useJaas: true
});
// This test is separate from passcode.spec.ts, because it needs to use a different room name, and webhooksProxy is only
// setup for the default room name.
describe('Setting passcode through settings provisioning', () => {
describe('Setting invalid passcode through settings provisioning', () => {
it('With an invalid passcode', async () => {
ctx.webhooksProxy.defaultMeetingSettings = {
passcode: 'passcode-must-be-digits-only'

Some files were not shown because too many files have changed in this diff Show More