mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-18 15:47:48 +00:00
Compare commits
157 Commits
8585
...
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 | ||
|
|
392d694563 | ||
|
|
107687583d | ||
|
|
0085544a36 | ||
|
|
79cab9e7df | ||
|
|
1be9107ac7 | ||
|
|
762d59a4ad | ||
|
|
b213c445de | ||
|
|
05079e5480 | ||
|
|
b087b54575 | ||
|
|
c586fd9592 | ||
|
|
b966796d0c | ||
|
|
734ab449a6 | ||
|
|
869a149ccd | ||
|
|
90a831074e | ||
|
|
3be2a2d607 | ||
|
|
ec61563e8a | ||
|
|
8459e38b30 | ||
|
|
916fd64178 | ||
|
|
34d7ff78c0 | ||
|
|
a99e122801 | ||
|
|
4735911b6c | ||
|
|
82f2060ebf | ||
|
|
0897db3efc | ||
|
|
4d7d1a84bb | ||
|
|
9f133b3a28 | ||
|
|
f24e02425f | ||
|
|
7d2771167a | ||
|
|
49fb2bbaa1 | ||
|
|
ca7ece97bd | ||
|
|
0c68a1eb51 | ||
|
|
8bab15d13a | ||
|
|
914ace576a | ||
|
|
9f80448f92 | ||
|
|
d5793c2445 | ||
|
|
805afd33d2 | ||
|
|
f22315cf92 | ||
|
|
0ee2378ff0 | ||
|
|
8157e528c3 | ||
|
|
ca16f54dc9 | ||
|
|
1825ab6c41 | ||
|
|
3d5c08d86f | ||
|
|
f2babbf994 | ||
|
|
403b9043b6 |
@@ -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'
|
||||
|
||||
152
conference.js
152
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,
|
||||
@@ -131,7 +131,6 @@ import {
|
||||
createLocalTracksF,
|
||||
getLocalJitsiAudioTrack,
|
||||
getLocalJitsiVideoTrack,
|
||||
getLocalTracks,
|
||||
getLocalVideoTrack,
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute
|
||||
@@ -206,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
|
||||
@@ -709,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
|
||||
@@ -732,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));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -802,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');
|
||||
|
||||
@@ -826,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));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1829,35 +1716,6 @@ export default {
|
||||
onStartMutedPolicyChanged(audio, video));
|
||||
}
|
||||
);
|
||||
room.on(JitsiConferenceEvents.STARTED_MUTED, () => {
|
||||
const audioMuted = room.isStartAudioMuted();
|
||||
const videoMuted = room.isStartVideoMuted();
|
||||
const localTracks = getLocalTracks(APP.store.getState()['features/base/tracks']);
|
||||
const promises = [];
|
||||
|
||||
APP.store.dispatch(setAudioMuted(audioMuted));
|
||||
APP.store.dispatch(setVideoMuted(videoMuted));
|
||||
|
||||
// Remove the tracks from the peerconnection.
|
||||
for (const track of localTracks) {
|
||||
// Always add the track on Safari because of a known issue where audio playout doesn't happen
|
||||
// if the user joins audio and video muted, i.e., if there is no local media capture.
|
||||
if (audioMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.AUDIO && !browser.isWebKitBased()) {
|
||||
promises.push(this.useAudioStream(null));
|
||||
}
|
||||
if (videoMuted && track.jitsiTrack?.getType() === MEDIA_TYPE.VIDEO) {
|
||||
promises.push(this.useVideoStream(null));
|
||||
}
|
||||
}
|
||||
|
||||
Promise.allSettled(promises)
|
||||
.then(() => {
|
||||
APP.store.dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
});
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.DATA_CHANNEL_OPENED, () => {
|
||||
|
||||
82
config.js
82
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.
|
||||
@@ -128,6 +133,9 @@ var config = {
|
||||
// Disables the reactions moderation feature.
|
||||
// disableReactionsModeration: false,
|
||||
|
||||
// Disables the reactions in chat feature.
|
||||
// disableReactionsInChat: false,
|
||||
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
|
||||
@@ -502,6 +510,15 @@ var config = {
|
||||
// // Enables automatic request of subtitles when transcriber is present in the meeting, uses the default
|
||||
// // language that is set
|
||||
// autoCaptionOnTranscribe: false,
|
||||
//
|
||||
// // 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,
|
||||
|
||||
// // Whether to invite jigasi when backend transcriptions are enabled (asyncTranscription is true in metadata).
|
||||
// // By default, we invite it.
|
||||
// inviteJigasiOnBackendTranscribing: true,
|
||||
// },
|
||||
|
||||
// Misc
|
||||
@@ -613,6 +630,7 @@ var config = {
|
||||
// medium: 5000,
|
||||
// long: 10000,
|
||||
// extraLong: 60000,
|
||||
// sticky: 0,
|
||||
// },
|
||||
|
||||
// // Options for the recording limit notification.
|
||||
@@ -830,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: [
|
||||
@@ -1114,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,
|
||||
@@ -1261,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.
|
||||
@@ -1292,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.
|
||||
@@ -1311,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'
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
|
||||
@@ -1365,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,
|
||||
@@ -1401,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
|
||||
@@ -1582,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"
|
||||
@@ -1781,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.
|
||||
@@ -1882,6 +1888,16 @@ var config = {
|
||||
|
||||
// If true remove the tint foreground on focused user camera in filmstrip
|
||||
// disableCameraTintForeground: false,
|
||||
|
||||
// File sharign service.
|
||||
// fileSharing: {
|
||||
// // The URL of the file sharing service API. See resources/file-sharing.yaml for more details.
|
||||
// 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,
|
||||
// },
|
||||
};
|
||||
|
||||
// Set the default values for JaaS customers
|
||||
|
||||
@@ -141,32 +141,6 @@
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.smileys-panel {
|
||||
bottom: 100%;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(0, 0, 0, .6) !important;
|
||||
height: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
width: calc(#{$sidebarWidth} - 32px);
|
||||
margin-bottom: 5px;
|
||||
margin-left: -5px;
|
||||
|
||||
/**
|
||||
* CSS transitions do not apply for auto dimensions. So to produce the css
|
||||
* accordion effect for showing and hiding the smiley-panel, while allowing
|
||||
* for variable panel, height, use a very large max-height and animate off
|
||||
* of that.
|
||||
*/
|
||||
transition: max-height 0.3s;
|
||||
|
||||
#smileysContainer {
|
||||
background-color: $chatBackgroundColor;
|
||||
border-top: 1px solid #A4B8D1;
|
||||
}
|
||||
}
|
||||
|
||||
#smileysContainer .smiley {
|
||||
font-size: 1.625rem;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,3 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-browser.shift-right {
|
||||
.participants_pane {
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,21 +60,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.desktop-browser {
|
||||
&.shift-right {
|
||||
|
||||
@media only screen and (max-width: $verySmallScreen + $sidebarWidth) {
|
||||
|
||||
#videoResolutionLabel {
|
||||
display: none;
|
||||
}
|
||||
.vertical-filmstrip .filmstrip {
|
||||
display: none;
|
||||
}
|
||||
.chrome-extension-banner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ $newToolbarSizeWithPadding: calc(#{$newToolbarSize} + 24px);
|
||||
* Chat
|
||||
*/
|
||||
$chatBackgroundColor: #131519;
|
||||
$sidebarWidth: 315px;
|
||||
|
||||
/**
|
||||
* Misc.
|
||||
|
||||
@@ -91,15 +91,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shift-right .remote-videos > div {
|
||||
/**
|
||||
* Max-width corresponding to the ASPECT_RATIO_BREAKPOINT from features/filmstrip/constants,
|
||||
* from which we subtract the chat size.
|
||||
*/
|
||||
@media only screen and (max-width: calc(500px + #{$sidebarWidth})) {
|
||||
video {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
@@ -124,7 +125,8 @@
|
||||
"title": "Inserisci un nickname per usare la chat",
|
||||
"titleWithCC": "Inserisci un nickname per usare la chat e i sottotitoli",
|
||||
"titleWithPolls": "Inserisci un nickname per usare la chat e i sondaggi",
|
||||
"titleWithPollsAndCC": "Inserisci un nickname per usare la chat, i sondaggi e i sottotitoli"
|
||||
"titleWithPollsAndCC": "Inserisci un nickname per usare la chat, i sondaggi e i sottotitoli",
|
||||
"titleWithPollsAndCCAndFileSharing": "Inserisci un nickname per usare la chat, i sondaggi, i sottotitoli e i file"
|
||||
},
|
||||
"noMessagesMessage": "Non ci sono ancora messaggi nella riunione. Inizia una conversazione, qui!",
|
||||
"privateNotice": "Messaggio privato a {{recipient}}",
|
||||
@@ -134,12 +136,14 @@
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"closedCaptions": "Sottotitoli",
|
||||
"fileSharing": "File",
|
||||
"polls": "Sondaggi"
|
||||
},
|
||||
"title": "Chat",
|
||||
"titleWithCC": "Chat e sottotitoli",
|
||||
"titleWithPolls": "Chat e sondaggi",
|
||||
"titleWithPollsAndCC": "Chat, sondaggi e sottotitoli",
|
||||
"titleWithCC": "Sottotitoli",
|
||||
"titleWithFeatures": "Chat e",
|
||||
"titleWithFileSharing": "File",
|
||||
"titleWithPolls": "Sondaggi",
|
||||
"you": "tu"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
@@ -272,7 +276,8 @@
|
||||
"Remove": "Rimuovi",
|
||||
"Share": "Condividi",
|
||||
"Submit": "Invia",
|
||||
"Understand": "Accetto",
|
||||
"Understand": "Accetto, mantieni microfono e videocamera disattivati per ora",
|
||||
"UnderstandAndUnmute": "Accetto, riattiva microfono e videocamera",
|
||||
"WaitForHostMsg": "La riunione non è ancora iniziata. Se sei l'organizzatore, per favore autenticati. Altrimenti, attendi l'arrivo dell'organizzatore.",
|
||||
"WaitForHostNoAuthMsg": "La riunione non è ancora iniziata perché nessun organizzatore si è ancora collegato. Si prega di attendere.",
|
||||
"WaitingForHostButton": "Attendi l'organizzatore",
|
||||
@@ -309,6 +314,7 @@
|
||||
"conferenceReloadMsg": "Risoluzione del problema in corso. Tentativo di riconnessione tra {{seconds}} secondi…",
|
||||
"conferenceReloadTitle": "Purtroppo qualcosa è andato storto.",
|
||||
"confirm": "Conferma",
|
||||
"confirmBack": "Indietro",
|
||||
"confirmNo": "No",
|
||||
"confirmYes": "Sì",
|
||||
"connectError": "Ops! Qualcosa è andato storto e non è stato possibile collegarsi alla riunione.",
|
||||
@@ -346,6 +352,7 @@
|
||||
"kickParticipantTitle": "Espellere questo partecipante?",
|
||||
"kickSystemTitle": "Oh! Sei stato espulso dalla riunione",
|
||||
"kickTitle": "Oh! {{participantDisplayName}} ti ha espulso dalla riunione.",
|
||||
"learnMore": "ulteriori informazioni",
|
||||
"linkMeeting": "Collega la riunione",
|
||||
"linkMeetingTitle": "Collega la riunione a Salesforce",
|
||||
"liveStreaming": "Diretta",
|
||||
@@ -403,7 +410,9 @@
|
||||
"recentlyUsedObjects": "Oggetti usati di recente",
|
||||
"recording": "Registrazione",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Impossibile durante una diretta.",
|
||||
"recordingInProgressDescription": "La registrazione di questa riunione è in corso. I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
|
||||
"recordingInProgressDescription": "Questa riunione sta venendo registrata e analizzata dall'IA. I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
|
||||
"recordingInProgressDescriptionFirstHalf": "Questa riunione sta venendo registrata e analizzata dall'IA",
|
||||
"recordingInProgressDescriptionSecondHalf": ". I tuoi audio e video sono stati disattivati. Riattivandoli, acconsenti ad essere registrato.",
|
||||
"recordingInProgressTitle": "Registrazione in corso",
|
||||
"rejoinNow": "Ricollegati ora",
|
||||
"remoteControlAllowedMessage": "{{user}} ha accettato la tua richiesta di controllo remoto!",
|
||||
@@ -534,6 +543,19 @@
|
||||
"veryBad": "Pessima",
|
||||
"veryGood": "Ottima"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "Si prega di riprovare.",
|
||||
"downloadFailedTitle": "Download non riuscito",
|
||||
"downloadFile": "Download",
|
||||
"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",
|
||||
"uploadFile": "Condividi file"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Miniatura video"
|
||||
@@ -887,6 +909,7 @@
|
||||
"visitorInQueue": " ({{count}} in attesa)",
|
||||
"visitorRequests": " ({{count}} richiesta/e)",
|
||||
"visitors": "Spettatori {{count}}",
|
||||
"visitorsList": "Spettatori ({{count}})",
|
||||
"waitingLobby": "({{count}}) in attesa"
|
||||
},
|
||||
"search": "Cerca partecipanti",
|
||||
@@ -927,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"
|
||||
|
||||
@@ -122,7 +122,9 @@
|
||||
"nickname": {
|
||||
"popover": "Izvēlieties vārdu",
|
||||
"title": "Ierakstiet vārdu, lai izmantotu tērzēšanā",
|
||||
"titleWithPolls": "Ierakstiet vārdu, lai izmantotu tērzēšanā un aptaujās"
|
||||
"titleWithCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā un slēptos subtitros",
|
||||
"titleWithPolls": "Ierakstiet segvārdu, lai izmantotu tērzēšanā un aptaujās",
|
||||
"titleWithPollsAndCC": "Ievadiet segvārdu, lai izmantotu tērzēšanā, aptaujās un slēptos subtitros"
|
||||
},
|
||||
"noMessagesMessage": "Sapulcē pagaidām nav nevienas ziņas. Uzsāciet saraksti!",
|
||||
"privateNotice": "Privāta ziņa adresātam {{recipient}}",
|
||||
@@ -131,10 +133,13 @@
|
||||
"systemDisplayName": "Sistēma",
|
||||
"tabs": {
|
||||
"chat": "Tērzēšana",
|
||||
"closedCaptions": "Slēptie subtitri",
|
||||
"polls": "Aptaujas"
|
||||
},
|
||||
"title": "Tērzēšana",
|
||||
"titleWithCC": "Tērzēšana un slēptie subtitri",
|
||||
"titleWithPolls": "Tērzēšana un Aptaujas",
|
||||
"titleWithPollsAndCC": "Tērzēšana, Aptaujas un Slēptie subtitri",
|
||||
"you": "jūs"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
@@ -144,6 +149,10 @@
|
||||
"dontShowAgain": "Nerādīt man šo vēlreiz",
|
||||
"installExtensionText": "Uzstādīt spraudni Google kalendāra un Office 365 integrācijai"
|
||||
},
|
||||
"closedCaptionsTab": {
|
||||
"emptyState": "Slēpto subtitru saturs būs pieejams, tiklīdz moderators uzsāks to.",
|
||||
"startClosedCaptionsButton": "Uzsākt slēptos subtitrus"
|
||||
},
|
||||
"connectingOverlay": {
|
||||
"joiningRoom": "Notiek pieslēgšanās jūsu sapulcei…"
|
||||
},
|
||||
@@ -264,6 +273,7 @@
|
||||
"Share": "Kopīgot",
|
||||
"Submit": "Iesniegt",
|
||||
"Understand": "Saprotu",
|
||||
"UnderstandAndUnmute": "Es saprotu, lūdzu, ieslēdziet skaņu.",
|
||||
"WaitForHostMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, autorizējieties, lai kļūtu par moderatoru. Pretējā gadījumā, lūdzu, uzgaidiet.",
|
||||
"WaitForHostNoAuthMsg": "Sapulce vēl nav sākusies, jo vēl nav ieradies neviens moderators. Lūdzu, uzgaidiet.",
|
||||
"WaitingForHostButton": "Gaidīt rīkotāju",
|
||||
@@ -300,6 +310,7 @@
|
||||
"conferenceReloadMsg": "Cenšamies to labot. Atkārtota savienojuma izveide pēc {{seconds}} sek….",
|
||||
"conferenceReloadTitle": "Diemžēl kaut kas nogāja greizi.",
|
||||
"confirm": "Apstiprināt",
|
||||
"confirmBack": "Atpakaļ",
|
||||
"confirmNo": "Nē",
|
||||
"confirmYes": "Jā",
|
||||
"connectError": "Hmm! Radās problēma, un mēs nevarējām izveidot savienojumu ar sapulci.",
|
||||
@@ -337,6 +348,7 @@
|
||||
"kickParticipantTitle": "Izraidīt šo dalībnieku?",
|
||||
"kickSystemTitle": "Ak! Jūs izraidīja no sapulces",
|
||||
"kickTitle": "Ak! {{participantDisplayName}} izraidīja jūs no sapulces",
|
||||
"learnMore": "uzzināt vairāk",
|
||||
"linkMeeting": "Sasaistīt sapulci",
|
||||
"linkMeetingTitle": "Sasaistīt sapulci ar Salesforce",
|
||||
"liveStreaming": "Tiešraides straumēšana",
|
||||
@@ -395,6 +407,8 @@
|
||||
"recording": "Ieraksts",
|
||||
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Nav iespējams, kamēr ir aktīva tiešraides straume",
|
||||
"recordingInProgressDescription": "Šī sapulce tiek ierakstīta. Jūsu audio un video ir izslēgti. Ja izvēlaties ieslēgt skaņu vai video, jūs piekrītat ierakstīšanai.",
|
||||
"recordingInProgressDescriptionFirstHalf": "Šo sanāksmi ieraksta un analizē mākslīgais intelekts",
|
||||
"recordingInProgressDescriptionSecondHalf": ". Jūsu audio un video skaņa ir izslēgta. Ja izvēlaties ieslēgt skaņu, jūs piekrītat ierakstīšanai.",
|
||||
"recordingInProgressTitle": "Notiek ierakstīšana",
|
||||
"rejoinNow": "Pieslēgties no jauna",
|
||||
"remoteControlAllowedMessage": "{{user}} apstiprināja jūsu attālās pārvaldības pieprasījumu!",
|
||||
@@ -752,7 +766,8 @@
|
||||
"dataChannelClosedDescriptionWithAudio": "Savienojuma kanāls nedarbojas, tāpēc var rasties audio un video traucējumi.",
|
||||
"dataChannelClosedWithAudio": "Audio un video kvalitāte var būt traucēta",
|
||||
"disabledIframe": "Iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm.",
|
||||
"disabledIframeSecondary": "{{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks atvienots pēc {{timeout}} minūtēm. Lūdzu, izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi kā Pakalpojums</a> produkcijas iegulšanai!",
|
||||
"disabledIframeSecondaryNative": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm.",
|
||||
"disabledIframeSecondaryWeb": "Domēna {{domain}} iegulšana ir paredzēta tikai demonstrācijas nolūkiem, tāpēc šis zvans tiks pārtraukts pēc {{timeout}} minūtēm. Lūdzu, produkcijas videi izmantojiet <a href='{{jaasDomain}}' rel='noopener noreferrer' target='_blank'>Jitsi as a Service</a>!",
|
||||
"disconnected": "savienojums pārtraukts",
|
||||
"displayNotifications": "Rādīt paziņojumus",
|
||||
"dontRemindMe": "Neatgādināt man",
|
||||
@@ -880,6 +895,7 @@
|
||||
"waitingLobby": "Gaida vestibilā ({{count}})"
|
||||
},
|
||||
"search": "Meklēt dalībniekus",
|
||||
"searchDescription": "Sāciet rakstīt, lai atlasītu dalībnieks",
|
||||
"title": "Dalībnieki"
|
||||
},
|
||||
"passwordDigitsOnly": "Līdz {{number}} cipariem",
|
||||
@@ -1106,6 +1122,7 @@
|
||||
"signedIn": "Pašreiz ir piekļuve e-pasta adreses {{email}} kalendāra notikumiem. Noklikšķiniet uz pogas |Atslēgt|, lai izslēgtu piekļuvi šiem kalendāra pasākumiem.",
|
||||
"title": "Kalendārs"
|
||||
},
|
||||
"chatWithPermissions": "Tērzēšanai nepieciešama atļauja",
|
||||
"desktopShareFramerate": "Darbvirsmas koplietošanas kadru ātrums",
|
||||
"desktopShareHighFpsWarning": "Lielāks kadru nomaiņas ātrums darbvirsmas koplietošanai var ietekmēt joslas platumu. Lai jaunie iestatījumi stātos spēkā, ir jārestartē ekrāna kopīgošana.",
|
||||
"desktopShareWarning": "Lai jaunie iestatījumi stātos spēkā, ir jārestartē ekrāna kopīgošana.",
|
||||
@@ -1135,6 +1152,7 @@
|
||||
"selectMic": "Mikrofons",
|
||||
"selfView": "Pašskats",
|
||||
"shortcuts": "Īsceļi",
|
||||
"showSubtitlesOnStage": "Rādīt subtitrus galvenajā skatā",
|
||||
"speakers": "Skaļruņi",
|
||||
"startAudioMuted": "Dalībnieki pievienojas ar izslēgtu mikrofonu",
|
||||
"startReactionsMuted": "Izslēgt reakcijas skaņas visiem",
|
||||
@@ -1194,6 +1212,7 @@
|
||||
"neutral": "Neitrāls",
|
||||
"sad": "Bēdīgs",
|
||||
"search": "Meklēt",
|
||||
"searchDescription": "Sāciet rakstīt, lai atlasītu dalībnieks",
|
||||
"searchHint": "Meklēt dalībniekus",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Dalībnieka uzstāšanās statistika",
|
||||
@@ -1230,6 +1249,7 @@
|
||||
"closeChat": "Aizvērt tērzēšanu",
|
||||
"closeMoreActions": "Aizvērt vairāk darbību izvēlni",
|
||||
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
|
||||
"closedCaptions": "Slēptie subtitri",
|
||||
"collapse": "Sakļaut",
|
||||
"document": "Kopīgotais dokuments (iesl./izsl.)",
|
||||
"documentClose": "Aizvērt kopīgoto dokumentu",
|
||||
@@ -1320,6 +1340,7 @@
|
||||
"closeChat": "Aizvērt tērzētavu",
|
||||
"closeParticipantsPane": "Aizvērt dalībnieku paneli",
|
||||
"closeReactionsMenu": "Aizvērt reakciju izvēlni",
|
||||
"closedCaptions": "Slēptie subtitri",
|
||||
"disableNoiseSuppression": "Atspējot trokšņu slāpēšanu",
|
||||
"disableReactionSounds": "Šai sapulcei varat atspējot reakcijas skaņas",
|
||||
"documentClose": "Aizvērt kopīgoto dokumentu",
|
||||
@@ -1412,13 +1433,16 @@
|
||||
"failed": "Atšifrējuma izveide neizdevās",
|
||||
"labelTooltip": "Šajā sapulcē notiek atšifrējuma izveide.",
|
||||
"labelTooltipExtra": "Turklāt vēlāk būs pieejams atšifrējums.",
|
||||
"openClosedCaptions": "Atvērt slēptos subtitrus",
|
||||
"original": "Oriģināls",
|
||||
"sourceLanguageDesc": "Pašlaik sapulces valoda ir iestatīta uz <b>{{sourceLanguage}}</b>. <br/> Varat to mainīt no ",
|
||||
"sourceLanguageHere": "šeit",
|
||||
"start": "Iesl. subtitru rādīšanu",
|
||||
"stop": "Izsl. subtitru rādīšanu",
|
||||
"subtitles": "Subtitri",
|
||||
"subtitlesOff": "Izslēgts",
|
||||
"tr": "TR"
|
||||
"tr": "TR",
|
||||
"translateTo": "Tulkot uz"
|
||||
},
|
||||
"unpinParticipant": "{{participantName}} — atspraust",
|
||||
"userMedia": {
|
||||
|
||||
@@ -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",
|
||||
@@ -124,7 +125,8 @@
|
||||
"title": "Enter a nickname to use chat",
|
||||
"titleWithCC": "Enter a nickname to use chat and closed captions",
|
||||
"titleWithPolls": "Enter a nickname to use chat and polls",
|
||||
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions"
|
||||
"titleWithPollsAndCC": "Enter a nickname to use chat, polls and closed captions",
|
||||
"titleWithPollsAndCCAndFileSharing": "Enter a nickname to use chat, polls, closed captions and files"
|
||||
},
|
||||
"noMessagesMessage": "There are no messages in the meeting yet. Start a conversation here!",
|
||||
"privateNotice": "Private message to {{recipient}}",
|
||||
@@ -134,19 +136,21 @@
|
||||
"tabs": {
|
||||
"chat": "Chat",
|
||||
"closedCaptions": "CC",
|
||||
"fileSharing": "Files",
|
||||
"polls": "Polls"
|
||||
},
|
||||
"title": "Chat",
|
||||
"titleWithCC": "Chat and CC",
|
||||
"titleWithPolls": "Chat and Polls",
|
||||
"titleWithPollsAndCC": "Chat, Polls and CC",
|
||||
"titleWithCC": "CC",
|
||||
"titleWithFeatures": "Chat and",
|
||||
"titleWithFileSharing": "Files",
|
||||
"titleWithPolls": "Polls",
|
||||
"you": "you"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"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": {
|
||||
@@ -229,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}}…",
|
||||
@@ -322,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.",
|
||||
@@ -428,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",
|
||||
@@ -457,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.",
|
||||
@@ -539,6 +543,19 @@
|
||||
"veryBad": "Very Bad",
|
||||
"veryGood": "Very Good"
|
||||
},
|
||||
"fileSharing": {
|
||||
"downloadFailedDescription": "Please try again.",
|
||||
"downloadFailedTitle": "Download failed",
|
||||
"downloadFile": "Download",
|
||||
"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",
|
||||
"uploadFile": "Share file"
|
||||
},
|
||||
"filmstrip": {
|
||||
"accessibilityLabel": {
|
||||
"heading": "Video thumbnails"
|
||||
@@ -648,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.",
|
||||
@@ -889,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",
|
||||
@@ -932,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"
|
||||
@@ -948,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!",
|
||||
@@ -1545,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',
|
||||
|
||||
@@ -478,9 +478,11 @@ export default class LargeVideoManager {
|
||||
if (isOpen && window.innerWidth > 580) {
|
||||
/**
|
||||
* If chat state is open, we re-compute the container width
|
||||
* by subtracting the default width of the chat.
|
||||
* by subtracting the chat width, which may be resized by the user.
|
||||
*/
|
||||
widthToUse -= CHAT_SIZE;
|
||||
const chatWidth = state['features/chat'].width?.current ?? CHAT_SIZE;
|
||||
|
||||
widthToUse -= chatWidth;
|
||||
}
|
||||
|
||||
if (resizableFilmstrip && visible && filmstripWidth.current >= FILMSTRIP_BREAKPOINT) {
|
||||
|
||||
3190
package-lock.json
generated
3190
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
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/v1984.0.0+dd4c41be/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",
|
||||
@@ -118,6 +119,7 @@
|
||||
"seamless-scroll-polyfill": "2.1.8",
|
||||
"semver": "7.5.4",
|
||||
"text-encoding": "0.7.0",
|
||||
"ts-ebml": "^3.0.1",
|
||||
"tss-react": "4.9.4",
|
||||
"util": "0.12.1",
|
||||
"uuid": "8.3.2",
|
||||
@@ -130,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",
|
||||
@@ -138,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",
|
||||
@@ -157,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",
|
||||
@@ -187,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;
|
||||
}
|
||||
|
||||
@@ -22,5 +22,6 @@ import '../toolbox/middleware';
|
||||
import '../face-landmarks/middleware';
|
||||
import '../gifs/middleware';
|
||||
import '../whiteboard/middleware.web';
|
||||
import '../file-sharing/middleware.web';
|
||||
|
||||
import './middlewares.any';
|
||||
|
||||
@@ -17,5 +17,6 @@ import '../screenshot-capture/reducer';
|
||||
import '../talk-while-muted/reducer';
|
||||
import '../virtual-background/reducer';
|
||||
import '../web-hid/reducer';
|
||||
import '../file-sharing/reducer';
|
||||
|
||||
import './reducers.any';
|
||||
|
||||
@@ -38,6 +38,7 @@ import { IE2EEState } from '../e2ee/reducer';
|
||||
import { IEtherpadState } from '../etherpad/reducer';
|
||||
import { IFaceLandmarksState } from '../face-landmarks/reducer';
|
||||
import { IFeedbackState } from '../feedback/reducer';
|
||||
import { IFileSharingState } from '../file-sharing/reducer';
|
||||
import { IFilmstripState } from '../filmstrip/reducer';
|
||||
import { IFollowMeState } from '../follow-me/reducer';
|
||||
import { IGifsState } from '../gifs/reducer';
|
||||
@@ -128,6 +129,7 @@ export interface IReduxState {
|
||||
'features/etherpad': IEtherpadState;
|
||||
'features/face-landmarks': IFaceLandmarksState;
|
||||
'features/feedback': IFeedbackState;
|
||||
'features/file-sharing': IFileSharingState;
|
||||
'features/filmstrip': IFilmstripState;
|
||||
'features/follow-me': IFollowMeState;
|
||||
'features/full-screen': IFullScreenState;
|
||||
|
||||
@@ -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() || '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../analytics/functions';
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { readyToClose } from '../../mobile/external-api/actions';
|
||||
import { transcriberJoined, transcriberLeft } from '../../transcribing/actions';
|
||||
@@ -12,9 +10,7 @@ import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants';
|
||||
import { hasAvailableDevices } from '../devices/functions.any';
|
||||
import JitsiMeetJS, { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet';
|
||||
import {
|
||||
setAudioMuted,
|
||||
setAudioUnmutePermissions,
|
||||
setVideoMuted,
|
||||
setVideoUnmutePermissions
|
||||
} from '../media/actions';
|
||||
import { MEDIA_TYPE, MediaType } from '../media/constants';
|
||||
@@ -32,7 +28,6 @@ import { IJitsiParticipant } from '../participants/types';
|
||||
import { toState } from '../redux/functions';
|
||||
import {
|
||||
destroyLocalTracks,
|
||||
replaceLocalTrack,
|
||||
trackAdded,
|
||||
trackRemoved
|
||||
} from '../tracks/actions.any';
|
||||
@@ -181,39 +176,6 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
|
||||
|
||||
// Dispatches into features/base/media follow:
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.STARTED_MUTED,
|
||||
() => {
|
||||
const audioMuted = Boolean(conference.isStartAudioMuted());
|
||||
const videoMuted = Boolean(conference.isStartVideoMuted());
|
||||
const localTracks = getLocalTracks(state['features/base/tracks']);
|
||||
|
||||
sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted));
|
||||
logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`);
|
||||
|
||||
// XXX Jicofo tells lib-jitsi-meet to start with audio and/or video
|
||||
// muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned
|
||||
// Jicofo's intent into reality by actually muting the respective
|
||||
// tracks. The reality is expressed in base/tracks already so what
|
||||
// is left is to express Jicofo's intent in base/media.
|
||||
// TODO Maybe the app needs to learn about Jicofo's intent and
|
||||
// transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet
|
||||
// acting on Jicofo's intent without the app's knowledge.
|
||||
dispatch(setAudioMuted(audioMuted));
|
||||
dispatch(setVideoMuted(videoMuted));
|
||||
|
||||
// Remove the tracks from peerconnection as well.
|
||||
for (const track of localTracks) {
|
||||
const trackType = track.jitsiTrack.getType();
|
||||
|
||||
// Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise.
|
||||
if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative')
|
||||
|| (videoMuted && trackType === MEDIA_TYPE.VIDEO)) {
|
||||
dispatch(replaceLocalTrack(track.jitsiTrack, null, conference));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
|
||||
(disableAudioMuteChange: boolean) => {
|
||||
@@ -826,10 +788,8 @@ export function nonParticipantMessageReceived(id: string, json: Object) {
|
||||
/**
|
||||
* Updates the known state of start muted policies.
|
||||
*
|
||||
* @param {boolean} audioMuted - Whether or not members will join the conference
|
||||
* as audio muted.
|
||||
* @param {boolean} videoMuted - Whether or not members will join the conference
|
||||
* as video muted.
|
||||
* @param {boolean} audioMuted - Whether or not members will join the conference as audio muted.
|
||||
* @param {boolean} videoMuted - Whether or not members will join the conference as video muted.
|
||||
* @returns {{
|
||||
* type: SET_START_MUTED_POLICY,
|
||||
* startAudioMutedPolicy: boolean,
|
||||
@@ -1040,10 +1000,8 @@ export function setRoom(room?: string) {
|
||||
/**
|
||||
* Sets whether or not members should join audio and/or video muted.
|
||||
*
|
||||
* @param {boolean} startAudioMuted - Whether or not members will join the
|
||||
* conference as audio muted.
|
||||
* @param {boolean} startVideoMuted - Whether or not members will join the
|
||||
* conference as video muted.
|
||||
* @param {boolean} startAudioMuted - Whether or not members will join the conference as audio muted.
|
||||
* @param {boolean} startVideoMuted - Whether or not members will join the conference as video muted.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function setStartMutedPolicy(
|
||||
@@ -1055,9 +1013,6 @@ export function setStartMutedPolicy(
|
||||
audio: startAudioMuted,
|
||||
video: startVideoMuted
|
||||
});
|
||||
|
||||
dispatch(
|
||||
onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
|
||||
return {
|
||||
hosts: config.oldConfig.hosts,
|
||||
focusUserJid: focusJid,
|
||||
disableLocalStats: false,
|
||||
disableLocalStatsBroadcast: false,
|
||||
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
|
||||
p2p: config.oldConfig.p2p,
|
||||
websocket: config.oldConfig.websocket
|
||||
@@ -330,7 +330,7 @@ export function getVisitorOptions(stateful: IStateful, vnode: string, focusJid:
|
||||
},
|
||||
focusUserJid: focusJid,
|
||||
disableFocus: true, // This flag disables sending the initial conference request
|
||||
disableLocalStats: true,
|
||||
disableLocalStatsBroadcast: true,
|
||||
bosh: config.bosh && appendURLParam(config.bosh, 'vnode', vnode),
|
||||
p2p: {
|
||||
...config.p2p,
|
||||
@@ -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 {
|
||||
@@ -70,6 +72,7 @@ import {
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { IConferenceMetadata } from './reducer';
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Handler for before unload event.
|
||||
@@ -96,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);
|
||||
|
||||
@@ -261,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());
|
||||
}
|
||||
@@ -653,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.
|
||||
@@ -661,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;
|
||||
@@ -670,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 }) => {
|
||||
|
||||
@@ -54,6 +54,19 @@ const DEFAULT_STATE = {
|
||||
};
|
||||
|
||||
export interface IConferenceMetadata {
|
||||
files: {
|
||||
[fileId: string]: {
|
||||
authorParticipantJid: string;
|
||||
authorParticipantName: string;
|
||||
conferenceFullName: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileType: string;
|
||||
progress?: number;
|
||||
timestamp: number;
|
||||
};
|
||||
};
|
||||
recording?: {
|
||||
isTranscribingEnabled: boolean;
|
||||
};
|
||||
@@ -85,6 +98,7 @@ export interface IJitsiConference {
|
||||
end: Function;
|
||||
getBreakoutRooms: Function;
|
||||
getConnection: Function;
|
||||
getFileSharing: Function;
|
||||
getLocalParticipantProperty: Function;
|
||||
getLocalTracks: Function;
|
||||
getMeetingUniqueId: Function;
|
||||
@@ -94,6 +108,7 @@ export interface IJitsiConference {
|
||||
getParticipantCount: Function;
|
||||
getParticipants: Function;
|
||||
getRole: Function;
|
||||
getShortTermCredentials: Function;
|
||||
getSpeakerStats: () => ISpeakerStats;
|
||||
getSsrcByTrack: Function;
|
||||
getTranscriptionStatus: Function;
|
||||
@@ -105,8 +120,6 @@ export interface IJitsiConference {
|
||||
isLobbySupported: Function;
|
||||
isP2PActive: Function;
|
||||
isSIPCallingSupported: Function;
|
||||
isStartAudioMuted: Function;
|
||||
isStartVideoMuted: Function;
|
||||
join: Function;
|
||||
joinLobby: Function;
|
||||
kickParticipant: Function;
|
||||
|
||||
61
react/features/base/conference/subscriber.ts
Normal file
61
react/features/base/conference/subscriber.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { setAudioMuted, setVideoMuted } from '../media/actions';
|
||||
import { VIDEO_MUTISM_AUTHORITY } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
let hasShownNotification = false;
|
||||
|
||||
/**
|
||||
* Handles changes in the start muted policy for audio and video tracks in the meta data set for the conference.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startAudioMutedPolicy,
|
||||
/* listener */ (startAudioMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, true);
|
||||
});
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startVideoMutedPolicy,
|
||||
/* listener */(startVideoMutedPolicy, store) => {
|
||||
_updateTrackMuteState(store, false);
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates the mute state of the track based on the start muted policy.
|
||||
*
|
||||
* @param {IStore} store - The redux store.
|
||||
* @param {boolean} isAudio - Whether the track is audio or video.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateTrackMuteState(store: IStore, isAudio: boolean) {
|
||||
const { dispatch, getState } = store;
|
||||
const mutedPolicyKey = isAudio ? 'startAudioMutedPolicy' : 'startVideoMutedPolicy';
|
||||
const mutedPolicyValue = getState()['features/base/conference'][mutedPolicyKey];
|
||||
|
||||
// Currently, the policy only supports force muting others, not unmuting them.
|
||||
if (!mutedPolicyValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let muteStateUpdated = false;
|
||||
const { muted } = isAudio ? getState()['features/base/media'].audio : getState()['features/base/media'].video;
|
||||
|
||||
if (isAudio && !Boolean(muted)) {
|
||||
dispatch(setAudioMuted(mutedPolicyValue, true));
|
||||
muteStateUpdated = true;
|
||||
} else if (!isAudio && !Boolean(muted)) {
|
||||
// TODO: Add a new authority for video mutism for the moderator case.
|
||||
dispatch(setVideoMuted(mutedPolicyValue, VIDEO_MUTISM_AUTHORITY.USER, true));
|
||||
muteStateUpdated = true;
|
||||
}
|
||||
|
||||
if (!hasShownNotification && muteStateUpdated) {
|
||||
hasShownNotification = true;
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.mutedTitle',
|
||||
descriptionKey: 'notify.muted'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -308,6 +297,7 @@ export interface IConfig {
|
||||
disablePolls?: boolean;
|
||||
disableProfile?: boolean;
|
||||
disableReactions?: boolean;
|
||||
disableReactionsInChat?: boolean;
|
||||
disableReactionsModeration?: boolean;
|
||||
disableRecordAudioNotification?: boolean;
|
||||
disableRemoteControl?: boolean;
|
||||
@@ -387,11 +377,18 @@ export interface IConfig {
|
||||
feedbackPercentage?: number;
|
||||
fileRecordingsServiceEnabled?: boolean;
|
||||
fileRecordingsServiceSharingEnabled?: boolean;
|
||||
fileSharing?: {
|
||||
apiUrl?: string;
|
||||
enabled?: boolean;
|
||||
maxFileSize?: number;
|
||||
};
|
||||
filmstrip?: {
|
||||
alwaysShowResizeBar?: boolean;
|
||||
disableResizable?: boolean;
|
||||
disableStageFilmstrip?: boolean;
|
||||
disableTopPanel?: boolean;
|
||||
disabled?: boolean;
|
||||
initialWidth?: number;
|
||||
minParticipantCountForTopPanel?: number;
|
||||
};
|
||||
flags?: {
|
||||
@@ -438,6 +435,7 @@ export interface IConfig {
|
||||
};
|
||||
iAmRecorder?: boolean;
|
||||
iAmSipGateway?: boolean;
|
||||
iAmSpot?: boolean;
|
||||
ignoreStartMuted?: boolean;
|
||||
inviteAppName?: string | null;
|
||||
inviteServiceCallFlowsUrl?: string;
|
||||
@@ -484,6 +482,7 @@ export interface IConfig {
|
||||
long?: number;
|
||||
medium?: number;
|
||||
short?: number;
|
||||
sticky?: number;
|
||||
};
|
||||
notifications?: Array<string>;
|
||||
notifyOnConferenceDestruction?: boolean;
|
||||
@@ -554,7 +553,7 @@ export interface IConfig {
|
||||
disableDemote?: boolean;
|
||||
disableGrantModerator?: boolean;
|
||||
disableKick?: boolean;
|
||||
disablePrivateChat?: boolean;
|
||||
disablePrivateChat?: 'all' | 'allow-moderator-chat';
|
||||
disabled?: boolean;
|
||||
};
|
||||
replaceParticipant?: string;
|
||||
@@ -596,6 +595,7 @@ export interface IConfig {
|
||||
failICE?: boolean;
|
||||
noAutoPlayVideo?: boolean;
|
||||
p2pTestMode?: boolean;
|
||||
showSpotConsentDialog?: boolean;
|
||||
skipInterimTranscriptions?: boolean;
|
||||
testMode?: boolean;
|
||||
};
|
||||
@@ -605,8 +605,9 @@ export interface IConfig {
|
||||
};
|
||||
tokenAuthUrl?: string;
|
||||
tokenAuthUrlAutoRedirect?: string;
|
||||
tokenGetUserInfoOutOfContext?: boolean;
|
||||
tokenLogoutUrl?: string;
|
||||
tokenRespectTenant?: string;
|
||||
tokenRespectTenant?: boolean;
|
||||
toolbarButtons?: Array<ToolbarButton>;
|
||||
toolbarConfig?: {
|
||||
alwaysVisible?: boolean;
|
||||
@@ -621,6 +622,7 @@ export interface IConfig {
|
||||
autoTranscribeOnRecord?: boolean;
|
||||
disableClosedCaptions?: boolean;
|
||||
enabled?: boolean;
|
||||
inviteJigasiOnBackendTranscribing?: boolean;
|
||||
preferredLanguage?: string;
|
||||
translationLanguages?: Array<string>;
|
||||
translationLanguagesHead?: Array<string>;
|
||||
|
||||
@@ -109,6 +109,7 @@ export default [
|
||||
'disablePolls',
|
||||
'disableProfile',
|
||||
'disableReactions',
|
||||
'disableReactionsInChat',
|
||||
'disableReactionsModeration',
|
||||
'disableRecordAudioNotification',
|
||||
'disableRemoteControl',
|
||||
@@ -150,6 +151,7 @@ export default [
|
||||
'enableTcc',
|
||||
'faceLandmarks',
|
||||
'feedbackPercentage',
|
||||
'fileSharing.enabled',
|
||||
'filmstrip',
|
||||
'flags',
|
||||
'forceTurnRelay',
|
||||
@@ -169,6 +171,7 @@ export default [
|
||||
'hideLobbyButton',
|
||||
'iAmRecorder',
|
||||
'iAmSipGateway',
|
||||
'iAmSpot',
|
||||
'ignoreStartMuted',
|
||||
'inviteAppName',
|
||||
'liveStreaming.enabled',
|
||||
|
||||
@@ -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,13 +1,14 @@
|
||||
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';
|
||||
import { JitsiConnectionErrors } from '../lib-jitsi-meet';
|
||||
|
||||
import { _connectInternal } from './actions.native';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -34,6 +35,8 @@ export function connect(id?: string, password?: string) {
|
||||
j && dispatch(setJWT(j));
|
||||
|
||||
return dispatch(_connectInternal(id, password));
|
||||
}).catch(e => {
|
||||
logger.error('Connection error', e);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import LocalRecordingManager from '../../recording/components/Recording/LocalRec
|
||||
import { setJWT } from '../jwt/actions';
|
||||
|
||||
import { _connectInternal } from './actions.any';
|
||||
import logger from './logger';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -38,6 +39,8 @@ export function connect(id?: string, password?: string) {
|
||||
j && dispatch(setJWT(j));
|
||||
|
||||
return dispatch(_connectInternal(id, password));
|
||||
}).catch(e => {
|
||||
logger.error('Connection error', e);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,11 @@ interface IProps extends IIconProps {
|
||||
*/
|
||||
id?: string;
|
||||
|
||||
/**
|
||||
* On click handler.
|
||||
*/
|
||||
onClick?: (e?: any) => void;
|
||||
|
||||
/**
|
||||
* Keydown handler.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,8 +2,7 @@ import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { CHAT_SIZE } from '../../chat/constants';
|
||||
import { getParticipantsPaneOpen } from '../../participants-pane/functions';
|
||||
import theme from '../components/themes/participantsPaneTheme.json';
|
||||
import { getParticipantsPaneWidth } from '../../participants-pane/functions';
|
||||
|
||||
import {
|
||||
CLIENT_RESIZED,
|
||||
@@ -43,16 +42,13 @@ export function clientResized(clientWidth: number, clientHeight: number) {
|
||||
|
||||
if (navigator.product !== 'ReactNative') {
|
||||
const state = getState();
|
||||
const { isOpen: isChatOpen } = state['features/chat'];
|
||||
const isParticipantsPaneOpen = getParticipantsPaneOpen(state);
|
||||
const { isOpen: isChatOpen, width } = state['features/chat'];
|
||||
|
||||
if (isChatOpen) {
|
||||
availableWidth -= CHAT_SIZE;
|
||||
availableWidth -= width?.current ?? CHAT_SIZE;
|
||||
}
|
||||
|
||||
if (isParticipantsPaneOpen) {
|
||||
availableWidth -= theme.participantsPaneWidth;
|
||||
}
|
||||
availableWidth -= getParticipantsPaneWidth(state);
|
||||
}
|
||||
|
||||
batch(() => {
|
||||
|
||||
@@ -18,3 +18,11 @@ export const ASPECT_RATIO_WIDE = Symbol('ASPECT_RATIO_WIDE');
|
||||
* Smallest supported mobile width.
|
||||
*/
|
||||
export const SMALL_MOBILE_WIDTH = '320';
|
||||
|
||||
/**
|
||||
* The width for desktop that we start hiding elements from the UI (video quality label, filmstrip, etc).
|
||||
* This should match the value for $verySmallScreen in _variables.scss.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export const SMALL_DESKTOP_WIDTH = 500;
|
||||
|
||||
21
react/features/base/responsive-ui/functions.ts
Normal file
21
react/features/base/responsive-ui/functions.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IStateful } from '../app/types';
|
||||
import { isMobileBrowser } from '../environment/utils';
|
||||
import { toState } from '../redux/functions';
|
||||
|
||||
import { SMALL_DESKTOP_WIDTH } from './constants';
|
||||
|
||||
/**
|
||||
* Determines if the screen is narrow with the chat panel open. If the function returns true video quality label,
|
||||
* filmstrip, etc will be hidden.
|
||||
*
|
||||
* @param {IStateful} stateful - The stateful object representing the application state.
|
||||
* @returns {boolean} - True if the screen is narrow with the chat panel open, otherwise `false`.
|
||||
*/
|
||||
export function isNarrowScreenWithChatOpen(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const isDesktopBrowser = !isMobileBrowser();
|
||||
const { isOpen, width } = state['features/chat'];
|
||||
const { clientWidth } = state['features/base/responsive-ui'];
|
||||
|
||||
return isDesktopBrowser && isOpen && (width?.current + SMALL_DESKTOP_WIDTH) > clientWidth;
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -118,8 +118,8 @@ export const colorMap = {
|
||||
|
||||
|
||||
export const font = {
|
||||
weightRegular: 400,
|
||||
weightSemiBold: 600
|
||||
weightRegular: '400',
|
||||
weightSemiBold: '600'
|
||||
};
|
||||
|
||||
export const shape = {
|
||||
@@ -128,8 +128,7 @@ export const shape = {
|
||||
boxShadow: 'inset 0px -1px 0px rgba(255, 255, 255, 0.15)'
|
||||
};
|
||||
|
||||
export const spacing
|
||||
= [ '0rem', '0.25rem', '0.5rem', '1rem', '1.5rem', '2rem', '2.5rem', '3rem', '3.5rem', '4rem', '4.5rem', '5rem', '5.5rem', '6rem', '6.5rem', '7rem', '7.5rem', '8rem' ];
|
||||
export const spacing = [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 ];
|
||||
|
||||
export const typography = {
|
||||
labelRegular: 'label01',
|
||||
@@ -137,64 +136,64 @@ export const typography = {
|
||||
labelBold: 'labelBold01',
|
||||
|
||||
bodyShortRegularSmall: {
|
||||
fontSize: '0.625rem',
|
||||
lineHeight: '1rem',
|
||||
fontSize: 10,
|
||||
lineHeight: 16,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyShortRegular: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.25rem',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyShortBold: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.25rem',
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyShortRegularLarge: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.375rem',
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyShortBoldLarge: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.375rem',
|
||||
fontSize: 16,
|
||||
lineHeight: 22,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongRegular: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongRegularLarge: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.625rem',
|
||||
fontSize: 16,
|
||||
lineHeight: 26,
|
||||
fontWeight: font.weightRegular,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongBold: {
|
||||
fontSize: '0.875rem',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: 14,
|
||||
lineHeight: 24,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
bodyLongBoldLarge: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.625rem',
|
||||
fontSize: 16,
|
||||
lineHeight: 26,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
@@ -204,29 +203,29 @@ export const typography = {
|
||||
heading2: 'heading02',
|
||||
|
||||
heading3: {
|
||||
fontSize: '2rem',
|
||||
lineHeight: '2.5rem',
|
||||
fontSize: 32,
|
||||
lineHeight: 40,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
heading4: {
|
||||
fontSize: '1.75rem',
|
||||
lineHeight: '2.25rem',
|
||||
fontSize: 28,
|
||||
lineHeight: 36,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
heading5: {
|
||||
fontSize: '1.25rem',
|
||||
lineHeight: '1.75rem',
|
||||
fontSize: 20,
|
||||
lineHeight: 28,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
},
|
||||
|
||||
heading6: {
|
||||
fontSize: '1rem',
|
||||
lineHeight: '1.625rem',
|
||||
fontSize: 16,
|
||||
lineHeight: 26,
|
||||
fontWeight: font.weightSemiBold,
|
||||
letterSpacing: 0
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { GestureResponderEvent } from 'react-native';
|
||||
|
||||
import { BUTTON_TYPES } from '../constants.any';
|
||||
|
||||
export interface IButtonProps {
|
||||
@@ -32,7 +30,7 @@ export interface IButtonProps {
|
||||
/**
|
||||
* Click callback.
|
||||
*/
|
||||
onClick?: (e?: React.MouseEvent<HTMLButtonElement> | GestureResponderEvent) => void;
|
||||
onClick?: (e?: any) => void;
|
||||
|
||||
/**
|
||||
* Key press callback.
|
||||
|
||||
@@ -213,7 +213,7 @@ const ContextMenu = ({
|
||||
|
||||
if (offsetTop + height > offsetHeight + scrollTop && height > offsetTop) {
|
||||
// top offset and + padding + border
|
||||
container.style.maxHeight = `calc(${offsetTop}px - (${spacing[2]} * 2 + 2px))`;
|
||||
container.style.maxHeight = `${offsetTop - ((spacing[2] * 2) + 2)}px`;
|
||||
}
|
||||
|
||||
// get the height after style changes
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { isMobileBrowser } from '../../../environment/utils';
|
||||
import Icon from '../../../icons/components/Icon';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
|
||||
interface ITabProps {
|
||||
@@ -14,8 +15,9 @@ interface ITabProps {
|
||||
controlsId: string;
|
||||
countBadge?: number;
|
||||
disabled?: boolean;
|
||||
icon?: Function;
|
||||
id: string;
|
||||
label: string;
|
||||
label?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -74,6 +76,10 @@ const useStyles = makeStyles()(theme => {
|
||||
borderRadius: '100%',
|
||||
backgroundColor: theme.palette.warning01,
|
||||
marginLeft: theme.spacing(2)
|
||||
},
|
||||
|
||||
icon: {
|
||||
marginRight: theme.spacing(1)
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -134,6 +140,9 @@ const Tabs = ({
|
||||
onKeyDown = { onKeyDown(index) }
|
||||
role = 'tab'
|
||||
tabIndex = { selected === tab.id ? undefined : -1 }>
|
||||
{tab.icon && <Icon
|
||||
className = { classes.icon }
|
||||
src = { tab.icon } />}
|
||||
{tab.label}
|
||||
{tab.countBadge && <span className = { classes.badge }>{tab.countBadge}</span>}
|
||||
</button>
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// Base font size in pixels (standard is 16px = 1rem)
|
||||
const BASE_FONT_SIZE = 16;
|
||||
|
||||
/**
|
||||
* Converts rem to pixels.
|
||||
*
|
||||
* @param {string} remValue - The value in rem units (e.g. '0.875rem').
|
||||
* @returns {number}
|
||||
*/
|
||||
export function remToPixels(remValue: string): number {
|
||||
const numericValue = parseFloat(remValue.replace('rem', ''));
|
||||
|
||||
return Math.round(numericValue * BASE_FONT_SIZE);
|
||||
}
|
||||
@@ -1,35 +1,6 @@
|
||||
import { DefaultTheme } from 'react-native-paper';
|
||||
|
||||
import { createColorTokens } from './utils';
|
||||
import { remToPixels } from './functions.any';
|
||||
|
||||
export * from './functions.any';
|
||||
|
||||
/**
|
||||
* Converts all rem to pixels in an object.
|
||||
*
|
||||
* @param {Object} obj - The object to convert rem values in.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function convertRemValues(obj: any): any {
|
||||
const converted: { [key: string]: any; } = {};
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
Object.entries(obj).forEach(([ key, value ]) => {
|
||||
if (typeof value === 'string' && value.includes('rem')) {
|
||||
converted[key] = remToPixels(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
converted[key] = convertRemValues(value);
|
||||
} else {
|
||||
converted[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a React Native Paper theme based on local UI tokens.
|
||||
@@ -42,10 +13,10 @@ export function createNativeTheme({ font, colorMap, shape, spacing, typography }
|
||||
...DefaultTheme,
|
||||
palette: createColorTokens(colorMap),
|
||||
shape,
|
||||
spacing: spacing.map(remToPixels),
|
||||
spacing,
|
||||
typography: {
|
||||
font,
|
||||
...convertRemValues(typography)
|
||||
...typography
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { Theme, adaptV4Theme, createTheme } from '@mui/material/styles';
|
||||
|
||||
import { ITypography, IPalette as Palette1 } from '../ui/types';
|
||||
|
||||
import { createColorTokens } from './utils';
|
||||
|
||||
export * from './functions.any';
|
||||
import { createColorTokens, createTypographyTokens } from './utils';
|
||||
|
||||
declare module '@mui/material/styles' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
@@ -20,7 +18,7 @@ interface ThemeProps {
|
||||
colorMap: Object;
|
||||
font: Object;
|
||||
shape: Object;
|
||||
spacing: Array<number | string>;
|
||||
spacing: Array<number>;
|
||||
typography: Object;
|
||||
}
|
||||
|
||||
@@ -38,7 +36,7 @@ export function createWebTheme({ font, colorMap, shape, spacing, typography, bre
|
||||
typography: {
|
||||
// @ts-ignore
|
||||
font,
|
||||
...typography
|
||||
...createTypographyTokens(typography)
|
||||
},
|
||||
breakpoints
|
||||
}));
|
||||
@@ -117,5 +115,5 @@ export function operatesWithEnterKey(element: Element): boolean {
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getVideospaceFloatingElementsBottomSpacing(theme: Theme, isToolbarVisible: boolean) {
|
||||
return isToolbarVisible ? theme.spacing(12) : theme.spacing(6);
|
||||
return parseInt(isToolbarVisible ? theme.spacing(12) : theme.spacing(6), 10);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
interface ITypographyType {
|
||||
fontSize: string;
|
||||
fontWeight: string;
|
||||
fontSize: number;
|
||||
fontWeight: number; // TODO: revisit this.
|
||||
letterSpacing: number;
|
||||
lineHeight: string;
|
||||
lineHeight: number;
|
||||
}
|
||||
|
||||
export interface IPalette {
|
||||
|
||||
@@ -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';
|
||||
@@ -19,3 +19,24 @@ export function createColorTokens(colorMap: Object): any {
|
||||
return Object.assign(result, { [token]: color });
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the typography tokens based on the typography theme and the association map.
|
||||
*
|
||||
* @param {Object} typography - A map between the token name and the actual typography value.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function createTypographyTokens(typography: Object): any {
|
||||
const allTokens = merge({}, tokens, jitsiTokens);
|
||||
|
||||
return Object.entries(typography)
|
||||
.reduce((result, [ token, value ]: [any, any]) => {
|
||||
let typographyValue = value;
|
||||
|
||||
if (typeof value === 'string') {
|
||||
typographyValue = allTokens[value as keyof typeof allTokens] || value;
|
||||
}
|
||||
|
||||
return Object.assign(result, { [token]: typographyValue });
|
||||
}, {});
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
export function loadScript(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) =>
|
||||
JitsiMeetJS.util.ScriptUtil.loadScript(
|
||||
url,
|
||||
/* async */ true,
|
||||
/* prepend */ false,
|
||||
/* relativeURL */ false,
|
||||
/* loadCallback */ resolve,
|
||||
/* errorCallback */ reject));
|
||||
{ src: url,
|
||||
async: true,
|
||||
prepend: false,
|
||||
relativeURL: false,
|
||||
loadCallback: resolve,
|
||||
errorCallback: reject
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { IReduxState } from '../../app/types';
|
||||
|
||||
/**
|
||||
* Checks if Jitsi Meet is running on Spot TV.
|
||||
*
|
||||
* @param {IReduxState} state - The redux state.
|
||||
* @returns {boolean} Whether or not Jitsi Meet is running on Spot TV.
|
||||
*/
|
||||
export function isSpotTV(): boolean {
|
||||
return navigator.userAgent.includes('SpotElectron/');
|
||||
export function isSpotTV(state: IReduxState): boolean {
|
||||
const { defaultLocalDisplayName, iAmSpot } = state['features/base/config'] || {};
|
||||
|
||||
return iAmSpot
|
||||
|| navigator.userAgent.includes('JitsiSpot/') // Jitsi Spot app
|
||||
|| navigator.userAgent.includes('8x8MeetingRooms/') // 8x8 Meeting Rooms app
|
||||
|| defaultLocalDisplayName === 'Meeting Room';
|
||||
}
|
||||
|
||||
@@ -115,23 +115,51 @@ export const SET_FOCUSED_TAB = 'SET_FOCUSED_TAB';
|
||||
* type: SET_LOBBY_CHAT_RECIPIENT
|
||||
* }
|
||||
*/
|
||||
export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT';
|
||||
export const SET_LOBBY_CHAT_RECIPIENT = 'SET_LOBBY_CHAT_RECIPIENT';
|
||||
|
||||
/**
|
||||
* The type of action sets the state of lobby messaging status.
|
||||
*
|
||||
* {
|
||||
* type: SET_LOBBY_CHAT_ACTIVE_STATE
|
||||
* payload: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE';
|
||||
/**
|
||||
* The type of action sets the state of lobby messaging status.
|
||||
*
|
||||
* {
|
||||
* type: SET_LOBBY_CHAT_ACTIVE_STATE
|
||||
* payload: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_LOBBY_CHAT_ACTIVE_STATE = 'SET_LOBBY_CHAT_ACTIVE_STATE';
|
||||
|
||||
/**
|
||||
* The type of action removes the lobby messaging from participant.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';
|
||||
/**
|
||||
* The type of action removes the lobby messaging from participant.
|
||||
*
|
||||
* {
|
||||
* type: REMOVE_LOBBY_CHAT_PARTICIPANT
|
||||
* }
|
||||
*/
|
||||
export const REMOVE_LOBBY_CHAT_PARTICIPANT = 'REMOVE_LOBBY_CHAT_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* The type of action which signals to set the width of the chat panel.
|
||||
*
|
||||
* {
|
||||
* type: SET_CHAT_WIDTH,
|
||||
* width: number
|
||||
* }
|
||||
*/
|
||||
export const SET_CHAT_WIDTH = 'SET_CHAT_WIDTH';
|
||||
|
||||
/**
|
||||
* The type of action which sets the width for the chat panel (user resized).
|
||||
* {
|
||||
* type: SET_USER_CHAT_WIDTH,
|
||||
* width: number
|
||||
* }
|
||||
*/
|
||||
export const SET_USER_CHAT_WIDTH = 'SET_USER_CHAT_WIDTH';
|
||||
|
||||
/**
|
||||
* The type of action which sets whether the user is resizing the chat panel or not.
|
||||
* {
|
||||
* type: SET_CHAT_IS_RESIZING,
|
||||
* resizing: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_CHAT_IS_RESIZING = 'SET_CHAT_IS_RESIZING';
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
|
||||
import { IStore } from '../app/types';
|
||||
|
||||
import { OPEN_CHAT } from './actionTypes';
|
||||
import {
|
||||
OPEN_CHAT,
|
||||
SET_CHAT_IS_RESIZING,
|
||||
SET_CHAT_WIDTH,
|
||||
SET_USER_CHAT_WIDTH
|
||||
} from './actionTypes';
|
||||
import { closeChat } from './actions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
@@ -45,3 +50,48 @@ export function toggleChat() {
|
||||
VideoLayout.onResize();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chat panel's width.
|
||||
*
|
||||
* @param {number} width - The new width of the chat panel.
|
||||
* @returns {{
|
||||
* type: SET_CHAT_WIDTH,
|
||||
* width: number
|
||||
* }}
|
||||
*/
|
||||
export function setChatWidth(width: number) {
|
||||
return {
|
||||
type: SET_CHAT_WIDTH,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the chat panel's width and the user preferred width.
|
||||
*
|
||||
* @param {number} width - The new width of the chat panel.
|
||||
* @returns {{
|
||||
* type: SET_USER_CHAT_WIDTH,
|
||||
* width: number
|
||||
* }}
|
||||
*/
|
||||
export function setUserChatWidth(width: number) {
|
||||
return {
|
||||
type: SET_USER_CHAT_WIDTH,
|
||||
width
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the user is resizing the chat panel or not.
|
||||
*
|
||||
* @param {boolean} resizing - Whether the user is resizing or not.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function setChatIsResizing(resizing: boolean) {
|
||||
return {
|
||||
type: SET_CHAT_IS_RESIZING,
|
||||
resizing
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from 'react';
|
||||
import { ReactReduxContext } from 'react-redux';
|
||||
|
||||
import { IMessage } from '../types';
|
||||
|
||||
@@ -16,6 +17,9 @@ export interface IProps {
|
||||
* @augments PureComponent
|
||||
*/
|
||||
export default class AbstractMessageContainer<P extends IProps, S> extends Component<P, S> {
|
||||
static override contextType = ReactReduxContext;
|
||||
declare context: React.ContextType<typeof ReactReduxContext>;
|
||||
|
||||
static defaultProps = {
|
||||
messages: [] as IMessage[]
|
||||
};
|
||||
@@ -33,9 +37,17 @@ export default class AbstractMessageContainer<P extends IProps, S> extends Compo
|
||||
let currentGrouping: IMessage[] = [];
|
||||
let currentGroupParticipantId;
|
||||
|
||||
const { store } = this.context;
|
||||
const state = store.getState();
|
||||
const { disableReactionsInChat } = state['features/base/config'];
|
||||
|
||||
for (let i = 0; i < messagesCount; i++) {
|
||||
const message = this.props.messages[i];
|
||||
|
||||
if (message.isReaction && disableReactionsInChat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (message.participantId === currentGroupParticipantId) {
|
||||
currentGrouping.push(message);
|
||||
} else {
|
||||
|
||||
@@ -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,17 +1,21 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
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 { 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 { withPixelLineHeight } from '../../../base/styles/functions.web';
|
||||
import Tabs from '../../../base/ui/components/web/Tabs';
|
||||
import { arePollsDisabled } from '../../../conference/functions.any';
|
||||
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 { 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 ChatHeader from './ChatHeader';
|
||||
@@ -22,6 +26,7 @@ import KeyboardAvoider from './KeyboardAvoider';
|
||||
import MessageContainer from './MessageContainer';
|
||||
import MessageRecipient from './MessageRecipient';
|
||||
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
@@ -34,6 +39,11 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
_isCCTabEnabled: boolean;
|
||||
|
||||
/**
|
||||
* True if file sharing tab is enabled.
|
||||
*/
|
||||
_isFileSharingTabEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the chat is opened in a modal or not (computed based on window width).
|
||||
*/
|
||||
@@ -49,6 +59,11 @@ interface IProps extends AbstractProps {
|
||||
*/
|
||||
_isPollsEnabled: boolean;
|
||||
|
||||
/**
|
||||
* Whether the user is currently resizing the chat panel.
|
||||
*/
|
||||
_isResizing: boolean;
|
||||
|
||||
/**
|
||||
* Number of unread poll messages.
|
||||
*/
|
||||
@@ -84,19 +99,30 @@ interface IProps extends AbstractProps {
|
||||
* Whether or not to block chat access with a nickname input form.
|
||||
*/
|
||||
_showNamePrompt: boolean;
|
||||
|
||||
/**
|
||||
* The current width of the chat panel.
|
||||
*/
|
||||
_width: number;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
const useStyles = makeStyles<{ _isResizing: boolean; width: number; }>()((theme, { _isResizing, width }) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
transition: 'width .16s ease-in-out',
|
||||
width: `${CHAT_SIZE}px`,
|
||||
transition: _isResizing ? undefined : 'width .16s ease-in-out',
|
||||
width: `${width}px`,
|
||||
zIndex: 300,
|
||||
|
||||
'&:hover, &:focus-within': {
|
||||
'& .dragHandleContainer': {
|
||||
visibility: 'visible'
|
||||
}
|
||||
},
|
||||
|
||||
'@media (max-width: 580px)': {
|
||||
height: '100dvh',
|
||||
position: 'fixed',
|
||||
@@ -123,7 +149,9 @@ const useStyles = makeStyles()(theme => {
|
||||
alignItems: 'center',
|
||||
boxSizing: 'border-box',
|
||||
color: theme.palette.text01,
|
||||
...withPixelLineHeight(theme.typography.heading6),
|
||||
...theme.typography.heading6,
|
||||
lineHeight: 'unset',
|
||||
fontWeight: theme.typography.heading6.fontWeight as any,
|
||||
|
||||
'.jitsi-icon': {
|
||||
cursor: 'pointer'
|
||||
@@ -146,6 +174,48 @@ const useStyles = makeStyles()(theme => {
|
||||
pollsPanel: {
|
||||
// extract header + tabs height
|
||||
height: 'calc(100% - 110px)'
|
||||
},
|
||||
|
||||
resizableChat: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%'
|
||||
},
|
||||
|
||||
dragHandleContainer: {
|
||||
height: '100%',
|
||||
width: '9px',
|
||||
backgroundColor: 'transparent',
|
||||
position: 'absolute',
|
||||
cursor: 'col-resize',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
visibility: 'hidden',
|
||||
right: '4px',
|
||||
top: 0,
|
||||
|
||||
'&:hover': {
|
||||
'& .dragHandle': {
|
||||
backgroundColor: theme.palette.icon01
|
||||
}
|
||||
},
|
||||
|
||||
'&.visible': {
|
||||
visibility: 'visible',
|
||||
|
||||
'& .dragHandle': {
|
||||
backgroundColor: theme.palette.icon01
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
dragHandle: {
|
||||
backgroundColor: theme.palette.icon02,
|
||||
height: '100px',
|
||||
width: '3px',
|
||||
borderRadius: '1px'
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -155,7 +225,9 @@ const Chat = ({
|
||||
_isOpen,
|
||||
_isPollsEnabled,
|
||||
_isCCTabEnabled,
|
||||
_isFileSharingTabEnabled,
|
||||
_focusedTab,
|
||||
_isResizing,
|
||||
_messages,
|
||||
_nbUnreadMessages,
|
||||
_nbUnreadPolls,
|
||||
@@ -164,10 +236,100 @@ const Chat = ({
|
||||
_onToggleChatTab,
|
||||
_onTogglePollsTab,
|
||||
_showNamePrompt,
|
||||
_width,
|
||||
dispatch,
|
||||
t
|
||||
}: IProps) => {
|
||||
const { classes, cx } = useStyles();
|
||||
const { classes, cx } = useStyles({ _isResizing, width: _width });
|
||||
const [ isMouseDown, setIsMouseDown ] = useState(false);
|
||||
const [ mousePosition, setMousePosition ] = useState<number | null>(null);
|
||||
const [ dragChatWidth, setDragChatWidth ] = useState<number | null>(null);
|
||||
const maxChatWidth = useSelector(getChatMaxSize);
|
||||
|
||||
/**
|
||||
* Handles mouse down on the drag handle.
|
||||
*
|
||||
* @param {MouseEvent} e - The mouse down event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragHandleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Store the initial mouse position and chat width
|
||||
setIsMouseDown(true);
|
||||
setMousePosition(e.clientX);
|
||||
setDragChatWidth(_width);
|
||||
|
||||
// Indicate that resizing is in progress
|
||||
dispatch(setChatIsResizing(true));
|
||||
|
||||
// Add visual feedback that we're dragging
|
||||
document.body.style.cursor = 'col-resize';
|
||||
|
||||
// Disable text selection during resize
|
||||
document.body.style.userSelect = 'none';
|
||||
|
||||
console.log('Chat resize: Mouse down', { clientX: e.clientX, initialWidth: _width });
|
||||
}, [ _width, dispatch ]);
|
||||
|
||||
/**
|
||||
* Drag handle mouse up handler.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
const onDragMouseUp = useCallback(() => {
|
||||
if (isMouseDown) {
|
||||
setIsMouseDown(false);
|
||||
dispatch(setChatIsResizing(false));
|
||||
|
||||
// Restore cursor and text selection
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
|
||||
console.log('Chat resize: Mouse up');
|
||||
}
|
||||
}, [ isMouseDown, dispatch ]);
|
||||
|
||||
/**
|
||||
* Handles drag handle mouse move.
|
||||
*
|
||||
* @param {MouseEvent} e - The mousemove event.
|
||||
* @returns {void}
|
||||
*/
|
||||
const onChatResize = useCallback(throttle((e: MouseEvent) => {
|
||||
// console.log('Chat resize: Mouse move', { clientX: e.clientX, isMouseDown, mousePosition, _width });
|
||||
if (isMouseDown && mousePosition !== null && dragChatWidth !== null) {
|
||||
// For chat panel resizing on the left edge:
|
||||
// - Dragging left (decreasing X coordinate) should make the panel wider
|
||||
// - Dragging right (increasing X coordinate) should make the panel narrower
|
||||
const diff = e.clientX - mousePosition;
|
||||
|
||||
const newWidth = Math.max(
|
||||
Math.min(dragChatWidth + diff, maxChatWidth),
|
||||
CHAT_SIZE
|
||||
);
|
||||
|
||||
// Update the width only if it has changed
|
||||
if (newWidth !== _width) {
|
||||
dispatch(setUserChatWidth(newWidth));
|
||||
}
|
||||
}
|
||||
}, 50, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
}), [ isMouseDown, mousePosition, dragChatWidth, _width, maxChatWidth, dispatch ]);
|
||||
|
||||
// Set up event listeners when component mounts
|
||||
useEffect(() => {
|
||||
document.addEventListener('mouseup', onDragMouseUp);
|
||||
document.addEventListener('mousemove', onChatResize);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', onDragMouseUp);
|
||||
document.removeEventListener('mousemove', onChatResize);
|
||||
};
|
||||
}, [ onDragMouseUp, onChatResize ]);
|
||||
|
||||
/**
|
||||
* Sends a text message.
|
||||
@@ -229,7 +391,10 @@ const Chat = ({
|
||||
aria-labelledby = { ChatTabs.CHAT }
|
||||
className = { cx(
|
||||
classes.chatPanel,
|
||||
!_isPollsEnabled && !_isCCTabEnabled && classes.chatPanelNoTabs,
|
||||
!_isPollsEnabled
|
||||
&& !_isCCTabEnabled
|
||||
&& !_isFileSharingTabEnabled
|
||||
&& classes.chatPanelNoTabs,
|
||||
_focusedTab !== ChatTabs.CHAT && 'hide'
|
||||
) }
|
||||
id = { `${ChatTabs.CHAT}-panel` }
|
||||
@@ -262,6 +427,14 @@ const Chat = ({
|
||||
tabIndex = { 2 }>
|
||||
<ClosedCaptionsTab />
|
||||
</div> }
|
||||
{ _isFileSharingTabEnabled && <div
|
||||
aria-labelledby = { ChatTabs.FILE_SHARING }
|
||||
className = { cx(classes.chatPanel, _focusedTab !== ChatTabs.FILE_SHARING && 'hide') }
|
||||
id = { `${ChatTabs.FILE_SHARING}-panel` }
|
||||
role = 'tabpanel'
|
||||
tabIndex = { 3 }>
|
||||
<FileSharing />
|
||||
</div> }
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -274,14 +447,14 @@ const Chat = ({
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
function renderTabs() {
|
||||
const tabs = [
|
||||
let tabs = [
|
||||
{
|
||||
accessibilityLabel: t('chat.tabs.chat'),
|
||||
countBadge:
|
||||
_focusedTab !== ChatTabs.CHAT && _nbUnreadMessages > 0 ? _nbUnreadMessages : undefined,
|
||||
id: ChatTabs.CHAT,
|
||||
controlsId: `${ChatTabs.CHAT}-panel`,
|
||||
label: t('chat.tabs.chat')
|
||||
icon: IconMessage
|
||||
}
|
||||
];
|
||||
|
||||
@@ -291,7 +464,7 @@ const Chat = ({
|
||||
countBadge: _focusedTab !== ChatTabs.POLLS && _nbUnreadPolls > 0 ? _nbUnreadPolls : undefined,
|
||||
id: ChatTabs.POLLS,
|
||||
controlsId: `${ChatTabs.POLLS}-panel`,
|
||||
label: t('chat.tabs.polls')
|
||||
icon: IconInfo
|
||||
});
|
||||
}
|
||||
|
||||
@@ -301,13 +474,36 @@ const Chat = ({
|
||||
countBadge: undefined,
|
||||
id: ChatTabs.CLOSED_CAPTIONS,
|
||||
controlsId: `${ChatTabs.CLOSED_CAPTIONS}-panel`,
|
||||
label: t('chat.tabs.closedCaptions')
|
||||
icon: IconSubtitles
|
||||
});
|
||||
}
|
||||
|
||||
if (_isFileSharingTabEnabled) {
|
||||
tabs.push({
|
||||
accessibilityLabel: t('chat.tabs.fileSharing'),
|
||||
countBadge: undefined,
|
||||
id: ChatTabs.FILE_SHARING,
|
||||
controlsId: `${ChatTabs.FILE_SHARING}-panel`,
|
||||
icon: IconShareDoc
|
||||
});
|
||||
}
|
||||
|
||||
if (tabs.length === 1) {
|
||||
tabs = [];
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs
|
||||
accessibilityLabel = { t(_isPollsEnabled ? 'chat.titleWithPolls' : 'chat.title') }
|
||||
accessibilityLabel = { _isPollsEnabled || _isCCTabEnabled || _isFileSharingTabEnabled
|
||||
? t('chat.titleWithFeatures', {
|
||||
features: [
|
||||
_isPollsEnabled ? t('chat.titleWithPolls') : '',
|
||||
_isCCTabEnabled ? t('chat.titleWithCC') : '',
|
||||
_isFileSharingTabEnabled ? t('chat.titleWithFileSharing') : ''
|
||||
].filter(Boolean).join(', ')
|
||||
})
|
||||
: t('chat.title')
|
||||
}
|
||||
onChange = { onChangeTab }
|
||||
selected = { _focusedTab }
|
||||
tabs = { tabs } />
|
||||
@@ -329,6 +525,15 @@ const Chat = ({
|
||||
isCCTabEnabled = { _isCCTabEnabled }
|
||||
isPollsEnabled = { _isPollsEnabled } />
|
||||
: renderChat()}
|
||||
<div
|
||||
className = { cx(
|
||||
classes.dragHandleContainer,
|
||||
(isMouseDown || _isResizing) && 'visible',
|
||||
'dragHandleContainer'
|
||||
) }
|
||||
onMouseDown = { onDragHandleMouseDown }>
|
||||
<div className = { cx(classes.dragHandle, 'dragHandle') } />
|
||||
</div>
|
||||
</div> : null
|
||||
);
|
||||
};
|
||||
@@ -349,11 +554,13 @@ const Chat = ({
|
||||
* _messages: Array<Object>,
|
||||
* _nbUnreadMessages: number,
|
||||
* _nbUnreadPolls: number,
|
||||
* _showNamePrompt: boolean
|
||||
* _showNamePrompt: boolean,
|
||||
* _width: number,
|
||||
* _isResizing: boolean
|
||||
* }}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
const { isOpen, focusedTab, messages, nbUnreadMessages } = state['features/chat'];
|
||||
const { isOpen, focusedTab, messages, nbUnreadMessages, width, isResizing } = state['features/chat'];
|
||||
const { nbUnreadPolls } = state['features/polls'];
|
||||
const _localParticipant = getLocalParticipant(state);
|
||||
|
||||
@@ -362,11 +569,14 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
|
||||
_isOpen: isOpen,
|
||||
_isPollsEnabled: !arePollsDisabled(state),
|
||||
_isCCTabEnabled: isCCTabEnabled(state),
|
||||
_isFileSharingTabEnabled: isFileSharingEnabled(state),
|
||||
_focusedTab: focusedTab,
|
||||
_messages: messages,
|
||||
_nbUnreadMessages: nbUnreadMessages,
|
||||
_nbUnreadPolls: nbUnreadPolls,
|
||||
_showNamePrompt: !_localParticipant?.name
|
||||
_showNamePrompt: !_localParticipant?.name,
|
||||
_width: width?.current || CHAT_SIZE,
|
||||
_isResizing: isResizing
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import { isFileSharingEnabled } from '../../../file-sharing/functions.any';
|
||||
import { toggleChat } from '../../actions.web';
|
||||
import { ChatTabs } from '../../constants';
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -34,9 +37,11 @@ interface IProps {
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function ChatHeader({ className, isPollsEnabled, isCCTabEnabled }: IProps) {
|
||||
function ChatHeader({ className, isCCTabEnabled, isPollsEnabled }: IProps) {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { focusedTab } = useSelector((state: IReduxState) => state['features/chat']);
|
||||
const fileSharingTabEnabled = useSelector(isFileSharingEnabled);
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
dispatch(toggleChat());
|
||||
@@ -51,12 +56,14 @@ function ChatHeader({ className, isPollsEnabled, isCCTabEnabled }: IProps) {
|
||||
|
||||
let title = 'chat.title';
|
||||
|
||||
if (isCCTabEnabled && isPollsEnabled) {
|
||||
title = 'chat.titleWithPollsAndCC';
|
||||
} else if (isCCTabEnabled) {
|
||||
title = 'chat.titleWithCC';
|
||||
} else if (isPollsEnabled) {
|
||||
title = 'chat.titleWithPolls';
|
||||
if (focusedTab === ChatTabs.CHAT) {
|
||||
title = 'chat.tabs.chat';
|
||||
} else if (isPollsEnabled && focusedTab === ChatTabs.POLLS) {
|
||||
title = 'chat.tabs.polls';
|
||||
} else if (isCCTabEnabled && focusedTab === ChatTabs.CLOSED_CAPTIONS) {
|
||||
title = 'chat.tabs.closedCaptions';
|
||||
} else if (fileSharingTabEnabled && focusedTab === ChatTabs.FILE_SHARING) {
|
||||
title = 'chat.tabs.fileSharing';
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Theme } from '@mui/material';
|
||||
import React, { Component, RefObject } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { withStyles } from 'tss-react/mui';
|
||||
|
||||
import { IReduxState, IStore } from '../../../app/types';
|
||||
import { isMobileBrowser } from '../../../base/environment/utils';
|
||||
@@ -8,10 +10,41 @@ 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';
|
||||
|
||||
|
||||
const styles = (_theme: Theme, { _chatWidth }: IProps) => {
|
||||
return {
|
||||
smileysPanel: {
|
||||
bottom: '100%',
|
||||
boxSizing: 'border-box' as const,
|
||||
backgroundColor: 'rgba(0, 0, 0, .6) !important',
|
||||
height: 'auto',
|
||||
display: 'flex' as const,
|
||||
overflow: 'hidden',
|
||||
position: 'absolute' as const,
|
||||
width: `${_chatWidth - 32}px`,
|
||||
marginBottom: '5px',
|
||||
marginLeft: '-5px',
|
||||
transition: 'max-height 0.3s',
|
||||
|
||||
'& #smileysContainer': {
|
||||
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,
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link ChatInput}.
|
||||
*/
|
||||
@@ -22,6 +55,9 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
_areSmileysDisabled: boolean;
|
||||
|
||||
|
||||
_chatWidth: number;
|
||||
|
||||
/**
|
||||
* Whether sending group chat messages is disabled.
|
||||
*/
|
||||
@@ -32,6 +68,11 @@ interface IProps extends WithTranslation {
|
||||
*/
|
||||
_privateMessageRecipientId?: string;
|
||||
|
||||
/**
|
||||
* An object containing the CSS classes.
|
||||
*/
|
||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
||||
|
||||
/**
|
||||
* Invoked to send chat messages.
|
||||
*/
|
||||
@@ -123,6 +164,17 @@ class ChatInput extends Component<IProps, IState> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
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' : ''}` }>
|
||||
<div id = 'chat-input' >
|
||||
@@ -130,7 +182,7 @@ class ChatInput extends Component<IProps, IState> {
|
||||
<div
|
||||
className = 'smiley-input'>
|
||||
<div
|
||||
className = 'smileys-panel' >
|
||||
className = { classes.smileysPanel } >
|
||||
<SmileysPanel
|
||||
onSmileySelect = { this._onSmileySelect } />
|
||||
</div>
|
||||
@@ -150,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' } />
|
||||
@@ -291,14 +342,15 @@ class ChatInput extends Component<IProps, IState> {
|
||||
* }}
|
||||
*/
|
||||
const mapStateToProps = (state: IReduxState) => {
|
||||
const { privateMessageRecipient } = state['features/chat'];
|
||||
const { privateMessageRecipient, width } = state['features/chat'];
|
||||
const isGroupChatDisabled = isSendGroupChatDisabled(state);
|
||||
|
||||
return {
|
||||
_areSmileysDisabled: areSmileysDisabled(state),
|
||||
_privateMessageRecipientId: privateMessageRecipient?.id,
|
||||
_isSendGroupChatDisabled: isGroupChatDisabled
|
||||
_isSendGroupChatDisabled: isGroupChatDisabled,
|
||||
_chatWidth: width.current ?? CHAT_SIZE,
|
||||
};
|
||||
};
|
||||
|
||||
export default translate(connect(mapStateToProps)(ChatInput));
|
||||
export default translate(connect(mapStateToProps)(withStyles(ChatInput, styles)));
|
||||
|
||||
@@ -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.
|
||||
@@ -88,6 +88,7 @@ export default function ClosedCaptionsTab() {
|
||||
const _isTranscribing = useSelector(isTranscribing);
|
||||
const _canStartSubtitles = useSelector(canStartSubtitles);
|
||||
const [ isButtonPressed, setButtonPressed ] = useState(false);
|
||||
const subtitlesError = useSelector((state: IReduxState) => state['features/subtitles']._hasError);
|
||||
|
||||
const filteredSubtitles = useMemo(() => {
|
||||
// First, create a map of transcription messages by message ID
|
||||
@@ -128,6 +129,10 @@ export default function ClosedCaptionsTab() {
|
||||
setButtonPressed(true);
|
||||
}, [ dispatch, isButtonPressed, setButtonPressed ]);
|
||||
|
||||
if (subtitlesError && isButtonPressed) {
|
||||
setButtonPressed(false);
|
||||
}
|
||||
|
||||
if (!_isTranscribing) {
|
||||
if (_canStartSubtitles) {
|
||||
return (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user