mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-30 17:47:46 +00:00
Compare commits
70 Commits
8874
...
tests-expe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b378d972e0 | ||
|
|
99a5d7eaa9 | ||
|
|
dad4fb9e06 | ||
|
|
3772b9a5ae | ||
|
|
89b9c75242 | ||
|
|
b24b60b735 | ||
|
|
486a1f6511 | ||
|
|
80b3f1d7d4 | ||
|
|
421b21edeb | ||
|
|
a58b0d9a85 | ||
|
|
1aca8ab985 | ||
|
|
f9daba728f | ||
|
|
fbb6456317 | ||
|
|
52ead26bed | ||
|
|
8d1da83e3c | ||
|
|
5453b615f5 | ||
|
|
81a7301a3e | ||
|
|
1138b7779b | ||
|
|
2fd653d928 | ||
|
|
012c9fb329 | ||
|
|
fdf95444e9 | ||
|
|
919c60b3d2 | ||
|
|
e02c4e8f7f | ||
|
|
3fd9ce5f11 | ||
|
|
93022b3281 | ||
|
|
5d63b31071 | ||
|
|
4432f727a4 | ||
|
|
6f1bdb513a | ||
|
|
ad144e6fd3 | ||
|
|
076d77a982 | ||
|
|
5afdda7568 | ||
|
|
6cb57c472c | ||
|
|
e026bac42c | ||
|
|
b29e48d471 | ||
|
|
99cb5e6c40 | ||
|
|
01834903c2 | ||
|
|
2929317972 | ||
|
|
57865d74c6 | ||
|
|
f8f331a576 | ||
|
|
c610e955cd | ||
|
|
53899947a9 | ||
|
|
bf23107e7a | ||
|
|
6784921429 | ||
|
|
a5ca57b8e4 | ||
|
|
fa9703a41e | ||
|
|
e82ef6de4b | ||
|
|
e0cad48734 | ||
|
|
fff6636a9e | ||
|
|
077602c427 | ||
|
|
b2f7b3be6c | ||
|
|
29fd5df16a | ||
|
|
f324122d93 | ||
|
|
82d4628976 | ||
|
|
8ab02d598c | ||
|
|
5b23072bd0 | ||
|
|
7b4cc552fb | ||
|
|
eb188ff02a | ||
|
|
b40c24db70 | ||
|
|
a855f76377 | ||
|
|
c481e7ede4 | ||
|
|
17b4c2156a | ||
|
|
ca6579e032 | ||
|
|
f3e99624e9 | ||
|
|
3b5c2d9b0b | ||
|
|
bab9ddbb57 | ||
|
|
8d4193ce1e | ||
|
|
7d2cf3dbf2 | ||
|
|
9309d61c00 | ||
|
|
4089702060 | ||
|
|
bf2254c753 |
@@ -14,3 +14,12 @@ trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.{java,kt}]
|
||||
indent_size = 4
|
||||
|
||||
[*.xml]
|
||||
indent_size = 2
|
||||
|
||||
[*.{swift,m,mm,h}]
|
||||
indent_size = 4
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#17A0DB</color>
|
||||
<color name="navigationBarColor">#161618</color>
|
||||
<color name="navigationBarColor">#040404</color>
|
||||
<color name="statusBarColor">#040404</color>
|
||||
</resources>
|
||||
@@ -3,6 +3,7 @@
|
||||
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
|
||||
<item name="android:forceDarkAllowed">false</item>
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
<item name="android:statusBarColor">@color/statusBarColor</item>
|
||||
<item name="android:windowDisablePreview">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
tools:node="merge">
|
||||
<meta-data android:name="org.jitsi.meet.sdk.JitsiInitializer"
|
||||
android:value="androidx.startup" />
|
||||
</provider>
|
||||
|
||||
@@ -135,7 +135,12 @@ public class JitsiMeetActivity extends AppCompatActivity
|
||||
JitsiMeetActivityDelegate.onHostResume(this);
|
||||
|
||||
setContentView(R.layout.activity_jitsi_meet);
|
||||
addTopBottomInsets(getWindow(),findViewById(android.R.id.content));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
||||
&& getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
addTopBottomInsets(getWindow(), findViewById(android.R.id.content));
|
||||
}
|
||||
|
||||
this.jitsiView = findViewById(R.id.jitsiView);
|
||||
|
||||
registerForBroadcastMessages();
|
||||
|
||||
@@ -37,7 +37,6 @@ public class JitsiMeetActivityDelegate {
|
||||
* React Native module.
|
||||
*/
|
||||
private static PermissionListener permissionListener;
|
||||
private static Callback permissionsCallback;
|
||||
|
||||
/**
|
||||
* Tells whether or not the permissions request is currently in progress.
|
||||
@@ -142,11 +141,6 @@ public class JitsiMeetActivityDelegate {
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onHostResume(activity, new DefaultHardwareBackBtnHandlerImpl(activity));
|
||||
}
|
||||
|
||||
if (permissionsCallback != null) {
|
||||
permissionsCallback.invoke();
|
||||
permissionsCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,15 +163,10 @@ public class JitsiMeetActivityDelegate {
|
||||
|
||||
public static void onRequestPermissionsResult(
|
||||
final int requestCode, final String[] permissions, final int[] grantResults) {
|
||||
permissionsCallback = new Callback() {
|
||||
@Override
|
||||
public void invoke(Object... args) {
|
||||
if (permissionListener != null
|
||||
&& permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
|
||||
permissionListener = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
// Invoke the callback immediately
|
||||
if (permissionListener != null && permissionListener.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
|
||||
permissionListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void requestPermissions(Activity activity, String[] permissions, int requestCode, PermissionListener listener) {
|
||||
|
||||
@@ -99,6 +99,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
|
||||
|
||||
public static void launch(Context context, HashMap<String, Object> extraData) {
|
||||
List<String> permissionsList = new ArrayList<>();
|
||||
Activity activity = (Activity) context;
|
||||
|
||||
PermissionListener listener = new PermissionListener() {
|
||||
@Override
|
||||
@@ -134,7 +135,7 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
|
||||
|
||||
if (permissionsArray.length > 0) {
|
||||
JitsiMeetActivityDelegate.requestPermissions(
|
||||
(Activity) context,
|
||||
activity,
|
||||
permissionsArray,
|
||||
PERMISSIONS_REQUEST_CODE,
|
||||
listener
|
||||
@@ -159,12 +160,20 @@ public class JitsiMeetOngoingConferenceService extends Service implements Ongoin
|
||||
stopSelf();
|
||||
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE);
|
||||
} else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||
} else {
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Handle ForegroundServiceStartNotAllowedException when app is in background and cannot start foreground service.
|
||||
// See: https://developer.android.com/develop/background-work/services/fgs/restrictions-bg-start#wiu-restrictions
|
||||
JitsiMeetLogger.w(TAG + " Failed to start foreground service", e);
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
android/sdk/src/main/res/values/colors.xml
Normal file
5
android/sdk/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="navigationBarColor">#040404</color>
|
||||
<color name="statusBarColor">#040404</color>
|
||||
</resources>
|
||||
@@ -1,3 +1,6 @@
|
||||
<resources>
|
||||
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar"/>
|
||||
<style name="JitsiMeetActivityStyle" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:navigationBarColor">@color/navigationBarColor</item>
|
||||
<item name="android:statusBarColor">@color/statusBarColor</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
@@ -1375,7 +1375,7 @@ export default {
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRemoteParticipantFeatures(user));
|
||||
logger.log(`USER ${id} connected:`, user);
|
||||
logger.log(`USER ${id} connected`);
|
||||
APP.UI.addUser(user);
|
||||
});
|
||||
|
||||
|
||||
@@ -139,6 +139,9 @@ var config = {
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
|
||||
// Disables chat feature entirely including notifications, sounds, and private messages.
|
||||
// disableChat: false,
|
||||
|
||||
// Disables demote button from self-view
|
||||
// disableSelfDemote: false,
|
||||
|
||||
@@ -904,6 +907,8 @@ var config = {
|
||||
// alwaysVisible: false,
|
||||
// // Indicates whether the toolbar should still autohide when chat is open
|
||||
// autoHideWhileChatIsOpen: false,
|
||||
// // Default background color for the main toolbar. Accepts any valid CSS color.
|
||||
// // backgroundColor: '#ffffff',
|
||||
// },
|
||||
|
||||
// Overrides the buttons displayed in the main toolbar. Depending on the screen size the number of displayed
|
||||
|
||||
@@ -114,6 +114,9 @@
|
||||
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
|
||||
"everyone": "Tutti",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
"fileAccessibleTitle": "{{user}} ha caricato un file",
|
||||
"fileAccessibleTitleMe": "ho caricato un file",
|
||||
"fileDeleted": "Un file è stato eliminato",
|
||||
"guestsChatIndicator": "(ospite)",
|
||||
"lobbyChatMessageTo": "Messaggio a {{recipient}} in sala d'attesa",
|
||||
"message": "Messaggio",
|
||||
@@ -280,7 +283,6 @@
|
||||
"Submit": "Invia",
|
||||
"Understand": "Accetto, mantieni microfono e videocamera disattivati per ora",
|
||||
"UnderstandAndUnmute": "Accetto, riattiva microfono e videocamera",
|
||||
"WaitForHostMsg": "La riunione non è ancora iniziata. Se sei l'organizzatore, per favore autenticati. Altrimenti, attendi l'arrivo dell'organizzatore.",
|
||||
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun organizzatore si è ancora collegato. Si prega di attendere.",
|
||||
"WaitingForHostButton": "Attendi l'organizzatore",
|
||||
"WaitingForHostTitle": "In attesa dell'organizzatore…",
|
||||
@@ -523,6 +525,7 @@
|
||||
"tokenAuthFailedWithReasons": "Non sei autorizzato a partecipare a questa chiamata. Possibile motivo: {{reason}}",
|
||||
"tokenAuthUnsupported": "Il token URL non è supportato.",
|
||||
"transcribing": "Trascrizione in corso",
|
||||
"unauthenticatedAccessDisabled": "Questa chiamata richiede l'autenticazione. Si prega di accedere per procedere.",
|
||||
"unlockRoom": "Rimuovi la $t(lockRoomPassword) alla riunione",
|
||||
"user": "Utente",
|
||||
"userIdentifier": "Identificatore utente",
|
||||
@@ -570,10 +573,12 @@
|
||||
"downloadStarted": "Download del file iniziato",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
|
||||
"fileRemovedByOther": "Il tuo file '{{ fileName }}' è stato rimosso",
|
||||
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Il file selezionato è troppo grande",
|
||||
"fileUploadProgress": "Caricamento del file in corso",
|
||||
"fileUploadedSuccessfully": "Il file è stato caricato con successo",
|
||||
"newFileNotification": "{{ participantName }} ha condiviso '{{ fileName }}'",
|
||||
"removeFile": "Rimuovi",
|
||||
"removeFileSuccess": "File rimosso con successo",
|
||||
"uploadFailedDescription": "Si prega di riprovare.",
|
||||
@@ -748,7 +753,8 @@
|
||||
"notificationTitle": "Sala d'attesa",
|
||||
"passwordJoinButton": "Entra",
|
||||
"title": "Sala d'attesa",
|
||||
"toggleLabel": "Attiva sala d'attesa"
|
||||
"toggleLabel": "Attiva sala d'attesa",
|
||||
"waitForModerator": "La riunione non è ancora iniziata, perché non è arrivato alcun organizzatore. Se vuoi diventarlo autenticati, altrimenti attendi."
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -964,6 +970,9 @@
|
||||
"by": "Da {{ name }}",
|
||||
"closeButton": "Chiudi sondaggio",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "Invia sondaggio"
|
||||
},
|
||||
"addOption": "Aggiungi opzione",
|
||||
"answerPlaceholder": "Opzione {{index}}",
|
||||
"cancel": "Annulla",
|
||||
@@ -972,8 +981,7 @@
|
||||
"pollQuestion": "Domanda del sondaggio",
|
||||
"questionPlaceholder": "Fai una domanda",
|
||||
"removeOption": "Elimina opzione",
|
||||
"save": "Salva",
|
||||
"send": "Invia"
|
||||
"save": "Salva"
|
||||
},
|
||||
"errors": {
|
||||
"notUniqueOption": "Le opzioni devono essere uniche"
|
||||
@@ -1617,6 +1625,8 @@
|
||||
"noMainParticipantsTitle": "La riunione non è ancora iniziata.",
|
||||
"noVisitorLobby": "Non puoi partecipare se la sala d'attesa è attiva per la riunione.",
|
||||
"notAllowedPromotion": "Un partecipante deve autorizzare la tua richiesta prima.",
|
||||
"requestToJoin": "Mano alzata",
|
||||
"requestToJoinDescription": "La tua richiesta è stata inviata ai relatori. Tieni duro!",
|
||||
"title": "Sei uno spettatore nella riunione"
|
||||
},
|
||||
"waitingMessage": "Ti unirai alla riunione quando inizierà!"
|
||||
|
||||
@@ -112,7 +112,12 @@
|
||||
"disabled": "聊天已禁用",
|
||||
"enter": "加入会议室",
|
||||
"error": "错误:你的消息未发送。原因:{{error}}",
|
||||
"everyone": "所有人",
|
||||
"fieldPlaceHolder": "在这里输入你的信息",
|
||||
"fileAccessibleTitle": "{{user}}上传了一个文件",
|
||||
"fileAccessibleTitleMe": "我上传了一个文件",
|
||||
"fileDeleted": "文件已被删除",
|
||||
"guestsChatIndicator": "(访客)",
|
||||
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
|
||||
"message": "信息",
|
||||
"messageAccessibleTitle": "{{user}}:",
|
||||
@@ -300,6 +305,12 @@
|
||||
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
|
||||
"applicationWindow": "应用程序窗口",
|
||||
"authenticationRequired": "需要身份验证",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "使用手机摄像头拍照并发送",
|
||||
"ok": "打开相机",
|
||||
"reject": "暂不使用",
|
||||
"title": "拍照"
|
||||
},
|
||||
"cameraConstraintFailedError": "你的摄像头未满足某些必要条件。",
|
||||
"cameraNotFoundError": "找不到摄像头",
|
||||
"cameraNotSendingData": "我们无法访问你的摄像头,请检查是否有其他应用程序正在使用此设备,从设置菜单中选择另一个设备,或尝试重新加载应用程序。",
|
||||
@@ -375,22 +386,34 @@
|
||||
"micTimeoutError": "无法开启音频设备,发生超时!",
|
||||
"micUnknownError": "由于未知原因,无法使用麦克风。",
|
||||
"moderationAudioLabel": "允许参会者自己解除静音",
|
||||
"moderationDesktopLabel": "允许非主持人共享屏幕",
|
||||
"moderationVideoLabel": "允许参会者自己开启视频",
|
||||
"muteEveryoneDialog": "参会者可以在任何时候解除自己的静音。",
|
||||
"muteEveryoneDialogModerationOn": "参会者可以在任何时候请求发言。",
|
||||
"muteEveryoneElseDialog": "静音后,你将无法为其解除静音,但是他们可以随时解除自己的静音。",
|
||||
"muteEveryoneElseTitle": "除了{{whom}}以外的将所有人静音?",
|
||||
"muteEveryoneElsesDesktopDialog": "一旦停止共享,你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的屏幕共享?",
|
||||
"muteEveryoneElsesVideoDialog": "一旦关闭,你将无法重新开启他们的摄像头,但他们随时可以重新开启。",
|
||||
"muteEveryoneElsesVideoTitle": "除了{{whom}}以外,关闭所有人的摄像头?",
|
||||
"muteEveryoneSelf": "你自己",
|
||||
"muteEveryoneStartMuted": "现在所有人都已静音",
|
||||
"muteEveryoneTitle": "静音所有人?",
|
||||
"muteEveryonesDesktopDialog": "参会者可以随时共享他们的屏幕",
|
||||
"muteEveryonesDesktopDialogModerationOn": "参会者可以随时请求共享他们的屏幕",
|
||||
"muteEveryonesDesktopTitle": "停止所有人的屏幕共享?",
|
||||
"muteEveryonesVideoDialog": "参会者可以随时开启他们的摄像头",
|
||||
"muteEveryonesVideoDialogModerationOn": "参会者可以随时请求开启他们的摄像头",
|
||||
"muteEveryonesVideoDialogOk": "关闭",
|
||||
"muteEveryonesVideoTitle": "关闭所有人的摄像头?",
|
||||
"muteParticipantBody": "你将无法为他们解除静音,但是他们可以随时解除自己的静音。",
|
||||
"muteParticipantButton": "静音",
|
||||
"muteParticipantsDesktopBody": "你无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteParticipantsDesktopBodyModerationOn": "你和他们都无法重新开启屏幕共享",
|
||||
"muteParticipantsDesktopButton": "停止屏幕共享",
|
||||
"muteParticipantsDesktopDialog": "你确定要停止这个参会者的屏幕共享吗?你将无法重新开启他们的屏幕共享,但他们可以随时重新开启。",
|
||||
"muteParticipantsDesktopDialogModerationOn": "你确定要停止这个参会者的屏幕共享吗?你和他们都无法重新开启屏幕共享。",
|
||||
"muteParticipantsDesktopTitle": "停止这个参会者的屏幕共享?",
|
||||
"muteParticipantsVideoBody": "你无法重新开启摄像头,但他们随时可以重新开启。",
|
||||
"muteParticipantsVideoBodyModerationOn": "你和他们都无法重新开启摄像头",
|
||||
"muteParticipantsVideoButton": "关闭摄像头",
|
||||
@@ -503,6 +526,7 @@
|
||||
"tokenAuthFailedWithReasons": "抱歉,你无法加入此通话,原因:",
|
||||
"tokenAuthUnsupported": "Token地址不支持",
|
||||
"transcribing": "转录中",
|
||||
"unauthenticatedAccessDisabled": "此会议需要身份验证,请先登录后继续。",
|
||||
"unlockRoom": "移除会议$t(lockRoomPassword)",
|
||||
"user": "用户",
|
||||
"userIdentifier": "用户ID",
|
||||
@@ -547,11 +571,17 @@
|
||||
"downloadFailedDescription": "请稍后重试",
|
||||
"downloadFailedTitle": "下载失败",
|
||||
"downloadFile": "下载",
|
||||
"downloadStarted": "文件下载已开始",
|
||||
"dragAndDrop": "拖拽文件到此处上传",
|
||||
"fileAlreadyUploaded": "文件已上传至本次会议",
|
||||
"fileRemovedByOther": "你的文件{{fileName}}已被移除",
|
||||
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "文件太大",
|
||||
"fileUploadProgress": "文件上传进度",
|
||||
"fileUploadedSuccessfully": "文件上传成功",
|
||||
"newFileNotification": "{{participantName}}分享了{{fileName}}",
|
||||
"removeFile": "移除",
|
||||
"removeFileSuccess": "文件移除成功",
|
||||
"uploadFailedDescription": "请稍后重试",
|
||||
"uploadFailedTitle": "上传失败",
|
||||
"uploadFile": "文件共享"
|
||||
@@ -724,7 +754,8 @@
|
||||
"notificationTitle": "等候室",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "等候室",
|
||||
"toggleLabel": "开启等候室模式"
|
||||
"toggleLabel": "开启等候室模式",
|
||||
"waitForModerator": "会议尚未开始,暂无主持人入会。如需成为主持人请先登录,或耐心等待会议开始。"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -767,8 +798,10 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAll": "允许全部",
|
||||
"allowAudio": "允许开启麦克风",
|
||||
"allowBoth": "允许音视频",
|
||||
"allowDesktop": "允许屏幕共享",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
|
||||
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
|
||||
@@ -782,6 +815,7 @@
|
||||
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
|
||||
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
|
||||
"dataChannelClosedWithAudio": "音视频质量可能受影响",
|
||||
"desktopMutedRemotelyTitle": "你的屏幕共享已被{{participantDisplayName}}停止",
|
||||
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>!",
|
||||
@@ -839,6 +873,7 @@
|
||||
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端,请确保您更新到我们的",
|
||||
"oldElectronClientDescription2": "最新版本",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"openChat": "打开聊天",
|
||||
"participantWantsToJoin": "想要加入会议",
|
||||
"participantsWantToJoin": "想要加入会议",
|
||||
"passwordRemovedRemotely": "其他参会者移除了$t(lockRoomPasswordUppercase)",
|
||||
@@ -862,6 +897,7 @@
|
||||
"suggestRecordingDescription": "是否需要录制本次会议?",
|
||||
"suggestRecordingTitle": "录制会议",
|
||||
"unmute": "解除静音",
|
||||
"unmuteScreen": "开始屏幕共享",
|
||||
"unmuteVideo": "开启摄像头",
|
||||
"videoMutedRemotelyDescription": "你可随时重新开启视频",
|
||||
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
|
||||
@@ -881,11 +917,14 @@
|
||||
"admit": "同意加入",
|
||||
"admitAll": "全部同意加入",
|
||||
"allow": "允许参会者:",
|
||||
"allowDesktop": "允许屏幕共享",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"askDesktop": "请求共享屏幕",
|
||||
"askUnmute": "请求取消静音",
|
||||
"audioModeration": "自行解除静音",
|
||||
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
|
||||
"breakoutRooms": "分组讨论室",
|
||||
"desktopModeration": "开始屏幕共享",
|
||||
"goLive": "开始直播",
|
||||
"invite": "邀请其他人",
|
||||
"lowerAllHands": "取消全部举手",
|
||||
@@ -897,6 +936,8 @@
|
||||
"muteAll": "全体静音",
|
||||
"muteEveryoneElse": "静音其他人",
|
||||
"reject": "拒绝",
|
||||
"stopDesktop": "停止屏幕共享",
|
||||
"stopEveryonesDesktop": "停止所有人的屏幕共享",
|
||||
"stopEveryonesVideo": "关闭所有人摄像头",
|
||||
"stopVideo": "关闭摄像头",
|
||||
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
|
||||
@@ -906,9 +947,11 @@
|
||||
"headings": {
|
||||
"lobby": "等候室(({{count}}人)",
|
||||
"participantsList": "会议参会者({{count}}人)",
|
||||
"viewerRequests": "观众请求({{count}}人)",
|
||||
"visitorInQueue": "(排队中:{{count}}人)",
|
||||
"visitorRequests": "(请求加入:{{count}}人)",
|
||||
"visitors": "观众(({{count}}人)",
|
||||
"visitorsList": "观众({{count}}人)",
|
||||
"waitingLobby": "在等候室等待({{count}}人)"
|
||||
},
|
||||
"search": "搜索参会者",
|
||||
@@ -929,6 +972,9 @@
|
||||
"by": "由{{ name }}发起",
|
||||
"closeButton": "结束投票",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "发送投票"
|
||||
},
|
||||
"addOption": "添加选项",
|
||||
"answerPlaceholder": "选项{{index}}",
|
||||
"cancel": "取消",
|
||||
@@ -1345,6 +1391,20 @@
|
||||
"videounmute": "打开摄像头"
|
||||
},
|
||||
"addPeople": "添加成员到通话中",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "回声消除"
|
||||
},
|
||||
"agc": {
|
||||
"label": "自动增益控制"
|
||||
},
|
||||
"ns": {
|
||||
"label": "降噪"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "立体声"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "关闭省流模式",
|
||||
"audioOnlyOn": "开启省流模式",
|
||||
"audioRoute": "选择音频设备",
|
||||
@@ -1416,6 +1476,7 @@
|
||||
"reactionHeart": "发送爱心",
|
||||
"reactionLaugh": "发送大笑",
|
||||
"reactionLike": "发送点赞",
|
||||
"reactionLove": "发送爱心",
|
||||
"reactionSilence": "发送沉默",
|
||||
"reactionSurprised": "发送惊讶",
|
||||
"reactions": "互动表情",
|
||||
@@ -1501,6 +1562,8 @@
|
||||
"connectionInfo": "连接信息",
|
||||
"demote": "设为观众",
|
||||
"domute": "静音",
|
||||
"domuteDesktop": "停止屏幕共享",
|
||||
"domuteDesktopOfOthers": "停止屏幕共享给其他人",
|
||||
"domuteOthers": "静音其他人",
|
||||
"domuteVideo": "关闭摄像头",
|
||||
"domuteVideoOfOthers": "关闭其他人摄像头",
|
||||
@@ -1565,6 +1628,8 @@
|
||||
"noMainParticipantsTitle": "会议尚未开始",
|
||||
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
|
||||
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
|
||||
"requestToJoin": "举手请求",
|
||||
"requestToJoinDescription": "你的请求已发送给主持人,请稍候!",
|
||||
"title": "你当前为会议观众"
|
||||
},
|
||||
"waitingMessage": "会议开始后将自动加入"
|
||||
|
||||
@@ -112,7 +112,12 @@
|
||||
"disabled": "聊天訊息已停用",
|
||||
"enter": "加入聊天室",
|
||||
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
|
||||
"everyone": "所有人",
|
||||
"fieldPlaceHolder": "在此輸入您的訊息",
|
||||
"fileAccessibleTitle": "{{user}}上傳了一個檔案",
|
||||
"fileAccessibleTitleMe": "我上傳了一個檔案",
|
||||
"fileDeleted": "檔案已被刪除",
|
||||
"guestsChatIndicator": "(訪客)",
|
||||
"lobbyChatMessageTo": "大廳聊天訊息傳送至 {{recipient}}",
|
||||
"message": "訊息",
|
||||
"messageAccessibleTitle": "{{user}}:",
|
||||
@@ -300,6 +305,12 @@
|
||||
"alreadySharedVideoTitle": "同一時間只允許一位影像分享",
|
||||
"applicationWindow": "應用程式視窗",
|
||||
"authenticationRequired": "需要驗證",
|
||||
"cameraCaptureDialog": {
|
||||
"description": "使用手機攝影機拍照並傳送",
|
||||
"ok": "開啟相機",
|
||||
"reject": "暫不使用",
|
||||
"title": "拍照"
|
||||
},
|
||||
"cameraConstraintFailedError": "您的網路攝影機不符合要求。",
|
||||
"cameraNotFoundError": "找不到網路攝影機。",
|
||||
"cameraNotSendingData": "我們無法存取您的網路攝影機,請檢查是否有其他應用程式正在使用這個裝置,並從裝置選單裡選擇其他設備或者重新載入。",
|
||||
@@ -375,22 +386,34 @@
|
||||
"micTimeoutError": "無法啟動音訊裝置,連線逾時!",
|
||||
"micUnknownError": "不明原因造成麥克風無法使用。",
|
||||
"moderationAudioLabel": "允許與會者自我解除靜音",
|
||||
"moderationDesktopLabel": "允許非主持人共享螢幕",
|
||||
"moderationVideoLabel": "允許與會者開啟視訊",
|
||||
"muteEveryoneDialog": "與會者可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneDialogModerationOn": "與會者可以隨時請求發言。",
|
||||
"muteEveryoneElseDialog": "靜音後,您就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
|
||||
"muteEveryoneElsesDesktopDialog": "一旦停止共享,您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteEveryoneElsesDesktopTitle": "停止除了{{whom}}以外所有人的螢幕共享?",
|
||||
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的網路攝影機,但對方隨時能重新開啟自己的網路攝影機。",
|
||||
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的網路攝影機?",
|
||||
"muteEveryoneSelf": "您自己",
|
||||
"muteEveryoneStartMuted": "現在所有人皆已靜音",
|
||||
"muteEveryoneTitle": "要將所有人靜音嗎?",
|
||||
"muteEveryonesDesktopDialog": "與會者可以隨時共享他們的螢幕",
|
||||
"muteEveryonesDesktopDialogModerationOn": "與會者可以隨時請求共享他們的螢幕",
|
||||
"muteEveryonesDesktopTitle": "停止所有人的螢幕共享?",
|
||||
"muteEveryonesVideoDialog": "與會者隨時可以重新開啟自己的網路攝影機。",
|
||||
"muteEveryonesVideoDialogModerationOn": "與會者可以隨時傳送開啟視訊請求。",
|
||||
"muteEveryonesVideoDialogOk": "停用",
|
||||
"muteEveryonesVideoTitle": "要關閉所有人的網路攝影機嗎?",
|
||||
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
|
||||
"muteParticipantButton": "靜音",
|
||||
"muteParticipantsDesktopBody": "您無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteParticipantsDesktopBodyModerationOn": "您和他們都無法重新開啟螢幕共享",
|
||||
"muteParticipantsDesktopButton": "停止螢幕分享",
|
||||
"muteParticipantsDesktopDialog": "您確定要停止這位與會者的螢幕共享嗎?您將無法重新開啟他們的螢幕共享,但他們可以隨時重新開啟。",
|
||||
"muteParticipantsDesktopDialogModerationOn": "您確定要停止這位與會者的螢幕共享嗎?您和他們都無法重新開啟螢幕共享。",
|
||||
"muteParticipantsDesktopTitle": "停止這位與會者的螢幕共享?",
|
||||
"muteParticipantsVideoBody": "您無法重新開啟,只有對方能自己重新開啟。",
|
||||
"muteParticipantsVideoBodyModerationOn": "您和他都無法再將視訊重新開啟。",
|
||||
"muteParticipantsVideoButton": "停用網路攝影機",
|
||||
@@ -503,6 +526,7 @@
|
||||
"tokenAuthFailedWithReasons": "抱歉,您無法參加這個通話,可能原因:{{reason}}",
|
||||
"tokenAuthUnsupported": "不支援權杖網址。",
|
||||
"transcribing": "轉錄中",
|
||||
"unauthenticatedAccessDisabled": "此會議需要身份驗證,請先登入後繼續。",
|
||||
"unlockRoom": "移除會議 $t(lockRoomPassword)",
|
||||
"user": "使用者",
|
||||
"userIdentifier": "使用者 ID",
|
||||
@@ -547,11 +571,17 @@
|
||||
"downloadFailedDescription": "請重試",
|
||||
"downloadFailedTitle": "下載失敗",
|
||||
"downloadFile": "下載",
|
||||
"downloadStarted": "檔案下載已開始",
|
||||
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
|
||||
"fileAlreadyUploaded": "檔案已上傳至此會議",
|
||||
"fileRemovedByOther": "您的檔案「{{fileName}}」已被移除",
|
||||
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "檔案過大",
|
||||
"fileUploadProgress": "檔案上傳進度",
|
||||
"fileUploadedSuccessfully": "檔案上傳成功",
|
||||
"newFileNotification": "{{participantName}}分享了「{{fileName}}」",
|
||||
"removeFile": "移除",
|
||||
"removeFileSuccess": "檔案移除成功",
|
||||
"uploadFailedDescription": "請重試",
|
||||
"uploadFailedTitle": "上傳失敗",
|
||||
"uploadFile": "分享檔案"
|
||||
@@ -724,7 +754,8 @@
|
||||
"notificationTitle": "大廳",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "大廳",
|
||||
"toggleLabel": "啟用大廳模式"
|
||||
"toggleLabel": "啟用大廳模式",
|
||||
"waitForModerator": "會議尚未開始,暫無主持人入會。如需成為主持人請先登入,或耐心等待會議開始。"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -767,8 +798,10 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAll": "允許全部",
|
||||
"allowAudio": "允許音訊",
|
||||
"allowBoth": "允許音訊與視訊",
|
||||
"allowDesktop": "允許螢幕分享",
|
||||
"allowVideo": "允許視訊",
|
||||
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
|
||||
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
|
||||
@@ -782,6 +815,7 @@
|
||||
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
|
||||
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
|
||||
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
|
||||
"desktopMutedRemotelyTitle": "您的螢幕分享已被{{participantDisplayName}}停止",
|
||||
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
|
||||
"disabledIframeSecondaryNative": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷。",
|
||||
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
|
||||
@@ -839,6 +873,7 @@
|
||||
"oldElectronClientDescription1": "您似乎正在使用存在已知安全漏洞的過時 Jitsi Meet 用戶端,請盡快更新至我們的",
|
||||
"oldElectronClientDescription2": "最新版本",
|
||||
"oldElectronClientDescription3": "!",
|
||||
"openChat": "開啟聊天",
|
||||
"participantWantsToJoin": "希望加入會議",
|
||||
"participantsWantToJoin": "希望加入會議",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) 已被其他與會者移除",
|
||||
@@ -862,6 +897,7 @@
|
||||
"suggestRecordingDescription": "是否要開始錄製這場會議?",
|
||||
"suggestRecordingTitle": "錄製此會議",
|
||||
"unmute": "取消靜音",
|
||||
"unmuteScreen": "開始螢幕分享",
|
||||
"unmuteVideo": "啟用視訊",
|
||||
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
|
||||
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
|
||||
@@ -881,11 +917,14 @@
|
||||
"admit": "準許",
|
||||
"admitAll": "準許所有人",
|
||||
"allow": "允許與會者能夠:",
|
||||
"allowDesktop": "允許螢幕分享",
|
||||
"allowVideo": "允許視訊",
|
||||
"askDesktop": "請求共享螢幕",
|
||||
"askUnmute": "要求解除靜音",
|
||||
"audioModeration": "自我解除靜音",
|
||||
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
|
||||
"breakoutRooms": "分組討論室",
|
||||
"desktopModeration": "開始螢幕分享",
|
||||
"goLive": "開始直播",
|
||||
"invite": "邀請他人",
|
||||
"lowerAllHands": "全部取消舉手",
|
||||
@@ -897,6 +936,8 @@
|
||||
"muteAll": "靜音所有人",
|
||||
"muteEveryoneElse": "靜音其他人",
|
||||
"reject": "拒絕",
|
||||
"stopDesktop": "停止螢幕分享",
|
||||
"stopEveryonesDesktop": "停止所有人的螢幕分享",
|
||||
"stopEveryonesVideo": "停用所有人的視訊",
|
||||
"stopVideo": "停用視訊",
|
||||
"unblockEveryoneMicCamera": "解除封鎖所有人的麥克風及網路攝影機",
|
||||
@@ -906,9 +947,11 @@
|
||||
"headings": {
|
||||
"lobby": "大廳({{count}} 人)",
|
||||
"participantsList": "會議與會者({{count}} 人)",
|
||||
"viewerRequests": "觀眾請求({{count}}人)",
|
||||
"visitorInQueue": "({{count}} 人等候中)",
|
||||
"visitorRequests": "{{count}} 人申請",
|
||||
"visitors": "訪客({{count}} 人)",
|
||||
"visitorsList": "觀眾({{count}}人)",
|
||||
"waitingLobby": "於大廳等候({{count}} 人)"
|
||||
},
|
||||
"search": "搜尋與會者",
|
||||
@@ -929,6 +972,9 @@
|
||||
"by": "由 {{ name }}",
|
||||
"closeButton": "結束投票",
|
||||
"create": {
|
||||
"accessibilityLabel": {
|
||||
"send": "傳送投票"
|
||||
},
|
||||
"addOption": "新增選項",
|
||||
"answerPlaceholder": "選項 {{index}}",
|
||||
"cancel": "取消",
|
||||
@@ -1345,6 +1391,20 @@
|
||||
"videounmute": "啟用網路攝影機"
|
||||
},
|
||||
"addPeople": "新增人員到您的通話中",
|
||||
"advancedAudioSettings": {
|
||||
"aec": {
|
||||
"label": "回聲消除"
|
||||
},
|
||||
"agc": {
|
||||
"label": "自動增益控制"
|
||||
},
|
||||
"ns": {
|
||||
"label": "降噪"
|
||||
},
|
||||
"stereo": {
|
||||
"label": "立體聲"
|
||||
}
|
||||
},
|
||||
"audioOnlyOff": "停用低頻寬模式",
|
||||
"audioOnlyOn": "啟用低頻寬模式",
|
||||
"audioRoute": "選擇音訊裝置",
|
||||
@@ -1416,6 +1476,7 @@
|
||||
"reactionHeart": "傳送愛心反應",
|
||||
"reactionLaugh": "傳送大笑反應",
|
||||
"reactionLike": "傳送比讚反應",
|
||||
"reactionLove": "傳送愛心",
|
||||
"reactionSilence": "傳送沉默反應",
|
||||
"reactionSurprised": "傳送驚訝反應",
|
||||
"reactions": "反應",
|
||||
@@ -1501,6 +1562,8 @@
|
||||
"connectionInfo": "連線資訊",
|
||||
"demote": "轉為訪客",
|
||||
"domute": "靜音",
|
||||
"domuteDesktop": "停止螢幕分享",
|
||||
"domuteDesktopOfOthers": "停止螢幕分享給其他人",
|
||||
"domuteOthers": "靜音其他人",
|
||||
"domuteVideo": "停用網路攝影機",
|
||||
"domuteVideoOfOthers": "停用其他人的網路攝影機",
|
||||
@@ -1565,6 +1628,8 @@
|
||||
"noMainParticipantsTitle": "會議尚未開始",
|
||||
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
|
||||
"notAllowedPromotion": "需由與會者同意您的申請",
|
||||
"requestToJoin": "舉手請求",
|
||||
"requestToJoinDescription": "您的請求已傳送給主持人,請稍候!",
|
||||
"title": "您是會議中的訪客"
|
||||
},
|
||||
"waitingMessage": "會議開始後您將自動加入!"
|
||||
|
||||
@@ -126,8 +126,16 @@
|
||||
"messagebox": "Type a message",
|
||||
"newMessages": "New messages",
|
||||
"nickname": {
|
||||
"featureChat": "chat",
|
||||
"featureClosedCaptions": "closed captions",
|
||||
"featureFileSharing": "file sharing",
|
||||
"featurePolls": "polls",
|
||||
"popover": "Choose a nickname",
|
||||
"title": "Enter a nickname to use chat",
|
||||
"titleWith1Features": "Enter a nickname to use {{feature1}}",
|
||||
"titleWith2Features": "Enter a nickname to use {{feature1}} and {{feature2}}",
|
||||
"titleWith3Features": "Enter a nickname to use {{feature1}}, {{feature2}} and {{feature3}}",
|
||||
"titleWith4Features": "Enter a nickname to use {{feature1}}, {{feature2}}, {{feature3}} and {{feature4}}",
|
||||
"titleWithCC": "Enter a nickname to use chat and closed captions",
|
||||
"titleWithPolls": "Enter a nickname to use chat and polls",
|
||||
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
|
||||
@@ -1429,6 +1437,7 @@
|
||||
"exitFullScreen": "Exit full screen",
|
||||
"exitTileView": "Exit tile view",
|
||||
"feedback": "Leave feedback",
|
||||
"fileSharing": "File sharing",
|
||||
"giphy": "Toggle GIPHY menu",
|
||||
"hangup": "Leave the meeting",
|
||||
"help": "Help",
|
||||
@@ -1464,6 +1473,7 @@
|
||||
"openReactionsMenu": "Open reactions menu",
|
||||
"participants": "Participants",
|
||||
"pip": "Enter Picture-in-Picture mode",
|
||||
"polls": "Polls",
|
||||
"privateMessage": "Send private message",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise your hand",
|
||||
|
||||
@@ -340,6 +340,7 @@ function initCommands() {
|
||||
|
||||
APP.store.dispatch(setAssumedBandwidthBps(value));
|
||||
},
|
||||
|
||||
'set-blurred-background': blurType => {
|
||||
const tracks = APP.store.getState()['features/base/tracks'];
|
||||
const videoTrack = getLocalVideoTrack(tracks)?.jitsiTrack;
|
||||
|
||||
@@ -158,11 +158,10 @@ const VideoLayout = {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const state = APP.store.getState();
|
||||
const currentContainer = largeVideo.getCurrentContainer();
|
||||
const currentContainerType = largeVideo.getCurrentContainerType();
|
||||
const isOnLarge = this.isCurrentlyOnLarge(id);
|
||||
const state = APP.store.getState();
|
||||
const participant = getParticipantById(state, id);
|
||||
const videoTrack = getVideoTrackByParticipant(state, participant);
|
||||
const videoStream = videoTrack?.jitsiTrack;
|
||||
|
||||
1388
package-lock.json
generated
1388
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
"@giphy/react-components": "6.9.4",
|
||||
"@giphy/react-native-sdk": "4.1.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/js-utils": "2.6.7",
|
||||
"@jitsi/logger": "2.1.1",
|
||||
"@jitsi/rnnoise-wasm": "0.2.1",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
@@ -72,7 +72,7 @@
|
||||
"js-md5": "0.6.1",
|
||||
"js-sha512": "0.8.0",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2101.0.0+8061f52a/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2109.0.0+cb9d000c/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -136,7 +136,7 @@
|
||||
"@babel/preset-env": "7.25.9",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@jitsi/eslint-config": "6.0.4",
|
||||
"@react-native-community/cli": "15.0.1",
|
||||
"@react-native-community/cli": "17.0.1",
|
||||
"@react-native-community/cli-platform-android": "15.0.1",
|
||||
"@react-native-community/cli-platform-ios": "15.0.1",
|
||||
"@react-native/babel-preset": "0.77.2",
|
||||
|
||||
@@ -600,31 +600,6 @@ export function createRemoteVideoMenuButtonEvent(buttonName: string, attributes
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The rtcstats websocket onclose event. We send this to amplitude in order
|
||||
* to detect trace ws prematurely closing.
|
||||
*
|
||||
* @param {Object} closeEvent - The event with which the websocket closed.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createRTCStatsTraceCloseEvent(closeEvent: { code: string; reason: string; }) {
|
||||
const event: {
|
||||
action: string;
|
||||
code?: string;
|
||||
reason?: string;
|
||||
source: string;
|
||||
} = {
|
||||
action: 'trace.onclose',
|
||||
source: 'rtcstats'
|
||||
};
|
||||
|
||||
event.code = closeEvent.code;
|
||||
event.reason = closeEvent.reason;
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event indicating that an action related to screen sharing
|
||||
* occurred (e.g. It was started or stopped).
|
||||
|
||||
@@ -139,7 +139,7 @@ function _upgradeRoleStarted(thenableWithCancel: Object) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function hideLoginDialog() {
|
||||
return hideDialog(LoginDialog);
|
||||
return hideDialog('LoginDialog', LoginDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +199,7 @@ export function enableModeratorLogin() {
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function openWaitForOwnerDialog() {
|
||||
return openDialog(WaitForOwnerDialog);
|
||||
return openDialog('WaitForOwnerDialog', WaitForOwnerDialog);
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export function waitForOwner() {
|
||||
* @returns {Action}
|
||||
*/
|
||||
export function openLoginDialog() {
|
||||
return openDialog(LoginDialog);
|
||||
return openDialog('LoginDialog', LoginDialog);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,7 +65,7 @@ export function openTokenAuthUrl(tokenAuthServiceUrl: string): any {
|
||||
|
||||
// Show warning for leaving conference only when in a conference.
|
||||
if (!browser.isElectron() && getState()['features/base/conference'].conference) {
|
||||
dispatch(openDialog(LoginQuestionDialog, {
|
||||
dispatch(openDialog('LoginQuestionDialog', LoginQuestionDialog, {
|
||||
handler: () => {
|
||||
// Give time for the dialog to close.
|
||||
setTimeout(() => redirect(), 500);
|
||||
|
||||
@@ -209,7 +209,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case STOP_WAIT_FOR_OWNER:
|
||||
_clearExistingWaitForOwnerTimeout(store);
|
||||
store.dispatch(hideDialog(WaitForOwnerDialog));
|
||||
store.dispatch(hideDialog('WaitForOwnerDialog', WaitForOwnerDialog));
|
||||
break;
|
||||
|
||||
case UPGRADE_ROLE_FINISHED: {
|
||||
|
||||
@@ -55,6 +55,28 @@ function getFirstGraphemeUpper(word: string) {
|
||||
return splitter.splitGraphemes(word)[0].toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips bracketed annotations from a display name. Handles multiple bracket types like (),
|
||||
* [], and {}.
|
||||
*
|
||||
* @param {string} name - The display name to clean.
|
||||
* @returns {string} The cleaned display name without bracketed annotations.
|
||||
*/
|
||||
function stripBracketedAnnotations(name: string): string {
|
||||
// Match content within any of the bracket types at the end of the string
|
||||
// This regex matches: (...) or [...] or {...} at the end
|
||||
const bracketRegex = /\s*[([{][^)\]}]*[)\]}]$/;
|
||||
|
||||
let cleaned = name;
|
||||
|
||||
// Remove all trailing bracketed annotations (handle multiple occurrences)
|
||||
while (bracketRegex.test(cleaned)) {
|
||||
cleaned = cleaned.replace(bracketRegex, '');
|
||||
}
|
||||
|
||||
return cleaned.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates initials for a simple string.
|
||||
*
|
||||
@@ -64,7 +86,15 @@ function getFirstGraphemeUpper(word: string) {
|
||||
export function getInitials(s?: string) {
|
||||
// We don't want to use the domain part of an email address, if it is one
|
||||
const initialsBasis = split(s, '@')[0];
|
||||
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
|
||||
|
||||
// Strip bracketed annotations (e.g., "(Department)", "[Team]", "{Org}")
|
||||
// to prevent them from being considered as name parts
|
||||
const cleanedName = stripBracketedAnnotations(initialsBasis);
|
||||
|
||||
// Fallback to original if cleaned name is empty
|
||||
const nameForInitials = cleanedName || initialsBasis;
|
||||
|
||||
const [ firstWord, ...remainingWords ] = nameForInitials.split(wordSplitRegex).filter(Boolean);
|
||||
|
||||
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
|
||||
}
|
||||
|
||||
@@ -285,6 +285,7 @@ export interface IConfig {
|
||||
disableAudioLevels?: boolean;
|
||||
disableBeforeUnloadHandlers?: boolean;
|
||||
disableCameraTintForeground?: boolean;
|
||||
disableChat?: boolean;
|
||||
disableChatSmileys?: boolean;
|
||||
disableDeepLinking?: boolean;
|
||||
disableFilmstripAutohiding?: boolean;
|
||||
@@ -393,6 +394,7 @@ export interface IConfig {
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
stageFilmstripParticipants?: number;
|
||||
};
|
||||
flags?: {
|
||||
ssrcRewritingEnabled: boolean;
|
||||
@@ -616,6 +618,10 @@ export interface IConfig {
|
||||
toolbarConfig?: {
|
||||
alwaysVisible?: boolean;
|
||||
autoHideWhileChatIsOpen?: boolean;
|
||||
/**
|
||||
* Background color for the main toolbar. Accepts any valid CSS color.
|
||||
*/
|
||||
backgroundColor?: string;
|
||||
initialTimeout?: number;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
@@ -94,6 +94,7 @@ export default [
|
||||
'disableAudioLevels',
|
||||
'disableBeforeUnloadHandlers',
|
||||
'disableCameraTintForeground',
|
||||
'disableChat',
|
||||
'disableChatSmileys',
|
||||
'disableDeepLinking',
|
||||
'disabledNotifications',
|
||||
|
||||
@@ -14,6 +14,7 @@ import logger from './logger';
|
||||
/**
|
||||
* Signals Dialog to close its dialog.
|
||||
*
|
||||
* @param {string|undefined} name - The name of the component for logging purposes.
|
||||
* @param {Object} [component] - The {@code Dialog} component to close/hide. If
|
||||
* {@code undefined}, closes/hides {@code Dialog} regardless of which
|
||||
* component it's rendering; otherwise, closes/hides {@code Dialog} only if
|
||||
@@ -23,8 +24,8 @@ import logger from './logger';
|
||||
* component: (React.Component | undefined)
|
||||
* }}
|
||||
*/
|
||||
export function hideDialog(component?: ComponentType<any>) {
|
||||
logger.info(`Hide dialog: ${getComponentDisplayName(component)}`);
|
||||
export function hideDialog(name?: string, component?: ComponentType<any>) {
|
||||
logger.info(`Hide dialog: ${name}`);
|
||||
|
||||
return {
|
||||
type: HIDE_DIALOG,
|
||||
@@ -48,6 +49,7 @@ export function hideSheet() {
|
||||
/**
|
||||
* Signals Dialog to open dialog.
|
||||
*
|
||||
* @param {string} name - The name of the component for logging purposes.
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} [componentProps] - The React {@code Component} props of the
|
||||
* specified {@code component}.
|
||||
@@ -57,8 +59,8 @@ export function hideSheet() {
|
||||
* componentProps: (Object | undefined)
|
||||
* }}
|
||||
*/
|
||||
export function openDialog(component: ComponentType<any>, componentProps?: Object) {
|
||||
logger.info(`Open dialog: ${getComponentDisplayName(component)}`);
|
||||
export function openDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
|
||||
logger.info(`Open dialog: ${name}`);
|
||||
|
||||
return {
|
||||
type: OPEN_DIALOG,
|
||||
@@ -92,35 +94,18 @@ export function openSheet(component: ComponentType<any>, componentProps?: Object
|
||||
* is not already open. If it is open, then Dialog is signaled to close its
|
||||
* dialog.
|
||||
*
|
||||
* @param {string} name - The name of the component for logging purposes.
|
||||
* @param {Object} component - The component to display as dialog.
|
||||
* @param {Object} [componentProps] - The React {@code Component} props of the
|
||||
* specified {@code component}.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleDialog(component: ComponentType<any>, componentProps?: Object) {
|
||||
export function toggleDialog(name: string, component: ComponentType<any>, componentProps?: Object) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (isDialogOpen(getState, component)) {
|
||||
dispatch(hideDialog(component));
|
||||
dispatch(hideDialog(name, component));
|
||||
} else {
|
||||
dispatch(openDialog(component, componentProps));
|
||||
dispatch(openDialog(name, component, componentProps));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a printable name for a dialog component.
|
||||
*
|
||||
* @param {Object} component - The component to extract the name for.
|
||||
*
|
||||
* @returns {string} The display name.
|
||||
*/
|
||||
function getComponentDisplayName(component?: ComponentType<any>) {
|
||||
if (!component) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const name = component.displayName ?? component.name ?? 'Component';
|
||||
|
||||
return name.replace('withI18nextTranslation(Connect(', '') // dialogs with translations
|
||||
.replace('))', ''); // dialogs with translations suffix
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ const options: i18next.InitOptions = {
|
||||
interpolation: {
|
||||
escapeValue: false // not needed for react as it escapes by default
|
||||
},
|
||||
load: 'languageOnly',
|
||||
load: 'all',
|
||||
ns: [ 'main', 'languages', 'countries', 'translation-languages' ],
|
||||
react: {
|
||||
// re-render when a new resource bundle is added
|
||||
|
||||
@@ -186,6 +186,9 @@ function _initLogging({ dispatch, getState }: IStore,
|
||||
Logger.addGlobalTransport(debugLogCollector);
|
||||
JitsiMeetJS.addGlobalLogTransport(debugLogCollector);
|
||||
debugLogCollector.start();
|
||||
|
||||
Logger.removeGlobalTransport(console);
|
||||
JitsiMeetJS.removeGlobalLogTransport(console);
|
||||
}
|
||||
} else if (logCollector && loggingConfig.disableLogCollector) {
|
||||
Logger.removeGlobalTransport(logCollector);
|
||||
|
||||
@@ -26,7 +26,7 @@ MiddlewareRegistry.register((store: IStore) => (next: Function) => (action: AnyA
|
||||
case SET_VIDEO_MUTED: {
|
||||
if (LocalRecordingManager.isRecordingLocally() && LocalRecordingManager.selfRecording.on) {
|
||||
if (action.muted && LocalRecordingManager.selfRecording.withVideo) {
|
||||
dispatch(openDialog(StopRecordingDialog, { localRecordingVideoStop: true }));
|
||||
dispatch(openDialog('StopRecordingDialog', StopRecordingDialog, { localRecordingVideoStop: true }));
|
||||
|
||||
return;
|
||||
} else if (!action.muted && !LocalRecordingManager.selfRecording.withVideo) {
|
||||
|
||||
@@ -827,17 +827,30 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
* Checks if private chat is enabled for the given participant or local participant.
|
||||
*
|
||||
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @param {boolean} [checkSelf=false] - Whether to check for local participant's ability to send messages.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
|
||||
export function isPrivateChatEnabled(
|
||||
participant: IParticipant | IVisitorChatParticipant | undefined,
|
||||
state: IReduxState,
|
||||
checkSelf: boolean = false
|
||||
): boolean {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
|
||||
// If checking self capability (if the local participant can send messages) ignore the local participant blocking rule
|
||||
const isLocal = !isVisitorChatParticipant(participant) && participant?.local;
|
||||
|
||||
if (isLocal && !checkSelf) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if private chat is disabled globally
|
||||
if (disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -857,3 +870,15 @@ export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatPar
|
||||
|
||||
return !disablePrivateChat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the local participant (can they send private messages).
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if the local participant can send private messages, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabledSelf(state: IReduxState): boolean {
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
return isPrivateChatEnabled(localParticipant, state, true);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
isRemoteScreenshareParticipant,
|
||||
isScreenShareParticipant
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
|
||||
|
||||
/**
|
||||
@@ -364,6 +365,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
|
||||
logger.debug('Remote screenshare participant joined', id);
|
||||
}
|
||||
|
||||
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
|
||||
@@ -448,6 +451,8 @@ ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
|
||||
sortedRemoteVirtualScreenshareParticipants.delete(id);
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
|
||||
logger.debug('Remote screenshare participant left', id);
|
||||
}
|
||||
|
||||
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getRemoteScreensharesBasedOnPresence,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { FakeParticipant } from './types';
|
||||
|
||||
StateListenerRegistry.register(
|
||||
@@ -69,14 +70,19 @@ function _createOrRemoveVirtualParticipants(
|
||||
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
|
||||
|
||||
if (removedScreenshareSourceNames.length) {
|
||||
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
|
||||
fakeParticipant: FakeParticipant.RemoteScreenShare
|
||||
})));
|
||||
removedScreenshareSourceNames.forEach(id => {
|
||||
logger.debug('Dispatching participantLeft for virtual screenshare', id);
|
||||
dispatch(participantLeft(id, conference, {
|
||||
fakeParticipant: FakeParticipant.RemoteScreenShare
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (addedScreenshareSourceNames.length) {
|
||||
addedScreenshareSourceNames.forEach(id => dispatch(
|
||||
createVirtualScreenshareParticipant(id, false, conference)));
|
||||
addedScreenshareSourceNames.forEach(id => {
|
||||
logger.debug('Creating virtual screenshare participant', id);
|
||||
dispatch(createVirtualScreenshareParticipant(id, false, conference));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isTrackStreamingStatusActive } from '../../connection-indicator/functions';
|
||||
import { handleToggleVideoMuted } from '../../toolbox/actions.any';
|
||||
import { muteLocal } from '../../video-menu/actions.any';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from '../media/constants';
|
||||
import { getParticipantById, isScreenShareParticipant } from '../participants/functions';
|
||||
import {
|
||||
@@ -78,3 +80,43 @@ export function isRemoteVideoReceived({ getState }: IStore, id: string): boolean
|
||||
|
||||
return Boolean(videoTrack && !videoTrack.muted && isTrackStreamingStatusActive(videoTrack));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the local audio. Same as clicking the audio mute button.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {Promise} Resolves when the action is complete.
|
||||
*/
|
||||
export function audioMute({ dispatch }: IStore) {
|
||||
return dispatch(muteLocal(true, MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmutes the local audio. Same as clicking the audio unmute button.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {Promise} Resolves when the action is complete.
|
||||
*/
|
||||
export function audioUnmute({ dispatch }: IStore) {
|
||||
return dispatch(muteLocal(false, MEDIA_TYPE.AUDIO));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes the local video. Same as clicking the video mute button.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {Promise} Resolves when the action is complete.
|
||||
*/
|
||||
export function videoMute({ dispatch }: IStore) {
|
||||
return dispatch(handleToggleVideoMuted(true, true, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unmutes the local video. Same as clicking the video unmute button.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @returns {Promise} Resolves when the action is complete.
|
||||
*/
|
||||
export function videoUnmute({ dispatch }: IStore) {
|
||||
return dispatch(handleToggleVideoMuted(false, true, true));
|
||||
}
|
||||
|
||||
@@ -8,11 +8,15 @@ import { getJitsiMeetGlobalNS } from '../util/helpers';
|
||||
|
||||
import { setConnectionState } from './actions';
|
||||
import {
|
||||
audioMute,
|
||||
audioUnmute,
|
||||
getLocalCameraEncoding,
|
||||
getRemoteVideoType,
|
||||
isLargeVideoReceived,
|
||||
isRemoteVideoReceived,
|
||||
isTestModeEnabled
|
||||
isTestModeEnabled,
|
||||
videoMute,
|
||||
videoUnmute
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
@@ -85,10 +89,14 @@ function _bindTortureHelpers(store: IStore) {
|
||||
|
||||
// All torture helper methods go in here
|
||||
getJitsiMeetGlobalNS().testing = {
|
||||
audioMute: audioMute.bind(null, store),
|
||||
audioUnmute: audioUnmute.bind(null, store),
|
||||
getRemoteVideoType: getRemoteVideoType.bind(null, store),
|
||||
isLargeVideoReceived: isLargeVideoReceived.bind(null, store),
|
||||
getLocalCameraEncoding: getLocalCameraEncoding.bind(null, store),
|
||||
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store)
|
||||
isRemoteVideoReceived: isRemoteVideoReceived.bind(null, store),
|
||||
videoMute: videoMute.bind(null, store),
|
||||
videoUnmute: videoUnmute.bind(null, store),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ export function setCameraFacingMode(facingMode: string | undefined) {
|
||||
* @returns {Object} - The open dialog action.
|
||||
*/
|
||||
export function openAllowToggleCameraDialog(onAllow: Function, initiatorId: string) {
|
||||
return openDialog(AllowToggleCameraDialog, {
|
||||
return openDialog('AllowToggleCameraDialog', AllowToggleCameraDialog, {
|
||||
onAllow,
|
||||
initiatorId
|
||||
});
|
||||
|
||||
@@ -211,18 +211,21 @@ export function getLocalJitsiAudioTrackSettings(state: IReduxState) {
|
||||
const jitsiTrack = getLocalJitsiAudioTrack(state);
|
||||
|
||||
if (!jitsiTrack) {
|
||||
const config = state['features/base/config'];
|
||||
const disableAP = Boolean(config?.disableAP);
|
||||
const disableAGC = Boolean(config?.disableAGC);
|
||||
const disableAEC = Boolean(config?.disableAEC);
|
||||
const disableNS = Boolean(config?.disableNS);
|
||||
const stereo = Boolean(config?.audioQuality?.stereo);
|
||||
const {
|
||||
audioQuality,
|
||||
disableAEC = false,
|
||||
disableAGC = false,
|
||||
disableAP = false,
|
||||
disableNS = false
|
||||
} = state['features/base/config'] || {};
|
||||
|
||||
const enableStereo = Boolean(audioQuality?.stereo);
|
||||
|
||||
return {
|
||||
autoGainControl: !disableAP && !disableAGC,
|
||||
channelCount: stereo ? 2 : 1,
|
||||
echoCancellation: !disableAP && !disableAEC,
|
||||
noiseSuppression: !disableAP && !disableNS
|
||||
autoGainControl: enableStereo ? false : !disableAP && !disableAGC,
|
||||
channelCount: enableStereo ? 2 : 1,
|
||||
echoCancellation: enableStereo ? false : !disableAP && !disableAEC,
|
||||
noiseSuppression: enableStereo ? false : !disableAP && !disableNS
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
|
||||
}, [ dispatch, room ]);
|
||||
|
||||
const onRenameBreakoutRoom = useCallback(() => {
|
||||
dispatch(openDialog(BreakoutRoomNamePrompt, {
|
||||
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
|
||||
breakoutRoomJid: room.jid,
|
||||
initialRoomName: room.name
|
||||
}));
|
||||
|
||||
@@ -22,7 +22,7 @@ export * from './actions.any';
|
||||
* }}
|
||||
*/
|
||||
export function openUpdateCalendarEventDialog(eventId: string) {
|
||||
return openDialog(UpdateCalendarEventDialog, { eventId });
|
||||
return openDialog('UpdateCalendarEventDialog', UpdateCalendarEventDialog, { eventId });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -220,6 +220,34 @@ export function openCCPanel() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the chat panel with polls tab active.
|
||||
*
|
||||
* @returns {Object} The redux action.
|
||||
*/
|
||||
export function openPollsPanel() {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
dispatch(setFocusedTab(ChatTabs.POLLS));
|
||||
dispatch({
|
||||
type: OPEN_CHAT
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the chat panel with file sharing tab active.
|
||||
*
|
||||
* @returns {Object} The redux action.
|
||||
*/
|
||||
export function openFileSharingPanel() {
|
||||
return async (dispatch: IStore['dispatch']) => {
|
||||
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
|
||||
dispatch({
|
||||
type: OPEN_CHAT
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initiates the sending of messages between a moderator and a lobby attendee.
|
||||
|
||||
@@ -1,30 +1,34 @@
|
||||
import { IStore } from '../app/types';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { navigate } from '../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { screen } from '../mobile/navigation/routes';
|
||||
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
import { setFocusedTab } from './actions.any';
|
||||
import { ChatTabs } from './constants';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Displays the chat panel.
|
||||
* Displays the chat panel with the CHAT tab active.
|
||||
*
|
||||
* @param {Object} participant - The recipient for the private chat.
|
||||
* @param {boolean} disablePolls - Checks if polls are disabled.
|
||||
*
|
||||
* @returns {{
|
||||
* participant: participant,
|
||||
* type: OPEN_CHAT
|
||||
* }}
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function openChat(participant?: IParticipant | undefined | Object, disablePolls?: boolean) {
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
}
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
if (disablePolls) {
|
||||
navigate(screen.conference.chat);
|
||||
} else {
|
||||
navigate(screen.conference.chatandpolls.main);
|
||||
}
|
||||
|
||||
return {
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
dispatch({
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,12 +8,13 @@ import {
|
||||
SET_CHAT_WIDTH,
|
||||
SET_USER_CHAT_WIDTH
|
||||
} from './actionTypes';
|
||||
import { closeChat } from './actions.any';
|
||||
import { closeChat, setFocusedTab } from './actions.any';
|
||||
import { ChatTabs } from './constants';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
/**
|
||||
* Displays the chat panel.
|
||||
* Displays the chat panel with the CHAT tab active.
|
||||
*
|
||||
* @param {Object} participant - The recipient for the private chat.
|
||||
* @param {Object} _disablePolls - Used on native.
|
||||
@@ -24,6 +25,7 @@ export * from './actions.any';
|
||||
*/
|
||||
export function openChat(participant?: Object, _disablePolls?: boolean) {
|
||||
return function(dispatch: IStore['dispatch']) {
|
||||
dispatch(setFocusedTab(ChatTabs.CHAT));
|
||||
dispatch({
|
||||
participant,
|
||||
type: OPEN_CHAT
|
||||
|
||||
@@ -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, getUnreadFilesCount, isChatDisabled } from '../../functions';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
@@ -65,7 +65,7 @@ class ChatButton extends AbstractButton<IProps> {
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const enabled = getFeatureFlag(state, CHAT_ENABLED, true);
|
||||
const enabled = getFeatureFlag(state, CHAT_ENABLED, true) && !isChatDisabled(state);
|
||||
const { visible = enabled } = ownProps;
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant, getRemoteParticipants } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, getRemoteParticipants, isPrivateChatEnabledSelf } from '../../../base/participants/functions';
|
||||
import Select from '../../../base/ui/components/web/Select';
|
||||
import Tabs from '../../../base/ui/components/web/Tabs';
|
||||
import { arePollsDisabled } from '../../../conference/functions.any';
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
toggleChat
|
||||
} from '../../actions.web';
|
||||
import { CHAT_SIZE, ChatTabs, OPTION_GROUPCHAT, SMALL_WIDTH_THRESHOLD } from '../../constants';
|
||||
import { getChatMaxSize } from '../../functions';
|
||||
import { getChatMaxSize, getFocusedTab, isChatDisabled } from '../../functions';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
|
||||
import ChatHeader from './ChatHeader';
|
||||
@@ -41,13 +41,18 @@ interface IProps extends AbstractProps {
|
||||
/**
|
||||
* The currently focused tab.
|
||||
*/
|
||||
_focusedTab: ChatTabs;
|
||||
_focusedTab?: ChatTabs;
|
||||
|
||||
/**
|
||||
* True if the CC tab is enabled and false otherwise.
|
||||
*/
|
||||
_isCCTabEnabled: boolean;
|
||||
|
||||
/**
|
||||
* True if chat is disabled.
|
||||
*/
|
||||
_isChatDisabled: boolean;
|
||||
|
||||
/**
|
||||
* True if file sharing tab is enabled.
|
||||
*/
|
||||
@@ -217,6 +222,7 @@ const Chat = ({
|
||||
_isOpen,
|
||||
_isPollsEnabled,
|
||||
_isCCTabEnabled,
|
||||
_isChatDisabled,
|
||||
_isFileSharingTabEnabled,
|
||||
_focusedTab,
|
||||
_isResizing,
|
||||
@@ -229,6 +235,11 @@ const Chat = ({
|
||||
dispatch,
|
||||
t
|
||||
}: IProps) => {
|
||||
// If no tabs are available, don't render the chat panel at all.
|
||||
if (_isChatDisabled && !_isPollsEnabled && !_isCCTabEnabled && !_isFileSharingTabEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width });
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
@@ -242,6 +253,7 @@ const Chat = ({
|
||||
} = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const privateMessageRecipient = useSelector((state: IReduxState) => state['features/chat'].privateMessageRecipient);
|
||||
const participants = useSelector(getRemoteParticipants);
|
||||
const isPrivateChatAllowed = useSelector((state: IReduxState) => isPrivateChatEnabledSelf(state));
|
||||
|
||||
const options = useMemo(() => {
|
||||
const o = Array.from(participants?.values() || [])
|
||||
@@ -415,7 +427,7 @@ const Chat = ({
|
||||
return (
|
||||
<>
|
||||
{renderTabs()}
|
||||
<div
|
||||
{!_isChatDisabled && (<div
|
||||
aria-labelledby = { ChatTabs.CHAT }
|
||||
className = { cx(
|
||||
classes.chatPanel,
|
||||
@@ -431,15 +443,17 @@ const Chat = ({
|
||||
<MessageContainer
|
||||
messages = { _messages } />
|
||||
<MessageRecipient />
|
||||
<Select
|
||||
containerClassName = { cx(classes.privateMessageRecipientsList) }
|
||||
id = 'select-chat-recipient'
|
||||
onChange = { onSelectedRecipientChange }
|
||||
options = { options }
|
||||
value = { privateMessageRecipient?.id || OPTION_GROUPCHAT } />
|
||||
{isPrivateChatAllowed && (
|
||||
<Select
|
||||
containerClassName = { cx(classes.privateMessageRecipientsList) }
|
||||
id = 'select-chat-recipient'
|
||||
onChange = { onSelectedRecipientChange }
|
||||
options = { options }
|
||||
value = { privateMessageRecipient?.id || OPTION_GROUPCHAT } />
|
||||
)}
|
||||
<ChatInput
|
||||
onSend = { onSendMessage } />
|
||||
</div>
|
||||
</div>) }
|
||||
{ _isPollsEnabled && (
|
||||
<>
|
||||
<div
|
||||
@@ -481,8 +495,18 @@ const Chat = ({
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function renderTabs() {
|
||||
let tabs = [
|
||||
{
|
||||
// The only way focused tab will be undefined is when no tab is enabled. Therefore this function won't be
|
||||
// executed because Chat component won't render anything. This should never happen but adding the check
|
||||
// here to make TS happy (when passing the _focusedTab in the selected prop for Tabs).
|
||||
if (!_focusedTab) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let tabs = [];
|
||||
|
||||
// Only add chat tab if chat is not disabled.
|
||||
if (!_isChatDisabled) {
|
||||
tabs.push({
|
||||
accessibilityLabel: t('chat.tabs.chat'),
|
||||
countBadge:
|
||||
_focusedTab !== ChatTabs.CHAT && _unreadMessagesCount > 0 ? _unreadMessagesCount : undefined,
|
||||
@@ -490,8 +514,8 @@ const Chat = ({
|
||||
controlsId: `${ChatTabs.CHAT}-panel`,
|
||||
icon: IconMessage,
|
||||
title: t('chat.tabs.chat')
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
if (_isPollsEnabled) {
|
||||
tabs.push({
|
||||
@@ -561,6 +585,8 @@ const Chat = ({
|
||||
{_showNamePrompt
|
||||
? <DisplayNameForm
|
||||
isCCTabEnabled = { _isCCTabEnabled }
|
||||
isChatDisabled = { _isChatDisabled }
|
||||
isFileSharingEnabled = { _isFileSharingTabEnabled }
|
||||
isPollsEnabled = { _isPollsEnabled } />
|
||||
: renderChat()}
|
||||
<div
|
||||
@@ -599,7 +625,7 @@ const Chat = ({
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { isOpen, focusedTab, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
|
||||
const { isOpen, messages, unreadMessagesCount, unreadFilesCount, width, isResizing } = state['features/chat'];
|
||||
const { unreadPollsCount } = state['features/polls'];
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
|
||||
@@ -608,8 +634,9 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
_isOpen: isOpen,
|
||||
_isPollsEnabled: !arePollsDisabled(state),
|
||||
_isCCTabEnabled: isCCTabEnabled(state),
|
||||
_isChatDisabled: isChatDisabled(state),
|
||||
_isFileSharingTabEnabled: isFileSharingEnabled(state),
|
||||
_focusedTab: focusedTab,
|
||||
_focusedTab: getFocusedTab(state),
|
||||
_messages: messages,
|
||||
_unreadMessagesCount: unreadMessagesCount,
|
||||
_unreadPollsCount: unreadPollsCount,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IconMessage } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { closeOverflowMenuIfOpen } from '../../../toolbox/actions.web';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import { isChatDisabled } from '../../functions';
|
||||
|
||||
import ChatCounter from './ChatCounter';
|
||||
|
||||
@@ -91,7 +92,8 @@ class ChatButton extends AbstractButton<IProps> {
|
||||
*/
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
return {
|
||||
_chatOpen: state['features/chat'].isOpen
|
||||
_chatOpen: state['features/chat'].isOpen,
|
||||
visible: !isChatDisabled(state)
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import { getFocusedTab, isChatDisabled } from '../../functions';
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -40,7 +40,8 @@ interface IProps {
|
||||
function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
|
||||
const _isChatDisabled = useSelector(isChatDisabled);
|
||||
const focusedTab = useSelector(getFocusedTab);
|
||||
const fileSharingTabEnabled = useSelector(isFileSharingEnabled);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
@@ -56,7 +57,7 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
|
||||
|
||||
let title = 'chat.title';
|
||||
|
||||
if (focusedTab === ChatTabs.CHAT) {
|
||||
if (!_isChatDisabled && focusedTab === ChatTabs.CHAT) {
|
||||
title = 'chat.tabs.chat';
|
||||
} else if (isPollsEnabled && focusedTab === ChatTabs.POLLS) {
|
||||
title = 'chat.tabs.polls';
|
||||
@@ -64,6 +65,11 @@ function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
|
||||
title = 'chat.tabs.closedCaptions';
|
||||
} else if (fileSharingTabEnabled && focusedTab === ChatTabs.FILE_SHARING) {
|
||||
title = 'chat.tabs.fileSharing';
|
||||
} else {
|
||||
// If the focused tab is not enabled, don't render the header.
|
||||
// This should not happen in normal circumstances since Chat.tsx already checks
|
||||
// if any tabs are available before rendering.
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -25,6 +25,16 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
isCCTabEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether chat is disabled.
|
||||
*/
|
||||
isChatDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether file sharing is enabled.
|
||||
*/
|
||||
isFileSharingEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the polls feature is enabled or not.
|
||||
*/
|
||||
@@ -74,18 +84,31 @@ class DisplayNameForm extends Component<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const { isCCTabEnabled, isPollsEnabled, t } = this.props;
|
||||
const { isCCTabEnabled, isChatDisabled, isFileSharingEnabled, isPollsEnabled, t } = this.props;
|
||||
|
||||
let title = 'chat.nickname.title';
|
||||
// Build array of enabled feature names (translated).
|
||||
const features = [
|
||||
!isChatDisabled ? t('chat.nickname.featureChat') : '',
|
||||
isPollsEnabled ? t('chat.nickname.featurePolls') : '',
|
||||
isFileSharingEnabled ? t('chat.nickname.featureFileSharing') : '',
|
||||
isCCTabEnabled ? t('chat.nickname.featureClosedCaptions') : ''
|
||||
].filter(Boolean);
|
||||
|
||||
if (isCCTabEnabled && isPollsEnabled) {
|
||||
title = 'chat.nickname.titleWithPollsAndCC';
|
||||
} else if (isCCTabEnabled) {
|
||||
title = 'chat.nickname.titleWithCC';
|
||||
} else if (isPollsEnabled) {
|
||||
title = 'chat.nickname.titleWithPolls';
|
||||
// Return null if no features available - component won't render.
|
||||
if (features.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build translation arguments dynamically: { feature1: "chat", feature2: "polls", ... }
|
||||
const translationArgs = features.reduce((acc, feature, index) => {
|
||||
acc[`feature${index + 1}`] = feature;
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
// Use dynamic translation key: "titleWith1Features", "titleWith2Features", etc.
|
||||
const title = t(`chat.nickname.titleWith${features.length}Features`, translationArgs);
|
||||
|
||||
return (
|
||||
<div id = 'nickname'>
|
||||
<form onSubmit = { this._onSubmit }>
|
||||
|
||||
@@ -12,11 +12,14 @@ import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { getParticipantById, isPrivateChatEnabled } from '../base/participants/functions';
|
||||
import { IParticipant } from '../base/participants/types';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
import { arePollsDisabled } from '../conference/functions.any';
|
||||
import { isFileSharingEnabled } from '../file-sharing/functions.any';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { isCCTabEnabled } from '../subtitles/functions.any';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
import { IVisitorChatParticipant } from '../visitors/types';
|
||||
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
|
||||
import { ChatTabs, MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
|
||||
import { IMessage } from './types';
|
||||
|
||||
/**
|
||||
@@ -153,6 +156,53 @@ export function areSmileysDisabled(state: IReduxState) {
|
||||
return disableChatSmileys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the chat feature is disabled.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} True if chat is disabled, false otherwise.
|
||||
*/
|
||||
export function isChatDisabled(state: IReduxState): boolean {
|
||||
return Boolean(state['features/base/config']?.disableChat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default focused tab based on what features are enabled.
|
||||
* Returns the first available tab in priority order: CHAT -> POLLS -> FILE_SHARING -> CLOSED_CAPTIONS.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {ChatTabs | undefined} The default focused tab.
|
||||
*/
|
||||
export function getDefaultFocusedTab(state: IReduxState): ChatTabs | undefined {
|
||||
if (!isChatDisabled(state)) {
|
||||
return ChatTabs.CHAT;
|
||||
}
|
||||
|
||||
if (!arePollsDisabled(state)) {
|
||||
return ChatTabs.POLLS;
|
||||
}
|
||||
|
||||
if (isFileSharingEnabled(state)) {
|
||||
return ChatTabs.FILE_SHARING;
|
||||
}
|
||||
|
||||
if (isCCTabEnabled(state)) {
|
||||
return ChatTabs.CLOSED_CAPTIONS;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently focused tab or the default focused tab if none is set.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {ChatTabs | undefined} The focused tab or undefined if no tabs are available.
|
||||
*/
|
||||
export function getFocusedTab(state: IReduxState): ChatTabs | undefined {
|
||||
return state['features/chat'].focusedTab || getDefaultFocusedTab(state);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the timestamp to display for the message.
|
||||
*
|
||||
|
||||
23
react/features/chat/hooks.web.ts
Normal file
23
react/features/chat/hooks.web.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ChatButton from './components/web/ChatButton';
|
||||
import { isChatDisabled } from './functions';
|
||||
|
||||
const chat = {
|
||||
key: 'chat',
|
||||
Content: ChatButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the chat button if chat is not disabled.
|
||||
*
|
||||
* @returns {Object | undefined} - The chat button object or undefined.
|
||||
*/
|
||||
export function useChatButton() {
|
||||
const _isChatDisabled = useSelector(isChatDisabled);
|
||||
|
||||
if (!_isChatDisabled) {
|
||||
return chat;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,8 @@ import { IParticipant } from '../base/participants/types';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds/actions';
|
||||
import { arePollsDisabled } from '../conference/functions.any';
|
||||
import { isFileSharingEnabled } from '../file-sharing/functions.any';
|
||||
import { addGif } from '../gifs/actions';
|
||||
import { extractGifURL, getGifDisplayMode, isGifEnabled, isGifMessage } from '../gifs/function.any';
|
||||
import { showMessageNotification } from '../notifications/actions';
|
||||
@@ -34,6 +36,7 @@ import { ADD_REACTION_MESSAGE } from '../reactions/actionTypes';
|
||||
import { pushReactions } from '../reactions/actions.any';
|
||||
import { ENDPOINT_REACTION_NAME } from '../reactions/constants';
|
||||
import { getReactionMessageFromBuffer, isReactionsEnabled } from '../reactions/functions.any';
|
||||
import { isCCTabEnabled } from '../subtitles/functions.any';
|
||||
import { showToolbox } from '../toolbox/actions';
|
||||
import { getDisplayName } from '../visitors/functions';
|
||||
|
||||
@@ -66,7 +69,9 @@ import {
|
||||
} from './constants';
|
||||
import {
|
||||
getDisplayNameSuffix,
|
||||
getFocusedTab,
|
||||
getUnreadCount,
|
||||
isChatDisabled,
|
||||
isSendGroupChatDisabled,
|
||||
isVisitorChatParticipant
|
||||
} from './functions';
|
||||
@@ -181,23 +186,28 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case SET_FOCUSED_TAB:
|
||||
case OPEN_CHAT: {
|
||||
const focusedTab = action.tabId || getState()['features/chat'].focusedTab;
|
||||
const state = store.getState();
|
||||
const focusedTab = action.tabId || getFocusedTab(state);
|
||||
|
||||
if (focusedTab === ChatTabs.CHAT) {
|
||||
// Don't allow opening chat if it's disabled AND user is trying to open the CHAT tab.
|
||||
if (isChatDisabled(state)) {
|
||||
return next(action);
|
||||
}
|
||||
unreadCount = 0;
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyChatUpdated(unreadCount, true);
|
||||
}
|
||||
|
||||
const { privateMessageRecipient } = store.getState()['features/chat'];
|
||||
const { privateMessageRecipient } = state['features/chat'];
|
||||
|
||||
if (
|
||||
isSendGroupChatDisabled(store.getState())
|
||||
isSendGroupChatDisabled(state)
|
||||
&& privateMessageRecipient
|
||||
&& !action.participant
|
||||
) {
|
||||
const participant = getParticipantById(store.getState(), privateMessageRecipient.id);
|
||||
const participant = getParticipantById(state, privateMessageRecipient.id);
|
||||
|
||||
if (participant) {
|
||||
action.participant = participant;
|
||||
@@ -207,7 +217,21 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
}
|
||||
} else if (focusedTab === ChatTabs.POLLS) {
|
||||
// Don't allow opening chat panel if polls are disabled AND user is trying to open the POLLS tab.
|
||||
if (arePollsDisabled(state)) {
|
||||
return next(action);
|
||||
}
|
||||
dispatch(resetUnreadPollsCount());
|
||||
|
||||
// Don't allow opening chat panel if file sharing is disabled AND user is trying to open the
|
||||
// FILE_SHARING tab.
|
||||
} else if (focusedTab === ChatTabs.FILE_SHARING && !isFileSharingEnabled(state)) {
|
||||
return next(action);
|
||||
|
||||
// Don't allow opening chat panel if closed captions are disabled AND user is trying to open the
|
||||
// CLOSED_CAPTIONS tab.
|
||||
} else if (focusedTab === ChatTabs.CLOSED_CAPTIONS && !isCCTabEnabled(state)) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -239,7 +263,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const participantExists = getParticipantById(state, shouldSendPrivateMessageTo.id);
|
||||
|
||||
if (participantExists || shouldSendPrivateMessageTo.isFromVisitor) {
|
||||
dispatch(openDialog(ChatPrivacyDialog, {
|
||||
dispatch(openDialog('ChatPrivacyDialog', ChatPrivacyDialog, {
|
||||
message: action.message,
|
||||
participantID: shouldSendPrivateMessageTo.id,
|
||||
isFromVisitor: shouldSendPrivateMessageTo.isFromVisitor,
|
||||
@@ -576,6 +600,11 @@ function _handleReceivedMessage({ dispatch, getState }: IStore,
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const { soundsIncomingMessage: soundEnabled, userSelectedNotifications } = state['features/base/settings'];
|
||||
|
||||
// Don't play sound or show notifications if chat is disabled.
|
||||
if (isChatDisabled(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (soundEnabled && shouldPlaySound && !isChatOpen) {
|
||||
dispatch(playSound(INCOMING_MSG_SOUND_ID));
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ const DEFAULT_STATE = {
|
||||
privateMessageRecipient: undefined,
|
||||
lobbyMessageRecipient: undefined,
|
||||
isLobbyChatActive: false,
|
||||
focusedTab: ChatTabs.CHAT,
|
||||
focusedTab: undefined,
|
||||
isResizing: false,
|
||||
width: {
|
||||
current: CHAT_SIZE,
|
||||
@@ -44,7 +44,7 @@ const DEFAULT_STATE = {
|
||||
};
|
||||
|
||||
export interface IChatState {
|
||||
focusedTab: ChatTabs;
|
||||
focusedTab?: ChatTabs;
|
||||
groupChatWithPermissions: boolean;
|
||||
isLobbyChatActive: boolean;
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -22,7 +22,7 @@ export function notifyKickedOut(participant: any, submit?: Function) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(openDialog(AlertDialog, {
|
||||
dispatch(openDialog('AlertDialog', AlertDialog, {
|
||||
contentKey: {
|
||||
key: participant ? 'dialog.kickTitle' : 'dialog.kickSystemTitle',
|
||||
params: {
|
||||
@@ -52,7 +52,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
|
||||
// we have to push the opening of the dialog to the queue
|
||||
// so that we make sure it will be visible after the events
|
||||
// of conference destroyed are done
|
||||
setTimeout(() => dispatch(openDialog(AlertDialog, {
|
||||
setTimeout(() => dispatch(openDialog('AlertDialog', AlertDialog, {
|
||||
contentKey: {
|
||||
key: reasonKey
|
||||
},
|
||||
@@ -60,7 +60,7 @@ export function notifyConferenceFailed(reasonKey: string, submit?: Function) {
|
||||
},
|
||||
onSubmit: () => {
|
||||
submit?.();
|
||||
dispatch(hideDialog(AlertDialog));
|
||||
dispatch(hideDialog('AlertDialog', AlertDialog));
|
||||
}
|
||||
})));
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import logger from './logger';
|
||||
*/
|
||||
export function openLeaveReasonDialog(title?: string) {
|
||||
return (dispatch: IStore['dispatch']): Promise<void> => new Promise(resolve => {
|
||||
dispatch(openDialog(LeaveReasonDialog, {
|
||||
dispatch(openDialog('LeaveReasonDialog', LeaveReasonDialog, {
|
||||
onClose: resolve,
|
||||
title
|
||||
}));
|
||||
|
||||
@@ -26,7 +26,7 @@ function SpeakerStatsLabel() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(openDialog(SpeakerStats, { conference }));
|
||||
dispatch(openDialog('SpeakerStats', SpeakerStats, { conference }));
|
||||
};
|
||||
|
||||
if (count <= 2 || _isSpeakerStatsDisabled) {
|
||||
|
||||
@@ -327,7 +327,7 @@ export function _mapDispatchToProps(dispatch: IStore['dispatch']) {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onOpenBandwidthDialog() {
|
||||
dispatch(openDialog(BandwidthSettingsDialog));
|
||||
dispatch(openDialog('BandwidthSettingsDialog', BandwidthSettingsDialog));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ type Options = {
|
||||
export function showDesktopPicker(options: Options = {}, onSourceChoose: Function) {
|
||||
const { desktopSharingSources } = options;
|
||||
|
||||
return openDialog(DesktopPicker, {
|
||||
return openDialog('DesktopPicker', DesktopPicker, {
|
||||
desktopSharingSources,
|
||||
onSourceChoose
|
||||
});
|
||||
|
||||
@@ -259,6 +259,29 @@ class AudioDevicesSelection extends AbstractDialogTab<IProps, {}> {
|
||||
|
||||
const newValue = name === 'channelCount' ? (checked ? 2 : 1) : checked;
|
||||
|
||||
if (name === 'channelCount' && newValue === 2) {
|
||||
super._onChange({
|
||||
audioSettings: {
|
||||
autoGainControl: false,
|
||||
channelCount: 2,
|
||||
echoCancellation: false,
|
||||
noiseSuppression: false
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
} else if (name !== 'channelCount' && newValue === true) {
|
||||
super._onChange({
|
||||
audioSettings: {
|
||||
...audioSettings,
|
||||
[name]: newValue,
|
||||
channelCount: 1
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
super._onChange({
|
||||
audioSettings: {
|
||||
...audioSettings,
|
||||
|
||||
@@ -14,7 +14,7 @@ export function openDisplayNamePrompt({ onPostSubmit, validateInput }: {
|
||||
onPostSubmit?: Function;
|
||||
validateInput?: Function;
|
||||
}) {
|
||||
return openDialog(DisplayNamePrompt, {
|
||||
return openDialog('DisplayNamePrompt', DisplayNamePrompt, {
|
||||
onPostSubmit,
|
||||
validateInput
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
case SETTINGS_UPDATED: {
|
||||
if (action.settings.displayName
|
||||
&& isDialogOpen(getState, DisplayNamePrompt)) {
|
||||
dispatch(hideDialog(DisplayNamePrompt));
|
||||
dispatch(hideDialog('DisplayNamePrompt', DisplayNamePrompt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ StateListenerRegistry.register(
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.E2EE_VERIFICATION_READY, (pId: string, sas: object) => {
|
||||
dispatch(openDialog(ParticipantVerificationDialog, { pId,
|
||||
dispatch(openDialog('ParticipantVerificationDialog', ParticipantVerificationDialog, { pId,
|
||||
sas }));
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class EmbedMeetingButton extends AbstractButton<AbstractButtonProps> {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
sendAnalytics(createToolbarEvent('embed.meeting'));
|
||||
dispatch(openDialog(EmbedMeetingDialog));
|
||||
dispatch(openDialog('EmbedMeetingDialog', EmbedMeetingDialog));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ export function maybeOpenFeedbackDialog(conference: IJitsiConference, title?: st
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function openFeedbackDialog(conference?: IJitsiConference, title?: string, onClose?: Function) {
|
||||
return openDialog(FeedbackDialog, {
|
||||
return openDialog('FeedbackDialog', FeedbackDialog, {
|
||||
conference,
|
||||
onClose,
|
||||
title
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconShareDoc } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { openFileSharingPanel } from '../../../chat/actions.any';
|
||||
import { isFileSharingEnabled } from '../../functions.any';
|
||||
|
||||
/**
|
||||
* Component that renders a button to open the file sharing panel.
|
||||
*
|
||||
* @augments AbstractButton
|
||||
*/
|
||||
class FileSharingButton extends AbstractButton<AbstractButtonProps> {
|
||||
override icon = IconShareDoc;
|
||||
override label = 'toolbar.fileSharing';
|
||||
override tooltip = 'toolbar.fileSharing';
|
||||
|
||||
/**
|
||||
* Handles clicking the button to open the file sharing panel.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openFileSharingPanel());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {Object} - Mapped props.
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
visible: isFileSharingEnabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(FileSharingButton));
|
||||
23
react/features/file-sharing/hooks.web.ts
Normal file
23
react/features/file-sharing/hooks.web.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import FileSharingButton from './components/web/FileSharingButton';
|
||||
import { isFileSharingEnabled } from './functions.any';
|
||||
|
||||
const fileSharing = {
|
||||
key: 'filesharing',
|
||||
Content: FileSharingButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the file sharing button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined} - The file sharing button object or undefined.
|
||||
*/
|
||||
export function useFileSharingButton() {
|
||||
const isEnabled = useSelector(isFileSharingEnabled);
|
||||
|
||||
if (isEnabled) {
|
||||
return fileSharing;
|
||||
}
|
||||
}
|
||||
@@ -559,7 +559,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_showFailedInviteAlert() {
|
||||
this.props.dispatch(openDialog(AlertDialog, {
|
||||
this.props.dispatch(openDialog('AlertDialog', AlertDialog, {
|
||||
contentKey: {
|
||||
key: 'inviteDialog.alertText'
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class DialInSummary extends PureComponent<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onError() {
|
||||
this.props.dispatch(openDialog(DialInSummaryErrorDialog));
|
||||
this.props.dispatch(openDialog('DialInSummaryErrorDialog', DialInSummaryErrorDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -41,7 +41,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
function _beginAddPeople({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
dispatch(openDialog(AddPeopleDialog));
|
||||
dispatch(openDialog('AddPeopleDialog', AddPeopleDialog));
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -60,7 +60,7 @@ function _beginAddPeople({ dispatch }: IStore, next: Function, action: AnyAction
|
||||
* @returns {*} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _hideAddPeopleDialog({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
dispatch(hideDialog(AddPeopleDialog));
|
||||
dispatch(hideDialog('AddPeopleDialog', AddPeopleDialog));
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { isFeatureDisabled } from './functions';
|
||||
export function maybeShowPremiumFeatureDialog(feature: ParticipantFeaturesKey) {
|
||||
return function(dispatch: IStore['dispatch'], getState: IStore['getState']) {
|
||||
if (isFeatureDisabled(getState(), feature)) {
|
||||
dispatch(openDialog(PremiumFeatureDialog));
|
||||
dispatch(openDialog('PremiumFeatureDialog', PremiumFeatureDialog));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -38,9 +38,10 @@ export const getKeyboardKey = (e: KeyboardEvent): string => {
|
||||
// If alt is pressed a different char can be returned so this takes
|
||||
// the char from the code. It also prefixes with a colon to differentiate
|
||||
// alt combo from simple keypress.
|
||||
if (altKey) {
|
||||
const replacedKey = code.replace('Key', '');
|
||||
|
||||
const replacedKey = code.replace('Key', '');
|
||||
|
||||
if (altKey) {
|
||||
return `:${replacedKey}`;
|
||||
}
|
||||
|
||||
@@ -54,6 +55,10 @@ export const getKeyboardKey = (e: KeyboardEvent): string => {
|
||||
return `-${key}`;
|
||||
}
|
||||
|
||||
if (code.startsWith('Key')) {
|
||||
return replacedKey;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
getVirtualScreenshareParticipantByOwnerId
|
||||
} from '../base/participants/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { getAutoPinSetting } from '../video-layout/functions';
|
||||
|
||||
import {
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
SET_LARGE_VIDEO_DIMENSIONS,
|
||||
UPDATE_KNOWN_LARGE_VIDEO_RESOLUTION
|
||||
} from './actionTypes';
|
||||
import { shouldHideLargeVideo } from './functions';
|
||||
|
||||
/**
|
||||
* Action to select the participant to be displayed in LargeVideo based on the
|
||||
@@ -34,8 +34,12 @@ export function selectParticipantInLargeVideo(participant?: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
|
||||
// Skip large video updates when the large video container is hidden.
|
||||
if (shouldHideLargeVideo(state)) {
|
||||
if (isStageFilmstripAvailable(state, 2)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep Etherpad open.
|
||||
if (state['features/etherpad'].editing) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { isStageFilmstripAvailable } from '../filmstrip/functions';
|
||||
import { shouldDisplayTileView } from '../video-layout/functions.any';
|
||||
|
||||
/**
|
||||
* Selector for the participant currently displaying on the large video.
|
||||
@@ -14,17 +12,3 @@ export function getLargeVideoParticipant(state: IReduxState) {
|
||||
|
||||
return getParticipantById(state, participantId ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the large video container should be hidden.
|
||||
* Large video is hidden in tile view, stage filmstrip mode (with multiple participants),
|
||||
* or when editing etherpad.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} True if large video should be hidden, false otherwise.
|
||||
*/
|
||||
export function shouldHideLargeVideo(state: IReduxState): boolean {
|
||||
return shouldDisplayTileView(state)
|
||||
|| isStageFilmstripAvailable(state, 2)
|
||||
|| Boolean(state['features/etherpad']?.editing);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
|
||||
import { SELECT_LARGE_VIDEO_PARTICIPANT } from './actionTypes';
|
||||
import { selectParticipantInLargeVideo } from './actions.any';
|
||||
import { shouldHideLargeVideo } from './functions';
|
||||
|
||||
/**
|
||||
* Updates the large video when transitioning from a hidden state to visible state.
|
||||
* This ensures the large video is properly updated when exiting tile view, stage filmstrip,
|
||||
* whiteboard, or etherpad editing modes.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => shouldHideLargeVideo(state),
|
||||
/* listener */ (isHidden, { dispatch }) => {
|
||||
// When transitioning from hidden to visible state, select participant (because currently it is undefined).
|
||||
// Otherwise set it to undefined because we don't show the large video.
|
||||
if (!isHidden) {
|
||||
dispatch(selectParticipantInLargeVideo());
|
||||
} else {
|
||||
dispatch({
|
||||
type: SELECT_LARGE_VIDEO_PARTICIPANT,
|
||||
participantId: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
import './subscriber.any';
|
||||
|
||||
@@ -4,7 +4,6 @@ import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { getVideoTrackByParticipant } from '../base/tracks/functions.web';
|
||||
|
||||
import { getLargeVideoParticipant } from './functions';
|
||||
import './subscriber.any';
|
||||
|
||||
/**
|
||||
* Updates the on stage participant video.
|
||||
|
||||
@@ -467,7 +467,7 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
_knocking: knocking,
|
||||
_lobbyChatMessages: messages,
|
||||
_lobbyMessageRecipient: lobbyMessageRecipient?.name,
|
||||
_login: showModeratorLogin,
|
||||
_login: showModeratorLogin && !state['features/base/jwt'].jwt,
|
||||
_hangUp: showHangUp,
|
||||
_isLobbyChatActive: isLobbyChatActive,
|
||||
_meetingName: getConferenceName(state),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getConferenceName } from '../../../base/conference/functions';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
@@ -35,6 +35,16 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
_aspectRatio: Symbol;
|
||||
|
||||
/**
|
||||
* The current height of the screen.
|
||||
*/
|
||||
_clientHeight: number;
|
||||
|
||||
/**
|
||||
* The current width of the screen.
|
||||
*/
|
||||
_clientWidth: number;
|
||||
|
||||
/**
|
||||
* The room name.
|
||||
*/
|
||||
@@ -63,33 +73,33 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
override render() {
|
||||
const { _aspectRatio, _roomName } = this.props;
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
const { _aspectRatio, _clientHeight, _clientWidth, _roomName } = this.props;
|
||||
const isTablet = Math.min(_clientWidth, _clientHeight) >= 768;
|
||||
|
||||
if (_aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapper;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
} else {
|
||||
contentWrapperStyles = preJoinStyles.contentWrapperWide;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainerWide;
|
||||
let contentContainerStyles = preJoinStyles.contentContainer;
|
||||
let largeVideoContainerStyles = preJoinStyles.largeVideoContainer;
|
||||
|
||||
if (isTablet && _aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
// @ts-ignore
|
||||
contentContainerStyles = preJoinStyles.contentContainerWide;
|
||||
largeVideoContainerStyles = preJoinStyles.largeVideoContainerWide;
|
||||
}
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
style = { preJoinStyles.contentWrapper }>
|
||||
<BrandingImageBackground />
|
||||
<View style = { largeVideoContainerStyles as ViewStyle }>
|
||||
<View style = { preJoinStyles.displayRoomNameBackdrop as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { preJoinStyles.preJoinRoomName }>
|
||||
{ _roomName }
|
||||
</Text>
|
||||
<View style = { preJoinStyles.conferenceInfo as ViewStyle }>
|
||||
<View style = { preJoinStyles.displayRoomNameBackdrop }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { preJoinStyles.preJoinRoomName }>
|
||||
{ _roomName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<LargeVideo />
|
||||
</View>
|
||||
@@ -304,6 +314,8 @@ function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_clientHeight: state['features/base/responsive-ui'].clientHeight,
|
||||
_clientWidth: state['features/base/responsive-ui'].clientWidth,
|
||||
_roomName: getConferenceName(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ export default {
|
||||
},
|
||||
|
||||
passwordJoinButtons: {
|
||||
top: 40
|
||||
top: BaseTheme.spacing[7]
|
||||
},
|
||||
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
bottom: BaseTheme.spacing[0],
|
||||
display: 'flex',
|
||||
height: 388,
|
||||
justifyContent: 'center',
|
||||
@@ -45,46 +45,6 @@ export default {
|
||||
marginBottom: BaseTheme.spacing[3]
|
||||
},
|
||||
|
||||
// KnockingParticipantList
|
||||
|
||||
knockingParticipantList: {
|
||||
backgroundColor: BaseTheme.palette.ui01
|
||||
},
|
||||
|
||||
|
||||
knockingParticipantListDetails: {
|
||||
flex: 1,
|
||||
marginLeft: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
knockingParticipantListEntry: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.ui01,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
knockingParticipantListText: {
|
||||
color: 'white'
|
||||
},
|
||||
|
||||
lobbyButtonAdmit: {
|
||||
position: 'absolute',
|
||||
right: 184,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonChat: {
|
||||
position: 'absolute',
|
||||
right: 104,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyButtonReject: {
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: 6
|
||||
},
|
||||
|
||||
lobbyTitle: {
|
||||
...BaseTheme.typography.heading5,
|
||||
color: BaseTheme.palette.text01,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { NavigationContainer, Theme } from '@react-navigation/native';
|
||||
import { createStackNavigator } from '@react-navigation/stack';
|
||||
import React, { useCallback } from 'react';
|
||||
import { StatusBar } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import DialInSummary from '../../../invite/components/dial-in-summary/native/DialInSummary';
|
||||
import Prejoin from '../../../prejoin/components/native/Prejoin';
|
||||
import UnsafeRoomWarning from '../../../prejoin/components/native/UnsafeRoomWarning';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions';
|
||||
import { isUnsafeRoomWarningEnabled } from '../../../prejoin/functions.native';
|
||||
import VisitorsQueue from '../../../visitors/components/native/VisitorsQueue';
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
@@ -70,11 +69,6 @@ const RootNavigationContainer = ({ dispatch, isUnsafeRoomWarningAvailable, isWel
|
||||
onReady = { onReady }
|
||||
ref = { rootNavigationRef }
|
||||
theme = { navigationContainerTheme as Theme }>
|
||||
<StatusBar
|
||||
animated = { true }
|
||||
backgroundColor = 'transparent'
|
||||
barStyle = { 'light-content' }
|
||||
translucent = { true } />
|
||||
<RootStack.Navigator
|
||||
initialRouteName = { initialRouteName }>
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createMaterialTopTabNavigator } from '@react-navigation/material-top-ta
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../../../app/types';
|
||||
import {
|
||||
getClientHeight,
|
||||
getClientWidth
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
import { setFocusedTab } from '../../../../../chat/actions.any';
|
||||
import Chat from '../../../../../chat/components/native/Chat';
|
||||
import { ChatTabs } from '../../../../../chat/constants';
|
||||
import { getFocusedTab } from '../../../../../chat/functions';
|
||||
import { resetUnreadPollsCount } from '../../../../../polls/actions';
|
||||
import PollsPane from '../../../../../polls/components/native/PollsPane';
|
||||
import { screen } from '../../../routes';
|
||||
@@ -23,8 +23,8 @@ const ChatAndPolls = () => {
|
||||
const clientHeight = useSelector(getClientHeight);
|
||||
const clientWidth = useSelector(getClientWidth);
|
||||
const dispatch = useDispatch();
|
||||
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
|
||||
const initialRouteName = focusedTab === ChatTabs.POLLS
|
||||
const currentFocusedTab = useSelector(getFocusedTab);
|
||||
const initialRouteName = currentFocusedTab === ChatTabs.POLLS
|
||||
? screen.conference.chatandpolls.tab.polls
|
||||
: screen.conference.chatandpolls.tab.chat;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { querySelector, querySelectorAll } from '@jitsi/js-utils/polyfills/querySelectorPolyfill';
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
import { atob, btoa } from 'abab';
|
||||
import { NativeModules, Platform } from 'react-native';
|
||||
@@ -8,7 +9,6 @@ import 'promise.withresolvers/auto'; // Promise.withResolvers.
|
||||
import 'react-native-url-polyfill/auto'; // Complete URL polyfill.
|
||||
|
||||
import Storage from './Storage';
|
||||
import { querySelector, querySelectorAll } from './querySelectorPolyfill';
|
||||
|
||||
const { AppInfo } = NativeModules;
|
||||
|
||||
@@ -193,7 +193,7 @@ const { AppInfo } = NativeModules;
|
||||
// - lib-jitsi-meet -> XMLUtils.ts -> parseXML
|
||||
if (typeof document.querySelector === 'undefined') {
|
||||
document.querySelector = function(selectors) {
|
||||
return this.documentElement ? querySelector(this.documentElement, selectors) : null;
|
||||
return querySelector(this, selectors);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ const { AppInfo } = NativeModules;
|
||||
// - lib-jitsi-meet -> XMLUtils.ts -> parseXML
|
||||
if (typeof document.querySelectorAll === 'undefined') {
|
||||
document.querySelectorAll = function(selectors) {
|
||||
return this.documentElement ? querySelectorAll(this.documentElement, selectors) : [];
|
||||
return querySelectorAll(this, selectors);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ const { AppInfo } = NativeModules;
|
||||
// - lib-jitsi-meet -> XMLUtils.ts -> parseXML
|
||||
if (typeof documentPrototype.querySelector === 'undefined') {
|
||||
documentPrototype.querySelector = function(selectors) {
|
||||
return this.documentElement ? querySelector(this.documentElement, selectors) : null;
|
||||
return querySelector(this, selectors);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ const { AppInfo } = NativeModules;
|
||||
// - lib-jitsi-meet -> XMLUtils.ts -> parseXML
|
||||
if (typeof documentPrototype.querySelectorAll === 'undefined') {
|
||||
documentPrototype.querySelectorAll = function(selectors) {
|
||||
return this.documentElement ? querySelectorAll(this.documentElement, selectors) : [];
|
||||
return querySelectorAll(this, selectors);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
// Regex constants for efficient reuse across selector parsing
|
||||
const SIMPLE_TAG_NAME_REGEX = /^[a-zA-Z][\w-]*$/;
|
||||
const MULTI_ATTRIBUTE_SELECTOR_REGEX = /^([a-zA-Z][\w-]*)?(\[(?:\*\|)?([^=\]]+)=["']?([^"'\]]+)["']?\])+$/;
|
||||
const SINGLE_ATTRIBUTE_REGEX = /\[(?:\*\|)?([^=\]]+)=["']?([^"'\]]+)["']?\]/g;
|
||||
const WHITESPACE_AROUND_COMBINATOR_REGEX = /\s*>\s*/g;
|
||||
|
||||
/**
|
||||
* Parses a CSS selector into reusable components.
|
||||
*
|
||||
* @param {string} selector - The CSS selector to parse.
|
||||
* @returns {Object} - Object with tagName and attrConditions properties.
|
||||
*/
|
||||
function _parseSelector(selector) {
|
||||
// Wildcard selector
|
||||
if (selector === '*') {
|
||||
return {
|
||||
tagName: null, // null means match all tag names
|
||||
attrConditions: []
|
||||
};
|
||||
}
|
||||
|
||||
// Simple tag name
|
||||
if (SIMPLE_TAG_NAME_REGEX.test(selector)) {
|
||||
return {
|
||||
tagName: selector,
|
||||
attrConditions: []
|
||||
};
|
||||
}
|
||||
|
||||
// Attribute selector: tagname[attr="value"] or
|
||||
// tagname[attr1="value1"][attr2="value2"] (with optional wildcard namespace)
|
||||
const multiAttrMatch = selector.match(MULTI_ATTRIBUTE_SELECTOR_REGEX);
|
||||
|
||||
if (multiAttrMatch) {
|
||||
const tagName = multiAttrMatch[1];
|
||||
const attrConditions = [];
|
||||
let attrMatch;
|
||||
|
||||
while ((attrMatch = SINGLE_ATTRIBUTE_REGEX.exec(selector)) !== null) {
|
||||
attrConditions.push({
|
||||
name: attrMatch[1], // This properly strips the *| prefix
|
||||
value: attrMatch[2]
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
tagName,
|
||||
attrConditions
|
||||
};
|
||||
}
|
||||
|
||||
// Unsupported selector
|
||||
throw new SyntaxError(`Unsupported selector pattern: '${selector}'`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters elements by selector pattern and handles findFirst logic.
|
||||
*
|
||||
* @param {Element[]} elements - Array of elements to filter.
|
||||
* @param {string} selector - CSS selector to match against.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Filtered results with proper return type.
|
||||
*/
|
||||
function _filterAndMatchElements(elements, selector, findFirst) {
|
||||
const { tagName, attrConditions } = _parseSelector(selector);
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const element of elements) {
|
||||
// Check tag name if specified
|
||||
if (tagName && !(element.localName === tagName || element.tagName === tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if all attribute conditions match
|
||||
const allMatch = attrConditions.every(condition =>
|
||||
element.getAttribute(condition.name) === condition.value
|
||||
);
|
||||
|
||||
if (allMatch) {
|
||||
results.push(element);
|
||||
if (findFirst) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return findFirst ? null : results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles direct child traversal for selectors with > combinators.
|
||||
* This is the shared logic used by both scope selectors and regular direct child selectors.
|
||||
*
|
||||
* @param {Element[]} startElements - Array of starting elements to traverse from.
|
||||
* @param {string[]} selectorParts - Array of selector parts split by '>'.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _traverseDirectChildren(startElements, selectorParts, findFirst) {
|
||||
let currentElements = startElements;
|
||||
|
||||
for (const part of selectorParts) {
|
||||
const nextElements = [];
|
||||
|
||||
currentElements.forEach(el => {
|
||||
// Get direct children
|
||||
const directChildren = Array.from(el.children || []);
|
||||
|
||||
// Use same helper as handlers
|
||||
const matchingChildren = _filterAndMatchElements(directChildren, part, false);
|
||||
|
||||
nextElements.push(...matchingChildren);
|
||||
});
|
||||
|
||||
currentElements = nextElements;
|
||||
|
||||
// If we have no results, we can stop early (applies to both querySelector and querySelectorAll)
|
||||
if (currentElements.length === 0) {
|
||||
return findFirst ? null : [];
|
||||
}
|
||||
}
|
||||
|
||||
return findFirst ? currentElements[0] || null : currentElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles :scope pseudo-selector cases with direct child combinators.
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _handleScopeSelector(node, selector, findFirst) {
|
||||
let searchSelector = selector.substring(6);
|
||||
|
||||
// Handle :scope > tagname (direct children)
|
||||
if (searchSelector.startsWith('>')) {
|
||||
searchSelector = searchSelector.substring(1);
|
||||
|
||||
// Split by > and use shared traversal logic
|
||||
const parts = searchSelector.split('>');
|
||||
|
||||
// Start from the node itself (scope)
|
||||
return _traverseDirectChildren([ node ], parts, findFirst);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles nested > selectors (direct child combinators).
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _handleDirectChildSelectors(node, selector, findFirst) {
|
||||
const parts = selector.split('>');
|
||||
|
||||
// First find elements matching the first part (this could be descendants, not just direct children)
|
||||
const startElements = _querySelectorInternal(node, parts[0], false);
|
||||
|
||||
// If no starting elements found, return early
|
||||
if (startElements.length === 0) {
|
||||
return findFirst ? null : [];
|
||||
}
|
||||
|
||||
// Use shared traversal logic for the remaining parts
|
||||
return _traverseDirectChildren(startElements, parts.slice(1), findFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles simple tag name selectors.
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _handleSimpleTagSelector(node, selector, findFirst) {
|
||||
const elements = Array.from(node.getElementsByTagName(selector));
|
||||
|
||||
if (findFirst) {
|
||||
return elements[0] || null;
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles attribute selectors: tagname[attr="value"] or tagname[attr1="value1"][attr2="value2"].
|
||||
* Supports single or multiple attributes with optional wildcard namespace (*|).
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _handleAttributeSelector(node, selector, findFirst) {
|
||||
const { tagName } = _parseSelector(selector); // Just to get tagName for optimization
|
||||
|
||||
// Handler's job: find the right elements to search
|
||||
const elementsToCheck = tagName
|
||||
? Array.from(node.getElementsByTagName(tagName))
|
||||
: Array.from(node.getElementsByTagName('*'));
|
||||
|
||||
// Common helper does the matching
|
||||
return _filterAndMatchElements(elementsToCheck, selector, findFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal function that implements the core selector matching logic for both
|
||||
* querySelector and querySelectorAll. Supports :scope pseudo-selector, direct
|
||||
* child selectors, and common CSS selectors.
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector to match elements against.
|
||||
* @param {boolean} findFirst - If true, return after finding the first match.
|
||||
* @returns {Element[]|Element|null} - Array of Elements for querySelectorAll,
|
||||
* single Element or null for querySelector.
|
||||
*/
|
||||
function _querySelectorInternal(node, selector, findFirst = false) {
|
||||
// Normalize whitespace around > combinators first
|
||||
const normalizedSelector = selector.replace(WHITESPACE_AROUND_COMBINATOR_REGEX, '>');
|
||||
|
||||
// Handle :scope pseudo-selector
|
||||
if (normalizedSelector.startsWith(':scope')) {
|
||||
return _handleScopeSelector(node, normalizedSelector, findFirst);
|
||||
}
|
||||
|
||||
// Handle nested > selectors (direct child combinators)
|
||||
if (normalizedSelector.includes('>')) {
|
||||
return _handleDirectChildSelectors(node, normalizedSelector, findFirst);
|
||||
}
|
||||
|
||||
// Fast path: simple tag name
|
||||
if (normalizedSelector === '*' || SIMPLE_TAG_NAME_REGEX.test(normalizedSelector)) {
|
||||
return _handleSimpleTagSelector(node, normalizedSelector, findFirst);
|
||||
}
|
||||
|
||||
// Attribute selector: tagname[attr="value"] or
|
||||
// tagname[attr1="value1"][attr2="value2"] (with optional wildcard namespace)
|
||||
if (normalizedSelector.match(MULTI_ATTRIBUTE_SELECTOR_REGEX)) {
|
||||
return _handleAttributeSelector(node, normalizedSelector, findFirst);
|
||||
}
|
||||
|
||||
// Unsupported selector - throw SyntaxError to match browser behavior
|
||||
throw new SyntaxError(`Failed to execute 'querySelector${
|
||||
findFirst ? '' : 'All'}' on 'Element': '${selector}' is not a valid selector.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements querySelector functionality using the shared internal logic.
|
||||
* Supports the same selectors as querySelectorAll but returns only the first match.
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selectors - The CSS selector to match elements against.
|
||||
* @returns {Element|null} - The first Element which matches the selector, or null.
|
||||
*/
|
||||
export function querySelector(node, selectors) {
|
||||
return _querySelectorInternal(node, selectors, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements querySelectorAll functionality using the shared internal logic.
|
||||
* Supports :scope pseudo-selector, direct child selectors, and common CSS selectors.
|
||||
*
|
||||
* @param {Node} node - The Node which is the root of the tree to query.
|
||||
* @param {string} selector - The CSS selector to match elements against.
|
||||
* @returns {Element[]} - Array of Elements matching the selector.
|
||||
*/
|
||||
export function querySelectorAll(node, selector) {
|
||||
return _querySelectorInternal(node, selector, false);
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import PageReloadDialog from '../base/dialog/components/native/PageReloadDialog'
|
||||
*/
|
||||
export function openPageReloadDialog(
|
||||
conferenceError?: Error, configError?: Error, connectionError?: ConnectionFailedError) {
|
||||
return openDialog(PageReloadDialog, {
|
||||
return openDialog('PageReloadDialog', PageReloadDialog, {
|
||||
conferenceError,
|
||||
configError,
|
||||
connectionError
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function RenameButton({ breakoutRoomJid, name }: IProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { classes, cx } = useStyles();
|
||||
const onRename = useCallback(() => {
|
||||
dispatch(openDialog(BreakoutRoomNamePrompt, {
|
||||
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
|
||||
breakoutRoomJid,
|
||||
initialRoomName: name
|
||||
}));
|
||||
|
||||
@@ -68,7 +68,7 @@ export const RoomContextMenu = ({
|
||||
}, [ dispatch, room ]);
|
||||
|
||||
const onRenameBreakoutRoom = useCallback(() => {
|
||||
dispatch(openDialog(BreakoutRoomNamePrompt, {
|
||||
dispatch(openDialog('BreakoutRoomNamePrompt', BreakoutRoomNamePrompt, {
|
||||
breakoutRoomJid: room?.jid,
|
||||
initialRoomName: room?.name
|
||||
}));
|
||||
|
||||
@@ -32,7 +32,7 @@ import styles from './styles';
|
||||
export const ContextMenuMore = () => {
|
||||
const dispatch = useDispatch();
|
||||
const muteAllVideo = useCallback(() => {
|
||||
dispatch(openDialog(MuteEveryonesVideoDialog));
|
||||
dispatch(openDialog('MuteEveryonesVideoDialog', MuteEveryonesVideoDialog));
|
||||
dispatch(hideSheet());
|
||||
}, [ dispatch ]);
|
||||
const conference = useSelector(getCurrentConference);
|
||||
|
||||
@@ -42,7 +42,7 @@ const ParticipantsPaneFooter = (): JSX.Element => {
|
||||
getFeatureFlag(state, BREAKOUT_ROOMS_BUTTON_ENABLED, true)
|
||||
);
|
||||
const openMoreMenu = useCallback(() => dispatch(openSheet(ContextMenuMore)), [ dispatch ]);
|
||||
const muteAll = useCallback(() => dispatch(openDialog(MuteEveryoneDialog)),
|
||||
const muteAll = useCallback(() => dispatch(openDialog('MuteEveryoneDialog', MuteEveryoneDialog)),
|
||||
[ dispatch ]);
|
||||
const showMoreActions = useSelector(isMoreActionsVisible);
|
||||
const showMuteAll = useSelector(isMuteAllVisible);
|
||||
|
||||
@@ -112,10 +112,10 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: IProp
|
||||
const { classes } = useStyles();
|
||||
|
||||
const muteAllVideo = useCallback(
|
||||
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
|
||||
() => dispatch(openDialog('MuteEveryonesVideoDialog', MuteEveryonesVideoDialog)), [ dispatch ]);
|
||||
|
||||
const muteAllDesktop = useCallback(
|
||||
() => dispatch(openDialog(MuteEveryonesDesktopDialog)), [ dispatch ]);
|
||||
() => dispatch(openDialog('MuteEveryonesDesktopDialog', MuteEveryonesDesktopDialog)), [ dispatch ]);
|
||||
|
||||
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
|
||||
|
||||
|
||||
@@ -181,7 +181,7 @@ const ParticipantsPane = () => {
|
||||
}, []);
|
||||
|
||||
const onMuteAll = useCallback(() => {
|
||||
dispatch(openDialog(MuteEveryoneDialog));
|
||||
dispatch(openDialog('MuteEveryoneDialog', MuteEveryoneDialog));
|
||||
}, []);
|
||||
|
||||
const onToggleContext = useCallback(() => {
|
||||
|
||||
45
react/features/polls/components/web/PollsButton.tsx
Normal file
45
react/features/polls/components/web/PollsButton.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { openPollsPanel } from '../../../chat/actions.any';
|
||||
import { arePollsDisabled } from '../../../conference/functions.any';
|
||||
|
||||
/**
|
||||
* Component that renders a button to open the polls panel.
|
||||
*
|
||||
* @augments AbstractButton
|
||||
*/
|
||||
class PollsButton extends AbstractButton<AbstractButtonProps> {
|
||||
override icon = IconInfo;
|
||||
override label = 'toolbar.polls';
|
||||
override tooltip = 'toolbar.polls';
|
||||
|
||||
/**
|
||||
* Handles clicking the button to open the polls panel.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openPollsPanel());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {Object} - Mapped props.
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
visible: !arePollsDisabled(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(PollsButton));
|
||||
24
react/features/polls/hooks.web.ts
Normal file
24
react/features/polls/hooks.web.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { arePollsDisabled } from '../conference/functions.any';
|
||||
|
||||
import PollsButton from './components/web/PollsButton';
|
||||
|
||||
const polls = {
|
||||
key: 'polls',
|
||||
Content: PollsButton,
|
||||
group: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* A hook that returns the polls button if it is enabled and undefined otherwise.
|
||||
*
|
||||
* @returns {Object | undefined} - The polls button object or undefined.
|
||||
*/
|
||||
export function usePollsButton() {
|
||||
const isPollsDisabled = useSelector(arePollsDisabled);
|
||||
|
||||
if (!isPollsDisabled) {
|
||||
return polls;
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,7 @@ import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import { getFieldValue } from '../../../base/react/functions';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import { updateSettings } from '../../../base/settings/actions';
|
||||
import Button from '../../../base/ui/components/native/Button';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
@@ -51,9 +51,10 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const isFocused = useIsFocused();
|
||||
const { t } = useTranslation();
|
||||
const aspectRatio = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']?.aspectRatio
|
||||
const { aspectRatio, clientHeight, clientWidth } = useSelector(
|
||||
(state: IReduxState) => state['features/base/responsive-ui']
|
||||
);
|
||||
const isTablet = Math.min(clientWidth, clientHeight) >= 768;
|
||||
const localParticipant = useSelector((state: IReduxState) => getLocalParticipant(state));
|
||||
const isDisplayNameMandatory = useSelector((state: IReduxState) => isDisplayNameRequired(state));
|
||||
const isDisplayNameVisible
|
||||
@@ -147,16 +148,11 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
});
|
||||
}, [ navigation ]);
|
||||
|
||||
let contentWrapperStyles;
|
||||
let contentContainerStyles;
|
||||
let largeVideoContainerStyles;
|
||||
let contentContainerStyles = styles.contentContainer;
|
||||
let largeVideoContainerStyles = styles.largeVideoContainer;
|
||||
|
||||
if (aspectRatio === ASPECT_RATIO_NARROW) {
|
||||
contentWrapperStyles = styles.contentWrapper;
|
||||
contentContainerStyles = styles.contentContainer;
|
||||
largeVideoContainerStyles = styles.largeVideoContainer;
|
||||
} else {
|
||||
contentWrapperStyles = styles.contentWrapperWide;
|
||||
if (isTablet && aspectRatio === ASPECT_RATIO_WIDE) {
|
||||
// @ts-ignore
|
||||
contentContainerStyles = styles.contentContainerWide;
|
||||
largeVideoContainerStyles = styles.largeVideoContainerWide;
|
||||
}
|
||||
@@ -165,7 +161,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
<JitsiScreen
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { contentWrapperStyles }>
|
||||
style = { styles.contentWrapper }>
|
||||
<BrandingImageBackground />
|
||||
{
|
||||
isFocused
|
||||
@@ -207,7 +203,7 @@ const Prejoin: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProps) => {
|
||||
}
|
||||
{
|
||||
showDisplayNameError && (
|
||||
<View style = { styles.errorContainer as StyleProp<TextStyle> }>
|
||||
<View style = { styles.errorContainer as StyleProp<ViewStyle> }>
|
||||
<Text style = { styles.error as StyleProp<TextStyle> }>
|
||||
{ t('prejoin.errorMissingName') }
|
||||
</Text>
|
||||
|
||||
@@ -86,12 +86,14 @@ const UnsafeRoomWarning: React.FC<IPrejoinProps> = ({ navigation }: IPrejoinProp
|
||||
addBottomPadding = { false }
|
||||
safeAreaInsets = { [ 'right' ] }
|
||||
style = { styles.unsafeRoomWarningContainer } >
|
||||
<View style = { styles.displayRoomNameBackdrop as StyleProp<TextStyle> }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
<View style = { styles.conferenceInfo as ViewStyle }>
|
||||
<View style = { styles.displayRoomNameBackdrop as ViewStyle }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
style = { styles.preJoinRoomName as StyleProp<TextStyle> }>
|
||||
{ roomName }
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style = { unsafeRoomContentContainer as StyleProp<ViewStyle> }>
|
||||
<View style = { styles.warningIconWrapper as StyleProp<ViewStyle> }>
|
||||
|
||||
@@ -10,14 +10,14 @@ export const preJoinStyles = {
|
||||
buttonStylesBorderless: {
|
||||
iconStyle: {
|
||||
color: BaseTheme.palette.icon01,
|
||||
fontSize: 24
|
||||
fontSize: BaseTheme.spacing[4]
|
||||
},
|
||||
style: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
margin: BaseTheme.spacing[3],
|
||||
height: 24,
|
||||
width: 24
|
||||
height: BaseTheme.spacing[4],
|
||||
width: BaseTheme.spacing[4]
|
||||
},
|
||||
underlayColor: 'transparent'
|
||||
},
|
||||
@@ -27,13 +27,8 @@ export const preJoinStyles = {
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
contentWrapperWide: {
|
||||
flex: 1,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
|
||||
largeVideoContainer: {
|
||||
height: '60%'
|
||||
height: '50%'
|
||||
},
|
||||
|
||||
largeVideoContainerWide: {
|
||||
@@ -46,9 +41,9 @@ export const preJoinStyles = {
|
||||
contentContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
bottom: 0,
|
||||
bottom: BaseTheme.spacing[0],
|
||||
display: 'flex',
|
||||
height: 280,
|
||||
height: '50%',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
@@ -115,6 +110,7 @@ export const preJoinStyles = {
|
||||
maxWidth: 273,
|
||||
zIndex: 1
|
||||
},
|
||||
|
||||
displayRoomNameBackdrop: {
|
||||
backgroundColor: BaseTheme.palette.uiBackground,
|
||||
borderRadius: BaseTheme.shape.borderRadius,
|
||||
@@ -122,6 +118,7 @@ export const preJoinStyles = {
|
||||
paddingHorizontal: BaseTheme.spacing[3],
|
||||
paddingVertical: BaseTheme.spacing[1]
|
||||
},
|
||||
|
||||
recordingWarning: {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
@@ -130,9 +127,11 @@ export const preJoinStyles = {
|
||||
marginTop: BaseTheme.spacing[1],
|
||||
width: 'auto'
|
||||
},
|
||||
|
||||
recordingWarningText: {
|
||||
color: BaseTheme.palette.text03
|
||||
},
|
||||
|
||||
unsafeRoomWarningContainer: {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
@@ -142,6 +141,7 @@ export const preJoinStyles = {
|
||||
justifyContent: 'center',
|
||||
color: 'white'
|
||||
},
|
||||
|
||||
unsafeRoomContentContainer: {
|
||||
justifySelf: 'center',
|
||||
height: '100%',
|
||||
@@ -167,6 +167,7 @@ export const preJoinStyles = {
|
||||
textAlign: 'center',
|
||||
marginBottom: BaseTheme.spacing[4]
|
||||
},
|
||||
|
||||
warningIconWrapper: {
|
||||
backgroundColor: BaseTheme.palette.warning01,
|
||||
borderRadius: BaseTheme.shape.circleRadius,
|
||||
@@ -175,8 +176,9 @@ export const preJoinStyles = {
|
||||
zIndex: 0
|
||||
|
||||
},
|
||||
|
||||
warningIcon: {
|
||||
color: BaseTheme.palette.ui01,
|
||||
fontSize: 40
|
||||
fontSize: BaseTheme.spacing[7]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,7 +110,7 @@ class ReactionMenuDialog extends PureComponent<IProps> {
|
||||
*/
|
||||
_onCancel() {
|
||||
if (this.props._isOpen) {
|
||||
this.props.dispatch(hideDialog(ReactionMenu_));
|
||||
this.props.dispatch(hideDialog('ReactionMenu_', ReactionMenu_));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ class ReactionsMenuButton extends AbstractButton<IProps> {
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
this.props.dispatch(openDialog(ReactionMenuDialog));
|
||||
this.props.dispatch(openDialog('ReactionMenuDialog', ReactionMenuDialog));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,7 +76,7 @@ export function showRecordingLimitNotification(streamType: string) {
|
||||
*/
|
||||
export function showStartRecordingNotification() {
|
||||
return (dispatch: IStore['dispatch']) => {
|
||||
const openDialogCallback = () => dispatch(openDialog(StartRecordingDialog));
|
||||
const openDialogCallback = () => dispatch(openDialog('StartRecordingDialog', StartRecordingDialog));
|
||||
|
||||
dispatch(showStartRecordingNotificationWithCallback(openDialogCallback));
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ class LiveStreamButton extends AbstractLiveStreamButton<Props> {
|
||||
const { _isLiveStreamRunning, dispatch } = this.props;
|
||||
|
||||
if (_isLiveStreamRunning) {
|
||||
dispatch(openDialog(StopLiveStreamDialog));
|
||||
dispatch(openDialog('StopLiveStreamDialog', StopLiveStreamDialog));
|
||||
} else {
|
||||
navigate(screen.conference.liveStream);
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ class LiveStreamButton extends AbstractLiveStreamButton<IProps> {
|
||||
*/
|
||||
override _onHandleClick() {
|
||||
const { _isLiveStreamRunning, dispatch } = this.props;
|
||||
const dialogComponent = _isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog;
|
||||
const dialogName = _isLiveStreamRunning ? 'StopLiveStreamDialog' : 'StartLiveStreamDialog';
|
||||
|
||||
dispatch(openDialog(
|
||||
_isLiveStreamRunning ? StopLiveStreamDialog : StartLiveStreamDialog
|
||||
));
|
||||
dispatch(openDialog(dialogName, dialogComponent));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ export default class AbstractHighlightButton<P extends IProps, S={}> extends Com
|
||||
const dialogShown = dispatch(maybeShowPremiumFeatureDialog(MEET_FEATURES.RECORDING));
|
||||
|
||||
if (!dialogShown) {
|
||||
dispatch(openDialog(StartRecordingDialog));
|
||||
dispatch(openDialog('StartRecordingDialog', StartRecordingDialog));
|
||||
}
|
||||
} ],
|
||||
appearance: NOTIFICATION_TYPE.NORMAL
|
||||
|
||||
@@ -37,7 +37,7 @@ class RecordButton extends AbstractRecordButton<Props> {
|
||||
const { _isRecordingRunning, dispatch } = this.props;
|
||||
|
||||
if (_isRecordingRunning) {
|
||||
dispatch(openDialog(StopRecordingDialog));
|
||||
dispatch(openDialog('StopRecordingDialog', StopRecordingDialog));
|
||||
} else {
|
||||
navigate(screen.conference.recording);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user