Compare commits

..

1 Commits

Author SHA1 Message Date
damencho
5ff3219935 debug: Drop. 2025-10-02 14:40:39 -05:00
140 changed files with 1326 additions and 1922 deletions

View File

@@ -73,6 +73,7 @@ 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,6 +96,9 @@ 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,6 +101,7 @@ 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,6 +24,8 @@ 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`);
logger.log(`USER ${id} connected:`, user);
APP.UI.addUser(user);
});

View File

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

View File

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

View File

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

View File

@@ -570,12 +570,10 @@
"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#release-8860",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -87,6 +87,7 @@
"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",
@@ -18259,7 +18260,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#0af202368649360ede8dcc1ca4d055910c6b9337",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"license": "Apache-2.0",
"dependencies": {
"@jitsi/js-utils": "2.4.6",
@@ -21868,6 +21870,14 @@
"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",
@@ -39705,8 +39715,8 @@
}
},
"lib-jitsi-meet": {
"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",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"integrity": "sha512-0FYPvOFSdg9L4ocH8bJw8doUE0rM55JnqRijXMOLS3ZOphbpeBg8tBTH33jwb+bqgo5jjmjTrvJkmkvGNF5/Jg==",
"requires": {
"@jitsi/js-utils": "2.4.6",
"@jitsi/logger": "2.1.1",
@@ -42355,6 +42365,10 @@
"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#release-8860",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2099.0.0+89536686/lib-jitsi-meet.tgz",
"lodash-es": "4.17.21",
"null-loader": "4.0.1",
"optional-require": "1.0.3",
@@ -93,6 +93,7 @@
"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
ditto "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}/sounds"
cp -R "${SOURCE_PATH}" "${TARGET_PATH}/${PROJECT_NAME}"
',
}
end

View File

@@ -73,6 +73,7 @@
"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,6 +4,7 @@ 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,6 +2,7 @@ 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,6 +52,7 @@ 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';
@@ -131,6 +132,7 @@ 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,8 +7,6 @@ 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';
@@ -24,7 +22,7 @@ import {
safeDecodeURIComponent
} from '../util/uri';
import { conferenceWillInit, setObfuscatedRoom } from './actions';
import { setObfuscatedRoom } from './actions';
import {
AVATAR_URL_COMMAND,
EMAIL_COMMAND,
@@ -620,34 +618,3 @@ 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,10 +23,6 @@ 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,11 +127,6 @@ 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,7 +654,6 @@ export interface IConfig {
video?: boolean;
};
queueService: string;
showJoinMeetingDialog?: boolean;
};
watchRTCConfigParams?: IWatchRTCConfiguration;
webhookProxyUrl?: string;

View File

@@ -99,7 +99,6 @@ export default [
'disabledNotifications',
'disabledSounds',
'disableFilmstripAutohiding',
'disableFocus',
'disableInitialGUM',
'disableInviteFunctions',
'disableIncomingMessageSound',
@@ -238,7 +237,6 @@ 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, shouldLeave = true) {
export function disconnect(isRedirect?: boolean) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']): Promise<void> => {
const state = getState();
@@ -407,26 +407,20 @@ export function disconnect(isRedirect?: boolean, shouldLeave = true) {
// intention to leave the conference.
dispatch(conferenceWillLeave(conference_, isRedirect));
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);
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,6 +78,12 @@ 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: 'all',
load: 'languageOnly',
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
react: {
// re-render when a new resource bundle is added

View File

@@ -186,9 +186,6 @@ 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,7 +27,6 @@ import {
isRemoteScreenshareParticipant,
isScreenShareParticipant
} from './functions';
import logger from './logger';
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
/**
@@ -365,8 +364,6 @@ 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.
@@ -451,8 +448,6 @@ 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,7 +21,6 @@ import {
getRemoteScreensharesBasedOnPresence,
getVirtualScreenshareParticipantOwnerId
} from './functions';
import logger from './logger';
import { FakeParticipant } from './types';
StateListenerRegistry.register(
@@ -70,19 +69,14 @@ function _createOrRemoveVirtualParticipants(
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
if (removedScreenshareSourceNames.length) {
removedScreenshareSourceNames.forEach(id => {
logger.debug('Dispatching participantLeft for virtual screenshare', id);
dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
}));
});
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
fakeParticipant: FakeParticipant.RemoteScreenShare
})));
}
if (addedScreenshareSourceNames.length) {
addedScreenshareSourceNames.forEach(id => {
logger.debug('Creating virtual screenshare participant', id);
dispatch(createVirtualScreenshareParticipant(id, false, conference));
});
addedScreenshareSourceNames.forEach(id => dispatch(
createVirtualScreenshareParticipant(id, false, conference)));
}
}

View File

@@ -1,5 +1,6 @@
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 {
@@ -56,12 +57,75 @@ export function isLargeVideoReceived({ getState }: IStore): boolean {
}
/**
* Returns the local video track's codec.
* Returns whether the local video track is encoded in AV1.
*
* @returns {string?} The local video track's codec.
* @param {IStore} store - The redux store.
* @returns {boolean}
*/
export function getLocalCameraEncoding({ getState }: IStore): string | undefined {
return getLocalVideoTrack(getState()['features/base/tracks'])?.codec?.toLowerCase();
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;
}
/**

View File

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

View File

@@ -71,16 +71,11 @@ const useStyles = makeStyles()(theme => {
badge: {
...theme.typography.labelBold,
alignItems: 'center',
backgroundColor: theme.palette.warning01,
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)}`
padding: `0 ${theme.spacing(1)}`,
borderRadius: '100%',
backgroundColor: theme.palette.warning01,
marginLeft: theme.spacing(2)
},
icon: {

View File

@@ -7,9 +7,7 @@ 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';
@@ -19,21 +17,6 @@ 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).
*/
@@ -113,9 +96,7 @@ class Chat extends Component<IProps> {
* @private
* @returns {{
* _messages: Array<Object>,
* _nbUnreadMessages: number,
* _nbUnreadPolls: number,
* _nbUnreadFiles: number
* _nbUnreadMessages: number
* }}
*/
function _mapStateToProps(state: IReduxState, _ownProps: any) {
@@ -123,16 +104,13 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
return {
_messages: messages,
_nbUnreadMessages: nbUnreadMessages,
_nbUnreadPolls: getUnreadPollCount(state),
_nbUnreadFiles: getUnreadFilesCount(state)
_nbUnreadMessages: nbUnreadMessages
};
}
export default translate(connect(_mapStateToProps)((props: IProps) => {
const { _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles, dispatch, navigation, t } = props;
const totalUnread = _nbUnreadMessages + _nbUnreadPolls + _nbUnreadFiles;
const unreadMessagesNr = totalUnread > 0;
const { _nbUnreadMessages, dispatch, navigation, t } = props;
const unreadMessagesNr = _nbUnreadMessages > 0;
const isFocused = useIsFocused();
@@ -143,14 +121,14 @@ export default translate(connect(_mapStateToProps)((props: IProps) => {
activeUnreadNr = { unreadMessagesNr }
isFocused = { isFocused }
label = { t('chat.tabs.chat') }
nbUnread = { totalUnread } />
nbUnread = { _nbUnreadMessages } />
)
});
return () => {
isFocused && dispatch(closeChat());
};
}, [ isFocused, _nbUnreadMessages, _nbUnreadPolls, _nbUnreadFiles ]);
}, [ isFocused, _nbUnreadMessages ]);
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, getUnreadFilesCount } from '../../functions';
import { getUnreadCount } from '../../functions';
interface IProps extends AbstractButtonProps {
@@ -70,7 +70,9 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
return {
_isPollsDisabled: arePollsDisabled(state),
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state) || getUnreadFilesCount(state),
// The toggled icon should also be available for new polls
_unreadMessageCount: getUnreadCount(state) || getUnreadPollCount(state),
visible
};
}

