mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-18 20:37:47 +00:00
Compare commits
47 Commits
8674
...
visitors-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a24ddf205c | ||
|
|
7a97d15e89 | ||
|
|
1acb99d763 | ||
|
|
adbe990867 | ||
|
|
a4367567ab | ||
|
|
7f56cbc4ce | ||
|
|
d636d084c8 | ||
|
|
298567be48 | ||
|
|
c233629e51 | ||
|
|
75b5702a7e | ||
|
|
540f01d47e | ||
|
|
5c7ed6a8b3 | ||
|
|
3c5d33fefa | ||
|
|
be04236834 | ||
|
|
ec1bfe73b3 | ||
|
|
d2ed9ffef6 | ||
|
|
6141ff78f8 | ||
|
|
c6a75fb9ed | ||
|
|
3438438219 | ||
|
|
7cedea6740 | ||
|
|
69f26c8a38 | ||
|
|
92a4750d0e | ||
|
|
370a884765 | ||
|
|
877fc98eef | ||
|
|
7bed0b36bd | ||
|
|
cd5aed37e9 | ||
|
|
b8dad082df | ||
|
|
f84f98e8e5 | ||
|
|
d1328d68f2 | ||
|
|
43d5c1e3ba | ||
|
|
22ed00724d | ||
|
|
0b095f36eb | ||
|
|
327376d85e | ||
|
|
f28bd67ff4 | ||
|
|
3a54c3418b | ||
|
|
b6026dcd04 | ||
|
|
2eff0d8f78 | ||
|
|
1ab7989a1a | ||
|
|
b8c6c2381c | ||
|
|
3f9202ce04 | ||
|
|
965b413d26 | ||
|
|
1cb2025951 | ||
|
|
4decb41a1e | ||
|
|
8a79d200c8 | ||
|
|
2f9436afb1 | ||
|
|
66dc158c22 | ||
|
|
921ed99676 |
@@ -10,13 +10,15 @@ package org.jitsi.meet.sdk;
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.oney.WebRTCModule.webrtcutils.SoftwareVideoDecoderFactoryProxy;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.HardwareVideoDecoderFactory;
|
||||
import org.webrtc.PlatformSoftwareVideoDecoderFactory;
|
||||
import org.webrtc.JitsiPlatformVideoDecoderFactory;
|
||||
import org.webrtc.Predicate;
|
||||
import org.webrtc.VideoCodecInfo;
|
||||
import org.webrtc.VideoDecoder;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
@@ -31,29 +33,34 @@ import java.util.LinkedHashSet;
|
||||
public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
|
||||
private final VideoDecoderFactory hardwareVideoDecoderFactory;
|
||||
private final VideoDecoderFactory softwareVideoDecoderFactory = new SoftwareVideoDecoderFactoryProxy();
|
||||
private final @Nullable VideoDecoderFactory platformSoftwareVideoDecoderFactory;
|
||||
private final VideoDecoderFactory platformSoftwareVideoDecoderFactory;
|
||||
|
||||
/**
|
||||
* Predicate to filter out the AV1 hardware decoder, as we've seen decoding issues with it.
|
||||
*/
|
||||
private static final String GOOGLE_AV1_DECODER = "c2.google.av1";
|
||||
private static final Predicate<MediaCodecInfo> hwCodecPredicate = arg -> {
|
||||
// Filter out the Google AV1 codec.
|
||||
return !arg.getName().startsWith(GOOGLE_AV1_DECODER);
|
||||
};
|
||||
private static final Predicate<MediaCodecInfo> swCodecPredicate = arg -> {
|
||||
// Noop, just making sure we can customize it easily if needed.
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create decoder factory using default hardware decoder factory.
|
||||
*/
|
||||
public JitsiVideoDecoderFactory(@Nullable EglBase.Context eglContext) {
|
||||
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext);
|
||||
this.platformSoftwareVideoDecoderFactory = new PlatformSoftwareVideoDecoderFactory(eglContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create decoder factory using explicit hardware decoder factory.
|
||||
*/
|
||||
JitsiVideoDecoderFactory(VideoDecoderFactory hardwareVideoDecoderFactory) {
|
||||
this.hardwareVideoDecoderFactory = hardwareVideoDecoderFactory;
|
||||
this.platformSoftwareVideoDecoderFactory = null;
|
||||
this.hardwareVideoDecoderFactory = new HardwareVideoDecoderFactory(eglContext, hwCodecPredicate);
|
||||
this.platformSoftwareVideoDecoderFactory = new JitsiPlatformVideoDecoderFactory(eglContext, swCodecPredicate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable VideoDecoder createDecoder(VideoCodecInfo codecType) {
|
||||
VideoDecoder softwareDecoder = softwareVideoDecoderFactory.createDecoder(codecType);
|
||||
final VideoDecoder hardwareDecoder = hardwareVideoDecoderFactory.createDecoder(codecType);
|
||||
if (softwareDecoder == null && platformSoftwareVideoDecoderFactory != null) {
|
||||
if (softwareDecoder == null) {
|
||||
softwareDecoder = platformSoftwareVideoDecoderFactory.createDecoder(codecType);
|
||||
}
|
||||
if (hardwareDecoder != null && softwareDecoder != null) {
|
||||
@@ -70,10 +77,7 @@ public class JitsiVideoDecoderFactory implements VideoDecoderFactory {
|
||||
|
||||
supportedCodecInfos.addAll(Arrays.asList(softwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
supportedCodecInfos.addAll(Arrays.asList(hardwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
if (platformSoftwareVideoDecoderFactory != null) {
|
||||
supportedCodecInfos.addAll(
|
||||
Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
}
|
||||
supportedCodecInfos.addAll(Arrays.asList(platformSoftwareVideoDecoderFactory.getSupportedCodecs()));
|
||||
|
||||
return supportedCodecInfos.toArray(new VideoCodecInfo[supportedCodecInfos.size()]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2018 The WebRTC project authors. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by a BSD-style license
|
||||
* that can be found in the LICENSE file in the root of the source
|
||||
* tree. An additional intellectual property rights grant can be found
|
||||
* in the file PATENTS. All contributing project authors may
|
||||
* be found in the AUTHORS file in the root of the source tree.
|
||||
*/
|
||||
|
||||
package org.webrtc;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/** Factory for Android platform software VideoDecoders. */
|
||||
public class JitsiPlatformVideoDecoderFactory extends MediaCodecVideoDecoderFactory {
|
||||
/**
|
||||
* Default allowed predicate.
|
||||
*/
|
||||
private static final Predicate<MediaCodecInfo> defaultAllowedPredicate =
|
||||
codecInfo -> {
|
||||
// We only want to use the platform software codecs.
|
||||
return MediaCodecUtils.isSoftwareOnly(codecInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a PlatformSoftwareVideoDecoderFactory that supports surface texture rendering.
|
||||
*
|
||||
* @param sharedContext The textures generated will be accessible from this context. May be null,
|
||||
* this disables texture support.
|
||||
*/
|
||||
public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext) {
|
||||
super(sharedContext, defaultAllowedPredicate);
|
||||
}
|
||||
|
||||
public JitsiPlatformVideoDecoderFactory(@Nullable EglBase.Context sharedContext, @Nullable Predicate<MediaCodecInfo> codecAllowedPredicate) {
|
||||
super(sharedContext, codecAllowedPredicate == null ? defaultAllowedPredicate : codecAllowedPredicate.and(defaultAllowedPredicate));
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ rootProject.name = 'jitsi-meet'
|
||||
include ':app', ':sdk'
|
||||
|
||||
include ':react-native-amplitude'
|
||||
project(':react-native-amplitude').projectDir = new File(rootProject.projectDir, '../node_modules/@amplitude/react-native/android')
|
||||
project(':react-native-amplitude').projectDir = new File(rootProject.projectDir, '../node_modules/@amplitude/analytics-react-native/android')
|
||||
include ':react-native-async-storage'
|
||||
project(':react-native-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-async-storage/async-storage/android')
|
||||
include ':react-native-background-timer'
|
||||
|
||||
21
config.js
21
config.js
@@ -117,6 +117,11 @@ var config = {
|
||||
|
||||
// Will replace ice candidates IPs with invalid ones in order to fail ice.
|
||||
// failICE: true,
|
||||
|
||||
// When running on Spot TV, this controls whether to show the recording consent dialog.
|
||||
// If false (default), Spot instances will not show the recording consent dialog.
|
||||
// If true, Spot instances will show the recording consent dialog like regular clients.
|
||||
// showSpotConsentDialog: false,
|
||||
},
|
||||
|
||||
// Disables moderator indicators.
|
||||
@@ -1126,10 +1131,6 @@ var config = {
|
||||
// The Amplitude APP Key:
|
||||
// amplitudeAPPKey: '<APP_KEY>',
|
||||
|
||||
// Enables Amplitude UTM tracking:
|
||||
// Default value is false.
|
||||
// amplitudeIncludeUTM: false,
|
||||
|
||||
// Obfuscates room name sent to analytics (amplitude, rtcstats)
|
||||
// Default value is false.
|
||||
// obfuscateRoomName: false,
|
||||
@@ -1360,8 +1361,9 @@ var config = {
|
||||
// disableKick: true,
|
||||
// // If set to true the 'Grant moderator' button will be disabled.
|
||||
// disableGrantModerator: true,
|
||||
// // If set to true the 'Send private message' button will be disabled.
|
||||
// disablePrivateChat: true,
|
||||
// // If set to 'all' the 'Private chat' button will be disabled for all participants.
|
||||
// // If set to 'allow-moderator-chat' the 'Private chat' button will be available for chats with moderators.
|
||||
// disablePrivateChat: 'all' | 'allow-moderator-chat',
|
||||
// },
|
||||
|
||||
|
||||
@@ -1778,6 +1780,13 @@ var config = {
|
||||
// // The minimum number of participants that must be in the call for
|
||||
// // the top panel layout to be used.
|
||||
// minParticipantCountForTopPanel: 50,
|
||||
|
||||
// // The width of the filmstrip on joining meeting. Can be resized afterwards.
|
||||
// initialWidth: 400,
|
||||
|
||||
// // Whether the draggable resize bar of the filmstrip is always visible. Setting this to true will make
|
||||
// // the filmstrip always visible in case `disableResizable` is false.
|
||||
// alwaysShowResizeBar: true,
|
||||
// },
|
||||
|
||||
// Tile view related config options.
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
PODS:
|
||||
- Amplitude (8.18.0):
|
||||
- AnalyticsConnector (~> 1.0.0)
|
||||
- amplitude-react-native (2.17.3):
|
||||
- Amplitude (= 8.18.0)
|
||||
- amplitude-react-native (1.4.13):
|
||||
- React-Core
|
||||
- AnalyticsConnector (1.0.3)
|
||||
- AppAuth (1.7.5):
|
||||
- AppAuth/Core (= 1.7.5)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.5)
|
||||
- AppAuth/Core (1.7.5)
|
||||
- AppAuth/ExternalUserAgent (1.7.5):
|
||||
- AppAuth (1.7.6):
|
||||
- AppAuth/Core (= 1.7.6)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||
- AppAuth/Core (1.7.6)
|
||||
- AppAuth/ExternalUserAgent (1.7.6):
|
||||
- AppAuth/Core
|
||||
- boost (1.84.0)
|
||||
- CocoaLumberjack (3.7.4):
|
||||
@@ -136,18 +132,18 @@ PODS:
|
||||
- hermes-engine (0.75.5):
|
||||
- hermes-engine/Pre-built (= 0.75.5)
|
||||
- hermes-engine/Pre-built (0.75.5)
|
||||
- JitsiWebRTC (124.0.1)
|
||||
- libwebp (1.3.2):
|
||||
- libwebp/demux (= 1.3.2)
|
||||
- libwebp/mux (= 1.3.2)
|
||||
- libwebp/sharpyuv (= 1.3.2)
|
||||
- libwebp/webp (= 1.3.2)
|
||||
- libwebp/demux (1.3.2):
|
||||
- JitsiWebRTC (124.0.2)
|
||||
- libwebp (1.5.0):
|
||||
- libwebp/demux (= 1.5.0)
|
||||
- libwebp/mux (= 1.5.0)
|
||||
- libwebp/sharpyuv (= 1.5.0)
|
||||
- libwebp/webp (= 1.5.0)
|
||||
- libwebp/demux (1.5.0):
|
||||
- libwebp/webp
|
||||
- libwebp/mux (1.3.2):
|
||||
- libwebp/mux (1.5.0):
|
||||
- libwebp/demux
|
||||
- libwebp/sharpyuv (1.3.2)
|
||||
- libwebp/webp (1.3.2):
|
||||
- libwebp/sharpyuv (1.5.0)
|
||||
- libwebp/webp (1.5.0):
|
||||
- libwebp/sharpyuv
|
||||
- nanopb (2.30908.0):
|
||||
- nanopb/decode (= 2.30908.0)
|
||||
@@ -1786,7 +1782,7 @@ PODS:
|
||||
- Yoga (0.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "amplitude-react-native (from `../node_modules/@amplitude/react-native`)"
|
||||
- "amplitude-react-native (from `../node_modules/@amplitude/analytics-react-native`)"
|
||||
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
|
||||
- CocoaLumberjack (= 3.7.4)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
@@ -1883,8 +1879,6 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Amplitude
|
||||
- AnalyticsConnector
|
||||
- AppAuth
|
||||
- CocoaLumberjack
|
||||
- Firebase
|
||||
@@ -1910,7 +1904,7 @@ SPEC REPOS:
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
amplitude-react-native:
|
||||
:path: "../node_modules/@amplitude/react-native"
|
||||
:path: "../node_modules/@amplitude/analytics-react-native"
|
||||
boost:
|
||||
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
|
||||
DoubleConversion:
|
||||
@@ -2088,10 +2082,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Amplitude: 184def4f87aa26f94a93a7faa334e06b1cae704d
|
||||
amplitude-react-native: 6b7a1d30627233fe6f03741109831561d0a5f69c
|
||||
AnalyticsConnector: a53214d38ae22734c6266106c0492b37832633a9
|
||||
AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
|
||||
amplitude-react-native: d0039a3ce502eb441ba818af1b8c8ba710ef16e7
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
boost: 4cb898d0bf20404aab1850c656dcea009429d6c1
|
||||
CocoaLumberjack: 543c79c114dadc3b1aba95641d8738b06b05b646
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
@@ -2113,8 +2105,8 @@ SPEC CHECKSUMS:
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
hermes-engine: c9fe5870af65876125fdbbf833071b6f329db30d
|
||||
JitsiWebRTC: d0ae5fd6a81e771bfd82c2ee6c6bb542ebd65ee8
|
||||
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
|
||||
JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0
|
||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
|
||||
ObjectiveDropboxOfficial: fe206ce8c0bc49976c249d472db7fdbc53ebbd53
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "L'invio di messaggi in chat è disabilitato.",
|
||||
"enter": "Entra nella conversazione",
|
||||
"error": "Errore: il tuo messaggio non è stato inviato. Motivo: {{error}}",
|
||||
"fieldPlaceHolder": "Scrivi qui il tuo messaggio",
|
||||
@@ -546,8 +547,10 @@
|
||||
"downloadFailedDescription": "Si prega di riprovare.",
|
||||
"downloadFailedTitle": "Download non riuscito",
|
||||
"downloadFile": "Download",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione",
|
||||
"dragAndDrop": "Trascina e rilascia i file qui o da qualsiasi altra parte nella schermata",
|
||||
"fileAlreadyUploaded": "Questo file è già stato caricato nella riunione.",
|
||||
"fileTooLargeDescription": "Assicurati che il file non superi {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "Il file selezionato è troppo grande",
|
||||
"removeFile": "Rimuovi",
|
||||
"uploadFailedDescription": "Si prega di riprovare.",
|
||||
"uploadFailedTitle": "Caricamento non riuscito",
|
||||
@@ -906,6 +909,7 @@
|
||||
"visitorInQueue": " ({{count}} in attesa)",
|
||||
"visitorRequests": " ({{count}} richiesta/e)",
|
||||
"visitors": "Spettatori {{count}}",
|
||||
"visitorsList": "Spettatori ({{count}})",
|
||||
"waitingLobby": "({{count}}) in attesa"
|
||||
},
|
||||
"search": "Cerca partecipanti",
|
||||
@@ -946,7 +950,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Cambia voto",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione. Crea un sondaggio qui!",
|
||||
"empty": "Non ci sono ancora sondaggi in questa riunione.",
|
||||
"hideDetailedResults": "Nascondi dettagli",
|
||||
"showDetailedResults": "Mostra dettagli",
|
||||
"vote": "Voti"
|
||||
|
||||
@@ -67,13 +67,18 @@
|
||||
"renameBreakoutRoom": "重命名分组讨论室",
|
||||
"sendToBreakoutRoom": "将参会者移至:"
|
||||
},
|
||||
"defaultName": "分组讨论室#{{index}}",
|
||||
"breakoutList": "分组讨论室列表",
|
||||
"buttonLabel": "分组讨论室",
|
||||
"defaultName": "分组讨论室 #{{index}}",
|
||||
"hideParticipantList": "隐藏成员列表",
|
||||
"mainRoom": "主会议室",
|
||||
"notifications": {
|
||||
"joined": "正在加入“{{name}}”分组讨论室",
|
||||
"joinedMainRoom": "正在加入主会议室",
|
||||
"joinedTitle": "分组讨论室"
|
||||
}
|
||||
},
|
||||
"showParticipantList": "显示成员列表",
|
||||
"title": "分组讨论室"
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "添加会议链接",
|
||||
@@ -104,10 +109,11 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "聊天已禁用",
|
||||
"enter": "加入会议室",
|
||||
"error": "错误:你的消息未发送。原因:{{error}}",
|
||||
"fieldPlaceHolder": "在这里输入你的信息",
|
||||
"lobbyChatMessageTo": "大厅聊天消息发送至{{recipient}}",
|
||||
"lobbyChatMessageTo": "等候室聊天消息发送至{{recipient}}",
|
||||
"message": "信息",
|
||||
"messageAccessibleTitle": "{{user}}:",
|
||||
"messageAccessibleTitleMe": "我:",
|
||||
@@ -116,19 +122,28 @@
|
||||
"newMessages": "新信息",
|
||||
"nickname": {
|
||||
"popover": "选择一个昵称",
|
||||
"title": "输入一个昵称用于聊天",
|
||||
"titleWithPolls": "输入一个昵称用于聊天和投票"
|
||||
"title": "输入昵称(用于聊天)",
|
||||
"titleWithCC": "输入昵称(用于聊天和字幕)",
|
||||
"titleWithPolls": "输入昵称(用于聊天和投票)",
|
||||
"titleWithPollsAndCC": "输入昵称(用于聊天、投票和字幕)",
|
||||
"titleWithPollsAndCCAndFileSharing": "输入昵称(用于聊天、投票、字幕和文件共享)"
|
||||
},
|
||||
"noMessagesMessage": "会议中还没有消息,在这里开始谈话吧!",
|
||||
"privateNotice": "与{{recipient}}的私聊",
|
||||
"sendButton": "发送",
|
||||
"smileysPanel": "表情符号面板",
|
||||
"systemDisplayName": "系統",
|
||||
"tabs": {
|
||||
"chat": "聊天",
|
||||
"closedCaptions": "字幕",
|
||||
"fileSharing": "文件共享",
|
||||
"polls": "投票"
|
||||
},
|
||||
"title": "聊天",
|
||||
"titleWithPolls": "聊天和投票",
|
||||
"titleWithCC": "字幕",
|
||||
"titleWithFeatures": "聊天和",
|
||||
"titleWithFileSharing": "文件",
|
||||
"titleWithPolls": "投票",
|
||||
"you": "你"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
@@ -138,6 +153,10 @@
|
||||
"dontShowAgain": "不要再问我了",
|
||||
"installExtensionText": "安装用于Google日历和Office 365集成的扩展插件"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "字幕将在主持人开启后显示",
|
||||
"startClosedCaptionsButton": "开启字幕"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "连接会议中……"
|
||||
},
|
||||
@@ -206,7 +225,7 @@
|
||||
"descriptionNew": "没反应?我们已经尝试在{{app}}的电脑应用程序中加入会议。<br /><br />你可以重试,或在网页端中启动。",
|
||||
"descriptionWithoutWeb": "没反应?我们已经尝试在{{app}}的电脑应用程序中加入会议。",
|
||||
"downloadApp": "下载APP",
|
||||
"downloadMobileApp": "从App Store下载",
|
||||
"downloadMobileApp": "前往App Store下载",
|
||||
"ifDoNotHaveApp": "如果你还没有这个APP:",
|
||||
"ifHaveApp": "如果你已经拥有该APP:",
|
||||
"joinInApp": "使用APP加入此会议",
|
||||
@@ -214,7 +233,9 @@
|
||||
"joinInBrowser": "在浏览器中加入",
|
||||
"launchMeetingLabel": "你希望如何加入此会议?",
|
||||
"launchWebButton": "在网页中启动",
|
||||
"noMobileApp": "你还没安装APP?",
|
||||
"noDesktopApp": "还没有安装桌面客户端?",
|
||||
"noMobileApp": "还没有安装手机App?",
|
||||
"or": "或",
|
||||
"termsAndConditions": "继续操作即表示你同意我们的<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>条款和条件。</a>",
|
||||
"title": "正在{{app}}中加入会议……",
|
||||
"titleNew": "正在启动你的会议……",
|
||||
@@ -250,12 +271,16 @@
|
||||
"dialog": {
|
||||
"Back": "返回",
|
||||
"Cancel": "取消",
|
||||
"IamHost": "我是主持人",
|
||||
"IamHost": "登录主持",
|
||||
"Ok": "确定",
|
||||
"Remove": "移除",
|
||||
"Share": "分享",
|
||||
"Submit": "提交",
|
||||
"WaitForHostMsg": "会议尚未开始。如果你是主持人,请进行身份验证。否则,请等待主持人加入。",
|
||||
"Understand": "我已知晓,暂时保持静音",
|
||||
"UnderstandAndUnmute": "我已知晓,请为我解除静音",
|
||||
"WaitForHostMsg": "会议尚未开始,主持人还未入会。如需成为主持人请先登录,或耐心等待会议开始。",
|
||||
"WaitForHostNoAuthMsg": "会议尚未开始,暂无主持人入会,请耐心等待",
|
||||
"WaitingForHostButton": "等待主持人",
|
||||
"WaitingForHostTitle": "正在等待主持人加入……",
|
||||
"Yes": "是",
|
||||
"accessibilityLabel": {
|
||||
@@ -269,6 +294,8 @@
|
||||
"addMeetingNote": "添加本次会议的备注",
|
||||
"addOptionalNote": "添加备注(可选):",
|
||||
"allow": "允许",
|
||||
"allowToggleCameraDialog": "你是否允许{{initiatorName}}切换你的摄像头?",
|
||||
"allowToggleCameraTitle": "允许切换摄像头?",
|
||||
"alreadySharedVideoMsg": "其他参会者正在分享视频,本次会议仅支持同时分享一个视频。",
|
||||
"alreadySharedVideoTitle": "同一时间只允许一个视频分享",
|
||||
"applicationWindow": "应用程序窗口",
|
||||
@@ -287,6 +314,7 @@
|
||||
"conferenceReloadMsg": "我们正在努力修复此问题,将在{{seconds}}秒后重新连接……",
|
||||
"conferenceReloadTitle": "糟了,好像有什么东西出错了。",
|
||||
"confirm": "确认",
|
||||
"confirmBack": "返回",
|
||||
"confirmNo": "否",
|
||||
"confirmYes": "是",
|
||||
"connectError": "发生错误,无法连接至会议!",
|
||||
@@ -295,19 +323,22 @@
|
||||
"contactSupport": "联系支持",
|
||||
"copied": "已复制",
|
||||
"copy": "复制",
|
||||
"demoteParticipantDialog": "确定将该成员调整为观众吗?",
|
||||
"demoteParticipantTitle": "调整为观众",
|
||||
"dismiss": "忽略",
|
||||
"displayNameRequired": "嗨!你叫什么名字?",
|
||||
"done": "完成",
|
||||
"e2eeDescription": "请注意,端到端加密目前处于实验阶段,开启端到端加密将禁用部分服务器端提供的服务,例如:通过电话加入会议。另外,通过网页版加入会议还需要使用支持Insertable Stream的浏览器。",
|
||||
"e2eeDisabledDueToMaxModeDescription": "由于会议中的人数过多,无法开启端到端加密。",
|
||||
"e2eeLabel": "开启端到端加密",
|
||||
"e2eeWarning": "警告:本次会议中并不是所有的参会者都支持端到端加密。如果启用它,他们将无法看到或听到你。",
|
||||
"e2eeWarning": "警告:本次会议中并不是所有的参会者都支持端到端加密。如果开启它,他们将无法看到或听到你。",
|
||||
"e2eeWillDisableDueToMaxModeDescription": "警告:如果有更多人加入会议,端到端加密将自动禁用。",
|
||||
"embedMeeting": "嵌入会议",
|
||||
"enterDisplayName": "请在此输入你的名字",
|
||||
"enterDisplayName": "请输入你的姓名",
|
||||
"error": "错误",
|
||||
"errorRoomCreationRestriction": "加入过于频繁,请稍后再试。",
|
||||
"gracefulShutdown": "我们的服务目前正在维护中,请稍后再试。",
|
||||
"grantModeratorDialog": "你确定要授予{{participantName}}主持人权限吗?",
|
||||
"grantModeratorDialog": "确定将{{participantName}}设为主持人吗?",
|
||||
"grantModeratorTitle": "授予主持人权限",
|
||||
"hide": "隐藏",
|
||||
"hideShareAudioHelper": "不要再显示",
|
||||
@@ -319,9 +350,11 @@
|
||||
"kickParticipantButton": "移除",
|
||||
"kickParticipantDialog": "你确定你要移除这位参会者吗?",
|
||||
"kickParticipantTitle": "移除这位参会者?",
|
||||
"kickSystemTitle": "你已被移出会议",
|
||||
"kickTitle": "{{participantDisplayName}}将你从会议中移除",
|
||||
"learnMore": "了解详情",
|
||||
"linkMeeting": "关联会议",
|
||||
"linkMeetingTitle": "将会议链接到Salesforce",
|
||||
"linkMeetingTitle": "议关联到Salesforce",
|
||||
"liveStreaming": "直播中",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "正在录制中,无法开启",
|
||||
"localUserControls": "本地用户控制",
|
||||
@@ -377,6 +410,10 @@
|
||||
"recentlyUsedObjects": "你最近使用的对象",
|
||||
"recording": "录制中",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "直播时无法使用",
|
||||
"recordingInProgressDescription": "本会议正由AI录制并分析{{learnMore}},你已被静音。如需发言,视为同意录制。",
|
||||
"recordingInProgressDescriptionFirstHalf": "本会议正由AI录制并分析",
|
||||
"recordingInProgressDescriptionSecondHalf": ",你已被静音。如需发言,视为同意录制。",
|
||||
"recordingInProgressTitle": "录制中",
|
||||
"rejoinNow": "马上重新加入",
|
||||
"remoteControlAllowedMessage": "{{user}}接受了你的远程控制请求!",
|
||||
"remoteControlDeniedMessage": "{{user}}拒绝了你的远程控制请求!",
|
||||
@@ -413,6 +450,7 @@
|
||||
"sessTerminatedReason": "会议已经结束",
|
||||
"sessionRestarted": "由于连接问题,呼叫重新启动。",
|
||||
"shareAudio": "继续",
|
||||
"shareAudioAltText": "如需分享内容,请切换到“浏览器标签页”,勾选“分享音频”并点击“分享”按钮",
|
||||
"shareAudioTitle": "如何分享音频",
|
||||
"shareAudioWarningD1": "你需要在分享你的音频之前停止共享屏幕。",
|
||||
"shareAudioWarningD2": "你需要重新启动你的共享屏幕并勾选“共享音频”。",
|
||||
@@ -423,7 +461,10 @@
|
||||
"shareScreenWarningD2": "你需要停止音频共享后,重新启动你的共享屏幕并勾选“共享音频”。",
|
||||
"shareScreenWarningH1": "如果你只想共享屏幕:",
|
||||
"shareScreenWarningTitle": "在共享你的屏幕之前,你需要停止共享音频",
|
||||
"shareVideoConfirmPlay": "即将打开外部网站,是否继续?",
|
||||
"shareVideoConfirmPlayTitle": "需先停止音频共享才能分享屏幕",
|
||||
"shareVideoLinkError": "请提供正确的视频链接。",
|
||||
"shareVideoLinkStopped": "{{name}}向你分享了一个视频",
|
||||
"shareVideoTitle": "分享视频",
|
||||
"shareYourScreen": "共享你的屏幕",
|
||||
"shareYourScreenDisabled": "共享屏幕已禁用。",
|
||||
@@ -474,6 +515,10 @@
|
||||
"viewUpgradeOptions": "查看升级选项",
|
||||
"viewUpgradeOptionsContent": "要获取无限制的高级功能,如录制、转录、RTMP 流等,请升级你的计划。",
|
||||
"viewUpgradeOptionsTitle": "你发现了高级版功能!",
|
||||
"whiteboardLimitContent": "同时在线白板人数已达上限,暂时无法继续使用",
|
||||
"whiteboardLimitReference": "详情请访问",
|
||||
"whiteboardLimitReferenceUrl": "我们的网站",
|
||||
"whiteboardLimitTitle": "白板人数受限",
|
||||
"yourEntireScreen": "你的整个屏幕"
|
||||
},
|
||||
"documentSharing": {
|
||||
@@ -498,6 +543,19 @@
|
||||
"veryBad": "非常差",
|
||||
"veryGood": "非常好"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "请稍后重试",
|
||||
"downloadFailedTitle": "下载失败",
|
||||
"downloadFile": "下载",
|
||||
"dragAndDrop": "拖拽文件到此处上传",
|
||||
"fileAlreadyUploaded": "文件已上传至本次会议",
|
||||
"fileTooLargeDescription": "请确保文件不超过 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "文件太大",
|
||||
"removeFile": "移除",
|
||||
"uploadFailedDescription": "请稍后重试",
|
||||
"uploadFailedTitle": "上传失败",
|
||||
"uploadFile": "文件共享"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "视频缩略图"
|
||||
@@ -545,13 +603,16 @@
|
||||
"noNumbers": "无呼入号码。",
|
||||
"noPassword": "无",
|
||||
"noRoom": "没有指定要呼入的会议室。",
|
||||
"noWhiteboard": "无法加载白板",
|
||||
"numbers": "拨入号码",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"reachedLimit": "你已达到套餐限制。",
|
||||
"sip": "SIP地址",
|
||||
"sipAudioOnly": "仅限SIP音频地址",
|
||||
"title": "分享",
|
||||
"tooltip": "分享此会议的链接和拨入信息",
|
||||
"upgradeOptions": "请查看升级选项于"
|
||||
"upgradeOptions": "请查看升级选项于",
|
||||
"whiteboardError": "加载白板出错,请稍后重试"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "我们似乎出了点小问题。",
|
||||
@@ -615,6 +676,7 @@
|
||||
"on": "直播已开始",
|
||||
"onBy": "{{name}}已开始直播",
|
||||
"pending": "直播启动中……",
|
||||
"policyError": "你尝试开始直播的速度太快,请稍后再试!",
|
||||
"serviceName": "直播服务",
|
||||
"sessionAlreadyActive": "本次会议已经在录制或直播",
|
||||
"signIn": "使用 Google 账号登录",
|
||||
@@ -630,13 +692,13 @@
|
||||
"lobby": {
|
||||
"backToKnockModeButton": "请求加入",
|
||||
"chat": "聊天",
|
||||
"dialogTitle": "大厅模式",
|
||||
"disableDialogContent": "大厅模式已开启,此功能确保未经授权的人士不能加入你的会议,确定要关闭吗?",
|
||||
"dialogTitle": "等候室模式",
|
||||
"disableDialogContent": "等候室模式已开启,此功能确保未经授权的人士不能加入你的会议,确定要关闭吗?",
|
||||
"disableDialogSubmit": "关闭",
|
||||
"emailField": "请输入你的邮箱",
|
||||
"enableDialogPasswordField": "设置密码(可选)",
|
||||
"enableDialogSubmit": "开启",
|
||||
"enableDialogText": "在大厅模式下,参会者只有被主持人同意后才能加入,进而保护你的会议。",
|
||||
"enableDialogText": "在等候室模式下,参会者只有被主持人同意后才能加入,进而保护你的会议。",
|
||||
"enterPasswordButton": "输入会议密码",
|
||||
"enterPasswordTitle": "输入密码加入会议",
|
||||
"errorMissingPassword": "加入请求被拒绝。",
|
||||
@@ -651,18 +713,18 @@
|
||||
"knockButton": "请求加入",
|
||||
"knockTitle": "有人想要加入会议",
|
||||
"knockingParticipantList": "请求加入的参会者名单",
|
||||
"lobbyChatStartedNotification": "{{moderator}}与{{attendee}}开始在大厅聊天",
|
||||
"lobbyChatStartedTitle": "{{moderator}}已开始与你进行大厅聊天",
|
||||
"nameField": "输入你的名字",
|
||||
"lobbyChatStartedNotification": "{{moderator}}与{{attendee}}开始在等候室聊天",
|
||||
"lobbyChatStartedTitle": "{{moderator}}已开始与你进行等候室聊天",
|
||||
"lobbyClosed": "等候室已关闭",
|
||||
"nameField": "输入你的姓名",
|
||||
"notificationLobbyAccessDenied": "{{originParticipantName}}已拒绝{{targetParticipantName}}的加入请求",
|
||||
"notificationLobbyAccessGranted": "{{originParticipantName}}已通过{{targetParticipantName}}的加入请求",
|
||||
"notificationLobbyDisabled": "{{originParticipantName}}关闭了大厅模式",
|
||||
"notificationLobbyEnabled": "{{originParticipantName}}开启了大厅模式",
|
||||
"notificationTitle": "大厅",
|
||||
"passwordField": "输入会议密码",
|
||||
"notificationLobbyDisabled": "{{originParticipantName}}关闭了等候室模式",
|
||||
"notificationLobbyEnabled": "{{originParticipantName}}开启了等候室模式",
|
||||
"notificationTitle": "等候室",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "大厅",
|
||||
"toggleLabel": "开启大厅模式"
|
||||
"title": "等候室",
|
||||
"toggleLabel": "开启等候室模式"
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
@@ -705,7 +767,9 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAction": "允许",
|
||||
"allowAudio": "允许开启麦克风",
|
||||
"allowBoth": "允许音视频",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"allowedUnmute": "你可以解除麦克风静音、启动摄像头或共享屏幕。",
|
||||
"audioUnmuteBlockedDescription": "由于系统限制,麦克风解除静音操作被暂时阻止。",
|
||||
"audioUnmuteBlockedTitle": "麦克风解除静音被阻止!",
|
||||
@@ -713,9 +777,14 @@
|
||||
"connectedOneMember": "{{name}}加入了会议",
|
||||
"connectedThreePlusMembers": "{{name}}和其他人加入了会议",
|
||||
"connectedTwoMembers": "{{first}}和{{second}}加入了会议",
|
||||
"dataChannelClosed": "视频质量受损",
|
||||
"dataChannelClosedDescription": "桥接通道已断开连接,因此视频质量限制为最低设置。",
|
||||
"disabledIframe": "嵌入仅用于演示目的,因此此通话将在 {{timeout}} 分钟后断开连接。",
|
||||
"connectionFailed": "连接失败,请稍后再试",
|
||||
"dataChannelClosed": "视频质量可能受影响",
|
||||
"dataChannelClosedDescription": "桥接通道已断开,视频质量可能会被限制为最低设置",
|
||||
"dataChannelClosedDescriptionWithAudio": "桥接通道已断开,音视频可能会出现卡顿或中断",
|
||||
"dataChannelClosedWithAudio": "音视频质量可能受影响",
|
||||
"disabledIframe": "嵌入仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryNative": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开",
|
||||
"disabledIframeSecondaryWeb": "嵌入{{domain}}仅用于演示,本次通话将在{{timeout}}分钟后自动断开。如需在正式环境嵌入,请使用<a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi服务</a>!",
|
||||
"disconnected": "已断开连接",
|
||||
"displayNotifications": "显示通知",
|
||||
"dontRemindMe": "不要提醒我",
|
||||
@@ -723,7 +792,10 @@
|
||||
"focusFail": "{{component}}不可用 - {{ms}}秒后重试",
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "通知",
|
||||
"hostAskedUnmute": "主持人希望你发言",
|
||||
"hostAskedUnmute": "主持人请你发言",
|
||||
"invalidTenant": "无效的组织空间",
|
||||
"invalidTenantHyphenDescription": "你所使用的组织空间名无效(不能以‘-’开头或结尾)",
|
||||
"invalidTenantLengthDescription": "你所使用的组织空间名过长",
|
||||
"invitedOneMember": "已邀请{{name}}",
|
||||
"invitedThreePlusMembers": "已邀请{{name}}以及另外{{count}}人",
|
||||
"invitedTwoMembers": "已邀请{{first}}和{{second}}",
|
||||
@@ -760,7 +832,8 @@
|
||||
"newDeviceAction": "使用",
|
||||
"newDeviceAudioTitle": "检测到新的音频设备",
|
||||
"newDeviceCameraTitle": "检测到新的摄像头",
|
||||
"noiseSuppressionDesktopAudioDescription": "在共享桌面音频时无法启用降噪功能,请禁用并重试。",
|
||||
"nextToSpeak": "下一个轮到你发言",
|
||||
"noiseSuppressionDesktopAudioDescription": "在共享桌面音频时无法开启降噪功能,请禁用并重试。",
|
||||
"noiseSuppressionFailedTitle": "无法启动降噪功能",
|
||||
"noiseSuppressionStereoDescription": "暂不支持立体声音频降噪。",
|
||||
"oldElectronClientDescription1": "你似乎正在使用存在已知安全漏洞的旧版Jitsi Meet客户端,请确保您更新到我们的",
|
||||
@@ -784,45 +857,62 @@
|
||||
"startSilentDescription": "重新加入会议以开启音频",
|
||||
"startSilentTitle": "你加入时没有开启音频!",
|
||||
"suboptimalBrowserWarning": "我们担心你本次会议体验欠佳,我们正在寻找改进的方法。与此同时,请尝试使用<a href='{{recommendedBrowserPageLink}}' target='_blank'>完全支持的浏览器</a>。",
|
||||
"suboptimalExperienceTitle": "浏览器警告",
|
||||
"suboptimalExperienceTitle": "浏览器提示",
|
||||
"suggestRecordingAction": "开始录制",
|
||||
"suggestRecordingDescription": "是否需要录制本次会议?",
|
||||
"suggestRecordingTitle": "录制会议",
|
||||
"unmute": "解除静音",
|
||||
"videoMutedRemotelyDescription": "你随时可以重新打开。",
|
||||
"unmuteVideo": "开启摄像头",
|
||||
"videoMutedRemotelyDescription": "你可随时重新开启视频",
|
||||
"videoMutedRemotelyTitle": "{{participantDisplayName}}已关闭你的视频",
|
||||
"videoUnmuteBlockedDescription": "由于系统限制,开启摄像头和共享屏幕操作已被暂时阻止。",
|
||||
"videoUnmuteBlockedTitle": "开启摄像头和共享屏幕被阻止!",
|
||||
"viewLobby": "查看大厅",
|
||||
"waitingParticipants": "{{waitingParticipants}}人"
|
||||
"videoUnmuteBlockedDescription": "由于系统限制,暂时无法开启摄像头或共享桌面",
|
||||
"videoUnmuteBlockedTitle": "摄像头和桌面共享被禁用!",
|
||||
"viewLobby": "查看等候室",
|
||||
"viewParticipants": "查看参会者",
|
||||
"viewVisitors": "查看观众",
|
||||
"waitingParticipants": "{{waitingParticipants}}人",
|
||||
"waitingVisitors": "排队等候的观众:{{waitingVisitors}}人",
|
||||
"waitingVisitorsTitle": "会议尚未开始!",
|
||||
"whiteboardLimitDescription": "因用户数量已接近上限,白板即将关闭,请及时保存内容。",
|
||||
"whiteboardLimitTitle": "白板使用限制"
|
||||
},
|
||||
"participantsPane": {
|
||||
"actions": {
|
||||
"admit": "同意",
|
||||
"admitAll": "同意全部",
|
||||
"admit": "同意加入",
|
||||
"admitAll": "全部同意加入",
|
||||
"allow": "允许参会者:",
|
||||
"allowVideo": "允许视频",
|
||||
"askUnmute": "请求解除静音",
|
||||
"audioModeration": "自我解除静音",
|
||||
"allowVideo": "允许开启摄像头",
|
||||
"askUnmute": "请求取消静音",
|
||||
"audioModeration": "自行解除静音",
|
||||
"blockEveryoneMicCamera": "禁用所有人的麦克风和摄像头",
|
||||
"breakoutRooms": "分组讨论室",
|
||||
"goLive": "开始直播",
|
||||
"invite": "邀请其他人",
|
||||
"lowerAllHands": "取消全部举手",
|
||||
"lowerHand": "取消举手",
|
||||
"moreModerationActions": "更多主持人选项",
|
||||
"moreModerationControls": "更多主持人控制",
|
||||
"moreParticipantOptions": "更多参会者选项",
|
||||
"mute": "静音",
|
||||
"muteAll": "全体静音",
|
||||
"muteEveryoneElse": "全体静音",
|
||||
"muteEveryoneElse": "静音其他人",
|
||||
"reject": "拒绝",
|
||||
"stopEveryonesVideo": "禁用所有人视频",
|
||||
"stopVideo": "禁用视频",
|
||||
"unblockEveryoneMicCamera": "允许所有人的麦克风和摄像头",
|
||||
"videoModeration": "开启视频"
|
||||
"stopEveryonesVideo": "关闭所有人摄像头",
|
||||
"stopVideo": "关闭摄像头",
|
||||
"unblockEveryoneMicCamera": "允许所有人开启麦克风和摄像头",
|
||||
"videoModeration": "允许开启视频"
|
||||
},
|
||||
"close": "关闭",
|
||||
"headings": {
|
||||
"lobby": "大厅(({{count}}人)",
|
||||
"lobby": "等候室(({{count}}人)",
|
||||
"participantsList": "会议参会者({{count}}人)",
|
||||
"visitors": "访客(({{count}}人)",
|
||||
"waitingLobby": "在大厅等待({{count}}人)"
|
||||
"visitorInQueue": "(排队中:{{count}}人)",
|
||||
"visitorRequests": "(请求加入:{{count}}人)",
|
||||
"visitors": "观众(({{count}}人)",
|
||||
"waitingLobby": "在等候室等待({{count}}人)"
|
||||
},
|
||||
"search": "搜索参会者",
|
||||
"searchDescription": "输入关键词快速筛选参会者",
|
||||
"title": "参会者"
|
||||
},
|
||||
"passwordDigitsOnly": "最多{{number}}位数字",
|
||||
@@ -831,20 +921,24 @@
|
||||
"pinnedParticipant": "参会者已固定",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"edit": "编辑",
|
||||
"send": "发送",
|
||||
"skip": "跳过",
|
||||
"submit": "提交"
|
||||
},
|
||||
"by": "由{{ name }}",
|
||||
"by": "由{{ name }}发起",
|
||||
"closeButton": "结束投票",
|
||||
"create": {
|
||||
"addOption": "添加选项",
|
||||
"answerPlaceholder": "选项{{index}}",
|
||||
"cancel": "取消",
|
||||
"create": "创建投票",
|
||||
"create": "新建投票",
|
||||
"pollOption": "投票选项{{index}}",
|
||||
"pollQuestion": "投票内容",
|
||||
"questionPlaceholder": "提出问题",
|
||||
"removeOption": "移除选项",
|
||||
"send": "发送"
|
||||
"questionPlaceholder": "请输入你的问题",
|
||||
"removeOption": "删除选项",
|
||||
"save": "保存",
|
||||
"send": "发布"
|
||||
},
|
||||
"errors": {
|
||||
"notUniqueOption": "选项必须是唯一的"
|
||||
@@ -866,61 +960,65 @@
|
||||
"audioAndVideoError": "音频和视频错误:",
|
||||
"audioDeviceProblem": "你的音频设备存在问题",
|
||||
"audioOnlyError": "音频错误:",
|
||||
"audioTrackError": "无法创建音轨。",
|
||||
"callMe": "给我打电话",
|
||||
"callMeAtNumber": "打电话给我,号码是:",
|
||||
"calling": "正在拨号",
|
||||
"audioTrackError": "无无法创建音频轨道",
|
||||
"callMe": "呼叫我",
|
||||
"callMeAtNumber": "请拨打我的号码:",
|
||||
"calling": "正在呼叫",
|
||||
"configuringDevices": "正在配置设备……",
|
||||
"connectedWithAudioQ": "你已连接音频?",
|
||||
"connection": {
|
||||
"failed": "连接测试失败!",
|
||||
"good": "你的网络连接看起来很好!",
|
||||
"nonOptimal": "你的网络连接不太理想",
|
||||
"poor": "你的网络连接不太理想"
|
||||
"poor": "你的网络连接较差",
|
||||
"running": "正在测试连接……"
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "我们预计你的音频会有卡顿现象。",
|
||||
"audioHighQuality": "我们预计你的音频质量会很好。",
|
||||
"audioLowNoVideo": "我们预计你的音频质量会很差且没有视频画面。",
|
||||
"goodQuality": "太棒了!你的媒体质量会很好。",
|
||||
"noMediaConnectivity": "我们无法建立连接,这通常是防火墙或NAT的问题。",
|
||||
"noVideo": "我们预计你的视频画质会很糟糕。",
|
||||
"undetectable": "如果仍无法在浏览器中进行通话,我们建议你检查扬声器、麦克风和摄像头的设置,确定浏览器是否有使用麦克风和摄像头的权限,并将浏览器升级到最新版本。如果仍未解决问题,请与开发人员联系。",
|
||||
"veryPoorConnection": "我们预计你的通话质量会非常糟糕。",
|
||||
"videoFreezing": "我们预计你的视频会冻结、变黑并且呈像素化状态。",
|
||||
"videoHighQuality": "我们预计你的视频质量会很好。",
|
||||
"videoLowQuality": "我们预计你的视频帧率和分辨率会很低。",
|
||||
"videoTearing": "我们预计你的视频会呈现像素化或有视觉伪影。"
|
||||
"audioClipping": "预计你的音频可能会断断续续",
|
||||
"audioHighQuality": "你的音频质量预计会很清晰",
|
||||
"audioLowNoVideo": "预计你的音频质量较差且无法显示视频画面",
|
||||
"goodQuality": "网络和设备状态都很好,体验会很流畅",
|
||||
"noMediaConnectivity": "无法建立音视频连接,通常是防火墙或NAT设置导致",
|
||||
"noVideo": "预计你的视频画面可能会很糟糕",
|
||||
"testFailed": "连接测试遇到异常,但不一定会影响实际体验",
|
||||
"undetectable": "如果你仍无法正常通话,请检查扬声器、麦克风、摄像头设置,并确保浏览器有相关权限且为最新版本。如仍有问题,建议联系技术支持。",
|
||||
"veryPoorConnection": "你的通话质量可能会非常差",
|
||||
"videoFreezing": "预计你的视频会出现卡顿、黑屏或马赛克",
|
||||
"videoHighQuality": "你的视频质量预计会很清晰",
|
||||
"videoLowQuality": "预计你的视频帧率和分辨率会很低或画面不流畅",
|
||||
"videoTearing": "预计你的视频画面会有马赛克或其它异常现象"
|
||||
},
|
||||
"copyAndShare": "复制并分享会议链接",
|
||||
"dialInMeeting": "拨打会议电话",
|
||||
"dialInPin": "拨打会议电话并输入PIN码:",
|
||||
"dialInMeeting": "拨电话接入会议",
|
||||
"dialInPin": "电话接入会议并输入PIN码:",
|
||||
"dialing": "正在拨号",
|
||||
"doNotShow": "不再显示",
|
||||
"doNotShow": "不再显示此页面",
|
||||
"errorDialOut": "无法拨出",
|
||||
"errorDialOutDisconnected": "无法拨出,已断开连接",
|
||||
"errorDialOutFailed": "无法拨出,呼叫失败",
|
||||
"errorDialOutStatus": "获取拨出状态时出错",
|
||||
"errorMissingName": "请输入你的名字以加入会议",
|
||||
"errorNoPermissions": "你需要允许访问麦克风和摄像头",
|
||||
"errorStatusCode": "拨出错误,状态代码:{{status}}",
|
||||
"errorValidation": "号码验证失败",
|
||||
"iWantToDialIn": "我想拨打电话加入",
|
||||
"initiated": "通话已发起",
|
||||
"errorDialOutFailed": "无法拨出,通话失败",
|
||||
"errorDialOutStatus": "获取拨出状态出错",
|
||||
"errorMissingName": "请输入姓名后再加入会议",
|
||||
"errorNoPermissions": "请开启麦克风和摄像头权限",
|
||||
"errorStatusCode": "拨号失败,状态码:{{status}}",
|
||||
"errorValidation": "号码校验失败",
|
||||
"iWantToDialIn": "我想通过电话加入",
|
||||
"initiated": "呼叫已发起",
|
||||
"joinAudioByPhone": "使用电话音频加入",
|
||||
"joinMeeting": "加入会议",
|
||||
"joinMeetingInLowBandwidthMode": "以省流模式加入",
|
||||
"joinWithoutAudio": "无音频加入",
|
||||
"keyboardShortcuts": "开启键盘快捷键",
|
||||
"keyboardShortcuts": "开启快捷键",
|
||||
"linkCopied": "链接已复制到剪贴板",
|
||||
"lookGood": "你的麦克风工作正常",
|
||||
"lookGood": "设备一切正常",
|
||||
"or": "或",
|
||||
"premeeting": "会前",
|
||||
"premeeting": "会前设置",
|
||||
"proceedAnyway": "仍然继续",
|
||||
"screenSharingError": "共享屏幕错误:",
|
||||
"startWithPhone": "以电话音频开始",
|
||||
"unsafeRoomConsent": "我了解风险,我想加入会议",
|
||||
"videoOnlyError": "视频错误:",
|
||||
"videoTrackError": "无法创建视频轨道。",
|
||||
"recordingWarning": "其他参会者可能正在录制本次会议",
|
||||
"screenSharingError": "屏幕共享出错:",
|
||||
"startWithPhone": "用电话音频开始",
|
||||
"unsafeRoomConsent": "我了解风险,仍然加入会议",
|
||||
"videoOnlyError": "视频出错:",
|
||||
"videoTrackError": "无法创建视频轨道",
|
||||
"viewAllNumbers": "查看所有号码"
|
||||
},
|
||||
"presenceStatus": {
|
||||
@@ -981,7 +1079,6 @@
|
||||
"limitNotificationDescriptionNative": "由于高需求,您的录制将限制在{{limit}}分钟内。若要无限制录制,请尝试<3>{{app}}</3>。",
|
||||
"limitNotificationDescriptionWeb": "由于高需求,您的录制将限制在{{limit}}分钟内。若要无限制录制,请尝试<a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
|
||||
"linkGenerated": "我们已生成录制链接。",
|
||||
"live": "直播中",
|
||||
"localRecordingNoNotificationWarning": "系统不会主动通知参会者录制已开启,主持人需另行提醒。",
|
||||
"localRecordingNoVideo": "视频未被录制",
|
||||
"localRecordingStartWarning": "请确保在退出会议之前停止录制,以便保存录制内容。",
|
||||
@@ -998,13 +1095,16 @@
|
||||
"onBy": "{{name}}开启了录制",
|
||||
"onlyRecordSelf": "仅录制我的音频和视频",
|
||||
"pending": "正在准备录制会议……",
|
||||
"rec": "录制中",
|
||||
"policyError": "",
|
||||
"recordAudioAndVideo": "",
|
||||
"recordTranscription": "",
|
||||
"saveLocalRecording": "本地保存录制文件(测试版)",
|
||||
"serviceDescription": "录制将由录制服务保存",
|
||||
"serviceDescriptionCloud": "云录制",
|
||||
"serviceDescriptionCloudInfo": "录制的会议将在录制后的24小时内自动清除。",
|
||||
"serviceName": "录制服务",
|
||||
"sessionAlreadyActive": "此会议已经在录制或直播中",
|
||||
"showAdvancedOptions": "高级选项",
|
||||
"signIn": "登录",
|
||||
"signOut": "注销",
|
||||
"surfaceError": "请选择当前选项卡。",
|
||||
@@ -1033,21 +1133,23 @@
|
||||
"audio": "音频",
|
||||
"buttonLabel": "设置",
|
||||
"calendar": {
|
||||
"about": "{{appName}}日历集成用于安全访问你的日历,以读取即将到来的活动。",
|
||||
"about": "{{appName}}日历集成可安全访问你的日历,读取即将开始的会议。",
|
||||
"disconnect": "断开连接",
|
||||
"microsoftSignIn": "使用 Microsoft 登录",
|
||||
"signedIn": "目前正在读取{{email}}的日历事件,点击下面的断开连接可以停止读取日历事件。",
|
||||
"signedIn": "正在读取{{email}}的日历事件,点击下方“断开连接”可停止同步日历。",
|
||||
"title": "日历"
|
||||
},
|
||||
"chatWithPermissions": "聊天需要相关权限",
|
||||
"desktopShareFramerate": "共享屏幕帧率",
|
||||
"desktopShareHighFpsWarning": "高帧率的共享屏幕的可能会影响你的网速,你需要重新启动共享屏幕以使新设置生效。",
|
||||
"desktopShareWarning": "你需要重新启动共享屏幕以使新设置生效。",
|
||||
"devices": "设备",
|
||||
"followMe": "所有人跟随",
|
||||
"framesPerSecond": "帧",
|
||||
"incomingMessage": "新消息",
|
||||
"followMe": "全员视角跟随我",
|
||||
"followMeRecorder": "录制画面跟随我",
|
||||
"framesPerSecond": "帧率(FPS)",
|
||||
"incomingMessage": "新消息提醒",
|
||||
"language": "语言",
|
||||
"loggedIn": "以{{name}}登录",
|
||||
"loggedIn": "已登录:{{name}}",
|
||||
"maxStageParticipants": "可以固定的最大参会者人数",
|
||||
"microphones": "麦克风",
|
||||
"moderator": "主持人",
|
||||
@@ -1057,52 +1159,55 @@
|
||||
"noDevice": "无",
|
||||
"notifications": "通知",
|
||||
"participantJoined": "参会者已加入",
|
||||
"participantKnocking": "参会者已进入大厅",
|
||||
"participantKnocking": "参会者已进入等候室",
|
||||
"participantLeft": "参会者已离开",
|
||||
"playSounds": "播放提示音",
|
||||
"reactions": "会议反应",
|
||||
"sameAsSystem": "与系统相同({{label}})",
|
||||
"selectAudioOutput": "音频输出",
|
||||
"reactions": "会议互动表情",
|
||||
"sameAsSystem": "与系统一致({{label}})",
|
||||
"selectAudioOutput": "音频输出设备",
|
||||
"selectCamera": "摄像头",
|
||||
"selectMic": "麦克风",
|
||||
"selfView": "本人视图",
|
||||
"shortcuts": "快捷键",
|
||||
"showSubtitlesOnStage": "主画面显示字幕",
|
||||
"speakers": "扬声器",
|
||||
"startAudioMuted": "所有人开始时静音",
|
||||
"startReactionsMuted": "关闭所有人反应提示音",
|
||||
"startVideoMuted": "所有人开始时隐藏视频画面",
|
||||
"talkWhileMuted": "通话时静音",
|
||||
"startAudioMuted": "所有人加入时静音",
|
||||
"startReactionsMuted": "关闭所有人互动音效",
|
||||
"startVideoMuted": "所有人加入时隐藏视频",
|
||||
"talkWhileMuted": "静音时说话提醒",
|
||||
"title": "设置",
|
||||
"video": "视频"
|
||||
},
|
||||
"settingsView": {
|
||||
"advanced": "高级",
|
||||
"advanced": "高级设置",
|
||||
"alertCancel": "取消",
|
||||
"alertOk": "确认",
|
||||
"alertTitle": "警告",
|
||||
"alertURLText": "输入的服务器网址无效",
|
||||
"buildInfoSection": "生成信息",
|
||||
"conferenceSection": "会议",
|
||||
"disableCallIntegration": "禁用本地电话",
|
||||
"disableCrashReporting": "禁用崩溃报告",
|
||||
"disableCrashReportingWarning": "你确定要禁用崩溃报告吗?重启应用后生效。",
|
||||
"disableP2P": "禁用点对点模式",
|
||||
"alertURLText": "输入的服务器地址无效",
|
||||
"apply": "应用",
|
||||
"buildInfoSection": "构建信息",
|
||||
"conferenceSection": "会议设置",
|
||||
"disableCallIntegration": "禁用系统通话集成",
|
||||
"disableCrashReporting": "关闭崩溃报告",
|
||||
"disableCrashReportingWarning": "确定要关闭崩溃报告吗?重启应用后设置才会生效",
|
||||
"disableP2P": "关闭点对点模式",
|
||||
"displayName": "显示名称",
|
||||
"displayNamePlaceholderText": "例如:张三",
|
||||
"email": "邮箱",
|
||||
"emailPlaceholderText": "email@example.com",
|
||||
"gavatarMessage": "如果你的邮箱已绑定Gravatar头像,我们将自动为你显示头像",
|
||||
"goTo": "前往",
|
||||
"header": "设置",
|
||||
"help": "帮助",
|
||||
"links": "链接",
|
||||
"links": "相关链接",
|
||||
"privacy": "隐私",
|
||||
"profileSection": "简介",
|
||||
"profileSection": "个人信息",
|
||||
"sdkVersion": "SDK版本",
|
||||
"serverURL": "服务器网址",
|
||||
"serverURL": "服务器地址",
|
||||
"showAdvanced": "显示高级设置",
|
||||
"startCarModeInLowBandwidthMode": "同时开启驾驶模式和省流模式",
|
||||
"startWithAudioMuted": "关闭音频并启动",
|
||||
"startWithVideoMuted": "关闭视频并启动",
|
||||
"startCarModeInLowBandwidthMode": "省流模式下开启驾驶模式",
|
||||
"startWithAudioMuted": "进入会议时音频静音",
|
||||
"startWithVideoMuted": "进入会议时关闭视频",
|
||||
"terms": "条款",
|
||||
"version": "APP版本"
|
||||
},
|
||||
@@ -1113,20 +1218,22 @@
|
||||
"speaker": "扬声器",
|
||||
"speakerStats": {
|
||||
"angry": "生气",
|
||||
"disgusted": "呕吐",
|
||||
"disgusted": "嫌弃",
|
||||
"displayEmotions": "显示表情",
|
||||
"fearful": "害怕",
|
||||
"happy": "笑脸",
|
||||
"happy": "开心",
|
||||
"hours": "{{count}}时",
|
||||
"labelTooltip": "参会者人数:{{count}}",
|
||||
"minutes": "{{count}}分",
|
||||
"name": "名字",
|
||||
"neutral": "中立",
|
||||
"sad": "悲伤",
|
||||
"neutral": "中性",
|
||||
"sad": "难过",
|
||||
"search": "搜索",
|
||||
"searchDescription": "输入关键词筛选参会者",
|
||||
"searchHint": "搜索参会者",
|
||||
"seconds": "{{count}}秒",
|
||||
"speakerStats": "发言统计",
|
||||
"speakerTime": "发言时间",
|
||||
"speakerStats": "参会者统计",
|
||||
"speakerTime": "发言时长",
|
||||
"surprised": "惊讶"
|
||||
},
|
||||
"startupoverlay": {
|
||||
@@ -1144,36 +1251,37 @@
|
||||
},
|
||||
"toggleTopPanelLabel": "切换顶部面板",
|
||||
"toolbar": {
|
||||
"Settings": "设置",
|
||||
"Settings": "打开设置",
|
||||
"accessibilityLabel": {
|
||||
"Settings": "切换设置",
|
||||
"audioOnly": "切换仅音频模式",
|
||||
"audioRoute": "选择音频设备",
|
||||
"boo": "嘘声",
|
||||
"breakoutRoom": "加入/离开分组讨论室",
|
||||
"callQuality": "管理视频质量",
|
||||
"breakoutRooms": "分组讨论室",
|
||||
"callQuality": "调整视频质量",
|
||||
"carmode": "驾驶模式",
|
||||
"cc": "切换字幕",
|
||||
"chat": "打开/关闭聊天",
|
||||
"clap": "鼓掌",
|
||||
"closeChat": "关闭聊天",
|
||||
"closeChat": "关闭聊天窗口",
|
||||
"closeMoreActions": "关闭更多操作菜单",
|
||||
"closeParticipantsPane": "关闭参会者面板",
|
||||
"collapse": "折叠",
|
||||
"closedCaptions": "隐藏字幕",
|
||||
"collapse": "收起",
|
||||
"document": "切换共享文档",
|
||||
"documentClose": "关闭共享文档",
|
||||
"documentOpen": "打开共享文档",
|
||||
"download": "下载我们的APP",
|
||||
"embedMeeting": "嵌入会议",
|
||||
"endConference": "结束会议",
|
||||
"endConference": "结束全体会议",
|
||||
"enterFullScreen": "进入全屏模式",
|
||||
"enterTileView": "进入平铺视图",
|
||||
"enterTileView": "进入画廊视图",
|
||||
"exitFullScreen": "退出全屏模式",
|
||||
"exitTileView": "退出平铺视图",
|
||||
"exitTileView": "退出画廊视图",
|
||||
"expand": "展开",
|
||||
"feedback": "提供反馈",
|
||||
"fullScreen": "切换全屏模式",
|
||||
"giphy": "切换GIPHY菜单",
|
||||
"giphy": "切换表情动图菜单",
|
||||
"grantModerator": "授予主持人权限",
|
||||
"hangup": "离开会议",
|
||||
"heading": "工具栏",
|
||||
@@ -1183,30 +1291,32 @@
|
||||
"kick": "移除参会者",
|
||||
"laugh": "大笑",
|
||||
"leaveConference": "离开会议",
|
||||
"like": "竖起大拇指",
|
||||
"linkToSalesforce": "链接到 Salesforce",
|
||||
"lobbyButton": "开启/关闭大厅模式",
|
||||
"localRecording": "切换本地录制控件",
|
||||
"lockRoom": "开启/关闭会议密码",
|
||||
"lowerHand": "放下手",
|
||||
"like": "点赞",
|
||||
"linkToSalesforce": "关联到Salesforce",
|
||||
"lobbyButton": "开启/关闭等候室",
|
||||
"localRecording": "切换本地录制",
|
||||
"lockRoom": "设置/取消会议密码",
|
||||
"love": "爱心",
|
||||
"lowerHand": "取消举手",
|
||||
"moreActions": "更多操作",
|
||||
"moreActionsMenu": "更多操作菜单",
|
||||
"moreOptions": "显示更多选项",
|
||||
"mute": "静音",
|
||||
"mute": "麦克风静音",
|
||||
"muteEveryone": "将所有人静音",
|
||||
"muteEveryoneElse": "将其他人静音",
|
||||
"muteEveryoneElsesVideoStream": "停止其他人的视频",
|
||||
"muteEveryonesVideoStream": "停止全部人的视频",
|
||||
"muteGUMPending": "连接你的麦克风",
|
||||
"noiseSuppression": "降噪",
|
||||
"openChat": "打开聊天",
|
||||
"participants": "打开参会者面板",
|
||||
"muteEveryoneElse": "除自己外全部静音",
|
||||
"muteEveryoneElsesVideoStream": "停止其他人摄像头",
|
||||
"muteEveryonesVideoStream": "关闭所有人摄像头",
|
||||
"muteGUMPending": "正在连接麦克风",
|
||||
"noiseSuppression": "开启降噪(测试版)",
|
||||
"openChat": "打开聊天窗口",
|
||||
"participants": "打开参会者面板,参会者(共{{participantsCount}}人)",
|
||||
"pip": "切换画中画模式",
|
||||
"privateMessage": "发送私人消息",
|
||||
"profile": "编辑你的个人资料",
|
||||
"privateMessage": "发送私聊",
|
||||
"profile": "编辑个人信息",
|
||||
"raiseHand": "举手",
|
||||
"reactions": "反应",
|
||||
"reactionsMenu": "反应菜单",
|
||||
"react": "消息表情",
|
||||
"reactions": "互动表情",
|
||||
"reactionsMenu": "互动表情菜单",
|
||||
"recording": "切换录制",
|
||||
"remoteMute": "静音参会者",
|
||||
"remoteVideoMute": "禁用参会者摄像头",
|
||||
@@ -1223,45 +1333,46 @@
|
||||
"silence": "沉默",
|
||||
"speakerStats": "切换参会者统计",
|
||||
"stopScreenSharing": "停止屏幕共享",
|
||||
"stopSharedVideo": "停止视频",
|
||||
"stopSharedVideo": "停止视频共享",
|
||||
"surprised": "惊讶",
|
||||
"tileView": "切换画廊视图",
|
||||
"toggleCamera": "切换摄像头",
|
||||
"toggleFilmstrip": "切换幻灯片",
|
||||
"unmute": "取消静音",
|
||||
"videoblur": "切换视频模糊",
|
||||
"toggleFilmstrip": "切换缩略图栏",
|
||||
"unmute": "取消麦克风静音",
|
||||
"videoblur": "切换虚化背景",
|
||||
"videomute": "关闭摄像头",
|
||||
"videomuteGUMPending": "连接你的摄像头",
|
||||
"videounmute": "启动摄像头"
|
||||
"videomuteGUMPending": "正在连接摄像头",
|
||||
"videounmute": "打开摄像头"
|
||||
},
|
||||
"addPeople": "添加人员到你的通话中",
|
||||
"addPeople": "添加成员到通话中",
|
||||
"audioOnlyOff": "关闭省流模式",
|
||||
"audioOnlyOn": "启用省流模式",
|
||||
"audioOnlyOn": "开启省流模式",
|
||||
"audioRoute": "选择音频设备",
|
||||
"audioSettings": "音频设置",
|
||||
"authenticate": "认证",
|
||||
"authenticate": "身份验证",
|
||||
"boo": "嘘声",
|
||||
"callQuality": "管理视频质量",
|
||||
"callQuality": "调整视频质量",
|
||||
"chat": "打开/关闭聊天",
|
||||
"clap": "鼓掌",
|
||||
"closeChat": "关闭聊天",
|
||||
"closeParticipantsPane": "关闭参会者面板",
|
||||
"closeReactionsMenu": "关闭反应菜单",
|
||||
"disableNoiseSuppression": "关闭降噪",
|
||||
"disableReactionSounds": "你可以禁用此会议的反应声音",
|
||||
"closeParticipantsPane": "关闭参会者列表",
|
||||
"closeReactionsMenu": "关闭互动表情菜单",
|
||||
"closedCaptions": "关闭字幕",
|
||||
"disableNoiseSuppression": "关闭降噪功能(测试版)",
|
||||
"disableReactionSounds": "你可以禁用此会议的互动音效",
|
||||
"documentClose": "关闭文件共享",
|
||||
"documentOpen": "开启文件共享",
|
||||
"download": "下载我们的APP",
|
||||
"e2ee": "端到端加密",
|
||||
"embedMeeting": "嵌入会议",
|
||||
"enableNoiseSuppression": "启用降噪",
|
||||
"endConference": "结束会议",
|
||||
"enableNoiseSuppression": "开启降噪功能(测试版)",
|
||||
"endConference": "结束全体会议",
|
||||
"enterFullScreen": "进入全屏模式",
|
||||
"enterTileView": "进入画廊视图",
|
||||
"exitFullScreen": "退出全屏模式",
|
||||
"exitTileView": "退出画廊视图",
|
||||
"feedback": "提供反馈",
|
||||
"giphy": "关闭GIPHY菜单",
|
||||
"feedback": "意见反馈",
|
||||
"giphy": "表情动画菜单",
|
||||
"hangup": "离开会议",
|
||||
"help": "帮助",
|
||||
"hideWhiteboard": "隐藏白板",
|
||||
@@ -1270,13 +1381,14 @@
|
||||
"laugh": "大笑",
|
||||
"leaveBreakoutRoom": "离开分组讨论室",
|
||||
"leaveConference": "离开会议",
|
||||
"like": "竖起大拇指",
|
||||
"like": "点赞",
|
||||
"linkToSalesforce": "链接到 Salesforce",
|
||||
"lobbyButtonDisable": "关闭大厅模式",
|
||||
"lobbyButtonEnable": "开启大厅模式",
|
||||
"lobbyButtonDisable": "关闭等候室模式",
|
||||
"lobbyButtonEnable": "开启等候室模式",
|
||||
"login": "登录",
|
||||
"logout": "注销",
|
||||
"lowerYourHand": "放下手",
|
||||
"love": "爱心",
|
||||
"lowerYourHand": "取消举手",
|
||||
"moreActions": "更多操作",
|
||||
"moreOptions": "显示更多选项",
|
||||
"mute": "静音",
|
||||
@@ -1288,24 +1400,25 @@
|
||||
"noAudioSignalDialInDesc": "你还可以拨打以下号码加入会议:",
|
||||
"noAudioSignalDialInLinkDesc": "拨打电话号码",
|
||||
"noAudioSignalTitle": "你的麦克风没有声音!",
|
||||
"noiseSuppression": "降噪",
|
||||
"noisyAudioInputDesc": "听起来你的麦克风在发出噪音,请考虑将其静音或更换设备。",
|
||||
"noiseSuppression": "降噪(测试版)",
|
||||
"noisyAudioInputDesc": "检测到你的麦克风有杂音,请考虑静音或更换设备。",
|
||||
"noisyAudioInputTitle": "你的麦克风似乎很嘈杂!",
|
||||
"openChat": "打开聊天",
|
||||
"openReactionsMenu": "打开反应菜单",
|
||||
"openReactionsMenu": "打开互动表情菜单",
|
||||
"participants": "参会者",
|
||||
"pip": "进入画中画模式",
|
||||
"privateMessage": "发送私人消息",
|
||||
"privateMessage": "发送私聊",
|
||||
"profile": "编辑你的个人资料",
|
||||
"raiseHand": "举手",
|
||||
"raiseYourHand": "举手",
|
||||
"reactionBoo": "发送嘘声反应",
|
||||
"reactionClap": "发送鼓掌反应",
|
||||
"reactionLaugh": "发送大笑反应",
|
||||
"reactionLike": "发送竖起大拇指反应",
|
||||
"reactionSilence": "发送沉默反应",
|
||||
"reactionSurprised": "发送惊讶反应",
|
||||
"reactions": "反应",
|
||||
"reactionBoo": "发送嘘声",
|
||||
"reactionClap": "发送鼓掌",
|
||||
"reactionHeart": "发送爱心",
|
||||
"reactionLaugh": "发送大笑",
|
||||
"reactionLike": "发送点赞",
|
||||
"reactionSilence": "发送沉默",
|
||||
"reactionSurprised": "发送惊讶",
|
||||
"reactions": "互动表情",
|
||||
"security": "安全选项",
|
||||
"selectBackground": "选择背景",
|
||||
"shareRoom": "邀请他人",
|
||||
@@ -1333,19 +1446,20 @@
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "开启/关闭字幕",
|
||||
"error": "转录失败,请重试。",
|
||||
"expandedLabel": "转录已开启",
|
||||
"failedToStart": "开启转录失败",
|
||||
"labelToolTip": "会议正在转录中",
|
||||
"off": "转录已停止",
|
||||
"pending": "准备转录会议中……",
|
||||
"failed": "转录失败",
|
||||
"labelTooltip": "本次会议正在进行转录",
|
||||
"labelTooltipExtra": "会后将提供转录文本",
|
||||
"openClosedCaptions": "打开字幕",
|
||||
"original": "原文",
|
||||
"sourceLanguageDesc": "当前会议语言设置为<b>{{sourceLanguage}}</b><br/>你可以在这里",
|
||||
"sourceLanguageHere": "更改",
|
||||
"start": "开启显示字幕",
|
||||
"stop": "停止显示字幕",
|
||||
"subtitles": "字幕",
|
||||
"subtitlesOff": "关",
|
||||
"tr": "转录"
|
||||
"subtitlesOff": "关闭",
|
||||
"tr": "转录",
|
||||
"translateTo": "翻译为"
|
||||
},
|
||||
"unpinParticipant": "{{participantName}} - 取消固定",
|
||||
"userMedia": {
|
||||
@@ -1385,15 +1499,16 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "连接信息",
|
||||
"demote": "设为观众",
|
||||
"domute": "静音",
|
||||
"domuteOthers": "静音所有人",
|
||||
"domuteOthers": "静音其他人",
|
||||
"domuteVideo": "关闭摄像头",
|
||||
"domuteVideoOfOthers": "关闭所有人的摄像头",
|
||||
"flip": "翻转",
|
||||
"domuteVideoOfOthers": "关闭其他人摄像头",
|
||||
"flip": "翻转画面",
|
||||
"grantModerator": "授予主持人权限",
|
||||
"hideSelfView": "隐藏本人视图",
|
||||
"kick": "移除",
|
||||
"mirrorVideo": "镜像我的视频",
|
||||
"kick": "移出会议",
|
||||
"mirrorVideo": "镜像我的画面",
|
||||
"moderator": "主持人",
|
||||
"mute": "参会者已被静音",
|
||||
"muted": "已静音",
|
||||
@@ -1436,12 +1551,23 @@
|
||||
"webAssemblyWarningDescription": "此浏览器禁用或不支持WebAssembly"
|
||||
},
|
||||
"visitors": {
|
||||
"chatIndicator": "(访客)",
|
||||
"labelTooltip": "访客人数:{{count}}",
|
||||
"chatIndicator": "(观众)",
|
||||
"joinMeeting": {
|
||||
"description": "你目前是本次会议的观众",
|
||||
"raiseHand": "举手申请发言",
|
||||
"title": "正在加入会议",
|
||||
"wishToSpeak": "如需发言,请点击下方举手,等待主持人同意"
|
||||
},
|
||||
"labelTooltip": "观众人数:{{count}}",
|
||||
"notification": {
|
||||
"description": "要参与,请举手",
|
||||
"title": "你是会议中的访客"
|
||||
}
|
||||
"demoteDescription": "你已被{{actor}}切换为观众身份,如需参与发言请举手申请",
|
||||
"noMainParticipantsDescription": "暂无参会者开启会议,请稍后再试",
|
||||
"noMainParticipantsTitle": "会议尚未开始",
|
||||
"noVisitorLobby": "当前会议已开启等候室,暂无法加入",
|
||||
"notAllowedPromotion": "需由会议成员同意才能参与讨论",
|
||||
"title": "你当前为会议观众"
|
||||
},
|
||||
"waitingMessage": "会议开始后将自动加入"
|
||||
},
|
||||
"volumeSlider": "音量滑块",
|
||||
"welcomepage": {
|
||||
@@ -1499,6 +1625,7 @@
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "白板"
|
||||
}
|
||||
},
|
||||
"screenTitle": "白板"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "聊天訊息已停用",
|
||||
"enter": "加入聊天室",
|
||||
"error": "錯誤:您的訊息未被傳送。原因:{{error}}",
|
||||
"fieldPlaceHolder": "在此輸入您的訊息",
|
||||
@@ -122,17 +123,26 @@
|
||||
"nickname": {
|
||||
"popover": "選擇名稱",
|
||||
"title": "輸入名稱來使用聊天",
|
||||
"titleWithPolls": "輸入名稱來使用聊天與投票"
|
||||
"titleWithCC": "輸入名稱以使用聊天與即時字幕",
|
||||
"titleWithPolls": "輸入名稱來使用聊天與投票",
|
||||
"titleWithPollsAndCC": "輸入名稱以使用聊天、投票及即時字幕",
|
||||
"titleWithPollsAndCCAndFileSharing": "輸入名稱以使用聊天、投票、即時字幕及檔案分享"
|
||||
},
|
||||
"noMessagesMessage": "此會議尚無訊息,在此開始對話聊天!",
|
||||
"privateNotice": "傳送私人訊息至 {{recipient}}",
|
||||
"sendButton": "傳送",
|
||||
"smileysPanel": "Emoji 面板",
|
||||
"systemDisplayName": "系統",
|
||||
"tabs": {
|
||||
"chat": "聊天",
|
||||
"closedCaptions": "即時字幕",
|
||||
"fileSharing": "檔案",
|
||||
"polls": "投票"
|
||||
},
|
||||
"title": "聊天",
|
||||
"titleWithCC": "即時字幕",
|
||||
"titleWithFeatures": "聊天與",
|
||||
"titleWithFileSharing": "檔案",
|
||||
"titleWithPolls": "聊天與投票",
|
||||
"you": "您"
|
||||
},
|
||||
@@ -143,6 +153,10 @@
|
||||
"dontShowAgain": "不要再問了",
|
||||
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "即時字幕內容將在主持人啟用後顯示",
|
||||
"startClosedCaptionsButton": "啟動即時字幕"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "正在將您連接至您的會議…"
|
||||
},
|
||||
@@ -219,7 +233,9 @@
|
||||
"joinInBrowser": "在瀏覽器中加入",
|
||||
"launchMeetingLabel": "您想如何加入此會議?",
|
||||
"launchWebButton": "在瀏覽器開啟",
|
||||
"noDesktopApp": "您尚未安裝桌面應用程式?",
|
||||
"noMobileApp": "您尚未安裝該應用程式?",
|
||||
"or": "或",
|
||||
"termsAndConditions": "繼續操作即表示您同意我們的<a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>條款與條件。</a>",
|
||||
"title": "正在 {{app}} 開啟您的會議…",
|
||||
"titleNew": "正在開啟您的會議…",
|
||||
@@ -260,7 +276,10 @@
|
||||
"Remove": "移除",
|
||||
"Share": "分享",
|
||||
"Submit": "送出",
|
||||
"Understand": "我了解,暫時保持靜音",
|
||||
"UnderstandAndUnmute": "我了解,請為我解除靜音",
|
||||
"WaitForHostMsg": "此會議尚未開始,如果您是會議主持人,請進行認證並以主持人身分開始會議。",
|
||||
"WaitForHostNoAuthMsg": "此會議尚未開始,目前沒有主持人加入,請稍候。",
|
||||
"WaitingForHostButton": "等待主持人",
|
||||
"WaitingForHostTitle": "正在等候主持人加入…",
|
||||
"Yes": "是",
|
||||
@@ -295,6 +314,7 @@
|
||||
"conferenceReloadMsg": "我們正試著修復狀況,將在 {{seconds}} 秒後重新連接…",
|
||||
"conferenceReloadTitle": "喔哦!好像有東西壞掉囉。",
|
||||
"confirm": "確認",
|
||||
"confirmBack": "返回",
|
||||
"confirmNo": "否",
|
||||
"confirmYes": "是",
|
||||
"connectError": "喔哦!發生錯誤,無法連接至會議。",
|
||||
@@ -303,6 +323,8 @@
|
||||
"contactSupport": "聯絡支援",
|
||||
"copied": "已複製",
|
||||
"copy": "複製",
|
||||
"demoteParticipantDialog": "您確定要將此與會者轉為僅檢視模式嗎?",
|
||||
"demoteParticipantTitle": "切換為檢視者",
|
||||
"dismiss": "取消",
|
||||
"displayNameRequired": "嗨!請問大名?",
|
||||
"done": "完成",
|
||||
@@ -314,6 +336,7 @@
|
||||
"embedMeeting": "嵌入會議",
|
||||
"enterDisplayName": "請在此輸入您自己的名字",
|
||||
"error": "錯誤",
|
||||
"errorRoomCreationRestriction": "您加入速度過快,請稍後再試。",
|
||||
"gracefulShutdown": "服務目前正在維護中,請稍後再試。",
|
||||
"grantModeratorDialog": "您確定要授予 {{participantName}} 主持人權限嗎?",
|
||||
"grantModeratorTitle": "授予主持人權限",
|
||||
@@ -327,7 +350,9 @@
|
||||
"kickParticipantButton": "移除",
|
||||
"kickParticipantDialog": "您確定要將這位與會者移除嗎?",
|
||||
"kickParticipantTitle": "移除這位與會者?",
|
||||
"kickSystemTitle": "您已被移出會議",
|
||||
"kickTitle": "噢!{{participantDisplayName}} 將您從會議中移除",
|
||||
"learnMore": "了解詳情",
|
||||
"linkMeeting": "連結會議",
|
||||
"linkMeetingTitle": "將會議連結至 Salesforce",
|
||||
"liveStreaming": "直播串流中",
|
||||
@@ -385,6 +410,10 @@
|
||||
"recentlyUsedObjects": "您最近使用過的物件",
|
||||
"recording": "錄製中",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "正在直播時無法使用",
|
||||
"recordingInProgressDescription": "本會議正在錄製並由 AI 分析{{learnMore}},您的音訊與影像已靜音。如果您選擇取消靜音,即表示您同意被錄製。",
|
||||
"recordingInProgressDescriptionFirstHalf": "本會議正在錄製並由 AI 分析",
|
||||
"recordingInProgressDescriptionSecondHalf": ",您的音訊與影像已靜音。如果您選擇取消靜音,即表示您同意被錄製。",
|
||||
"recordingInProgressTitle": "正在錄製",
|
||||
"rejoinNow": "立即重新加入",
|
||||
"remoteControlAllowedMessage": "{{user}} 接受您進行遠端控制的請求!",
|
||||
"remoteControlDeniedMessage": "{{user}} 拒絕您進行遠端控制的請求!",
|
||||
@@ -421,6 +450,7 @@
|
||||
"sessTerminatedReason": "會議已經終止",
|
||||
"sessionRestarted": "通話因連線問題重新啟動。",
|
||||
"shareAudio": "繼續",
|
||||
"shareAudioAltText": "如需分享內容,請選擇「瀏覽器分頁」,勾選「分享音訊」後再按「分享」",
|
||||
"shareAudioTitle": "如何分享音訊",
|
||||
"shareAudioWarningD1": "您必須先停用分享螢幕才能分享音訊。",
|
||||
"shareAudioWarningD2": "您必須重新啟動螢幕分享並勾選「分享音訊」選項。",
|
||||
@@ -431,7 +461,10 @@
|
||||
"shareScreenWarningD2": "您必須先停用分享音訊後,重新啟動螢幕分享並勾選「分享音訊」選項。",
|
||||
"shareScreenWarningH1": "如果您只要分享螢幕:",
|
||||
"shareScreenWarningTitle": "您必須先停用分享音訊才能分享螢幕",
|
||||
"shareVideoConfirmPlay": "您即將開啟外部網站,是否繼續?",
|
||||
"shareVideoConfirmPlayTitle": "{{name}} 與您分享了一段影片。",
|
||||
"shareVideoLinkError": "請提供正確的影片網址。",
|
||||
"shareVideoLinkStopped": "{{name}} 的影片已停止播放",
|
||||
"shareVideoTitle": "分享影像",
|
||||
"shareYourScreen": "分享您的螢幕",
|
||||
"shareYourScreenDisabled": "螢幕分享已停用。",
|
||||
@@ -510,6 +543,19 @@
|
||||
"veryBad": "極差",
|
||||
"veryGood": "極好"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "請重試",
|
||||
"downloadFailedTitle": "下載失敗",
|
||||
"downloadFile": "下載",
|
||||
"dragAndDrop": "將檔案拖曳至此或畫面任一處上傳",
|
||||
"fileAlreadyUploaded": "檔案已上傳至此會議",
|
||||
"fileTooLargeDescription": "請確認檔案未超過 {{ maxFileSize }}",
|
||||
"fileTooLargeTitle": "檔案過大",
|
||||
"removeFile": "移除",
|
||||
"uploadFailedDescription": "請重試",
|
||||
"uploadFailedTitle": "上傳失敗",
|
||||
"uploadFile": "分享檔案"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "視頻縮略圖"
|
||||
@@ -557,6 +603,7 @@
|
||||
"noNumbers": "無撥入號碼。",
|
||||
"noPassword": "無",
|
||||
"noRoom": "沒有會議室指定要撥入。",
|
||||
"noWhiteboard": "無法載入白板",
|
||||
"numbers": "撥入號碼",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"reachedLimit": "您已達到您的方案上限",
|
||||
@@ -564,7 +611,8 @@
|
||||
"sipAudioOnly": "SIP 僅音訊位址",
|
||||
"title": "分享",
|
||||
"tooltip": "顯示此會議的連結及電話撥入號碼",
|
||||
"upgradeOptions": "請查看升級選項於"
|
||||
"upgradeOptions": "請查看升級選項於",
|
||||
"whiteboardError": "載入白板時發生錯誤,請稍後再試。"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "好像有點卡卡不順。",
|
||||
@@ -628,6 +676,7 @@
|
||||
"on": "直播串流已啟動",
|
||||
"onBy": "{{name}} 啟動了直播串流",
|
||||
"pending": "啟動直播串流…",
|
||||
"policyError": "您啟動直播的過快,請稍後重試!",
|
||||
"serviceName": "直播串流服務",
|
||||
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
|
||||
"signIn": "使用 Google 帳號登入",
|
||||
@@ -666,13 +715,13 @@
|
||||
"knockingParticipantList": "請求加入的與會者名單",
|
||||
"lobbyChatStartedNotification": "{{moderator}} 與 {{attendee}} 開始在大廳中聊天",
|
||||
"lobbyChatStartedTitle": "{{moderator}} 與您開始在大廳中聊天。",
|
||||
"lobbyClosed": "大廳已關閉",
|
||||
"nameField": "輸入您的名字",
|
||||
"notificationLobbyAccessDenied": "{{originParticipantName}} 拒絕了 {{targetParticipantName}} 的加入請求",
|
||||
"notificationLobbyAccessGranted": "{{originParticipantName}} 同意了 {{targetParticipantName}} 的加入請求",
|
||||
"notificationLobbyDisabled": "{{originParticipantName}} 已停用大廳模式",
|
||||
"notificationLobbyEnabled": "{{originParticipantName}} 已啟用大廳模式",
|
||||
"notificationTitle": "大廳",
|
||||
"passwordField": "輸入會議密碼",
|
||||
"passwordJoinButton": "加入",
|
||||
"title": "大廳",
|
||||
"toggleLabel": "啟用大廳模式"
|
||||
@@ -718,7 +767,9 @@
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"OldElectronAPPTitle": "安全漏洞!",
|
||||
"allowAction": "允許",
|
||||
"allowAudio": "允許音訊",
|
||||
"allowBoth": "允許音訊與視訊",
|
||||
"allowVideo": "允許視訊",
|
||||
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的螢幕。",
|
||||
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
|
||||
"audioUnmuteBlockedTitle": "麥克風解除靜音遭封鎖!",
|
||||
@@ -726,10 +777,14 @@
|
||||
"connectedOneMember": "{{name}} 加入了會議",
|
||||
"connectedThreePlusMembers": "{{name}} 與其他人加入了會議",
|
||||
"connectedTwoMembers": "{{first}} 與{{second}} 加入了會議",
|
||||
"connectionFailed": "連線失敗,請稍後重試!",
|
||||
"dataChannelClosed": "視訊品質受限",
|
||||
"dataChannelClosedDescription": "橋接通道已斷開,視訊品質降至最低設定。",
|
||||
"dataChannelClosedDescriptionWithAudio": "橋接通道已斷開,音訊和視訊可能會受到影響。",
|
||||
"dataChannelClosedWithAudio": "音訊和視訊品質可能會降低。",
|
||||
"disabledIframe": "嵌入僅供示範使用,此通話將於 {{timeout}} 分鐘後中斷連線。",
|
||||
"disabledIframeSecondary": "內嵌 {{domain}} 僅為展示用途,此通話將在 {{timeout}} 分鐘後中斷連線。請使用在正式環境使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a>來內嵌!",
|
||||
"disabledIframeSecondaryNative": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷。",
|
||||
"disabledIframeSecondaryWeb": "嵌入 {{domain}} 僅供示範,此通話將於 {{timeout}} 分鐘後中斷,請使用 <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi 服務</a> 來進行正式嵌入!",
|
||||
"disconnected": "已經中斷連接",
|
||||
"displayNotifications": "顯示通知給",
|
||||
"dontRemindMe": "不要再提醒我",
|
||||
@@ -738,6 +793,9 @@
|
||||
"gifsMenu": "GIPHY",
|
||||
"groupTitle": "通知",
|
||||
"hostAskedUnmute": "主持人希望您發言",
|
||||
"invalidTenant": "無效的租用者名稱",
|
||||
"invalidTenantHyphenDescription": "您使用的租用者名稱無效(開頭或結尾不可為「-」)。",
|
||||
"invalidTenantLengthDescription": "您使用的租用者名稱過長。",
|
||||
"invitedOneMember": "{{name}} 已受邀請",
|
||||
"invitedThreePlusMembers": "{{name}} 與 {{count}} 位人員已受邀請",
|
||||
"invitedTwoMembers": "{{first}} 與 {{second}} 已受邀請",
|
||||
@@ -774,6 +832,7 @@
|
||||
"newDeviceAction": "使用",
|
||||
"newDeviceAudioTitle": "偵測到新的音效裝置",
|
||||
"newDeviceCameraTitle": "偵測到新的網路攝影機",
|
||||
"nextToSpeak": "下一位輪到您發言",
|
||||
"noiseSuppressionDesktopAudioDescription": "分享電腦音訊時無法啟用雜訊抑制,請停用後重試。",
|
||||
"noiseSuppressionFailedTitle": "啟用雜訊抑制失敗",
|
||||
"noiseSuppressionStereoDescription": "目前不支援立體聲降噪功能。",
|
||||
@@ -799,13 +858,21 @@
|
||||
"startSilentTitle": "您以無音訊輸出方式加入了會議!",
|
||||
"suboptimalBrowserWarning": "我們恐怕您本次會議體驗不佳,我們會努力改善。在此期間,請嘗試使用<a href='{{recommendedBrowserPageLink}}' target='_blank'>支援的瀏覽器</a> 。",
|
||||
"suboptimalExperienceTitle": "瀏覽器警告",
|
||||
"suggestRecordingAction": "開始",
|
||||
"suggestRecordingDescription": "是否要開始錄製這場會議?",
|
||||
"suggestRecordingTitle": "錄製此會議",
|
||||
"unmute": "取消靜音",
|
||||
"unmuteVideo": "啟用視訊",
|
||||
"videoMutedRemotelyDescription": "您隨時可以再次啟用。",
|
||||
"videoMutedRemotelyTitle": "您的視訊已被 {{participantDisplayName}} 停用",
|
||||
"videoUnmuteBlockedDescription": "啟用網路攝影機與分享螢幕由於系統限制而被暫時封鎖。",
|
||||
"videoUnmuteBlockedTitle": "啟用網路攝影機與分享螢幕遭封鎖!",
|
||||
"viewLobby": "檢視大廳",
|
||||
"viewParticipants": "查看與會者",
|
||||
"viewVisitors": "查看訪客",
|
||||
"waitingParticipants": "{{waitingParticipants}} 人",
|
||||
"waitingVisitors": "排隊中的訪客: {{waitingVisitors}} 人",
|
||||
"waitingVisitorsTitle": "會議尚未開始!",
|
||||
"whiteboardLimitDescription": "由於即將超出使用者限制,白板將關閉,請儲存您的進度。",
|
||||
"whiteboardLimitTitle": "白板使用情況"
|
||||
},
|
||||
@@ -819,7 +886,10 @@
|
||||
"audioModeration": "自我解除靜音",
|
||||
"blockEveryoneMicCamera": "停用所有人的麥克風和網路攝影機",
|
||||
"breakoutRooms": "分組討論室",
|
||||
"goLive": "開始直播",
|
||||
"invite": "邀請他人",
|
||||
"lowerAllHands": "全部取消舉手",
|
||||
"lowerHand": "取消舉手",
|
||||
"moreModerationActions": "更多主持人選項",
|
||||
"moreModerationControls": "更多主持人操作",
|
||||
"moreParticipantOptions": "更多與會者選項",
|
||||
@@ -836,10 +906,13 @@
|
||||
"headings": {
|
||||
"lobby": "大廳({{count}} 人)",
|
||||
"participantsList": "會議與會者({{count}} 人)",
|
||||
"visitorInQueue": "({{count}} 人等候中)",
|
||||
"visitorRequests": "{{count}} 人申請",
|
||||
"visitors": "訪客({{count}} 人)",
|
||||
"waitingLobby": "於大廳等候({{count}} 人)"
|
||||
},
|
||||
"search": "搜尋與會者",
|
||||
"searchDescription": "輸入關鍵字篩選與會者",
|
||||
"title": "與會者"
|
||||
},
|
||||
"passwordDigitsOnly": "上限為 {{number}} 位數",
|
||||
@@ -848,10 +921,13 @@
|
||||
"pinnedParticipant": "與會者被釘選",
|
||||
"polls": {
|
||||
"answer": {
|
||||
"edit": "編輯",
|
||||
"send": "送出",
|
||||
"skip": "跳過",
|
||||
"submit": "送出"
|
||||
},
|
||||
"by": "由 {{ name }}",
|
||||
"closeButton": "結束投票",
|
||||
"create": {
|
||||
"addOption": "新增選項",
|
||||
"answerPlaceholder": "選項 {{index}}",
|
||||
@@ -861,6 +937,7 @@
|
||||
"pollQuestion": "投票問題",
|
||||
"questionPlaceholder": "詢問問題",
|
||||
"removeOption": "移除選項",
|
||||
"save": "儲存",
|
||||
"send": "傳送"
|
||||
},
|
||||
"errors": {
|
||||
@@ -890,9 +967,11 @@
|
||||
"configuringDevices": "設定裝置中…",
|
||||
"connectedWithAudioQ": "您有連接音訊設備嗎?",
|
||||
"connection": {
|
||||
"good": "您的連線品質良好",
|
||||
"failed": "連線測試失敗!",
|
||||
"good": "您的連線品質良好!",
|
||||
"nonOptimal": "您的連線品質不理想",
|
||||
"poor": "您的連線品質不佳"
|
||||
"poor": "您的連線品質不佳",
|
||||
"running": "正在測試連線中…"
|
||||
},
|
||||
"connectionDetails": {
|
||||
"audioClipping": "您的音訊將會斷斷續續。",
|
||||
@@ -901,6 +980,7 @@
|
||||
"goodQuality": "太好了!您的媒體品質良好。",
|
||||
"noMediaConnectivity": "我們無法為此測試建立媒體連線,通常是防火牆或 NAT 的問題。",
|
||||
"noVideo": "您的視訊畫質將會很糟糕。",
|
||||
"testFailed": "連線測試遇到異常,但不一定會影響會議。",
|
||||
"undetectable": "如果您仍無法在瀏覽器中進行通話,我們建議您檢查喇叭、麥克風、及網路攝影機的設置,確認是否允許瀏覽器存取麥克風及網路攝影機,並將瀏覽器更新到最新版本。如果以上步驟無法解決問題,請聯絡網頁程式的開發者。",
|
||||
"veryPoorConnection": "您的通話品質將會非常糟糕。",
|
||||
"videoFreezing": "您的視訊將會突然黑頻、卡住、或像素化。",
|
||||
@@ -933,6 +1013,7 @@
|
||||
"or": "或",
|
||||
"premeeting": "會議前",
|
||||
"proceedAnyway": "仍然繼續",
|
||||
"recordingWarning": "其他與會者可能正在錄製此通話",
|
||||
"screenSharingError": "螢幕分享錯誤:",
|
||||
"startWithPhone": "使用手機音訊開始",
|
||||
"unsafeRoomConsent": "我了解風險,我想要加入會議",
|
||||
@@ -998,7 +1079,6 @@
|
||||
"limitNotificationDescriptionNative": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <3>{{app}}</3>。",
|
||||
"limitNotificationDescriptionWeb": "由於目前流量過大,您的錄製時間被限制在 {{limit}} 分鐘。若要無限制的錄製,請試試 <a href={{url}}rel='noopener noreferrer' target='_blank'>{{app}}</a>。",
|
||||
"linkGenerated": "我們建立了您的錄製檔案的連結。",
|
||||
"live": "直播",
|
||||
"localRecordingNoNotificationWarning": "系統不會主動知會與會者錄製已開啟,主持人需另行通知。",
|
||||
"localRecordingNoVideo": "沒有錄製的視訊",
|
||||
"localRecordingStartWarning": "請確保在退出會議之前停用錄製以便保存。",
|
||||
@@ -1015,13 +1095,16 @@
|
||||
"onBy": "{{name}} 開始了錄製",
|
||||
"onlyRecordSelf": "僅錄製我的音訊和影片串流",
|
||||
"pending": "正在準備錄製會議…",
|
||||
"rec": "錄製中",
|
||||
"policyError": "您啟動錄製的速度過快,請稍後再試!",
|
||||
"recordAudioAndVideo": "錄製音訊和視訊",
|
||||
"recordTranscription": "錄製字幕內容",
|
||||
"saveLocalRecording": "將錄製檔案保存在本機(測試版)",
|
||||
"serviceDescription": "您的錄製會由錄製服務儲存",
|
||||
"serviceDescriptionCloud": "雲端錄製",
|
||||
"serviceDescriptionCloudInfo": "已錄製的會議將在 24 小時後自動清除。",
|
||||
"serviceName": "錄製服務",
|
||||
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
|
||||
"showAdvancedOptions": "進階選項",
|
||||
"signIn": "登入",
|
||||
"signOut": "登出",
|
||||
"surfaceError": "請選擇目前分頁",
|
||||
@@ -1056,11 +1139,13 @@
|
||||
"signedIn": "目前正在存取 {{email}} 的行事曆事件,點按下方中斷連接可以停用存取行事曆事件。",
|
||||
"title": "行事曆"
|
||||
},
|
||||
"chatWithPermissions": "聊天功能需取得權限",
|
||||
"desktopShareFramerate": "桌面螢幕分享影格率",
|
||||
"desktopShareHighFpsWarning": "較高的桌面螢幕分享影格率可能會影響您的頻寬,您必須重新啟動桌面螢幕分享以套用新的設定。",
|
||||
"desktopShareWarning": "您必須重新啟動桌面螢幕分享以套用新的設定。",
|
||||
"devices": "裝置",
|
||||
"followMe": "全部人跟隨我",
|
||||
"followMeRecorder": "錄影將跟隨我的視角",
|
||||
"framesPerSecond": "fps",
|
||||
"incomingMessage": "新訊息",
|
||||
"language": "語言",
|
||||
@@ -1084,6 +1169,7 @@
|
||||
"selectMic": "麥克風",
|
||||
"selfView": "自身螢幕",
|
||||
"shortcuts": "快捷鍵",
|
||||
"showSubtitlesOnStage": "在主畫面顯示字幕",
|
||||
"speakers": "喇叭",
|
||||
"startAudioMuted": "所有人啟動時處於靜音",
|
||||
"startReactionsMuted": "關閉所有人反應音效",
|
||||
@@ -1137,11 +1223,13 @@
|
||||
"fearful": "可怕",
|
||||
"happy": "笑臉",
|
||||
"hours": "{{count}} 小時",
|
||||
"labelTooltip": "與會者人數:{{count}}",
|
||||
"minutes": "{{count}} 分",
|
||||
"name": "名字",
|
||||
"neutral": "中立",
|
||||
"sad": "悲傷",
|
||||
"search": "搜尋",
|
||||
"searchDescription": "輸入關鍵字篩選與會者",
|
||||
"searchHint": "搜尋與會者",
|
||||
"seconds": "{{count}} 秒",
|
||||
"speakerStats": "發言統計",
|
||||
@@ -1169,7 +1257,7 @@
|
||||
"audioOnly": "切換僅音訊",
|
||||
"audioRoute": "選擇音訊裝置",
|
||||
"boo": "倒喝彩",
|
||||
"breakoutRoom": "進入/離開分組討論室",
|
||||
"breakoutRooms": "分組討論室",
|
||||
"callQuality": "管理視訊品質",
|
||||
"carmode": "行車模式",
|
||||
"cc": "切換字幕",
|
||||
@@ -1178,6 +1266,7 @@
|
||||
"closeChat": "關閉聊天",
|
||||
"closeMoreActions": "關閉更多操作選單",
|
||||
"closeParticipantsPane": "關閉與會者窗格",
|
||||
"closedCaptions": "停用即時字幕",
|
||||
"collapse": "收回",
|
||||
"document": "切換檔案分享",
|
||||
"documentClose": "關閉檔案分享",
|
||||
@@ -1207,6 +1296,7 @@
|
||||
"lobbyButton": "啟用/停用大廳模式",
|
||||
"localRecording": "切換本機錄製控制",
|
||||
"lockRoom": "切換會議密碼",
|
||||
"love": "愛心",
|
||||
"lowerHand": "放下手",
|
||||
"moreActions": "更多動作",
|
||||
"moreActionsMenu": "更多動作選單",
|
||||
@@ -1217,13 +1307,14 @@
|
||||
"muteEveryoneElsesVideoStream": "停止其他人的視訊",
|
||||
"muteEveryonesVideoStream": "停止所有人的視訊",
|
||||
"muteGUMPending": "正在連接您的麥克風",
|
||||
"noiseSuppression": "雜訊抑制",
|
||||
"noiseSuppression": "雜訊抑制(BETA)",
|
||||
"openChat": "打開聊天",
|
||||
"participants": "打開與會者窗格",
|
||||
"pip": "切換子母螢幕模式",
|
||||
"privateMessage": "傳送私人訊息",
|
||||
"profile": "編輯您的個人檔案",
|
||||
"raiseHand": "舉手",
|
||||
"react": "訊息反應",
|
||||
"reactions": "反應",
|
||||
"reactionsMenu": "反應選單",
|
||||
"recording": "切換錄製",
|
||||
@@ -1266,14 +1357,15 @@
|
||||
"closeChat": "關閉聊天",
|
||||
"closeParticipantsPane": "關閉與會者窗格",
|
||||
"closeReactionsMenu": "關閉反應選單",
|
||||
"disableNoiseSuppression": "停用雜訊抑制",
|
||||
"closedCaptions": "停用即時字幕",
|
||||
"disableNoiseSuppression": "停用雜訊抑制(BETA)",
|
||||
"disableReactionSounds": "您可以停用此會議的反應音效",
|
||||
"documentClose": "關閉分享檔案欄",
|
||||
"documentOpen": "開啟分享檔案欄",
|
||||
"download": "下載我們的應用程式",
|
||||
"e2ee": "端對端加密",
|
||||
"embedMeeting": "嵌入會議",
|
||||
"enableNoiseSuppression": "開啟雜訊抑制",
|
||||
"enableNoiseSuppression": "開啟雜訊抑制(BETA)",
|
||||
"endConference": "結束會議(所有人)",
|
||||
"enterFullScreen": "放大全螢幕",
|
||||
"enterTileView": "進入畫廊檢視",
|
||||
@@ -1295,6 +1387,7 @@
|
||||
"lobbyButtonEnable": "啟用大廳模式",
|
||||
"login": "登入",
|
||||
"logout": "登出",
|
||||
"love": "愛心",
|
||||
"lowerYourHand": "放下您的手",
|
||||
"moreActions": "更多動作",
|
||||
"moreOptions": "更多選項",
|
||||
@@ -1307,7 +1400,7 @@
|
||||
"noAudioSignalDialInDesc": "您亦可使用下述方式撥入:",
|
||||
"noAudioSignalDialInLinkDesc": "撥入號碼",
|
||||
"noAudioSignalTitle": "您的麥克風沒有訊號!",
|
||||
"noiseSuppression": "雜訊抑制",
|
||||
"noiseSuppression": "雜訊抑制(BETA)",
|
||||
"noisyAudioInputDesc": "噪音聽起來是從您的麥克風傳來的,請考慮靜音或更換裝置。",
|
||||
"noisyAudioInputTitle": "您的麥克風疑似有雜音!",
|
||||
"openChat": "開啟聊天",
|
||||
@@ -1320,6 +1413,7 @@
|
||||
"raiseYourHand": "舉手",
|
||||
"reactionBoo": "傳送倒喝彩反應",
|
||||
"reactionClap": "傳送鼓掌反應",
|
||||
"reactionHeart": "傳送愛心反應",
|
||||
"reactionLaugh": "傳送大笑反應",
|
||||
"reactionLike": "傳送比讚反應",
|
||||
"reactionSilence": "傳送沉默反應",
|
||||
@@ -1352,21 +1446,22 @@
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "啟動/停用字幕",
|
||||
"error": "轉錄失敗,請再試一次。",
|
||||
"expandedLabel": "轉錄已開啟",
|
||||
"failedToStart": "轉錄啟動失敗",
|
||||
"labelToolTip": "此會議正在轉錄",
|
||||
"off": "轉錄已停用",
|
||||
"pending": "準備轉錄會議…",
|
||||
"failed": "轉錄失敗",
|
||||
"labelTooltip": "此會議正在轉錄",
|
||||
"labelTooltipExtra": "稍後將提供完整轉錄紀錄",
|
||||
"openClosedCaptions": "開啟字幕",
|
||||
"original": "原文",
|
||||
"sourceLanguageDesc": "會議語言目前設定為 <b>{{sourceLanguage}}</b><br/> 您可以在這裡",
|
||||
"sourceLanguageHere": "修改",
|
||||
"start": "開始顯示字幕",
|
||||
"stop": "停用顯示字幕",
|
||||
"subtitles": "字幕",
|
||||
"subtitlesOff": "關",
|
||||
"tr": "轉錄"
|
||||
"tr": "轉錄",
|
||||
"translateTo": "翻譯成"
|
||||
},
|
||||
"unpinParticipant": "",
|
||||
"unpinParticipant": "{{participantName}} - 取消釘選",
|
||||
"userMedia": {
|
||||
"grantPermissions": "請允許使用您的網路攝影機和麥克風的權限."
|
||||
},
|
||||
@@ -1404,6 +1499,7 @@
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "連線資訊",
|
||||
"demote": "轉為訪客",
|
||||
"domute": "靜音",
|
||||
"domuteOthers": "靜音其他人",
|
||||
"domuteVideo": "停用網路攝影機",
|
||||
@@ -1456,11 +1552,22 @@
|
||||
},
|
||||
"visitors": {
|
||||
"chatIndicator": "(訪客)",
|
||||
"joinMeeting": {
|
||||
"description": "您目前以訪客身分參與本會議",
|
||||
"raiseHand": "舉手",
|
||||
"title": "正在加入會議",
|
||||
"wishToSpeak": "若您想發言,請先舉手並等候主持人同意"
|
||||
},
|
||||
"labelTooltip": "訪客數量:{{count}}",
|
||||
"notification": {
|
||||
"description": "若要參與,請舉手",
|
||||
"demoteDescription": "由 {{actor}} 調整為訪客,若要參與請舉手",
|
||||
"noMainParticipantsDescription": "須有主要與會者啟動會議,請稍後再試",
|
||||
"noMainParticipantsTitle": "會議尚未開始",
|
||||
"noVisitorLobby": "此會議啟用大廳,暫時無法加入",
|
||||
"notAllowedPromotion": "需由與會者同意您的申請",
|
||||
"title": "您是會議中的訪客"
|
||||
}
|
||||
},
|
||||
"waitingMessage": "會議開始後您將自動加入!"
|
||||
},
|
||||
"volumeSlider": "音量滑桿",
|
||||
"welcomepage": {
|
||||
@@ -1518,6 +1625,7 @@
|
||||
"whiteboard": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "白板"
|
||||
}
|
||||
},
|
||||
"screenTitle": "白板"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"disabled": "Sending chat messages is disabled.",
|
||||
"enter": "Enter room",
|
||||
"error": "Error: your message was not sent. Reason: {{error}}",
|
||||
"fieldPlaceHolder": "Aa",
|
||||
@@ -149,7 +150,7 @@
|
||||
"buttonText": "Install Chrome Extension",
|
||||
"buttonTextEdge": "Install Edge Extension",
|
||||
"close": "Close",
|
||||
"dontShowAgain": "Don’t show me this again",
|
||||
"dontShowAgain": "Don't show me this again",
|
||||
"installExtensionText": "Install the extension for Google Calendar and Office 365 integration"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
@@ -232,8 +233,8 @@
|
||||
"joinInBrowser": "Join in browser",
|
||||
"launchMeetingLabel": "How do you want to join this meeting?",
|
||||
"launchWebButton": "Launch in web",
|
||||
"noDesktopApp": "You don’t have the app?",
|
||||
"noMobileApp": "You don’t have the app?",
|
||||
"noDesktopApp": "You don't have the app?",
|
||||
"noMobileApp": "You don't have the app?",
|
||||
"or": "OR",
|
||||
"termsAndConditions": "By continuing you agree to our <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>terms & conditions.</a>",
|
||||
"title": "Launching your meeting in {{app}}…",
|
||||
@@ -325,7 +326,7 @@
|
||||
"demoteParticipantDialog": "Are you sure you want to move this participant to viewer?",
|
||||
"demoteParticipantTitle": "Move to viewer",
|
||||
"dismiss": "Dismiss",
|
||||
"displayNameRequired": "Hi! What’s your name?",
|
||||
"displayNameRequired": "Hi! What's your name?",
|
||||
"done": "Done",
|
||||
"e2eeDescription": "End-to-End Encryption is currently EXPERIMENTAL. Please keep in mind that turning on end-to-end encryption will effectively disable server-side provided services such as: phone participation. Also keep in mind that the meeting will only work for people joining from browsers with support for insertable streams.",
|
||||
"e2eeDisabledDueToMaxModeDescription": "Cannot enable End-to-End Encryption due to large number of participants in the conference.",
|
||||
@@ -431,7 +432,7 @@
|
||||
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
|
||||
"retry": "Retry",
|
||||
"screenSharingAudio": "Share audio",
|
||||
"screenSharingFailed": "Oops! Something went wrong, we weren’t able to start screen sharing!",
|
||||
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
|
||||
"screenSharingFailedTitle": "Screen sharing failed!",
|
||||
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing permissions. Please reload and try again.",
|
||||
"searchInSalesforce": "Search in Salesforce",
|
||||
@@ -460,7 +461,7 @@
|
||||
"shareScreenWarningD2": "you need to stop audio sharing, start screen sharing and check the \"share audio\" option.",
|
||||
"shareScreenWarningH1": "If you want to share just your screen:",
|
||||
"shareScreenWarningTitle": "You need to stop audio sharing before sharing your screen",
|
||||
"shareVideoConfirmPlay": "You’re about to open an external website. Do you want to continue?",
|
||||
"shareVideoConfirmPlay": "You're about to open an external website. Do you want to continue?",
|
||||
"shareVideoConfirmPlayTitle": "{{name}} has shared a video with you.",
|
||||
"shareVideoLinkError": "Oops, this video cannot be played.",
|
||||
"shareVideoLinkStopped": "The video from {{name}} was stopped.",
|
||||
@@ -664,7 +665,7 @@
|
||||
"expandedOn": "The meeting is currently being live streamed",
|
||||
"expandedPending": "The live streaming is being started…",
|
||||
"failedToStart": "Live Streaming failed to start",
|
||||
"getStreamKeyManually": "We weren’t able to fetch any live streams. Try getting your live stream key from YouTube.",
|
||||
"getStreamKeyManually": "We weren't able to fetch any live streams. Try getting your live stream key from YouTube.",
|
||||
"googlePrivacyPolicy": "Google Privacy Policy",
|
||||
"inProgress": "Recording or live streaming in progress",
|
||||
"invalidStreamKey": "Live stream key may be incorrect.",
|
||||
@@ -905,9 +906,11 @@
|
||||
"headings": {
|
||||
"lobby": "Lobby ({{count}})",
|
||||
"participantsList": "Meeting participants ({{count}})",
|
||||
"viewerRequests": "Viewers requests {{count}}",
|
||||
"visitorInQueue": " (waiting {{count}})",
|
||||
"visitorRequests": " (requests {{count}})",
|
||||
"visitors": "Viewers {{count}}",
|
||||
"visitorsList": "Viewers ({{count}})",
|
||||
"waitingLobby": "Waiting in lobby ({{count}})"
|
||||
},
|
||||
"search": "Search participants",
|
||||
@@ -948,7 +951,7 @@
|
||||
},
|
||||
"results": {
|
||||
"changeVote": "Change vote",
|
||||
"empty": "There are no polls in the meeting yet. Start a poll here!",
|
||||
"empty": "There are no polls in the meeting yet.",
|
||||
"hideDetailedResults": "Hide details",
|
||||
"showDetailedResults": "Show details",
|
||||
"vote": "Vote"
|
||||
@@ -964,7 +967,7 @@
|
||||
"callMeAtNumber": "Call me at this number:",
|
||||
"calling": "Calling",
|
||||
"configuringDevices": "Configuring devices…",
|
||||
"connectedWithAudioQ": "You’re connected with audio?",
|
||||
"connectedWithAudioQ": "You're connected with audio?",
|
||||
"connection": {
|
||||
"failed": "Connection test failed!",
|
||||
"good": "Your internet connection looks good!",
|
||||
@@ -1561,7 +1564,7 @@
|
||||
"notification": {
|
||||
"demoteDescription": "Sent here by {{actor}}, raise your hand to participate",
|
||||
"noMainParticipantsDescription": "A participant needs to start the meeting. Please try again in a bit.",
|
||||
"noMainParticipantsTitle": "This meeting hasn’t started yet.",
|
||||
"noMainParticipantsTitle": "This meeting hasn't started yet.",
|
||||
"noVisitorLobby": "You cannot join while there is a lobby enabled for the meeting.",
|
||||
"notAllowedPromotion": "A participant needs to allow your request first.",
|
||||
"title": "You are a viewer in the meeting"
|
||||
|
||||
518
package-lock.json
generated
518
package-lock.json
generated
@@ -10,7 +10,8 @@
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@amplitude/react-native": "2.17.3",
|
||||
"@amplitude/analytics-browser": "2.17.12",
|
||||
"@amplitude/analytics-react-native": "1.4.13",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@emotion/react": "11.10.6",
|
||||
"@emotion/styled": "11.10.6",
|
||||
@@ -62,7 +63,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/v2012.0.0+86b76227/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -104,6 +105,7 @@
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
@@ -246,21 +248,175 @@
|
||||
"extraneous": true,
|
||||
"devDependencies": {}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
|
||||
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
|
||||
},
|
||||
"node_modules/@amplitude/react-native": {
|
||||
"version": "2.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/react-native/-/react-native-2.17.3.tgz",
|
||||
"integrity": "sha512-UjXjbt/rPotjJ5hHZjYI8q9TQ/LpcLjWGtOsswHVL4+Vh7FlMzvDSum6QDdXW00rBP0ibCMQBcng1lLG4dh/Ug==",
|
||||
"node_modules/@amplitude/analytics-browser": {
|
||||
"version": "2.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.12.tgz",
|
||||
"integrity": "sha512-oADGSPJWcfnNyBqN7qDgEOEVrLxSb9Qh4wINi7DsRS8Mj55Ukh9M1iqQoLXrCXK/lbV4QMhUWn1ZmBAMCKYZKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"@amplitude/analytics-remote-config": "^0.4.0",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.4.0",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.2.0",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.32",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-browser/node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-client-common": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-1.2.5.tgz",
|
||||
"integrity": "sha512-W881IHihCmCUymhNY8tLEj58AsKiYjDszORknD0Q3FrKw5CZlEQbXFVqFpDzfUnUb9jtg2zo4JCsneZe1A3oqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.5.0",
|
||||
"@amplitude/analytics-core": "^1.2.7",
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-connector": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz",
|
||||
"integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/analytics-core": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.7.tgz",
|
||||
"integrity": "sha512-SM9jdQ+l2q+hy+DdCQm5vtfOTiI+53c+alSSc7fwiuFnTExllXHf9RUK6kKhw3ky+N2o6yqbo+0OGoepLhNf6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-react-native": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-react-native/-/analytics-react-native-1.4.13.tgz",
|
||||
"integrity": "sha512-znl7IkazV0j++R+F6GEzWNaVbCBipwKNBbIrhsMpaw0vMhdgiS0Jr36HiKUrX2Md6LvqRZ5JhM4KbMzRFD5ujQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": "^1.2.5",
|
||||
"@amplitude/analytics-core": "^1.2.7",
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"@amplitude/ua-parser-js": "^0.7.31",
|
||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-remote-config": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-remote-config/-/analytics-remote-config-0.4.1.tgz",
|
||||
"integrity": "sha512-BYl6kQ9qjztrCACsugpxO+foLaQIC0aSEzoXEAb/gwOzInmqkyyI+Ub+aWTBih4xgB/lhWlOcidWHAmNiTJTNw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": ">=1 <3",
|
||||
"@amplitude/analytics-core": ">=1 <3",
|
||||
"@amplitude/analytics-types": ">=1 <3",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-types": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.3.5.tgz",
|
||||
"integrity": "sha512-IpncCNTZZ6VoGe4fNwTTZtpi+ZNm3mtsocdbCHtIwmKg2wmOF2E09CAwvyF7mK5aRlMIrSAKQyR3GwraATghSw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/plugin-autocapture-browser": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.4.0.tgz",
|
||||
"integrity": "sha512-vvqfCflFS8vCkMAmMxUP59h/g4hImsYKUMqevmT1dGbM9bq7eccRTP8JsTpoTomG70VLCUfqitQTMfXuWf9GVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-autocapture-browser/node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-network-capture-browser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.2.0.tgz",
|
||||
"integrity": "sha512-exFqJ3MWCs1d00Zj+XfwP0eXD8R4jG8Zd+bp3pUguGOVa9+x4UTbYWul2S9lt40Arp33DFijobl+ocDSRBCIFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-network-capture-browser/node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
|
||||
"version": "2.3.32",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.32.tgz",
|
||||
"integrity": "sha512-Y+6OUv0De1hI4GSdxlWFSsTvjKGsfWM2AP2+elLsPQuZxgBUXjmfkPwRLh4TLMmtbQPfX38vbddBVYWPSStRFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": "^2.3.26",
|
||||
"@amplitude/analytics-types": "^2.9.2",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser/node_modules/@amplitude/analytics-client-common": {
|
||||
"version": "2.3.26",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.26.tgz",
|
||||
"integrity": "sha512-YVMZkugw01fsok2i3x/HhAYG8t9kYxnmNDl6oKbvZkzCzfRUuOKa+f0OLUD2SRJRxTKRanAwHINZuDdk3cR7qQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.4.8",
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"@amplitude/analytics-types": "^2.9.2",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser/node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser/node_modules/@amplitude/analytics-types": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.2.tgz",
|
||||
"integrity": "sha512-juhTz396dDP/jLJYP9zDOEAZBtJM0JVvP8G10p1OxUDBVwVIprpQL598F9GRQwVFyqV4CEhDmNyAY0HqqU5bhA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/types": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-1.10.2.tgz",
|
||||
@@ -3471,13 +3627,12 @@
|
||||
"integrity": "sha512-W9G6crS2oqTn7g0RpvYu1l/sna4LnivRTk25jmxdzujOFb9kvQ+VFM/v9RPYV2GIBnzT/maW/EwjFIba9jkflA=="
|
||||
},
|
||||
"node_modules/@giphy/js-util": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.4.2.tgz",
|
||||
"integrity": "sha512-ltthBrFDGXeDTdtuHM+Y8x1FMX7WaT3sCZ8MC/I179+1DJg82/IvDks9yR0mDywYtHvkNxgg5IqC5arvnA8V/w==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-5.2.0.tgz",
|
||||
"integrity": "sha512-Qt7pGh2cqiNmXLeWAgb459wK8+BuMLtIxTfg4ZksnPHPsLthiHT9hhzs2QhqUh7Pp/HOq+Cbv2etGDfnq+xiKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@giphy/js-types": "*",
|
||||
"dompurify": "^2.2.2",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
@@ -4885,6 +5040,7 @@
|
||||
"version": "1.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-1.23.1.tgz",
|
||||
"integrity": "sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"merge-options": "^3.0.4"
|
||||
},
|
||||
@@ -7958,9 +8114,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10232,9 +10388,10 @@
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -11937,12 +12094,6 @@
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
|
||||
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)"
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
@@ -12188,10 +12339,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -14244,10 +14396,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/filelist/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -15322,10 +15475,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
"http-proxy": "^1.18.1",
|
||||
@@ -17085,10 +17239,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/js-beautify/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -17594,11 +17749,11 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2012.0.0+86b76227/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-xRA4aLoqOSTasNnf8QmiACU0Q+Oo9Yi3ItSNN2bcmaiC+6lOcJP2MHTHcbLcruLvab9CK0jeo7nLbyhIx9tdAQ==",
|
||||
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-21uWTA9II38ldYZGCxBC2kqFsMrAGUGZxUQKcugmncfZ7SCQbWGUMopvcZC0RhSdeL6Mm+RAafc/HcX369egQA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
"@jitsi/precall-test": "1.0.6",
|
||||
"@jitsi/rtcstats": "9.7.0",
|
||||
@@ -17615,6 +17770,17 @@
|
||||
"webrtc-adapter": "8.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lib-jitsi-meet/node_modules/@jitsi/js-utils": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.4.6.tgz",
|
||||
"integrity": "sha512-z/VbM9c0V35T8Zkhxq2gdWbMWmM/3w4BD68xJVmQNrq/NQHxH0fDkRoT/MUds9Mp6dK3AV/h15tCKxVA/0w8Kg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@hapi/bourne": "^3.0.0",
|
||||
"js-md5": "0.7.3",
|
||||
"ua-parser-js": "1.0.35"
|
||||
}
|
||||
},
|
||||
"node_modules/lib-jitsi-meet/node_modules/@jitsi/rtcstats": {
|
||||
"version": "9.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.7.0.tgz",
|
||||
@@ -17632,6 +17798,12 @@
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lib-jitsi-meet/node_modules/js-md5": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
@@ -19001,10 +19173,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mocha/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -22126,6 +22299,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
@@ -22797,7 +22980,6 @@
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
@@ -26458,15 +26640,162 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.5.0.tgz",
|
||||
"integrity": "sha512-T8mOYzB9RRxckzhL0NTHwdge9xuFxXEOplC8B1Y3UX3NHa3BLh7DlBUZlCOwQgMc2nxDfnSweDL5S3bhC+W90g=="
|
||||
"@amplitude/analytics-browser": {
|
||||
"version": "2.17.12",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.12.tgz",
|
||||
"integrity": "sha512-oADGSPJWcfnNyBqN7qDgEOEVrLxSb9Qh4wINi7DsRS8Mj55Ukh9M1iqQoLXrCXK/lbV4QMhUWn1ZmBAMCKYZKQ==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"@amplitude/analytics-remote-config": "^0.4.0",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.4.0",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.2.0",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.32",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@amplitude/react-native": {
|
||||
"version": "2.17.3",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/react-native/-/react-native-2.17.3.tgz",
|
||||
"integrity": "sha512-UjXjbt/rPotjJ5hHZjYI8q9TQ/LpcLjWGtOsswHVL4+Vh7FlMzvDSum6QDdXW00rBP0ibCMQBcng1lLG4dh/Ug=="
|
||||
"@amplitude/analytics-client-common": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-1.2.5.tgz",
|
||||
"integrity": "sha512-W881IHihCmCUymhNY8tLEj58AsKiYjDszORknD0Q3FrKw5CZlEQbXFVqFpDzfUnUb9jtg2zo4JCsneZe1A3oqw==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.5.0",
|
||||
"@amplitude/analytics-core": "^1.2.7",
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-connector": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz",
|
||||
"integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q=="
|
||||
},
|
||||
"@amplitude/analytics-core": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.7.tgz",
|
||||
"integrity": "sha512-SM9jdQ+l2q+hy+DdCQm5vtfOTiI+53c+alSSc7fwiuFnTExllXHf9RUK6kKhw3ky+N2o6yqbo+0OGoepLhNf6w==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-react-native": {
|
||||
"version": "1.4.13",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-react-native/-/analytics-react-native-1.4.13.tgz",
|
||||
"integrity": "sha512-znl7IkazV0j++R+F6GEzWNaVbCBipwKNBbIrhsMpaw0vMhdgiS0Jr36HiKUrX2Md6LvqRZ5JhM4KbMzRFD5ujQ==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-client-common": "^1.2.5",
|
||||
"@amplitude/analytics-core": "^1.2.7",
|
||||
"@amplitude/analytics-types": "^1.3.5",
|
||||
"@amplitude/ua-parser-js": "^0.7.31",
|
||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-remote-config": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-remote-config/-/analytics-remote-config-0.4.1.tgz",
|
||||
"integrity": "sha512-BYl6kQ9qjztrCACsugpxO+foLaQIC0aSEzoXEAb/gwOzInmqkyyI+Ub+aWTBih4xgB/lhWlOcidWHAmNiTJTNw==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-client-common": ">=1 <3",
|
||||
"@amplitude/analytics-core": ">=1 <3",
|
||||
"@amplitude/analytics-types": ">=1 <3",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-types": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.3.5.tgz",
|
||||
"integrity": "sha512-IpncCNTZZ6VoGe4fNwTTZtpi+ZNm3mtsocdbCHtIwmKg2wmOF2E09CAwvyF7mK5aRlMIrSAKQyR3GwraATghSw=="
|
||||
},
|
||||
"@amplitude/plugin-autocapture-browser": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.4.0.tgz",
|
||||
"integrity": "sha512-vvqfCflFS8vCkMAmMxUP59h/g4hImsYKUMqevmT1dGbM9bq7eccRTP8JsTpoTomG70VLCUfqitQTMfXuWf9GVQ==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@amplitude/plugin-network-capture-browser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.2.0.tgz",
|
||||
"integrity": "sha512-exFqJ3MWCs1d00Zj+XfwP0eXD8R4jG8Zd+bp3pUguGOVa9+x4UTbYWul2S9lt40Arp33DFijobl+ocDSRBCIFg==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@amplitude/plugin-page-view-tracking-browser": {
|
||||
"version": "2.3.32",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.32.tgz",
|
||||
"integrity": "sha512-Y+6OUv0De1hI4GSdxlWFSsTvjKGsfWM2AP2+elLsPQuZxgBUXjmfkPwRLh4TLMmtbQPfX38vbddBVYWPSStRFw==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-client-common": "^2.3.26",
|
||||
"@amplitude/analytics-types": "^2.9.2",
|
||||
"tslib": "^2.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": {
|
||||
"version": "2.3.26",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.26.tgz",
|
||||
"integrity": "sha512-YVMZkugw01fsok2i3x/HhAYG8t9kYxnmNDl6oKbvZkzCzfRUuOKa+f0OLUD2SRJRxTKRanAwHINZuDdk3cR7qQ==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.4.8",
|
||||
"@amplitude/analytics-core": "^2.14.0",
|
||||
"@amplitude/analytics-types": "^2.9.2",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-core": {
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.14.0.tgz",
|
||||
"integrity": "sha512-JmKzlZ5I4JAlWLx2kcCuxH4XNthyaIa+NDAOfpP2xbgZGFM9Gsd9TO/a+SMyGAEryaxb1b5tYdIkMFhjHPjuHA==",
|
||||
"requires": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"@amplitude/analytics-types": {
|
||||
"version": "2.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.2.tgz",
|
||||
"integrity": "sha512-juhTz396dDP/jLJYP9zDOEAZBtJM0JVvP8G10p1OxUDBVwVIprpQL598F9GRQwVFyqV4CEhDmNyAY0HqqU5bhA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@amplitude/types": {
|
||||
"version": "1.10.2",
|
||||
@@ -28507,12 +28836,11 @@
|
||||
"integrity": "sha512-W9G6crS2oqTn7g0RpvYu1l/sna4LnivRTk25jmxdzujOFb9kvQ+VFM/v9RPYV2GIBnzT/maW/EwjFIba9jkflA=="
|
||||
},
|
||||
"@giphy/js-util": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-4.4.2.tgz",
|
||||
"integrity": "sha512-ltthBrFDGXeDTdtuHM+Y8x1FMX7WaT3sCZ8MC/I179+1DJg82/IvDks9yR0mDywYtHvkNxgg5IqC5arvnA8V/w==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@giphy/js-util/-/js-util-5.2.0.tgz",
|
||||
"integrity": "sha512-Qt7pGh2cqiNmXLeWAgb459wK8+BuMLtIxTfg4ZksnPHPsLthiHT9hhzs2QhqUh7Pp/HOq+Cbv2etGDfnq+xiKA==",
|
||||
"requires": {
|
||||
"@giphy/js-types": "*",
|
||||
"dompurify": "^2.2.2",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -31658,9 +31986,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -33280,9 +33608,9 @@
|
||||
"integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@@ -34470,11 +34798,6 @@
|
||||
"domelementtype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"dompurify": {
|
||||
"version": "2.5.8",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz",
|
||||
"integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw=="
|
||||
},
|
||||
"domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
@@ -34648,9 +34971,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -36118,9 +36441,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -36884,9 +37207,9 @@
|
||||
}
|
||||
},
|
||||
"http-proxy-middleware": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"version": "2.0.9",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
|
||||
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
@@ -38073,9 +38396,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -38480,10 +38803,10 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2012.0.0+86b76227/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-xRA4aLoqOSTasNnf8QmiACU0Q+Oo9Yi3ItSNN2bcmaiC+6lOcJP2MHTHcbLcruLvab9CK0jeo7nLbyhIx9tdAQ==",
|
||||
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"integrity": "sha512-21uWTA9II38ldYZGCxBC2kqFsMrAGUGZxUQKcugmncfZ7SCQbWGUMopvcZC0RhSdeL6Mm+RAafc/HcX369egQA==",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
"@jitsi/js-utils": "2.4.6",
|
||||
"@jitsi/logger": "2.0.2",
|
||||
"@jitsi/precall-test": "1.0.6",
|
||||
"@jitsi/rtcstats": "9.7.0",
|
||||
@@ -38500,6 +38823,16 @@
|
||||
"webrtc-adapter": "8.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jitsi/js-utils": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.4.6.tgz",
|
||||
"integrity": "sha512-z/VbM9c0V35T8Zkhxq2gdWbMWmM/3w4BD68xJVmQNrq/NQHxH0fDkRoT/MUds9Mp6dK3AV/h15tCKxVA/0w8Kg==",
|
||||
"requires": {
|
||||
"@hapi/bourne": "^3.0.0",
|
||||
"js-md5": "0.7.3",
|
||||
"ua-parser-js": "1.0.35"
|
||||
}
|
||||
},
|
||||
"@jitsi/rtcstats": {
|
||||
"version": "9.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/rtcstats/-/rtcstats-9.7.0.tgz",
|
||||
@@ -38515,6 +38848,11 @@
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz",
|
||||
"integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="
|
||||
},
|
||||
"js-md5": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz",
|
||||
"integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -39544,9 +39882,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -41616,6 +41954,11 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"react-virtualized-auto-sizer": {
|
||||
"version": "1.0.26",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz",
|
||||
"integrity": "sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A=="
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.6.tgz",
|
||||
@@ -42087,7 +42430,6 @@
|
||||
"version": "7.8.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
|
||||
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"author": "",
|
||||
"readmeFilename": "README.md",
|
||||
"dependencies": {
|
||||
"@amplitude/react-native": "2.17.3",
|
||||
"@amplitude/analytics-browser": "2.17.12",
|
||||
"@amplitude/analytics-react-native": "1.4.13",
|
||||
"@braintree/sanitize-url": "7.0.0",
|
||||
"@emotion/react": "11.10.6",
|
||||
"@emotion/styled": "11.10.6",
|
||||
@@ -68,7 +69,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/v2012.0.0+86b76227/lib-jitsi-meet.tgz",
|
||||
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v2025.0.0+49eb29a8/lib-jitsi-meet.tgz",
|
||||
"lodash-es": "4.17.21",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
@@ -110,6 +111,7 @@
|
||||
"react-native-youtube-iframe": "2.3.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-textarea-autosize": "8.3.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.26",
|
||||
"react-window": "1.8.6",
|
||||
"react-youtube": "10.1.0",
|
||||
"redux": "4.0.4",
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
"zxcvbn": "0.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@amplitude/react-native": "0.0.0",
|
||||
"@amplitude/analytics-react-native": "0.0.0",
|
||||
"@giphy/react-native-sdk": "0.0.0",
|
||||
"@react-native-async-storage/async-storage": "0.0.0",
|
||||
"@react-native-clipboard/clipboard": "0.0.0",
|
||||
|
||||
@@ -84,7 +84,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
} = config;
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
scriptURLs,
|
||||
matomoEndpoint,
|
||||
@@ -94,7 +93,6 @@ export async function createHandlers({ getState }: IStore) {
|
||||
const { group, user } = state['features/base/jwt'];
|
||||
const handlerConstructorOptions = {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM,
|
||||
blackListedEvents,
|
||||
envType: deploymentInfo?.envType || 'dev',
|
||||
matomoEndpoint,
|
||||
|
||||
@@ -11,7 +11,6 @@ export interface IEvent {
|
||||
|
||||
interface IOptions {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
envType?: string;
|
||||
group?: string;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Identify } from '@amplitude/analytics-core';
|
||||
|
||||
import logger from '../logger';
|
||||
|
||||
import AbstractHandler, { IEvent } from './AbstractHandler';
|
||||
import { fixDeviceID } from './amplitude/fixDeviceID';
|
||||
import amplitude from './amplitude/lib';
|
||||
import amplitude, { initAmplitude } from './amplitude/lib';
|
||||
|
||||
/**
|
||||
* Analytics handler for Amplitude.
|
||||
*/
|
||||
export default class AmplitudeHandler extends AbstractHandler {
|
||||
_deviceId: string;
|
||||
_userId: Object;
|
||||
|
||||
/**
|
||||
* Creates new instance of the Amplitude analytics handler.
|
||||
*
|
||||
* @param {Object} options - The amplitude options.
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API.
|
||||
* @param {boolean} options.amplitudeIncludeUTM - Whether to include UTM parameters
|
||||
* @param {string} options.amplitudeAPPKey - The Amplitude app key required by the Amplitude API
|
||||
* in the Amplitude events.
|
||||
*/
|
||||
constructor(options: any) {
|
||||
@@ -24,62 +23,38 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
|
||||
const {
|
||||
amplitudeAPPKey,
|
||||
amplitudeIncludeUTM: includeUtm = true,
|
||||
user
|
||||
} = options;
|
||||
|
||||
this._enabled = true;
|
||||
|
||||
const onError = (e: Error) => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
};
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon
|
||||
const onExitPage = () => {
|
||||
// @ts-ignore
|
||||
amplitude.getInstance().sendEvents();
|
||||
};
|
||||
|
||||
if (navigator.product === 'ReactNative') {
|
||||
amplitude.getInstance().init(amplitudeAPPKey);
|
||||
fixDeviceID(amplitude.getInstance()).then(() => {
|
||||
amplitude.getInstance().getDeviceId()
|
||||
|
||||
// @ts-ignore
|
||||
.then((deviceId: string) => {
|
||||
this._deviceId = deviceId;
|
||||
});
|
||||
initAmplitude(amplitudeAPPKey, user)
|
||||
.then(() => {
|
||||
logger.info('Amplitude initialized');
|
||||
fixDeviceID(amplitude);
|
||||
})
|
||||
.catch(e => {
|
||||
logger.error('Error initializing Amplitude', e);
|
||||
this._enabled = false;
|
||||
});
|
||||
} else {
|
||||
const amplitudeOptions: any = {
|
||||
includeReferrer: true,
|
||||
includeUtm,
|
||||
saveParamsReferrerOncePerSession: false,
|
||||
onError,
|
||||
onExitPage
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
amplitude.getInstance().init(amplitudeAPPKey, undefined, amplitudeOptions);
|
||||
fixDeviceID(amplitude.getInstance());
|
||||
}
|
||||
|
||||
if (user) {
|
||||
this._userId = user;
|
||||
amplitude.getInstance().setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Amplitude user properties.
|
||||
*
|
||||
* @param {Object} userProps - The user portperties.
|
||||
* @param {Object} userProps - The user properties.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserProperties(userProps: any) {
|
||||
if (this._enabled) {
|
||||
amplitude.getInstance().setUserProperties(userProps);
|
||||
const identify = new Identify();
|
||||
|
||||
// Set all properties
|
||||
Object.entries(userProps).forEach(([ key, value ]) => {
|
||||
identify.set(key, value as any);
|
||||
});
|
||||
|
||||
amplitude.identify(identify);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +71,9 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
amplitude.getInstance().logEvent(this._extractName(event) ?? '', event);
|
||||
const eventName = this._extractName(event) ?? '';
|
||||
|
||||
amplitude.track(eventName, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,21 +82,10 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
* @returns {Object}
|
||||
*/
|
||||
getIdentityProps() {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
return {
|
||||
deviceId: this._deviceId,
|
||||
userId: this._userId
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sessionId: amplitude.getInstance().getSessionId(),
|
||||
|
||||
// @ts-ignore
|
||||
deviceId: amplitude.getInstance().options.deviceId,
|
||||
|
||||
// @ts-ignore
|
||||
userId: amplitude.getInstance().options.userId
|
||||
sessionId: amplitude.getSessionId(),
|
||||
deviceId: amplitude.getDeviceId(),
|
||||
userId: amplitude.getUserId()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Amplitude } from '@amplitude/react-native';
|
||||
import { Types } from '@amplitude/analytics-react-native';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
import { getUniqueId } from 'react-native-device-info';
|
||||
|
||||
@@ -8,16 +8,16 @@ import logger from '../../logger';
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {AmplitudeClient} amplitude - The amplitude instance.
|
||||
* @param {Types.ReactNativeClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export async function fixDeviceID(amplitude: Amplitude) {
|
||||
export async function fixDeviceID(amplitude: Types.ReactNativeClient) {
|
||||
await DefaultPreference.setName('jitsi-preferences');
|
||||
|
||||
const current = await DefaultPreference.get('amplitudeDeviceId');
|
||||
|
||||
if (current) {
|
||||
await amplitude.setDeviceId(current);
|
||||
amplitude.setDeviceId(current);
|
||||
} else {
|
||||
const uid = await getUniqueId();
|
||||
|
||||
@@ -27,7 +27,7 @@ export async function fixDeviceID(amplitude: Amplitude) {
|
||||
return;
|
||||
}
|
||||
|
||||
await amplitude.setDeviceId(uid as string);
|
||||
amplitude.setDeviceId(uid as string);
|
||||
await DefaultPreference.set('amplitudeDeviceId', uid as string);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
import { AmplitudeClient } from 'amplitude-js';
|
||||
import { Types } from '@amplitude/analytics-browser';
|
||||
// @ts-ignore
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
|
||||
import logger from '../../logger';
|
||||
|
||||
/**
|
||||
* Key used to store the device id in local storage.
|
||||
*/
|
||||
const DEVICE_ID_KEY = '__AMDID';
|
||||
|
||||
/**
|
||||
* Custom logic for setting the correct device id.
|
||||
*
|
||||
* @param {AmplitudeClient} _amplitude - The amplitude instance.
|
||||
* @param {Types.BrowserClient} amplitude - The amplitude instance.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function fixDeviceID(_amplitude: AmplitudeClient): Promise<any> {
|
||||
return new Promise(resolve => resolve(true));
|
||||
export function fixDeviceID(amplitude: Types.BrowserClient) {
|
||||
const deviceId = jitsiLocalStorage.getItem(DEVICE_ID_KEY);
|
||||
|
||||
if (deviceId) {
|
||||
// Set the device id in Amplitude.
|
||||
try {
|
||||
amplitude.setDeviceId(JSON.parse(deviceId));
|
||||
} catch (error) {
|
||||
logger.error('Failed to set device ID in Amplitude', error);
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
} else {
|
||||
const newDeviceId = amplitude.getDeviceId();
|
||||
|
||||
if (newDeviceId) {
|
||||
jitsiLocalStorage.setItem(DEVICE_ID_KEY, JSON.stringify(newDeviceId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import { Amplitude } from '@amplitude/react-native';
|
||||
import amplitude from '@amplitude/analytics-react-native';
|
||||
|
||||
export default Amplitude;
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
return amplitude.init(amplitudeAPPKey, user, {}).promise;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,38 @@
|
||||
import amplitude from 'amplitude-js';
|
||||
import { createInstance } from '@amplitude/analytics-browser';
|
||||
|
||||
const amplitude = createInstance();
|
||||
|
||||
export default amplitude;
|
||||
|
||||
/**
|
||||
* Initializes the Amplitude instance.
|
||||
*
|
||||
* @param {string} amplitudeAPPKey - The Amplitude app key.
|
||||
* @param {string | undefined} user - The user ID.
|
||||
* @returns {Promise} The initialized Amplitude instance.
|
||||
*/
|
||||
export function initAmplitude(
|
||||
amplitudeAPPKey: string, user: string | undefined): Promise<unknown> {
|
||||
|
||||
// Forces sending all events on exit (flushing) via sendBeacon.
|
||||
window.addEventListener('pagehide', () => {
|
||||
// Set https transport to use sendBeacon API.
|
||||
amplitude.setTransport('beacon');
|
||||
// Send all pending events to server.
|
||||
amplitude.flush();
|
||||
});
|
||||
|
||||
const options = {
|
||||
autocapture: {
|
||||
attribution: true,
|
||||
pageViews: true,
|
||||
sessions: false,
|
||||
fileDownloads: false,
|
||||
formInteractions: false,
|
||||
elementInteractions: false
|
||||
},
|
||||
defaultTracking: false
|
||||
};
|
||||
|
||||
return amplitude.init(amplitudeAPPKey, user, options).promise;
|
||||
}
|
||||
|
||||
@@ -24,20 +24,17 @@ MiddlewareRegistry.register(() => (next: Function) => (action: AnyAction) => {
|
||||
case APP_WILL_MOUNT: {
|
||||
// Disable it inside an iframe until Google fixes the origin trial for 3rd party sources:
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1504167
|
||||
if (!isEmbedded() && 'PressureObserver' in globalThis) {
|
||||
if (!isEmbedded() && 'PressureObserver' in window) {
|
||||
pressureObserver = new window.PressureObserver(
|
||||
(records: typeof window.PressureRecord) => {
|
||||
logger.info('Compute pressure state changed:', JSON.stringify(records));
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyComputePressureChanged(records);
|
||||
}
|
||||
},
|
||||
{ sampleRate: 1 }
|
||||
APP.API.notifyComputePressureChanged(records);
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
pressureObserver
|
||||
.observe('cpu')
|
||||
.observe('cpu', { sampleInterval: 30_000 })
|
||||
.catch((e: any) => logger.error('CPU pressure observer failed to start', e));
|
||||
} catch (e: any) {
|
||||
logger.error('CPU pressure observer failed to start', e);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { IStore } from '../../app/types';
|
||||
import { removeLobbyChatParticipant } from '../../chat/actions.any';
|
||||
import { openDisplayNamePrompt } from '../../display-name/actions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { clearNotifications, showErrorNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { INotificationProps } from '../../notifications/types';
|
||||
import { hasDisplayName } from '../../prejoin/utils';
|
||||
@@ -25,7 +25,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
|
||||
import { AudioMixerEffect } from '../../stream-effects/audio-mixer/AudioMixerEffect';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { overwriteConfig } from '../config/actions';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
|
||||
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED, CONNECTION_WILL_CONNECT } from '../connection/actionTypes';
|
||||
import { connectionDisconnected, disconnect } from '../connection/actions';
|
||||
import { validateJwt } from '../jwt/functions';
|
||||
import { JitsiConferenceErrors, JitsiConferenceEvents, JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
@@ -99,6 +99,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONNECTION_FAILED:
|
||||
return _connectionFailed(store, next, action);
|
||||
|
||||
case CONNECTION_WILL_CONNECT:
|
||||
// we are starting a new join process, let's clear the error notifications if any from any previous attempt
|
||||
store.dispatch(clearNotifications());
|
||||
break;
|
||||
|
||||
case CONFERENCE_SUBJECT_CHANGED:
|
||||
return _conferenceSubjectChanged(store, next, action);
|
||||
|
||||
|
||||
@@ -172,7 +172,6 @@ export interface IConfig {
|
||||
_screenshotHistoryRegionUrl?: number;
|
||||
analytics?: {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
disabled?: boolean;
|
||||
matomoEndpoint?: string;
|
||||
@@ -384,10 +383,12 @@ export interface IConfig {
|
||||
maxFileSize?: number;
|
||||
};
|
||||
filmstrip?: {
|
||||
alwaysShowResizeBar?: boolean;
|
||||
disableResizable?: boolean;
|
||||
disableStageFilmstrip?: boolean;
|
||||
disableTopPanel?: boolean;
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
};
|
||||
flags?: {
|
||||
@@ -552,7 +553,7 @@ export interface IConfig {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
@@ -594,6 +595,7 @@ export interface IConfig {
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { SET_DYNAMIC_BRANDING_DATA } from '../../dynamic-branding/actionTypes';
|
||||
import { setUserFilmstripWidth } from '../../filmstrip/actions.web';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import { updateSettings } from '../settings/actions';
|
||||
@@ -79,12 +80,18 @@ function _setConfig({ dispatch, getState }: IStore, next: Function, action: AnyA
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.config.filmstrip?.stageFilmstripParticipants !== undefined) {
|
||||
const { initialWidth, stageFilmstripParticipants } = action.config.filmstrip || {};
|
||||
|
||||
if (stageFilmstripParticipants !== undefined) {
|
||||
dispatch(updateSettings({
|
||||
maxStageParticipants: action.config.filmstrip.stageFilmstripParticipants
|
||||
maxStageParticipants: stageFilmstripParticipants
|
||||
}));
|
||||
}
|
||||
|
||||
if (initialWidth) {
|
||||
dispatch(setUserFilmstripWidth(initialWidth));
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
|
||||
@@ -185,6 +185,15 @@ function _setConfig(state: IConfig, { config }: { config: IConfig; }) {
|
||||
});
|
||||
}
|
||||
|
||||
const { alwaysShowResizeBar, disableResizable } = config.filmstrip || {};
|
||||
|
||||
if (alwaysShowResizeBar && disableResizable) {
|
||||
config.filmstrip = {
|
||||
...config.filmstrip,
|
||||
alwaysShowResizeBar: false
|
||||
};
|
||||
}
|
||||
|
||||
const newState = merge(
|
||||
{},
|
||||
config,
|
||||
|
||||
@@ -18,6 +18,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
ROOM: 'room',
|
||||
SCREEN_SHARING: 'screen-sharing',
|
||||
SEND_GROUPCHAT: 'send-groupchat',
|
||||
LIST_VISITORS: 'list-visitors',
|
||||
SIP_INBOUND_CALL: 'sip-inbound-call',
|
||||
SIP_OUTBOUND_CALL: 'sip-outbound-call',
|
||||
TRANSCRIPTION: 'transcription'
|
||||
|
||||
@@ -823,3 +823,25 @@ export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispat
|
||||
dispatch(toggleShareDialog(true));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | undefined, state: IReduxState) {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if (participant?.local || state['features/visitors'].iAmVisitor || disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'allow-moderator-chat') {
|
||||
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
|
||||
}
|
||||
|
||||
return !disablePrivateChat;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface IParticipantFeatures {
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
|
||||
@@ -12,13 +12,7 @@ const JITSI_MEET_APPS = [
|
||||
'org.jitsi.meet',
|
||||
|
||||
// Android debug app.
|
||||
'org.jitsi.meet.debug',
|
||||
|
||||
// 8x8 Work (Android).
|
||||
'org.vom8x8.sipua',
|
||||
|
||||
// 8x8 Work (iOS).
|
||||
'com.yourcompany.Virtual-Office'
|
||||
'org.jitsi.meet.debug'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Platform, View, ViewStyle } from 'react-native';
|
||||
import { Platform, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { Text } from 'react-native-paper';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
@@ -92,6 +93,18 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
inputBarStyles = styles.inputBarNarrow;
|
||||
}
|
||||
|
||||
if (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) {
|
||||
return (
|
||||
<View
|
||||
id = 'no-messages-message'
|
||||
style = { styles.disabledSendWrapper as ViewStyle }>
|
||||
<Text style = { styles.emptyComponentText as TextStyle }>
|
||||
{ this.props.t('chat.disabled') }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
id = 'chat-input'
|
||||
@@ -112,8 +125,7 @@ class ChatInputBar extends Component<IProps, IState> {
|
||||
returnKeyType = 'send'
|
||||
value = { this.state.message } />
|
||||
<IconButton
|
||||
disabled = { !this.state.message
|
||||
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
|
||||
disabled = { !this.state.message }
|
||||
id = { this.props.t('chat.sendButton') }
|
||||
onPress = { this._onSubmit }
|
||||
src = { IconSend }
|
||||
|
||||
@@ -38,6 +38,11 @@ export default {
|
||||
flex: 1
|
||||
},
|
||||
|
||||
chatDisabled: {
|
||||
padding: BaseTheme.spacing[2],
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
emptyComponentText: {
|
||||
color: BaseTheme.palette.text03,
|
||||
textAlign: 'center'
|
||||
@@ -115,6 +120,15 @@ export default {
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
disabledSendWrapper: {
|
||||
alignSelf: 'center',
|
||||
flex: 0,
|
||||
padding: BoxModel.padding,
|
||||
paddingBottom: '8%',
|
||||
paddingTop: '8%',
|
||||
maxWidth: '80%'
|
||||
},
|
||||
|
||||
/**
|
||||
* A special padding to avoid issues on some devices (such as Android devices with custom suggestions bar).
|
||||
*/
|
||||
|
||||
@@ -447,7 +447,7 @@ const Chat = ({
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function renderTabs() {
|
||||
const tabs = [
|
||||
let tabs = [
|
||||
{
|
||||
accessibilityLabel: t('chat.tabs.chat'),
|
||||
countBadge:
|
||||
@@ -488,6 +488,10 @@ const Chat = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (tabs.length === 1) {
|
||||
tabs = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
accessibilityLabel = { _isPollsEnabled || _isCCTabEnabled || _isFileSharingTabEnabled
|
||||
|
||||
@@ -35,6 +35,12 @@ const styles = (_theme: Theme, { _chatWidth }: IProps) => {
|
||||
backgroundColor: '#131519',
|
||||
borderTop: '1px solid #A4B8D1'
|
||||
}
|
||||
},
|
||||
chatDisabled: {
|
||||
borderTop: `1px solid ${_theme.palette.ui02}`,
|
||||
boxSizing: 'border-box' as const,
|
||||
padding: _theme.spacing(4),
|
||||
textAlign: 'center' as const,
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -159,7 +165,15 @@ class ChatInput extends Component<IProps, IState> {
|
||||
*/
|
||||
override render() {
|
||||
const classes = withStyles.getClasses(this.props);
|
||||
const hideInput = this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId;
|
||||
|
||||
if (hideInput) {
|
||||
return (
|
||||
<div className = { classes.chatDisabled }>
|
||||
{this.props.t('chat.disabled')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { `chat-input-container${this.state.message.trim().length ? ' populated' : ''}` }>
|
||||
@@ -188,8 +202,7 @@ class ChatInput extends Component<IProps, IState> {
|
||||
value = { this.state.message } />
|
||||
<Button
|
||||
accessibilityLabel = { this.props.t('chat.sendButton') }
|
||||
disabled = { !this.state.message.trim()
|
||||
|| (this.props._isSendGroupChatDisabled && !this.props._privateMessageRecipientId) }
|
||||
disabled = { !this.state.message.trim() }
|
||||
icon = { IconSend }
|
||||
onClick = { this._onSubmitMessage }
|
||||
size = { isMobileBrowser() ? 'large' : 'medium' } />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { getParticipantDisplayName } from '../../../base/participants/functions';
|
||||
import { getParticipantById, getParticipantDisplayName, isPrivateChatEnabled } from '../../../base/participants/functions';
|
||||
import Popover from '../../../base/popover/components/Popover.web';
|
||||
import Message from '../../../base/react/components/web/Message';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -412,10 +412,12 @@ const ChatMessage = ({
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, { message }: IProps) {
|
||||
const { knocking } = state['features/lobby'];
|
||||
const localParticipantId = state['features/base/participants'].local?.id;
|
||||
|
||||
const participant = getParticipantById(state, message.participantId);
|
||||
const enablePrivateChat = isPrivateChatEnabled(participant, state);
|
||||
|
||||
return {
|
||||
shouldDisplayChatMessageMenu: message.participantId !== localParticipantId,
|
||||
shouldDisplayChatMessageMenu: enablePrivateChat,
|
||||
knocking,
|
||||
state
|
||||
};
|
||||
|
||||
@@ -234,6 +234,7 @@ const FileSharing = () => {
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
processFiles(e.target.files as FileList, store);
|
||||
e.target.value = ''; // Reset the input value to allow re-uploading the same file
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
import { iAmVisitor } from '../visitors/functions';
|
||||
|
||||
import { uploadFiles } from './actions';
|
||||
import { MAX_FILE_SIZE } from './constants';
|
||||
@@ -150,5 +151,7 @@ export const processFiles = (fileList: FileList | File[], store: IStore) => {
|
||||
* @returns {boolean} Indication of whether local user can upload files.
|
||||
*/
|
||||
export function isFileUploadingEnabled(state: IReduxState): boolean {
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false) && isFileSharingEnabled(state);
|
||||
return !iAmVisitor(state)
|
||||
&& isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false)
|
||||
&& isFileSharingEnabled(state);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { addFile, removeFile, updateFileProgress } from './actions';
|
||||
import { getFileExtension } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { IFileMetadata } from './types';
|
||||
import { downloadFile } from './utils';
|
||||
|
||||
|
||||
/**
|
||||
@@ -122,14 +123,14 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
}))
|
||||
.then((response: any) => response.json())
|
||||
.then((data: { presignedUrl: any; }) => {
|
||||
const url = data.presignedUrl;
|
||||
.then((data: { fileName: string; presignedUrl: string; }) => {
|
||||
const { presignedUrl, fileName } = data;
|
||||
|
||||
if (!url) {
|
||||
if (!presignedUrl) {
|
||||
throw new Error('No presigned URL found in the response.');
|
||||
}
|
||||
|
||||
window.open(url, '_blank', 'noreferrer,noopener');
|
||||
return downloadFile(presignedUrl, fileName);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not download file:', error);
|
||||
@@ -215,7 +216,7 @@ function uploadFile(file: File, store: IStore, token: string): void {
|
||||
if (event.lengthComputable) {
|
||||
// We use 99% as the max value to avoid showing 100% before the
|
||||
// upload is actually finished, that is, when the request is completed.
|
||||
const percent = Math.max((event.loaded / event.total) * 100, 99);
|
||||
const percent = Math.min((event.loaded / event.total) * 100, 99);
|
||||
|
||||
store.dispatch(updateFileProgress(fileId, percent));
|
||||
}
|
||||
|
||||
27
react/features/file-sharing/utils.ts
Normal file
27
react/features/file-sharing/utils.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
const generateDownloadUrl = async (url: string) => {
|
||||
const resp = await fetch(url);
|
||||
const respBlob = await resp.blob();
|
||||
|
||||
const blob = new Blob([ respBlob ]);
|
||||
|
||||
return URL.createObjectURL(blob);
|
||||
};
|
||||
|
||||
export const downloadFile = async (url: string, fileName: string) => {
|
||||
const dowloadUrl = await generateDownloadUrl(url);
|
||||
const link = document.createElement('a');
|
||||
|
||||
if (fileName) {
|
||||
link.download = fileName;
|
||||
}
|
||||
link.href = dowloadUrl;
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// fix for certain browsers
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(dowloadUrl);
|
||||
}, 0);
|
||||
};
|
||||
@@ -179,6 +179,9 @@ function styles(theme: Theme, props: IProps) {
|
||||
|
||||
'&.top-panel-filmstrip': {
|
||||
flexDirection: 'column' as const
|
||||
},
|
||||
'&.always-show-resize-bar': {
|
||||
backgroundColor: BACKGROUND_COLOR
|
||||
}
|
||||
},
|
||||
|
||||
@@ -245,6 +248,10 @@ function styles(theme: Theme, props: IProps) {
|
||||
* The type of the React {@code Component} props of {@link Filmstrip}.
|
||||
*/
|
||||
export interface IProps extends WithTranslation {
|
||||
/**
|
||||
* Whether to always show the resize bar on filmstrip. This will make the filmstrip always visible.
|
||||
*/
|
||||
_alwaysShowResizeBar?: boolean;
|
||||
|
||||
/**
|
||||
* Additional CSS class names top add to the root.
|
||||
@@ -533,6 +540,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
override render() {
|
||||
const filmstripStyle: any = { };
|
||||
const {
|
||||
_alwaysShowResizeBar,
|
||||
_currentLayout,
|
||||
_disableSelfView,
|
||||
_filmstripDisabled,
|
||||
@@ -649,11 +657,12 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
{_resizableFilmstrip
|
||||
? <div
|
||||
className = { clsx('resizable-filmstrip', classes.resizableFilmstripContainer,
|
||||
_topPanelFilmstrip && 'top-panel-filmstrip') }>
|
||||
_topPanelFilmstrip && 'top-panel-filmstrip',
|
||||
_alwaysShowResizeBar && 'always-show-resize-bar') }>
|
||||
<div
|
||||
className = { clsx('dragHandleContainer',
|
||||
classes.dragHandleContainer,
|
||||
isMouseDown && 'visible',
|
||||
(isMouseDown || _alwaysShowResizeBar) && 'visible',
|
||||
_topPanelFilmstrip && 'top-panel')
|
||||
}
|
||||
onMouseDown = { this._onDragHandleMouseDown }>
|
||||
@@ -1081,7 +1090,7 @@ class Filmstrip extends PureComponent <IProps, IState> {
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { _hasScroll = false, filmstripType, _topPanelFilmstrip, _remoteParticipants } = ownProps;
|
||||
const { toolbarButtons } = state['features/toolbox'];
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const { iAmRecorder, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
|
||||
const { topPanelHeight, topPanelVisible, visible, width: verticalFilmstripWidth } = state['features/filmstrip'];
|
||||
const { localScreenShare } = state['features/base/participants'];
|
||||
const reduceHeight = state['features/toolbox'].visible && toolbarButtons?.length;
|
||||
@@ -1134,7 +1143,8 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_topPanelVisible,
|
||||
_verticalFilmstripWidth: verticalFilmstripWidth.current,
|
||||
_verticalViewMaxWidth: getVerticalViewMaxWidth(state),
|
||||
_videosClassName: videosClassName
|
||||
_videosClassName: videosClassName,
|
||||
_alwaysShowResizeBar: alwaysShowResizeBar
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -81,13 +81,13 @@ export function shouldRemoteVideosBeVisible(state: IReduxState) {
|
||||
// in the filmstrip.
|
||||
const participantCount = getParticipantCountWithFake(state);
|
||||
let pinnedParticipant;
|
||||
const { disable1On1Mode } = state['features/base/config'];
|
||||
const { disable1On1Mode, filmstrip: { alwaysShowResizeBar } = {} } = state['features/base/config'];
|
||||
const { contextMenuOpened } = state['features/base/responsive-ui'];
|
||||
|
||||
return Boolean(
|
||||
contextMenuOpened
|
||||
|| participantCount > 2
|
||||
|
||||
|| alwaysShowResizeBar
|
||||
// Always show the filmstrip when there is another participant to
|
||||
// show and the local video is pinned, or the toolbar is displayed.
|
||||
|| (participantCount > 1
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import { normalizeAccents } from '../../../base/util/strings.web';
|
||||
import { subscribeVisitorsList } from '../../../visitors/actions';
|
||||
import {
|
||||
getVisitorsCount,
|
||||
getVisitorsList,
|
||||
isVisitorsListEnabled,
|
||||
isVisitorsListSubscribed,
|
||||
shouldDisplayCurrentVisitorsList
|
||||
} from '../../../visitors/functions';
|
||||
import { ACTION_TRIGGER, MEDIA_STATE } from '../../constants';
|
||||
|
||||
import ParticipantItem from './ParticipantItem';
|
||||
|
||||
/**
|
||||
* Props for the {@code CurrentVisitorsList} component.
|
||||
*/
|
||||
interface IProps {
|
||||
searchString: string;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
marginTop: theme.spacing(3),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
flexGrow: 1
|
||||
},
|
||||
heading: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
cursor: 'pointer',
|
||||
padding: `${theme.spacing(1)} 0`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortBold),
|
||||
color: theme.palette.text02,
|
||||
flexShrink: 0
|
||||
},
|
||||
arrowContainer: {
|
||||
backgroundColor: theme.palette.ui03,
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '6px',
|
||||
marginLeft: theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: 'none'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1,
|
||||
minHeight: '200px',
|
||||
maxHeight: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Renders the visitors list inside the participants pane.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React$Element<any>} The component.
|
||||
*/
|
||||
export default function CurrentVisitorsList({ searchString }: IProps) {
|
||||
const visitorsCount = useSelector(getVisitorsCount);
|
||||
const visitors = useSelector(getVisitorsList);
|
||||
const featureEnabled = useSelector(isVisitorsListEnabled);
|
||||
const shouldDisplayList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
const [ collapsed, setCollapsed ] = useState(true);
|
||||
const isSubscribed = useSelector(isVisitorsListSubscribed);
|
||||
|
||||
const toggleCollapsed = useCallback(() => {
|
||||
setCollapsed(c => {
|
||||
const newCollapsed = !c;
|
||||
|
||||
if (featureEnabled && !newCollapsed && !isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
|
||||
return newCollapsed;
|
||||
});
|
||||
}, [ dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (featureEnabled && searchString) {
|
||||
setCollapsed(false);
|
||||
if (!isSubscribed) {
|
||||
dispatch(subscribeVisitorsList());
|
||||
}
|
||||
}
|
||||
}, [ searchString, dispatch, isSubscribed, featureEnabled ]);
|
||||
|
||||
if (!shouldDisplayList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filtered = visitors.filter(v =>
|
||||
normalizeAccents(v.name).toLowerCase().includes(normalizeAccents(searchString).toLowerCase())
|
||||
);
|
||||
|
||||
// ListItem height is 56px including padding so the item size
|
||||
// for virtualization needs to match it exactly to avoid clipping.
|
||||
const itemSize = 56;
|
||||
|
||||
const Row = ({ index, style }: { index: number; style: any; }) => {
|
||||
const v = filtered[index];
|
||||
|
||||
return (
|
||||
<div style = { style }>
|
||||
<ParticipantItem
|
||||
actionsTrigger = { ACTION_TRIGGER.HOVER }
|
||||
audioMediaState = { MEDIA_STATE.NONE }
|
||||
displayName = { v.name }
|
||||
participantID = { v.id }
|
||||
videoMediaState = { MEDIA_STATE.NONE } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const styles = {
|
||||
overflowX: 'hidden' as const,
|
||||
overflowY: 'auto' as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<div
|
||||
className = { classes.heading }
|
||||
onClick = { toggleCollapsed }>
|
||||
<span>{ t('participantsPane.headings.visitorsList', { count: visitorsCount }) }</span>
|
||||
<span className = { classes.arrowContainer }>
|
||||
<Icon
|
||||
size = { 14 }
|
||||
src = { collapsed ? IconArrowDown : IconArrowUp } />
|
||||
</span>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className = { classes.listContainer }>
|
||||
<AutoSizer>
|
||||
{ ({ height, width }) => (
|
||||
<FixedSizeList
|
||||
height = { Math.max(height, 200) }
|
||||
itemCount = { filtered.length }
|
||||
itemSize = { itemSize }
|
||||
style = { styles }
|
||||
width = { width }>
|
||||
{ Row }
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { findAncestorByClass } from '../../../base/ui/functions.web';
|
||||
import { isAddBreakoutRoomButtonVisible } from '../../../breakout-rooms/functions';
|
||||
import MuteEveryoneDialog from '../../../video-menu/components/web/MuteEveryoneDialog';
|
||||
import { shouldDisplayCurrentVisitorsList } from '../../../visitors/functions';
|
||||
import { close } from '../../actions.web';
|
||||
import {
|
||||
getParticipantsPaneOpen,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
import { AddBreakoutRoomButton } from '../breakout-rooms/components/web/AddBreakoutRoomButton';
|
||||
import { RoomList } from '../breakout-rooms/components/web/RoomList';
|
||||
|
||||
import CurrentVisitorsList from './CurrentVisitorsList';
|
||||
import { FooterContextMenu } from './FooterContextMenu';
|
||||
import LobbyParticipants from './LobbyParticipants';
|
||||
import MeetingParticipants from './MeetingParticipants';
|
||||
@@ -73,6 +75,8 @@ const useStyles = makeStyles<IStylesProps>()((theme, { isChatOpen }) => {
|
||||
overflowY: 'auto',
|
||||
position: 'relative',
|
||||
padding: `0 ${participantsPaneTheme.panePadding}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none'
|
||||
@@ -129,6 +133,7 @@ const ParticipantsPane = () => {
|
||||
const paneOpen = useSelector(getParticipantsPaneOpen);
|
||||
const isBreakoutRoomsSupported = useSelector((state: IReduxState) => state['features/base/conference'])
|
||||
.conference?.getBreakoutRooms()?.isSupported();
|
||||
const showCurrentVisitorsList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
const showAddRoomButton = useSelector(isAddBreakoutRoomButtonVisible);
|
||||
const showFooter = useSelector(isLocalParticipantModerator);
|
||||
const showMuteAllButton = useSelector(isMuteAllVisible);
|
||||
@@ -193,6 +198,7 @@ const ParticipantsPane = () => {
|
||||
setSearchString = { setSearchString } />
|
||||
{isBreakoutRoomsSupported && <RoomList searchString = { searchString } />}
|
||||
{showAddRoomButton && <AddBreakoutRoomButton />}
|
||||
{showCurrentVisitorsList && <CurrentVisitorsList searchString = { searchString } />}
|
||||
</div>
|
||||
{showFooter && (
|
||||
<div className = { classes.footer }>
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
getPromotionRequests,
|
||||
getVisitorsCount,
|
||||
getVisitorsInQueueCount,
|
||||
isVisitorsLive
|
||||
isVisitorsLive,
|
||||
shouldDisplayCurrentVisitorsList
|
||||
} from '../../../visitors/functions';
|
||||
|
||||
import { VisitorsItem } from './VisitorsItem';
|
||||
@@ -74,6 +75,7 @@ export default function VisitorsList() {
|
||||
const visitorsInQueueCount = useSelector(getVisitorsInQueueCount);
|
||||
const isLive = useSelector(isVisitorsLive);
|
||||
const showVisitorsInQueue = visitorsInQueueCount > 0 && isLive === false;
|
||||
const showCurrentVisitorsList = useSelector(shouldDisplayCurrentVisitorsList);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { classes, cx } = useStyles();
|
||||
@@ -91,15 +93,19 @@ export default function VisitorsList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showCurrentVisitorsList && requests.length <= 0 && !showVisitorsInQueue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className = { classes.headingContainer }>
|
||||
<div
|
||||
className = { cx(classes.heading, classes.headingW) }
|
||||
id = 'visitor-list-header' >
|
||||
{ t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ !showCurrentVisitorsList && t('participantsPane.headings.visitors', { count: visitorsCount })}
|
||||
{ requests.length > 0
|
||||
&& t('participantsPane.headings.visitorRequests', { count: requests.length }) }
|
||||
&& t(`participantsPane.headings.${showCurrentVisitorsList ? 'viewerRequests' : 'visitorRequests'}`, { count: requests.length }) }
|
||||
{ showVisitorsInQueue
|
||||
&& t('participantsPane.headings.visitorInQueue', { count: visitorsInQueueCount }) }
|
||||
</div>
|
||||
@@ -116,17 +122,19 @@ export default function VisitorsList() {
|
||||
onClick = { goLiveCb }>{ t('participantsPane.actions.goLive') }</div>
|
||||
}
|
||||
</div>
|
||||
<div
|
||||
className = { classes.container }
|
||||
id = 'visitor-list'>
|
||||
{
|
||||
requests.map(r => (
|
||||
<VisitorsItem
|
||||
key = { r.from }
|
||||
request = { r } />)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{ requests.length > 0
|
||||
&& <div
|
||||
className = { classes.container }
|
||||
id = 'visitor-list'>
|
||||
{
|
||||
requests.map(r => (
|
||||
<VisitorsItem
|
||||
key = { r.from }
|
||||
request = { r } />)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -56,14 +56,13 @@ const PollsPane = (props: AbstractProps) => {
|
||||
? <PollCreate setCreateMode = { setCreateMode } />
|
||||
: <>
|
||||
<PollsList setCreateMode = { setCreateMode } />
|
||||
<Button
|
||||
{!isCreatePollsDisabled && <Button
|
||||
accessibilityLabel = 'polls.create.create'
|
||||
disabled = { isCreatePollsDisabled }
|
||||
id = { t('polls.create.create') }
|
||||
labelKey = 'polls.create.create'
|
||||
onClick = { onCreate }
|
||||
style = { createPollButtonStyles }
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
type = { BUTTON_TYPES.PRIMARY } />}
|
||||
</>
|
||||
}
|
||||
</JitsiScreen>
|
||||
|
||||
@@ -37,14 +37,13 @@ const PollsPane = ({ createMode, isCreatePollsDisabled, onCreate, setCreateMode,
|
||||
<div className = { classes.listContainer } >
|
||||
<PollsList setCreateMode = { setCreateMode } />
|
||||
</div>
|
||||
<div className = { classes.footer }>
|
||||
{ !isCreatePollsDisabled && <div className = { classes.footer }>
|
||||
<Button
|
||||
accessibilityLabel = { t('polls.create.create') }
|
||||
disabled = { isCreatePollsDisabled }
|
||||
fullWidth = { true }
|
||||
labelKey = { 'polls.create.create' }
|
||||
onClick = { onCreate } />
|
||||
</div>
|
||||
</div>}
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -443,14 +443,16 @@ export function shouldRequireRecordingConsent(recorderSession: any, state: IRedu
|
||||
= state['features/dynamic-branding'] || {};
|
||||
const { conference } = state['features/base/conference'] || {};
|
||||
const { requireConsent, skipConsentInMeeting } = state['features/base/config'].recordings || {};
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const { iAmRecorder, testing: { showSpotConsentDialog = false } = {} } = state['features/base/config'];
|
||||
const { consentRequested } = state['features/recording'];
|
||||
|
||||
if (iAmRecorder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSpotTV(state)) {
|
||||
// For Spot TV instances, check the showSpotConsentDialog config parameter
|
||||
// If showSpotConsentDialog is false (or undefined, defaulting to false), don't show consent dialog
|
||||
if (isSpotTV(state) && !showSpotConsentDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,13 +46,17 @@ ReducerRegistry.register<ITranscribingState>('features/transcribing',
|
||||
(state = _getInitialState(), action): ITranscribingState => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_PROPERTIES_CHANGED: {
|
||||
const audioRecordingEnabled = action.properties?.['audio-recording-enabled'] === 'true';
|
||||
const audioRecording = action.properties?.['audio-recording-enabled'];
|
||||
|
||||
if (state.isTranscribing !== audioRecordingEnabled) {
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: audioRecordingEnabled
|
||||
};
|
||||
if (typeof audioRecording !== 'undefined') {
|
||||
const audioRecordingEnabled = audioRecording === 'true';
|
||||
|
||||
if (state.isTranscribing !== audioRecordingEnabled) {
|
||||
return {
|
||||
...state,
|
||||
isTranscribing: audioRecordingEnabled
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
getParticipantById,
|
||||
getParticipantDisplayName,
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator
|
||||
isLocalParticipantModerator,
|
||||
isPrivateChatEnabled
|
||||
} from '../../../base/participants/functions';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { IRoom } from '../../../breakout-rooms/types';
|
||||
@@ -60,16 +61,16 @@ interface IProps {
|
||||
*/
|
||||
_disableKick: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display the send private message button.
|
||||
*/
|
||||
_disablePrivateChat: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display the remote mute buttons.
|
||||
*/
|
||||
_disableRemoteMute: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not to display the send private message button.
|
||||
*/
|
||||
_enablePrivateChat: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the current room is a breakout room.
|
||||
*/
|
||||
@@ -150,9 +151,9 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
override render() {
|
||||
const {
|
||||
_disableKick,
|
||||
_disablePrivateChat,
|
||||
_disableRemoteMute,
|
||||
_disableGrantModerator,
|
||||
_enablePrivateChat,
|
||||
_isBreakoutRoom,
|
||||
_isParticipantAvailable,
|
||||
_isParticipantSilent,
|
||||
@@ -191,7 +192,7 @@ class RemoteVideoMenu extends PureComponent<IProps> {
|
||||
{ !_disableGrantModerator && !_isBreakoutRoom && <GrantModeratorButton { ...buttonProps } /> }
|
||||
<PinButton { ...buttonProps } />
|
||||
{ _showDemote && <DemoteToVisitorButton { ...buttonProps } /> }
|
||||
{ !_disablePrivateChat && <PrivateMessageButton { ...buttonProps } /> }
|
||||
{ _enablePrivateChat && <PrivateMessageButton { ...buttonProps } /> }
|
||||
<ConnectionStatusButton { ...connectionStatusButtonProps } />
|
||||
{_moderator && _rooms.length > 1 && <>
|
||||
{/* @ts-ignore */}
|
||||
@@ -258,7 +259,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { participantId } = ownProps;
|
||||
const { remoteVideoMenu = {}, disableRemoteMute } = state['features/base/config'];
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const { disableKick, disablePrivateChat } = remoteVideoMenu;
|
||||
const { disableKick } = remoteVideoMenu;
|
||||
const _rooms = Object.values(getBreakoutRooms(state));
|
||||
const _currentRoomId = getCurrentRoomId(state);
|
||||
const shouldDisableKick = disableKick || !kickOutEnabled;
|
||||
@@ -271,7 +272,7 @@ function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
_currentRoomId,
|
||||
_disableKick: Boolean(shouldDisableKick),
|
||||
_disableRemoteMute: Boolean(disableRemoteMute),
|
||||
_disablePrivateChat: Boolean(disablePrivateChat) || _iAmVisitor,
|
||||
_enablePrivateChat: isPrivateChatEnabled(participant, state),
|
||||
_isBreakoutRoom,
|
||||
_isParticipantAvailable: Boolean(participant),
|
||||
_isParticipantSilent: Boolean(participant?.isSilent),
|
||||
|
||||
@@ -9,7 +9,7 @@ import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { isIosMobileBrowser, isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { MEDIA_TYPE } from '../../../base/media/constants';
|
||||
import { PARTICIPANT_ROLE } from '../../../base/participants/constants';
|
||||
import { getLocalParticipant, hasRaisedHand } from '../../../base/participants/functions';
|
||||
import { getLocalParticipant, hasRaisedHand, isPrivateChatEnabled } from '../../../base/participants/functions';
|
||||
import { IParticipant } from '../../../base/participants/types';
|
||||
import { isParticipantAudioMuted, isParticipantVideoMuted } from '../../../base/tracks/functions.any';
|
||||
import ContextMenu from '../../../base/ui/components/web/ContextMenu';
|
||||
@@ -24,7 +24,6 @@ import { getQuickActionButtonType, isForceMuted } from '../../../participants-pa
|
||||
import { requestRemoteControl, stopController } from '../../../remote-control/actions';
|
||||
import { getParticipantMenuButtonsWithNotifyClick, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { NOTIFY_CLICK_MODE } from '../../../toolbox/types';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../constants';
|
||||
|
||||
import AskToUnmuteButton from './AskToUnmuteButton';
|
||||
@@ -141,9 +140,8 @@ const ParticipantContextMenu = ({
|
||||
const _overflowDrawer: boolean = useSelector(showOverflowDrawer);
|
||||
const { remoteVideoMenu = {}, disableRemoteMute, startSilent, customParticipantMenuButtons }
|
||||
= useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const visitorsMode = useSelector((state: IReduxState) => iAmVisitor(state));
|
||||
const visitorsSupported = useSelector((state: IReduxState) => state['features/visitors'].supported);
|
||||
const { disableDemote, disableKick, disableGrantModerator, disablePrivateChat } = remoteVideoMenu;
|
||||
const { disableDemote, disableKick, disableGrantModerator } = remoteVideoMenu;
|
||||
const { participantsVolume } = useSelector((state: IReduxState) => state['features/filmstrip']);
|
||||
const _volume = (participant?.local ?? true ? undefined
|
||||
: participant?.id ? participantsVolume[participant?.id] : undefined) ?? 1;
|
||||
@@ -153,6 +151,7 @@ const ParticipantContextMenu = ({
|
||||
const stageFilmstrip = useSelector(isStageFilmstripAvailable);
|
||||
const shouldDisplayVerification = useSelector((state: IReduxState) => displayVerification(state, participant?.id));
|
||||
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
|
||||
const enablePrivateChat = useSelector((state: IReduxState) => isPrivateChatEnabled(participant, state));
|
||||
|
||||
const _currentRoomId = useSelector(getCurrentRoomId);
|
||||
const _rooms: IRoom[] = Object.values(useSelector(getBreakoutRooms));
|
||||
@@ -268,7 +267,7 @@ const ParticipantContextMenu = ({
|
||||
buttons2.push(<TogglePinToStageButton { ...getButtonProps(BUTTONS.PIN_TO_STAGE) } />);
|
||||
}
|
||||
|
||||
if (!disablePrivateChat && !visitorsMode) {
|
||||
if (enablePrivateChat) {
|
||||
buttons2.push(<PrivateMessageMenuButton { ...getButtonProps(BUTTONS.PRIVATE_MESSAGE) } />);
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,7 @@ class VirtualBackgroundPreview extends PureComponent<IProps, IState> {
|
||||
this.props.dispatch(
|
||||
showWarningNotification({
|
||||
titleKey: 'virtualBackground.backgroundEffectError',
|
||||
description: 'Failed to access camera device.'
|
||||
descriptionKey: 'deviceError.cameraError'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG)
|
||||
);
|
||||
logger.error('Failed to access camera device. Error on apply background effect.');
|
||||
|
||||
127
react/features/visitors/VisitorsListWebsocketClient.ts
Normal file
127
react/features/visitors/VisitorsListWebsocketClient.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Client } from '@stomp/stompjs';
|
||||
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
/**
|
||||
* Websocket client impl, used for visitors list.
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class VisitorsListWebsocketClient extends WebsocketClient {
|
||||
private static client: VisitorsListWebsocketClient;
|
||||
|
||||
/**
|
||||
* Creates a new instance of the VisitorsListWebsocketClient.
|
||||
*
|
||||
* @static
|
||||
* @returns {VisitorsListWebsocketClient}
|
||||
*/
|
||||
static override getInstance(): VisitorsListWebsocketClient {
|
||||
if (!this.client) {
|
||||
this.client = new VisitorsListWebsocketClient();
|
||||
}
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the visitors list with initial queue subscription, then switches to topic deltas.
|
||||
*
|
||||
* @param {string} queueServiceURL - The service URL to use.
|
||||
* @param {string} queueEndpoint - The queue endpoint for initial list.
|
||||
* @param {string} topicEndpoint - The topic endpoint for deltas.
|
||||
* @param {Function} initialCallback - Callback executed with initial visitors list.
|
||||
* @param {Function} deltaCallback - Callback executed with delta updates.
|
||||
* @param {string} token - The token to be used for authorization.
|
||||
* @param {Function?} connectCallback - Callback executed when connected.
|
||||
* @returns {void}
|
||||
*/
|
||||
connectVisitorsList(queueServiceURL: string,
|
||||
queueEndpoint: string,
|
||||
topicEndpoint: string,
|
||||
initialCallback: (visitors: Array<{ n: string; r: string; }>) => void,
|
||||
deltaCallback: (updates: Array<{ n: string; r: string; s: string; }>) => void,
|
||||
token: string | undefined,
|
||||
connectCallback?: () => void) {
|
||||
this.stompClient = new Client({
|
||||
brokerURL: queueServiceURL,
|
||||
forceBinaryWSFrames: true,
|
||||
appendMissingNULLonIncoming: true
|
||||
});
|
||||
|
||||
const errorConnecting = (error: any) => {
|
||||
if (this.retriesCount > 3) {
|
||||
this.stompClient?.deactivate();
|
||||
this.stompClient = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.retriesCount++;
|
||||
|
||||
logger.error(`Error connecting to ${queueServiceURL} ${JSON.stringify(error)}`);
|
||||
};
|
||||
|
||||
this.stompClient.onWebSocketError = errorConnecting;
|
||||
|
||||
this.stompClient.onStompError = frame => {
|
||||
logger.error('STOMP error received', frame);
|
||||
errorConnecting(frame.headers.message);
|
||||
};
|
||||
|
||||
if (token) {
|
||||
this.stompClient.connectHeaders = {
|
||||
Authorization: `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
this.stompClient.onConnect = () => {
|
||||
if (!this.stompClient) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('Connected to visitors list websocket');
|
||||
connectCallback?.();
|
||||
|
||||
let initialReceived = false;
|
||||
const cachedDeltas: Array<{ n: string; r: string; s: string; }> = [];
|
||||
|
||||
// Subscribe first for deltas so we don't miss any while waiting for the initial list
|
||||
this.stompClient.subscribe(topicEndpoint, deltaMessage => {
|
||||
try {
|
||||
const updates: Array<{ n: string; r: string; s: string; }> = JSON.parse(deltaMessage.body);
|
||||
|
||||
if (!initialReceived) {
|
||||
cachedDeltas.push(...updates);
|
||||
} else {
|
||||
deltaCallback(updates);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing visitors delta response: ${deltaMessage}`, e);
|
||||
}
|
||||
});
|
||||
|
||||
// Subscribe for the initial list after topic subscription is active
|
||||
const queueSubscription = this.stompClient.subscribe(queueEndpoint, message => {
|
||||
try {
|
||||
const visitors: Array<{ n: string; r: string; }> = JSON.parse(message.body);
|
||||
|
||||
logger.debug(`Received initial visitors list with ${visitors.length} visitors`);
|
||||
initialReceived = true;
|
||||
initialCallback(visitors);
|
||||
|
||||
queueSubscription.unsubscribe();
|
||||
|
||||
if (cachedDeltas.length) {
|
||||
deltaCallback(cachedDeltas);
|
||||
cachedDeltas.length = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(`Error parsing initial visitors response: ${message}`, e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.stompClient.activate();
|
||||
}
|
||||
}
|
||||
@@ -68,3 +68,14 @@ export const SET_VISITOR_DEMOTE_ACTOR = 'SET_VISITOR_DEMOTE_ACTOR';
|
||||
* }
|
||||
*/
|
||||
export const SET_VISITORS_SUPPORTED = 'SET_VISITORS_SUPPORTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current visitors list.
|
||||
*/
|
||||
export const UPDATE_VISITORS_LIST = 'UPDATE_VISITORS_LIST';
|
||||
|
||||
/**
|
||||
* Action dispatched when the visitors list is expanded for the first time
|
||||
* and the client should subscribe for updates.
|
||||
*/
|
||||
export const SUBSCRIBE_VISITORS_LIST = 'SUBSCRIBE_VISITORS_LIST';
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import logger from './logger';
|
||||
@@ -216,6 +218,32 @@ export function updateVisitorsInQueueCount(count: number) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current list of visitors.
|
||||
*
|
||||
* @param {Array<Object>} visitors - The visitors list.
|
||||
* @returns {{
|
||||
* type: UPDATE_VISITORS_LIST,
|
||||
* }}
|
||||
*/
|
||||
export function updateVisitorsList(visitors: Array<{ id: string; name: string; }>) {
|
||||
return {
|
||||
type: UPDATE_VISITORS_LIST,
|
||||
visitors
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the start of the visitors list websocket subscription.
|
||||
*
|
||||
* @returns {{ type: SUBSCRIBE_VISITORS_LIST }}
|
||||
*/
|
||||
export function subscribeVisitorsList() {
|
||||
return {
|
||||
type: SUBSCRIBE_VISITORS_LIST
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the overflow menu if opened.
|
||||
*
|
||||
@@ -227,6 +255,7 @@ export function goLive() {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
conference?.getMetadataHandler().setMetadata('visitors', {
|
||||
...(conference?.getMetadataHandler().getMetadata()?.visitors || {}),
|
||||
live: true
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
/**
|
||||
@@ -68,6 +70,26 @@ export function isVisitorsSupported(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].supported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current visitor list.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getVisitorsList(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitors ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the visitors list websocket subscription has been requested.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isVisitorsListSubscribed(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].visitorsListSubscribed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether visitor mode is live.
|
||||
*
|
||||
@@ -89,3 +111,31 @@ export function isVisitorsLive(stateful: IStateful) {
|
||||
export function showVisitorsQueue(stateful: IStateful) {
|
||||
return toState(stateful)['features/visitors'].inQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the visitors list feature is enabled based on JWT and config.js.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} Whether the feature is allowed.
|
||||
*/
|
||||
export function isVisitorsListEnabled(state: IReduxState): boolean {
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
|
||||
if (!visitorsConfig?.queueService) { // if the queue service is not configured, we can't retrieve the visitors list
|
||||
return false;
|
||||
}
|
||||
|
||||
return isJwtFeatureEnabled(state, MEET_FEATURES.LIST_VISITORS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current visitors list should be displayed.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store or {@code getState} function.
|
||||
* @returns {boolean} Whether the visitors list should be shown.
|
||||
*/
|
||||
export function shouldDisplayCurrentVisitorsList(stateful: IStateful): boolean {
|
||||
const state = toState(stateful);
|
||||
|
||||
return isVisitorsListEnabled(state) && getVisitorsCount(state) > 0;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IStore } from '../app/types';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
ENDPOINT_MESSAGE_RECEIVED,
|
||||
UPDATE_CONFERENCE_METADATA
|
||||
} from '../base/conference/actionTypes';
|
||||
@@ -35,7 +36,8 @@ import { INotificationProps } from '../notifications/types';
|
||||
import { open as openParticipantsPane } from '../participants-pane/actions';
|
||||
import { joinConference } from '../prejoin/actions';
|
||||
|
||||
import { UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import { VisitorsListWebsocketClient } from './VisitorsListWebsocketClient';
|
||||
import { SUBSCRIBE_VISITORS_LIST, UPDATE_VISITORS_IN_QUEUE_COUNT } from './actionTypes';
|
||||
import {
|
||||
approveRequest,
|
||||
clearPromotionRequest,
|
||||
@@ -45,10 +47,11 @@ import {
|
||||
setInVisitorsQueue,
|
||||
setVisitorDemoteActor,
|
||||
setVisitorsSupported,
|
||||
updateVisitorsInQueueCount
|
||||
updateVisitorsInQueueCount,
|
||||
updateVisitorsList
|
||||
} from './actions';
|
||||
import { JoinMeetingDialog } from './components';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount } from './functions';
|
||||
import { getPromotionRequests, getVisitorsInQueueCount, isVisitorsListEnabled } from './functions';
|
||||
import logger from './logger';
|
||||
import { WebsocketClient } from './websocket-client';
|
||||
|
||||
@@ -135,6 +138,17 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
WebsocketClient.getInstance().disconnect();
|
||||
VisitorsListWebsocketClient.getInstance().disconnect();
|
||||
break;
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
if (isVisitorsListEnabled(getState()) && !VisitorsListWebsocketClient.getInstance().isActive()) {
|
||||
_subscribeVisitorsList(getState, dispatch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ENDPOINT_MESSAGE_RECEIVED: {
|
||||
const { data } = action;
|
||||
|
||||
@@ -154,9 +168,9 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
const { hosts, visitors: visitorsConfig } = getState()['features/base/config'];
|
||||
const { locationURL, preferVisitor } = getState()['features/base/connection'];
|
||||
const { locationURL } = getState()['features/base/connection'];
|
||||
|
||||
if (!visitorsConfig?.queueService || !locationURL || !preferVisitor) {
|
||||
if (!visitorsConfig?.queueService || !locationURL) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -314,6 +328,71 @@ function _subscribeQueueStats(stateful: IStateful, dispatch: IStore['dispatch'])
|
||||
toState(stateful)['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes to the visitors list via WebSocket for real-time updates. This function establishes a WebSocket
|
||||
* connection to track visitors in a conference.
|
||||
*
|
||||
* @param {IStore.getState} getState - Function to retrieve the current Redux state.
|
||||
* @param {IStore.dispatch} dispatch - Function to dispatch Redux actions.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _subscribeVisitorsList(getState: IStore['getState'], dispatch: IStore['dispatch']) {
|
||||
const state = getState();
|
||||
const { visitors: visitorsConfig } = state['features/base/config'];
|
||||
const conference = state['features/base/conference'].conference;
|
||||
const meetingId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const participantId = localParticipant?.id;
|
||||
|
||||
if (!visitorsConfig?.queueService || !meetingId || !participantId) {
|
||||
logger.warn(`Missing required data for visitors list subscription', ${JSON.stringify({
|
||||
queueService: visitorsConfig?.queueService,
|
||||
meetingId,
|
||||
participantId: participantId ? 'participantId present' : 'participantId missing'
|
||||
})}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queueEndpoint = `/secured/conference/visitors-list/queue/${meetingId}/${participantId}`;
|
||||
const topicEndpoint = `/secured/conference/visitors-list/topic/${meetingId}`;
|
||||
|
||||
logger.debug('Starting visitors list subscription');
|
||||
|
||||
VisitorsListWebsocketClient.getInstance()
|
||||
.connectVisitorsList(
|
||||
`wss://${visitorsConfig.queueService}/visitors-list/websocket`,
|
||||
queueEndpoint,
|
||||
topicEndpoint,
|
||||
// Initial list callback - replace entire list
|
||||
initialVisitors => {
|
||||
const visitors = initialVisitors.map(v => ({ id: v.r, name: v.n }));
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
// Delta updates callback - apply incremental changes
|
||||
updates => {
|
||||
let visitors = [ ...(getState()['features/visitors'].visitors ?? []) ];
|
||||
|
||||
updates.forEach(u => {
|
||||
if (u.s === 'j') {
|
||||
const index = visitors.findIndex(v => v.id === u.r);
|
||||
|
||||
if (index === -1) {
|
||||
visitors.push({ id: u.r, name: u.n });
|
||||
} else {
|
||||
visitors[index] = { id: u.r, name: u.n };
|
||||
}
|
||||
} else if (u.s === 'l') {
|
||||
visitors = visitors.filter(v => v.id !== u.r);
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(updateVisitorsList(visitors));
|
||||
},
|
||||
getState()['features/base/jwt'].jwt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to handle the promotion notification.
|
||||
*
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
SET_IN_VISITORS_QUEUE,
|
||||
SET_VISITORS_SUPPORTED,
|
||||
SET_VISITOR_DEMOTE_ACTOR,
|
||||
SUBSCRIBE_VISITORS_LIST,
|
||||
UPDATE_VISITORS_IN_QUEUE_COUNT,
|
||||
UPDATE_VISITORS_LIST,
|
||||
VISITOR_PROMOTION_REQUEST
|
||||
} from './actionTypes';
|
||||
import { IPromotionRequest } from './types';
|
||||
import { IPromotionRequest, IVisitor } from './types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
count: 0,
|
||||
@@ -19,7 +21,9 @@ const DEFAULT_STATE = {
|
||||
inQueueCount: 0,
|
||||
showNotification: false,
|
||||
supported: false,
|
||||
promotionRequests: []
|
||||
promotionRequests: [],
|
||||
visitors: [] as IVisitor[],
|
||||
visitorsListSubscribed: false
|
||||
};
|
||||
|
||||
export interface IVisitorsState {
|
||||
@@ -30,6 +34,8 @@ export interface IVisitorsState {
|
||||
inQueueCount?: number;
|
||||
promotionRequests: IPromotionRequest[];
|
||||
supported: boolean;
|
||||
visitors: IVisitor[];
|
||||
visitorsListSubscribed: boolean;
|
||||
}
|
||||
ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_STATE, action): IVisitorsState => {
|
||||
switch (action.type) {
|
||||
@@ -90,6 +96,18 @@ ReducerRegistry.register<IVisitorsState>('features/visitors', (state = DEFAULT_S
|
||||
supported: action.value
|
||||
};
|
||||
}
|
||||
case SUBSCRIBE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitorsListSubscribed: true
|
||||
};
|
||||
}
|
||||
case UPDATE_VISITORS_LIST: {
|
||||
return {
|
||||
...state,
|
||||
visitors: action.visitors
|
||||
};
|
||||
}
|
||||
case VISITOR_PROMOTION_REQUEST: {
|
||||
const currentRequests = state.promotionRequests || [];
|
||||
|
||||
|
||||
@@ -2,3 +2,8 @@ export interface IPromotionRequest {
|
||||
from: string;
|
||||
nick: string;
|
||||
}
|
||||
|
||||
export interface IVisitor {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@ export interface VisitorResponse extends QueueServiceResponse {
|
||||
* Uses STOMP for authenticating (https://stomp.github.io/).
|
||||
*/
|
||||
export class WebsocketClient {
|
||||
private stompClient: Client | undefined;
|
||||
protected stompClient: Client | undefined;
|
||||
|
||||
private static instance: WebsocketClient;
|
||||
|
||||
private retriesCount = 0;
|
||||
protected retriesCount = 0;
|
||||
|
||||
private _connectCount = 0;
|
||||
|
||||
|
||||
@@ -127,7 +127,9 @@ Component 'conference.vX.meet.jitsi' 'muc'
|
||||
'muc_domain_mapper';
|
||||
'muc_meeting_id';
|
||||
'fmuc';
|
||||
's2soutinjection';
|
||||
's2s_bidi';
|
||||
's2s_whitelist';
|
||||
's2sout_override';
|
||||
}
|
||||
muc_room_default_presence_broadcast = {
|
||||
visitor = false;
|
||||
|
||||
@@ -5,6 +5,7 @@ local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local is_admin = util.is_admin;
|
||||
local array = require "util.array";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
@@ -151,6 +152,29 @@ function notify_jid_approved(jid, from, room, mediaType)
|
||||
send_json_message(jid, json_message);
|
||||
end
|
||||
|
||||
function start_av_moderation(room, mediaType, occupant)
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
@@ -208,26 +232,7 @@ function on_message(event)
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array{};
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
end
|
||||
else
|
||||
enabled = false;
|
||||
@@ -317,10 +322,30 @@ end
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- when first moderator joins if av_can_unmute from password preset is set to false, we enable av moderation for both
|
||||
-- audio and video, and set the first moderator as the actor that enabled it
|
||||
if room._data.av_can_unmute ~= nil
|
||||
and not room._data.av_first_moderator_joined
|
||||
|
||||
-- occupant.role is not reflecting the actual role after set_affiliation is used in same occupant_joined event
|
||||
and room:get_role(occupant.nick) == 'moderator' then
|
||||
|
||||
if not room._data.av_can_unmute then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
|
||||
notify_occupants_enable(nil, true, room, occupant.nick, mediaType);
|
||||
end
|
||||
|
||||
room._data.av_first_moderator_joined = true;
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
for _,mediaType in pairs({'audio', 'video'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
|
||||
@@ -130,6 +130,10 @@ function on_message(event)
|
||||
return true;
|
||||
end
|
||||
|
||||
if not room.jitsi_shared_files then
|
||||
return;
|
||||
end
|
||||
|
||||
room.jitsi_shared_files[message.attr.fileId] = nil;
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
|
||||
@@ -78,6 +78,8 @@ function process_set_affiliation(event)
|
||||
end
|
||||
occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group
|
||||
or actor_session.granted_jitsi_meet_context_group_id;
|
||||
-- even if token and features are set we may want to re-send permissions
|
||||
occupant_session.force_permissions_update = true;
|
||||
elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then
|
||||
occupant_session.granted_jitsi_meet_context_user_id = nil;
|
||||
occupant_session.granted_jitsi_meet_context_group_id = nil;
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
-- Component "metadata.jitmeet.example.com" "room_metadata_component"
|
||||
-- muc_component = "conference.jitmeet.example.com"
|
||||
-- breakout_rooms_component = "breakout.jitmeet.example.com"
|
||||
local array = require 'util.array';
|
||||
local filters = require 'util.filters';
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local json = require 'cjson.safe';
|
||||
local json = require 'util.json';
|
||||
local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
|
||||
@@ -78,24 +79,25 @@ function send_metadata(occupant, room, json_msg)
|
||||
|
||||
-- we want to send the main meeting participants only to jicofo
|
||||
if is_admin(occupant.bare_jid) then
|
||||
local participants = {};
|
||||
local participants;
|
||||
local moderators = array();
|
||||
|
||||
if room._data.mainMeetingParticipants then
|
||||
table_add(participants, room._data.mainMeetingParticipants);
|
||||
if room._data.participants then
|
||||
participants = array();
|
||||
participants:append(room._data.participants);
|
||||
end
|
||||
|
||||
if room._data.moderator_id then
|
||||
table.insert(participants, room._data.moderator_id);
|
||||
moderators:push(room._data.moderator_id);
|
||||
end
|
||||
|
||||
if room._data.moderators then
|
||||
table_add(participants, room._data.moderators);
|
||||
moderators:append(room._data.moderators);
|
||||
end
|
||||
|
||||
if #participants > 0 then
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.mainMeetingParticipants = participants;
|
||||
end
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.participants = participants;
|
||||
metadata_to_send.moderators = moderators;
|
||||
end
|
||||
|
||||
json_msg = getMetadataJSON(room, metadata_to_send);
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
-- Using version https://hg.prosody.im/prosody-modules/file/4fb922aa0ace/mod_s2soutinjection/mod_s2soutinjection.lua
|
||||
local st = require"util.stanza";
|
||||
local new_outgoing = require"core.s2smanager".new_outgoing;
|
||||
local bounce_sendq = module:depends"s2s".route_to_new_session.bounce_sendq;
|
||||
local initialize_filters = require "util.filters".initialize;
|
||||
|
||||
local portmanager = require "core.portmanager";
|
||||
|
||||
local addclient = require "net.server".addclient;
|
||||
|
||||
module:depends("s2s");
|
||||
|
||||
local sessions = module:shared("sessions");
|
||||
|
||||
local injected = module:get_option("s2s_connect_overrides");
|
||||
|
||||
-- The proxy_listener handles connection while still connecting to the proxy,
|
||||
-- then it hands them over to the normal listener (in mod_s2s)
|
||||
local proxy_listener = { default_port = nil, default_mode = "*a", default_interface = "*" };
|
||||
|
||||
function proxy_listener.onconnect(conn)
|
||||
local session = sessions[conn];
|
||||
|
||||
-- needed in mod_rate_limit
|
||||
session.ip = conn:ip();
|
||||
|
||||
-- Now the real s2s listener can take over the connection.
|
||||
local listener = portmanager.get_service("s2s").listener;
|
||||
|
||||
local log = session.log;
|
||||
|
||||
local filter = initialize_filters(session);
|
||||
|
||||
session.version = 1;
|
||||
|
||||
session.sends2s = function (t)
|
||||
-- log("debug", "sending (s2s over proxy): %s", (t.top_tag and t:top_tag()) or t:match("^[^>]*>?"));
|
||||
if t.name then
|
||||
t = filter("stanzas/out", t);
|
||||
end
|
||||
if t then
|
||||
t = filter("bytes/out", tostring(t));
|
||||
if t then
|
||||
return conn:write(tostring(t));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
session.open_stream = function ()
|
||||
session.sends2s(st.stanza("stream:stream", {
|
||||
xmlns='jabber:server', ["xmlns:db"]='jabber:server:dialback',
|
||||
["xmlns:stream"]='http://etherx.jabber.org/streams',
|
||||
from=session.from_host, to=session.to_host, version='1.0', ["xml:lang"]='en'}):top_tag());
|
||||
end
|
||||
|
||||
conn.setlistener(conn, listener);
|
||||
|
||||
listener.register_outgoing(conn, session);
|
||||
|
||||
listener.onconnect(conn);
|
||||
end
|
||||
|
||||
function proxy_listener.register_outgoing(conn, session)
|
||||
session.direction = "outgoing";
|
||||
sessions[conn] = session;
|
||||
end
|
||||
|
||||
function proxy_listener.ondisconnect(conn, err)
|
||||
sessions[conn] = nil;
|
||||
end
|
||||
|
||||
module:hook("route/remote", function(event)
|
||||
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
|
||||
local inject = injected and injected[to_host];
|
||||
if not inject then return end
|
||||
-- module:log("debug", "opening a new outgoing connection for this stanza");
|
||||
local host_session = new_outgoing(from_host, to_host);
|
||||
|
||||
-- Store in buffer
|
||||
host_session.bounce_sendq = bounce_sendq;
|
||||
host_session.sendq = { {tostring(stanza), stanza.attr.type ~= "error" and stanza.attr.type ~= "result" and st.reply(stanza)} };
|
||||
-- host_session.log("debug", "stanza [%s] queued until connection complete", tostring(stanza.name));
|
||||
|
||||
local host, port = inject[1] or inject, tonumber(inject[2]) or 5269;
|
||||
|
||||
local conn = addclient(host, port, proxy_listener, "*a");
|
||||
|
||||
proxy_listener.register_outgoing(conn, host_session);
|
||||
|
||||
host_session.conn = conn;
|
||||
return true;
|
||||
end, -2);
|
||||
|
||||
@@ -23,6 +23,9 @@ local new_id = require 'util.id'.medium;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
@@ -63,6 +66,17 @@ local visitors_promotion_requests = {};
|
||||
local cache = require 'util.cache';
|
||||
local sent_iq_cache = cache.new(200);
|
||||
|
||||
-- Function to get visitors room metadata
|
||||
local function get_visitors_room_metadata(room)
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
if not room.jitsiMetadata.visitors then
|
||||
room.jitsiMetadata.visitors = {};
|
||||
end
|
||||
return room.jitsiMetadata.visitors;
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
@@ -73,9 +87,14 @@ function send_json_message(to_jid, json_message)
|
||||
end
|
||||
|
||||
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, group_id, force_promote_requested)
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received promotion request from %s for room %s, nick: %s, time: %s, user_id: %s, group_id: %s, force_promote_requested: %s',
|
||||
from_jid, room.jid, nick, time, user_id, group_id, force_promote_requested);
|
||||
end
|
||||
|
||||
-- if visitors is enabled for the room
|
||||
if visitors_promotion_map[room.jid] then
|
||||
local force_promote = auto_allow_promotion;
|
||||
local force_promote = auto_allow_promotion or get_visitors_room_metadata(room).autoPromote;
|
||||
if not force_promote and force_promote_requested == 'true' then
|
||||
-- Let's do the force_promote checks if requested
|
||||
-- if it is vpaas meeting we trust the moderator computation from visitor node (value of force_promote_requested)
|
||||
@@ -231,16 +250,6 @@ function get_visitors_languages(room)
|
||||
return count, languages:sort():concat(',');
|
||||
end
|
||||
|
||||
local function get_visitors_room_metadata(room)
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
if not room.jitsiMetadata.visitors then
|
||||
room.jitsiMetadata.visitors = {};
|
||||
end
|
||||
return room.jitsiMetadata.visitors;
|
||||
end
|
||||
|
||||
-- listens for iq request for promotion and forward it to moderators in the meeting for approval
|
||||
-- or auto-allow it if such the config is set enabling it
|
||||
local function stanza_handler(event)
|
||||
@@ -250,6 +259,10 @@ local function stanza_handler(event)
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received stanza %s from %s', stanza, origin.full_jid);
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
@@ -367,6 +380,11 @@ local function process_promotion_response(room, id, approved)
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Processing promotion response for room %s, id %s, approved %s',
|
||||
room.jid, id, approved);
|
||||
end
|
||||
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
@@ -400,13 +418,24 @@ end
|
||||
-- if room metadata does not have visitors.live set to `true` and there are no occupants in the meeting
|
||||
-- it will skip calling goLive endpoint
|
||||
local function go_live(room)
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Checking if room %s is live', room.jid);
|
||||
end
|
||||
|
||||
if room._jitsi_go_live_sent then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s already sent go live request, skipping', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
-- if missing we assume room is live, only skip if it is marked explicitly as false
|
||||
if room.jitsiMetadata and room.jitsiMetadata.visitors
|
||||
and room.jitsiMetadata.visitors.live ~= nil and room.jitsiMetadata.visitors.live == false then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s is not live, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -420,6 +449,9 @@ local function go_live(room)
|
||||
|
||||
-- when there is an occupant then go live
|
||||
if not has_occupant then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s has no occupants, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -461,6 +493,10 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
local room, stanza, occupant, session = event.room, event.stanza, event.occupant, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s',
|
||||
room.jid, occupant.bare_jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -472,12 +508,20 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
|
||||
-- we skip any checks when auto-allow is enabled
|
||||
if auto_allow_promotion
|
||||
local is_live = get_visitors_room_metadata(room).live;
|
||||
|
||||
-- we skip any checks when auto-allow is enabled and room is live
|
||||
if (auto_allow_promotion or get_visitors_room_metadata(room).autoPromote and (is_live or is_live == nil))
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
|
||||
or is_sip_jigasi(stanza)
|
||||
or is_sip_jibri_join(stanza) then
|
||||
return;
|
||||
or is_sip_jibri_join(stanza)
|
||||
or table_find(room._data.moderators, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id)
|
||||
or (room._data.moderator_id and room._data.moderator_id == (session.jitsi_meet_context_user and session.jitsi_meet_context_user.id))
|
||||
or table_find(room._data.participants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Auto-allowing visitor %s in room %s', stanza.attr.from, room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] then
|
||||
@@ -514,6 +558,16 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
:tag('no-main-participants', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
elseif room._data.participants then
|
||||
-- This is non jaas room which has a list of participants allowed to participate in the main room
|
||||
-- but this occupant is not one of them and the room is either not live or has no participants joined
|
||||
session.log('warn',
|
||||
'Deny user join in the main not live meeting, not in the list of main participants');
|
||||
session.send(st.error_reply(
|
||||
stanza, 'cancel', 'not-allowed',
|
||||
'Tried to join the main (not live or without main participants) room')
|
||||
:tag('not-live-room', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
|
||||
end, 7); -- after muc_meeting_id, the logic for not joining before jicofo
|
||||
@@ -525,8 +579,16 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Occupant %s joined room %s', occupant.jid, room.jid);
|
||||
end
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) or occupant.role ~= 'moderator' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s or not moderator %s',
|
||||
room.jid, occupant.bare_jid, occupant.role);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
@@ -652,6 +714,13 @@ process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_modul
|
||||
host_module:context(host):hook("muc-config-form", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
room.jitsiMetadata.visitorsEnabled = true;
|
||||
module:context(muc_domain_prefix..'.'..muc_domain_base)
|
||||
:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
|
||||
|
||||
@@ -597,7 +597,7 @@ function table_shallow_copy(t)
|
||||
end
|
||||
|
||||
local function table_find(tab, val)
|
||||
if not tab then
|
||||
if not tab or val == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
@@ -300,7 +300,11 @@ export class Participant {
|
||||
*/
|
||||
async waitForPageToLoad(): Promise<boolean> {
|
||||
return this.driver.waitUntil(
|
||||
() => this.execute(() => document.readyState === 'complete'),
|
||||
() => this.execute(() => {
|
||||
console.log(`${new Date().toISOString()} document.readyState: ${document.readyState}`);
|
||||
|
||||
return document.readyState === 'complete';
|
||||
}),
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
|
||||
|
||||
@@ -5,6 +5,13 @@ import { ensureTwoParticipants } from '../../helpers/participants';
|
||||
describe('URL Normalisation', () => {
|
||||
it('joining the meeting', async () => {
|
||||
|
||||
// if we are running with token this becomes ugly to match the URL
|
||||
if (process.env.JWT_ACCESS_TOKEN) {
|
||||
ctx.skipSuiteTests = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// a hack to extract the baseUrl that the test will use
|
||||
const baseUrl = multiremotebrowser.getInstance('p1').options.baseUrl;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user