Compare commits

...

54 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
ef9913c1c7 fix(rn) fix broken mobile build
Importing the entire feature triggers web UI imports.
2021-12-11 15:22:03 +01:00
Дамян Минков
ce195d82d6 chore(deps) lib-jitsi-meet@latest
* fix: Fixes set effect and correct presence state.

8a95fda259...d630bc32a1
2021-12-10 15:39:51 -06:00
Jaya Allamsetty
229e65133c feat(media) Disable desktopshare when the video sender limit is reached. 2021-12-10 14:58:55 -05:00
Jaya Allamsetty
11eb6689dc chore(deps) lib-jitsi-meet@latest
* fix(presence): Do not sent presence if there is no active media session.
* fix: server-side callstats is never enabled (#1823)
* fix(JitsiConference) squelch bogus p2p session end error

8aa35dae40...8a95fda259
2021-12-10 12:23:43 -05:00
Calinteodor
bf3cc65f4c feat(security) created SecurityOptions React Navigation screen (#10509)
* feat(security) Security Options screen
2021-12-10 18:23:27 +02:00
Hristo Terezov
75e6dd389f feat(index.html): Add fonts.html SSI. 2021-12-10 10:20:54 -06:00
Rokixy
076113940b typo fix
dekstopAudioStream --> desktopAudioStream
2021-12-10 09:03:18 -06:00
Saúl Ibarra Corretgé
e6accd40e1 fix(breakout-rooms) ensure we use the same media types when joining
Only audio and video are considered. Screen sharing won't be preserved.
2021-12-10 15:51:06 +01:00
Saúl Ibarra Corretgé
7f0cfed981 fix(rn,breakout-rooms) create desired local tracks when joining rooms 2021-12-10 15:50:11 +01:00
Saúl Ibarra Corretgé
9279586e8c fix(tracks) fix disposing of local tracks
Don't just clear the storage for them, local tracks must be disposed, in order
for RN capturer to be freed for example.
2021-12-10 15:49:44 +01:00
Robert Pintilii
f8908c143e fix(disableSelfView) Fix issue with remote participant video (#10582)
Fix issue where last participant is shown as inactive when the self view is hidden
2021-12-10 15:16:02 +02:00
Saúl Ibarra Corretgé
c705dbaa2f fix(rn,breakout-rooms) fix not rendering display names 2021-12-10 13:37:59 +01:00
Saúl Ibarra Corretgé
1fa43ca4e7 chore(rn,versions) bump app and sdk versions 2021-12-10 10:05:34 +01:00
Jaya Allamsetty
9528cbad0e fix(notifications) Adjust the timeout for unmute blocked notifications. 2021-12-09 14:03:16 -06:00
Jaya Allamsetty
a23d57b5ff fix(shortcuts) Ignore keyboard shortcuts when the button are disabled. 2021-12-09 14:03:16 -06:00
Дамян Минков
ae5c364333 fix(breakout-rooms) cleanup local tracks when a conference is left. 2021-12-09 12:13:59 -06:00
Jaya Allamsetty
700c7c523d feat(tracks) Clean up the track if a source addition is rejected. (#10562)
* feat(tracks) Clean up the track if a source addition is rejected.
When jicofo rejects a source-add because of sendder limits, dispose and remove the local track from the conference.

* chore(deps) update LJM to latest.
2021-12-09 07:48:55 -06:00
Calin Chitu
5dbf4845fb feat(chat) keyboard covering input on android fix 2021-12-09 14:51:27 +02:00
Payetus
661a3d34be fix(lang) update Catalan translation
Co-authored-by: Payetus <payet91@gmail.xom>
2021-12-09 13:27:21 +01:00
Robert Pintilii
0e9b40c410 fix(notifications) Create web middleware (#10568)
Fixes broken native
2021-12-09 13:31:43 +02:00
Avram Tudor
51b827ebb0 fix(browser-compatibility) hide launch in web for unsupported mobile browsers (#10569) 2021-12-09 13:16:08 +02:00
Avram Tudor
16a2c729e0 chore(deps) lib-jitsi-meet@latest (#10566)
* fix(browser-capabilities) expose supported mobile browsers

e9e37099a1...161da84ac0
2021-12-09 12:36:11 +02:00
Saúl Ibarra Corretgé
ed5bb871f4 fix(android) fix NoClassDefFoundError for Landroid/graphics/ColorSpace
Fixes: https://github.com/jitsi/jitsi-meet/issues/10182
2021-12-09 10:51:42 +01:00
robertpin
d96ecc5b65 fix(screenshot-capture) Use feature on web only
Fixes broken native
2021-12-09 10:02:23 +01:00
robertpin
41f11e5adb feat(self-view) Added ability to hide self view
Added config option disableSelfView. This disables it on web and native

Added button on local video menu and toggle in settings on web to change the setting
2021-12-09 08:45:16 +01:00
Saúl Ibarra Corretgé
c48aa44af3 fix(breakout-rooms) cleanup remote tracks when a conference is left 2021-12-08 16:52:38 -06:00
Christoph Settgast
085b07efcd fix(rn) join conference if started by moderator
Issue was that APP is not defined at this place on mobile,
thus this raising ReferenceError which caused the waitForOwner to break

Closes: #10211
2021-12-08 16:52:23 -06:00
Saúl Ibarra Corretgé
60dcac96a6 fix(breakout-rooms) fix non-functional context menu
Close the menu after sending a participant to a breakout room, that will detach
the context menu from the current participant and it will work properly when
they join back.
2021-12-08 15:11:18 -06:00
robertpin
2e22eb5169 fix(overflow-drawer) Only use overflow drawer on mobile 2021-12-08 19:55:16 +01:00
Дамян Минков
e2beb2f3b1 chore(deps) lib-jitsi-meet@latest
* fix(tpc) disable ulpfec on chrome 96 (jitsi/lib-jitsi-meet#1816)
* fix: Fixes call to _setTrackMuteStatus.
* Refactor presence update (jitsi/lib-jitsi-meet#1814)

9e5d83f4ac...e9e37099a1
2021-12-08 12:41:47 -05:00
tmoldovan8x8
664f23a395 chore(rn) updates react-native-webrtc 2021-12-08 18:00:04 +02:00
Robert Pintilii
dc20b2fafe fix(screenshot-capture) Update screenshot capture feature (#10443)
* fix(screenshot-capture) Update screenshot capture feature

Add participants jid list to request

Enable screenshot capture only when recording is also on

Updated interval
2021-12-08 16:15:59 +02:00
Saúl Ibarra Corretgé
00ae2dc6a9 fix(virtual-backgrounds) make error message translatable 2021-12-08 15:12:32 +01:00
Saúl Ibarra Corretgé
a182f53cdc fix(virtual-backgrounds) fix error if we failed to load the model
If another try is made, show an error notification instead of failing with an
exception.
2021-12-08 15:12:32 +01:00
Horatiu Muresan
9b3f254c6b feat(end-meet-for-all) Trigger notifyReadyToClose event on end meetin… (#10549) 2021-12-08 16:11:31 +02:00
Vlad Piersec
0188086dde fix(participants-list): Avoid ui moving on input focus 2021-12-08 15:32:51 +02:00
JH
340698a546 fix(lang) update sv translation 2021-12-08 14:05:45 +01:00
SiderealArt
0920bda7b8 fix(lang) update Traditional Chinese (Taiwan) translation 2021-12-08 09:14:11 +01:00
Horatiu Muresan
10e5b2f572 feat(prejoin) Add possibility to hide extra join options buttons (#10434) 2021-12-08 09:53:19 +02:00
José Luís Andrade
81e9d01921 fix(lang) update Portuguese translation 2021-12-08 08:33:21 +01:00
Gabriel Borlea
0d5beb0c4e fix(facial-expressions) load worker as a blob 2021-12-08 08:27:17 +01:00
Ali Alhaidary
b890f34a53 fix(lang) update Arabic translation 2021-12-08 08:25:38 +01:00
Jaya Allamsetty
b19e4d76b5 fix(media) dispatch the unmute blocked action irrepective of the muted state.
This fixes an issue where the user muted by focus is able to unmute themselves even when the sender limit has been reached.
2021-12-07 17:13:29 -06:00
Christoph Settgast
65589937ea fix(breakout,av-moderation): support non-ascii tenant names
Same applies to tenant names as it applies to room names, as the
tenants/subdomains are prefixed to the room names in prosody.

Signed-off-by: Christoph Settgast <csett86@web.de>
2021-12-07 18:31:36 +01:00
Christoph Settgast
a6bc51cff1 fix(breakout,av-moderation): support non-ascii room names
rooms are created in prosody in their urlencoded form, eg täst
becomes t%c3%a4st@conference.jitsi.example.org

As local params = formdecode(query) contains an urldecode, we
need to reencode the room name so that the room can be found in
prosody.

Closes: #10525
Signed-off-by: Christoph Settgast <csett86@web.de>
2021-12-07 18:31:36 +01:00
Saúl Ibarra Corretgé
ae3fb20d65 fix(breakout-rooms) fix not waiting to leave the room
We need to wait for it before we start joining.
2021-12-07 16:40:13 +01:00
Vlad Piersec
bfc0567e32 fix(rn,chat): Fix chat and polls title 2021-12-07 11:13:25 +01:00
Mihaela Dumitru
0fb29a0c7a feat(external-api): enhance recordingLinkAvailable to provide ttl info 2021-12-07 10:57:04 +01:00
gpatel-fr
822850d888 fix(lang) update french translation 2021-12-04 13:18:24 +01:00
Christoph Settgast
64e6c5f461 fix(lang) update German translation 2021-12-04 07:20:26 +01:00
Andrei Oltean
a077043f1b feat: (moderate-reaction-sounds) enable moderator to mute reaction sounds 2021-12-03 18:36:57 +02:00
tmoldovan8x8
646fdef6bb fix(screen-sharing, picture-in-picture) re-enables PIP after stopping screen-share 2021-12-03 15:44:19 +02:00
Saúl Ibarra Corretgé
da0cb2b837 feat(breakout-rooms) add notification when joining rooms 2021-12-03 10:48:31 +01:00
Saúl Ibarra Corretgé
4401ea8818 fix(rn,navbar) fix invalid boolean check
`startTimestamp` is not a boolean, so make sure we return one.
2021-12-03 08:38:58 +01:00
129 changed files with 2229 additions and 695 deletions

View File

@@ -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 *;
}

View File

@@ -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

View File

@@ -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')

View File

@@ -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(),

View File

@@ -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'

View File

@@ -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 => {

View File

@@ -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.)

View File

@@ -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
View File

View 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>

View File

@@ -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
#

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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": "شعار السياسة"

View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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": "啟用大廳"
}
}

View File

@@ -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",

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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';

View File

@@ -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));
}

View File

@@ -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}.

View File

@@ -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.
*

View File

@@ -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'
];

View File

@@ -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: {

View File

@@ -41,6 +41,7 @@ function _toggleScreenSharing(enabled, store) {
}
} else {
dispatch(destroyLocalDesktopTrackIfExists());
setPictureInPictureDisabled(false);
}
}

View File

@@ -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);

View File

@@ -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',

View File

@@ -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

View File

@@ -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) {

View File

@@ -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));
}
});