View File

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

View File

@@ -131,16 +131,6 @@ 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,7 +1,6 @@
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 {
@@ -31,7 +30,6 @@ const DEFAULT_STATE = {
notifyPrivateRecipientsChangedTimestamp: undefined,
reactions: {},
nbUnreadMessages: 0,
nbUnreadFiles: 0,
privateMessageRecipient: undefined,
lobbyMessageRecipient: undefined,
isLobbyChatActive: false,
@@ -55,7 +53,6 @@ export interface IChatState {
name: string;
} | ILocalParticipant;
messages: IMessage[];
nbUnreadFiles: number;
nbUnreadMessages: number;
notifyPrivateRecipientsChangedTimestamp?: number;
privateMessageRecipient?: IParticipant | IVisitorChatParticipant;
@@ -238,8 +235,7 @@ ReducerRegistry.register<IChatState>('features/chat', (state = DEFAULT_STATE, ac
return {
...state,
focusedTab: action.tabId,
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages,
nbUnreadFiles: action.tabId === ChatTabs.FILE_SHARING ? 0 : state.nbUnreadFiles
nbUnreadMessages: action.tabId === ChatTabs.CHAT ? 0 : state.nbUnreadMessages
};
case SET_CHAT_WIDTH: {
@@ -275,23 +271,6 @@ 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,7 +3,9 @@ import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
Platform,
SafeAreaView,
StatusBar,
View,
ViewStyle
} from 'react-native';
@@ -14,6 +16,8 @@ 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';
@@ -92,6 +96,11 @@ 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.
*/
@@ -268,6 +277,7 @@ class Conference extends AbstractConference<IProps, State> {
override render() {
const {
_brandingStyles,
_fullscreenEnabled
} = this.props;
return (
@@ -277,6 +287,13 @@ class Conference extends AbstractConference<IProps, State> {
_brandingStyles
] }>
<BrandingImageBackground />
{
Platform.OS === 'android'
&& <StatusBar
barStyle = 'light-content'
hidden = { _fullscreenEnabled }
translucent = { _fullscreenEnabled } />
}
{ this._renderContent() }
</Container>
);
@@ -573,6 +590,7 @@ 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,14 +39,12 @@ 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, shouldIncrementUnread = false) {
export function addFile(file: IFileMetadata) {
return {
type: ADD_FILE,
file,
shouldIncrementUnread
file
};
}

View File

@@ -6,10 +6,8 @@ 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';
@@ -25,40 +23,12 @@ import { downloadFile } from './utils';
*/
StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, { dispatch, getState }, previousConference) => {
(conference, { dispatch }, previousConference) => {
if (conference && !previousConference) {
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
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));
}
dispatch(addFile(file));
});
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
@@ -66,13 +36,9 @@ StateListenerRegistry.register(
});
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
const state = getState();
const localParticipant = getLocalParticipant(state);
dispatch({
type: _FILE_LIST_RECEIVED,
files,
localParticipantId: localParticipant?.id
files
});
});
}
@@ -86,17 +52,6 @@ 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,11 +1,6 @@
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 {
@@ -25,7 +20,6 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.set(action.file.fileId, action.file);
return {
...state,
files: newFiles
};
}
@@ -36,7 +30,6 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
newFiles.delete(action.fileId);
return {
...state,
files: newFiles
};
}
@@ -50,14 +43,12 @@ 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 && !state['features/base/jwt'].jwt,
_login: showModeratorLogin,
_hangUp: showHangUp,
_isLobbyChatActive: isLobbyChatActive,
_meetingName: getConferenceName(state),

