mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-18 10:47:48 +00:00
Compare commits
114 Commits
8620
...
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 | ||
|
|
bd612ef8ea | ||
|
|
b93c69c24e | ||
|
|
cc6326a23f | ||
|
|
d6776f234d | ||
|
|
2763c4fdee | ||
|
|
2aaf0ed543 | ||
|
|
17f335f0c9 | ||
|
|
e280d1d963 | ||
|
|
a43472985b | ||
|
|
e5187de9c3 | ||
|
|
b296776ed7 | ||
|
|
93bc4019ad | ||
|
|
a4c20469cd | ||
|
|
196fd455cd | ||
|
|
e75bacae4a | ||
|
|
d238386085 | ||
|
|
a1634eb813 | ||
|
|
566d76a28d | ||
|
|
494afde82a | ||
|
|
51ba5d31aa | ||
|
|
5f88b117ae | ||
|
|
4500a5aba5 | ||
|
|
fb64d1b68b | ||
|
|
a39d8d35a2 | ||
|
|
6bc12766f9 | ||
|
|
ecf9bee7d0 | ||
|
|
6b5245be44 | ||
|
|
6b71122cac | ||
|
|
f060ab9d26 | ||
|
|
df3dd2b14e | ||
|
|
be9e1136de | ||
|
|
e2337e8db8 | ||
|
|
293b6fa908 | ||
|
|
34da0ff99e | ||
|
|
06713a4ffa | ||
|
|
94813163e8 | ||
|
|
bb1eade1f0 | ||
|
|
2f0ca6c839 | ||
|
|
45bbf06a85 | ||
|
|
042007adb5 | ||
|
|
dc73d1e328 | ||
|
|
db4c9666c3 | ||
|
|
45cfc0e112 | ||
|
|
fa9aab953d | ||
|
|
c16f652378 | ||
|
|
6f3ae47a2e | ||
|
|
6afbff9b36 | ||
|
|
c7b6af1df3 | ||
|
|
308faf71bf | ||
|
|
78efddc447 | ||
|
|
d370e79237 | ||
|
|
97b01b910e | ||
|
|
3bb28c4090 | ||
|
|
589baab2ae | ||
|
|
85fe5cf31e | ||
|
|
7fabb33733 | ||
|
|
5342712019 | ||
|
|
deb0e36f84 | ||
|
|
65e3886d43 | ||
|
|
337435e738 | ||
|
|
7a1f5885d0 | ||
|
|
253f43ab9e | ||
|
|
f17a4387d9 | ||
|
|
7ac43abd03 | ||
|
|
67b44f4406 | ||
|
|
7f601db349 | ||
|
|
26423f8e76 |
@@ -83,10 +83,8 @@ dependencies {
|
||||
|
||||
// Firebase
|
||||
// - Crashlytics
|
||||
// - Dynamic Links
|
||||
implementation 'com.google.firebase:firebase-analytics:21.3.0'
|
||||
implementation 'com.google.firebase:firebase-crashlytics:18.4.3'
|
||||
implementation 'com.google.firebase:firebase-dynamic-links:21.1.0'
|
||||
}
|
||||
|
||||
implementation project(':sdk')
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics;
|
||||
import com.google.firebase.dynamiclinks.FirebaseDynamicLinks;
|
||||
|
||||
import org.jitsi.meet.sdk.JitsiMeet;
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivity;
|
||||
@@ -22,18 +21,6 @@ final class GoogleServicesHelper {
|
||||
Log.d(activity.getClass().getSimpleName(), "Initializing Google Services");
|
||||
|
||||
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!JitsiMeet.isCrashReportingDisabled(activity));
|
||||
FirebaseDynamicLinks.getInstance().getDynamicLink(activity.getIntent())
|
||||
.addOnSuccessListener(activity, pendingDynamicLinkData -> {
|
||||
Uri dynamicLink = null;
|
||||
|
||||
if (pendingDynamicLinkData != null) {
|
||||
dynamicLink = pendingDynamicLinkData.getLink();
|
||||
}
|
||||
|
||||
if (dynamicLink != null) {
|
||||
activity.join(dynamicLink.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,6 @@ dependencies {
|
||||
api "com.facebook.react:react-android:$rootProject.ext.rnVersion"
|
||||
api "com.facebook.react:hermes-android:$rootProject.ext.rnVersion"
|
||||
|
||||
//noinspection GradleDynamicVersion
|
||||
implementation 'org.webkit:android-jsc:+'
|
||||
|
||||
implementation 'com.facebook.fresco:animated-gif:2.5.0'
|
||||
implementation 'com.dropbox.core:dropbox-core-sdk:4.0.1'
|
||||
implementation 'com.jakewharton.timber:timber:5.0.1'
|
||||
|
||||
@@ -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'
|
||||
|
||||
122
conference.js
122
conference.js
@@ -89,7 +89,7 @@ import {
|
||||
setVideoMuted,
|
||||
setVideoUnmutePermissions
|
||||
} from './react/features/base/media/actions';
|
||||
import { MEDIA_TYPE, VIDEO_TYPE } from './react/features/base/media/constants';
|
||||
import { MEDIA_TYPE, VIDEO_MUTISM_AUTHORITY, VIDEO_TYPE } from './react/features/base/media/constants';
|
||||
import {
|
||||
getStartWithAudioMuted,
|
||||
getStartWithVideoMuted,
|
||||
@@ -205,23 +205,6 @@ function sendData(command, value) {
|
||||
room.sendCommand(command, { value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute or unmute local audio stream if it exists.
|
||||
* @param {boolean} muted - if audio stream should be muted or unmuted.
|
||||
*/
|
||||
function muteLocalAudio(muted) {
|
||||
APP.store.dispatch(setAudioMuted(muted));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mute or unmute local video stream if it exists.
|
||||
* @param {boolean} muted if video stream should be muted or unmuted.
|
||||
*
|
||||
*/
|
||||
function muteLocalVideo(muted) {
|
||||
APP.store.dispatch(setVideoMuted(muted));
|
||||
}
|
||||
|
||||
/**
|
||||
* A queue for the async replaceLocalTrack action so that multiple audio
|
||||
* replacements cannot happen simultaneously. This solves the issue where
|
||||
@@ -708,11 +691,10 @@ export default {
|
||||
* Simulates toolbar button click for audio mute. Used by shortcuts and API.
|
||||
*
|
||||
* @param {boolean} mute true for mute and false for unmute.
|
||||
* @param {boolean} [showUI] when set to false will not display any error
|
||||
* dialogs in case of media permissions error.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async muteAudio(mute, showUI = true) {
|
||||
async muteAudio(mute) {
|
||||
const state = APP.store.getState();
|
||||
|
||||
if (!mute
|
||||
@@ -731,47 +713,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// Not ready to modify track's state yet
|
||||
if (!this._localTracksInitialized) {
|
||||
// This will only modify base/media.audio.muted which is then synced
|
||||
// up with the track at the end of local tracks initialization.
|
||||
muteLocalAudio(mute);
|
||||
this.updateAudioIconEnabled();
|
||||
|
||||
return;
|
||||
} else if (this.isLocalAudioMuted() === mute) {
|
||||
// NO-OP
|
||||
return;
|
||||
}
|
||||
|
||||
const localAudio = getLocalJitsiAudioTrack(APP.store.getState());
|
||||
|
||||
if (!localAudio && !mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.store.dispatch(notifyMicError(error));
|
||||
};
|
||||
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
await createLocalTracksF({ devices: [ 'audio' ] })
|
||||
.then(([ audioTrack ]) => audioTrack)
|
||||
.catch(error => {
|
||||
maybeShowErrorDialog(error);
|
||||
|
||||
// Rollback the audio muted status by using null track
|
||||
return null;
|
||||
})
|
||||
.then(async audioTrack => {
|
||||
await this._maybeApplyAudioMixerEffect(audioTrack);
|
||||
|
||||
return this.useAudioStream(audioTrack);
|
||||
})
|
||||
.finally(() => {
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.AUDIO ], IGUMPendingState.NONE));
|
||||
});
|
||||
} else {
|
||||
muteLocalAudio(mute);
|
||||
}
|
||||
await APP.store.dispatch(setAudioMuted(mute, true));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -801,10 +743,9 @@ export default {
|
||||
/**
|
||||
* Simulates toolbar button click for video mute. Used by shortcuts and API.
|
||||
* @param mute true for mute and false for unmute.
|
||||
* @param {boolean} [showUI] when set to false will not display any error
|
||||
* dialogs in case of media permissions error.
|
||||
*/
|
||||
muteVideo(mute, showUI = true) {
|
||||
muteVideo(mute) {
|
||||
if (this.videoSwitchInProgress) {
|
||||
logger.warn('muteVideo - unable to perform operations while video switch is in progress');
|
||||
|
||||
@@ -825,60 +766,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
// If not ready to modify track's state yet adjust the base/media
|
||||
if (!this._localTracksInitialized) {
|
||||
// This will only modify base/media.video.muted which is then synced
|
||||
// up with the track at the end of local tracks initialization.
|
||||
muteLocalVideo(mute);
|
||||
this.setVideoMuteStatus();
|
||||
|
||||
return;
|
||||
} else if (this.isLocalVideoMuted() === mute) {
|
||||
// NO-OP
|
||||
return;
|
||||
}
|
||||
|
||||
const localVideo = getLocalJitsiVideoTrack(state);
|
||||
|
||||
if (!localVideo && !mute && !this.isCreatingLocalTrack) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
this.isCreatingLocalTrack = true;
|
||||
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
// Try to create local video if there wasn't any.
|
||||
// This handles the case when user joined with no video
|
||||
// (dismissed screen sharing screen or in audio only mode), but
|
||||
// decided to add it later on by clicking on muted video icon or
|
||||
// turning off the audio only mode.
|
||||
//
|
||||
// FIXME when local track creation is moved to react/redux
|
||||
// it should take care of the use case described above
|
||||
createLocalTracksF({ devices: [ 'video' ] })
|
||||
.then(([ videoTrack ]) => videoTrack)
|
||||
.catch(error => {
|
||||
// FIXME should send some feedback to the API on error ?
|
||||
maybeShowErrorDialog(error);
|
||||
|
||||
// Rollback the video muted status by using null track
|
||||
return null;
|
||||
})
|
||||
.then(videoTrack => {
|
||||
logger.debug(`muteVideo: calling useVideoStream for track: ${videoTrack}`);
|
||||
|
||||
return this.useVideoStream(videoTrack);
|
||||
})
|
||||
.finally(() => {
|
||||
this.isCreatingLocalTrack = false;
|
||||
APP.store.dispatch(gumPending([ MEDIA_TYPE.VIDEO ], IGUMPendingState.NONE));
|
||||
});
|
||||
} else {
|
||||
// FIXME show error dialog if it fails (should be handled by react)
|
||||
muteLocalVideo(mute);
|
||||
}
|
||||
APP.store.dispatch(setVideoMuted(mute, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
67
config.js
67
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.
|
||||
@@ -509,7 +514,11 @@ var config = {
|
||||
// // Disables everything related to closed captions - the tab in the chat area, the button in the menu,
|
||||
// // subtitles on stage and the "Show subtitles on stage" checkbox in the settings.
|
||||
// // Note: Starting transcriptions from the recording dialog will still work.
|
||||
// disableClosedCaptions: false
|
||||
// disableClosedCaptions: false,
|
||||
|
||||
// // Whether to invite jigasi when backend transcriptions are enabled (asyncTranscription is true in metadata).
|
||||
// // By default, we invite it.
|
||||
// inviteJigasiOnBackendTranscribing: true,
|
||||
// },
|
||||
|
||||
// Misc
|
||||
@@ -839,8 +848,7 @@ var config = {
|
||||
// some other values in config.js to be enabled. Also, the "profile" button will
|
||||
// not display for users with a JWT.
|
||||
// Notes:
|
||||
// - it's impossible to choose which buttons go in the "More actions" menu
|
||||
// - it's impossible to control the placement of buttons
|
||||
// - it's possible to reorder the buttons in the maintoolbar by changing the order of the mainToolbarButtons
|
||||
// - 'desktop' controls the "Share your screen" button
|
||||
// - if `toolbarButtons` is undefined, we fallback to enabling all buttons on the UI
|
||||
// toolbarButtons: [
|
||||
@@ -1123,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,
|
||||
@@ -1270,9 +1274,6 @@ var config = {
|
||||
// disableDeepLinking: false,
|
||||
|
||||
// The deeplinking config.
|
||||
// For information about the properties of
|
||||
// deeplinking.[ios/android].dynamicLink check:
|
||||
// https://firebase.google.com/docs/dynamic-links/create-manually
|
||||
// deeplinking: {
|
||||
//
|
||||
// // The desktop deeplinking config, disabled by default.
|
||||
@@ -1301,13 +1302,6 @@ var config = {
|
||||
// appScheme: 'org.jitsi.meet',
|
||||
// // Custom URL for downloading ios mobile app.
|
||||
// downloadLink: 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905',
|
||||
// dynamicLink: {
|
||||
// apn: 'org.jitsi.meet',
|
||||
// appCode: 'w2atb',
|
||||
// customDomain: undefined,
|
||||
// ibi: 'com.atlassian.JitsiMeet.ios',
|
||||
// isi: '1165103905'
|
||||
// }
|
||||
// },
|
||||
|
||||
// // The android deeplinking config.
|
||||
@@ -1320,13 +1314,6 @@ var config = {
|
||||
// // Android app package name.
|
||||
// appPackage: 'org.jitsi.meet',
|
||||
// fDroidUrl: 'https://f-droid.org/en/packages/org.jitsi.meet/',
|
||||
// dynamicLink: {
|
||||
// apn: 'org.jitsi.meet',
|
||||
// appCode: 'w2atb',
|
||||
// customDomain: undefined,
|
||||
// ibi: 'com.atlassian.JitsiMeet.ios',
|
||||
// isi: '1165103905'
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
|
||||
@@ -1374,18 +1361,11 @@ 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',
|
||||
// },
|
||||
|
||||
// Endpoint that enables support for salesforce integration with in-meeting resource linking
|
||||
// This is required for:
|
||||
// listing the most recent records - salesforceUrl/records/recents
|
||||
// searching records - salesforceUrl/records?text=${text}
|
||||
// retrieving record details - salesforceUrl/records/${id}?type=${type}
|
||||
// and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
|
||||
//
|
||||
// salesforceUrl: 'https://api.example.com/',
|
||||
|
||||
// If set to true all muting operations of remote participants will be disabled.
|
||||
// disableRemoteMute: true,
|
||||
@@ -1410,6 +1390,13 @@ var config = {
|
||||
logoClickUrl: 'https://example-company.org',
|
||||
// The url used for the image used as logo
|
||||
logoImageUrl: 'https://example.com/logo-img.png',
|
||||
// Endpoint that enables support for salesforce integration with in-meeting resource linking
|
||||
// This is required for:
|
||||
// listing the most recent records - salesforceUrl/records/recents
|
||||
// searching records - salesforceUrl/records?text=${text}
|
||||
// retrieving record details - salesforceUrl/records/${id}?type=${type}
|
||||
// and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
|
||||
// salesforceUrl: 'https://api.example.com/',
|
||||
// Overwrite for pool of background images for avatars
|
||||
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
|
||||
// The lobby/prejoin screen background
|
||||
@@ -1591,6 +1578,9 @@ var config = {
|
||||
// tokenAuthUrlAutoRedirect: false
|
||||
// An option to respect the context.tenant jwt field compared to the current tenant from the url
|
||||
// tokenRespectTenant: false,
|
||||
// An option to get for user info (name, picture, email) in the token outside the user context.
|
||||
// Can be used with Firebase tokens.
|
||||
// tokenGetUserInfoOutOfContext: false,
|
||||
|
||||
// You can put an array of values to target different entity types in the invite dialog.
|
||||
// Valid values are "phone", "room", "sip", "user", "videosipgw" and "email"
|
||||
@@ -1790,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.
|
||||
@@ -1898,6 +1895,8 @@ var config = {
|
||||
// apiUrl: 'https://example.com',
|
||||
// // Whether the file sharing service is enabled or not.
|
||||
// enabled: true,
|
||||
// // Maximum file size limit (-1 value disables any file size limit check)
|
||||
// maxFileSize: 50,
|
||||
// },
|
||||
};
|
||||
|
||||
|
||||
@@ -58,28 +58,21 @@ VirtualHost "jitmeet.example.com"
|
||||
key = "/etc/prosody/certs/jitmeet.example.com.key";
|
||||
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
|
||||
}
|
||||
av_moderation_component = "avmoderation.jitmeet.example.com"
|
||||
speakerstats_component = "speakerstats.jitmeet.example.com"
|
||||
end_conference_component = "endconference.jitmeet.example.com"
|
||||
-- we need bosh
|
||||
modules_enabled = {
|
||||
"bosh";
|
||||
"websocket";
|
||||
"smacks";
|
||||
"ping"; -- Enable mod_ping
|
||||
"speakerstats";
|
||||
"external_services";
|
||||
"features_identity";
|
||||
"conference_duration";
|
||||
"end_conference";
|
||||
"muc_lobby_rooms";
|
||||
"muc_breakout_rooms";
|
||||
"av_moderation";
|
||||
"room_metadata";
|
||||
}
|
||||
c2s_require_encryption = false
|
||||
lobby_muc = "lobby.jitmeet.example.com"
|
||||
breakout_rooms_muc = "breakout.jitmeet.example.com"
|
||||
room_metadata_component = "metadata.jitmeet.example.com"
|
||||
main_muc = "conference.jitmeet.example.com"
|
||||
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
|
||||
|
||||
@@ -155,6 +148,9 @@ Component "endconference.jitmeet.example.com" "end_conference"
|
||||
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
Component "filesharing.jitmeet.example.com" "filesharing_component"
|
||||
muc_component = "conference.jitmeet.example.com"
|
||||
|
||||
Component "lobby.jitmeet.example.com" "muc"
|
||||
storage = "memory"
|
||||
restrict_room_creation = true
|
||||
|
||||
@@ -192,17 +192,6 @@ var interfaceConfig = {
|
||||
|
||||
// NATIVE_APP_NAME: 'Jitsi Meet',
|
||||
|
||||
/**
|
||||
* Specify Firebase dynamic link properties for the mobile apps.
|
||||
*/
|
||||
// MOBILE_DYNAMIC_LINK: {
|
||||
// APN: 'org.jitsi.meet',
|
||||
// APP_CODE: 'w2atb',
|
||||
// CUSTOM_DOMAIN: undefined,
|
||||
// IBI: 'com.atlassian.JitsiMeet.ios',
|
||||
// ISI: '1165103905'
|
||||
// },
|
||||
|
||||
/**
|
||||
* Hide the logo on the deep linking pages.
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,6 @@ target 'JitsiMeet' do
|
||||
|
||||
pod 'Firebase/Analytics', '~> 8.0'
|
||||
pod 'Firebase/Crashlytics', '~> 8.0'
|
||||
pod 'Firebase/DynamicLinks', '~> 8.0'
|
||||
end
|
||||
|
||||
target 'JitsiMeetSDK' do
|
||||
|
||||
@@ -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):
|
||||
@@ -27,9 +23,6 @@ PODS:
|
||||
- Firebase/Crashlytics (8.15.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseCrashlytics (~> 8.15.0)
|
||||
- Firebase/DynamicLinks (8.15.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseDynamicLinks (~> 8.15.0)
|
||||
- FirebaseAnalytics (8.15.0):
|
||||
- FirebaseAnalytics/AdIdSupport (= 8.15.0)
|
||||
- FirebaseCore (~> 8.0)
|
||||
@@ -64,8 +57,6 @@ PODS:
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (~> 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- FirebaseDynamicLinks (8.15.0):
|
||||
- FirebaseCore (~> 8.0)
|
||||
- FirebaseInstallations (8.15.0):
|
||||
- FirebaseCore (~> 8.0)
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
@@ -141,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)
|
||||
@@ -1791,14 +1782,13 @@ 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`)
|
||||
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
|
||||
- Firebase/Analytics (~> 8.0)
|
||||
- Firebase/Crashlytics (~> 8.0)
|
||||
- Firebase/DynamicLinks (~> 8.0)
|
||||
- fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
|
||||
- "giphy-react-native-sdk (from `../node_modules/@giphy/react-native-sdk`)"
|
||||
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
|
||||
@@ -1889,8 +1879,6 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Amplitude
|
||||
- AnalyticsConnector
|
||||
- AppAuth
|
||||
- CocoaLumberjack
|
||||
- Firebase
|
||||
@@ -1898,7 +1886,6 @@ SPEC REPOS:
|
||||
- FirebaseCore
|
||||
- FirebaseCoreDiagnostics
|
||||
- FirebaseCrashlytics
|
||||
- FirebaseDynamicLinks
|
||||
- FirebaseInstallations
|
||||
- Giphy
|
||||
- GoogleAppMeasurement
|
||||
@@ -1917,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:
|
||||
@@ -2095,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
|
||||
@@ -2108,7 +2093,6 @@ SPEC CHECKSUMS:
|
||||
FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1
|
||||
FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb
|
||||
FirebaseCrashlytics: feb07e4e9187be3c23c6a846cce4824e5ce2dd0b
|
||||
FirebaseDynamicLinks: 1dc816ef789c5adac6fede0b46d11478175c70e4
|
||||
FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
|
||||
fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
|
||||
Giphy: 83628960ed04e1c3428ff1b4fb2b027f65e82f50
|
||||
@@ -2121,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
|
||||
@@ -2209,6 +2193,6 @@ SPEC CHECKSUMS:
|
||||
SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d
|
||||
Yoga: 1dd9dabb9df8fe08f12cd522eae04a2da0e252eb
|
||||
|
||||
PODFILE CHECKSUM: 4f6abcf3cec0d9e8e1d5f5d81a35d99adde9ae45
|
||||
PODFILE CHECKSUM: 8ff981f2324fa4b013b6690257898dcd2be9d3c3
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -539,7 +539,7 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if test \"$PRODUCT_BUNDLE_IDENTIFIER\" = \"com.atlassian.JitsiMeet.ios\"; then\n ENTITLEMENTS_PLIST=\"$PROJECT_DIR/app.entitlements\"\n \n /usr/libexec/PlistBuddy -c \"Add :com.apple.developer.avfoundation.multitasking-camera-access bool 1\" $ENTITLEMENTS_PLIST\nfi\n";
|
||||
shellScript = "#if test \"$PRODUCT_BUNDLE_IDENTIFIER\" = \"com.atlassian.JitsiMeet.ios\"; then\n# ENTITLEMENTS_PLIST=\"$PROJECT_DIR/app.entitlements\"\n# \n# /usr/libexec/PlistBuddy -c \"Add :com.apple.developer.avfoundation.multitasking-camera-access bool 1\" $ENTITLEMENTS_PLIST\n#fi\n";
|
||||
};
|
||||
69BC5020DBE393B56BD76636 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
|
||||
@@ -54,19 +54,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
// MARK: Linking delegate methods
|
||||
|
||||
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
||||
if self.appContainsRealServiceInfoPlist() {
|
||||
let handled = DynamicLinks.dynamicLinks().handleUniversalLink(userActivity.webpageURL!) { dynamicLink, error in
|
||||
if let firebaseUrl = self.extractURL(from: dynamicLink) {
|
||||
userActivity.webpageURL = firebaseUrl
|
||||
JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
}
|
||||
|
||||
if handled {
|
||||
return handled
|
||||
}
|
||||
}
|
||||
|
||||
return JitsiMeet.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
|
||||
@@ -75,16 +62,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return false
|
||||
}
|
||||
|
||||
var openUrl = url
|
||||
|
||||
if self.appContainsRealServiceInfoPlist() {
|
||||
if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url),
|
||||
let firebaseUrl = self.extractURL(from: dynamicLink) {
|
||||
openUrl = firebaseUrl
|
||||
}
|
||||
}
|
||||
|
||||
return JitsiMeet.sharedInstance().application(app, open: openUrl, options: options)
|
||||
return JitsiMeet.sharedInstance().application(app, open: url, options: options)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
@@ -97,13 +75,4 @@ extension AppDelegate {
|
||||
func appContainsRealServiceInfoPlist() -> Bool {
|
||||
return InfoPlistUtil.containsRealServiceInfoPlist(in: Bundle.main)
|
||||
}
|
||||
|
||||
func extractURL(from dynamicLink: DynamicLink?) -> URL? {
|
||||
guard let dynamicLink = dynamicLink,
|
||||
let dynamicLinkURL = dynamicLink.url,
|
||||
dynamicLink.matchType == .unique || dynamicLink.matchType == .default else {
|
||||
return nil
|
||||
}
|
||||
return dynamicLinkURL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
@@ -546,8 +547,10 @@
|
||||
"downloadFailedDescription": "Please try again.",
|
||||
"downloadFailedTitle": "Download failed",
|
||||
"downloadFile": "Download",
|
||||
"dragAndDrop": "Drag and drop files here",
|
||||
"fileAlreadyUploaded": "File has already been uploaded to this meeting",
|
||||
"dragAndDrop": "Drag and drop files here or anywhere on screen",
|
||||
"fileAlreadyUploaded": "File has already been uploaded to this meeting.",
|
||||
"fileTooLargeDescription": "Please make sure the file does not exceed {{ maxFileSize }}.",
|
||||
"fileTooLargeTitle": "The selected file is too large",
|
||||
"removeFile": "Remove",
|
||||
"uploadFailedDescription": "Please try again.",
|
||||
"uploadFailedTitle": "Upload failed",
|
||||
@@ -662,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.",
|
||||
@@ -903,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",
|
||||
@@ -946,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"
|
||||
@@ -962,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!",
|
||||
@@ -1559,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"
|
||||
|
||||
@@ -108,6 +108,7 @@ import {
|
||||
} from '../../react/features/participants-pane/actions';
|
||||
import { getParticipantsPaneOpen, isForceMuted } from '../../react/features/participants-pane/functions';
|
||||
import { startLocalVideoRecording, stopLocalVideoRecording } from '../../react/features/recording/actions.any';
|
||||
import { grantRecordingConsent, grantRecordingConsentAndUnmute } from '../../react/features/recording/actions.web';
|
||||
import { RECORDING_METADATA_ID, RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession, supportsLocalRecording } from '../../react/features/recording/functions';
|
||||
import { startAudioScreenShareFlow, startScreenShareFlow } from '../../react/features/screen-share/actions';
|
||||
@@ -209,6 +210,10 @@ function initCommands() {
|
||||
}
|
||||
APP.store.dispatch(grantModerator(participantId));
|
||||
},
|
||||
'grant-recording-consent': unmute => {
|
||||
unmute ? APP.store.dispatch(grantRecordingConsentAndUnmute())
|
||||
: APP.store.dispatch(grantRecordingConsent());
|
||||
},
|
||||
'display-name': displayName => {
|
||||
sendAnalytics(createApiEvent('display.name.changed'));
|
||||
APP.store.dispatch(updateSettings({ displayName: getNormalizedDisplayName(displayName) }));
|
||||
@@ -769,7 +774,7 @@ function initCommands() {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
APP.store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
@@ -1918,6 +1923,19 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that the recording consent dialog open state has changed.
|
||||
*
|
||||
* @param {boolean} open - True if the dialog is open, false otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyRecordingConsentDialogOpen(open) {
|
||||
this._sendEvent({
|
||||
name: 'recording-consent-dialog-open',
|
||||
open
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of the current meeting requiring a password
|
||||
* to join.
|
||||
|
||||
2
modules/API/external/external_api.js
vendored
2
modules/API/external/external_api.js
vendored
@@ -38,6 +38,7 @@ const commands = {
|
||||
endConference: 'end-conference',
|
||||
email: 'email',
|
||||
grantModerator: 'grant-moderator',
|
||||
grantRecordingConsent: 'grant-recording-consent',
|
||||
hangup: 'video-hangup',
|
||||
hideNotification: 'hide-notification',
|
||||
initiatePrivateChat: 'initiate-private-chat',
|
||||
@@ -151,6 +152,7 @@ const events = {
|
||||
'proxy-connection-event': 'proxyConnectionEvent',
|
||||
'raise-hand-updated': 'raiseHandUpdated',
|
||||
'ready': 'ready',
|
||||
'recording-consent-dialog-open': 'recordingConsentDialogOpen',
|
||||
'recording-link-available': 'recordingLinkAvailable',
|
||||
'recording-status-changed': 'recordingStatusChanged',
|
||||
'participant-menu-button-clicked': 'participantMenuButtonClick',
|
||||
|
||||
2831
package-lock.json
generated
2831
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -16,12 +16,13 @@
|
||||
"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",
|
||||
"@giphy/js-fetch-api": "4.7.1",
|
||||
"@giphy/react-components": "6.8.1",
|
||||
"@giphy/js-fetch-api": "4.9.3",
|
||||
"@giphy/react-components": "6.9.4",
|
||||
"@giphy/react-native-sdk": "2.3.0",
|
||||
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.19/jitsi-excalidraw-0.0.19.tgz",
|
||||
"@jitsi/js-utils": "2.2.1",
|
||||
@@ -54,6 +55,7 @@
|
||||
"bc-css-flags": "3.0.0",
|
||||
"clipboard-copy": "4.0.1",
|
||||
"clsx": "1.1.1",
|
||||
"dayjs": "1.11.13",
|
||||
"dropbox": "10.7.0",
|
||||
"focus-visible": "5.1.0",
|
||||
"grapheme-splitter": "1.0.4",
|
||||
@@ -67,10 +69,8 @@
|
||||
"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/v1988.0.0+83c2ac30/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",
|
||||
"moment": "2.29.4",
|
||||
"moment-duration-format": "2.2.2",
|
||||
"null-loader": "4.0.1",
|
||||
"optional-require": "1.0.3",
|
||||
"pixelmatch": "5.3.0",
|
||||
@@ -111,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",
|
||||
@@ -131,7 +132,7 @@
|
||||
"@babel/plugin-transform-private-methods": "7.25.9",
|
||||
"@babel/preset-env": "7.25.9",
|
||||
"@babel/preset-react": "7.25.9",
|
||||
"@jitsi/eslint-config": "6.0.1",
|
||||
"@jitsi/eslint-config": "6.0.4",
|
||||
"@react-native/metro-config": "0.75.5",
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
@@ -139,9 +140,7 @@
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/jsonwebtoken": "9.0.7",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/minimatch": "5.1.2",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/moment-duration-format": "2.2.6",
|
||||
"@types/offscreencanvas": "2019.7.2",
|
||||
"@types/pixelmatch": "5.2.5",
|
||||
"@types/pretty": "2.0.3",
|
||||
@@ -158,12 +157,12 @@
|
||||
"@types/w3c-image-capture": "1.0.6",
|
||||
"@types/w3c-web-hid": "1.0.3",
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@wdio/allure-reporter": "9.4.3",
|
||||
"@wdio/cli": "9.4.3",
|
||||
"@wdio/globals": "9.4.3",
|
||||
"@wdio/junit-reporter": "9.4.3",
|
||||
"@wdio/local-runner": "9.4.3",
|
||||
"@wdio/mocha-framework": "9.4.3",
|
||||
"@wdio/allure-reporter": "9.16.0",
|
||||
"@wdio/cli": "9.16.0",
|
||||
"@wdio/globals": "9.16.0",
|
||||
"@wdio/junit-reporter": "9.16.0",
|
||||
"@wdio/local-runner": "9.16.0",
|
||||
"@wdio/mocha-framework": "9.16.0",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
@@ -188,7 +187,7 @@
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "5.7.2",
|
||||
"unorm": "1.6.0",
|
||||
"webdriverio": "9.4.3",
|
||||
"webdriverio": "9.16.0",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "5.1.4",
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
"@react-navigation/stack": "0.0.0",
|
||||
"@stomp/stompjs": "0.0.0",
|
||||
"@xmldom/xmldom": "0.0.0",
|
||||
"abab": "0.0.0",
|
||||
"base64-js": "0.0.0",
|
||||
"dayjs": "0.0.0",
|
||||
"grapheme-splitter": "0.0.0",
|
||||
"i18n-iso-countries": "0.0.0",
|
||||
"i18next": "0.0.0",
|
||||
@@ -32,8 +34,6 @@
|
||||
"jwt-decode": "0.0.0",
|
||||
"lib-jitsi-meet": "0.0.0",
|
||||
"lodash-es": "0.0.0",
|
||||
"moment": "0.0.0",
|
||||
"moment-duration-format": "0.0.0",
|
||||
"optional-require": "0.0.0",
|
||||
"promise.allsettled": "0.0.0",
|
||||
"promise.withresolvers": "0.0.0",
|
||||
@@ -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,
|
||||
@@ -187,7 +185,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
overwritesDefaultLogoUrl?: boolean;
|
||||
overwritesDeploymentUrls?: boolean;
|
||||
overwritesLiveStreamingUrls?: boolean;
|
||||
overwritesSalesforceUrl?: boolean;
|
||||
overwritesSupportUrl?: boolean;
|
||||
server?: string;
|
||||
tenant?: string;
|
||||
@@ -229,7 +226,6 @@ export function initAnalytics(store: IStore, handlers: Array<Object>): boolean {
|
||||
|
||||
// TODO: Temporary metric. To be removed once we don't need it.
|
||||
permanentProperties.overwritesSupportUrl = 'interfaceConfig.SUPPORT_URL' in params;
|
||||
permanentProperties.overwritesSalesforceUrl = 'config.salesforceUrl' in params;
|
||||
permanentProperties.overwritesDefaultLogoUrl = 'config.defaultLogoUrl' in params;
|
||||
|
||||
const deploymentUrlsConfig = params['config.deploymentUrls'] ?? {};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { MEDIA_TYPE } from '../base/media/constants';
|
||||
import type { MediaType } from '../base/media/constants';
|
||||
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
|
||||
import {
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -64,9 +64,9 @@ function getFirstGraphemeUpper(word: string) {
|
||||
export function getInitials(s?: string) {
|
||||
// We don't want to use the domain part of an email address, if it is one
|
||||
const initialsBasis = split(s, '@')[0];
|
||||
const [ firstWord, secondWord ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
|
||||
const [ firstWord, ...remainingWords ] = initialsBasis.split(wordSplitRegex).filter(Boolean);
|
||||
|
||||
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(secondWord);
|
||||
return getFirstGraphemeUpper(firstWord) + getFirstGraphemeUpper(remainingWords.pop() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -394,19 +394,6 @@ export function isP2pActive(stateful: IStateful): boolean | null {
|
||||
return conference.isP2PActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current conference has audio recording property which is on.
|
||||
*
|
||||
* @param {IStateful} stateful - The redux store, state, or {@code getState} function.
|
||||
* @returns {boolean|null}
|
||||
*/
|
||||
export function isConferenceAudioRecordingOn(stateful: IStateful): boolean | null {
|
||||
const state = getConferenceState(toState(stateful));
|
||||
|
||||
// @ts-ignore
|
||||
return state.properties?.['audio-recording-enabled'] === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored room name.
|
||||
*
|
||||
|
||||
@@ -16,18 +16,20 @@ 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';
|
||||
import { stopLocalVideoRecording } from '../../recording/actions.any';
|
||||
import LocalRecordingManager from '../../recording/components/Recording/LocalRecordingManager';
|
||||
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';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../participants/constants';
|
||||
import {
|
||||
@@ -97,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);
|
||||
|
||||
@@ -262,7 +269,9 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
|
||||
_removeUnloadHandler(getState);
|
||||
}
|
||||
|
||||
if (enableForcedReload && error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
|
||||
if (enableForcedReload
|
||||
&& (error?.name === JitsiConferenceErrors.CONFERENCE_RESTARTED
|
||||
|| error?.name === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
dispatch(reloadNow());
|
||||
}
|
||||
@@ -654,7 +663,7 @@ function _setRoom({ dispatch, getState }: IStore, next: Function, action: AnyAct
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
|
||||
async function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction) {
|
||||
const track = action.track;
|
||||
|
||||
// TODO All track swapping should happen here instead of conference.js.
|
||||
@@ -662,7 +671,6 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
let promise;
|
||||
|
||||
if (conference) {
|
||||
const jitsiTrack = action.track.jitsiTrack;
|
||||
@@ -671,14 +679,22 @@ function _trackAddedOrRemoved(store: IStore, next: Function, action: AnyAction)
|
||||
// If gUM is slow and tracks are created after the user has already joined the conference, avoid
|
||||
// adding the tracks to the conference if the user is a visitor.
|
||||
if (!iAmVisitor(state)) {
|
||||
promise = _addLocalTracksToConference(conference, [ jitsiTrack ]);
|
||||
const { desktopAudioTrack } = state['features/screen-share'];
|
||||
|
||||
// If the user is sharing their screen and has a desktop audio track, we need to replace that with
|
||||
// the audio mixer effect so that the desktop audio is mixed in with the microphone audio.
|
||||
if (typeof APP !== 'undefined' && desktopAudioTrack && track.mediaType === MEDIA_TYPE.AUDIO) {
|
||||
await conference.replaceTrack(desktopAudioTrack, null);
|
||||
const audioMixerEffect = new AudioMixerEffect(desktopAudioTrack);
|
||||
|
||||
await jitsiTrack.setEffect(audioMixerEffect);
|
||||
await conference.replaceTrack(null, jitsiTrack);
|
||||
} else {
|
||||
await _addLocalTracksToConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
promise = _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
return promise.then(() => next(action));
|
||||
await _removeLocalTracksFromConference(conference, [ jitsiTrack ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { CONNECTION_DISCONNECTED, CONNECTION_ESTABLISHED } from '../connection/actionTypes';
|
||||
import { hangup } from '../connection/actions.web';
|
||||
import { JitsiConferenceErrors, browser } from '../lib-jitsi-meet';
|
||||
import { JitsiConferenceErrors, JitsiConnectionErrors, browser } from '../lib-jitsi-meet';
|
||||
import { gumPending, setInitialGUMPromise } from '../media/actions';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import { IGUMPendingState } from '../media/types';
|
||||
@@ -117,7 +117,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case CONFERENCE_FAILED: {
|
||||
const errorName = action.error?.name;
|
||||
|
||||
if (enableForcedReload && errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED) {
|
||||
if (enableForcedReload
|
||||
&& (errorName === JitsiConferenceErrors.CONFERENCE_RESTARTED
|
||||
|| errorName === JitsiConnectionErrors.SHARD_CHANGED_ERROR)) {
|
||||
dispatch(setSkipPrejoinOnReload(true));
|
||||
}
|
||||
|
||||
@@ -188,7 +190,8 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
|
||||
return APP.conference.startConference(jitsiTracks);
|
||||
});
|
||||
})
|
||||
.catch(logger.error);
|
||||
});
|
||||
} else {
|
||||
promise.then(({ tracks }) => {
|
||||
|
||||
@@ -98,6 +98,7 @@ export interface IJitsiConference {
|
||||
end: Function;
|
||||
getBreakoutRooms: Function;
|
||||
getConnection: Function;
|
||||
getFileSharing: Function;
|
||||
getLocalParticipantProperty: Function;
|
||||
getLocalTracks: Function;
|
||||
getMeetingUniqueId: Function;
|
||||
@@ -107,6 +108,7 @@ export interface IJitsiConference {
|
||||
getParticipantCount: Function;
|
||||
getParticipants: Function;
|
||||
getRole: Function;
|
||||
getShortTermCredentials: Function;
|
||||
getSpeakerStats: () => ISpeakerStats;
|
||||
getSsrcByTrack: Function;
|
||||
getTranscriptionStatus: Function;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
let hasShownNotification = false;
|
||||
|
||||
|
||||
@@ -86,15 +86,6 @@ export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
|
||||
'RECORDING_ON_SOUND' |
|
||||
'TALK_WHILE_MUTED_SOUND';
|
||||
|
||||
|
||||
export interface IMobileDynamicLink {
|
||||
apn: string;
|
||||
appCode: string;
|
||||
customDomain?: string;
|
||||
ibi: string;
|
||||
isi: string;
|
||||
}
|
||||
|
||||
export interface IDeeplinkingPlatformConfig {
|
||||
appName: string;
|
||||
appScheme: string;
|
||||
@@ -103,7 +94,6 @@ export interface IDeeplinkingPlatformConfig {
|
||||
export interface IDeeplinkingMobileConfig extends IDeeplinkingPlatformConfig {
|
||||
appPackage?: string;
|
||||
downloadLink: string;
|
||||
dynamicLink?: IMobileDynamicLink;
|
||||
fDroidUrl?: string;
|
||||
}
|
||||
|
||||
@@ -182,7 +172,6 @@ export interface IConfig {
|
||||
_screenshotHistoryRegionUrl?: number;
|
||||
analytics?: {
|
||||
amplitudeAPPKey?: string;
|
||||
amplitudeIncludeUTM?: boolean;
|
||||
blackListedEvents?: string[];
|
||||
disabled?: boolean;
|
||||
matomoEndpoint?: string;
|
||||
@@ -391,12 +380,15 @@ export interface IConfig {
|
||||
fileSharing?: {
|
||||
apiUrl?: string;
|
||||
enabled?: boolean;
|
||||
maxFileSize?: number;
|
||||
};
|
||||
filmstrip?: {
|
||||
alwaysShowResizeBar?: boolean;
|
||||
disableResizable?: boolean;
|
||||
disableStageFilmstrip?: boolean;
|
||||
disableTopPanel?: boolean;
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
};
|
||||
flags?: {
|
||||
@@ -561,7 +553,7 @@ export interface IConfig {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
@@ -603,6 +595,7 @@ export interface IConfig {
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
@@ -612,8 +605,9 @@ export interface IConfig {
|
||||
};
|
||||
tokenAuthUrl?: string;
|
||||
tokenAuthUrlAutoRedirect?: string;
|
||||
tokenGetUserInfoOutOfContext?: boolean;
|
||||
tokenLogoutUrl?: string;
|
||||
tokenRespectTenant?: string;
|
||||
tokenRespectTenant?: boolean;
|
||||
toolbarButtons?: Array<ToolbarButton>;
|
||||
toolbarConfig?: {
|
||||
alwaysVisible?: boolean;
|
||||
@@ -628,6 +622,7 @@ export interface IConfig {
|
||||
autoTranscribeOnRecord?: boolean;
|
||||
disableClosedCaptions?: boolean;
|
||||
enabled?: boolean;
|
||||
inviteJigasiOnBackendTranscribing?: boolean;
|
||||
preferredLanguage?: string;
|
||||
translationLanguages?: Array<string>;
|
||||
translationLanguagesHead?: Array<string>;
|
||||
|
||||
@@ -193,6 +193,13 @@ export function overrideConfigJSON(config: IConfig, interfaceConfig: any, json:
|
||||
* that are whitelisted.
|
||||
*/
|
||||
export function getWhitelistedJSON(configName: 'interfaceConfig' | 'config', configJSON: any): Object {
|
||||
// Disable whitelisting in dev mode.
|
||||
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
||||
logger.warn('Whitelisting is disabled in dev mode, accepting any overrides');
|
||||
|
||||
return configJSON;
|
||||
}
|
||||
|
||||
if (configName === 'interfaceConfig') {
|
||||
return pick(configJSON, INTERFACE_CONFIG_WHITELIST);
|
||||
} else if (configName === 'config') {
|
||||
@@ -364,12 +371,6 @@ export function setConfigFromURLParams(
|
||||
logger.warn('Using liveStreaming config URL overwrite and/or LIVE_STREAMING_HELP_LINK interfaceConfig URL'
|
||||
+ ' overwrite is deprecated. Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
if ('config.customToolbarButtons' in params) {
|
||||
logger.warn('Using customToolbarButtons config URL overwrite is deprecated.'
|
||||
+ ' Please use liveStreaming from advanced branding!');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* eslint-enable max-params */
|
||||
|
||||
@@ -77,12 +77,6 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
|
||||
ios.appScheme = ios.appScheme || 'org.jitsi.meet';
|
||||
ios.downloadLink = ios.downloadLink
|
||||
|| 'https://itunes.apple.com/us/app/jitsi-meet/id1165103905';
|
||||
if (ios.dynamicLink) {
|
||||
ios.dynamicLink.apn = ios.dynamicLink.apn || 'org.jitsi.meet';
|
||||
ios.dynamicLink.appCode = ios.dynamicLink.appCode || 'w2atb';
|
||||
ios.dynamicLink.ibi = ios.dynamicLink.ibi || 'com.atlassian.JitsiMeet.ios';
|
||||
ios.dynamicLink.isi = ios.dynamicLink.isi || '1165103905';
|
||||
}
|
||||
|
||||
android.appName = android.appName || 'Jitsi Meet';
|
||||
android.appScheme = android.appScheme || 'org.jitsi.meet';
|
||||
@@ -90,10 +84,4 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
|
||||
|| 'https://play.google.com/store/apps/details?id=org.jitsi.meet';
|
||||
android.appPackage = android.appPackage || 'org.jitsi.meet';
|
||||
android.fDroidUrl = android.fDroidUrl || 'https://f-droid.org/packages/org.jitsi.meet/';
|
||||
if (android.dynamicLink) {
|
||||
android.dynamicLink.apn = android.dynamicLink.apn || 'org.jitsi.meet';
|
||||
android.dynamicLink.appCode = android.dynamicLink.appCode || 'w2atb';
|
||||
android.dynamicLink.ibi = android.dynamicLink.ibi || 'com.atlassian.JitsiMeet.ios';
|
||||
android.dynamicLink.isi = android.dynamicLink.isi || '1165103905';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,5 @@ export default [
|
||||
'customToolbarButtons',
|
||||
'defaultLogoUrl',
|
||||
'deploymentUrls',
|
||||
'liveStreaming',
|
||||
'salesforceUrl'
|
||||
'liveStreaming'
|
||||
];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,8 +18,7 @@ import {
|
||||
IConfig,
|
||||
IDeeplinkingConfig,
|
||||
IDeeplinkingDesktopConfig,
|
||||
IDeeplinkingMobileConfig,
|
||||
IMobileDynamicLink
|
||||
IDeeplinkingMobileConfig
|
||||
} from './configType';
|
||||
import { _cleanupConfig, _setDeeplinkingDefaults } from './functions';
|
||||
|
||||
@@ -186,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,
|
||||
@@ -321,15 +329,6 @@ function _translateInterfaceConfig(oldValue: IConfig) {
|
||||
};
|
||||
|
||||
if (typeof interfaceConfig === 'object') {
|
||||
const mobileDynamicLink = interfaceConfig.MOBILE_DYNAMIC_LINK;
|
||||
const dynamicLink: IMobileDynamicLink | undefined = mobileDynamicLink ? {
|
||||
apn: mobileDynamicLink.APN,
|
||||
appCode: mobileDynamicLink.APP_CODE,
|
||||
ibi: mobileDynamicLink.IBI,
|
||||
isi: mobileDynamicLink.ISI,
|
||||
customDomain: mobileDynamicLink.CUSTOM_DOMAIN
|
||||
} : undefined;
|
||||
|
||||
if (deeplinking.desktop) {
|
||||
deeplinking.desktop.appName = interfaceConfig.NATIVE_APP_NAME;
|
||||
}
|
||||
@@ -340,14 +339,12 @@ function _translateInterfaceConfig(oldValue: IConfig) {
|
||||
appScheme: interfaceConfig.APP_SCHEME,
|
||||
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_ANDROID,
|
||||
appPackage: interfaceConfig.ANDROID_APP_PACKAGE,
|
||||
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID,
|
||||
dynamicLink
|
||||
fDroidUrl: interfaceConfig.MOBILE_DOWNLOAD_LINK_F_DROID
|
||||
};
|
||||
deeplinking.ios = {
|
||||
appName: interfaceConfig.NATIVE_APP_NAME,
|
||||
appScheme: interfaceConfig.APP_SCHEME,
|
||||
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS,
|
||||
dynamicLink
|
||||
downloadLink: interfaceConfig.MOBILE_DOWNLOAD_LINK_IOS
|
||||
};
|
||||
}
|
||||
newValue.deeplinking = deeplinking;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { appNavigate } from '../../app/actions.native';
|
||||
import { IStore } from '../../app/types';
|
||||
import { getCustomerDetails } from '../../jaas/actions.any';
|
||||
import { isVpaasMeeting, getJaasJWT } from '../../jaas/functions';
|
||||
import { getJaasJWT, isVpaasMeeting } from '../../jaas/functions';
|
||||
import { navigateRoot } from '../../mobile/navigation/rootNavigationContainerRef';
|
||||
import { screen } from '../../mobile/navigation/routes';
|
||||
import { setJWT } from '../jwt/actions';
|
||||
|
||||
@@ -1,65 +1,81 @@
|
||||
import moment from 'moment';
|
||||
import momentDurationFormatSetup from 'moment-duration-format';
|
||||
import dayjs from 'dayjs';
|
||||
import durationPlugin from 'dayjs/plugin/duration';
|
||||
import localizedFormatPlugin from 'dayjs/plugin/localizedFormat';
|
||||
import relativeTimePlugin from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import i18next from './i18next';
|
||||
|
||||
// allows for moment durations to be formatted
|
||||
momentDurationFormatSetup(moment);
|
||||
dayjs.extend(durationPlugin);
|
||||
dayjs.extend(relativeTimePlugin);
|
||||
dayjs.extend(localizedFormatPlugin);
|
||||
|
||||
// MomentJS uses static language bundle loading, so in order to support dynamic
|
||||
// Day.js uses static language bundle loading, so in order to support dynamic
|
||||
// language selection in the app we need to load all bundles that we support in
|
||||
// the app.
|
||||
require('moment/locale/af');
|
||||
require('moment/locale/ar');
|
||||
require('moment/locale/be');
|
||||
require('moment/locale/bg');
|
||||
require('moment/locale/ca');
|
||||
require('moment/locale/cs');
|
||||
require('moment/locale/da');
|
||||
require('moment/locale/de');
|
||||
require('moment/locale/el');
|
||||
require('moment/locale/en-gb');
|
||||
require('moment/locale/eo');
|
||||
require('moment/locale/es-us');
|
||||
require('moment/locale/es');
|
||||
require('moment/locale/et');
|
||||
require('moment/locale/eu');
|
||||
require('moment/locale/fa');
|
||||
require('moment/locale/fi');
|
||||
require('moment/locale/fr-ca');
|
||||
require('moment/locale/fr');
|
||||
require('moment/locale/gl');
|
||||
require('moment/locale/he');
|
||||
require('moment/locale/hr');
|
||||
require('moment/locale/hu');
|
||||
require('moment/locale/hy-am');
|
||||
require('moment/locale/id');
|
||||
require('moment/locale/is');
|
||||
require('moment/locale/it');
|
||||
require('moment/locale/ja');
|
||||
require('moment/locale/ko');
|
||||
require('moment/locale/lt');
|
||||
require('moment/locale/lv');
|
||||
require('moment/locale/ml');
|
||||
require('moment/locale/mn');
|
||||
require('moment/locale/mr');
|
||||
require('moment/locale/nb');
|
||||
require('moment/locale/nl');
|
||||
require('moment/locale/oc-lnc');
|
||||
require('moment/locale/pl');
|
||||
require('moment/locale/pt');
|
||||
require('moment/locale/pt-br');
|
||||
require('moment/locale/ro');
|
||||
require('moment/locale/ru');
|
||||
require('moment/locale/sk');
|
||||
require('moment/locale/sl');
|
||||
require('moment/locale/sr');
|
||||
require('moment/locale/sv');
|
||||
require('moment/locale/tr');
|
||||
require('moment/locale/uk');
|
||||
require('moment/locale/vi');
|
||||
require('moment/locale/zh-cn');
|
||||
require('moment/locale/zh-tw');
|
||||
import 'dayjs/locale/af';
|
||||
import 'dayjs/locale/ar';
|
||||
import 'dayjs/locale/be';
|
||||
import 'dayjs/locale/bg';
|
||||
import 'dayjs/locale/ca';
|
||||
import 'dayjs/locale/cs';
|
||||
import 'dayjs/locale/da';
|
||||
import 'dayjs/locale/de';
|
||||
import 'dayjs/locale/el';
|
||||
import 'dayjs/locale/eo';
|
||||
import 'dayjs/locale/es';
|
||||
import 'dayjs/locale/es-us';
|
||||
import 'dayjs/locale/et';
|
||||
import 'dayjs/locale/eu';
|
||||
import 'dayjs/locale/fa';
|
||||
import 'dayjs/locale/fi';
|
||||
import 'dayjs/locale/fr';
|
||||
import 'dayjs/locale/fr-ca';
|
||||
import 'dayjs/locale/gl';
|
||||
import 'dayjs/locale/he';
|
||||
import 'dayjs/locale/hi';
|
||||
import 'dayjs/locale/hr';
|
||||
import 'dayjs/locale/hu';
|
||||
import 'dayjs/locale/hy-am';
|
||||
import 'dayjs/locale/id';
|
||||
import 'dayjs/locale/is';
|
||||
import 'dayjs/locale/it';
|
||||
import 'dayjs/locale/ja';
|
||||
import 'dayjs/locale/ko';
|
||||
import 'dayjs/locale/lt';
|
||||
import 'dayjs/locale/lv';
|
||||
import 'dayjs/locale/ml';
|
||||
import 'dayjs/locale/mn';
|
||||
import 'dayjs/locale/mr';
|
||||
import 'dayjs/locale/nb';
|
||||
import 'dayjs/locale/nl';
|
||||
import 'dayjs/locale/oc-lnc';
|
||||
import 'dayjs/locale/pl';
|
||||
import 'dayjs/locale/pt';
|
||||
import 'dayjs/locale/pt-br';
|
||||
import 'dayjs/locale/ro';
|
||||
import 'dayjs/locale/ru';
|
||||
import 'dayjs/locale/sk';
|
||||
import 'dayjs/locale/sl';
|
||||
import 'dayjs/locale/sq';
|
||||
import 'dayjs/locale/sr';
|
||||
import 'dayjs/locale/sv';
|
||||
import 'dayjs/locale/te';
|
||||
import 'dayjs/locale/tr';
|
||||
import 'dayjs/locale/uk';
|
||||
import 'dayjs/locale/vi';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import 'dayjs/locale/zh-tw';
|
||||
|
||||
const LOCALE_MAPPING: Record<string, string> = {
|
||||
// i18next -> dayjs
|
||||
'hy': 'hy-am',
|
||||
'oc': 'oc-lnc',
|
||||
'zhCN': 'zh-cn',
|
||||
'zhTW': 'zh-tw',
|
||||
'ptBR': 'pt-br',
|
||||
'esUS': 'es-us',
|
||||
'frCA': 'fr-ca'
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a localized date formatter initialized with a specific {@code Date}
|
||||
@@ -71,7 +87,7 @@ require('moment/locale/zh-tw');
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
|
||||
return moment(dateOrTimeStamp).locale(_getSupportedLocale());
|
||||
return dayjs(dateOrTimeStamp).locale(_getSupportedLocale());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,23 +100,17 @@ export function getLocalizedDateFormatter(dateOrTimeStamp: Date | number) {
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function getLocalizedDurationFormatter(duration: number) {
|
||||
// FIXME The flow-type definition of moment is v2.3 while our package.json
|
||||
// states v2.19 so maybe locale on moment's duration was introduced in
|
||||
// between?
|
||||
//
|
||||
|
||||
// If the conference is under an hour long we want to display it without
|
||||
// showing the hour and we want to include the hour if the conference is
|
||||
// more than an hour long
|
||||
|
||||
// @ts-ignore
|
||||
if (moment.duration(duration).format('h') !== '0') {
|
||||
// @ts-ignore
|
||||
return moment.duration(duration).format('h:mm:ss');
|
||||
const d = dayjs.duration(duration);
|
||||
|
||||
if (d.hours() !== 0) {
|
||||
return d.format('H:mm:ss');
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return moment.duration(duration).format('mm:ss', { trim: false });
|
||||
return d.format('mm:ss');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,9 +120,18 @@ export function getLocalizedDurationFormatter(duration: number) {
|
||||
* @returns {string}
|
||||
*/
|
||||
function _getSupportedLocale() {
|
||||
const availableLocales = Object.keys(dayjs.Ls);
|
||||
const i18nLocale = i18next.language;
|
||||
let supportedLocale;
|
||||
|
||||
if (LOCALE_MAPPING[i18nLocale]) {
|
||||
return LOCALE_MAPPING[i18nLocale];
|
||||
}
|
||||
|
||||
if (availableLocales.includes(i18nLocale)) {
|
||||
return i18nLocale;
|
||||
}
|
||||
|
||||
if (i18nLocale) {
|
||||
const localeRegexp = new RegExp('^([a-z]{2,2})(-)*([a-z]{2,2})*$');
|
||||
const localeResult = localeRegexp.exec(i18nLocale.toLowerCase());
|
||||
@@ -123,11 +142,7 @@ function _getSupportedLocale() {
|
||||
`^${localeResult[1]}(-)*${`(${localeResult[3]})*` || ''}`);
|
||||
|
||||
supportedLocale
|
||||
|
||||
// FIXME The flow-type definition of moment is v2.3 while our
|
||||
// package.json states v2.19 so maybe locales on moment was
|
||||
// introduced in between?
|
||||
= moment.locales().find(lang => currentLocaleRegexp.exec(lang));
|
||||
= availableLocales.find(lang => currentLocaleRegexp.exec(lang));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
/**
|
||||
* The type of redux action which set an avatar URL for delayed loading.
|
||||
*
|
||||
* {
|
||||
* type: SET_DELAYED_LOAD_OF_AVATAR_URL
|
||||
* avatarUrl: string
|
||||
* }
|
||||
*/
|
||||
export const SET_DELAYED_LOAD_OF_AVATAR_URL = 'SET_DELAYED_LOAD_OF_AVATAR_URL';
|
||||
|
||||
/**
|
||||
* The type of redux action which stores a specific JSON Web Token (JWT) into
|
||||
* the redux store.
|
||||
@@ -8,3 +18,13 @@
|
||||
* }
|
||||
*/
|
||||
export const SET_JWT = 'SET_JWT';
|
||||
|
||||
/**
|
||||
* The type of redux action which sets a known avatar URL.
|
||||
*
|
||||
* {
|
||||
* type: SET_KNOWN_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }
|
||||
*/
|
||||
export const SET_KNOWN_AVATAR_URL = 'SET_KNOWN_AVATAR_URL';
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Sets an avatar URL for delayed loading.
|
||||
*
|
||||
* @param {string} avatarUrl - The avatar URL to set for delayed loading.
|
||||
* @returns {{
|
||||
* type: SET_DELAYED_LOAD_OF_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }}
|
||||
*/
|
||||
export function setDelayedLoadOfAvatarUrl(avatarUrl?: string) {
|
||||
return {
|
||||
type: SET_DELAYED_LOAD_OF_AVATAR_URL,
|
||||
avatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a specific JSON Web Token (JWT) into the redux store.
|
||||
*
|
||||
* @param {string} [jwt] - The JSON Web Token (JWT) to store.
|
||||
* @returns {{
|
||||
* type: SET_TOKEN_DATA,
|
||||
* type: SET_JWT,
|
||||
* jwt: (string|undefined)
|
||||
* }}
|
||||
*/
|
||||
@@ -15,3 +31,19 @@ export function setJWT(jwt?: string) {
|
||||
jwt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a known avatar URL.
|
||||
*
|
||||
* @param {string} avatarUrl - The avatar URL to set as known.
|
||||
* @returns {{
|
||||
* type: SET_KNOWN_AVATAR_URL,
|
||||
* avatarUrl: string
|
||||
* }}
|
||||
*/
|
||||
export function setKnownAvatarUrl(avatarUrl: string) {
|
||||
return {
|
||||
type: SET_KNOWN_AVATAR_URL,
|
||||
avatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const MEET_FEATURES: Record<string, ParticipantFeaturesKey> = {
|
||||
BRANDING: 'branding',
|
||||
CALENDAR: 'calendar',
|
||||
CREATE_POLLS: 'create-polls',
|
||||
FILE_UPLOAD: 'file-upload',
|
||||
FLIP: 'flip',
|
||||
INBOUND_CALL: 'inbound-call',
|
||||
LIVESTREAMING: 'livestreaming',
|
||||
@@ -17,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'
|
||||
|
||||
@@ -4,19 +4,33 @@ import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
import { SET_LOCATION_URL } from '../connection/actionTypes';
|
||||
import { CONNECTION_ESTABLISHED, SET_LOCATION_URL } from '../connection/actionTypes';
|
||||
import { participantUpdated } from '../participants/actions';
|
||||
import { getLocalParticipant } from '../participants/functions';
|
||||
import { IParticipant } from '../participants/types';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { parseURIString } from '../util/uri';
|
||||
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { setJWT } from './actions';
|
||||
import { setDelayedLoadOfAvatarUrl, setJWT, setKnownAvatarUrl } from './actions';
|
||||
import { parseJWTFromURLParams } from './functions';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Set up a state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, e.g. Clear any delayed load avatar url.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch }, previousConference): void => {
|
||||
if (conference !== previousConference) {
|
||||
dispatch(setDelayedLoadOfAvatarUrl());
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Middleware to parse token data upon setting a new room URL.
|
||||
*
|
||||
@@ -31,7 +45,18 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
// XXX The JSON Web Token (JWT) is not the only piece of state that we
|
||||
// have decided to store in the feature jwt
|
||||
return _setConfigOrLocationURL(store, next, action);
|
||||
case CONNECTION_ESTABLISHED: {
|
||||
const state = store.getState();
|
||||
const delayedLoadOfAvatarUrl = state['features/base/jwt'].delayedLoadOfAvatarUrl;
|
||||
|
||||
if (delayedLoadOfAvatarUrl) {
|
||||
_overwriteLocalParticipant(store, {
|
||||
avatarURL: delayedLoadOfAvatarUrl
|
||||
});
|
||||
store.dispatch(setDelayedLoadOfAvatarUrl());
|
||||
store.dispatch(setKnownAvatarUrl(delayedLoadOfAvatarUrl));
|
||||
}
|
||||
}
|
||||
case SET_JWT:
|
||||
return _setJWT(store, next, action);
|
||||
}
|
||||
@@ -140,6 +165,7 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
|
||||
|
||||
if (jwtPayload) {
|
||||
const { context, iss, sub } = jwtPayload;
|
||||
const { tokenGetUserInfoOutOfContext, tokenRespectTenant } = state['features/base/config'];
|
||||
|
||||
action.jwt = jwt;
|
||||
action.issuer = iss;
|
||||
@@ -155,7 +181,6 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
|
||||
const newUser = user ? { ...user } : {};
|
||||
|
||||
let features = context.features;
|
||||
const { tokenRespectTenant } = state['features/base/config'];
|
||||
|
||||
// eslint-disable-next-line max-depth
|
||||
if (!isVpaasMeeting(state) && tokenRespectTenant && context.tenant) {
|
||||
@@ -167,6 +192,16 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
|
||||
features = context.tenant === tenant || tenant === '' ? features : {};
|
||||
}
|
||||
|
||||
if (newUser.avatarURL) {
|
||||
const { knownAvatarUrl } = state['features/base/jwt'];
|
||||
|
||||
if (knownAvatarUrl !== newUser.avatarURL) {
|
||||
store.dispatch(setDelayedLoadOfAvatarUrl(newUser.avatarURL));
|
||||
|
||||
newUser.avatarURL = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_overwriteLocalParticipant(
|
||||
store, { ...newUser,
|
||||
features });
|
||||
@@ -175,7 +210,8 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
|
||||
if (context.user && context.user.role === 'visitor') {
|
||||
action.preferVisitor = true;
|
||||
}
|
||||
} else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) {
|
||||
} else if (tokenGetUserInfoOutOfContext
|
||||
&& (jwtPayload.name || jwtPayload.picture || jwtPayload.email)) {
|
||||
// there are some tokens (firebase) having picture and name on the main level.
|
||||
_overwriteLocalParticipant(store, {
|
||||
avatarURL: jwtPayload.picture,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
import PersistenceRegistry from '../redux/PersistenceRegistry';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { equals } from '../redux/functions';
|
||||
|
||||
import { SET_JWT } from './actionTypes';
|
||||
import { SET_DELAYED_LOAD_OF_AVATAR_URL, SET_JWT, SET_KNOWN_AVATAR_URL } from './actionTypes';
|
||||
import logger from './logger';
|
||||
|
||||
export interface IJwtState {
|
||||
callee?: {
|
||||
name: string;
|
||||
};
|
||||
delayedLoadOfAvatarUrl?: string;
|
||||
group?: string;
|
||||
jwt?: string;
|
||||
knownAvatarUrl?: string;
|
||||
server?: string;
|
||||
tenant?: string;
|
||||
user?: {
|
||||
@@ -17,6 +21,10 @@ export interface IJwtState {
|
||||
};
|
||||
}
|
||||
|
||||
PersistenceRegistry.register('features/base/jwt', {
|
||||
knownAvatarUrl: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Reduces redux actions which affect the JSON Web Token (JWT) stored in the
|
||||
* redux store.
|
||||
@@ -30,15 +38,35 @@ ReducerRegistry.register<IJwtState>(
|
||||
'features/base/jwt',
|
||||
(state = {}, action): IJwtState => {
|
||||
switch (action.type) {
|
||||
case SET_DELAYED_LOAD_OF_AVATAR_URL: {
|
||||
const nextState = {
|
||||
...state,
|
||||
delayedLoadOfAvatarUrl: action.avatarUrl
|
||||
};
|
||||
|
||||
if (equals(state, nextState)) {
|
||||
return state;
|
||||
}
|
||||
|
||||
logger.info('JWT avatarURL temporarily not loaded till jwt is verified on connect');
|
||||
|
||||
return nextState;
|
||||
}
|
||||
case SET_JWT: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { type, ...payload } = action;
|
||||
const nextState = {
|
||||
...state,
|
||||
...payload
|
||||
};
|
||||
|
||||
return equals(state, nextState) ? state : nextState;
|
||||
}
|
||||
case SET_KNOWN_AVATAR_URL:
|
||||
return {
|
||||
...state,
|
||||
knownAvatarUrl: action.avatarUrl
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import NetInfo from '@react-native-community/netinfo';
|
||||
import type { NetInfoState, NetInfoSubscription } from '@react-native-community/netinfo';
|
||||
import {
|
||||
default as NetInfo,
|
||||
type NetInfoState,
|
||||
type NetInfoSubscription
|
||||
} from '@react-native-community/netinfo';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-expect-error
|
||||
// @ts-ignore
|
||||
import EventEmitter from 'events';
|
||||
|
||||
import { ONLINE_STATE_CHANGED_EVENT } from './events';
|
||||
|
||||
@@ -5,6 +5,7 @@ import { IReduxState, IStore } from '../../app/types';
|
||||
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
|
||||
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
|
||||
import { toggleShareDialog } from '../../share-room/actions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { IStateful } from '../app/types';
|
||||
import { GRAVATAR_BASE_URL } from '../avatar/constants';
|
||||
import { isCORSAvatarURL } from '../avatar/functions';
|
||||
@@ -413,6 +414,22 @@ export function getParticipantCountWithFake(stateful: IStateful) {
|
||||
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known participants in the passed in redux state,
|
||||
* including fake participants. Subtract 1 when the local participant is a visitor as we do not show a local thumbnail.
|
||||
* The number used to display the participant count in the UI.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCountForDisplay(stateful: IStateful) {
|
||||
const _iAmVisitor = iAmVisitor(stateful);
|
||||
|
||||
return getParticipantCount(stateful) - (_iAmVisitor ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant's display name.
|
||||
*
|
||||
@@ -806,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;
|
||||
}
|
||||
|
||||
@@ -58,8 +58,10 @@ export interface IParticipantFeatures {
|
||||
'branding'?: boolean | string;
|
||||
'calendar'?: boolean | string;
|
||||
'create-polls'?: boolean | string;
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
|
||||
@@ -54,7 +54,7 @@ class Message extends Component<IProps> {
|
||||
|
||||
// Tokenize the text in order to avoid emoji substitution for URLs
|
||||
const tokens = text ? text.split(' ') : [];
|
||||
const content = [];
|
||||
const content: any[] = [];
|
||||
const { gifEnabled } = this.props;
|
||||
|
||||
// check if the message is a GIF
|
||||
@@ -72,7 +72,11 @@ class Message extends Component<IProps> {
|
||||
// Bypass the emojification when urls or matrix ids are involved
|
||||
content.push(token);
|
||||
} else {
|
||||
content.push(...toArray(token, { className: 'smiley' }));
|
||||
const emojified = [ ...toArray(token, { className: 'smiley' }) ];
|
||||
|
||||
content.push(
|
||||
...emojified.some(item => typeof item === 'string') ? [ token ] : emojified
|
||||
);
|
||||
}
|
||||
|
||||
content.push(' ');
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Action, combineReducers } from 'redux';
|
||||
import type { Reducer } from 'redux';
|
||||
import { Action, type Reducer, combineReducers } from 'redux';
|
||||
|
||||
/**
|
||||
* The type of the dictionary/map which associates a reducer (function) with the
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IStateful } from '../app/types';
|
||||
import { isMobileBrowser } from '../environment/utils';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
import { SMALL_DESKTOP_WIDTH } from './constants';
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,8 +4,10 @@ import Icon from '../../icons/components/Icon';
|
||||
import Tooltip from '../../tooltip/components/Tooltip';
|
||||
import ContextMenuItem from '../../ui/components/web/ContextMenuItem';
|
||||
|
||||
import AbstractToolboxItem from './AbstractToolboxItem';
|
||||
import type { IProps as AbstractToolboxItemProps } from './AbstractToolboxItem';
|
||||
import {
|
||||
default as AbstractToolboxItem,
|
||||
type IProps as AbstractToolboxItemProps
|
||||
} from './AbstractToolboxItem';
|
||||
|
||||
interface IProps extends AbstractToolboxItemProps {
|
||||
|
||||
|
||||
@@ -421,12 +421,14 @@ export function trackAdded(track: any) {
|
||||
}
|
||||
|
||||
track.on(JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => dispatch({
|
||||
type: TRACK_STOPPED,
|
||||
track: {
|
||||
jitsiTrack: track
|
||||
}
|
||||
}));
|
||||
() => {
|
||||
logger.debug(`Local track stopped: ${track}, removing it from the conference`);
|
||||
dispatch({
|
||||
type: TRACK_STOPPED,
|
||||
track: {
|
||||
jitsiTrack: track
|
||||
} });
|
||||
});
|
||||
} else {
|
||||
participantId = track.getParticipantId();
|
||||
isReceivingData = true;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { _RESET_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import {
|
||||
SET_AUDIO_MUTED,
|
||||
@@ -203,11 +202,8 @@ function _setMuted(store: IStore, { ensureTrack, muted }: {
|
||||
setTrackMuted(jitsiTrack, muted, state, dispatch)
|
||||
.catch(() => dispatch(trackMuteUnmuteFailed(localTrack, muted)));
|
||||
}
|
||||
} else if (!muted && ensureTrack && (typeof APP === 'undefined' || isPrejoinPageVisible(state))) {
|
||||
} else if (!muted && ensureTrack) {
|
||||
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.PENDING_UNMUTE));
|
||||
|
||||
// FIXME: This only runs on mobile now because web has its own way of
|
||||
// creating local tracks. Adjust the check once they are unified.
|
||||
dispatch(createLocalTracksA({ devices: [ mediaType ] })).then(() => {
|
||||
typeof APP !== 'undefined' && dispatch(gumPending([ mediaType ], IGUMPendingState.NONE));
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import Icon from '../../../icons/components/Icon';
|
||||
import { IconCloseCircle } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { IInputProps } from '../types';
|
||||
|
||||
import { HiddenDescription } from './HiddenDescription';
|
||||
|
||||
interface IProps extends IInputProps {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { merge } from 'lodash';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import * as jitsiTokens from './jitsiTokens.json';
|
||||
import * as tokens from './tokens.json';
|
||||
|
||||
@@ -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';
|
||||
@@ -10,9 +11,9 @@ import { ASPECT_RATIO_WIDE } from '../../../base/responsive-ui/constants';
|
||||
import IconButton from '../../../base/ui/components/native/IconButton';
|
||||
import Input from '../../../base/ui/components/native/Input';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
|
||||
import { isSendGroupChatDisabled } from '../../functions';
|
||||
|
||||
import styles from './styles';
|
||||
import { isSendGroupChatDisabled } from '../../functions';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
@@ -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).
|
||||
*/
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { throttle } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
import { throttle } from 'lodash-es';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconInfo, IconMessage, IconShareDoc, IconSubtitles } from '../../../base/icons/svg';
|
||||
import { getLocalParticipant } from '../../../base/participants/functions';
|
||||
import Tabs from '../../../base/ui/components/web/Tabs';
|
||||
import { arePollsDisabled } from '../../../conference/functions.any';
|
||||
@@ -12,10 +13,10 @@ import FileSharing from '../../../file-sharing/components/web/FileSharing';
|
||||
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
|
||||
import PollsPane from '../../../polls/components/web/PollsPane';
|
||||
import { isCCTabEnabled } from '../../../subtitles/functions.any';
|
||||
import { setChatIsResizing, setUserChatWidth, sendMessage, setFocusedTab, toggleChat } from '../../actions.web';
|
||||
import { sendMessage, setChatIsResizing, setFocusedTab, setUserChatWidth, toggleChat } from '../../actions.web';
|
||||
import { CHAT_SIZE, ChatTabs, SMALL_WIDTH_THRESHOLD } from '../../constants';
|
||||
import { getChatMaxSize } from '../../functions';
|
||||
import { IChatProps as AbstractProps } from '../../types';
|
||||
import { IconMessage, IconInfo, IconSubtitles, IconShareDoc } from '../../../base/icons/svg';
|
||||
|
||||
import ChatHeader from './ChatHeader';
|
||||
import ChatInput from './ChatInput';
|
||||
@@ -24,7 +25,7 @@ import DisplayNameForm from './DisplayNameForm';
|
||||
import KeyboardAvoider from './KeyboardAvoider';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
import { getChatMaxSize } from '../../functions';
|
||||
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
@@ -149,6 +150,7 @@ const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme,
|
||||
boxSizing: 'border-box',
|
||||
color: theme.palette.text01,
|
||||
...theme.typography.heading6,
|
||||
lineHeight: 'unset',
|
||||
fontWeight: theme.typography.heading6.fontWeight as any,
|
||||
|
||||
'.jitsi-icon': {
|
||||
@@ -445,7 +447,7 @@ const Chat = ({
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function renderTabs() {
|
||||
const tabs = [
|
||||
let tabs = [
|
||||
{
|
||||
accessibilityLabel: t('chat.tabs.chat'),
|
||||
countBadge:
|
||||
@@ -486,6 +488,10 @@ const Chat = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (tabs.length === 1) {
|
||||
tabs = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
accessibilityLabel = { _isPollsEnabled || _isCCTabEnabled || _isFileSharingTabEnabled
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { ChatTabs } from '../../constants';
|
||||
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import { ChatTabs } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ import { translate } from '../../../base/i18n/functions';
|
||||
import { IconFaceSmile, IconSend } from '../../../base/icons/svg';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Input from '../../../base/ui/components/web/Input';
|
||||
import { CHAT_SIZE } from '../../constants';
|
||||
import { areSmileysDisabled, isSendGroupChatDisabled } from '../../functions';
|
||||
|
||||
import SmileysPanel from './SmileysPanel';
|
||||
import { CHAT_SIZE } from '../../constants';
|
||||
|
||||
|
||||
const styles = (_theme: Theme, { _chatWidth }: IProps) => {
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconSubtitles } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
@@ -15,7 +16,6 @@ import { ISubtitle } from '../../../subtitles/types';
|
||||
import { isTranscribing } from '../../../transcribing/functions';
|
||||
|
||||
import { SubtitlesMessagesContainer } from './SubtitlesMessagesContainer';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
|
||||
/**
|
||||
* The styles for the ClosedCaptionsTab component.
|
||||
|
||||
@@ -11,11 +11,11 @@ import { MEET_FEATURES } from '../base/jwt/constants';
|
||||
import { isJwtFeatureEnabled } from '../base/jwt/functions';
|
||||
import { getParticipantById } from '../base/participants/functions';
|
||||
import { escapeRegexp } from '../base/util/helpers';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
|
||||
import { MESSAGE_TYPE_ERROR, MESSAGE_TYPE_LOCAL, TIMESTAMP_FORMAT } from './constants';
|
||||
import { IMessage } from './types';
|
||||
import { getParticipantsPaneWidth } from '../participants-pane/functions';
|
||||
import { VIDEO_SPACE_MIN_SIZE } from '../video-layout/constants';
|
||||
|
||||
/**
|
||||
* An ASCII emoticon regexp array to find and replace old-style ASCII
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
import { ILocalParticipant, IParticipant } from '../base/participants/types';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
import { ChatTabs } from './constants';
|
||||
|
||||
import {
|
||||
ADD_MESSAGE,
|
||||
@@ -18,9 +18,8 @@ import {
|
||||
SET_PRIVATE_MESSAGE_RECIPIENT,
|
||||
SET_USER_CHAT_WIDTH
|
||||
} from './actionTypes';
|
||||
import { CHAT_SIZE } from './constants';
|
||||
import { CHAT_SIZE, ChatTabs } from './constants';
|
||||
import { IMessage } from './types';
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
groupChatWithPermissions: false,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { clientResized } from '../base/responsive-ui/actions';
|
||||
|
||||
import { setChatWidth } from './actions.web';
|
||||
import { CHAT_SIZE } from './constants';
|
||||
import { getChatMaxSize } from './functions';
|
||||
|
||||
@@ -17,9 +17,9 @@ import { translate } from '../../base/i18n/functions';
|
||||
import Icon from '../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../base/icons/svg';
|
||||
import { browser } from '../../base/lib-jitsi-meet';
|
||||
import { isNarrowScreenWithChatOpen } from '../../base/responsive-ui/functions';
|
||||
import { isVpaasMeeting } from '../../jaas/functions';
|
||||
import logger from '../logger';
|
||||
import { isNarrowScreenWithChatOpen } from '../../base/responsive-ui/functions';
|
||||
|
||||
const emptyObject = {};
|
||||
|
||||
|
||||
@@ -46,9 +46,9 @@ import Toolbox from '../../../toolbox/components/native/Toolbox';
|
||||
import { isToolboxVisible } from '../../../toolbox/functions.native';
|
||||
import {
|
||||
AbstractConference,
|
||||
type AbstractProps,
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractConference';
|
||||
import type { AbstractProps } from '../AbstractConference';
|
||||
import { isConnecting } from '../functions.native';
|
||||
|
||||
import AlwaysOnLabels from './AlwaysOnLabels';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { throttle } from 'lodash-es';
|
||||
import React from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect as reactReduxConnect } from 'react-redux';
|
||||
import { connect as reactReduxConnect, useDispatch, useSelector, useStore } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import VideoLayout from '../../../../../modules/UI/videolayout/VideoLayout';
|
||||
@@ -11,7 +11,10 @@ import { hangup } from '../../../base/connection/actions.web';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { setColorAlpha } from '../../../base/util/helpers';
|
||||
import { openChat, setFocusedTab } from '../../../chat/actions.web';
|
||||
import Chat from '../../../chat/components/web/Chat';
|
||||
import { ChatTabs } from '../../../chat/constants';
|
||||
import { isFileUploadingEnabled, processFiles } from '../../../file-sharing/functions.any';
|
||||
import MainFilmstrip from '../../../filmstrip/components/web/MainFilmstrip';
|
||||
import ScreenshareFilmstrip from '../../../filmstrip/components/web/ScreenshareFilmstrip';
|
||||
import StageFilmstrip from '../../../filmstrip/components/web/StageFilmstrip';
|
||||
@@ -22,7 +25,7 @@ import { getIsLobbyVisible } from '../../../lobby/functions';
|
||||
import { getOverlayToRender } from '../../../overlay/functions.web';
|
||||
import ParticipantsPane from '../../../participants-pane/components/web/ParticipantsPane';
|
||||
import Prejoin from '../../../prejoin/components/web/Prejoin';
|
||||
import { isPrejoinPageVisible } from '../../../prejoin/functions';
|
||||
import { isPrejoinPageVisible } from '../../../prejoin/functions.web';
|
||||
import ReactionAnimations from '../../../reactions/components/web/ReactionsAnimations';
|
||||
import { toggleToolboxVisible } from '../../../toolbox/actions.any';
|
||||
import { fullScreenChanged, showToolbox } from '../../../toolbox/actions.web';
|
||||
@@ -36,9 +39,9 @@ import { init } from '../../actions.web';
|
||||
import { maybeShowSuboptimalExperienceNotification } from '../../functions.web';
|
||||
import {
|
||||
AbstractConference,
|
||||
type AbstractProps,
|
||||
abstractMapStateToProps
|
||||
} from '../AbstractConference';
|
||||
import type { AbstractProps } from '../AbstractConference';
|
||||
|
||||
import ConferenceInfo from './ConferenceInfo';
|
||||
import { default as Notice } from './Notice';
|
||||
@@ -430,4 +433,65 @@ function _mapStateToProps(state: IReduxState) {
|
||||
};
|
||||
}
|
||||
|
||||
export default reactReduxConnect(_mapStateToProps)(translate(Conference));
|
||||
export default reactReduxConnect(_mapStateToProps)(translate(props => {
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
|
||||
const [ isDragging, setIsDragging ] = useState(false);
|
||||
|
||||
const { isOpen: isChatOpen } = useSelector((state: IReduxState) => state['features/chat']);
|
||||
const isFileUploadEnabled = useSelector(isFileUploadingEnabled);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(true);
|
||||
}, []);
|
||||
|
||||
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!isFileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
if (!isChatOpen) {
|
||||
dispatch(openChat());
|
||||
}
|
||||
dispatch(setFocusedTab(ChatTabs.FILE_SHARING));
|
||||
}
|
||||
}, [ isChatOpen, isDragging, isFileUploadEnabled ]);
|
||||
|
||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (!isFileUploadEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.dataTransfer.files?.length > 0) {
|
||||
processFiles(e.dataTransfer.files, store);
|
||||
}
|
||||
}, [ isFileUploadEnabled, processFiles ]);
|
||||
|
||||
return (
|
||||
<div
|
||||
onDragEnter = { handleDragEnter }
|
||||
onDragLeave = { handleDragLeave }
|
||||
onDragOver = { handleDragOver }
|
||||
onDrop = { handleDrop }>
|
||||
{/* @ts-ignore */}
|
||||
<Conference { ...props } />
|
||||
</div>
|
||||
);
|
||||
}));
|
||||
|
||||
@@ -7,11 +7,10 @@ import { openDialog } from '../../../base/dialog/actions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import Label from '../../../base/label/components/web/Label';
|
||||
import { COLORS } from '../../../base/label/constants';
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
import Tooltip from '../../../base/tooltip/components/Tooltip';
|
||||
import SpeakerStats from '../../../speaker-stats/components/web/SpeakerStats';
|
||||
import { isSpeakerStatsDisabled } from '../../../speaker-stats/functions';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
|
||||
/**
|
||||
* ParticipantsCount react component.
|
||||
@@ -21,17 +20,11 @@ import { iAmVisitor } from '../../../visitors/functions';
|
||||
*/
|
||||
function SpeakerStatsLabel() {
|
||||
const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
|
||||
let count = useSelector(getParticipantCount);
|
||||
const iAmVisitorState = useSelector(iAmVisitor);
|
||||
const count = useSelector(getParticipantCountForDisplay);
|
||||
const _isSpeakerStatsDisabled = useSelector(isSpeakerStatsDisabled);
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// visitor has hidden its own video and should not count itself
|
||||
if (iAmVisitorState) {
|
||||
count--;
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
dispatch(openDialog(SpeakerStats, { conference }));
|
||||
};
|
||||
|
||||
@@ -106,29 +106,10 @@ const DeepLinkingMobilePage: React.FC<WithTranslation> = ({ t }) => {
|
||||
const { classes: styles } = useStyles();
|
||||
|
||||
const generateDownloadURL = useCallback(() => {
|
||||
const { downloadLink, dynamicLink, appScheme }
|
||||
const { downloadLink }
|
||||
= (deeplinkingCfg?.[Platform.OS as keyof typeof deeplinkingCfg] || {}) as IDeeplinkingMobileConfig;
|
||||
|
||||
if (downloadLink && typeof dynamicLink === 'undefined') {
|
||||
return downloadLink;
|
||||
}
|
||||
|
||||
const {
|
||||
apn,
|
||||
appCode,
|
||||
customDomain,
|
||||
ibi,
|
||||
isi
|
||||
} = dynamicLink || {};
|
||||
|
||||
const domain = customDomain ?? `https://${appCode}.app.goo.gl`;
|
||||
|
||||
return `${domain}/?link=${
|
||||
encodeURIComponent(window.location.href)}&apn=${
|
||||
apn}&ibi=${
|
||||
ibi}&isi=${
|
||||
isi}&ius=${
|
||||
appScheme}&efr=1`;
|
||||
return downloadLink;
|
||||
}, [ deeplinkingCfg ]);
|
||||
|
||||
const onDownloadApp = useCallback(() => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { PARTICIPANT_ROLE_CHANGED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
import { SET_DYNAMIC_BRANDING_DATA, SET_DYNAMIC_BRANDING_READY } from './actionTypes';
|
||||
import { fetchCustomIcons } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { getLocalParticipant, isLocalParticipantModerator } from '../base/participants/functions';
|
||||
import { PARTICIPANT_ROLE_CHANGED } from '../base/participants/actionTypes';
|
||||
import { PARTICIPANT_ROLE } from '../base/participants/constants';
|
||||
import { IStateful } from '../base/app/types';
|
||||
import { toState } from '../base/redux/functions';
|
||||
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
|
||||
@@ -17,45 +17,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
|
||||
case SET_DYNAMIC_BRANDING_DATA: {
|
||||
const {
|
||||
avatarBackgrounds = [],
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
peopleSearchUrl,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl,
|
||||
userDocumentationUrl,
|
||||
} = action.value;
|
||||
|
||||
action.value = {
|
||||
avatarBackgrounds,
|
||||
backgroundColor,
|
||||
backgroundImageUrl,
|
||||
brandedIcons,
|
||||
customParticipantMenuButtons,
|
||||
customToolbarButtons,
|
||||
didPageUrl,
|
||||
downloadAppsUrl,
|
||||
etherpadBase,
|
||||
inviteDomain,
|
||||
labels,
|
||||
liveStreamingDialogUrls,
|
||||
peopleSearchUrl,
|
||||
salesforceUrl,
|
||||
sharedVideoAllowedURLDomains,
|
||||
supportUrl,
|
||||
userDocumentationUrl
|
||||
};
|
||||
const { avatarBackgrounds = [] } = action.value;
|
||||
|
||||
// The backend may send an empty string, make sure we skip that.
|
||||
if (Array.isArray(avatarBackgrounds)) {
|
||||
|
||||
@@ -30,7 +30,27 @@ export const UPLOAD_FILES = 'UPLOAD_FILES';
|
||||
export const ADD_FILE = 'ADD_FILE';
|
||||
|
||||
/**
|
||||
* The type of redux action to remove a file from the state.
|
||||
* The type of redux action to add files to the state.
|
||||
*
|
||||
* {
|
||||
* type: _FILE_LIST_RECEIVED,
|
||||
* files: Array<IFileMetadata>
|
||||
* }
|
||||
*/
|
||||
export const _FILE_LIST_RECEIVED = '_FILE_LIST_RECEIVED';
|
||||
|
||||
/**
|
||||
* The type of redux action to remove a file from the state as it was removed from the backend.
|
||||
*
|
||||
* {
|
||||
* type: _FILE_REMOVED,
|
||||
* fileId: string
|
||||
* }
|
||||
*/
|
||||
export const _FILE_REMOVED = '_FILE_REMOVED';
|
||||
|
||||
/**
|
||||
* The type of redux action to remove a file from the backend.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_FILE,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
ADD_FILE,
|
||||
DOWNLOAD_FILE,
|
||||
REMOVE_FILE,
|
||||
UPDATE_FILE_UPLOAD_PROGRESS,
|
||||
UPLOAD_FILES,
|
||||
DOWNLOAD_FILE
|
||||
UPLOAD_FILES
|
||||
} from './actionTypes';
|
||||
import { IFileMetadata } from './types';
|
||||
|
||||
@@ -49,7 +49,7 @@ export function addFile(file: IFileMetadata) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file.
|
||||
* Remove a file from the backend.
|
||||
*
|
||||
* @param {string} fileId - The ID of the file to remove.
|
||||
* @returns {Object}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
import React, { useCallback, useState, useRef } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Avatar from '../../../base/avatar/components/Avatar';
|
||||
import { IconCloudUpload, IconDownload, IconTrash } from '../../../base/icons/svg';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import { IconCloudUpload, IconDownload, IconTrash } from '../../../base/icons/svg';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.web';
|
||||
import { downloadFile, removeFile, uploadFiles } from '../../actions';
|
||||
import { formatFileSize, formatTimestamp, getFileIcon } from '../../functions.any';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { downloadFile, removeFile } from '../../actions';
|
||||
import {
|
||||
formatFileSize,
|
||||
formatTimestamp,
|
||||
getFileIcon,
|
||||
isFileUploadingEnabled,
|
||||
processFiles
|
||||
} from '../../functions.any';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
@@ -51,7 +56,6 @@ const useStyles = makeStyles()(theme => {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
transition: 'opacity 0.15s ease-in-out',
|
||||
zIndex: 0,
|
||||
|
||||
'&.dragging': {
|
||||
@@ -205,9 +209,10 @@ const FileSharing = () => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const store = useStore();
|
||||
const { files } = useSelector((state: IReduxState) => state['features/file-sharing']);
|
||||
const sortedFiles = Array.from(files.values()).sort((a, b) => a.fileName.localeCompare(b.fileName));
|
||||
const isModerator = useSelector(isLocalParticipantModerator);
|
||||
const isUploadEnabled = useSelector(isFileUploadingEnabled);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -226,15 +231,10 @@ const FileSharing = () => {
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const processFiles = useCallback((fileList: FileList | File[]) => {
|
||||
const newFiles = Array.from(fileList);
|
||||
|
||||
dispatch(uploadFiles(newFiles));
|
||||
}, [ dispatch ]);
|
||||
|
||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files) {
|
||||
processFiles(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 ]);
|
||||
|
||||
@@ -243,8 +243,8 @@ const FileSharing = () => {
|
||||
e.stopPropagation();
|
||||
setIsDragging(false);
|
||||
|
||||
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
||||
processFiles(e.dataTransfer.files);
|
||||
if (e.dataTransfer.files?.length > 0) {
|
||||
processFiles(e.dataTransfer.files as FileList, store);
|
||||
}
|
||||
}, [ processFiles ]);
|
||||
|
||||
@@ -261,7 +261,7 @@ const FileSharing = () => {
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
{
|
||||
isModerator && (
|
||||
isUploadEnabled && (
|
||||
<>
|
||||
<div
|
||||
className = { `${classes.dropZone} ${
|
||||
@@ -347,7 +347,7 @@ const FileSharing = () => {
|
||||
size = { 24 }
|
||||
src = { IconDownload } />
|
||||
{
|
||||
isModerator && (
|
||||
isUploadEnabled && (
|
||||
<Icon
|
||||
className = { `${classes.actionIcon} actionIconVisibility` }
|
||||
color = { BaseTheme.palette.icon01 }
|
||||
@@ -378,7 +378,7 @@ const FileSharing = () => {
|
||||
)
|
||||
}
|
||||
{
|
||||
isModerator && (
|
||||
isUploadEnabled && (
|
||||
<Button
|
||||
accessibilityLabel = { t('fileSharing.uploadFile') }
|
||||
className = { classes.uploadButton }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
* The key for file sharing metadata in the conference.
|
||||
* The max file size we accept for upload.
|
||||
*/
|
||||
export const FILE_SHARING_ID = 'files';
|
||||
export const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { IReduxState } from '../app/types';
|
||||
import { IReduxState, IStore } from '../app/types';
|
||||
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
import {
|
||||
IconImage,
|
||||
IconShareDoc,
|
||||
IconVideo,
|
||||
IconVolumeUp
|
||||
} from '../base/icons/svg';
|
||||
import { getLocalizedDateFormatter } from '../base/i18n/dateUtil';
|
||||
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';
|
||||
|
||||
/**
|
||||
* Checks whether file sharing feature is enabled.
|
||||
@@ -92,3 +100,58 @@ export function formatTimestamp(timestamp: number): string {
|
||||
|
||||
return `${monthDay}\n${time}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a list of files for upload.
|
||||
*
|
||||
* @param {FileList|File[]} fileList - The list of files to process.
|
||||
* @param {Dispatch} dispatch - The Redux dispatch function.
|
||||
* @returns {void}
|
||||
*/
|
||||
// @ts-ignore
|
||||
export const processFiles = (fileList: FileList | File[], store: IStore) => {
|
||||
const state = store.getState();
|
||||
const dispatch = store.dispatch;
|
||||
|
||||
const { maxFileSize = MAX_FILE_SIZE } = state['features/base/config']?.fileSharing ?? {};
|
||||
|
||||
const newFiles = Array.from(fileList as File[]).filter((file: File) => {
|
||||
|
||||
// No file size limitation
|
||||
if (maxFileSize === -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check file size before upload
|
||||
if (file.size > maxFileSize) {
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.fileTooLargeTitle',
|
||||
descriptionKey: 'fileSharing.fileTooLargeDescription',
|
||||
descriptionArguments: {
|
||||
maxFileSize: formatFileSize(maxFileSize)
|
||||
},
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (newFiles.length > 0) {
|
||||
dispatch(uploadFiles(newFiles as File[]));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines if file uploading is enabled based on JWT feature flags and file sharing settings.
|
||||
*
|
||||
* @param {IReduxState} state - Current state.
|
||||
* @returns {boolean} Indication of whether local user can upload files.
|
||||
*/
|
||||
export function isFileUploadingEnabled(state: IReduxState): boolean {
|
||||
return !iAmVisitor(state)
|
||||
&& isJwtFeatureEnabled(state, MEET_FEATURES.FILE_UPLOAD, false)
|
||||
&& isFileSharingEnabled(state);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { IStore } from '../app/types';
|
||||
import { getCurrentConference } from '../base/conference/functions';
|
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
|
||||
import { getLocalParticipant, getParticipantDisplayName } from '../base/participants/functions';
|
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry';
|
||||
import { showErrorNotification } from '../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, NOTIFICATION_TYPE } from '../notifications/constants';
|
||||
|
||||
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES } from './actionTypes';
|
||||
import { DOWNLOAD_FILE, REMOVE_FILE, UPLOAD_FILES, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
|
||||
import { addFile, removeFile, updateFileProgress } from './actions';
|
||||
import { getFileExtension } from './functions.any';
|
||||
import logger from './logger';
|
||||
import { FILE_SHARING_ID } from './constants';
|
||||
import { IFileMetadata } from './types';
|
||||
import { downloadFile } from './utils';
|
||||
|
||||
|
||||
/**
|
||||
* Registers a change handler for state['features/base/conference'].conference to
|
||||
* set the event listeners needed for the file sharing feature to operate.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => state['features/base/conference'].conference,
|
||||
(conference, { dispatch }, previousConference) => {
|
||||
if (conference && !previousConference) {
|
||||
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_ADDED, (file: IFileMetadata) => {
|
||||
dispatch(addFile(file));
|
||||
});
|
||||
conference.on(JitsiConferenceEvents.FILE_SHARING_FILE_REMOVED, (fileId: string) => {
|
||||
dispatch({
|
||||
type: _FILE_REMOVED,
|
||||
fileId
|
||||
});
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.FILE_SHARING_FILES_RECEIVED, (files: object) => {
|
||||
dispatch({
|
||||
type: _FILE_LIST_RECEIVED,
|
||||
files
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Middleware that handles file sharing actions.
|
||||
@@ -24,93 +55,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case UPLOAD_FILES: {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { connection } = state['features/base/connection'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const roomJid = conference?.room?.roomjid;
|
||||
|
||||
for (const file of action.files) {
|
||||
const jid = connection!.getJid();
|
||||
const fileId = uuidv4();
|
||||
const fileMetadata: IFileMetadata = {
|
||||
authorParticipantId: localParticipant!.id,
|
||||
authorParticipantJid: jid,
|
||||
authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
|
||||
conferenceFullName: roomJid ?? '',
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: getFileExtension(file.name),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
store.dispatch(addFile(fileMetadata));
|
||||
store.dispatch(updateFileProgress(fileId, 1));
|
||||
|
||||
// Upload file.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('metadata', JSON.stringify(fileMetadata));
|
||||
|
||||
// @ts-ignore
|
||||
formData.append('file', file as Blob, file.name);
|
||||
|
||||
// Use XMLHttpRequest to track upload
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const handleError = () => {
|
||||
logger.warn('Could not upload file:', xhr.statusText);
|
||||
|
||||
store.dispatch(removeFile(fileId));
|
||||
store.dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.uploadFailedTitle',
|
||||
descriptionKey: 'fileSharing.uploadFailedDescription',
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
};
|
||||
|
||||
xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
if (jwt) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${jwt}`);
|
||||
conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType()).then((token: string) => {
|
||||
for (const file of action.files) {
|
||||
uploadFile(file, store, token);
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = event => {
|
||||
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);
|
||||
|
||||
store.dispatch(updateFileProgress(fileId, percent));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
store.dispatch(updateFileProgress(fileId, 100));
|
||||
|
||||
const metadataHandler = conference?.getMetadataHandler();
|
||||
|
||||
if (metadataHandler) {
|
||||
const existingMetadata = metadataHandler.getMetadata()[FILE_SHARING_ID] ?? {};
|
||||
|
||||
metadataHandler.setMetadata(FILE_SHARING_ID, {
|
||||
...existingMetadata,
|
||||
[fileId]: fileMetadata
|
||||
});
|
||||
}
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = handleError;
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
});
|
||||
|
||||
return next(action);
|
||||
}
|
||||
@@ -118,41 +68,41 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case REMOVE_FILE: {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { jwt } = state['features/base/jwt'];
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
let doDelete = false;
|
||||
const { files } = state['features/file-sharing'];
|
||||
const fileId = action.fileId;
|
||||
const existingMetadata = files.get(fileId);
|
||||
|
||||
// First remove the file metadata so others won't attempt to download it anymore.
|
||||
const metadataHandler = conference?.getMetadataHandler();
|
||||
|
||||
if (metadataHandler) {
|
||||
const existingMetadata = metadataHandler.getMetadata()[FILE_SHARING_ID] ?? {};
|
||||
const fileMetadata = existingMetadata[action.fileId];
|
||||
|
||||
doDelete = (fileMetadata?.process ?? 100) === 100;
|
||||
|
||||
delete existingMetadata[action.fileId];
|
||||
metadataHandler.setMetadata(FILE_SHARING_ID, existingMetadata);
|
||||
}
|
||||
|
||||
if (!doDelete) {
|
||||
// ignore remove a file till the file is actually uploaded
|
||||
if (!conference || (existingMetadata?.progress ?? 100) !== 100) {
|
||||
return next(action);
|
||||
}
|
||||
|
||||
// First, remove the file metadata so others won't attempt to download it anymore.
|
||||
conference.getFileSharing().removeFile(fileId);
|
||||
|
||||
// remove it from local state
|
||||
store.dispatch({
|
||||
type: _FILE_REMOVED,
|
||||
fileId
|
||||
});
|
||||
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const sessionId = conference.getMeetingUniqueId();
|
||||
|
||||
// Now delete it from the server.
|
||||
fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
|
||||
conference.getShortTermCredentials(conference.getFileSharing().getIdentityType())
|
||||
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${fileId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
...jwt && { 'Authorization': `Bearer ${jwt}` }
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
}))
|
||||
.then((response: { ok: any; statusText: any; }) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete file: ${response.statusText}`);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not delete file:', error);
|
||||
});
|
||||
|
||||
@@ -165,30 +115,31 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
|
||||
fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/document`, {
|
||||
conference?.getShortTermCredentials(conference?.getFileSharing()?.getIdentityType())
|
||||
.then((token: string) => fetch(`${fileSharing!.apiUrl!}/sessions/${sessionId}/files/${action.fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-File-Id': action.fileId,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const url = data.presignedUrl;
|
||||
}))
|
||||
.then((response: any) => response.json())
|
||||
.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 => {
|
||||
.catch((error: any) => {
|
||||
logger.warn('Could not download file:', error);
|
||||
|
||||
store.dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.downloadFailedTitle',
|
||||
descriptionKey: 'fileSharing.downloadFailedDescription',
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
});
|
||||
|
||||
return next(action);
|
||||
@@ -197,3 +148,93 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Uploads a file to the server.
|
||||
*
|
||||
* @param {File} file - The file to upload.
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {string} token - The token to use for requests.
|
||||
* @returns {void}
|
||||
*/
|
||||
function uploadFile(file: File, store: IStore, token: string): void {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const sessionId = conference?.getMeetingUniqueId();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const { fileSharing } = state['features/base/config'];
|
||||
const { connection } = state['features/base/connection'];
|
||||
const roomJid = conference?.room?.roomjid;
|
||||
|
||||
const jid = connection!.getJid();
|
||||
const fileId = uuidv4();
|
||||
const fileMetadata: IFileMetadata = {
|
||||
authorParticipantId: localParticipant!.id,
|
||||
authorParticipantJid: jid,
|
||||
authorParticipantName: getParticipantDisplayName(state, localParticipant!.id),
|
||||
conferenceFullName: roomJid ?? '',
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
fileSize: file.size,
|
||||
fileType: getFileExtension(file.name),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
store.dispatch(addFile(fileMetadata));
|
||||
store.dispatch(updateFileProgress(fileId, 1));
|
||||
|
||||
// Upload file.
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('metadata', JSON.stringify(fileMetadata));
|
||||
|
||||
// @ts-ignore
|
||||
formData.append('file', file as Blob, file.name);
|
||||
|
||||
// Use XMLHttpRequest to track upload
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
const handleError = () => {
|
||||
logger.warn('Could not upload file:', xhr.statusText);
|
||||
|
||||
store.dispatch(removeFile(fileId));
|
||||
store.dispatch(showErrorNotification({
|
||||
titleKey: 'fileSharing.uploadFailedTitle',
|
||||
descriptionKey: 'fileSharing.uploadFailedDescription',
|
||||
appearance: NOTIFICATION_TYPE.ERROR
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
};
|
||||
|
||||
xhr.open('POST', `${fileSharing!.apiUrl!}/sessions/${sessionId}/files`);
|
||||
xhr.responseType = 'json';
|
||||
|
||||
if (token) {
|
||||
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
xhr.upload.onprogress = event => {
|
||||
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.min((event.loaded / event.total) * 100, 99);
|
||||
|
||||
store.dispatch(updateFileProgress(fileId, percent));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
store.dispatch(updateFileProgress(fileId, 100));
|
||||
|
||||
const fileSharingHandler = conference?.getFileSharing();
|
||||
|
||||
fileSharingHandler.addFile(fileMetadata);
|
||||
} else {
|
||||
handleError();
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = handleError;
|
||||
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../base/conference/actionTypes';
|
||||
import ReducerRegistry from '../base/redux/ReducerRegistry';
|
||||
|
||||
import {
|
||||
ADD_FILE,
|
||||
REMOVE_FILE,
|
||||
UPDATE_FILE_UPLOAD_PROGRESS
|
||||
} from './actionTypes';
|
||||
import { FILE_SHARING_ID } from './constants';
|
||||
import { ADD_FILE, UPDATE_FILE_UPLOAD_PROGRESS, _FILE_LIST_RECEIVED, _FILE_REMOVED } from './actionTypes';
|
||||
import { IFileMetadata } from './types';
|
||||
|
||||
export interface IFileSharingState {
|
||||
@@ -30,7 +24,7 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
|
||||
};
|
||||
}
|
||||
|
||||
case REMOVE_FILE: {
|
||||
case _FILE_REMOVED: {
|
||||
const newFiles = new Map(state.files);
|
||||
|
||||
newFiles.delete(action.fileId);
|
||||
@@ -53,22 +47,10 @@ ReducerRegistry.register<IFileSharingState>('features/file-sharing',
|
||||
};
|
||||
}
|
||||
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
const files = metadata?.[FILE_SHARING_ID];
|
||||
|
||||
if (files) {
|
||||
const newFiles: Map<string, IFileMetadata> = new Map(Object.entries(files));
|
||||
|
||||
return {
|
||||
files: new Map([
|
||||
...state.files,
|
||||
...newFiles
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
case _FILE_LIST_RECEIVED: {
|
||||
return {
|
||||
files: new Map(Object.entries(action.files))
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
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);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import { throttle } from 'lodash-es';
|
||||
import React, { PureComponent } from 'react';
|
||||
@@ -13,6 +14,7 @@ import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconArrowDown, IconArrowUp } from '../../../base/icons/svg';
|
||||
import { isNarrowScreenWithChatOpen } from '../../../base/responsive-ui/functions';
|
||||
import { getHideSelfView } from '../../../base/settings/functions.any';
|
||||
import { registerShortcut, unregisterShortcut } from '../../../keyboard-shortcuts/actions';
|
||||
import { showToolbox } from '../../../toolbox/actions.web';
|
||||
@@ -47,8 +49,6 @@ import {
|
||||
import AudioTracksContainer from './AudioTracksContainer';
|
||||
import Thumbnail from './Thumbnail';
|
||||
import ThumbnailWrapper from './ThumbnailWrapper';
|
||||
import { Theme } from '@mui/material';
|
||||
import { isNarrowScreenWithChatOpen } from '../../../base/responsive-ui/functions';
|
||||
|
||||
|
||||
const BACKGROUND_COLOR = 'rgba(51, 51, 51, .5)';
|
||||
@@ -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
|
||||
|
||||
@@ -588,7 +588,7 @@ function _registerForNativeEvents(store: IStore) {
|
||||
}
|
||||
|
||||
if (transcription) {
|
||||
store.dispatch(setRequestingSubtitles(true, false, null));
|
||||
store.dispatch(setRequestingSubtitles(true, false, null, true));
|
||||
conference.getMetadataHandler().setMetadata(RECORDING_METADATA_ID, {
|
||||
isTranscribingEnabled: true
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ type ErrorType = {
|
||||
*/
|
||||
const RN_NO_RELOAD_DIALOG_ERRORS = [
|
||||
JitsiConnectionErrors.NOT_LIVE_ERROR,
|
||||
JitsiConnectionErrors.SHARD_CHANGED_ERROR,
|
||||
JitsiConferenceErrors.CONFERENCE_ACCESS_DENIED,
|
||||
JitsiConferenceErrors.CONFERENCE_DESTROYED,
|
||||
JitsiConferenceErrors.CONNECTION_ERROR,
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../../../breakout-rooms/functions';
|
||||
import { doInvitePeople } from '../../../invite/actions.native';
|
||||
import { getInviteOthersControl } from '../../../share-room/functions';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { participantMatchesSearch, shouldRenderInviteButton } from '../../functions';
|
||||
|
||||
import MeetingParticipantItem from './MeetingParticipantItem';
|
||||
@@ -37,6 +38,7 @@ const MeetingParticipantList = () => {
|
||||
const keyExtractor
|
||||
= useCallback((e: undefined, i: number) => i.toString(), []);
|
||||
const localParticipant = useSelector(getLocalParticipant);
|
||||
const _iAmVisitor = useSelector(iAmVisitor);
|
||||
const onInvite = useCallback(() => {
|
||||
setShareDialogVisiblity(isAddPeopleFeatureEnabled, dispatch);
|
||||
dispatch(doInvitePeople());
|
||||
@@ -103,7 +105,10 @@ const MeetingParticipantList = () => {
|
||||
placeholder = { t('participantsPane.search') }
|
||||
value = { searchString } />
|
||||
<FlatList
|
||||
data = { [ localParticipant?.id, ...sortedRemoteParticipants ] as Array<any> }
|
||||
data = { _iAmVisitor
|
||||
? [ ...sortedRemoteParticipants ]
|
||||
: [ localParticipant?.id, ...sortedRemoteParticipants ] as Array<any>
|
||||
}
|
||||
keyExtractor = { keyExtractor }
|
||||
|
||||
/* eslint-disable react/jsx-no-bind */
|
||||
|
||||
@@ -2,12 +2,12 @@ import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
const ParticipantsCounter = () => {
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
const participantsCount = useSelector(getParticipantCountForDisplay);
|
||||
|
||||
return <Text style = { styles.participantsBadge }>{participantsCount}</Text>;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconUsers } from '../../../base/icons/svg';
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { navigate }
|
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
@@ -83,7 +83,7 @@ class ParticipantsPaneButton extends AbstractButton<IProps> {
|
||||
*/
|
||||
function mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_participantsCount: getParticipantCount(state)
|
||||
_participantsCount: getParticipantCountForDisplay(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 { normalizeAccents } from '../../../base/util/strings.web';
|
||||
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
|
||||
import { isButtonEnabled, showOverflowDrawer } from '../../../toolbox/functions.web';
|
||||
import { muteRemote } from '../../../video-menu/actions.web';
|
||||
import { iAmVisitor } from '../../../visitors/functions';
|
||||
import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
|
||||
import { useParticipantDrawer } from '../../hooks';
|
||||
import RenameButton from '../breakout-rooms/components/web/RenameButton';
|
||||
@@ -174,12 +175,18 @@ function MeetingParticipants({
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
let sortedParticipantIds: any = getSortedParticipantIds(state);
|
||||
const _iAmVisitor = iAmVisitor(state);
|
||||
|
||||
// Filter out the virtual screenshare participants since we do not want them to be displayed as separate
|
||||
// participants in the participants pane.
|
||||
// Filter local participant when in visitor mode
|
||||
sortedParticipantIds = sortedParticipantIds.filter((id: any) => {
|
||||
const participant = getParticipantById(state, id);
|
||||
|
||||
if (_iAmVisitor && participant?.local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isScreenShareParticipant(participant);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { getParticipantCount } from '../../../base/participants/functions';
|
||||
import { getParticipantCountForDisplay } from '../../../base/participants/functions';
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
@@ -26,7 +26,7 @@ const useStyles = makeStyles()(theme => {
|
||||
|
||||
const ParticipantsCounter = () => {
|
||||
const { classes } = useStyles();
|
||||
const participantsCount = useSelector(getParticipantCount);
|
||||
const participantsCount = useSelector(getParticipantCountForDisplay);
|
||||
|
||||
return <span className = { classes.badge }>{participantsCount}</span>;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { makeStyles } from 'tss-react/mui';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import participantsPaneTheme from '../../../base/components/themes/participantsPaneTheme.json';
|
||||
import { openDialog } from '../../../base/dialog/actions';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
import { IconCloseLarge, IconDotsHorizontal } from '../../../base/icons/svg';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants/functions';
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
@@ -14,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,
|
||||
@@ -23,11 +25,11 @@ 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';
|
||||
import VisitorsList from './VisitorsList';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
|
||||
/**
|
||||
* Interface representing the properties used for styles.
|
||||
@@ -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 }>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user