View File

@@ -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());
}
/**

View File

@@ -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;
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -14,7 +14,8 @@
* serverURL: string,
* startAudioOnly: boolean,
* startWithAudioMuted: boolean,
* startWithVideoMuted: boolean
* startWithVideoMuted: boolean,
* startWithReactionsMuted: boolean
* }
* }
*/

View File

@@ -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
* }
* }}
*/

View File

@@ -21,6 +21,7 @@ const DEFAULT_STATE = {
disableCallIntegration: undefined,
disableCrashReporting: undefined,
disableP2P: undefined,
disableSelfView: false,
displayName: undefined,
email: undefined,
localFlipX: true,

View File

@@ -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.

View File

@@ -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.
*

View File

@@ -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',

View File

@@ -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));
}
};
}

View File

@@ -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>

View File

@@ -53,6 +53,7 @@ class Chat extends AbstractChat<Props> {
return (
<JitsiScreen
hasBottomTextInput = { true }
hasTabNavigator = { true }
style = { styles.chatContainer }>
<MessageContainer messages = { _messages } />

View File

@@ -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 }

View File

@@ -222,6 +222,13 @@ export const speakerStatsScreenOptions = {
...presentationScreenOptions
};
/**
* Screen options for security options modal.
*/
export const securityScreenOptions = {
...presentationScreenOptions
};
/**
* Screen options for shared document.
*/

View File

@@ -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,

View File

@@ -21,6 +21,7 @@ export const screen = {
polls: 'Polls'
}
},
security: 'Security Options',
speakerStats: 'Speaker Stats',
participants: 'Participants',
invite: 'Invite',