View File

@@ -0,0 +1,12 @@
/**
* 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

@@ -0,0 +1,21 @@
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

@@ -0,0 +1,22 @@
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

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

View File

@@ -0,0 +1,102 @@
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

@@ -0,0 +1,21 @@
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,7 +13,6 @@ 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 {
@@ -445,11 +444,10 @@ 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 || _iAmVisitor) {
if (iAmRecorder) {
return false;
}

View File

@@ -96,11 +96,6 @@ 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,7 +11,6 @@ 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.
@@ -114,49 +113,39 @@ 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>} previousScreenShares - Array containing the list of all the screen sharing endpoints
* @param {Array<string>} screenShares - 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(
previousScreenShares: Array<string>,
currentScreenShares: Array<string>,
{ dispatch, getState }: IStore) {
screenShares: Array<string>, { dispatch, getState }: IStore) {
const state = getState();
const remoteScreenShares = state['features/video-layout'].remoteScreenShares;
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 (!currentScreenShares?.length) {
if (!remoteScreenShares?.length) {
let participantId = null;
if (pinned && !previousScreenShares.find((share: string) => share === pinned.id)) {
if (pinned && !screenShares.find(share => share === pinned.id)) {
participantId = pinned.id;
}
logger.debug('No more screenshares, unpinning or restoring previous pin', participantId);
dispatch(pinParticipant(participantId));
return;
}
const latestScreenShareParticipantId = currentScreenShares[currentScreenShares.length - 1];
const latestScreenShareParticipantId = remoteScreenShares[remoteScreenShares.length - 1];
if (latestScreenShareParticipantId) {
const alreadyPinned = pinned?.id === latestScreenShareParticipantId;
if (!alreadyPinned) {
logger.debug(`Auto pinning latest screen share participant: ${latestScreenShareParticipantId}`);
dispatch(pinParticipant(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,21 +27,13 @@ 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;
}
}
@@ -83,9 +75,9 @@ MiddlewareRegistry.register(store => next => action => {
}
if (shouldUpdateAutoPin) {
const newScreenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
const screenShares = store.getState()['features/video-layout'].remoteScreenShares || [];
updateAutoPinnedParticipant(oldScreenShares, newScreenShares, store);
updateAutoPinnedParticipant(screenShares, store);
}
return result;

View File

@@ -4,7 +4,6 @@ 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,
@@ -23,18 +22,14 @@ 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, newScreenSharesOrder, store);
} else {
logger.debug('Auto pinning is disabled or Follow Me is active, skipping auto pinning of screenshare.');
updateAutoPinnedParticipant(oldScreenSharesOrder, store);
}
}
});

View File

@@ -64,7 +64,6 @@ 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 = {
@@ -79,7 +78,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
dispatch(showNotification(notificationParams, NOTIFICATION_TIMEOUT_TYPE.STICKY));
dispatch(setVisitorDemoteActor(undefined));
});
} else if (showJoinMeetingDialog) {
} else {
dispatch(openDialog(JoinMeetingDialog));
}

View File

@@ -1,58 +0,0 @@
-- 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,7 +6,6 @@ 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;
@@ -18,8 +17,6 @@ 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", "");
@@ -50,7 +47,6 @@ 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
@@ -82,13 +78,8 @@ function handle_terminate_meeting (event)
module:log("warn", "Room not found")
return { status_code = 404 };
else
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
module:log("info", "Destroy room jid %s", room.jid)
room:destroy(nil, "The meeting has been terminated")
end
event_count_success()
return { status_code = 200 };

View File

@@ -48,7 +48,6 @@ 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;
@@ -106,7 +105,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_jid, actor, jid, display_name, granted)
function notify_lobby_access(room, actor, jid, display_name, granted)
local notify_json = {
value = jid,
name = display_name
@@ -117,12 +116,6 @@ function notify_lobby_access(room_jid, 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
@@ -234,7 +227,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_jid = room.jid;
new_room.main_room = room;
room._data.lobbyroom = new_room.jid;
room:save(true);
return true
@@ -252,13 +245,6 @@ 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;
@@ -426,18 +412,13 @@ 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_jid then
if not room.main_room then
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
return 'none';
end
-- moderators in main room are moderators here
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);
local role = room.main_room.get_affiliation(room.main_room, jid);
if role then
return role;
end
@@ -452,7 +433,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_jid, actor, occupant.nick, display_name, false);
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
end
end);
end
@@ -493,16 +474,9 @@ 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, nil);
destroy_lobby_room(room, room.jid);
module:fire_event('jitsi-lobby-disabled', { room = room; });
notify_lobby_enabled(room, actor, false);
end
@@ -615,7 +589,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.jid, from, occupant.nick, display_name, true);
notify_lobby_access(room, from, occupant.nick, display_name, true);
end
end
end
@@ -661,13 +635,6 @@ 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,8 +10,6 @@ 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;
@@ -160,7 +158,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
local function filterTranscriptionResult(event)
module:hook('message/bare', function(event)
local stanza = event.stanza;
if stanza.attr.type ~= 'groupchat' then
@@ -192,6 +190,12 @@ local function filterTranscriptionResult(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
@@ -199,18 +203,6 @@ local function filterTranscriptionResult(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;
@@ -248,7 +240,4 @@ local function filterTranscriptionResult(event)
room = room, occupant = occupant, message = msg_obj,
origin = event.origin,
stanza = stanza, raw_message = json_message });
end
module:hook('message/bare', filterTranscriptionResult);
module:hook('jitsi-visitor-groupchat-pre-route', filterTranscriptionResult);
end);

View File

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

View File

@@ -11,7 +11,6 @@
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');
@@ -111,8 +110,7 @@ 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)
or main_room.destroying then
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
return;
end
@@ -146,13 +144,9 @@ 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;
if not lobby_room.main_room_jid then
return;
end
local main_room = get_room_from_jid(lobby_room.main_room_jid);
local main_room = lobby_room.main_room;
if not main_room or is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
return;

View File

@@ -54,18 +54,13 @@ 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 = sub,
sub = session.jitsi_web_query_prefix or module.host,
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,22 +69,8 @@ local function verify_user(session, stanza)
local user_bare_jid = jid_bare(user_jid);
local _, user_domain = jid_split(user_jid);
-- 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
-- allowlist for participants
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) 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,10 +565,6 @@ 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(
@@ -704,13 +700,6 @@ 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}
*/
async log(message: string): Promise<void> {
await logInfo(this.driver, message);
log(message: string): void {
logInfo(this.driver, message);
}
/**
@@ -678,34 +678,32 @@ export class Participant {
* Hangups the participant by leaving the page. base.html is an empty page on all deployments.
*/
async hangup() {
console.log('Hanging up');
if ((await this.driver.getUrl()).endsWith('/base.html')) {
console.log('Already hung up');
const current = await this.driver.getUrl();
// already hangup
if (current.endsWith('/base.html')) {
return;
}
// @ts-ignore
await this.execute(() => window.APP?.conference?.hangup());
// do a hangup, to make sure unavailable presence is sent
await this.execute(() => typeof APP !== 'undefined' && APP.conference?.hangup());
// Wait until _room is unset, which is one of the last things hangup() does.
// let's give it some time to leave the muc, we redirect after hangup so we should wait for the
// change of url
await this.driver.waitUntil(
async () => {
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;
}
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;
},
{
timeout: 8000,
timeoutMsg: `${this.name} failed to hang up`
timeoutMsg: `${this.name} did not leave the muc in 8s initialUrl: ${current}`
}
);
console.log('Hung up');
await this.driver.url('/base.html')

View File

@@ -2,26 +2,44 @@
* 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 uses the webhook proxy if available. */
/** The test requires the webhook proxy. */
useWebhookProxy: boolean;
usesBrowsers: string[];
usesBrowsers?: string[];
};
const defaultProperties: ITestProperties = {
useWebhookProxy: false,
requireWebhookProxy: false,
useJaas: false,
usesBrowsers: [ 'p1' ]
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
};
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.
@@ -41,7 +59,7 @@ export function setTestProperties(filename: string, properties: Partial<ITestPro
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
}
testProperties[filename] = { ...defaultProperties, ...properties };
testProperties[filename] = { ...getDefaultProperties(filename), ...properties };
}
let testFilesLoaded = false;
@@ -79,7 +97,7 @@ export function loadTestFiles(files: string[]): void {
require(file);
if (!testProperties[file]) {
// If no properties were set, apply defaults
setTestProperties(file, { ...defaultProperties });
setTestProperties(file, getDefaultProperties(file));
}
} catch (error) {
console.warn(`Warning: Could not analyze ${file}:`, (error as Error).message);
@@ -108,11 +126,5 @@ export function loadTestFiles(files: string[]): void {
* @returns Promise<ITestProperties> - The test properties with defaults applied
*/
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
const properties = testProperties[testFilePath] || { ...defaultProperties };
if (properties.requireWebhookProxy && !properties.useWebhookProxy) {
properties.useWebhookProxy = true;
}
return properties;
return testProperties[testFilePath] || getDefaultProperties(testFilePath);
}

View File

@@ -4,7 +4,18 @@
export const config = {
/** Enable debug logging. Note this includes private information from .env */
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
expectationsFile: process.env.EXPECTATIONS?.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-')
);
})(),
jaas: {
customerId: (() => {
if (typeof process.env.JAAS_TENANT !== 'undefined') {

View File

@@ -45,11 +45,13 @@ export function getLogs(driver: WebdriverIO.Browser) {
}
/**
* 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.
* 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 function saveLogs(driver: WebdriverIO.Browser, value: string) {
export function logInfo(driver: WebdriverIO.Browser, message: string) {
// @ts-ignore
if (!driver.logFile) {
return;
@@ -57,27 +59,9 @@ export function saveLogs(driver: WebdriverIO.Browser, value: string) {
try {
// @ts-ignore
fs.appendFileSync(driver.logFile, value);
fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
} 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

@@ -1,57 +0,0 @@
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,13 +38,3 @@ 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,9 +18,8 @@ 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 | string;
skipSuiteTests: boolean;
/**
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
* set up the context appropriately.

View File

@@ -3,7 +3,8 @@ import { config as testsConfig } from './TestsConfig';
const https = require('https');
export function generateRoomName(testName: string) {
const rand = (Math.floor(Math.random() * 400) + 1).toString();
// 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');
let roomName = `${testName}-${rand}`;
if (testsConfig.roomName.prefix) {

View File

@@ -119,7 +119,7 @@ class BreakoutRoom extends BasePageObject {
await listItem.click();
const button = listItem.$(`button[title="${MORE_LABEL}"]`);
const button = listItem.$(`aria/${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.$(`button=${ADD_BREAKOUT_ROOM}`);
const addBreakoutButton = this.participant.driver.$(`aria/${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.$(`button=${LEAVE_ROOM_LABEL}`);
const leaveButton = this.participant.driver.$(`aria/${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.$(`button=${AUTO_ASSIGN_LABEL}`);
const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
await button.waitForClickable();
await button.click();
@@ -204,9 +204,8 @@ export default class BreakoutRooms extends BasePageObject {
await participantsPane.selectParticipant(participant);
await participantsPane.openParticipantContextMenu(participant);
const sendButton = this.participant.driver.$(`div=${roomName}`);
const sendButton = this.participant.driver.$(`aria/${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.$('button=Create a poll').click();
await this.participant.driver.$('aria/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.$('button=Add option').click();
await this.participant.driver.$('aria/Add option').click();
}
/**
@@ -141,35 +141,35 @@ export default class ChatPanel extends BasePageObject {
* Clicks the "Save" button.
*/
async clickSavePollButton() {
await this.participant.driver.$('button=Save').click();
await this.participant.driver.$('aria/Save').click();
}
/**
* Clicks the "Edit" button.
*/
async clickEditPollButton() {
await this.participant.driver.$('button=Edit').click();
await this.participant.driver.$('aria/Edit').click();
}
/**
* Clicks the "Skip" button.
*/
async clickSkipPollButton() {
await this.participant.driver.$('button=Skip').click();
await this.participant.driver.$('aria/Skip').click();
}
/**
* Clicks the "Send" button.
*/
async clickSendPollButton() {
await this.participant.driver.$('button=Send').click();
await this.participant.driver.$('aria/Send poll').click();
}
/**
* Waits for the "Send" button to be visible.
*/
async waitForSendButton() {
await this.participant.driver.$('button=Send').waitForExist({
await this.participant.driver.$('aria/Send poll').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.$('button=Submit').click();
await this.participant.driver.$('aria/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 = 'button[title="Local user controls"]';
const LOCAL_USER_CONTROLS = 'aria/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();
await this.participant.log(`Extracted text in invite dialog: ${fullText}`);
this.participant.log(`Extracted text in invite dialog: ${fullText}`);
return fullText.split(':')[1].trim();
}

View File

@@ -106,12 +106,17 @@ export default class Notifications extends BasePageObject {
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
const closeButton = this.participant.driver.$('[data-testid="${testId}"] #close-notification');
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);
if (skipNonExisting && !await closeButton.isExisting()) {
if (skipNonExisting && !await notification.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.$(`button=${INVITE}`);
const inviteButton = this.participant.driver.$(`aria/${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.$(`button[title="More moderation options ${participantNameToReject}"]`);
= this.participant.driver.$(`aria/More moderation options ${participantNameToReject}`);
await moreOptionsButton.click();

View File

@@ -40,7 +40,6 @@ 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-label="${accessibilityCSSSelector}"]`);
return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
}
/**
@@ -55,8 +55,8 @@ export default class Toolbar extends BasePageObject {
*
* @returns {Promise<void>}
*/
async clickAudioMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Mute Button');
clickAudioMuteButton(): Promise<void> {
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>}
*/
async clickAudioUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Audio Unmute Button');
clickAudioUnmuteButton(): Promise<void> {
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>}
*/
async clickVideoMuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Mute Button');
clickVideoMuteButton(): Promise<void> {
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>}
*/
async clickVideoUnmuteButton(): Promise<void> {
await this.participant.log('Clicking on: Video Unmute Button');
clickVideoUnmuteButton(): Promise<void> {
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>}
*/
async clickCloseParticipantsPaneButton(): Promise<void> {
await this.participant.log('Clicking on: Close Participants pane Button');
clickCloseParticipantsPaneButton(): Promise<void> {
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>}
*/
async clickParticipantsPaneButton(): Promise<void> {
await this.participant.log('Clicking on: Participants pane Button');
clickParticipantsPaneButton(): Promise<void> {
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.
*/
async clickRaiseHandButton(): Promise<void> {
await this.participant.log('Clicking on: Raise hand Button');
clickRaiseHandButton(): Promise<void> {
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.
*/
async clickChatButton(): Promise<void> {
await this.participant.log('Clicking on: Chat Button');
clickChatButton(): Promise<void> {
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.
*/
async clickCloseChatButton(): Promise<void> {
await this.participant.log('Clicking on: Close Chat Button');
clickCloseChatButton(): Promise<void> {
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.
*/
async clickHangupButton(): Promise<void> {
await this.participant.log('Clicking on: Hangup Button');
clickHangupButton(): Promise<void> {
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();
await this.participant.log(`Clicking on: ${accessibilityLabel}`);
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-label="${OVERFLOW_MENU}"]`).length > 0;
return await this.participant.driver.$$(`aria/${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.$('div=Joining meeting');
return this.participant.driver.$('aria/Joining meeting');
}
/**

View File

@@ -1,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Audio-only mode', () => {
describe('Audio only', () => {
it('joining the meeting', () => ensureTwoParticipants());
/**

View File

@@ -1,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Display name', () => {
describe('DisplayName', () => {
it('joining the meeting', () => ensureTwoParticipants({ skipDisplayName: true }));
it('check change', async () => {

View File

@@ -1,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Hangup', () => {
describe('End Conference', () => {
it('joining the meeting', () => ensureTwoParticipants());
it('hangup call and check', async () => {

View File

@@ -0,0 +1,52 @@
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

@@ -0,0 +1,42 @@
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

@@ -0,0 +1,44 @@
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,56 +1,41 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { ensureOneParticipant, ensureTwoParticipants, joinSecondParticipant } from '../../helpers/participants';
import type SecurityDialog from '../../pageobjects/SecurityDialog';
setTestProperties(__filename, {
description: '1. Set a room password (assert the image changes to locked). \
2. Join with a second participant. \
3. Assert password is required (and padlock is locked). \
4. Assert wrong password fails. \
5. Unlock the room (assert the padlock is unlocked) \
6. Assert room is unlocked and the padlock is unlocked.',
usesBrowsers: [ 'p1', 'p2' ]
});
let roomKey: string;
/**
* 1. Lock the room (make sure the image changes to locked)
* 2. Join with a second browser/tab
* 3. Make sure we are required to enter a password.
* (Also make sure the padlock is locked)
* 4. Enter wrong password, make sure we are not joined in the room
* 5. Unlock the room (Make sure the padlock is unlocked)
* 6. Join again and make sure we are not asked for a password and that
* the padlock is unlocked.
*/
describe('Lock Room', () => {
it('joining the meeting', () => ensureOneParticipant());
describe('Lock room', () => {
let p1: Participant, p2: Participant;
let roomKey: string;
it('locks the room', () => participant1LockRoom());
it('setup', async () => {
if (!expectations.moderation.setPasswordAvailable) {
ctx.skipSuiteTests = 'setPasswordAvailable is not expected to be available';
return;
}
await ensureOneParticipant();
p1 = ctx.p1;
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
await setPassword(p1, roomKey);
});
it('enter participant in locked room', async () => {
// first enter wrong pin then correct one
await joinSecondParticipant({
skipWaitToJoin: true
});
p2 = ctx.p2;
const { p2 } = ctx;
// wait for password prompt
const p2PasswordDialog = p2.getPasswordDialog();
// Submit a wrong password
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(`${roomKey}1234`);
// give some time to the password prompt to disappear and send the password
// TODO: wait until the dialog is not displayed? Assert the room is not joined?
// give sometime to the password prompt to disappear and send the password
await p2.driver.pause(500);
// Submit the correct password
// wait for password prompt
await p2PasswordDialog.waitForDialog();
await p2PasswordDialog.submitPassword(roomKey);
@@ -65,51 +50,57 @@ describe('Lock room', () => {
});
it('unlock room', async () => {
await p2.hangup();
// Unlock room. Check whether room is still locked. Click remove and check whether it is unlocked.
await ctx.p2.hangup();
await removePassword(p1);
await participant1UnlockRoom();
});
it('join the unlocked room', async () => {
it('enter participant in unlocked room', async () => {
// Just enter the room and check that is not locked.
// if we fail to unlock the room this one will detect it
// as participant will fail joining
await ensureTwoParticipants();
p2 = ctx.p2;
const { p2 } = ctx;
const p2SecurityDialog = p2.getSecurityDialog();
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
await waitForRoomLockState(p2SecurityDialog, false);
await p2SecurityDialog.clickCloseButton();
});
it('set password while participants are in the room', async () => {
it('update locked state while participants in room', async () => {
// Both participants are in unlocked room, lock it and see whether the
// change is reflected on the second participant icon.
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
await setPassword(p1, roomKey);
await participant1LockRoom();
const { p2 } = ctx;
const p2SecurityDialog = p2.getSecurityDialog();
await p2.getToolbar().clickSecurityButton();
await p2SecurityDialog.waitForDisplay();
await waitForRoomLockState(p2SecurityDialog, true);
await removePassword(p1);
await participant1UnlockRoom();
await waitForRoomLockState(p2SecurityDialog, false);
});
it('unlock after participant enter wrong password', async () => {
// P1 locks the room. Participant tries to enter using wrong password.
// P1 unlocks the room and Participant submits the password prompt with no password entered and
// should enter of unlocked room.
await p2.hangup();
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
await setPassword(p1, roomKey);
await ctx.p2.hangup();
await participant1LockRoom();
await joinSecondParticipant({
skipWaitToJoin: true
});
p2 = ctx.p2;
const { p2 } = ctx;
// wait for password prompt
const p2PasswordDialog = p2.getPasswordDialog();
@@ -123,7 +114,7 @@ describe('Lock room', () => {
// wait for password prompt
await p2PasswordDialog.waitForDialog();
await removePassword(p1);
await participant1UnlockRoom();
await p2PasswordDialog.clickOkButton();
await p2.waitToJoinMUC();
@@ -138,33 +129,46 @@ describe('Lock room', () => {
});
/**
* Set a room password via the UI.
* Participant1 locks the room.
*/
async function setPassword(p: Participant, password: string) {
const securityDialog = p.getSecurityDialog();
async function participant1LockRoom() {
roomKey = `${Math.trunc(Math.random() * 1_000_000)}`;
await p.getToolbar().clickSecurityButton();
await securityDialog.waitForDisplay();
await waitForRoomLockState(securityDialog, false);
await securityDialog.addPassword(password);
await securityDialog.clickCloseButton();
await p.getToolbar().clickSecurityButton();
await securityDialog.waitForDisplay();
await waitForRoomLockState(securityDialog, true);
await securityDialog.clickCloseButton();
const { p1 } = ctx;
const p1SecurityDialog = p1.getSecurityDialog();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.addPassword(roomKey);
await p1SecurityDialog.clickCloseButton();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await waitForRoomLockState(p1SecurityDialog, true);
await p1SecurityDialog.clickCloseButton();
}
/**
* Remove the room password via the UI.
* Participant1 unlocks the room.
*/
async function removePassword(p: Participant) {
const securityDialog = p.getSecurityDialog();
async function participant1UnlockRoom() {
const { p1 } = ctx;
const p1SecurityDialog = p1.getSecurityDialog();
await p.getToolbar().clickSecurityButton();
await securityDialog.waitForDisplay();
await securityDialog.removePassword();
await waitForRoomLockState(securityDialog, false);
await securityDialog.clickCloseButton();
await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();
await p1SecurityDialog.removePassword();
await waitForRoomLockState(p1SecurityDialog, false);
await p1SecurityDialog.clickCloseButton();
}
/**

View File

@@ -1,5 +1,4 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import {
checkForScreensharingTile,
ensureOneParticipant,
@@ -12,10 +11,6 @@ import {
unmuteVideoAndCheck
} from '../helpers/mute';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Mute', () => {
it('joining the meeting', () => ensureTwoParticipants());

View File

@@ -1,10 +1,5 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Polls', () => {
it('joining the meeting', async () => {
await ensureTwoParticipants();

View File

@@ -1,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureOneParticipant, joinFirstParticipant, joinSecondParticipant } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Pre-join screen', () => {
describe('PreJoin', () => {
it('display name required', async () => {
await joinFirstParticipant({
configOverwrite: {

View File

@@ -1,11 +1,6 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Self view', () => {
it('joining the meeting', () => ensureTwoParticipants());

View File

@@ -0,0 +1,29 @@
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,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
import { muteVideoAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Stop video', () => {
it('joining the meeting', () => ensureTwoParticipants());

View File

@@ -0,0 +1,32 @@
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,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureTwoParticipants } from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2' ]
});
describe('Pinning', () => {
describe('SwitchVideo', () => {
it('joining the meeting', () => ensureTwoParticipants());
it('p1 click on local', () => ctx.p1.getFilmstrip().pinParticipant(ctx.p1));

View File

@@ -0,0 +1,26 @@
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

@@ -0,0 +1,41 @@
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,13 +1,8 @@
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureThreeParticipants } from '../../helpers/participants';
import { muteAudioAndCheck } from '../helpers/mute';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
describe('Active speaker', () => {
describe('ActiveSpeaker', () => {
it('testActiveSpeaker', async () => {
await ensureThreeParticipants();
@@ -46,19 +41,19 @@ describe('Active speaker', () => {
*/
async function testActiveSpeaker(
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
await activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
const speakerEndpoint = await activeSpeaker.getEndpointId();
// just a debug print to go in logs
await activeSpeaker.log('Unmuting in testActiveSpeaker');
activeSpeaker.log('Unmuting in testActiveSpeaker');
// Unmute
await activeSpeaker.getToolbar().clickAudioUnmuteButton();
// just a debug print to go in logs
await otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
await otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
@@ -73,14 +68,14 @@ async function testActiveSpeaker(
});
// just a debug print to go in logs
await activeSpeaker.log('Muting in testActiveSpeaker');
activeSpeaker.log('Muting in testActiveSpeaker');
// Mute back again
await activeSpeaker.getToolbar().clickAudioMuteButton();
// just a debug print to go in logs
await otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
await otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
}

View File

@@ -1,6 +1,5 @@
import { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import { expectations } from '../../helpers/expectations';
import { config } from '../../helpers/TestsConfig';
import {
ensureOneParticipant,
ensureThreeParticipants, ensureTwoParticipants,
@@ -8,22 +7,17 @@ import {
} from '../../helpers/participants';
import { unmuteAudioAndCheck, unmuteVideoAndCheck } from '../helpers/mute';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
describe('AVModeration', () => {
describe('Audio/video moderation', () => {
it('setup', async () => {
it('check for moderators', async () => {
// if all 3 participants are moderators, skip this test
await ensureThreeParticipants();
const { p1, p2, p3 } = ctx;
// if all 3 participants are moderators, skip this test
if (!await p1.isModerator()
|| (await p1.isModerator() && await p2.isModerator() && await p3.isModerator())) {
ctx.skipSuiteTests = `Unsupported moderator configuration: p1=${await p1.isModerator()},\
p2=${await p2.isModerator()}, p3=${await p3.isModerator()}`;
ctx.skipSuiteTests = true;
}
});
@@ -85,7 +79,7 @@ describe('Audio/video moderation', () => {
it('hangup and change moderator', async () => {
// The test below is only correct when the environment is configured to automatically elect a new moderator
// when the moderator leaves. For environments where this is not the case, the test is skipped.
if (!expectations.autoModerator) {
if (!config.autoModerator) {
return;
}

View File

@@ -1,11 +1,6 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
import { unmuteVideoAndCheck } from '../helpers/mute';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
const EMAIL = 'support@jitsi.org';
const HASH = '38f014e4b7dde0f64f8157d26a8c812e';

View File

@@ -1,7 +1,6 @@
import type { ChainablePromiseElement } from 'webdriverio';
import type { Participant } from '../../helpers/Participant';
import { setTestProperties } from '../../helpers/TestProperties';
import {
checkSubject,
ensureThreeParticipants,
@@ -9,20 +8,16 @@ import {
hangupAllParticipants
} from '../../helpers/participants';
setTestProperties(__filename, {
usesBrowsers: [ 'p1', 'p2', 'p3' ]
});
const MAIN_ROOM_NAME = 'Main room';
const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
const LIST_ITEM_CONTAINER = 'list-item-container';
describe('Breakout rooms', () => {
describe('BreakoutRooms', () => {
it('check support', async () => {
await ensureTwoParticipants();
if (!await ctx.p1.isBreakoutRoomsSupported()) {
ctx.skipSuiteTests = 'The environment does not support breakout rooms.';
ctx.skipSuiteTests = true;
}
});
@@ -39,7 +34,7 @@ describe('Breakout rooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -47,7 +42,7 @@ describe('Breakout rooms', () => {
// second participant should also see one breakout room
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p2'
});
});
@@ -59,7 +54,7 @@ describe('Breakout rooms', () => {
// there should be one breakout room
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'No breakout room seen by p1'
});
@@ -100,7 +95,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the breakout room'
});
});
@@ -142,7 +137,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'P2 is seeing p1 in the breakout room'
});
});
@@ -157,14 +152,14 @@ describe('Breakout rooms', () => {
// there should be no breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 0, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p1'
});
// the second participant should also see no breakout rooms
await p2.driver.waitUntil(
async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'Breakout room was not removed for p2'
});
});
@@ -181,7 +176,7 @@ describe('Breakout rooms', () => {
// there should be two breakout rooms
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 2, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'Breakout room was not created by p1'
});
@@ -225,7 +220,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1
&& (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'P2 is not seeing p1 in the main room'
});
});
@@ -249,7 +244,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 1 && list[1].participantCount === 1;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing two breakout rooms'
});
@@ -310,7 +305,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount + list[1].participantCount === 1;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
});
@@ -341,7 +336,7 @@ describe('Breakout rooms', () => {
await p1.driver.waitUntil(
async () => await p1BreakoutRooms.getRoomsCount() === 1
&& (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'No breakout room added for p1'
});
@@ -379,7 +374,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 1;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'P1 is not seeing p2 in the breakout room'
});
@@ -425,7 +420,7 @@ describe('Breakout rooms', () => {
return list[0].name === myNewRoomName;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'The breakout room was not renamed for p1'
});
@@ -447,7 +442,7 @@ describe('Breakout rooms', () => {
return list[0].participantCount === 0;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'The breakout room not found or not empty for p1'
});
@@ -457,7 +452,7 @@ describe('Breakout rooms', () => {
return list?.length === 1;
}, {
timeout: 5000,
timeout: 3000,
timeoutMsg: 'The breakout room not seen by p2'
});

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