mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-06 23:02:28 +00:00
Compare commits
54 Commits
5634
...
android-sd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef9913c1c7 | ||
|
|
ce195d82d6 | ||
|
|
229e65133c | ||
|
|
11eb6689dc | ||
|
|
bf3cc65f4c | ||
|
|
75e6dd389f | ||
|
|
076113940b | ||
|
|
e6accd40e1 | ||
|
|
7f0cfed981 | ||
|
|
9279586e8c | ||
|
|
f8908c143e | ||
|
|
c705dbaa2f | ||
|
|
1fa43ca4e7 | ||
|
|
9528cbad0e | ||
|
|
a23d57b5ff | ||
|
|
ae5c364333 | ||
|
|
700c7c523d | ||
|
|
5dbf4845fb | ||
|
|
661a3d34be | ||
|
|
0e9b40c410 | ||
|
|
51b827ebb0 | ||
|
|
16a2c729e0 | ||
|
|
ed5bb871f4 | ||
|
|
d96ecc5b65 | ||
|
|
41f11e5adb | ||
|
|
c48aa44af3 | ||
|
|
085b07efcd | ||
|
|
60dcac96a6 | ||
|
|
2e22eb5169 | ||
|
|
e2beb2f3b1 | ||
|
|
664f23a395 | ||
|
|
dc20b2fafe | ||
|
|
00ae2dc6a9 | ||
|
|
a182f53cdc | ||
|
|
9b3f254c6b | ||
|
|
0188086dde | ||
|
|
340698a546 | ||
|
|
0920bda7b8 | ||
|
|
10e5b2f572 | ||
|
|
81e9d01921 | ||
|
|
0d5beb0c4e | ||
|
|
b890f34a53 | ||
|
|
b19e4d76b5 | ||
|
|
65589937ea | ||
|
|
a6bc51cff1 | ||
|
|
ae3fb20d65 | ||
|
|
bfc0567e32 | ||
|
|
0fb29a0c7a | ||
|
|
822850d888 | ||
|
|
64e6c5f461 | ||
|
|
a077043f1b | ||
|
|
646fdef6bb | ||
|
|
da0cb2b837 | ||
|
|
4401ea8818 |
7
android/app/proguard-rules.pro
vendored
7
android/app/proguard-rules.pro
vendored
@@ -85,4 +85,9 @@
|
||||
# ^^^ We added the above when we switched minifyEnabled on.
|
||||
|
||||
# Rule to avoid build errors related to SVGs.
|
||||
-keep public class com.horcrux.svg.** {*;}
|
||||
-keep public class com.horcrux.svg.** {*;}
|
||||
|
||||
# https://github.com/facebook/fresco/issues/2638
|
||||
-keep public class com.facebook.imageutils.** {
|
||||
public *;
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.bundle.enableUncompressedNativeLibs=false
|
||||
|
||||
appVersion=21.5.0
|
||||
sdkVersion=4.0.0
|
||||
appVersion=21.6.0
|
||||
sdkVersion=4.1.0
|
||||
|
||||
@@ -68,6 +68,7 @@ dependencies {
|
||||
implementation project(':react-native-async-storage')
|
||||
implementation project(':react-native-background-timer')
|
||||
implementation project(':react-native-calendar-events')
|
||||
implementation project(':react-native-community_clipboard')
|
||||
implementation project(':react-native-community_netinfo')
|
||||
implementation project(':react-native-default-preference')
|
||||
implementation project(':react-native-gesture-handler')
|
||||
|
||||
@@ -180,6 +180,7 @@ class ReactInstanceManagerHolder {
|
||||
new com.calendarevents.CalendarEventsPackage(),
|
||||
new com.corbt.keepawake.KCKeepAwakePackage(),
|
||||
new com.facebook.react.shell.MainReactPackage(),
|
||||
new com.reactnativecommunity.clipboard.ClipboardPackage(),
|
||||
new com.reactnativecommunity.netinfo.NetInfoPackage(),
|
||||
new com.oblador.performance.PerformancePackage(),
|
||||
new com.reactnativecommunity.slider.ReactSliderPackage(),
|
||||
|
||||
@@ -9,6 +9,8 @@ include ':react-native-background-timer'
|
||||
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
|
||||
include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
include ':react-native-community_clipboard'
|
||||
project(':react-native-community_clipboard').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/clipboard/android')
|
||||
include ':react-native-community_netinfo'
|
||||
project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
|
||||
include ':react-native-default-preference'
|
||||
|
||||
@@ -71,13 +71,12 @@ import {
|
||||
JitsiMediaDevicesEvents,
|
||||
JitsiParticipantConnectionStatus,
|
||||
JitsiTrackErrors,
|
||||
JitsiTrackEvents
|
||||
JitsiTrackEvents,
|
||||
JitsiRecordingConstants
|
||||
} from './react/features/base/lib-jitsi-meet';
|
||||
import {
|
||||
getStartWithAudioMuted,
|
||||
getStartWithVideoMuted,
|
||||
isAudioMuted,
|
||||
isVideoMuted,
|
||||
isVideoMutedByUser,
|
||||
MEDIA_TYPE,
|
||||
setAudioAvailable,
|
||||
@@ -142,6 +141,7 @@ import {
|
||||
setJoiningInProgress,
|
||||
setPrejoinPageVisibility
|
||||
} from './react/features/prejoin';
|
||||
import { getActiveSession } from './react/features/recording/functions';
|
||||
import { disableReceiver, stopReceiver } from './react/features/remote-control';
|
||||
import { setScreenAudioShareState, isScreenAudioShared } from './react/features/screen-share/';
|
||||
import { toggleScreenshotCaptureSummary } from './react/features/screenshot-capture';
|
||||
@@ -1333,18 +1333,19 @@ export default {
|
||||
/**
|
||||
* Used by the Breakout Rooms feature to join a breakout room or go back to the main room.
|
||||
*/
|
||||
async joinRoom(roomName) {
|
||||
async joinRoom(roomName, options) {
|
||||
// Reset VideoLayout. It's destroyed in features/video-layout/middleware.web.js so re-initialize it.
|
||||
VideoLayout.initLargeVideo();
|
||||
VideoLayout.resizeVideoArea();
|
||||
|
||||
// Destroy old tracks.
|
||||
APP.store.dispatch(destroyLocalTracks());
|
||||
// Restore initial state.
|
||||
this._localTracksInitialized = false;
|
||||
this.isSharingScreen = false;
|
||||
this.localPresenterVideo = null;
|
||||
|
||||
this.roomName = roomName;
|
||||
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks();
|
||||
const { tryCreateLocalTracks, errors } = this.createInitialLocalTracks(options);
|
||||
const localTracks = await tryCreateLocalTracks;
|
||||
|
||||
this._displayErrorsForCreateInitialLocalTracks(errors);
|
||||
@@ -1710,10 +1711,10 @@ export default {
|
||||
= this._turnScreenSharingOff.bind(this, didHaveVideo);
|
||||
|
||||
const desktopVideoStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.VIDEO);
|
||||
const dekstopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
|
||||
const desktopAudioStream = desktopStreams.find(stream => stream.getType() === MEDIA_TYPE.AUDIO);
|
||||
|
||||
if (dekstopAudioStream) {
|
||||
dekstopAudioStream.on(
|
||||
if (desktopAudioStream) {
|
||||
desktopAudioStream.on(
|
||||
JitsiTrackEvents.LOCAL_TRACK_STOPPED,
|
||||
() => {
|
||||
logger.debug(`Local screensharing audio track stopped. ${this.isSharingScreen}`);
|
||||
@@ -1935,7 +1936,9 @@ export default {
|
||||
.then(() => {
|
||||
this.videoSwitchInProgress = false;
|
||||
if (config.enableScreenshotCapture) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
|
||||
if (getActiveSession(APP.store.getState(), JitsiRecordingConstants.mode.FILE)) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
}
|
||||
sendAnalytics(createScreenSharingEvent('started'));
|
||||
logger.log('Screen sharing started');
|
||||
@@ -2133,6 +2136,8 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, track => APP.store.dispatch(destroyLocalTracks(track)));
|
||||
|
||||
room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
||||
subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
|
||||
|
||||
@@ -2264,22 +2269,12 @@ export default {
|
||||
room.on(
|
||||
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
|
||||
disableAudioMuteChange => {
|
||||
const muted = isAudioMuted(APP.store.getState());
|
||||
|
||||
// Disable the mute button only if its muted.
|
||||
if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
|
||||
APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
|
||||
}
|
||||
APP.store.dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
|
||||
});
|
||||
room.on(
|
||||
JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
|
||||
disableVideoMuteChange => {
|
||||
const muted = isVideoMuted(APP.store.getState());
|
||||
|
||||
// Disable the mute button only if its muted.
|
||||
if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
|
||||
APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
}
|
||||
APP.store.dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
|
||||
|
||||
13
config.js
13
config.js
@@ -83,6 +83,9 @@ var config = {
|
||||
// Disables polls feature.
|
||||
// disablePolls: false,
|
||||
|
||||
// Disables self-view tile. (hides it from tile view and from filmstrip)
|
||||
// disableSelfView: false,
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
// signalling.
|
||||
// webrtcIceUdpDisable: false,
|
||||
@@ -496,8 +499,14 @@ var config = {
|
||||
// and microsoftApiApplicationClientID
|
||||
// enableCalendarIntegration: false,
|
||||
|
||||
// When 'true', it shows an intermediate page before joining, where the user can configure their devices.
|
||||
// prejoinPageEnabled: false,
|
||||
// Configs for prejoin page.
|
||||
// prejoinConfig: {
|
||||
// // When 'true', it shows an intermediate page before joining, where the user can configure their devices.
|
||||
// // This replaces `prejoinPageEnabled`.
|
||||
// enabled: true,
|
||||
// // List of buttons to hide from the extra join options dropdown.
|
||||
// hideExtraJoinButtons: ['no-audio', 'by-phone']
|
||||
// },
|
||||
|
||||
// When 'true', the user cannot edit the display name.
|
||||
// (Mainly useful when used in conjuction with the JWT so the JWT name becomes read only.)
|
||||
|
||||
@@ -65,6 +65,11 @@
|
||||
text-align: left;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.moderator-settings-wrapper {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.profile-edit-field {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
0
fonts.html
Normal file
0
fonts.html
Normal file
@@ -9,6 +9,7 @@
|
||||
|
||||
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="css/all.css">
|
||||
<!--#include virtual="fonts.html"-->
|
||||
<link rel="manifest" id="manifest-placeholder">
|
||||
|
||||
<script>
|
||||
|
||||
13
ios/Podfile
13
ios/Podfile
@@ -61,23 +61,24 @@ target 'JitsiMeetSDK' do
|
||||
pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
|
||||
pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo'
|
||||
pod 'react-native-performance', :path => '../node_modules/react-native-performance/ios'
|
||||
pod 'react-native-safe-area-context', :path => '../node_modules/react-native-safe-area-context'
|
||||
pod 'react-native-slider', :path => '../node_modules/@react-native-community/slider'
|
||||
pod 'react-native-splash-screen', :path => '../node_modules/react-native-splash-screen'
|
||||
pod 'react-native-video', :path => '../node_modules/react-native-video/react-native-video.podspec'
|
||||
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
|
||||
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'
|
||||
pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage'
|
||||
pod 'RNCClipboard', :path => '../node_modules/@react-native-community/clipboard'
|
||||
pod 'RNCMaskedView', :path => '../node_modules/@react-native-masked-view/masked-view'
|
||||
pod 'RNDefaultPreference', :path => '../node_modules/react-native-default-preference'
|
||||
pod 'RNDeviceInfo', :path => '../node_modules/react-native-device-info'
|
||||
pod 'RNGestureHandler', :path => '../node_modules/react-native-gesture-handler'
|
||||
pod 'RNGoogleSignin', :path => '../node_modules/@react-native-community/google-signin'
|
||||
pod 'RNReanimated', :path => '../node_modules/react-native-reanimated'
|
||||
pod 'RNScreens', :path => '../node_modules/react-native-screens'
|
||||
pod 'RNSound', :path => '../node_modules/react-native-sound'
|
||||
pod 'RNSVG', :path => '../node_modules/react-native-svg'
|
||||
pod 'RNWatch', :path => '../node_modules/react-native-watch-connectivity'
|
||||
pod 'RNDefaultPreference', :path => '../node_modules/react-native-default-preference'
|
||||
pod 'RNGestureHandler', :path => '../node_modules/react-native-gesture-handler'
|
||||
pod 'RNReanimated', :path => '../node_modules/react-native-reanimated'
|
||||
pod 'RNScreens', :path => '../node_modules/react-native-screens'
|
||||
pod 'react-native-safe-area-context', :path => '../node_modules/react-native-safe-area-context'
|
||||
pod 'RNCMaskedView', :path => '../node_modules/@react-native-masked-view/masked-view'
|
||||
|
||||
# Native pod dependencies
|
||||
#
|
||||
|
||||
@@ -303,7 +303,7 @@ PODS:
|
||||
- react-native-video/Video (= 5.1.1)
|
||||
- react-native-video/Video (5.1.1):
|
||||
- React-Core
|
||||
- react-native-webrtc (1.94.0):
|
||||
- react-native-webrtc (1.94.1):
|
||||
- React-Core
|
||||
- react-native-webview (11.0.2):
|
||||
- React-Core
|
||||
@@ -363,6 +363,8 @@ PODS:
|
||||
- ReactCommon/turbomodule/core (= 0.61.5-jitsi.2)
|
||||
- RNCAsyncStorage (1.15.5):
|
||||
- React-Core
|
||||
- RNCClipboard (1.5.1):
|
||||
- React-Core
|
||||
- RNCMaskedView (0.2.6):
|
||||
- React-Core
|
||||
- RNDefaultPreference (1.4.2):
|
||||
@@ -435,6 +437,7 @@ DEPENDENCIES:
|
||||
- React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`)
|
||||
- ReactCommon/turbomodule (from `../node_modules/react-native/ReactCommon`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- "RNCClipboard (from `../node_modules/@react-native-community/clipboard`)"
|
||||
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
|
||||
- RNDefaultPreference (from `../node_modules/react-native-default-preference`)
|
||||
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
|
||||
@@ -547,6 +550,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||
RNCClipboard:
|
||||
:path: "../node_modules/@react-native-community/clipboard"
|
||||
RNCMaskedView:
|
||||
:path: "../node_modules/@react-native-masked-view/masked-view"
|
||||
RNDefaultPreference:
|
||||
@@ -616,7 +621,7 @@ SPEC CHECKSUMS:
|
||||
react-native-slider: e99fc201cefe81270fc9d81714a7a0f5e566b168
|
||||
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
|
||||
react-native-video: 0bb76b6d6b77da3009611586c7dbf817b947f30e
|
||||
react-native-webrtc: e22646adc86f2009328b21eb0e40ca404c77258c
|
||||
react-native-webrtc: 2f20515f3ebb9dbf1f2aad638cc7573396cf948f
|
||||
react-native-webview: dfd7202ff115c44d3ea401c2f36122fb3ac79f07
|
||||
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
|
||||
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6
|
||||
@@ -629,6 +634,7 @@ SPEC CHECKSUMS:
|
||||
React-RCTVibration: c1041024893fdfdb8371e7c720c437751b711676
|
||||
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
|
||||
RNCAsyncStorage: 56a3355a10b5d660c48c6e37325ac85ebfd09885
|
||||
RNCClipboard: 41d8d918092ae8e676f18adada19104fa3e68495
|
||||
RNCMaskedView: c298b644a10c0c142055b3ae24d83879ecb13ccd
|
||||
RNDefaultPreference: 1f8133ec0bc0f9453cdada578564ba1ef551fb44
|
||||
RNDeviceInfo: 87d2d175c760f6bcf58acd036f887e8b2392802c
|
||||
@@ -641,6 +647,6 @@ SPEC CHECKSUMS:
|
||||
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
|
||||
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c
|
||||
|
||||
PODFILE CHECKSUM: 836d4804218c0608e1326471ec83fe31cfa9c86d
|
||||
PODFILE CHECKSUM: 0cfc1f35e2872ceb0a86252e14e226bd489a2602
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.5.0</string>
|
||||
<string>21.6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>NSExtension</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.5.0</string>
|
||||
<string>21.6.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.5.0</string>
|
||||
<string>21.6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>21.5.0</string>
|
||||
<string>21.6.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.0.0</string>
|
||||
<string>4.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -39,6 +39,9 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "معدل تبادل البيانات منخفض"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "انتهى الاجتماع."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"defaultName": "غرفة الاجتماعات الفرعية رقم {{index}}",
|
||||
"mainRoom": "الغرفة الرئيسية",
|
||||
@@ -51,6 +54,11 @@
|
||||
"more": "أكثر",
|
||||
"remove": "إزالة",
|
||||
"sendToBreakoutRoom": "أرسل المشارك إلى:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "غرف جانبية",
|
||||
"joined": " الغرفة الجانبيةالانضمام إلى\"{{name}}\"",
|
||||
"joinedMainRoom": "الانضمام للغرفة الرئيسية"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -175,7 +183,7 @@
|
||||
"tryAgainButton": "جرب مرة أخرى في تطبيق الحاسوب"
|
||||
},
|
||||
"defaultLink": "{{url}} مثلًا",
|
||||
"defaultNickname": "محمد عمر مثلًا",
|
||||
"defaultNickname": "محمد علي مثلًا",
|
||||
"deviceError": {
|
||||
"cameraError": "فشل الوصول إلى كاميرتك",
|
||||
"cameraPermission": "خطأ في تحصيل إذن استخدام كاميرتك",
|
||||
@@ -569,6 +577,8 @@
|
||||
"notify": {
|
||||
"allowAction": "يسمح",
|
||||
"allowedUnmute": "يمكنك إعادة صوت الميكروفون و بدء تشغيل الكاميرا أو مشاركة شاشتك.",
|
||||
"audioUnmuteBlockedTitle": "تم حظر إعادة صوت الميكروفون!",
|
||||
"audioUnmuteBlockedDescription": "تم حظر عملية إلغاء كتم صوت الميكروفون مؤقتًا بسبب قيود النظام.",
|
||||
"connectedOneMember": "انضم {{name}} للاجتماع",
|
||||
"connectedThreePlusMembers": "انضم {{name}} وعدد {{count}} غيره إلى الاجتماع",
|
||||
"connectedTwoMembers": "انضم {{first}} و {{second}} إلى الاجتماع",
|
||||
@@ -580,6 +590,9 @@
|
||||
"invitedThreePlusMembers": "دُعِي {{name}} وعدد {{count}} آخرين",
|
||||
"invitedTwoMembers": "دُعِي {{first}} و {{second}}",
|
||||
"kickParticipant": "طرد {{kicked}} المشارك {{kicker}}",
|
||||
"leftOneMember": "{{name}} غادر الاجتماع",
|
||||
"leftThreePlusMembers": "غادر {{name}} والعديد من الأشخاص الآخرين الاجتماع",
|
||||
"leftTwoMembers": "غادر {{first}} و {{second}} الاجتماع",
|
||||
"me": "أنا",
|
||||
"moderator": "مُنحَت صلاحية رئيس الجلسة!",
|
||||
"muted": "بدأ المحادثة مكتوب الصوت.",
|
||||
@@ -591,6 +604,7 @@
|
||||
"passwordRemovedRemotely": "أزال أحد المشاركين {{participantDisplayName}}",
|
||||
"passwordSetRemotely": "ضبط أحد المشاركين $t(lockRoomPasswordUppercase)",
|
||||
"raisedHand": "يريد {{name}} التحدث",
|
||||
"raisedHands": "{{participantName}} و {{raisedHands}}المزيد من الناس",
|
||||
"screenShareNoAudio": "لم يتم تحديد مربع مشاركة الصوت في شاشة تحديد النافذة.",
|
||||
"screenShareNoAudioTitle": "تعذرت مشاركة صوت النظام!",
|
||||
"somebody": "شخص ما",
|
||||
@@ -619,7 +633,10 @@
|
||||
"moderationToggleDescription": "من {{participantDisplayName}}",
|
||||
"raiseHandAction": "رفع اليد",
|
||||
"reactionSounds": "تعطيل الأصوات",
|
||||
"groupTitle": "إشعارات"
|
||||
"reactionSoundsForAll": "تعطيل الأصوات للجميع",
|
||||
"groupTitle": "إشعارات",
|
||||
"videoUnmuteBlockedTitle": "تم حظر إعادة الكاميرا!",
|
||||
"videoUnmuteBlockedDescription": "تم حظر عملية إلغاء كتم الكاميرا مؤقتًا بسبب قيود النظام."
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "غلق",
|
||||
@@ -644,7 +661,8 @@
|
||||
"stopEveryonesVideo": "أوقف فيديو الجميع",
|
||||
"stopVideo": "أوقف الفيديو",
|
||||
"unblockEveryoneMicCamera": "قم بإلغاء حظر ميكروفون وكاميرا الجميع",
|
||||
"videoModeration": "ابدأ الفيديو الخاص بهم"
|
||||
"videoModeration": "ابدأ الفيديو الخاص بهم",
|
||||
"moreModerationControls": "المزيد من ضوابط الاشراف"
|
||||
},
|
||||
"search": "بحث"
|
||||
},
|
||||
@@ -839,6 +857,7 @@
|
||||
"sounds": "اصوات",
|
||||
"speakers": "المذياع (مكبر الصوت)",
|
||||
"startAudioMuted": "بدء الجميع مكتومي الصوت",
|
||||
"startReactionsMuted": "كتم رد فعل الصوت للجميع",
|
||||
"startVideoMuted": "بدء الجميع دون فيديو",
|
||||
"talkWhileMuted": "تحدث أثناء كتم الصوت",
|
||||
"title": "الإعدادات"
|
||||
@@ -1155,7 +1174,7 @@
|
||||
"logo": {
|
||||
"calendar": "شعار التقويم",
|
||||
"microsoftLogo": "شعار مايكروسوفت",
|
||||
"logoDeepLinking": "شعار jitsi",
|
||||
"logoDeepLinking": "شعار جيتسي",
|
||||
"desktopPreviewThumbnail": "صورة مصغرة لمعاينة سطح المكتب",
|
||||
"googleLogo": "شعار كوكل",
|
||||
"policyLogo": "شعار السياسة"
|
||||
|
||||
@@ -292,6 +292,29 @@
|
||||
"documentSharing": {
|
||||
"title": "Document compartit"
|
||||
},
|
||||
"virtualBackground": {
|
||||
"apply": "Aplicar",
|
||||
"title": "Fons Virtuals",
|
||||
"blur": "Difuminos",
|
||||
"slightBlur": "Lleu Difuminos",
|
||||
"removeBackground": "Elimina el Fons",
|
||||
"addBackground": "Afegeix fons",
|
||||
"pleaseWait": "Si us plau, espereu...",
|
||||
"none": "Cap",
|
||||
"uploadedImage": "Imatge carregada {{index}}",
|
||||
"deleteImage": "Esborra la imatge",
|
||||
"image1" : "Platja",
|
||||
"image2" : "Paret blanca neutra",
|
||||
"image3" : "Habitació blanca buida",
|
||||
"image4" : "Llum de peu negre",
|
||||
"image5" : "Muntanya",
|
||||
"image6" : "Bosc ",
|
||||
"image7" : "Sortida del sol",
|
||||
"desktopShareError": "No s'ha pogut crear l'escriptori compartit",
|
||||
"desktopShare":"Compartir escriptori",
|
||||
"webAssemblyWarning": "no compatible",
|
||||
"backgroundEffectError": "No s'ha pogut aplicar l'efecte de fons."
|
||||
},
|
||||
"feedback": {
|
||||
"average": "Mitjana",
|
||||
"bad": "Dolenta",
|
||||
@@ -654,6 +677,9 @@
|
||||
"toggleCamera": "Activa o desactiva la càmera",
|
||||
"videomute": "Activa o desactiva el vídeo",
|
||||
"videoblur": "Activa o desactiva el difuminat",
|
||||
"selectBackground": "Seleccioneu Fons",
|
||||
"expand": "Ampliar",
|
||||
"collapse": "Col·lapse",
|
||||
"toggleFilmstrip": "Activa o desactiva la tira"
|
||||
},
|
||||
"addPeople": "Afegeix persones a la trucada",
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"more": "Mehr",
|
||||
"remove": "Entfernen",
|
||||
"sendToBreakoutRoom": "Anwesende in Breakout-Raum verschieben:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Breakout-Räume",
|
||||
"joined": "Breakout-Raum \"{{name}}\" betreten",
|
||||
"joinedMainRoom": "Hauptraum betreten"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -585,6 +590,9 @@
|
||||
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
|
||||
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
|
||||
"kickParticipant": "{{kicked}} wurde von {{kicker}} ausgewiesen",
|
||||
"leftOneMember": "{{name}} hat die Konferenz verlassen",
|
||||
"leftThreePlusMembers": "{{name}} und Weitere haben die Konferenz verlassen",
|
||||
"leftTwoMembers": "{{first}} und {{second}} haben die Konferenz verlassen",
|
||||
"me": "Ich",
|
||||
"moderator": "Moderationsrechte vergeben!",
|
||||
"muted": "Der Konferenz wurde stumm beigetreten.",
|
||||
@@ -596,6 +604,7 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
|
||||
"raisedHand": "{{name}} möchte sprechen.",
|
||||
"raisedHands": "{{participantName}} und {{raisedHands}} weitere möchten sprechen",
|
||||
"screenShareNoAudio": "Die Option \"Audio freigeben\" wurde bei der Auswahl des Fensters nicht ausgewählt.",
|
||||
"screenShareNoAudioTitle": "Share audio was not checked",
|
||||
"somebody": "Jemand",
|
||||
@@ -624,6 +633,7 @@
|
||||
"moderationToggleDescription": "von {{participantDisplayName}}",
|
||||
"raiseHandAction": "Melden",
|
||||
"reactionSounds": "Interaktionstöne deaktivieren",
|
||||
"reactionSoundsForAll": "Interaktionstöne für alle deaktivieren",
|
||||
"groupTitle": "Benachrichtigungen",
|
||||
"videoUnmuteBlockedTitle": "Kamera kann nicht aktiviert werden!",
|
||||
"videoUnmuteBlockedDescription": "Die Kamera kann aus Überlastungsschutzgründen temporär nicht eingeschaltet werden."
|
||||
@@ -846,6 +856,7 @@
|
||||
"sounds": "Hinweistöne",
|
||||
"speakers": "Lautsprecher",
|
||||
"startAudioMuted": "Alle Personen treten stummgeschaltet bei",
|
||||
"startReactionsMuted": "Interaktionstöne für alle deaktivieren",
|
||||
"startVideoMuted": "Alle Personen treten ohne Video bei",
|
||||
"talkWhileMuted": "Wenn bei Stummschaltung gesprochen wird",
|
||||
"title": "Einstellungen"
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"more": "Plus",
|
||||
"remove": "Supprimer",
|
||||
"sendToBreakoutRoom": "Envoyer le participant dans:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Salles annexes",
|
||||
"joined": "Entrée en salle annexe \"{{name}}\"",
|
||||
"joinedMainRoom": "Retour à la salle principalem"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -585,6 +590,9 @@
|
||||
"invitedThreePlusMembers": "{{name}} et {{count}} autres ont été invités",
|
||||
"invitedTwoMembers": "{{first}} et {{second}} ont été invités",
|
||||
"kickParticipant": "{{kicked}} a été expulsé par {{kicker}}",
|
||||
"leftOneMember": "{{name}} a quitté la réunion",
|
||||
"leftThreePlusMembers": "{{name}} et beaucoup d'autres ont quitté la réunion",
|
||||
"leftTwoMembers": "{{first}} et {{second}} ont quitté la réunion",
|
||||
"me": "Moi",
|
||||
"moderator": "Droits modérateur accordés !",
|
||||
"muted": "Vous avez commencé la conversation en muet.",
|
||||
@@ -596,6 +604,7 @@
|
||||
"passwordRemovedRemotely": "Le $t(lockRoomPassword) a été supprimé par un autre participant",
|
||||
"passwordSetRemotely": "Un $t(lockRoomPassword) a été défini par un autre participant",
|
||||
"raisedHand": "{{name}} aimerait prendre la parole.",
|
||||
"raisedHands": "{{participantName}} et {{raisedHands}} autres personnes",
|
||||
"screenShareNoAudio": " La case Partager l'audio n'a pas été cochée dans l'écran de sélection de la fenêtre.",
|
||||
"screenShareNoAudioTitle": "La case Partager l'audio n'a pas été cochée",
|
||||
"somebody": "Quelqu'un",
|
||||
@@ -624,6 +633,7 @@
|
||||
"moderationToggleDescription": "par {{participantDisplayName}}",
|
||||
"raiseHandAction": "Lever la main",
|
||||
"reactionSounds": "Bloquer les réactions sonores",
|
||||
"reactionSoundsForAll": "Bloquer les réactions sonores pour tous",
|
||||
"groupTitle": "Notifications",
|
||||
"videoUnmuteBlockedTitle": "Rétablissement de la caméra bloqué !",
|
||||
"videoUnmuteBlockedDescription": "Le rétablissement de la vidéo a été bloqué temporairement en raison de limites système."
|
||||
@@ -651,7 +661,8 @@
|
||||
"stopEveryonesVideo": "Couper toutes les caméras",
|
||||
"stopVideo": "Couper la vidéo",
|
||||
"unblockEveryoneMicCamera": "Débloquer tous les micros et caméras",
|
||||
"videoModeration": "Démarrer leur vidéo"
|
||||
"videoModeration": "Démarrer leur vidéo",
|
||||
"moreModerationControls": "Options de modération supplémentaires"
|
||||
},
|
||||
"search": "Rechercher des participants"
|
||||
},
|
||||
@@ -846,6 +857,7 @@
|
||||
"sounds": "Sons",
|
||||
"speakers": "Haut-parleurs",
|
||||
"startAudioMuted": "Tout le monde commence en muet",
|
||||
"startReactionsMuted": "Tout le monde commence avec les réactions sonores bloquées",
|
||||
"startVideoMuted": "Tout le monde commence sans vidéo",
|
||||
"talkWhileMuted": "vous parlez en étant muet",
|
||||
"title": "Paramètres"
|
||||
|
||||
@@ -39,6 +39,28 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "Largura de banda baixa"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "A reunião terminou."
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"defaultName": "Salas simultâneas #{{index}}",
|
||||
"mainRoom": "Sala principal",
|
||||
"actions": {
|
||||
"add": "Adicionar salas simultâneas",
|
||||
"autoAssign": "Atribuição automática de salas simultâneas",
|
||||
"close": "Fechar",
|
||||
"join": "Entrar na sala",
|
||||
"leaveBreakoutRoom": "Sair da sala",
|
||||
"more": "Mais",
|
||||
"remove": "Eliminar sala",
|
||||
"sendToBreakoutRoom": "Enviar participante para:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Salas simultâneas",
|
||||
"joined": "Entrada do \"{{name}}\" na sala",
|
||||
"joinedMainRoom": "Entrada na sala principal"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "Adicionar um link da reunião",
|
||||
"confirmAddLink": "Gostaria de adicionar um link do Jitsi a esse evento?",
|
||||
@@ -501,6 +523,7 @@
|
||||
"expandedPending": "Iniciando a transmissão em direto...",
|
||||
"failedToStart": "Falha ao iniciar a transmissão em direto",
|
||||
"getStreamKeyManually": "Não conseguimos buscar nenhuma transmissão em direto. Tente obter sua chave de transmissão em direto no YouTube.",
|
||||
"inProgress": "Gravação ou transmissão em direto em curso",
|
||||
"invalidStreamKey": "A senha para transmissão em direto pode estar incorreta.",
|
||||
"off": "Transmissão em direto encerrada",
|
||||
"offBy": "{{name}} parou a transmissão em direto",
|
||||
@@ -508,6 +531,7 @@
|
||||
"onBy": "{{name}} iniciou a transmissão em direto",
|
||||
"pending": "Iniciando Transmissão em Direto...",
|
||||
"serviceName": "Serviço de Transmissão em Direto",
|
||||
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
|
||||
"signedInAs": "Você está conectado como:",
|
||||
"signIn": "Faça login no Google",
|
||||
"signInCTA": "Faça login ou insira sua chave de transmissão em Direto do YouTube.",
|
||||
@@ -553,6 +577,8 @@
|
||||
"notify": {
|
||||
"allowAction": "Permitir",
|
||||
"allowedUnmute": "Pode ligar o seu microfone, ligar a sua câmara ou partilhar o seu ecrã.",
|
||||
"audioUnmuteBlockedTitle": "Ligar microfone bloqueado!",
|
||||
"audioUnmuteBlockedDescription": "A operação de ligar o microfone foi temporariamente bloqueada devido aos limites do sistema.",
|
||||
"connectedOneMember": "{{name}} entrou na reunião",
|
||||
"connectedThreePlusMembers": "{{name}} e muitos outros entraram na reunião",
|
||||
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
|
||||
@@ -564,6 +590,9 @@
|
||||
"invitedThreePlusMembers": "{{name}} e {{count}} outros foram convidados",
|
||||
"invitedTwoMembers": "{{first}} e {{second}} foram convidados",
|
||||
"kickParticipant": "{{kicked}} foi expulso por {{kicker}}",
|
||||
"leftOneMember": "{{name}} deixou a reunião",
|
||||
"leftThreePlusMembers": "{{name}} e muitos outros deixaram a reunião",
|
||||
"leftTwoMembers": "{{first}} e {{second}} deixaram a reunião",
|
||||
"me": "Eu",
|
||||
"moderator": "É agora um moderador",
|
||||
"muted": "Você iniciou uma conversa com o microfone desativado.",
|
||||
@@ -575,6 +604,7 @@
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definido por outro participante",
|
||||
"raisedHand": "Gostaria de falar.",
|
||||
"raisedHands": "{{participantName}} e mais {{raisedHands}} pessoas",
|
||||
"screenShareNoAudio": " A caixa de compartilhar áudio não foi marcada no ecrã de seleção da janela.",
|
||||
"screenShareNoAudioTitle": "Não foi possível partilhar o áudio do sistema!",
|
||||
"somebody": "Alguém",
|
||||
@@ -603,7 +633,10 @@
|
||||
"moderationToggleDescription": "pelo {{participantDisplayName}}",
|
||||
"raiseHandAction": "Levantar a mão",
|
||||
"reactionSounds": "Desactivar sons",
|
||||
"groupTitle": "Notificações"
|
||||
"reactionSoundsForAll": "Desativar sons para todos",
|
||||
"groupTitle": "Notificações",
|
||||
"videoUnmuteBlockedTitle": "Ligar câmara bloqueada!",
|
||||
"videoUnmuteBlockedDescription": "A operação de ligar a câmara foi temporariamente bloqueada devido aos limites do sistema."
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Fechar",
|
||||
@@ -621,13 +654,15 @@
|
||||
"invite": "Convidar alguém",
|
||||
"askUnmute": "Pedir para ligar o microfone",
|
||||
"moreModerationActions": "Mais opções de moderação",
|
||||
"moreParticipantOptions": "Mais opções de participantes",
|
||||
"mute": "Silenciar",
|
||||
"muteAll": "Silenciar todos",
|
||||
"muteEveryoneElse": "Silenciar todos os outros",
|
||||
"stopEveryonesVideo": "Desligar a câmara de todos",
|
||||
"stopVideo": "Desligar a câmara",
|
||||
"unblockEveryoneMicCamera": "Desbloquear o microfone e a câmara de todos",
|
||||
"videoModeration": "Ligarem a câmara deles"
|
||||
"videoModeration": "Ligarem a câmara deles",
|
||||
"moreModerationControls": "Mais controlos de moderação"
|
||||
},
|
||||
"search": "Pesquisar participantes"
|
||||
},
|
||||
@@ -760,6 +795,7 @@
|
||||
"expandedPending": "Iniciando gravação...",
|
||||
"failedToStart": "Falha ao iniciar a gravação",
|
||||
"fileSharingdescription": "Compartilhar gravação com participantes da reunião",
|
||||
"inProgress": "Gravação ou transmissão em direto em curso",
|
||||
"linkGenerated": "Gerámos um link para a sua gravação.",
|
||||
"live": "DIRETO",
|
||||
"loggedIn": "Conectado como {{userName}}",
|
||||
@@ -772,6 +808,7 @@
|
||||
"serviceDescription": "Sua gravação será salva pelo serviço de gravação",
|
||||
"serviceDescriptionCloud": "Gravação na nuvem",
|
||||
"serviceName": "Serviço de gravação",
|
||||
"sessionAlreadyActive": "Esta sessão já está a ser gravada ou transmitida em direto.",
|
||||
"signIn": "Entrar",
|
||||
"signOut": "Sair",
|
||||
"unavailable": "Oops! O {{serviceName}} está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
|
||||
@@ -820,6 +857,7 @@
|
||||
"sounds": "Sons",
|
||||
"speakers": "Participantes",
|
||||
"startAudioMuted": "Todos começam com microfone desligado",
|
||||
"startReactionsMuted": "Sons de reação silenciados para todos",
|
||||
"startVideoMuted": "Todos começam com câmara desligada",
|
||||
"talkWhileMuted": "Se fala e está com microfone desligado",
|
||||
"title": "Definições"
|
||||
@@ -858,7 +896,14 @@
|
||||
"name": "Nome",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Estatísticas dos Participantes",
|
||||
"speakerTime": "Tempo do Participante"
|
||||
"speakerTime": "Tempo do Participante",
|
||||
"happy": "Feliz",
|
||||
"neutral": "Neutro",
|
||||
"sad": "Triste",
|
||||
"surprised": "Surpreendido",
|
||||
"angry": "Zangado",
|
||||
"fearful": "Temeroso",
|
||||
"disgusted": "Desgostoso"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -875,6 +920,7 @@
|
||||
"audioOnly": "Mudar para apenas áudio",
|
||||
"audioRoute": "Selecionar o dispositivo de som",
|
||||
"boo": "Vaia",
|
||||
"breakoutRoom": "Entrar/Sair salas instantâneas",
|
||||
"callQuality": "Gerir a qualidade do vídeo",
|
||||
"cc": "Mudar legendas",
|
||||
"chat": "Abrir / Fechar chat",
|
||||
@@ -958,7 +1004,9 @@
|
||||
"hangup": "Sair da reunião",
|
||||
"help": "Ajuda",
|
||||
"invite": "Convidar pessoas",
|
||||
"joinBreakoutRoom": "Entrar na sala",
|
||||
"laugh": "Risos",
|
||||
"leaveBreakoutRoom": "Sair da sala",
|
||||
"like": "Aprovado",
|
||||
"lobbyButtonDisable": "Desativar sala de espera",
|
||||
"lobbyButtonEnable": "Ativar sala de espera",
|
||||
@@ -1136,6 +1184,12 @@
|
||||
"button": "Convidar outros",
|
||||
"youAreAlone": "É o único na reunião"
|
||||
},
|
||||
"termsView": {
|
||||
"header": "Termos"
|
||||
},
|
||||
"privacyView": {
|
||||
"header": "Privacidade"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "Centro de ajuda"
|
||||
},
|
||||
|
||||
@@ -1079,7 +1079,7 @@
|
||||
"title": "Delade dokument"
|
||||
},
|
||||
"e2ee": {
|
||||
"labelToolTip": "jud- och videokommunikation för detta samtal är krypterad från dator till dator"
|
||||
"labelToolTip": "Ljud- och videokommunikation för detta samtal är krypterad från dator till dator"
|
||||
},
|
||||
"embedMeeting": {
|
||||
"title": "Bädda in möte"
|
||||
@@ -1165,8 +1165,8 @@
|
||||
"send": "Skicka"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "Skicka",
|
||||
"submit": "Skippa"
|
||||
"skip": "Skippa",
|
||||
"submit": "Skicka"
|
||||
},
|
||||
"results": {
|
||||
"vote": "Rösta",
|
||||
|
||||
@@ -39,6 +39,28 @@
|
||||
"audioOnly": {
|
||||
"audioOnly": "低頻寬"
|
||||
},
|
||||
"blankPage": {
|
||||
"meetingEnded": "會議已結束。"
|
||||
},
|
||||
"breakoutRooms": {
|
||||
"defaultName": "分組討論室 #{{index}}",
|
||||
"mainRoom": "主會議室",
|
||||
"actions": {
|
||||
"add": "新增討論室",
|
||||
"autoAssign": "自動分配至討論室",
|
||||
"close": "關閉",
|
||||
"join": "加入",
|
||||
"leaveBreakoutRoom": "離開討論室",
|
||||
"more": "更多",
|
||||
"remove": "移除",
|
||||
"sendToBreakoutRoom": "將參與者移至:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "分組討論室",
|
||||
"joined": "正在加入 \"{{name}}\" 分組討論室",
|
||||
"joinedMainRoom": "正在加入主會議室"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "增加會議連結",
|
||||
"confirmAddLink": "您要為此活動加入 Jitsi 連結嗎?",
|
||||
@@ -70,13 +92,17 @@
|
||||
"titleWithPolls": "輸入名稱來使用交談"
|
||||
},
|
||||
"privateNotice": "私人訊息傳送至 {{recipient}}",
|
||||
"title": "對話",
|
||||
"titleWithPolls": "對話",
|
||||
"you": "您",
|
||||
"message": "訊息",
|
||||
"messageAccessibleTitle": "{{user}} 說:",
|
||||
"messageAccessibleTitleMe": "您說:",
|
||||
"smileysPanel": "表情符號面板"
|
||||
"smileysPanel": "表情符號面板",
|
||||
"tabs": {
|
||||
"chat": "聊天",
|
||||
"polls": "投票"
|
||||
},
|
||||
"title": "對話",
|
||||
"titleWithPolls": "對話",
|
||||
"you": "您"
|
||||
},
|
||||
"chromeExtensionBanner": {
|
||||
"installExtensionText": "安裝適用於 Google 行事曆及 Office 365 整合的擴充功能",
|
||||
@@ -209,7 +235,9 @@
|
||||
"done": "完成",
|
||||
"e2eeDescription": "端對端加密目前是實驗性功能。請注意:啟用端對端加密將停用伺服器端提供的服務,例如:錄影、直播、及電話參與。且會議將只適用於支援 Insertable Streams 的瀏覽器。",
|
||||
"e2eeLabel": "啟用端對端加密",
|
||||
"e2eeDisabledDueToMaxModeDescription": "由於會議中的人數過多,故無法啟用端對端加密。",
|
||||
"e2eeWarning": "警告:看來不是每位此會議的參與者都有啟用端對端加密,如果您啟用了,他們可能無法看/聽到您。",
|
||||
"e2eeWillDisableDueToMaxModeDescription": "警告:如果有更多參與者加入會議,端對端加密將被自動停用。",
|
||||
"enterDisplayName": "請在此輸入您自己的名字",
|
||||
"embedMeeting": "嵌入會議",
|
||||
"error": "錯誤",
|
||||
@@ -245,24 +273,30 @@
|
||||
"micPermissionDeniedError": "您未取得權限使用麥克風。您仍然可參加會議,但是其他人無法聽到您。可以利用位址欄中的攝影裝置按鈕來修正。",
|
||||
"micTimeoutError": "無法啟動音訊裝置。連線逾時!",
|
||||
"micUnknownError": "不明原因造成麥克風無法使用。",
|
||||
"moderationAudioLabel": "允許參與者自我解除靜音",
|
||||
"moderationVideoLabel": "允許參與者開啟視訊",
|
||||
"muteEveryoneElseDialog": "靜音後,你就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneElseTitle": "是否要讓除了 {{whom}} 以外的人靜音?",
|
||||
"muteEveryoneDialog": "是否要靜音所有人?靜音後,你就不能再解除對方的靜音,但對方可以隨時解除自己的靜音狀態。",
|
||||
"muteEveryoneDialogModerationOn": "參與者可以隨時傳送說話請求。",
|
||||
"muteEveryoneTitle": "靜音所有人?",
|
||||
"muteEveryoneElsesVideoDialog": "一旦停用,您就不能再重新開啟對方的攝影機,但對方隨時能重新開啟自己的攝影機。",
|
||||
"muteEveryoneElsesVideoTitle": "是否要關閉除了 {{whom}} 以外的人的攝影機?",
|
||||
"muteEveryonesVideoDialog": "您確定要停用所有人的攝影機嗎?停用後,您就無法再重新開啟,只有對方能自己重新開啟。",
|
||||
"muteEveryonesVideoDialogModerationOn": "參與者可以隨時傳送開啟視訊請求。",
|
||||
"muteEveryonesVideoDialogOk": "停用",
|
||||
"muteEveryonesVideoTitle": "關閉所有人的攝影機?",
|
||||
"muteEveryoneSelf": "您自己",
|
||||
"muteEveryoneStartMuted": "現在所有人皆已靜音",
|
||||
"muteParticipantBody": "您無法對他們解除靜音,但是他們自己隨時可以解除靜音。",
|
||||
"muteParticipantButton": "靜音",
|
||||
"muteParticipantDialog": "確定要將這位參與者設為靜音?您無法為他們解除,但他們可以隨時自行解除靜音。",
|
||||
"muteParticipantTitle": "將這位參與者設為靜音?",
|
||||
"muteParticipantsVideoDialog": "確定要將這位參與者設為靜音?您無法為他們解除,但他們可以隨時自行解除靜音。",
|
||||
"muteParticipantsVideoDialogModerationOn": "您確定要關閉此參與者的視訊鏡頭嗎?您和他都無法再將視訊重新開啟。",
|
||||
"muteParticipantsVideoButton": "停用攝影機",
|
||||
"muteParticipantsVideoTitle": "停用此參與者的攝影機?",
|
||||
"muteParticipantsVideoBody": "您無法重新開啟,只有對方能自己重新開啟。",
|
||||
"muteParticipantsVideoBodyModerationOn": "您和他都無法再將視訊重新開啟。",
|
||||
"noDropboxToken": "沒有有效的 Dropbox 權杖",
|
||||
"Ok": "確定",
|
||||
"password": "密碼",
|
||||
"passwordLabel": "會議已被參與者鎖定。請輸入 $t(lockRoomPassword) 以加入。",
|
||||
@@ -323,6 +357,7 @@
|
||||
"shareScreenWarningH1": "如果您只要分享畫面:",
|
||||
"shareScreenWarningD1": "您必須先停止分享音訊才能分享畫面。",
|
||||
"shareScreenWarningD2": "您必須先停止分享音訊,啟動畫面分享,然後勾選 \"分享音訊\" 選項。",
|
||||
"sharedVideoLinkPlaceholder": "YouTube 或影片連結",
|
||||
"stopLiveStreaming": "停止直播串流",
|
||||
"stopRecording": "停止錄影",
|
||||
"stopRecordingWarning": "確定要停止錄影嗎?",
|
||||
@@ -382,7 +417,8 @@
|
||||
"image7" : "日出",
|
||||
"desktopShareError": "無法建立桌面分享",
|
||||
"desktopShare":"桌面分享",
|
||||
"webAssemblyWarning": "不支援 WebAssembly"
|
||||
"webAssemblyWarning": "不支援 WebAssembly",
|
||||
"backgroundEffectError": "無法套用背景效果。"
|
||||
},
|
||||
"feedback": {
|
||||
"average": "普通",
|
||||
@@ -419,6 +455,10 @@
|
||||
"invitePhone": "要用電話參加會議,請使用:{{number}},,{{conferenceID}}#\n",
|
||||
"invitePhoneAlternatives": "要找另一組撥入號碼?\n請見會議撥入號碼:{{url}}\n\n\n如果也要用室內電話撥打,不用連接語音進行加入:{{silentUrl}}",
|
||||
"inviteSipEndpoint": "如果要透過 SIP 地址加入,請輸入:{{sipUri}}",
|
||||
"inviteTextiOSPersonal": "{{name}} 邀請您加入會議。",
|
||||
"inviteTextiOSJoinSilent": "如果您使用了市內電話撥入,請使用此連結來停用音訊:{{silentUrl}}。",
|
||||
"inviteTextiOSInviteUrl": "點擊此連結以加入:{{inviteUrl}}。",
|
||||
"inviteTextiOSPhone": "若要透過電話加入,請使用此號碼:{{number}},,{{conferenceID}}#。如果您需要其他號碼,點擊此連結以檢視完整列表:{{didUrl}}。",
|
||||
"inviteURLFirstPartGeneral": "您受邀參加會議。",
|
||||
"inviteURLFirstPartPersonal": "{{name}} 正在邀請您加入會議。\n",
|
||||
"inviteURLSecondPart": "\n加入會議:\n{{url}}\n",
|
||||
@@ -483,6 +523,7 @@
|
||||
"expandedPending": "直播串流正被啟動...",
|
||||
"failedToStart": "直播串流啟動失敗",
|
||||
"getStreamKeyManually": "我們無法解析任何直播串流,請從 YouTube 取得您的直播串流金鑰。",
|
||||
"inProgress": "正在錄製或直播",
|
||||
"invalidStreamKey": "直播串流金鑰可能不正確。",
|
||||
"off": "直播串流已經停止",
|
||||
"offBy": "{{name}} 停止了直播串流",
|
||||
@@ -490,6 +531,7 @@
|
||||
"onBy": "{{name}} 啟動了直播串流",
|
||||
"pending": "啟動直播串流...",
|
||||
"serviceName": "直播串流服務",
|
||||
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
|
||||
"signedInAs": "您目前登入名稱為:",
|
||||
"signIn": "使用 Google 帳戶登入",
|
||||
"signInCTA": "輸入 YouTube 直播串流密鑰,或登入 YouTube 帳號。",
|
||||
@@ -533,6 +575,10 @@
|
||||
"lockRoomPasswordUppercase": "密碼",
|
||||
"me": "我",
|
||||
"notify": {
|
||||
"allowAction": "允許",
|
||||
"allowedUnmute": "您可以將麥克風解除靜音、開啟視訊,或是分享您的畫面。",
|
||||
"audioUnmuteBlockedTitle": "麥克風解除靜音遭封鎖!",
|
||||
"audioUnmuteBlockedDescription": "麥克風解除靜音操作由於系統限制而被暫時封鎖。",
|
||||
"connectedOneMember": "{{name}} 加入了會議",
|
||||
"connectedThreePlusMembers": "{{name}} 及 {{count}} 位人員加入了會議",
|
||||
"connectedTwoMembers": "{{first}} 及 {{second}} 加入了會議",
|
||||
@@ -545,6 +591,9 @@
|
||||
"invitedThreePlusMembers": "{{name}} 及 {{count}} 位人員已受邀請",
|
||||
"invitedTwoMembers": "{{first}} 及 {{second}} 已受邀請",
|
||||
"kickParticipant": "{{kicked}} 已被 {{kicker}} 踢出會議",
|
||||
"leftOneMember": "{{name}} 已離開會議",
|
||||
"leftThreePlusMembers": "{{name}} 和其他人已離開會議",
|
||||
"leftTwoMembers": "{{first}} 和 {{second}} 已離開會議",
|
||||
"me": "自己",
|
||||
"moderator": "主持人權限已經取得!",
|
||||
"muted": "您已經啟動通話,處於靜音。",
|
||||
@@ -583,26 +632,71 @@
|
||||
"moderationStoppedTitle": "停止管理",
|
||||
"moderationToggleDescription": "由 {{participantDisplayName}}",
|
||||
"raiseHandAction": "舉手",
|
||||
"groupTitle": "通知"
|
||||
"reactionSounds": "停用音效",
|
||||
"reactionSoundsForAll": "為所有人停用音效",
|
||||
"groupTitle": "通知",
|
||||
"videoUnmuteBlockedTitle": "視訊鏡頭解除靜音遭封鎖!",
|
||||
"videoUnmuteBlockedDescription": "視訊鏡頭解除靜音操作由於系統限制而被暫時封鎖。"
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "關閉",
|
||||
"header": "參與者",
|
||||
"headings": {
|
||||
"lobby": "大廳 ({{count}})",
|
||||
"participantsList": "會議參與者 ({{count}})"
|
||||
"participantsList": "會議參與者 ({{count}})",
|
||||
"waitingLobby": "於大廳等待 ({{count}})"
|
||||
},
|
||||
"actions": {
|
||||
"allow": "允許參與者能夠:",
|
||||
"allowVideo": "允許視訊",
|
||||
"audioModeration": "自我解除靜音",
|
||||
"blockEveryoneMicCamera": "封鎖所有人的麥克風和視訊鏡頭",
|
||||
"invite": "邀請他人",
|
||||
"askUnmute": "要求解除靜音",
|
||||
"moreModerationActions": "更多管理選項",
|
||||
"moreParticipantOptions": "更多參與者選項",
|
||||
"mute": "靜音",
|
||||
"muteAll": "靜音所有人",
|
||||
"startModeration": "將他們解除靜音或開始視訊",
|
||||
"muteEveryoneElse": "靜音其他人",
|
||||
"stopEveryonesVideo": "停止所有人的視訊",
|
||||
"stopVideo": "停止影片"
|
||||
}
|
||||
"stopVideo": "停止影片",
|
||||
"unblockEveryoneMicCamera": "解除封鎖所有人的麥克風及視訊鏡頭",
|
||||
"videoModeration": "開啟視訊",
|
||||
"moreModerationControls": "更多管理控制項"
|
||||
},
|
||||
"search": "搜尋參與者"
|
||||
},
|
||||
"passwordSetRemotely": "由其他參與者設定",
|
||||
"passwordDigitsOnly": "上限為 {{number}} 位數",
|
||||
"polls": {
|
||||
"by": "由 {{ name }}",
|
||||
"create": {
|
||||
"addOption": "新增選項",
|
||||
"answerPlaceholder": "選項 {{index}}",
|
||||
"create": "建立投票",
|
||||
"cancel": "取消",
|
||||
"pollOption" : "投票選項 {{index}}",
|
||||
"pollQuestion" : "投票問題",
|
||||
"questionPlaceholder": "詢問問題",
|
||||
"removeOption": "移除選項",
|
||||
"send": "傳送"
|
||||
},
|
||||
"answer": {
|
||||
"skip": "跳過",
|
||||
"submit": "提交"
|
||||
},
|
||||
"results": {
|
||||
"vote": "投票",
|
||||
"changeVote": "修改投票",
|
||||
"empty": "目前會議中沒有任何投票。在這裡建立您的投票吧!",
|
||||
"hideDetailedResults": "隱藏詳細資訊",
|
||||
"showDetailedResults": "顯示詳細資訊"
|
||||
},
|
||||
"notification": {
|
||||
"title": "此會議有一項新投票",
|
||||
"description": "開啟投票分頁以參與投票"
|
||||
}
|
||||
},
|
||||
"poweredby": "技術支援",
|
||||
"prejoin": {
|
||||
"audioAndVideoError": "音訊及視訊錯誤:",
|
||||
@@ -643,6 +737,7 @@
|
||||
"errorDialOutFailed": "因通話失敗而無法撥出。",
|
||||
"errorDialOutStatus": "取得撥出狀態時發生錯誤",
|
||||
"errorMissingName": "請輸入您的名字以加入會議",
|
||||
"errorNoPermissions": "您必須啟用麥克風及視訊鏡頭存取權限",
|
||||
"errorStatusCode": "撥出失敗,狀態代碼:{{status}}",
|
||||
"errorValidation": "號碼驗證失敗",
|
||||
"iWantToDialIn": "我想要撥入",
|
||||
@@ -700,6 +795,7 @@
|
||||
"expandedPending": "錄影正在啟動...",
|
||||
"failedToStart": "錄影啟動失敗",
|
||||
"fileSharingdescription": "分享錄影給會議參與者",
|
||||
"inProgress": "正在錄製或直播",
|
||||
"linkGenerated": "我們建立了您的錄影檔的連結。",
|
||||
"live": "直播",
|
||||
"loggedIn": "以 {{userName}} 登入",
|
||||
@@ -712,6 +808,7 @@
|
||||
"serviceDescription": "您的錄影會由錄影服務儲存",
|
||||
"serviceDescriptionCloud": "雲端錄製",
|
||||
"serviceName": "錄影服務",
|
||||
"sessionAlreadyActive": "已在錄製或直播此工作階段。",
|
||||
"signIn": "登入",
|
||||
"signOut": "登出",
|
||||
"unavailable": "喔哦!{{serviceName}} 目前無法使用。我們正在解決此問題,請稍後再試。",
|
||||
@@ -741,6 +838,7 @@
|
||||
"devices": "裝置",
|
||||
"followMe": "全部人跟隨我",
|
||||
"framesPerSecond": "幀數",
|
||||
"incomingMessage": "新訊息",
|
||||
"language": "語言",
|
||||
"loggedIn": "以 {{name}} 登入",
|
||||
"microphones": "麥克風",
|
||||
@@ -748,13 +846,20 @@
|
||||
"more": "更多",
|
||||
"name": "名稱",
|
||||
"noDevice": "無",
|
||||
"participantJoined": "參與者已加入",
|
||||
"participantLeft": "參與者已離開",
|
||||
"playSounds": "播放音效",
|
||||
"reactions": "會議反應",
|
||||
"sameAsSystem": "系統預設 ({{label}})",
|
||||
"selectAudioOutput": "音訊輸出",
|
||||
"selectCamera": "攝影裝置",
|
||||
"selectMic": "麥克風",
|
||||
"sounds": "音效",
|
||||
"speakers": "喇叭",
|
||||
"startAudioMuted": "全部人啟動時處於靜音",
|
||||
"startReactionsMuted": "為所有人關閉反應音效",
|
||||
"startVideoMuted": "全部人啟動時處於隱藏",
|
||||
"talkWhileMuted": "靜音時說話",
|
||||
"title": "設定"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -785,12 +890,20 @@
|
||||
},
|
||||
"speaker": "喇叭",
|
||||
"speakerStats": {
|
||||
"search": "搜尋",
|
||||
"hours": "{{count}}時",
|
||||
"minutes": "{{count}}分",
|
||||
"name": "名稱",
|
||||
"seconds": "{{count}}秒",
|
||||
"speakerStats": "聲音輸出數據",
|
||||
"speakerTime": "聲音輸出時間"
|
||||
"speakerTime": "聲音輸出時間",
|
||||
"happy": "開心",
|
||||
"neutral": "中立",
|
||||
"sad": "悲傷",
|
||||
"surprised": "驚訝",
|
||||
"angry": "憤怒",
|
||||
"fearful": "害怕",
|
||||
"disgusted": "作嘔"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -806,6 +919,7 @@
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "切換僅聲音",
|
||||
"audioRoute": "選擇音訊裝置",
|
||||
"boo": "喝倒彩",
|
||||
"callQuality": "管理影像品質",
|
||||
"cc": "切換字幕",
|
||||
"chat": "切換聊天視窗",
|
||||
@@ -870,11 +984,13 @@
|
||||
"audioOnlyOn": "啟用低頻寬模式",
|
||||
"audioRoute": "選擇音訊裝置",
|
||||
"authenticate": "認證",
|
||||
"boo": "喝倒彩",
|
||||
"callQuality": "管理影像品質",
|
||||
"chat": "開啟/關閉聊天欄",
|
||||
"clap": "鼓掌",
|
||||
"closeChat": "關閉聊天欄",
|
||||
"closeReactionsMenu": "關閉反應選單",
|
||||
"disableReactionSounds": "您可以停用此會議的反應音效",
|
||||
"documentClose": "關閉分享檔案欄",
|
||||
"documentOpen": "開啟分享檔案欄",
|
||||
"download": "下載我們的應用程式",
|
||||
@@ -888,7 +1004,9 @@
|
||||
"hangup": "離開",
|
||||
"help": "說明",
|
||||
"invite": "邀請人員",
|
||||
"joy": "高興",
|
||||
"joinBreakoutRoom": "加入分組討論室",
|
||||
"laugh": "大笑",
|
||||
"leaveBreakoutRoom": "離開分組討論室",
|
||||
"like": "比讚",
|
||||
"lobbyButtonDisable": "停用大廳模式",
|
||||
"lobbyButtonEnable": "啟用大廳模式",
|
||||
@@ -910,17 +1028,16 @@
|
||||
"openChat": "開啟聊天欄",
|
||||
"openReactionsMenu": "開啟反應選單",
|
||||
"participants": "參與者",
|
||||
"party": "慶祝",
|
||||
"pip": "進入子母畫面模式",
|
||||
"privateMessage": "發送私人訊息",
|
||||
"privateMessage": "傳送私人訊息",
|
||||
"profile": "編輯您的簡介",
|
||||
"raiseHand": "舉手/取消請求發言",
|
||||
"raiseYourHand": "舉手發言",
|
||||
"raiseHand": "舉手/放下",
|
||||
"raiseYourHand": "舉手",
|
||||
"reactionBoo": "傳送喝倒彩反應",
|
||||
"reactionClap": "傳送鼓掌反應",
|
||||
"reactionJoy": "傳送高興反應",
|
||||
"reactionLaugh": "傳送大笑反應",
|
||||
"reactionLike": "傳送比讚反應",
|
||||
"reactionParty": "傳送拉炮反應",
|
||||
"reactionSmile": "傳送微笑反應",
|
||||
"reactionSilence": "傳送靜默反應",
|
||||
"reactionSurprised": "傳送驚訝反應",
|
||||
"security": "安全性選項",
|
||||
"Settings": "設定",
|
||||
@@ -979,7 +1096,10 @@
|
||||
"pending": "已向 {{displayName}} 發送邀請"
|
||||
},
|
||||
"videoStatus": {
|
||||
"adjustFor": "調整:",
|
||||
"audioOnly": "僅聲音",
|
||||
"bestPerformance": "最佳效能",
|
||||
"highestQuality": "最佳品質",
|
||||
"audioOnlyExpanded": "您目前處於低頻寬模式。在此模式下您僅會收到語音及螢幕分享。",
|
||||
"callQuality": "影像品質",
|
||||
"hd": "HD",
|
||||
@@ -990,6 +1110,7 @@
|
||||
"ld": "LD",
|
||||
"ldTooltip": "觀看低解析度影像",
|
||||
"lowDefinition": "低解析度",
|
||||
"performanceSettings": "效能設定",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "觀看標準解析度影像",
|
||||
"standardDefinition": "標準解析度"
|
||||
@@ -1063,6 +1184,12 @@
|
||||
"button": "邀請其他人",
|
||||
"youAreAlone": "您是會議中的唯一一個人"
|
||||
},
|
||||
"termsView": {
|
||||
"header": "條款"
|
||||
},
|
||||
"privacyView": {
|
||||
"header": "隱私權"
|
||||
},
|
||||
"helpView": {
|
||||
"header": "說明中心"
|
||||
},
|
||||
@@ -1081,6 +1208,7 @@
|
||||
"enableDialogText": "大廳模式能夠保護您的會議,只有被管理員認可後才能加入會議。",
|
||||
"enterPasswordButton": "輸入會議密碼",
|
||||
"enterPasswordTitle": "輸入密碼以加入會議",
|
||||
"errorMissingPassword": "請輸入會議密碼",
|
||||
"invalidPassword": "密碼錯誤",
|
||||
"joiningMessage": "一旦他人接受您的請求,即可加入會議",
|
||||
"joinWithPasswordMessage": "正在嘗試透過密碼加入,請稍候...",
|
||||
@@ -1099,6 +1227,7 @@
|
||||
"passwordField": "輸入會議密碼",
|
||||
"passwordJoinButton": "加入",
|
||||
"reject": "拒絕",
|
||||
"rejectAll": "拒絕所有人",
|
||||
"toggleLabel": "啟用大廳"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
"more": "More",
|
||||
"remove": "Remove",
|
||||
"sendToBreakoutRoom": "Send participant to:"
|
||||
},
|
||||
"notifications": {
|
||||
"joinedTitle": "Breakout Rooms",
|
||||
"joined": "Joining the \"{{name}}\" breakout room",
|
||||
"joinedMainRoom": "Joining the main room"
|
||||
}
|
||||
},
|
||||
"calendarSync": {
|
||||
@@ -175,7 +180,8 @@
|
||||
"joinInApp": "Join this meeting using the app",
|
||||
"launchWebButton": "Launch in web",
|
||||
"title": "Launching your meeting in {{app}}...",
|
||||
"tryAgainButton": "Try again in desktop"
|
||||
"tryAgainButton": "Try again in desktop",
|
||||
"unsupportedBrowser": "It looks like you're using a browser we don't support."
|
||||
},
|
||||
"defaultLink": "e.g. {{url}}",
|
||||
"defaultNickname": "ex. Jane Pink",
|
||||
@@ -413,6 +419,7 @@
|
||||
"desktopShareError": "Could not create desktop share",
|
||||
"desktopShare":"Desktop share",
|
||||
"webAssemblyWarning": "WebAssembly not supported",
|
||||
"webAssemblyWarningDescription": "WebAssembly disabled or not supported by this browser",
|
||||
"backgroundEffectError": "Failed to apply background effect."
|
||||
},
|
||||
"feedback": {
|
||||
@@ -463,7 +470,7 @@
|
||||
"noPassword": "None",
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"password": "$t(lockRoomPasswordUppercase): ",
|
||||
"sip": "SIP address",
|
||||
"title": "Share",
|
||||
"tooltip": "Share link and dial-in info for this meeting",
|
||||
@@ -602,6 +609,7 @@
|
||||
"raisedHands": "{{participantName}} and {{raisedHands}} more people",
|
||||
"screenShareNoAudio": " Share audio box was not checked in the window selection screen.",
|
||||
"screenShareNoAudioTitle": "Couldn't share system audio!",
|
||||
"selfViewTitle": "You can always un-hide the self-view from settings",
|
||||
"somebody": "Somebody",
|
||||
"startSilentTitle": "You joined with no audio output!",
|
||||
"startSilentDescription": "Rejoin the meeting to enable audio",
|
||||
@@ -628,9 +636,10 @@
|
||||
"moderationToggleDescription": "by {{participantDisplayName}}",
|
||||
"raiseHandAction": "Raise hand",
|
||||
"reactionSounds": "Disable sounds",
|
||||
"reactionSoundsForAll": "Disable sounds for all",
|
||||
"groupTitle": "Notifications",
|
||||
"videoUnmuteBlockedTitle": "Camera unmute blocked!",
|
||||
"videoUnmuteBlockedDescription": "Camera unmute operation has been temporarily blocked because of system limits."
|
||||
"videoUnmuteBlockedTitle": "Camera unmute and desktop sharing blocked!",
|
||||
"videoUnmuteBlockedDescription": "Camera unmute and desktop sharing operation have been temporarily blocked because of system limits."
|
||||
},
|
||||
"participantsPane": {
|
||||
"close": "Close",
|
||||
@@ -655,7 +664,8 @@
|
||||
"stopEveryonesVideo": "Stop everyone's video",
|
||||
"stopVideo": "Stop video",
|
||||
"unblockEveryoneMicCamera": "Unblock everyone's mic and camera",
|
||||
"videoModeration": "Start their video"
|
||||
"videoModeration": "Start their video",
|
||||
"moreModerationControls": "More moderation controls"
|
||||
},
|
||||
"search": "Search participants"
|
||||
},
|
||||
@@ -814,8 +824,8 @@
|
||||
"security": {
|
||||
"about": "You can add a $t(lockRoomPassword) to your meeting. Participants will need to provide the $t(lockRoomPassword) before they are allowed to join the meeting.",
|
||||
"aboutReadOnly": "Moderator participants can add a $t(lockRoomPassword) to the meeting. Participants will need to provide the $t(lockRoomPassword) before they are allowed to join the meeting.",
|
||||
"insecureRoomNameWarning": "The room name is unsafe. Unwanted participants may join your conference. Consider securing your meeting using the security button.",
|
||||
"securityOptions": "Security options"
|
||||
"header": "Security Options",
|
||||
"insecureRoomNameWarning": "The room name is unsafe. Unwanted participants may join your conference. Consider securing your meeting using the security button."
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
@@ -850,6 +860,7 @@
|
||||
"sounds": "Sounds",
|
||||
"speakers": "Speakers",
|
||||
"startAudioMuted": "Everyone starts muted",
|
||||
"startReactionsMuted": "Mute reaction sounds for everyone",
|
||||
"startVideoMuted": "Everyone starts hidden",
|
||||
"talkWhileMuted": "Talk while muted",
|
||||
"title": "Settings"
|
||||
@@ -882,20 +893,20 @@
|
||||
},
|
||||
"speaker": "Speaker",
|
||||
"speakerStats": {
|
||||
"search": "Search",
|
||||
"angry": "Angry",
|
||||
"disgusted": "Disgusted",
|
||||
"fearful": "Fearful",
|
||||
"happy": "Happy",
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Name",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Speaker Stats",
|
||||
"speakerTime": "Speaker Time",
|
||||
"happy": "Happy",
|
||||
"neutral": "Neutral",
|
||||
"sad": "Sad",
|
||||
"surprised": "Surprised",
|
||||
"angry": "Angry",
|
||||
"fearful": "Fearful",
|
||||
"disgusted": "Disgusted"
|
||||
"search": "Search",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerTime": "Speaker Time",
|
||||
"speakerStats": "Speaker Stats",
|
||||
"surprised": "Surprised"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -1115,6 +1126,7 @@
|
||||
"domuteVideoOfOthers": "Disable camera of everyone else",
|
||||
"flip": "Flip",
|
||||
"grantModerator": "Grant Moderator Rights",
|
||||
"hideSelfView": "Hide self view",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Participant is muted",
|
||||
|
||||
@@ -66,8 +66,9 @@ import { toggleLobbyMode, setKnockingParticipantApproval } from '../../react/fea
|
||||
import { isForceMuted } from '../../react/features/participants-pane/functions';
|
||||
import { RECORDING_TYPES } from '../../react/features/recording/constants';
|
||||
import { getActiveSession } from '../../react/features/recording/functions';
|
||||
import { isScreenAudioSupported } from '../../react/features/screen-share';
|
||||
import { isScreenAudioSupported, isScreenVideoShared } from '../../react/features/screen-share';
|
||||
import { startScreenShareFlow, startAudioScreenShareFlow } from '../../react/features/screen-share/actions';
|
||||
import { toggleScreenshotCaptureSummary } from '../../react/features/screenshot-capture';
|
||||
import { playSharedVideo, stopSharedVideo } from '../../react/features/shared-video/actions.any';
|
||||
import { toggleTileView, setTileView } from '../../react/features/video-layout';
|
||||
import { muteAllParticipants } from '../../react/features/video-menu/actions';
|
||||
@@ -470,6 +471,9 @@ function initCommands() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isScreenVideoShared(APP.store.getState())) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
conference.startRecording(recordingConfig);
|
||||
},
|
||||
|
||||
@@ -498,6 +502,7 @@ function initCommands() {
|
||||
const activeSession = getActiveSession(state, mode);
|
||||
|
||||
if (activeSession && activeSession.id) {
|
||||
APP.store.dispatch(toggleScreenshotCaptureSummary(false));
|
||||
conference.stopRecording(activeSession.id);
|
||||
} else {
|
||||
logger.error('No recording or streaming session found');
|
||||
@@ -1470,12 +1475,14 @@ class API {
|
||||
* available.
|
||||
*
|
||||
* @param {string} link - The recording download link.
|
||||
* @param {number} ttl - The recording download link time to live.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyRecordingLinkAvailable(link: string) {
|
||||
notifyRecordingLinkAvailable(link: string, ttl: number) {
|
||||
this._sendEvent({
|
||||
name: 'recording-link-available',
|
||||
link
|
||||
link,
|
||||
ttl
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
41
package-lock.json
generated
41
package-lock.json
generated
@@ -36,6 +36,7 @@
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.15.5",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
"@react-native-community/netinfo": "4.1.5",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
@@ -65,7 +66,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9e5d83f4aceb9f922583c871d933e97ee2b05753",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#d630bc32a1262afd0432d8b8d8d0025dd5afd1e5",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -103,7 +104,7 @@
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-video": "5.1.1",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "1.94.0",
|
||||
"react-native-webrtc": "1.94.1",
|
||||
"react-native-webview": "11.0.2",
|
||||
"react-native-youtube-iframe": "2.1.1",
|
||||
"react-redux": "7.1.0",
|
||||
@@ -4097,6 +4098,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/clipboard": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz",
|
||||
"integrity": "sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.0",
|
||||
"react-native": ">=0.57.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/google-signin": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/google-signin/-/google-signin-3.0.1.tgz",
|
||||
@@ -12509,8 +12519,8 @@
|
||||
},
|
||||
"node_modules/lib-jitsi-meet": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#9e5d83f4aceb9f922583c871d933e97ee2b05753",
|
||||
"integrity": "sha512-qqRAIX1bgqHo1EaaOpsycQ5t7ReBFu1drb6ZKh/MC5fzF3Bov3JSxSlZegYJ7d9ly8e9hAeIaGey6qqObNiLaA==",
|
||||
"resolved": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#d630bc32a1262afd0432d8b8d8d0025dd5afd1e5",
|
||||
"integrity": "sha512-LvIU+CghVMyI0nc+pjWH2EXgo5i7z2wgzjXhpTan0SB58nGQSa4SzWCgCRSOfeT20BwHNlejPOvNrtXe2pu98A==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -16126,9 +16136,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-webrtc": {
|
||||
"version": "1.94.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.0.tgz",
|
||||
"integrity": "sha512-VmkKCwMYL/ZsLqNFX+Rqxwf3ZfHuJRfF2AZ+dQ1ZZ8O1Pg4LeZNisZo6djl9bze0fNhj7eMJTwnzk/kK2mW0hg==",
|
||||
"version": "1.94.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.1.tgz",
|
||||
"integrity": "sha512-S+qU2i0PY2QfWzh8choBlVzUF6I6DVKhrUFbGrriuffieMu+/sR40pPTNCCFEXo4USNZAAM8osBgn6E/aZsJbg==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"base64-js": "1.5.1",
|
||||
@@ -23388,6 +23398,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/cli-types/-/cli-types-3.0.0.tgz",
|
||||
"integrity": "sha512-ng6Tm537E/M42GjE4TRUxQyL8sRfClcL7bQWblOCoxPZzJ2J3bdALsjeG3vDnVCIfI/R0AeFalN9KjMt0+Z/Zg=="
|
||||
},
|
||||
"@react-native-community/clipboard": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/clipboard/-/clipboard-1.5.1.tgz",
|
||||
"integrity": "sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA=="
|
||||
},
|
||||
"@react-native-community/google-signin": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/google-signin/-/google-signin-3.0.1.tgz",
|
||||
@@ -29978,9 +29993,9 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#9e5d83f4aceb9f922583c871d933e97ee2b05753",
|
||||
"integrity": "sha512-qqRAIX1bgqHo1EaaOpsycQ5t7ReBFu1drb6ZKh/MC5fzF3Bov3JSxSlZegYJ7d9ly8e9hAeIaGey6qqObNiLaA==",
|
||||
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#9e5d83f4aceb9f922583c871d933e97ee2b05753",
|
||||
"version": "git+ssh://git@github.com/jitsi/lib-jitsi-meet.git#d630bc32a1262afd0432d8b8d8d0025dd5afd1e5",
|
||||
"integrity": "sha512-LvIU+CghVMyI0nc+pjWH2EXgo5i7z2wgzjXhpTan0SB58nGQSa4SzWCgCRSOfeT20BwHNlejPOvNrtXe2pu98A==",
|
||||
"from": "lib-jitsi-meet@github:jitsi/lib-jitsi-meet#d630bc32a1262afd0432d8b8d8d0025dd5afd1e5",
|
||||
"requires": {
|
||||
"@jitsi/js-utils": "2.0.0",
|
||||
"@jitsi/logger": "2.0.0",
|
||||
@@ -32893,9 +32908,9 @@
|
||||
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "1.94.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.0.tgz",
|
||||
"integrity": "sha512-VmkKCwMYL/ZsLqNFX+Rqxwf3ZfHuJRfF2AZ+dQ1ZZ8O1Pg4LeZNisZo6djl9bze0fNhj7eMJTwnzk/kK2mW0hg==",
|
||||
"version": "1.94.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.94.1.tgz",
|
||||
"integrity": "sha512-S+qU2i0PY2QfWzh8choBlVzUF6I6DVKhrUFbGrriuffieMu+/sR40pPTNCCFEXo4USNZAAM8osBgn6E/aZsJbg==",
|
||||
"requires": {
|
||||
"base64-js": "1.5.1",
|
||||
"event-target-shim": "6.0.2",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.15.5",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
"@react-native-community/netinfo": "4.1.5",
|
||||
"@react-native-community/slider": "3.0.3",
|
||||
@@ -70,7 +71,7 @@
|
||||
"jquery-i18next": "1.2.1",
|
||||
"js-md5": "0.6.1",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#9e5d83f4aceb9f922583c871d933e97ee2b05753",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#d630bc32a1262afd0432d8b8d8d0025dd5afd1e5",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.21",
|
||||
"moment": "2.29.1",
|
||||
@@ -108,7 +109,7 @@
|
||||
"react-native-url-polyfill": "1.2.0",
|
||||
"react-native-video": "5.1.1",
|
||||
"react-native-watch-connectivity": "0.4.3",
|
||||
"react-native-webrtc": "1.94.0",
|
||||
"react-native-webrtc": "1.94.1",
|
||||
"react-native-webview": "11.0.2",
|
||||
"react-native-youtube-iframe": "2.1.1",
|
||||
"react-redux": "7.1.0",
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../external-api/middleware';
|
||||
import '../keyboard-shortcuts/middleware';
|
||||
import '../local-recording/middleware';
|
||||
import '../no-audio-signal/middleware';
|
||||
import '../notifications/middleware';
|
||||
import '../noise-detection/middleware';
|
||||
import '../old-client-notification/middleware';
|
||||
import '../power-monitor/middleware';
|
||||
|
||||
@@ -98,11 +98,11 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
customActionNameKey: 'notify.raiseHandAction',
|
||||
customActionHandler: () => batch(() => {
|
||||
customActionNameKey: [ 'notify.raiseHandAction' ],
|
||||
customActionHandler: [ () => batch(() => {
|
||||
dispatch(raiseHand(true));
|
||||
dispatch(hideNotification(uid));
|
||||
}),
|
||||
}) ],
|
||||
descriptionKey,
|
||||
sticky: true,
|
||||
titleKey,
|
||||
@@ -221,8 +221,8 @@ StateListenerRegistry.register(
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.hostAskedUnmute',
|
||||
sticky: true,
|
||||
customActionNameKey: 'notify.unmute',
|
||||
customActionHandler: () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO))
|
||||
customActionNameKey: [ 'notify.unmute' ],
|
||||
customActionHandler: [ () => dispatch(muteLocal(false, MEDIA_TYPE.AUDIO)) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
|
||||
dispatch(playSound(ASKED_TO_UNMUTE_SOUND_ID));
|
||||
}
|
||||
|
||||
@@ -172,6 +172,17 @@ export const SEND_TONES = 'SEND_TONES';
|
||||
*/
|
||||
export const SET_FOLLOW_ME = 'SET_FOLLOW_ME';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which updates the current known status of the
|
||||
* Mute Reactions Sound feature.
|
||||
*
|
||||
* {
|
||||
* type: SET_START_REACTIONS_MUTED,
|
||||
* enabled: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_START_REACTIONS_MUTED = 'SET_START_REACTIONS_MUTED';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which sets the password to join or lock a specific
|
||||
* {@code JitsiConference}.
|
||||
|
||||
@@ -12,8 +12,6 @@ import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
|
||||
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
isAudioMuted,
|
||||
isVideoMuted,
|
||||
setAudioMuted,
|
||||
setAudioUnmutePermissions,
|
||||
setVideoMuted,
|
||||
@@ -29,7 +27,13 @@ import {
|
||||
participantRoleChanged,
|
||||
participantUpdated
|
||||
} from '../participants';
|
||||
import { getLocalTracks, replaceLocalTrack, trackAdded, trackRemoved } from '../tracks';
|
||||
import {
|
||||
destroyLocalTracks,
|
||||
getLocalTracks,
|
||||
replaceLocalTrack,
|
||||
trackAdded,
|
||||
trackRemoved
|
||||
} from '../tracks';
|
||||
import { getBackendSafeRoomName } from '../util';
|
||||
|
||||
import {
|
||||
@@ -53,7 +57,8 @@ import {
|
||||
SET_PASSWORD_FAILED,
|
||||
SET_ROOM,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_START_MUTED_POLICY
|
||||
SET_START_MUTED_POLICY,
|
||||
SET_START_REACTIONS_MUTED
|
||||
} from './actionTypes';
|
||||
import {
|
||||
AVATAR_URL_COMMAND,
|
||||
@@ -157,22 +162,12 @@ function _addConferenceListeners(conference, dispatch, state) {
|
||||
conference.on(
|
||||
JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED,
|
||||
disableAudioMuteChange => {
|
||||
const muted = isAudioMuted(state);
|
||||
|
||||
// Disable the mute button only if its muted.
|
||||
if (!disableAudioMuteChange || (disableAudioMuteChange && muted)) {
|
||||
dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
|
||||
}
|
||||
dispatch(setAudioUnmutePermissions(disableAudioMuteChange));
|
||||
});
|
||||
conference.on(
|
||||
JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED,
|
||||
disableVideoMuteChange => {
|
||||
const muted = isVideoMuted(state);
|
||||
|
||||
// Disable the mute button only if its muted.
|
||||
if (!disableVideoMuteChange || (disableVideoMuteChange && muted)) {
|
||||
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
}
|
||||
dispatch(setVideoUnmutePermissions(disableVideoMuteChange));
|
||||
});
|
||||
|
||||
// Dispatches into features/base/tracks follow:
|
||||
@@ -192,6 +187,8 @@ function _addConferenceListeners(conference, dispatch, state) {
|
||||
}
|
||||
});
|
||||
|
||||
conference.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, track => dispatch(destroyLocalTracks(track)));
|
||||
|
||||
// Dispatches into features/base/participants follow:
|
||||
conference.on(
|
||||
JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
|
||||
@@ -512,7 +509,7 @@ export function checkIfCanJoin() {
|
||||
const { authRequired, password }
|
||||
= getState()['features/base/conference'];
|
||||
|
||||
const replaceParticipant = getReplaceParticipant(APP.store.getState());
|
||||
const replaceParticipant = getReplaceParticipant(getState());
|
||||
|
||||
authRequired && dispatch(_conferenceWillJoin(authRequired));
|
||||
authRequired && authRequired.join(password, replaceParticipant);
|
||||
@@ -669,6 +666,22 @@ export function setFollowMe(enabled: boolean) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the Mute reaction sounds feature.
|
||||
*
|
||||
* @param {boolean} muted - Whether or not reaction sounds should be muted for all participants.
|
||||
* @returns {{
|
||||
* type: SET_START_REACTIONS_MUTED,
|
||||
* muted: boolean
|
||||
* }}
|
||||
*/
|
||||
export function setStartReactionsMuted(muted: boolean) {
|
||||
return {
|
||||
type: SET_START_REACTIONS_MUTED,
|
||||
muted
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the password to join or lock a specific JitsiConference.
|
||||
*
|
||||
|
||||
@@ -27,3 +27,7 @@ export const EMAIL_COMMAND = 'email';
|
||||
* from the outside is not cool but it should suffice for now.
|
||||
*/
|
||||
export const JITSI_CONFERENCE_URL_KEY = Symbol('url');
|
||||
|
||||
export const TRIGGER_READY_TO_CLOSE_REASONS = [
|
||||
'The meeting has been terminated'
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { readyToClose } from '../../../features/mobile/external-api/actions';
|
||||
import {
|
||||
ACTION_PINNED,
|
||||
ACTION_UNPINNED,
|
||||
@@ -40,6 +41,7 @@ import {
|
||||
createConference,
|
||||
setSubject
|
||||
} from './actions';
|
||||
import { TRIGGER_READY_TO_CLOSE_REASONS } from './constants';
|
||||
import {
|
||||
_addLocalTracksToConference,
|
||||
_removeLocalTracksFromConference,
|
||||
@@ -130,6 +132,14 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
|
||||
titleKey: 'dialog.sessTerminated'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
|
||||
if (TRIGGER_READY_TO_CLOSE_REASONS.includes(reason)) {
|
||||
if (typeof APP === undefined) {
|
||||
dispatch(readyToClose());
|
||||
} else {
|
||||
APP.API.notifyReadyToClose();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case JitsiConferenceErrors.CONFERENCE_RESTARTED: {
|
||||
|
||||
@@ -41,6 +41,7 @@ function _toggleScreenSharing(enabled, store) {
|
||||
}
|
||||
} else {
|
||||
dispatch(destroyLocalDesktopTrackIfExists());
|
||||
setPictureInPictureDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
SET_PASSWORD,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM,
|
||||
SET_START_MUTED_POLICY
|
||||
SET_START_MUTED_POLICY,
|
||||
SET_START_REACTIONS_MUTED
|
||||
} from './actionTypes';
|
||||
import { isRoomValid } from './functions';
|
||||
|
||||
@@ -77,6 +78,9 @@ ReducerRegistry.register(
|
||||
case SET_FOLLOW_ME:
|
||||
return set(state, 'followMeEnabled', action.enabled);
|
||||
|
||||
case SET_START_REACTIONS_MUTED:
|
||||
return set(state, 'startReactionsMuted', action.muted);
|
||||
|
||||
case SET_LOCATION_URL:
|
||||
return set(state, 'room', undefined);
|
||||
|
||||
|
||||
@@ -113,6 +113,7 @@ export default [
|
||||
'disableRemoteMute',
|
||||
'disableResponsiveTiles',
|
||||
'disableRtx',
|
||||
'disableSelfView',
|
||||
'disableScreensharingVirtualBackground',
|
||||
'disableShortcuts',
|
||||
'disableShowMoreStats',
|
||||
@@ -183,6 +184,7 @@ export default [
|
||||
'pcStatsInterval',
|
||||
'preferH264',
|
||||
'preferredCodec',
|
||||
'prejoinConfig',
|
||||
'prejoinPageEnabled',
|
||||
'requireDisplayName',
|
||||
'remoteVideoMenu',
|
||||
|
||||
@@ -125,6 +125,12 @@ function _setConfig({ dispatch, getState }, next, action) {
|
||||
}));
|
||||
}
|
||||
|
||||
if (action.config.disableSelfView) {
|
||||
dispatch(updateSettings({
|
||||
disableSelfView: true
|
||||
}));
|
||||
}
|
||||
|
||||
dispatch(updateConfig(config));
|
||||
|
||||
// FIXME On Web we rely on the global 'config' variable which gets altered
|
||||
|
||||
@@ -282,6 +282,13 @@ function _translateLegacyConfig(oldValue: Object) {
|
||||
};
|
||||
}
|
||||
|
||||
newValue.prejoinConfig = oldValue.prejoinConfig || {};
|
||||
if (oldValue.hasOwnProperty('prejoinPageEnabled')
|
||||
&& !newValue.prejoinConfig.hasOwnProperty('enabled')
|
||||
) {
|
||||
newValue.prejoinConfig.enabled = oldValue.prejoinPageEnabled;
|
||||
}
|
||||
|
||||
newValue.disabledSounds = newValue.disabledSounds || [];
|
||||
|
||||
if (oldValue.disableJoinLeaveSounds) {
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { processExternalDeviceRequest } from '../../device-selection';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showNotification, showWarningNotification } from '../../notifications';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
showNotification,
|
||||
showWarningNotification
|
||||
} from '../../notifications';
|
||||
import { replaceAudioTrackById, replaceVideoTrackById, setDeviceStatusWarning } from '../../prejoin/actions';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
@@ -294,8 +298,8 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
dispatch(showNotification({
|
||||
description,
|
||||
titleKey,
|
||||
customActionNameKey: 'notify.newDeviceAction',
|
||||
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
|
||||
customActionNameKey: [ 'notify.newDeviceAction' ],
|
||||
customActionHandler: [ _useDevice.bind(undefined, store, devicesArray) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -94,11 +94,7 @@ export function isSupportedBrowser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We are intentionally allow mobile browsers because:
|
||||
// - the WelcomePage is mobile ready;
|
||||
// - if the URL points to a conference then deep-linking will take
|
||||
// care of it.
|
||||
return isMobileBrowser() || JitsiMeetJS.isWebRtcSupported();
|
||||
return isMobileBrowser() ? isSupportedMobileBrowser() : JitsiMeetJS.isWebRtcSupported();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,9 +104,8 @@ export function isSupportedBrowser() {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isSupportedMobileBrowser() {
|
||||
return (Platform.OS === 'android' && browser.isChromiumBased())
|
||||
|| (Platform.OS === 'android' && browser.isFirefox())
|
||||
|| (Platform.OS === 'ios' && browser.isWebKitBased());
|
||||
return (Platform.OS === 'android' && browser.isSupportedAndroidBrowser())
|
||||
|| (Platform.OS === 'ios' && browser.isSupportedIOSBrowser());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
showWarningNotification
|
||||
} from '../../notifications';
|
||||
import { isForceMuted } from '../../participants-pane/functions';
|
||||
import { isScreenMediaShared } from '../../screen-share/functions';
|
||||
import { SET_AUDIO_ONLY, setAudioOnly } from '../audio-only';
|
||||
import { isRoomValid, SET_ROOM } from '../conference';
|
||||
import { getLocalParticipant } from '../participants';
|
||||
@@ -20,6 +21,7 @@ import { MiddlewareRegistry } from '../redux';
|
||||
import { getPropertyValue } from '../settings';
|
||||
import {
|
||||
destroyLocalTracks,
|
||||
isLocalTrackMuted,
|
||||
isLocalVideoTrackDesktop,
|
||||
setTrackMuted,
|
||||
TRACK_ADDED
|
||||
@@ -85,12 +87,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case SET_AUDIO_UNMUTE_PERMISSIONS: {
|
||||
const { blocked } = action;
|
||||
const state = store.getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const isAudioMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.AUDIO);
|
||||
|
||||
if (blocked) {
|
||||
if (blocked && isAudioMuted) {
|
||||
store.dispatch(showWarningNotification({
|
||||
descriptionKey: 'notify.audioUnmuteBlockedDescription',
|
||||
titleKey: 'notify.audioUnmuteBlockedTitle'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -107,12 +112,16 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
case SET_VIDEO_UNMUTE_PERMISSIONS: {
|
||||
const { blocked } = action;
|
||||
const state = store.getState();
|
||||
const tracks = state['features/base/tracks'];
|
||||
const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
|
||||
const isMediaShared = isScreenMediaShared(state);
|
||||
|
||||
if (blocked) {
|
||||
if (blocked && isVideoMuted && !isMediaShared) {
|
||||
store.dispatch(showWarningNotification({
|
||||
descriptionKey: 'notify.videoUnmuteBlockedDescription',
|
||||
titleKey: 'notify.videoUnmuteBlockedTitle'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ import { CAMERA_FACING_MODE } from './constants';
|
||||
*/
|
||||
export const _AUDIO_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
blocked: false,
|
||||
unmuteBlocked: false,
|
||||
muted: false
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
case SET_AUDIO_UNMUTE_PERMISSIONS:
|
||||
return {
|
||||
...state,
|
||||
blocked: action.blocked
|
||||
unmuteBlocked: action.blocked
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -92,7 +92,7 @@ function _audio(state = _AUDIO_INITIAL_MEDIA_STATE, action) {
|
||||
*/
|
||||
export const _VIDEO_INITIAL_MEDIA_STATE = {
|
||||
available: true,
|
||||
blocked: false,
|
||||
unmuteBlocked: false,
|
||||
facingMode: CAMERA_FACING_MODE.USER,
|
||||
muted: 0,
|
||||
|
||||
@@ -139,7 +139,7 @@ function _video(state = _VIDEO_INITIAL_MEDIA_STATE, action) {
|
||||
case SET_VIDEO_UNMUTE_PERMISSIONS:
|
||||
return {
|
||||
...state,
|
||||
blocked: action.blocked
|
||||
unmuteBlocked: action.blocked
|
||||
};
|
||||
|
||||
case STORE_VIDEO_TRANSFORM:
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useHeaderHeight } from '@react-navigation/stack';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
Platform
|
||||
Platform,
|
||||
StatusBar
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
@@ -22,6 +23,11 @@ type Props = {
|
||||
*/
|
||||
contentContainerStyle?: StyleType,
|
||||
|
||||
/**
|
||||
* Is a text input rendered at the bottom of the screen?
|
||||
*/
|
||||
hasBottomTextInput: boolean,
|
||||
|
||||
/**
|
||||
* Is the screen rendering a tab navigator?
|
||||
*/
|
||||
@@ -38,6 +44,7 @@ const JitsiKeyboardAvoidingView = (
|
||||
children,
|
||||
contentContainerStyle,
|
||||
hasTabNavigator,
|
||||
hasBottomTextInput,
|
||||
style
|
||||
}: Props) => {
|
||||
const headerHeight = useHeaderHeight();
|
||||
@@ -54,8 +61,10 @@ const JitsiKeyboardAvoidingView = (
|
||||
const tabNavigatorPadding
|
||||
= hasTabNavigator ? headerHeight : 0;
|
||||
const noNotchDevicePadding = bottomPadding || 10;
|
||||
const iosVerticalOffset = headerHeight + noNotchDevicePadding + tabNavigatorPadding;
|
||||
const androidVerticalOffset = headerHeight;
|
||||
const iosVerticalOffset
|
||||
= headerHeight + noNotchDevicePadding + tabNavigatorPadding;
|
||||
const androidVerticalOffset = hasBottomTextInput
|
||||
? headerHeight + StatusBar.currentHeight : headerHeight;
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
|
||||
@@ -27,6 +27,11 @@ type Props = {
|
||||
*/
|
||||
footerComponent?: Function,
|
||||
|
||||
/**
|
||||
* Is a text input rendered at the bottom of the screen?
|
||||
*/
|
||||
hasBottomTextInput?: boolean,
|
||||
|
||||
/**
|
||||
* Is the screen rendering a tab navigator?
|
||||
*/
|
||||
@@ -43,12 +48,14 @@ const JitsiScreen = ({
|
||||
children,
|
||||
footerComponent,
|
||||
hasTabNavigator = false,
|
||||
hasBottomTextInput = false,
|
||||
style
|
||||
}: Props) => (
|
||||
<View
|
||||
style = { styles.jitsiScreenContainer }>
|
||||
<JitsiKeyboardAvoidingView
|
||||
contentContainerStyle = { contentContainerStyle }
|
||||
hasBottomTextInput = { hasBottomTextInput }
|
||||
hasTabNavigator = { hasTabNavigator }
|
||||
style = { style }>
|
||||
<SafeAreaView
|
||||
|
||||
@@ -555,8 +555,8 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
|
||||
}
|
||||
|
||||
const action = shouldDisplayAllowAction ? {
|
||||
customActionNameKey: 'notify.allowAction',
|
||||
customActionHandler: () => dispatch(approveParticipant(participantId))
|
||||
customActionNameKey: [ 'notify.allowAction' ],
|
||||
customActionHandler: [ () => dispatch(approveParticipant(participantId)) ]
|
||||
} : {};
|
||||
|
||||
if (raisedHandTimestamp) {
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* serverURL: string,
|
||||
* startAudioOnly: boolean,
|
||||
* startWithAudioMuted: boolean,
|
||||
* startWithVideoMuted: boolean
|
||||
* startWithVideoMuted: boolean,
|
||||
* startWithReactionsMuted: boolean
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
@@ -15,9 +15,11 @@ import { SETTINGS_UPDATED } from './actionTypes';
|
||||
* localFlipX: boolean,
|
||||
* micDeviceId: string,
|
||||
* serverURL: string,
|
||||
* soundsReactions: boolean,
|
||||
* startAudioOnly: boolean,
|
||||
* startWithAudioMuted: boolean,
|
||||
* startWithVideoMuted: boolean
|
||||
* startWithVideoMuted: boolean,
|
||||
* startWithReactionsMuted: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,7 @@ const DEFAULT_STATE = {
|
||||
disableCallIntegration: undefined,
|
||||
disableCrashReporting: undefined,
|
||||
disableP2P: undefined,
|
||||
disableSelfView: false,
|
||||
displayName: undefined,
|
||||
email: undefined,
|
||||
localFlipX: true,
|
||||
|
||||
@@ -188,12 +188,19 @@ export function createLocalTracksA(options = {}) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls JitsiLocalTrack#dispose() on all local tracks ignoring errors when
|
||||
* Calls JitsiLocalTrack#dispose() on the given track or on all local tracks (if none are passed) ignoring errors if
|
||||
* track is already disposed. After that signals tracks to be removed.
|
||||
*
|
||||
* @param {JitsiLocalTrack|null} [track] - The local track that needs to be destroyed.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function destroyLocalTracks() {
|
||||
export function destroyLocalTracks(track = null) {
|
||||
if (track) {
|
||||
return dispatch => {
|
||||
dispatch(_disposeAndRemoveTracks([ track ]));
|
||||
};
|
||||
}
|
||||
|
||||
return (dispatch, getState) => {
|
||||
// First wait until any getUserMedia in progress is settled and then get
|
||||
// rid of all local tracks.
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// @flow
|
||||
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { showModeratedNotification } from '../../av-moderation/actions';
|
||||
import { shouldShowModeratedNotification } from '../../av-moderation/functions';
|
||||
import { hideNotification, isModerationNotificationDisplayed } from '../../notifications';
|
||||
import { isPrejoinPageVisible } from '../../prejoin/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { getAvailableDevices } from '../devices/actions';
|
||||
import {
|
||||
CAMERA_FACING_MODE,
|
||||
@@ -17,7 +20,7 @@ import {
|
||||
toggleCameraFacingMode,
|
||||
VIDEO_TYPE
|
||||
} from '../media';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import { MiddlewareRegistry, StateListenerRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
TRACK_ADDED,
|
||||
@@ -28,8 +31,10 @@ import {
|
||||
} from './actionTypes';
|
||||
import {
|
||||
createLocalTracksA,
|
||||
destroyLocalTracks,
|
||||
showNoDataFromSourceVideoError,
|
||||
toggleScreensharing,
|
||||
trackRemoved,
|
||||
trackNoDataFromSourceNotificationInfoChanged
|
||||
} from './actions';
|
||||
import {
|
||||
@@ -204,6 +209,26 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Set up state change listener to perform maintenance tasks when the conference
|
||||
* is left or failed, remove all tracks from the store.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => getCurrentConference(state),
|
||||
(conference, { dispatch, getState }, prevConference) => {
|
||||
if (prevConference && !conference) {
|
||||
// Clear all tracks.
|
||||
const remoteTracks = getState()['features/base/tracks'].filter(t => !t.local);
|
||||
|
||||
batch(() => {
|
||||
dispatch(destroyLocalTracks());
|
||||
for (const track of remoteTracks) {
|
||||
dispatch(trackRemoved(track.jitsiTrack));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Handles no data from source errors.
|
||||
*
|
||||
|
||||
@@ -19,6 +19,7 @@ export const colors = {
|
||||
primary09: '#CCDDF9',
|
||||
primary10: '#17A0DB',
|
||||
primary11: '#1081B2',
|
||||
primary12: '#B8C7E0',
|
||||
|
||||
surface00: '#111111',
|
||||
surface01: '#040404',
|
||||
@@ -158,6 +159,9 @@ export const colorMap = {
|
||||
// Text for drawer menu displayed name
|
||||
text05: 'surface06',
|
||||
|
||||
// Text for saved input values
|
||||
text06: 'surface03',
|
||||
|
||||
// error messages
|
||||
textError: 'error06',
|
||||
|
||||
@@ -226,6 +230,8 @@ export const colorMap = {
|
||||
// Line separators
|
||||
border03: 'surface04',
|
||||
|
||||
border04: 'primary12',
|
||||
|
||||
// Color for error border & message
|
||||
borderError: 'error06',
|
||||
|
||||
|
||||
@@ -11,9 +11,23 @@ import {
|
||||
createConference,
|
||||
getCurrentConference
|
||||
} from '../base/conference';
|
||||
import { setAudioMuted, setVideoMuted } from '../base/media';
|
||||
import {
|
||||
MEDIA_TYPE,
|
||||
setAudioMuted,
|
||||
setVideoMuted
|
||||
} from '../base/media';
|
||||
import { getRemoteParticipants } from '../base/participants';
|
||||
import { clearNotifications } from '../notifications';
|
||||
import {
|
||||
getLocalTracks,
|
||||
isLocalCameraTrackMuted,
|
||||
isLocalTrackMuted
|
||||
} from '../base/tracks';
|
||||
import { createDesiredLocalTracks } from '../base/tracks/actions';
|
||||
import {
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
clearNotifications,
|
||||
showNotification
|
||||
} from '../notifications';
|
||||
|
||||
import { _RESET_BREAKOUT_ROOMS, _UPDATE_ROOM_COUNTER } from './actionTypes';
|
||||
import { FEATURE_KEY } from './constants';
|
||||
@@ -155,8 +169,9 @@ export function sendParticipantToRoom(participantId: string, roomId: string) {
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function moveToRoom(roomId?: string) {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
let _roomId = roomId || getMainRoom(getState)?.id;
|
||||
return async (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const mainRoomId = getMainRoom(getState)?.id;
|
||||
let _roomId = roomId || mainRoomId;
|
||||
|
||||
// Check if we got a full JID.
|
||||
// $FlowExpectedError
|
||||
@@ -175,6 +190,18 @@ export function moveToRoom(roomId?: string) {
|
||||
_roomId.domain = domainParts.join('@');
|
||||
}
|
||||
|
||||
// $FlowExpectedError
|
||||
const roomIdStr = _roomId?.toString();
|
||||
const goToMainRoom = roomIdStr === mainRoomId;
|
||||
const rooms = getBreakoutRooms(getState);
|
||||
const targetRoom = rooms[roomIdStr];
|
||||
|
||||
if (!targetRoom) {
|
||||
logger.warn(`Unknown room: ${targetRoom}`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: _RESET_BREAKOUT_ROOMS
|
||||
});
|
||||
@@ -185,23 +212,55 @@ export function moveToRoom(roomId?: string) {
|
||||
|
||||
dispatch(conferenceWillLeave(conference));
|
||||
|
||||
conference.leave()
|
||||
.catch(error => {
|
||||
logger.warn('JitsiConference.leave() rejected with:', error);
|
||||
try {
|
||||
await conference.leave();
|
||||
} catch (error) {
|
||||
logger.warn('JitsiConference.leave() rejected with:', error);
|
||||
|
||||
dispatch(conferenceLeft(conference));
|
||||
})
|
||||
.then(() => {
|
||||
dispatch(clearNotifications());
|
||||
dispatch(conferenceLeft(conference));
|
||||
}
|
||||
|
||||
// dispatch(setRoom(_roomId));
|
||||
dispatch(createConference(_roomId));
|
||||
dispatch(setAudioMuted(audio.muted));
|
||||
dispatch(setVideoMuted(video.muted));
|
||||
});
|
||||
dispatch(clearNotifications());
|
||||
|
||||
// dispatch(setRoom(_roomId));
|
||||
dispatch(createConference(_roomId));
|
||||
dispatch(setAudioMuted(audio.muted));
|
||||
dispatch(setVideoMuted(video.muted));
|
||||
dispatch(createDesiredLocalTracks());
|
||||
} else {
|
||||
APP.conference.leaveRoom(false /* doDisconnect */)
|
||||
.finally(() => APP.conference.joinRoom(_roomId));
|
||||
const localTracks = getLocalTracks(getState()['features/base/tracks']);
|
||||
const isAudioMuted = isLocalTrackMuted(localTracks, MEDIA_TYPE.AUDIO);
|
||||
const isVideoMuted = isLocalCameraTrackMuted(localTracks);
|
||||
|
||||
try {
|
||||
await APP.conference.leaveRoom(false /* doDisconnect */);
|
||||
} catch (error) {
|
||||
logger.warn('APP.conference.leaveRoom() rejected with:', error);
|
||||
|
||||
// TODO: revisit why we don't dispatch CONFERENCE_LEFT here.
|
||||
}
|
||||
|
||||
APP.conference.joinRoom(_roomId, {
|
||||
startWithAudioMuted: isAudioMuted,
|
||||
startWithVideoMuted: isVideoMuted
|
||||
});
|
||||
}
|
||||
|
||||
if (goToMainRoom) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'breakoutRooms.notifications.joinedTitle',
|
||||
descriptionKey: 'breakoutRooms.notifications.joinedMainRoom',
|
||||
concatText: true,
|
||||
maxLines: 2
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
} else {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'breakoutRooms.notifications.joinedTitle',
|
||||
descriptionKey: 'breakoutRooms.notifications.joined',
|
||||
descriptionArguments: { name: targetRoom.name },
|
||||
concatText: true,
|
||||
maxLines: 2
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export const CollapsibleRoom = ({ room }: Props) => {
|
||||
horizontal = { false }
|
||||
keyExtractor = { _keyExtractor }
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
renderItem = { item => <BreakoutRoomParticipantItem item = { item } /> }
|
||||
renderItem = { ({ item: participant }) => <BreakoutRoomParticipantItem item = { participant } /> }
|
||||
showsHorizontalScrollIndicator = { false }
|
||||
windowSize = { 2 } />}
|
||||
</View>
|
||||
|
||||
@@ -53,6 +53,7 @@ class Chat extends AbstractChat<Props> {
|
||||
|
||||
return (
|
||||
<JitsiScreen
|
||||
hasBottomTextInput = { true }
|
||||
hasTabNavigator = { true }
|
||||
style = { styles.chatContainer }>
|
||||
<MessageContainer messages = { _messages } />
|
||||
|
||||
@@ -13,6 +13,8 @@ import AddPeopleDialog
|
||||
from '../../../invite/components/add-people-dialog/native/AddPeopleDialog';
|
||||
import LobbyScreen from '../../../lobby/components/native/LobbyScreen';
|
||||
import { ParticipantsPane } from '../../../participants-pane/components/native';
|
||||
import SecurityDialog
|
||||
from '../../../security/components/security-dialog/native/SecurityDialog';
|
||||
import SpeakerStats
|
||||
from '../../../speaker-stats/components/native/SpeakerStats';
|
||||
import { getDisablePolls } from '../../functions';
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
lobbyScreenOptions,
|
||||
navigationContainerTheme,
|
||||
participantsScreenOptions,
|
||||
securityScreenOptions,
|
||||
sharedDocumentScreenOptions,
|
||||
speakerStatsScreenOptions
|
||||
} from './ConferenceNavigatorScreenOptions';
|
||||
@@ -38,14 +41,19 @@ const ConferenceStack = createStackNavigator();
|
||||
|
||||
const ConferenceNavigationContainer = () => {
|
||||
const isPollsDisabled = useSelector(getDisablePolls);
|
||||
const ChatScreen
|
||||
= isPollsDisabled
|
||||
? Chat
|
||||
: ChatAndPolls;
|
||||
const chatScreenName
|
||||
= isPollsDisabled
|
||||
? screen.conference.chat
|
||||
: screen.conference.chatandpolls.main;
|
||||
let ChatScreen;
|
||||
let chatScreenName;
|
||||
let chatTitleString;
|
||||
|
||||
if (isPollsDisabled) {
|
||||
ChatScreen = Chat;
|
||||
chatScreenName = screen.conference.chat;
|
||||
chatTitleString = 'chat.title';
|
||||
} else {
|
||||
ChatScreen = ChatAndPolls;
|
||||
chatScreenName = screen.conference.chatandpolls.main;
|
||||
chatTitleString = 'chat.titleWithPolls';
|
||||
}
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -66,7 +74,7 @@ const ConferenceNavigationContainer = () => {
|
||||
name = { chatScreenName }
|
||||
options = {{
|
||||
...chatScreenOptions,
|
||||
title: t('chat.title')
|
||||
title: t(chatTitleString)
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { ParticipantsPane }
|
||||
@@ -75,11 +83,19 @@ const ConferenceNavigationContainer = () => {
|
||||
...participantsScreenOptions,
|
||||
title: t('participantsPane.header')
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { SecurityDialog }
|
||||
name = { screen.conference.security }
|
||||
options = {{
|
||||
...securityScreenOptions,
|
||||
title: t('security.header')
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { SpeakerStats }
|
||||
name = { screen.conference.speakerStats }
|
||||
options = {{
|
||||
...speakerStatsScreenOptions
|
||||
...speakerStatsScreenOptions,
|
||||
title: t('speakerStats.speakerStats')
|
||||
}} />
|
||||
<ConferenceStack.Screen
|
||||
component = { LobbyScreen }
|
||||
|
||||
@@ -222,6 +222,13 @@ export const speakerStatsScreenOptions = {
|
||||
...presentationScreenOptions
|
||||
};
|
||||
|
||||
/**
|
||||
* Screen options for security options modal.
|
||||
*/
|
||||
export const securityScreenOptions = {
|
||||
...presentationScreenOptions
|
||||
};
|
||||
|
||||
/**
|
||||
* Screen options for shared document.
|
||||
*/
|
||||
|
||||
@@ -97,7 +97,7 @@ function _mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
_conferenceTimerEnabled:
|
||||
getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp,
|
||||
Boolean(getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer && startTimestamp),
|
||||
_meetingName: getConferenceName(state),
|
||||
_meetingNameEnabled:
|
||||
getFeatureFlag(state, MEETING_NAME_ENABLED, true) && !hideConferenceSubject,
|
||||
|
||||
@@ -21,6 +21,7 @@ export const screen = {
|
||||
polls: 'Polls'
|
||||
}
|
||||
},
|
||||
security: 'Security Options',
|
||||
speakerStats: 'Speaker Stats',
|
||||
participants: 'Participants',
|
||||
invite: 'Invite',
|
||||
|
||||
@@ -163,13 +163,19 @@ class DeepLinkingMobilePage extends Component<Props> {
|
||||
</a>
|
||||
{
|
||||
isSupportedMobileBrowser()
|
||||
&& <a
|
||||
onClick = { this._onLaunchWeb }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.launchWebButton`) }
|
||||
</button>
|
||||
</a>
|
||||
? (
|
||||
<a
|
||||
onClick = { this._onLaunchWeb }
|
||||
target = '_top'>
|
||||
<button className = { downloadButtonClassName }>
|
||||
{ t(`${_TNS}.launchWebButton`) }
|
||||
</button>
|
||||
</a>
|
||||
) : (
|
||||
<b>
|
||||
{ t(`${_TNS}.unsupportedBrowser`) }
|
||||
</b>
|
||||
)
|
||||
}
|
||||
{ renderPromotionalFooter() }
|
||||
<DialInSummary
|
||||
|
||||
@@ -56,7 +56,20 @@ export function loadWorker() {
|
||||
|
||||
return;
|
||||
}
|
||||
worker = new Worker('libs/facial-expressions-worker.min.js', { name: 'Facial Expression Worker' });
|
||||
let baseUrl = '';
|
||||
const app: Object = document.querySelector('script[src*="app.bundle.min.js"]');
|
||||
|
||||
if (app) {
|
||||
const idx = app.src.lastIndexOf('/');
|
||||
|
||||
baseUrl = `${app.src.substring(0, idx)}/`;
|
||||
}
|
||||
let workerUrl = `${baseUrl}facial-expressions-worker.min.js`;
|
||||
|
||||
const workerBlob = new Blob([ `importScripts("${workerUrl}");` ], { type: 'application/javascript' });
|
||||
|
||||
workerUrl = window.URL.createObjectURL(workerBlob);
|
||||
worker = new Worker(workerUrl, { name: 'Facial Expression Worker' });
|
||||
worker.onmessage = function(e: Object) {
|
||||
const { type, value } = e.data;
|
||||
|
||||
@@ -89,6 +102,11 @@ export function loadWorker() {
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.postMessage({
|
||||
id: 'SET_MODELS_URL',
|
||||
url: baseUrl
|
||||
});
|
||||
dispatch(startFacialRecognition());
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import * as faceapi from 'face-api.js';
|
||||
*/
|
||||
let modelsLoaded = false;
|
||||
|
||||
/**
|
||||
* The url where the models for the facial detection of expressions are located.
|
||||
*/
|
||||
let modelsURL;
|
||||
|
||||
/**
|
||||
* A flag that indicates whether the tensorflow backend is set or not.
|
||||
*/
|
||||
@@ -41,22 +46,26 @@ const window = {
|
||||
|
||||
};
|
||||
|
||||
|
||||
onmessage = async function(message) {
|
||||
if (message.data.id === 'SET_MODELS_URL') {
|
||||
modelsURL = message.data.url;
|
||||
}
|
||||
|
||||
// Receives image data
|
||||
if (message.data.id === 'SET_TIMEOUT') {
|
||||
|
||||
if (message.data.imageData === null || message.data.imageData === undefined) {
|
||||
return;
|
||||
if (!message.data.imageData || !modelsURL) {
|
||||
self.postMessage({
|
||||
type: 'facial-expression',
|
||||
value: null
|
||||
});
|
||||
}
|
||||
|
||||
// the models are loaded
|
||||
if (!modelsLoaded) {
|
||||
await faceapi.loadTinyFaceDetectorModel('.');
|
||||
await faceapi.loadFaceExpressionModel('.');
|
||||
await faceapi.loadTinyFaceDetectorModel(modelsURL);
|
||||
await faceapi.loadFaceExpressionModel(modelsURL);
|
||||
modelsLoaded = true;
|
||||
}
|
||||
|
||||
faceapi.tf.engine().startScope();
|
||||
const tensor = faceapi.tf.browser.fromPixels(message.data.imageData);
|
||||
const detections = await faceapi.detectSingleFace(
|
||||
@@ -82,29 +91,17 @@ onmessage = async function(message) {
|
||||
}
|
||||
}
|
||||
faceapi.tf.engine().endScope();
|
||||
|
||||
let facialExpression;
|
||||
|
||||
if (detections) {
|
||||
facialExpression = detections.expressions.asSortedArray()[0].expression;
|
||||
}
|
||||
|
||||
if (timeoutDuration === -1) {
|
||||
|
||||
timer = setTimeout(() => {
|
||||
self.postMessage({
|
||||
type: 'facial-expression',
|
||||
value: facialExpression
|
||||
});
|
||||
} else {
|
||||
timer = setTimeout(() => {
|
||||
self.postMessage({
|
||||
type: 'facial-expression',
|
||||
value: facialExpression
|
||||
});
|
||||
}, timeoutDuration);
|
||||
}
|
||||
|
||||
|
||||
}, timeoutDuration);
|
||||
} else if (message.data.id === 'CLEAR_TIMEOUT') {
|
||||
// Clear the timeout.
|
||||
if (timer) {
|
||||
@@ -112,5 +109,4 @@ onmessage = async function(message) {
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -29,7 +29,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
|
||||
}
|
||||
if (action.type === CONFERENCE_JOINED) {
|
||||
dispatch(loadWorker());
|
||||
dispatch(startFacialRecognition());
|
||||
|
||||
return next(action);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
calculateThumbnailSizeForTileView,
|
||||
calculateThumbnailSizeForVerticalView
|
||||
} from './functions';
|
||||
import { getDisableSelfView } from './functions.any';
|
||||
|
||||
export * from './actions.any';
|
||||
|
||||
@@ -78,6 +79,7 @@ export function setVerticalViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
const thumbnails = calculateThumbnailSizeForVerticalView(clientWidth);
|
||||
|
||||
dispatch({
|
||||
@@ -87,7 +89,8 @@ export function setVerticalViewDimensions() {
|
||||
remoteVideosContainer: {
|
||||
width: thumbnails?.local?.width
|
||||
+ TILE_HORIZONTAL_MARGIN + STAGE_VIEW_THUMBNAIL_HORIZONTAL_BORDER + SCROLL_SIZE,
|
||||
height: clientHeight - thumbnails?.local?.height - VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
height: clientHeight - (disableSelfView ? 0 : thumbnails?.local?.height)
|
||||
- VERTICAL_FILMSTRIP_VERTICAL_MARGIN
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +107,7 @@ export function setHorizontalViewDimensions() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const state = getState();
|
||||
const { clientHeight = 0, clientWidth = 0 } = state['features/base/responsive-ui'];
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
const thumbnails = calculateThumbnailSizeForHorizontalView(clientHeight);
|
||||
|
||||
dispatch({
|
||||
@@ -111,7 +115,7 @@ export function setHorizontalViewDimensions() {
|
||||
dimensions: {
|
||||
...thumbnails,
|
||||
remoteVideosContainer: {
|
||||
width: clientWidth - thumbnails?.local?.width - HORIZONTAL_FILMSTRIP_MARGIN,
|
||||
width: clientWidth - (disableSelfView ? 0 : thumbnails?.local?.width) - HORIZONTAL_FILMSTRIP_MARGIN,
|
||||
height: thumbnails?.local?.height
|
||||
+ TILE_VERTICAL_MARGIN + STAGE_VIEW_THUMBNAIL_VERTICAL_BORDER + SCROLL_SIZE
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { connect } from '../../../base/redux';
|
||||
import { ASPECT_RATIO_NARROW } from '../../../base/responsive-ui/constants';
|
||||
import { setVisibleRemoteParticipants } from '../../actions';
|
||||
import { isFilmstripVisible, shouldRemoteVideosBeVisible } from '../../functions';
|
||||
import { getDisableSelfView } from '../../functions.any';
|
||||
|
||||
import LocalThumbnail from './LocalThumbnail';
|
||||
import Thumbnail from './Thumbnail';
|
||||
@@ -31,6 +32,11 @@ type Props = {
|
||||
|
||||
_clientHeight: number,
|
||||
|
||||
/**
|
||||
* Whether or not to hide the self view.
|
||||
*/
|
||||
_disableSelfView: boolean,
|
||||
|
||||
_localParticipantId: string,
|
||||
|
||||
/**
|
||||
@@ -170,7 +176,9 @@ class Filmstrip extends PureComponent<Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = [] }) {
|
||||
if (!this._separateLocalThumbnail && viewableItems[0]?.index === 0) {
|
||||
const { _disableSelfView } = this.props;
|
||||
|
||||
if (!this._separateLocalThumbnail && !_disableSelfView && viewableItems[0]?.index === 0) {
|
||||
// Skip the local thumbnail.
|
||||
viewableItems.shift();
|
||||
}
|
||||
@@ -183,7 +191,7 @@ class Filmstrip extends PureComponent<Props> {
|
||||
let startIndex = viewableItems[0].index;
|
||||
let endIndex = viewableItems[viewableItems.length - 1].index;
|
||||
|
||||
if (!this._separateLocalThumbnail) {
|
||||
if (!this._separateLocalThumbnail && !_disableSelfView) {
|
||||
// We are off by one in the remote participants array.
|
||||
startIndex -= 1;
|
||||
endIndex -= 1;
|
||||
@@ -215,7 +223,7 @@ class Filmstrip extends PureComponent<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _aspectRatio, _localParticipantId, _participants, _visible } = this.props;
|
||||
const { _aspectRatio, _localParticipantId, _participants, _visible, _disableSelfView } = this.props;
|
||||
|
||||
if (!_visible) {
|
||||
return null;
|
||||
@@ -229,13 +237,15 @@ class Filmstrip extends PureComponent<Props> {
|
||||
? width / (thumbnailWidth + (2 * margin))
|
||||
: height / (thumbnailHeight + (2 * margin))
|
||||
);
|
||||
const participants = this._separateLocalThumbnail ? _participants : [ _localParticipantId, ..._participants ];
|
||||
const participants = this._separateLocalThumbnail || _disableSelfView
|
||||
? _participants : [ _localParticipantId, ..._participants ];
|
||||
|
||||
return (
|
||||
<SafeAreaView style = { filmstripStyle }>
|
||||
{
|
||||
this._separateLocalThumbnail
|
||||
&& !isNarrowAspectRatio
|
||||
&& !_disableSelfView
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
<FlatList
|
||||
@@ -254,7 +264,9 @@ class Filmstrip extends PureComponent<Props> {
|
||||
viewabilityConfig = { this._viewabilityConfig }
|
||||
windowSize = { 2 } />
|
||||
{
|
||||
this._separateLocalThumbnail && isNarrowAspectRatio
|
||||
this._separateLocalThumbnail
|
||||
&& isNarrowAspectRatio
|
||||
&& !_disableSelfView
|
||||
&& <LocalThumbnail />
|
||||
}
|
||||
</SafeAreaView>
|
||||
@@ -271,6 +283,7 @@ class Filmstrip extends PureComponent<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { enabled, remoteParticipants } = state['features/filmstrip'];
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
const showRemoteVideos = shouldRemoteVideosBeVisible(state);
|
||||
const responsiveUI = state['features/base/responsive-ui'];
|
||||
|
||||
@@ -278,6 +291,7 @@ function _mapStateToProps(state) {
|
||||
_aspectRatio: state['features/base/responsive-ui'].aspectRatio,
|
||||
_clientHeight: responsiveUI.clientHeight,
|
||||
_clientWidth: responsiveUI.clientWidth,
|
||||
_disableSelfView: disableSelfView,
|
||||
_localParticipantId: getLocalParticipant(state)?.id,
|
||||
_participants: showRemoteVideos ? remoteParticipants : NO_REMOTE_VIDEOS,
|
||||
_visible: enabled && isFilmstripVisible(state)
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { Dispatch } from 'redux';
|
||||
import { getLocalParticipant, getParticipantCountWithFake } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { setVisibleRemoteParticipants } from '../../actions.web';
|
||||
import { getDisableSelfView } from '../../functions.any';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
import styles from './styles';
|
||||
@@ -30,6 +31,11 @@ type Props = {
|
||||
*/
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* Whether or not to hide the self view.
|
||||
*/
|
||||
_disableSelfView: boolean,
|
||||
|
||||
/**
|
||||
* Application's viewport height.
|
||||
*/
|
||||
@@ -146,7 +152,9 @@ class TileView extends PureComponent<Props> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onViewableItemsChanged({ viewableItems = [] }: { viewableItems: Array<Object> }) {
|
||||
if (viewableItems[0]?.index === 0) {
|
||||
const { _disableSelfView } = this.props;
|
||||
|
||||
if (viewableItems[0]?.index === 0 && !_disableSelfView) {
|
||||
// Skip the local thumbnail.
|
||||
viewableItems.shift();
|
||||
}
|
||||
@@ -157,8 +165,8 @@ class TileView extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
// We are off by one in the remote participants array.
|
||||
const startIndex = viewableItems[0].index - 1;
|
||||
const endIndex = viewableItems[viewableItems.length - 1].index - 1;
|
||||
const startIndex = viewableItems[0].index - (_disableSelfView ? 0 : 1);
|
||||
const endIndex = viewableItems[viewableItems.length - 1].index - (_disableSelfView ? 0 : 1);
|
||||
|
||||
this.props.dispatch(setVisibleRemoteParticipants(startIndex, endIndex));
|
||||
}
|
||||
@@ -221,12 +229,16 @@ class TileView extends PureComponent<Props> {
|
||||
* @returns {Participant[]}
|
||||
*/
|
||||
_getSortedParticipants() {
|
||||
const { _localParticipant, _remoteParticipants } = this.props;
|
||||
const { _localParticipant, _remoteParticipants, _disableSelfView } = this.props;
|
||||
|
||||
if (!_localParticipant) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
if (_disableSelfView) {
|
||||
return _remoteParticipants;
|
||||
}
|
||||
|
||||
return [ _localParticipant?.id, ..._remoteParticipants ];
|
||||
}
|
||||
|
||||
@@ -263,12 +275,14 @@ class TileView extends PureComponent<Props> {
|
||||
function _mapStateToProps(state) {
|
||||
const responsiveUi = state['features/base/responsive-ui'];
|
||||
const { remoteParticipants, tileViewDimensions } = state['features/filmstrip'];
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
const { height } = tileViewDimensions.thumbnailSize;
|
||||
const { columns } = tileViewDimensions;
|
||||
|
||||
return {
|
||||
_aspectRatio: responsiveUi.aspectRatio,
|
||||
_columns: columns,
|
||||
_disableSelfView: disableSelfView,
|
||||
_height: responsiveUi.clientHeight,
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_participantCount: getParticipantCountWithFake(state),
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
TOOLBAR_HEIGHT_MOBILE
|
||||
} from '../../constants';
|
||||
import { shouldRemoteVideosBeVisible } from '../../functions';
|
||||
import { getDisableSelfView } from '../../functions.any';
|
||||
|
||||
import AudioTracksContainer from './AudioTracksContainer';
|
||||
import Thumbnail from './Thumbnail';
|
||||
@@ -54,6 +55,11 @@ type Props = {
|
||||
*/
|
||||
_columns: number,
|
||||
|
||||
/**
|
||||
* Whether or not to hide the self view.
|
||||
*/
|
||||
_disableSelfView: boolean,
|
||||
|
||||
/**
|
||||
* The width of the filmstrip.
|
||||
*/
|
||||
@@ -189,7 +195,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
*/
|
||||
render() {
|
||||
const filmstripStyle = { };
|
||||
const { _currentLayout } = this.props;
|
||||
const { _currentLayout, _disableSelfView } = this.props;
|
||||
const tileViewActive = _currentLayout === LAYOUTS.TILE_VIEW;
|
||||
|
||||
switch (_currentLayout) {
|
||||
@@ -214,16 +220,18 @@ class Filmstrip extends PureComponent <Props> {
|
||||
<div
|
||||
className = { this.props._videosClassName }
|
||||
id = 'remoteVideos'>
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalVideo'>
|
||||
<div id = 'filmstripLocalVideoThumbnail'>
|
||||
{
|
||||
!tileViewActive && <Thumbnail
|
||||
key = 'local' />
|
||||
}
|
||||
{!_disableSelfView && (
|
||||
<div
|
||||
className = 'filmstrip__videos'
|
||||
id = 'filmstripLocalVideo'>
|
||||
<div id = 'filmstripLocalVideoThumbnail'>
|
||||
{
|
||||
!tileViewActive && <Thumbnail
|
||||
key = 'local' />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
this._renderRemoteParticipants()
|
||||
}
|
||||
@@ -241,11 +249,11 @@ class Filmstrip extends PureComponent <Props> {
|
||||
* @returns {Object}
|
||||
*/
|
||||
_calculateIndices(startIndex, stopIndex) {
|
||||
const { _currentLayout, _iAmRecorder, _thumbnailsReordered } = this.props;
|
||||
const { _currentLayout, _iAmRecorder, _thumbnailsReordered, _disableSelfView } = this.props;
|
||||
let start = startIndex;
|
||||
let stop = stopIndex;
|
||||
|
||||
if (_thumbnailsReordered) {
|
||||
if (_thumbnailsReordered && !_disableSelfView) {
|
||||
// In tile view, the indices needs to be offset by 1 because the first thumbnail is that of the local
|
||||
// endpoint. The remote participants start from index 1.
|
||||
if (!_iAmRecorder && _currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
@@ -301,6 +309,7 @@ class Filmstrip extends PureComponent <Props> {
|
||||
*/
|
||||
_gridItemKey({ columnIndex, rowIndex }) {
|
||||
const {
|
||||
_disableSelfView,
|
||||
_columns,
|
||||
_iAmRecorder,
|
||||
_remoteParticipants,
|
||||
@@ -310,8 +319,8 @@ class Filmstrip extends PureComponent <Props> {
|
||||
const index = (rowIndex * _columns) + columnIndex;
|
||||
|
||||
// When the thumbnails are reordered, local participant is inserted at index 0.
|
||||
const localIndex = _thumbnailsReordered ? 0 : _remoteParticipantsLength;
|
||||
const remoteIndex = _thumbnailsReordered && !_iAmRecorder ? index - 1 : index;
|
||||
const localIndex = _thumbnailsReordered && !_disableSelfView ? 0 : _remoteParticipantsLength;
|
||||
const remoteIndex = _thumbnailsReordered && !_iAmRecorder && !_disableSelfView ? index - 1 : index;
|
||||
|
||||
if (index > _remoteParticipantsLength - (_iAmRecorder ? 1 : 0)) {
|
||||
return `empty-${index}`;
|
||||
@@ -571,6 +580,7 @@ function _mapStateToProps(state) {
|
||||
thumbnailSize: tileViewThumbnailSize
|
||||
} = state['features/filmstrip'].tileViewDimensions;
|
||||
const _currentLayout = getCurrentLayout(state);
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
|
||||
const { clientHeight, clientWidth } = state['features/base/responsive-ui'];
|
||||
const availableSpace = clientHeight - filmstripHeight;
|
||||
@@ -624,6 +634,7 @@ function _mapStateToProps(state) {
|
||||
_className: className,
|
||||
_columns: gridDimensions.columns,
|
||||
_currentLayout,
|
||||
_disableSelfView: disableSelfView,
|
||||
_filmstripHeight: remoteFilmstripHeight,
|
||||
_filmstripWidth: remoteFilmstripWidth,
|
||||
_iAmRecorder: Boolean(iAmRecorder),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { shouldComponentUpdate } from 'react-window';
|
||||
|
||||
import { connect } from '../../../base/redux';
|
||||
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
|
||||
import { getDisableSelfView } from '../../functions.any';
|
||||
|
||||
import Thumbnail from './Thumbnail';
|
||||
|
||||
@@ -12,6 +13,11 @@ import Thumbnail from './Thumbnail';
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Whether or not to hide the self view.
|
||||
*/
|
||||
_disableSelfView: boolean,
|
||||
|
||||
/**
|
||||
* The horizontal offset in px for the thumbnail. Used to center the thumbnails in the last row in tile view.
|
||||
*/
|
||||
@@ -69,14 +75,14 @@ class ThumbnailWrapper extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { _participantID, style, _horizontalOffset = 0 } = this.props;
|
||||
const { _participantID, style, _horizontalOffset = 0, _disableSelfView } = this.props;
|
||||
|
||||
if (typeof _participantID !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (_participantID === 'local') {
|
||||
return (
|
||||
return _disableSelfView ? null : (
|
||||
<Thumbnail
|
||||
horizontalOffset = { _horizontalOffset }
|
||||
key = 'local'
|
||||
@@ -105,6 +111,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
const { remoteParticipants } = state['features/filmstrip'];
|
||||
const remoteParticipantsLength = remoteParticipants.length;
|
||||
const { testing = {} } = state['features/base/config'];
|
||||
const disableSelfView = getDisableSelfView(state);
|
||||
const enableThumbnailReordering = testing.enableThumbnailReordering ?? true;
|
||||
|
||||
if (_currentLayout === LAYOUTS.TILE_VIEW) {
|
||||
@@ -114,7 +121,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
const index = (rowIndex * columns) + columnIndex;
|
||||
let horizontalOffset;
|
||||
const { iAmRecorder } = state['features/base/config'];
|
||||
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1);
|
||||
const participantsLenght = remoteParticipantsLength + (iAmRecorder ? 0 : 1) - (disableSelfView ? 1 : 0);
|
||||
|
||||
if (rowIndex === rows - 1) { // center the last row
|
||||
const { width: thumbnailWidth } = thumbnailSize;
|
||||
@@ -130,11 +137,12 @@ function _mapStateToProps(state, ownProps) {
|
||||
}
|
||||
|
||||
// When the thumbnails are reordered, local participant is inserted at index 0.
|
||||
const localIndex = enableThumbnailReordering ? 0 : remoteParticipantsLength;
|
||||
const remoteIndex = enableThumbnailReordering && !iAmRecorder ? index - 1 : index;
|
||||
const localIndex = enableThumbnailReordering && !disableSelfView ? 0 : remoteParticipantsLength;
|
||||
const remoteIndex = enableThumbnailReordering && !iAmRecorder && !disableSelfView ? index - 1 : index;
|
||||
|
||||
if (!iAmRecorder && index === localIndex) {
|
||||
return {
|
||||
_disableSelfView: disableSelfView,
|
||||
_participantID: 'local',
|
||||
_horizontalOffset: horizontalOffset
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { getParticipantCount } from '../base/participants';
|
||||
|
||||
import { setRemoteParticipants } from './actions';
|
||||
|
||||
/**
|
||||
@@ -80,3 +82,16 @@ export function updateRemoteParticipantsOnLeave(store: Object, participantId: ?s
|
||||
reorderedParticipants.delete(participantId)
|
||||
&& store.dispatch(setRemoteParticipants(Array.from(reorderedParticipants)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the disable self view flag.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function getDisableSelfView(state: Object) {
|
||||
const { disableSelfView } = state['features/base/settings'];
|
||||
const participantsCount = getParticipantCount(state);
|
||||
|
||||
return participantsCount === 1 ? false : disableSelfView;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { isMobileBrowser } from '../base/environment/utils';
|
||||
import { getParticipantCountWithFake } from '../base/participants';
|
||||
import { StateListenerRegistry, equals } from '../base/redux';
|
||||
import { clientResized } from '../base/responsive-ui';
|
||||
@@ -26,8 +27,13 @@ import './subscriber.any';
|
||||
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ getParticipantCountWithFake,
|
||||
/* listener */ (numberOfParticipants, store) => {
|
||||
/* selector */ state => {
|
||||
return {
|
||||
numberOfParticipants: getParticipantCountWithFake(state),
|
||||
disableSelfView: state['features/base/settings'].disableSelfView
|
||||
};
|
||||
},
|
||||
/* listener */ (currentState, store) => {
|
||||
const state = store.getState();
|
||||
|
||||
if (shouldDisplayTileView(state)) {
|
||||
@@ -38,6 +44,8 @@ StateListenerRegistry.register(
|
||||
store.dispatch(setTileViewDimensions(gridDimensions));
|
||||
}
|
||||
}
|
||||
}, {
|
||||
deepEquals: true
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -99,7 +107,9 @@ StateListenerRegistry.register(
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/responsive-ui'].clientWidth < DISPLAY_DRAWER_THRESHOLD,
|
||||
/* listener */ (widthBelowThreshold, store) => {
|
||||
store.dispatch(setOverflowDrawer(widthBelowThreshold));
|
||||
if (isMobileBrowser()) {
|
||||
store.dispatch(setOverflowDrawer(widthBelowThreshold));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -147,8 +147,8 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
<TouchableRipple
|
||||
disabled = { this._isAddDisabled() }
|
||||
rippleColor = { palette.screen01Header } >
|
||||
<Text
|
||||
style = { styles.headerSendInvite }>{ t('inviteDialog.send') }
|
||||
<Text style = { styles.headerSendInvite }>
|
||||
{ t('inviteDialog.send') }
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
)
|
||||
@@ -171,9 +171,8 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
|
||||
disabled = { this._isAddDisabled() }
|
||||
onPress = { this._onInvite }
|
||||
rippleColor = { palette.screen01Header } >
|
||||
<Text
|
||||
/* eslint-disable-next-line react-native/no-inline-styles */
|
||||
style = { styles.headerSendInvite }>{ t('inviteDialog.send') }
|
||||
<Text style = { styles.headerSendInvite }>
|
||||
{ t('inviteDialog.send') }
|
||||
</Text>
|
||||
</TouchableRipple>
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import { ColorPalette } from '../../../base/styles';
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme';
|
||||
|
||||
const SECONDARY_COLOR = '#B8C7E0';
|
||||
const SECONDARY_COLOR = BaseTheme.palette.border04;
|
||||
|
||||
export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight;
|
||||
export const ENABLED_TRACK_COLOR = ColorPalette.blue;
|
||||
export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey;
|
||||
export const ENABLED_THUMB_COLOR = BaseTheme.palette.action04;
|
||||
export const ENABLED_TRACK_COLOR = BaseTheme.palette.screen01Header;
|
||||
export const DISABLED_THUMB_COLOR = BaseTheme.palette.icon04;
|
||||
|
||||
export default {
|
||||
button: {
|
||||
@@ -61,7 +61,7 @@ export default {
|
||||
},
|
||||
|
||||
fieldError: {
|
||||
color: ColorPalette.warning,
|
||||
color: BaseTheme.palette.warning07,
|
||||
fontSize: 10
|
||||
},
|
||||
|
||||
@@ -165,7 +165,7 @@ export default {
|
||||
|
||||
lobbySwitchContainer: {
|
||||
flexDirection: 'column',
|
||||
marginTop: 16
|
||||
marginTop: BaseTheme.spacing[2]
|
||||
},
|
||||
|
||||
lobbySwitchIcon: {
|
||||
|
||||
@@ -94,8 +94,8 @@ async function _handleNoAudioSignalNotification({ dispatch, getState }, action)
|
||||
// at the point of the implementation the showNotification function only supports doing that for
|
||||
// the description.
|
||||
// TODO Add support for arguments to showNotification title and customAction strings.
|
||||
customActionNameKey = `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}`;
|
||||
customActionHandler = () => {
|
||||
customActionNameKey = [ `Switch to ${formatDeviceLabel(activeDevice.deviceLabel)}` ];
|
||||
customActionHandler = [ () => {
|
||||
// Select device callback
|
||||
dispatch(
|
||||
updateSettings({
|
||||
@@ -105,7 +105,7 @@ async function _handleNoAudioSignalNotification({ dispatch, getState }, action)
|
||||
);
|
||||
|
||||
dispatch(setAudioInputDevice(activeDevice.deviceId));
|
||||
};
|
||||
} ];
|
||||
}
|
||||
|
||||
const notification = await dispatch(showNotification({
|
||||
|
||||
@@ -20,12 +20,12 @@ export type Props = {
|
||||
/**
|
||||
* Callback invoked when the custom button is clicked.
|
||||
*/
|
||||
customActionHandler: Function,
|
||||
customActionHandler: Function[],
|
||||
|
||||
/**
|
||||
* The text to display as button in the notification for the custom action.
|
||||
*/
|
||||
customActionNameKey: string,
|
||||
customActionNameKey: string[],
|
||||
|
||||
/**
|
||||
* The text to display in the body of the notification. If not passed
|
||||
|
||||
@@ -128,17 +128,17 @@ class Notification extends AbstractNotification<Props> {
|
||||
];
|
||||
|
||||
default:
|
||||
if (this.props.customActionNameKey && this.props.customActionHandler) {
|
||||
return [
|
||||
{
|
||||
content: this.props.t(this.props.customActionNameKey),
|
||||
if (this.props.customActionNameKey?.length && this.props.customActionHandler?.length) {
|
||||
return this.props.customActionNameKey.map((customAction: string, customActionIndex: number) => {
|
||||
return {
|
||||
content: this.props.t(customAction),
|
||||
onClick: () => {
|
||||
if (this.props.customActionHandler()) {
|
||||
if (this.props.customActionHandler[customActionIndex]()) {
|
||||
this._onDismissed();
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return [];
|
||||
|
||||
1
react/features/notifications/middleware.native.js
Normal file
1
react/features/notifications/middleware.native.js
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware.any';
|
||||
40
react/features/notifications/middleware.web.js
Normal file
40
react/features/notifications/middleware.web.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/* @flow */
|
||||
|
||||
import { CONFERENCE_JOINED } from '../base/conference';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { openSettingsDialog, SETTINGS_TABS } from '../settings';
|
||||
|
||||
import {
|
||||
showNotification
|
||||
} from './actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from './constants';
|
||||
|
||||
import './middleware.any';
|
||||
|
||||
/**
|
||||
* Middleware that captures actions to display notifications.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
const { dispatch, getState } = store;
|
||||
const { disableSelfView } = getState()['features/base/settings'];
|
||||
|
||||
if (disableSelfView) {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.selfViewTitle',
|
||||
customActionNameKey: [ 'settings.title' ],
|
||||
customActionHandler: [ () =>
|
||||
dispatch(openSettingsDialog(SETTINGS_TABS.PROFILE))
|
||||
]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -72,7 +72,7 @@ const useStyles = makeStyles(theme => {
|
||||
padding: '10px 16px',
|
||||
|
||||
'&.focused': {
|
||||
border: `3px solid ${theme.palette.field01Focus}`
|
||||
outline: `3px solid ${theme.palette.field01Focus}`
|
||||
}
|
||||
},
|
||||
clearButton: {
|
||||
|
||||
@@ -17,12 +17,17 @@ import {
|
||||
} from '../../../av-moderation/functions';
|
||||
import { ContextMenu, ContextMenuItemGroup } from '../../../base/components';
|
||||
import { openDialog } from '../../../base/dialog';
|
||||
import { IconCheck, IconVideoOff } from '../../../base/icons';
|
||||
import {
|
||||
IconCheck,
|
||||
IconHorizontalPoints,
|
||||
IconVideoOff
|
||||
} from '../../../base/icons';
|
||||
import { MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
getParticipantCount,
|
||||
isEveryoneModerator
|
||||
} from '../../../base/participants';
|
||||
import { openSettingsDialog, SETTINGS_TABS } from '../../../settings';
|
||||
import { MuteEveryonesVideoDialog } from '../../../video-menu/components';
|
||||
|
||||
const useStyles = makeStyles(theme => {
|
||||
@@ -95,6 +100,8 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
|
||||
const muteAllVideo = useCallback(
|
||||
() => dispatch(openDialog(MuteEveryonesVideoDialog)), [ dispatch ]);
|
||||
|
||||
const openModeratorSettings = () => dispatch(openSettingsDialog(SETTINGS_TABS.MODERATOR));
|
||||
|
||||
const actions = [
|
||||
{
|
||||
accessibilityLabel: t('participantsPane.actions.audioModeration'),
|
||||
@@ -139,6 +146,14 @@ export const FooterContextMenu = ({ isOpen, onDrawerClose, onMouseLeave }: Props
|
||||
</div>
|
||||
</ContextMenuItemGroup>
|
||||
)}
|
||||
<ContextMenuItemGroup
|
||||
actions = { [ {
|
||||
accessibilityLabel: t('participantsPane.actions.moreModerationControls'),
|
||||
id: 'participants-pane-open-moderation-control-settings',
|
||||
icon: IconHorizontalPoints,
|
||||
onClick: openModeratorSettings,
|
||||
text: t('participantsPane.actions.moreModerationControls')
|
||||
} ] } />
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -306,8 +306,9 @@ class MeetingParticipantContextMenu extends Component<Props> {
|
||||
*/
|
||||
_onSendToRoom(room: Object) {
|
||||
return () => {
|
||||
const { _participant, dispatch } = this.props;
|
||||
const { _participant, dispatch, onSelect } = this.props;
|
||||
|
||||
onSelect(true);
|
||||
sendAnalytics(createBreakoutRoomsEvent('send.participant.to.room'));
|
||||
dispatch(sendParticipantToRoom(_participant.id, room.id));
|
||||
};
|
||||
|
||||
@@ -59,7 +59,7 @@ const styles = theme => {
|
||||
display: 'flex',
|
||||
height: 40,
|
||||
fontSize: 15,
|
||||
lineHeight: 24,
|
||||
lineHeight: '24px',
|
||||
padding: '0 16px',
|
||||
backgroundColor: theme.palette.field02,
|
||||
|
||||
|
||||
@@ -59,6 +59,11 @@ type Props = {
|
||||
*/
|
||||
updateSettings: Function,
|
||||
|
||||
/**
|
||||
* The prejoin config.
|
||||
*/
|
||||
prejoinConfig?: Object,
|
||||
|
||||
/**
|
||||
* Whether the name input should be read only or not.
|
||||
*/
|
||||
@@ -139,6 +144,7 @@ class Prejoin extends Component<Props, State> {
|
||||
this._onJoinConferenceWithoutAudioKeyPress = this._onJoinConferenceWithoutAudioKeyPress.bind(this);
|
||||
this._showDialogKeyPress = this._showDialogKeyPress.bind(this);
|
||||
this._onJoinKeyPress = this._onJoinKeyPress.bind(this);
|
||||
this._getExtraJoinButtons = this._getExtraJoinButtons.bind(this);
|
||||
}
|
||||
_onJoinButtonClick: () => void;
|
||||
|
||||
@@ -277,6 +283,40 @@ class Prejoin extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_getExtraJoinButtons: () => Object;
|
||||
|
||||
/**
|
||||
* Gets the list of extra join buttons.
|
||||
*
|
||||
* @returns {Object} - The list of extra buttons.
|
||||
*/
|
||||
_getExtraJoinButtons() {
|
||||
const { joinConferenceWithoutAudio, t } = this.props;
|
||||
|
||||
const noAudio = {
|
||||
key: 'no-audio',
|
||||
dataTestId: 'prejoin.joinWithoutAudio',
|
||||
icon: IconVolumeOff,
|
||||
label: t('prejoin.joinWithoutAudio'),
|
||||
onButtonClick: joinConferenceWithoutAudio,
|
||||
onKeyPressed: this._onJoinConferenceWithoutAudioKeyPress
|
||||
};
|
||||
|
||||
const byPhone = {
|
||||
key: 'by-phone',
|
||||
dataTestId: 'prejoin.joinByPhone',
|
||||
icon: IconPhone,
|
||||
label: t('prejoin.joinAudioByPhone'),
|
||||
onButtonClick: this._showDialog,
|
||||
onKeyPressed: this._showDialogKeyPress
|
||||
};
|
||||
|
||||
return {
|
||||
noAudio,
|
||||
byPhone
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -290,15 +330,25 @@ class Prejoin extends Component<Props, State> {
|
||||
joinConference,
|
||||
joinConferenceWithoutAudio,
|
||||
name,
|
||||
prejoinConfig,
|
||||
readOnlyName,
|
||||
showCameraPreview,
|
||||
showDialog,
|
||||
t,
|
||||
videoTrack
|
||||
} = this.props;
|
||||
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress,
|
||||
_onOptionsClick, _setName } = this;
|
||||
|
||||
const { _closeDialog, _onDropdownClose, _onJoinButtonClick, _onJoinKeyPress, _showDialogKeyPress,
|
||||
_onJoinConferenceWithoutAudioKeyPress, _onOptionsClick, _setName, _showDialog } = this;
|
||||
const extraJoinButtons = this._getExtraJoinButtons();
|
||||
let extraButtonsToRender = Object.values(extraJoinButtons).filter((val: Object) =>
|
||||
!(prejoinConfig?.hideExtraJoinButtons || []).includes(val.key)
|
||||
);
|
||||
|
||||
if (!hasJoinByPhoneButton) {
|
||||
extraButtonsToRender = extraButtonsToRender.filter((btn: Object) => btn.key !== 'by-phone');
|
||||
}
|
||||
const hasExtraJoinButtons = Boolean(extraButtonsToRender.length);
|
||||
const { showJoinByPhoneButtons, showError } = this.state;
|
||||
|
||||
return (
|
||||
@@ -327,19 +377,12 @@ class Prejoin extends Component<Props, State> {
|
||||
|
||||
<div className = 'prejoin-preview-dropdown-container'>
|
||||
<InlineDialog
|
||||
content = { <div className = 'prejoin-preview-dropdown-btns'>
|
||||
<DropdownButton
|
||||
dataTestId = 'prejoin.joinWithoutAudio'
|
||||
icon = { IconVolumeOff }
|
||||
label = { t('prejoin.joinWithoutAudio') }
|
||||
onButtonClick = { joinConferenceWithoutAudio }
|
||||
onKeyPressed = { _onJoinConferenceWithoutAudioKeyPress } />
|
||||
{hasJoinByPhoneButton && <DropdownButton
|
||||
dataTestId = 'prejoin.joinByPhone'
|
||||
icon = { IconPhone }
|
||||
label = { t('prejoin.joinAudioByPhone') }
|
||||
onButtonClick = { _showDialog }
|
||||
onKeyPressed = { _showDialogKeyPress } />}
|
||||
content = { hasExtraJoinButtons && <div className = 'prejoin-preview-dropdown-btns'>
|
||||
{extraButtonsToRender.map(({ key, ...rest }: Object) => (
|
||||
<DropdownButton
|
||||
key = { key }
|
||||
{ ...rest } />
|
||||
))}
|
||||
</div> }
|
||||
isOpen = { showJoinByPhoneButtons }
|
||||
onClose = { _onDropdownClose }>
|
||||
@@ -348,7 +391,7 @@ class Prejoin extends Component<Props, State> {
|
||||
ariaDropDownLabel = { t('prejoin.joinWithoutAudio') }
|
||||
ariaLabel = { t('prejoin.joinMeeting') }
|
||||
ariaPressed = { showJoinByPhoneButtons }
|
||||
hasOptions = { true }
|
||||
hasOptions = { hasExtraJoinButtons }
|
||||
onClick = { _onJoinButtonClick }
|
||||
onKeyPress = { _onJoinKeyPress }
|
||||
onOptionsClick = { _onOptionsClick }
|
||||
@@ -390,7 +433,8 @@ function mapStateToProps(state): Object {
|
||||
hasJoinByPhoneButton: isJoinByPhoneButtonVisible(state),
|
||||
readOnlyName: isNameReadOnly(state),
|
||||
showCameraPreview: !isVideoMutedByUser(state),
|
||||
videoTrack: getLocalJitsiVideoTrack(state)
|
||||
videoTrack: getLocalJitsiVideoTrack(state),
|
||||
prejoinConfig: state['features/base/config'].prejoinConfig
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ export default class PrejoinApp extends BaseApp<Props> {
|
||||
const { startWithAudioMuted, startWithVideoMuted } = store.getState()['features/base/settings'];
|
||||
|
||||
dispatch(setConfig({
|
||||
prejoinPageEnabled: true,
|
||||
prejoinConfig: {
|
||||
enabled: true
|
||||
},
|
||||
startWithAudioMuted,
|
||||
startWithVideoMuted
|
||||
}));
|
||||
|
||||
@@ -148,7 +148,7 @@ export function isJoinByPhoneDialogVisible(state: Object): boolean {
|
||||
*/
|
||||
export function isPrejoinPageEnabled(state: Object): boolean {
|
||||
return navigator.product !== 'ReactNative'
|
||||
&& state['features/base/config'].prejoinPageEnabled
|
||||
&& state['features/base/config'].prejoinConfig?.enabled
|
||||
&& !state['features/base/settings'].userSelectedSkipPrejoin
|
||||
&& !(state['features/base/config'].enableForcedReload && state['features/prejoin'].skipPrejoinOnReload);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,13 @@ import {
|
||||
*/
|
||||
export const ENDPOINT_REACTION_NAME = 'endpoint-reaction';
|
||||
|
||||
/**
|
||||
* The (name of the) command which transports the state (represented by
|
||||
* {State} for the local state at the time of this writing) of a {MuteReactions}
|
||||
* (instance) between moderator and participants.
|
||||
*/
|
||||
export const MUTE_REACTIONS_COMMAND = 'mute-reactions';
|
||||
|
||||
/**
|
||||
* The prefix for all reaction sound IDs. Also the ID used in config to disable reaction sounds.
|
||||
*/
|
||||
|
||||
@@ -4,9 +4,15 @@ import { batch } from 'react-redux';
|
||||
|
||||
import { createReactionSoundsDisabledEvent, sendAnalytics } from '../analytics';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../base/app';
|
||||
import { getParticipantCount } from '../base/participants';
|
||||
import { CONFERENCE_WILL_JOIN, setStartReactionsMuted } from '../base/conference';
|
||||
import {
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
isLocalParticipantModerator
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { SETTINGS_UPDATED, updateSettings } from '../base/settings';
|
||||
import { SETTINGS_UPDATED } from '../base/settings/actionTypes';
|
||||
import { updateSettings } from '../base/settings/actions';
|
||||
import { playSound, registerSound, unregisterSound } from '../base/sounds';
|
||||
import { getDisabledSounds } from '../base/sounds/functions.any';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, showNotification } from '../notifications';
|
||||
@@ -31,7 +37,8 @@ import {
|
||||
RAISE_HAND_SOUND_ID,
|
||||
REACTIONS,
|
||||
REACTION_SOUND,
|
||||
SOUNDS_THRESHOLDS
|
||||
SOUNDS_THRESHOLDS,
|
||||
MUTE_REACTIONS_COMMAND
|
||||
} from './constants';
|
||||
import {
|
||||
getReactionMessageFromBuffer,
|
||||
@@ -39,8 +46,11 @@ import {
|
||||
getReactionsWithId,
|
||||
sendReactionsWebhook
|
||||
} from './functions.any';
|
||||
import logger from './logger';
|
||||
import { RAISE_HAND_SOUND_FILE } from './sounds';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -95,7 +105,15 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case CONFERENCE_WILL_JOIN: {
|
||||
const { conference } = action;
|
||||
|
||||
conference.addCommandListener(
|
||||
MUTE_REACTIONS_COMMAND, ({ attributes }, id) => {
|
||||
_onMuteReactionsCommand(attributes, id, store);
|
||||
});
|
||||
break;
|
||||
}
|
||||
case FLUSH_REACTION_BUFFER: {
|
||||
const state = getState();
|
||||
const { buffer } = state['features/reactions'];
|
||||
@@ -163,12 +181,26 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
|
||||
case SHOW_SOUNDS_NOTIFICATION: {
|
||||
const state = getState();
|
||||
const isModerator = isLocalParticipantModerator(state);
|
||||
|
||||
const customActions = [ 'notify.reactionSounds' ];
|
||||
const customFunctions = [ () => dispatch(updateSettings({
|
||||
soundsReactions: false
|
||||
})) ];
|
||||
|
||||
if (isModerator) {
|
||||
customActions.push('notify.reactionSoundsForAll');
|
||||
customFunctions.push(() => batch(() => {
|
||||
dispatch(setStartReactionsMuted(true));
|
||||
dispatch(updateSettings({ soundsReactions: false }));
|
||||
}));
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'toolbar.disableReactionSounds',
|
||||
customActionNameKey: 'notify.reactionSounds',
|
||||
customActionHandler: () => dispatch(updateSettings({
|
||||
soundsReactions: false
|
||||
}))
|
||||
customActionNameKey: customActions,
|
||||
customActionHandler: customFunctions
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
break;
|
||||
}
|
||||
@@ -176,3 +208,51 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Notifies this instance about a "Mute Reaction Sounds" command received by the Jitsi
|
||||
* conference.
|
||||
*
|
||||
* @param {Object} attributes - The attributes carried by the command.
|
||||
* @param {string} id - The identifier of the participant who issuing the
|
||||
* command. A notable idiosyncrasy to be mindful of here is that the command
|
||||
* may be issued by the local participant.
|
||||
* @param {Object} store - The redux store. Used to calculate and dispatch
|
||||
* updates.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _onMuteReactionsCommand(attributes = {}, id, store) {
|
||||
const state = store.getState();
|
||||
|
||||
// We require to know who issued the command because (1) only a
|
||||
// moderator is allowed to send commands and (2) a command MUST be
|
||||
// issued by a defined commander.
|
||||
if (typeof id === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const participantSendingCommand = getParticipantById(state, id);
|
||||
|
||||
// The Command(s) API will send us our own commands and we don't want
|
||||
// to act upon them.
|
||||
if (participantSendingCommand.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (participantSendingCommand.role !== 'moderator') {
|
||||
logger.warn('Received mute-reactions command not from moderator');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = Boolean(state['features/base/conference'].startReactionsMuted);
|
||||
const newState = attributes.startReactionsMuted === 'true';
|
||||
|
||||
if (oldState !== newState) {
|
||||
batch(() => {
|
||||
store.dispatch(setStartReactionsMuted(newState));
|
||||
store.dispatch(updateSettings({ soundsReactions: !newState }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
45
react/features/reactions/subscriber.js
Normal file
45
react/features/reactions/subscriber.js
Normal file
@@ -0,0 +1,45 @@
|
||||
// @flow
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { isLocalParticipantModerator } from '../base/participants';
|
||||
import { StateListenerRegistry } from '../base/redux';
|
||||
|
||||
import { MUTE_REACTIONS_COMMAND } from './constants';
|
||||
|
||||
/**
|
||||
* Subscribes to changes to the Mute Reaction Sounds setting for the local participant to
|
||||
* notify remote participants of current user interface status.
|
||||
* Changing newSelectedValue param to off, when feature is turned of so we can
|
||||
* notify all listeners.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'].startReactionsMuted,
|
||||
/* listener */ (newSelectedValue, store) => _sendMuteReactionsCommand(newSelectedValue || false, store));
|
||||
|
||||
|
||||
/**
|
||||
* Sends the mute-reactions command, when a local property change occurs.
|
||||
*
|
||||
* @param {*} newSelectedValue - The changed selected value from the selector.
|
||||
* @param {Object} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _sendMuteReactionsCommand(newSelectedValue, store) {
|
||||
const state = store.getState();
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (!conference) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only a moderator is allowed to send commands.
|
||||
if (!isLocalParticipantModerator(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
conference.sendCommand(
|
||||
MUTE_REACTIONS_COMMAND,
|
||||
{ attributes: { startReactionsMuted: Boolean(newSelectedValue) } }
|
||||
);
|
||||
}
|
||||
@@ -170,7 +170,6 @@ export function showStartedRecordingNotification(
|
||||
const initiatorId = getResourceId(initiator);
|
||||
const participantName = getParticipantDisplayName(state, initiatorId);
|
||||
let dialogProps = {
|
||||
customActionNameKey: undefined,
|
||||
descriptionKey: participantName ? 'liveStreaming.onBy' : 'liveStreaming.on',
|
||||
descriptionArguments: { name: participantName },
|
||||
isDismissAllowed: true,
|
||||
@@ -199,15 +198,16 @@ export function showStartedRecordingNotification(
|
||||
const tenant = getVpaasTenant(state);
|
||||
|
||||
try {
|
||||
const link = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
|
||||
const response = await getRecordingLink(recordingSharingUrl, sessionId, region, tenant);
|
||||
const { url: link, urlExpirationTimeMillis: ttl } = response;
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
APP.API.notifyRecordingLinkAvailable(link);
|
||||
APP.API.notifyRecordingLinkAvailable(link, ttl);
|
||||
}
|
||||
|
||||
// add the option to copy recording link
|
||||
dialogProps.customActionNameKey = 'recording.copyLink';
|
||||
dialogProps.customActionHandler = () => copyText(link);
|
||||
dialogProps.customActionNameKey = [ 'recording.copyLink' ];
|
||||
dialogProps.customActionHandler = [ () => copyText(link) ];
|
||||
dialogProps.titleKey = 'recording.on';
|
||||
dialogProps.descriptionKey = 'recording.linkGenerated';
|
||||
dialogProps.isDismissAllowed = false;
|
||||
|
||||
@@ -57,6 +57,16 @@ type Props = {
|
||||
*/
|
||||
_rToken: string,
|
||||
|
||||
/**
|
||||
* Whether or not the local participant is screensharing.
|
||||
*/
|
||||
_screensharing: boolean,
|
||||
|
||||
/**
|
||||
* Whether or not the screenshot capture feature is enabled.
|
||||
*/
|
||||
_screenshotCaptureEnabled: boolean,
|
||||
|
||||
/**
|
||||
* Access token's expiration date as UNIX timestamp.
|
||||
*/
|
||||
@@ -128,6 +138,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
||||
this._onSelectedRecordingServiceChanged
|
||||
= this._onSelectedRecordingServiceChanged.bind(this);
|
||||
this._onSharingSettingChanged = this._onSharingSettingChanged.bind(this);
|
||||
this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this);
|
||||
|
||||
let selectedRecordingService;
|
||||
|
||||
@@ -315,6 +326,7 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
||||
createRecordingDialogEvent('start', 'confirm.button', attributes)
|
||||
);
|
||||
|
||||
this._toggleScreenshotCapture();
|
||||
_conference.startRecording({
|
||||
mode: JitsiRecordingConstants.mode.FILE,
|
||||
appData
|
||||
@@ -327,6 +339,11 @@ class AbstractStartRecordingDialog extends Component<Props, State> {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be overwritten by web component.
|
||||
*/
|
||||
_toggleScreenshotCapture:() => void;
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog content.
|
||||
*
|
||||
|
||||
@@ -25,6 +25,11 @@ export type Props = {
|
||||
*/
|
||||
_fileRecordingSession: Object,
|
||||
|
||||
/**
|
||||
* The redux dispatch function.
|
||||
*/
|
||||
dispatch: Function,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
@@ -49,6 +54,7 @@ export default class AbstractStopRecordingDialog<P: Props>
|
||||
|
||||
// Bind event handler so it is only bound once for every instance.
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._toggleScreenshotCapture = this._toggleScreenshotCapture.bind(this);
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
@@ -66,10 +72,16 @@ export default class AbstractStopRecordingDialog<P: Props>
|
||||
|
||||
if (_fileRecordingSession) {
|
||||
this.props._conference.stopRecording(_fileRecordingSession.id);
|
||||
this._toggleScreenshotCapture();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be overwritten by web component.
|
||||
*/
|
||||
_toggleScreenshotCapture: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,10 @@ import React from 'react';
|
||||
import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { isScreenVideoShared } from '../../../../screen-share';
|
||||
import { toggleScreenshotCaptureSummary } from '../../../../screenshot-capture';
|
||||
import AbstractStartRecordingDialog, {
|
||||
mapStateToProps
|
||||
mapStateToProps as abstractMapStateToProps
|
||||
} from '../AbstractStartRecordingDialog';
|
||||
import StartRecordingDialogContent from '../StartRecordingDialogContent';
|
||||
|
||||
@@ -64,10 +66,37 @@ class StartRecordingDialog extends AbstractStartRecordingDialog {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles screenshot capture feature.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_toggleScreenshotCapture() {
|
||||
const { dispatch, _screensharing, _screenshotCaptureEnabled } = this.props;
|
||||
|
||||
if (_screenshotCaptureEnabled && _screensharing) {
|
||||
dispatch(toggleScreenshotCaptureSummary(true));
|
||||
}
|
||||
}
|
||||
|
||||
_areIntegrationsEnabled: () => boolean;
|
||||
_onSubmit: () => boolean;
|
||||
_onSelectedRecordingServiceChanged: (string) => void;
|
||||
_onSharingSettingChanged: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps redux state to component props.
|
||||
*
|
||||
* @param {Object} state - Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
...abstractMapStateToProps(state),
|
||||
_screensharing: isScreenVideoShared(state),
|
||||
_screenshotCaptureEnabled: state['features/base/config'].enableScreenshotCapture
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(StartRecordingDialog));
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import { Dialog } from '../../../../base/dialog';
|
||||
import { translate } from '../../../../base/i18n';
|
||||
import { connect } from '../../../../base/redux';
|
||||
import { toggleScreenshotCaptureSummary } from '../../../../screenshot-capture';
|
||||
import AbstractStopRecordingDialog, {
|
||||
type Props,
|
||||
_mapStateToProps
|
||||
@@ -38,6 +39,15 @@ class StopRecordingDialog extends AbstractStopRecordingDialog<Props> {
|
||||
}
|
||||
|
||||
_onSubmit: () => boolean;
|
||||
|
||||
/**
|
||||
* Toggles screenshot capture.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_toggleScreenshotCapture() {
|
||||
this.props.dispatch(toggleScreenshotCaptureSummary(false));
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(StopRecordingDialog));
|
||||
|
||||
@@ -65,7 +65,7 @@ export async function getRecordingLink(url: string, recordingSessionId: string,
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
return res.ok ? json.url : Promise.reject(json);
|
||||
return res.ok ? json : Promise.reject(json);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import { Switch, Text, View } from 'react-native';
|
||||
|
||||
import { translate } from '../../base/i18n';
|
||||
import { connect } from '../../base/redux';
|
||||
import { LOCKED_REMOTELY } from '../constants';
|
||||
|
||||
import styles, {
|
||||
DISABLED_THUMB_COLOR,
|
||||
ENABLED_THUMB_COLOR, ENABLED_TRACK_COLOR
|
||||
} from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link RoomLockSwitch}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Checks if the room is locked based on defined room lock constants.
|
||||
*/
|
||||
locked: string,
|
||||
|
||||
/**
|
||||
* Whether the switch is disabled.
|
||||
*/
|
||||
disabled: boolean,
|
||||
|
||||
/**
|
||||
* Callback to be invoked when the user toggles room lock.
|
||||
*/
|
||||
onToggleRoomLock: Function,
|
||||
|
||||
/**
|
||||
* Control for room lock.
|
||||
*/
|
||||
toggleRoomLock: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Component meant to Add/Remove meeting password.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function RoomLockSwitch(
|
||||
{
|
||||
locked,
|
||||
disabled,
|
||||
onToggleRoomLock,
|
||||
toggleRoomLock,
|
||||
t
|
||||
}: Props) {
|
||||
|
||||
return (
|
||||
<View style = { styles.roomLockSwitchContainer }>
|
||||
<Text>
|
||||
{
|
||||
locked === LOCKED_REMOTELY
|
||||
&& t('passwordSetRemotely')
|
||||
}
|
||||
</Text>
|
||||
<Switch
|
||||
disabled = { disabled }
|
||||
onValueChange = { onToggleRoomLock }
|
||||
thumbColor = {
|
||||
toggleRoomLock
|
||||
? ENABLED_THUMB_COLOR
|
||||
: DISABLED_THUMB_COLOR
|
||||
}
|
||||
trackColor = {{ true: ENABLED_TRACK_COLOR }}
|
||||
value = { toggleRoomLock } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default translate(connect()(RoomLockSwitch));
|
||||
@@ -1,16 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import { ColorPalette } from '../../base/styles';
|
||||
|
||||
export const ENABLED_THUMB_COLOR = ColorPalette.blueHighlight;
|
||||
export const ENABLED_TRACK_COLOR = ColorPalette.blue;
|
||||
export const DISABLED_THUMB_COLOR = ColorPalette.darkGrey;
|
||||
|
||||
export default {
|
||||
roomLockSwitchContainer: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: 16
|
||||
}
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import './createImageBitmap';
|
||||
|
||||
import { createScreensharingCaptureTakenEvent, sendAnalytics } from '../analytics';
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { getRemoteParticipants } from '../base/participants';
|
||||
import { extractFqnFromPath } from '../dynamic-branding';
|
||||
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
POLL_INTERVAL,
|
||||
SET_INTERVAL
|
||||
} from './constants';
|
||||
import { getParticipantJid } from './functions';
|
||||
import { processScreenshot } from './processScreenshot';
|
||||
import { timerWorkerScript } from './worker';
|
||||
|
||||
@@ -140,6 +142,12 @@ export default class ScreenshotCaptureSummary {
|
||||
const timestamp = Date.now();
|
||||
const { jwt } = this._state['features/base/jwt'];
|
||||
const meetingFqn = extractFqnFromPath();
|
||||
const remoteParticipants = getRemoteParticipants(this._state);
|
||||
const participants = [];
|
||||
|
||||
remoteParticipants.forEach(p => participants.push(
|
||||
getParticipantJid(this._state, p.id)
|
||||
));
|
||||
|
||||
this._storedImageData = imageData;
|
||||
|
||||
@@ -148,7 +156,8 @@ export default class ScreenshotCaptureSummary {
|
||||
jwt,
|
||||
sessionId,
|
||||
timestamp,
|
||||
meetingFqn
|
||||
meetingFqn,
|
||||
participants
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export const PERCENTAGE_LOWER_BOUND = 5;
|
||||
/**
|
||||
* Number of milliseconds that represent how often screenshots should be taken.
|
||||
*/
|
||||
export const POLL_INTERVAL = 2000;
|
||||
export const POLL_INTERVAL = 4000;
|
||||
|
||||
/**
|
||||
* SET_INTERVAL constant is used to set interval and it is set in
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import { getCurrentConference } from '../base/conference';
|
||||
import { toState } from '../base/redux';
|
||||
|
||||
import ScreenshotCaptureSummary from './ScreenshotCaptureSummary';
|
||||
@@ -18,3 +19,26 @@ export function createScreenshotCaptureSummary(stateful: Object | Function) {
|
||||
|
||||
return new ScreenshotCaptureSummary(toState(stateful));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a participant's connection JID given its ID.
|
||||
*
|
||||
* @param {Object} state - The redux store state.
|
||||
* @param {string} participantId - ID of the given participant.
|
||||
* @returns {string|undefined} - The participant connection JID if found.
|
||||
*/
|
||||
export function getParticipantJid(state: Object, participantId: string) {
|
||||
const conference = getCurrentConference(state);
|
||||
|
||||
if (!conference) {
|
||||
return;
|
||||
}
|
||||
|
||||
const participant = conference.getParticipantById(participantId);
|
||||
|
||||
if (!participant) {
|
||||
return;
|
||||
}
|
||||
|
||||
return participant.getJid();
|
||||
}
|
||||
|
||||
@@ -9,15 +9,11 @@ import {
|
||||
MEETING_PASSWORD_ENABLED,
|
||||
SECURITY_OPTIONS_ENABLED
|
||||
} from '../../../base/flags';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { IconSecurityOff, IconSecurityOn } from '../../../base/icons';
|
||||
import { isLocalParticipantModerator } from '../../../base/participants';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components';
|
||||
import { toggleSecurityDialog } from '../../actions';
|
||||
|
||||
|
||||
type Props = AbstractButtonProps & {
|
||||
export type Props = AbstractButtonProps & {
|
||||
|
||||
/**
|
||||
* Whether the shared document is being edited or not.
|
||||
@@ -32,9 +28,10 @@ type Props = AbstractButtonProps & {
|
||||
|
||||
|
||||
/**
|
||||
* Implements an {@link AbstractButton} to open the security dialog.
|
||||
* Implements an {@link AbstractButton} to open the security dialog/screen.
|
||||
*/
|
||||
class SecurityDialogButton extends AbstractButton<Props, *> {
|
||||
export default class AbstractSecurityDialogButton<P: Props, S:*>
|
||||
extends AbstractButton<P, S> {
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.security';
|
||||
icon = IconSecurityOff;
|
||||
label = 'toolbar.security';
|
||||
@@ -42,13 +39,24 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
|
||||
tooltip = 'toolbar.security';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens / closes the appropriate dialog.
|
||||
* Helper function to be implemented by subclasses, which should be used
|
||||
* to handle the security button being clicked / pressed.
|
||||
*
|
||||
* @protected
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClickSecurityButton() {
|
||||
// To be implemented by subclass.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_handleClick() {
|
||||
const { _locked, dispatch, handleClick } = this.props;
|
||||
const { _locked, handleClick } = this.props;
|
||||
|
||||
if (handleClick) {
|
||||
handleClick();
|
||||
@@ -57,7 +65,7 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
|
||||
}
|
||||
|
||||
sendAnalytics(createToolbarEvent('toggle.security', { enable: !_locked }));
|
||||
dispatch(toggleSecurityDialog());
|
||||
this._handleClickSecurityButton();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,7 +85,7 @@ class SecurityDialogButton extends AbstractButton<Props, *> {
|
||||
* @param {Object} state - The redux store/state.
|
||||
* @returns {Props}
|
||||
*/
|
||||
function mapStateToProps(state: Object) {
|
||||
export function _mapStateToProps(state: Object) {
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { hideLobbyButton } = state['features/base/config'];
|
||||
const { locked } = state['features/base/conference'];
|
||||
@@ -93,5 +101,3 @@ function mapStateToProps(state: Object) {
|
||||
visible: enabledFlag || (enabledLobbyModeFlag || enabledMeetingPassFlag)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(mapStateToProps)(SecurityDialogButton));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user