View File

@@ -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

View File

@@ -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());
};
}

View File

@@ -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;
}
}
};

View File

@@ -29,7 +29,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
}
if (action.type === CONFERENCE_JOINED) {
dispatch(loadWorker());
dispatch(startFacialRecognition());
return next(action);
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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),

View File

@@ -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),

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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));
}
});
/**

View File

@@ -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>
)

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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

View File

@@ -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 [];

View File

@@ -0,0 +1 @@
import './middleware.any';

View 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);
});

View File

@@ -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: {

View File

@@ -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>
);
};

View File

@@ -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));
};

View File

@@ -59,7 +59,7 @@ const styles = theme => {
display: 'flex',
height: 40,
fontSize: 15,
lineHeight: 24,
lineHeight: '24px',
padding: '0 16px',
backgroundColor: theme.palette.field02,

View File

@@ -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
};
}

View File

@@ -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
}));

View File

@@ -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);
}

View File

@@ -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.
*/

View File

@@ -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 }));
});
}
}

View 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) } }
);
}

View File

@@ -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;

View File

@@ -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.
*

View File

@@ -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;
}
/**

View File

@@ -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));

View File

@@ -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));

View File

@@ -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);
}
/**

View File

@@ -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));

View File

@@ -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
}
};

View File

@@ -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
});
}

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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