Compare commits

..

65 Commits

Author SHA1 Message Date
kerem
1ac86cf979 feat(rn) add toggleCamera action 2023-08-02 15:05:56 +02:00
Ricardo Corrie
1303040e17 (react-native-sdk) Adds CONFERENCE_FOCUSED and CONFERENCE_BLURRED events to the external and RN api (#13657)
* (react-native-sdk) Adds `CONFERENCE_FOCUSED` and `CONFERENCE_BLURRED` events to the external and RN API
2023-08-02 12:24:52 +03:00
Calinteodor
5e2f745407 sdk(resources): update-mobile-rnsdk-version script (#13655)
* sdk(resources): update-mobile-rnsdk-version script
2023-08-01 16:11:42 +03:00
Ricardo Corrie
dc6861d5a4 feat(rnsdk) makes flags and eventListeners props optional 2023-08-01 15:07:57 +02:00
Calin-Teodor
7e5833bed2 sdk(react-native-sdk): revert version to 0.0.0 2023-08-01 13:40:17 +03:00
Calin-Teodor
55af587836 sdk(react-native-sdk): removed extra empty space 2023-08-01 13:40:02 +03:00
Fabrizio Corpora
f59174d6ce Rn jitsi sdk component unmount close (#13636)
* sdk(react-native-sdk): rnsdk JitsiMeeting component unmount close
2023-08-01 13:33:46 +03:00
Saúl Ibarra Corretgé
c83c4488bf feat(rnsdk) share bootsrap code with app 2023-08-01 11:40:44 +02:00
Calin-Teodor
38280c358f sdk(react-native-sdk): alpha sort actionTypes 2023-08-01 11:16:40 +03:00
Ricardo Corrie
f7c1500bc0 (react-native-sdk): Adds onEnterPictureInPicture handler prop to JitsiMeeting component 2023-08-01 00:10:33 +03:00
Calinteodor
ae90e96a3e sdk(react-native-sdk): rnsdk updates fixes (#13627)
* sdk(react-native-sdk): created IEventListeners interface, alpha sorted props and other updates
2023-07-31 22:01:06 +03:00
Mihaela Dumitru
c05a49567d fix(local-storage) allow disabling window.localStorage (#13592) 2023-07-31 17:24:02 +03:00
Gabriel Borlea
96fe34e6c7 feat(android): add retrieveParticipantsInfo builder in helper (#13642) 2023-07-31 15:36:59 +03:00
damencho
c9f6de1371 fix: Fixes redirect to token auth URL from welcome page.
The redirect was being canceled by the other redirect to the room when clicking start meeting.
2023-07-28 14:04:37 -05:00
damencho
60a995d654 fix(docs): Fixes a typo in visitors example config. 2023-07-28 14:04:37 -05:00
Fabrizio Corpora
1b053b0ff2 Rn jitsi sdk user info meeting options (#13630)
* add meetingOptions.userInfo
2023-07-28 18:52:13 +03:00
Hristo Terezov
6293b586f1 fix(connection): Dispatch _connectInternal.
Instead of just calling it.
2023-07-28 09:21:49 -05:00
damencho
f615b0133c fix: Fixes redirect to auth url, set_room can be undefined. 2023-07-27 21:15:26 -05:00
damencho
06f509b475 feat: Adds auto redirect config for token auth URL.
Option that enables auto redirect, so once a user has authenticated we will use that authentication on next join.
2023-07-27 17:18:39 -05:00
damencho
47d261e45c fix(visitors): Fixes visitors to work with anonymous domain. 2023-07-27 17:18:39 -05:00
Jaya Allamsetty
961dbd81c7 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1669.0.0+da04ed9d...v1670.0.0+10ebc843
2023-07-27 17:57:08 -04:00
Christoph Settgast
190755126b lang: update German translation 2023-07-27 14:29:46 -05:00
José Luís Andrade
f21ebf63ba lang: Update Portuguese translation (#13625) 2023-07-27 14:29:36 -05:00
Calinteodor
beb30c5224 feat(mobile/external-api): fixed screenshare event toggle tracking (#13620)
* feat(mobile/external-api): fixed screenshare event toggle tracking
2023-07-27 18:10:55 +03:00
Gabriel Borlea
2253393ac8 fix(welcome, rn): key warning for unsafe room warning (#13626) 2023-07-27 17:19:21 +03:00
damencho
965a6981db fix: Fix wait for owner log. 2023-07-27 07:16:12 -05:00
Ricardo Corrie
214c53220f fix(react-native-sdk): Declare JitsiMeeting flags prop as an object instead of an array 2023-07-27 15:05:00 +03:00
Hristo Terezov
c5af0ba621 chore(LJM): Update 2023-07-26 15:12:38 -05:00
Hristo Terezov
22eff7fa21 fix(BaseDialog):Update onSubmit for keyDown. 2023-07-26 15:12:38 -05:00
Hristo Terezov
0f57928585 fix(breakoutRooms): Fix rename.
Converts the JS files into TS.
Converts the propmt components into functional components.
Adds missing icon.
Other little fixes.
2023-07-26 15:12:38 -05:00
Werner Fleischer
7fee0297e8 feat(breakout-rooms): renaming 2023-07-26 15:12:38 -05:00
damencho
df81e0fe53 feat: Adds wait for host prosody module and handling it.
While the host arrives all other participants are waiting in lobby and once the host arrives lobby is destroyed and the participants join the call.
Adds reading some other fields in jwt to extract email.
Introduces tokenLogoutUrl to be used for logout.
2023-07-26 12:19:02 -05:00
damencho
c58de5c690 feat: Adds option to pass extra params (reason) when creating lobby. 2023-07-26 12:19:02 -05:00
damencho
cf1fa52426 feat: Uses some extra fields to discover name and avatar.
Some jwt token may have the name and avatar in different fields, other than the jitsi context one.
2023-07-26 12:19:02 -05:00
damencho
e58cad0464 feat: Adds option for a url to list of pub keys for jwt to pre-fetch.
The URL is a link to a json file having a mapping kid -> public key.
The mapping can contain also certificates, which will be used to get the public key.
The list is being updated before the cache-control max-age header value is reached, if such a header is returned from the server.
2023-07-26 12:19:02 -05:00
Calin-Teodor
2bf982452e feat(conference): hide lonely meeting exp in tile view 2023-07-25 16:19:06 +03:00
Calin-Teodor
891278d0b3 feat(display-name): fixed display name label 2023-07-25 16:18:48 +03:00
Saúl Ibarra Corretgé
056226b8af fix(ios) disable CallKit when running in a simulator
It doesn't work and causes weird failures. We used to disable it in the
iOS app, but it's better to move it to JS since any SDK will benefit
from it.
2023-07-25 11:46:08 +03:00
Saúl Ibarra Corretgé
f8dae86798 fix(ios) remove unnecessary fallback for SDK version detection 2023-07-25 11:46:08 +03:00
Andrei Gavrilescu
309f23ba94 feat(noise-suppression): persist noise suppression setting (#13593)
* persist noise suppression setting

* address code review
2023-07-25 10:57:01 +03:00
brlarini
3f93a81818 Update main-ptBR.json
Added missing translation strings.
2023-07-24 15:54:01 -05:00
damencho
83196cda10 fix: Skips lobby check for jicofo.
Skip any lobby check when the occupant is jicofo.
In case of serverside turn on lobby on room creation we do not check and allow jicofo to join.
We check for resource and no is_admin as in default configuration admins is set only for the main muc component.
2023-07-24 07:38:15 -05:00
Robert Pintilii
f3670ce86d fix(notifications) Show correct info on updated notification (#13607) 2023-07-24 13:33:34 +03:00
damencho
5fd5804245 fix: Fixes max users and web joining. 2023-07-21 16:49:19 -05:00
damencho
c58e557019 fix(authentication): Fixes web login dialog to connect and join. 2023-07-21 16:49:19 -05:00
Saúl Ibarra Corretgé
50515e0143 fix(api) fix parsing API ID
Parsing the API ID happens at import time, which is not great because it
also runs when loading the external API file.

In sites with weird URL patterns, such as Angular this will throw an
exception.

Ignore parsing errors so it's left undefined. When modules/ is
refactored we should look into making this a getter of some sort.

Fixes: https://github.com/jitsi/jitsi-meet/issues/11565
2023-07-21 15:13:49 +02:00
Mihaela Dumitru
05f082ef06 fix(raised-hand) remove duplicate callback (#13598) 2023-07-21 15:11:11 +03:00
dependabot[bot]
20ef19ecf1 chore(deps-dev): bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-21 10:42:13 +02:00
Jaya Allamsetty
65450e36ef chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1664.0.0+739d8025...v1665.0.0+9ffab6aa
2023-07-20 13:03:17 -04:00
Mihaela Dumitru
1b7a81afa5 feat(external-api) extend event to listen to system buttons and add config to prevent execution 2023-07-20 12:25:40 +02:00
subhamcyara
470e987fad feat(stats) add support for watchRTC 2023-07-20 08:40:48 +02:00
Jaya Allamsetty
419db67267 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1662.0.0+6c4381c8...v1664.0.0+739d8025
2023-07-20 08:39:04 +02:00
Дамян Минков
ff70025429 feat: Returns an error on join request with no display name. (#13546)
* feat: Returns an error on join request with no display name.

When someone tries to join a room with lobby enabled and display name is not set returns an error.

* squash: Fixes handling DISPLAY_NAME_REQUIRED with preJoin disabled.

* squash: Fixes mobile build.

* squash: Move isDisplayNameRequired redux state in lobby and introduces isDisplayNameRequiredError.

* squash: Drops unused isDisplayNameRequired.

This was used on showing prejoin when connection was established on showing prejoin. We no longer establish it at that time, so it is not possible to hit it and act on it.
2023-07-19 15:35:27 -05:00
Javier García
7a305ef96e feat: New config disable feature virtual background (#13580)
* New config disable feature virtual background

* Change enableVirtualBackground to disableVirtualBackground in config file and correct lint problems

* Fix comment disable virtual background

* Change deprecated APP.storage to IReduxState
2023-07-19 11:07:57 -05:00
kerem
28efcea9d8 add Syntax highlighting to README 2023-07-19 17:00:05 +03:00
Calinteodor
3927f3423f sdk(react-native-sdk): update readme with jwt token example (#13587)
* sdk(react-native-sdk): update readme with jwt token example
2023-07-19 14:48:18 +03:00
Calin-Teodor
0c3b5139b1 sdk(react-native-sdk): changed rn-immersive to rn-immersive-mode 2023-07-19 13:14:37 +03:00
Saúl Ibarra Corretgé
ec9fcdf1cb RN refactor immersive mode (#13583)
* fix(android): refactor immersive mode
2023-07-19 12:00:47 +03:00
Jaya Allamsetty
d335438f12 chore(deps) lib-jitsi-meet@latest
https://github.com/jitsi/lib-jitsi-meet/compare/v1659.0.0+5d322ea5...v1662.0.0+6c4381c8
2023-07-18 17:15:54 -04:00
Saúl Ibarra Corretgé
69b536cfb9 misc(package.json) mark as private
Prevents us from accidentally publishing it.
2023-07-18 20:51:34 +02:00
Calin-Teodor
edb4555699 sdk(react-native-sdk): update README file 2023-07-18 20:02:40 +03:00
pallab-gain-tt
8b6728c65d chroe(assets) copy sound files
- Add `script_phase` into podspec to copy sound files into host application
2023-07-18 17:50:48 +03:00
pallab-gain-tt
d3336192c3 chroe(assets) copy sound files
- Now when integrated with a host app, sound files will be copied as a part of gradle (build)process. Just a copy from `android/sdk/build.gradle` `mergeAssetsTask` subtask
2023-07-18 17:50:48 +03:00
Avram Tudor
10eadbb7e6 fix(css) fix cut off borders and barely visible scrollbars (#13576) 2023-07-18 16:02:02 +03:00
Saúl Ibarra Corretgé
36a5282823 fix(rnsdk) update README 2023-07-18 11:23:39 +02:00
140 changed files with 2673 additions and 957 deletions

View File

@@ -82,7 +82,7 @@ dependencies {
if (!rootProject.ext.libreBuild) {
// Sync with react-native-google-signin
implementation 'com.google.android.gms:play-services-auth:19.0.2'
implementation 'com.google.android.gms:play-services-auth:19.2.0'
// Firebase
// - Crashlytics

View File

@@ -74,7 +74,7 @@ dependencies {
}
implementation project(':react-native-gesture-handler')
implementation project(':react-native-get-random-values')
implementation project(':react-native-immersive')
implementation project(':react-native-immersive-mode')
implementation project(':react-native-keep-awake')
implementation project(':react-native-orientation-locker')
implementation project(':react-native-pager-view')

View File

@@ -77,7 +77,8 @@ public class BroadcastAction {
CLOSE_CHAT("org.jitsi.meet.CLOSE_CHAT"),
SEND_CHAT_MESSAGE("org.jitsi.meet.SEND_CHAT_MESSAGE"),
SET_VIDEO_MUTED("org.jitsi.meet.SET_VIDEO_MUTED"),
SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED");
SET_CLOSED_CAPTIONS_ENABLED("org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED"),
TOGGLE_CAMERA("org.jitsi.meet.TOGGLE_CAMERA");
private final String action;

View File

@@ -75,6 +75,8 @@ public class BroadcastEvent {
}
public enum Type {
CONFERENCE_BLURRED("org.jitsi.meet.CONFERENCE_BLURRED"),
CONFERENCE_FOCUSED("org.jitsi.meet.CONFERENCE_FOCUSED"),
CONFERENCE_JOINED("org.jitsi.meet.CONFERENCE_JOINED"),
CONFERENCE_TERMINATED("org.jitsi.meet.CONFERENCE_TERMINATED"),
CONFERENCE_WILL_JOIN("org.jitsi.meet.CONFERENCE_WILL_JOIN"),
@@ -89,6 +91,8 @@ public class BroadcastEvent {
VIDEO_MUTED_CHANGED("org.jitsi.meet.VIDEO_MUTED_CHANGED"),
READY_TO_CLOSE("org.jitsi.meet.READY_TO_CLOSE");
private static final String CONFERENCE_BLURRED_NAME = "CONFERENCE_BLURRED";
private static final String CONFERENCE_FOCUSED_NAME = "CONFERENCE_FOCUSED";
private static final String CONFERENCE_WILL_JOIN_NAME = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_JOINED_NAME = "CONFERENCE_JOINED";
private static final String CONFERENCE_TERMINATED_NAME = "CONFERENCE_TERMINATED";
@@ -124,6 +128,10 @@ public class BroadcastEvent {
private static Type buildTypeFromName(String name) {
switch (name) {
case CONFERENCE_BLURRED_NAME:
return CONFERENCE_BLURRED;
case CONFERENCE_FOCUSED_NAME:
return CONFERENCE_FOCUSED;
case CONFERENCE_WILL_JOIN_NAME:
return CONFERENCE_WILL_JOIN;
case CONFERENCE_JOINED_NAME:

View File

@@ -54,4 +54,14 @@ public class BroadcastIntentHelper {
intent.putExtra("enabled", enabled);
return intent;
}
public static Intent buildRetrieveParticipantsInfo(String requestId) {
Intent intent = new Intent(BroadcastAction.Type.RETRIEVE_PARTICIPANTS_INFO.getAction());
intent.putExtra("requestId", requestId);
return intent;
}
public static Intent buildToggleCameraIntent() {
return new Intent(BroadcastAction.Type.TOGGLE_CAMERA.getAction());
}
}

View File

@@ -96,6 +96,7 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
constants.put("SEND_CHAT_MESSAGE", BroadcastAction.Type.SEND_CHAT_MESSAGE.getAction());
constants.put("SET_VIDEO_MUTED", BroadcastAction.Type.SET_VIDEO_MUTED.getAction());
constants.put("SET_CLOSED_CAPTIONS_ENABLED", BroadcastAction.Type.SET_CLOSED_CAPTIONS_ENABLED.getAction());
constants.put("TOGGLE_CAMERA", BroadcastAction.Type.TOGGLE_CAMERA.getAction());
return constants;
}

View File

@@ -26,7 +26,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.ReactRootView;
import com.rnimmersive.RNImmersiveModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
@@ -229,22 +228,4 @@ public class JitsiMeetView extends FrameLayout {
dispose();
super.onDetachedFromWindow();
}
/**
* Called when the window containing this view gains or loses focus.
*
* @param hasFocus If the window of this view now has focus, {@code true};
* otherwise, {@code false}.
*/
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
// https://github.com/mockingbot/react-native-immersive#restore-immersive-state
RNImmersiveModule immersive = RNImmersiveModule.getInstance();
if (hasFocus && immersive != null) {
immersive.emitImmersiveStateChangeEvent();
}
}
}

View File

@@ -105,7 +105,7 @@ class ReactInstanceManagerHolder {
new com.oney.WebRTCModule.WebRTCModulePackage(),
new com.swmansion.gesturehandler.RNGestureHandlerPackage(),
new org.linusu.RNGetRandomValuesPackage(),
new com.rnimmersive.RNImmersivePackage(),
new com.rnimmersivemode.RNImmersiveModePackage(),
new com.swmansion.rnscreens.RNScreensPackage(),
new com.zmxv.RNSound.RNSoundPackage(),
new com.th3rdwave.safeareacontext.SafeAreaContextPackage(),

View File

@@ -27,8 +27,8 @@ include ':react-native-giphy'
project(':react-native-giphy').projectDir = new File(rootProject.projectDir, '../node_modules/@giphy/react-native-sdk/android')
include ':react-native-google-signin'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-google-signin/google-signin/android')
include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-immersive-mode'
project(':react-native-immersive-mode').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive-mode/android')
include ':react-native-keep-awake'
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
include ':react-native-orientation-locker'

View File

@@ -1646,10 +1646,14 @@ export default {
APP.store.dispatch(conferenceUniqueIdSet(room, ...args));
});
room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled, authLogin) =>
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
// we want to ignore this event in case of tokenAuthUrl config
// we are deprecating this and at some point will get rid of it
if (!config.tokenAuthUrl) {
room.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled, authLogin) =>
APP.store.dispatch(authStatusChanged(authEnabled, authLogin)));
}
room.on(JitsiConferenceEvents.PARTCIPANT_FEATURES_CHANGED, user => {
APP.store.dispatch(updateRemoteParticipantFeatures(user));

View File

@@ -826,6 +826,42 @@ var config = {
// 'whiteboard',
// ],
// Participant context menu buttons which have their click/tap event exposed through the API on
// `participantMenuButtonClick`. Passing a string for the button key will
// prevent execution of the click/tap routine; passing an object with `key` and
// `preventExecution` flag on false will not prevent execution of the click/tap
// routine. Below array with mixed mode for passing the buttons.
// participantMenuButtonsWithNotifyClick: [
// 'allow-video',
// {
// key: 'ask-unmute',
// preventExecution: false
// },
// 'conn-status',
// 'flip-local-video',
// 'grant-moderator',
// {
// key: 'kick',
// preventExecution: true
// },
// {
// key: 'hide-self-view',
// preventExecution: false
// },
// 'mute',
// 'mute-others',
// 'mute-others-video',
// 'mute-video',
// 'pinToStage',
// 'privateMessage',
// {
// key: 'remote-control',
// preventExecution: false
// },
// 'send-participant-to-room',
// 'verify',
// ],
// List of pre meeting screens buttons to hide. The values must be one or more of the 5 allowed buttons:
// 'microphone', 'camera', 'select-background', 'invite', 'settings'
// hiddenPremeetingButtons: [],
@@ -1008,6 +1044,11 @@ var config = {
// "libs/analytics-ga.min.js", // google-analytics
// "https://example.com/my-custom-analytics.js",
// ],
// By enabling watchRTCEnabled option you would want to use watchRTC feature
// This would also require to configure watchRTCConfigParams.
// Please remember to keep rtcstatsEnabled disabled for watchRTC to work.
// watchRTCEnabled: false,
},
// Logs that should go be passed through the 'log' event if a handler is defined for it
@@ -1306,6 +1347,9 @@ var config = {
// hideJoinRoomButton: false,
// },
// When true, virtual background feature will be disabled.
// disableVirtualBackground: false,
// When true the user cannot add more images to be used as virtual background.
// Only the default ones from will be available.
// disableAddingBackgroundImages: false,
@@ -1403,6 +1447,8 @@ var config = {
peopleSearchUrl
requireDisplayName
tokenAuthUrl
tokenAuthUrlAutoRedirect
tokenLogoutUrl
*/
/**
@@ -1599,6 +1645,40 @@ var config = {
// // https://github.com/jitsi/excalidraw-backend
// collabServerBaseUrl: 'https://excalidraw-backend.example.com',
// },
// The watchRTC initialize config params as described :
// https://testrtc.com/docs/installing-the-watchrtc-javascript-sdk/#h-set-up-the-sdk
// https://www.npmjs.com/package/@testrtc/watchrtc-sdk
// watchRTCConfigParams: {
// /** Watchrtc api key */
// rtcApiKey: string;
// /** Identifier for the session */
// rtcRoomId?: string;
// /** Identifier for the current peer */
// rtcPeerId?: string;
// /**
// * ["tag1", "tag2", "tag3"]
// * @deprecated use 'keys' instead
// */
// rtcTags?: string[];
// /** { "key1": "value1", "key2": "value2"} */
// keys?: any;
// /** Enables additional logging */
// debug?: boolean;
// rtcToken?: string;
// /**
// * @deprecated No longer needed. Use "proxyUrl" instead.
// */
// wsUrl?: string;
// proxyUrl?: string;
// console?: {
// level: string;
// override: boolean;
// };
// allowBrowserLogCollection?: boolean;
// collectionInterval?: number;
// logGetStats?: boolean;
// },
};
// Temporary backwards compatibility with old mobile clients.

View File

@@ -166,7 +166,7 @@ form {
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, .5);
background: #3D3D3D;
border-radius: 4px;
}

View File

@@ -39,11 +39,6 @@
[builder setFeatureFlag:@"ios.screensharing.enabled" withBoolean:YES];
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
#if TARGET_IPHONE_SIMULATOR
// CallKit has started to create problems starting with the iOS 16 simulator.
// Disable it since it never worked in the simulator anyway.
[builder setFeatureFlag:@"call-integration.enabled" withBoolean:NO];
#endif
}];
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];

View File

@@ -70,10 +70,7 @@ RCT_EXPORT_MODULE();
= [[NSBundle bundleForClass:self.class] infoDictionary];
NSString *sdkVersion = sdkInfoDictionary[@"CFBundleShortVersionString"];
if (sdkVersion == nil) {
sdkVersion = sdkInfoDictionary[@"CFBundleVersion"];
if (sdkVersion == nil) {
sdkVersion = @"";
}
sdkVersion = @"";
}
// build number

View File

@@ -30,5 +30,6 @@ static NSString * const sendEventNotificationName = @"org.jitsi.meet.SendEvent";
- (void)sendChatMessage:(NSString*)message :(NSString*)to ;
- (void)sendSetVideoMuted:(BOOL)muted;
- (void)sendSetClosedCaptionsEnabled:(BOOL)enabled;
- (void)toggleCamera;
@end

View File

@@ -27,6 +27,7 @@ static NSString * const closeChatAction = @"org.jitsi.meet.CLOSE_CHAT";
static NSString * const sendChatMessageAction = @"org.jitsi.meet.SEND_CHAT_MESSAGE";
static NSString * const setVideoMutedAction = @"org.jitsi.meet.SET_VIDEO_MUTED";
static NSString * const setClosedCaptionsEnabledAction = @"org.jitsi.meet.SET_CLOSED_CAPTIONS_ENABLED";
static NSString * const toggleCameraAction = @"org.jitsi.meet.TOGGLE_CAMERA";
@implementation ExternalAPI
@@ -50,7 +51,8 @@ RCT_EXPORT_MODULE();
@"CLOSE_CHAT": closeChatAction,
@"SEND_CHAT_MESSAGE": sendChatMessageAction,
@"SET_VIDEO_MUTED" : setVideoMutedAction,
@"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction
@"SET_CLOSED_CAPTIONS_ENABLED": setClosedCaptionsEnabledAction,
@"TOGGLE_CAMERA": toggleCameraAction
};
};
@@ -75,7 +77,8 @@ RCT_EXPORT_MODULE();
closeChatAction,
sendChatMessageAction,
setVideoMutedAction,
setClosedCaptionsEnabledAction
setClosedCaptionsEnabledAction,
toggleCameraAction
];
}
@@ -173,4 +176,8 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
[self sendEventWithName:setClosedCaptionsEnabledAction body:data];
}
- (void)toggleCamera {
[self sendEventWithName:toggleCameraAction body:nil];
}
@end

View File

@@ -46,5 +46,6 @@
- (void)sendChatMessage:(NSString * _Nonnull)message :(NSString * _Nullable)to;
- (void)setVideoMuted:(BOOL)muted;
- (void)setClosedCaptionsEnabled:(BOOL)enabled;
- (void)toggleCamera;
@end

View File

@@ -138,6 +138,11 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
[externalAPI sendSetClosedCaptionsEnabled:enabled];
}
- (void)toggleCamera {
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
[externalAPI toggleCamera];
}
#pragma mark Private methods
- (void)registerObservers {

View File

@@ -63,6 +63,8 @@
"leaveBreakoutRoom": "Breakout-Raum verlassen",
"more": "Mehr",
"remove": "Entfernen",
"rename": "Umbenennen",
"renameBreakoutRoom": "Breakout-Raum umbenennen",
"sendToBreakoutRoom": "Anwesende in Breakout-Raum verschieben:"
},
"defaultName": "Breakout-Raum #{{index}}",
@@ -370,8 +372,6 @@
"permissionCameraRequiredError": "Der Zugriff auf die Kamera wird benötigt, um in Videokonferenzen teilzunehmen. Bitte in den Einstellungen zulassen",
"permissionErrorTitle": "Berechtigung benötigt",
"permissionMicRequiredError": "Der Zugriff auf das Mikrofon wird benötigt, um an Konferenzen mit Ton teilzunehmen. Bitte in den Einstellungen zulassen",
"popupError": "Ihr Browser blockiert Pop-ups von dieser Website. Bitte aktivieren Sie Pop-ups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
"popupErrorTitle": "Pop-up blockiert",
"readMore": "mehr",
"recentlyUsedObjects": "Ihre zuletzt verwendeten Objekte",
"recording": "Aufnahme",
@@ -388,6 +388,8 @@
"removePassword": "$t(lockRoomPasswordUppercase) entfernen",
"removeSharedVideoMsg": "Sind Sie sicher, dass Sie das geteilte Video entfernen möchten?",
"removeSharedVideoTitle": "Freigegebenes Video entfernen",
"renameBreakoutRoomLabel": "Raumname",
"renameBreakoutRoomTitle": "Breakout-Raum umbenennen",
"reservationError": "Fehler im Reservierungssystem",
"reservationErrorMsg": "Fehler, Nummer: {{code}}, Nachricht: {{msg}}",
"retry": "Wiederholen",
@@ -439,6 +441,7 @@
"token": "Token",
"tokenAuthFailed": "Sie sind nicht berechtigt, dieser Konferenz beizutreten.",
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
"tokenAuthUnsupported": "Token-Authentifizierung wird nicht unterstützt.",
"transcribing": "Wird transkribiert",
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
"user": "Anmeldename",
@@ -742,7 +745,6 @@
"newDeviceCameraTitle": "Neue Kamera erkannt",
"noiseSuppressionDesktopAudioDescription": "Die Rauschunterdrückung kann nicht genutzt werden, wenn der Computersound geteilt wird, bitte zuerst deaktivieren und dann nochmals versuchen.",
"noiseSuppressionFailedTitle": "Rauschunterdrückung konnte nicht gestartet werden",
"noiseSuppressionNoTrackDescription": "Bitte eigenes Mikrofon zuerst aktivieren.",
"noiseSuppressionStereoDescription": "Rauschunterdrückung unterstützt aktuell keinen Stereoton.",
"oldElectronClientDescription1": "Sie scheinen eine alte Version des Jitsi-Meet-Clients zu nutzen. Diese hat bekannte Schwachstellen. Bitte aktualisieren Sie auf unsere ",
"oldElectronClientDescription2": "aktuelle Version",

View File

@@ -63,6 +63,8 @@
"leaveBreakoutRoom": "Sair da sala",
"more": "Mais",
"remove": "Eliminar sala",
"rename": "Renomear",
"renameBreakoutRoom": "Renomear sala",
"sendToBreakoutRoom": "Enviar participante para:"
},
"defaultName": "Sala #{{index}}",
@@ -370,8 +372,6 @@
"permissionCameraRequiredError": "É necessária a autorização da câmara para participar em conferências com vídeo. Por favor, conceda-a em Definições",
"permissionErrorTitle": "Permissão necessária",
"permissionMicRequiredError": "É necessária a permissão do microfone para participar em conferências com áudio. Por favor, conceda-a em Definições",
"popupError": "O seu navegador está a bloquear janelas pop-up a partir deste site. Por favor, active os pop-ups nas definições de segurança do seu browser e tente novamente.",
"popupErrorTitle": "Pop-up bloqueado",
"readMore": "mais",
"recentlyUsedObjects": "Os seus objetos recentemente utilizados",
"recording": "A gravar",
@@ -388,6 +388,8 @@
"removePassword": "Remover $t(lockRoomPassword)",
"removeSharedVideoMsg": "Tem a certeza de que gostaria de remover o seu vídeo partilhado?",
"removeSharedVideoTitle": "Remover vídeo partilhado",
"renameBreakoutRoomLabel": "Nome da sala",
"renameBreakoutRoomTitle": "Mudar o nome da sala",
"reservationError": "Erro no sistema de reservas",
"reservationErrorMsg": "Código de erro: {{code}}, mensagem: {{msg}}",
"retry": "Tentativa",
@@ -439,6 +441,7 @@
"token": "token",
"tokenAuthFailed": "Desculpe, não está autorizado a juntar-se a esta chamada.",
"tokenAuthFailedTitle": "A autenticação falhou",
"tokenAuthUnsupported": "O URL de token não é suportado.",
"transcribing": "Transcrição",
"unlockRoom": "Retirar reunião $t(lockRoomPassword)",
"user": "Utilizador",
@@ -674,6 +677,8 @@
"sessionToken": "Token de Sessão",
"start": "Iniciar gravação",
"stop": "Parar gravação",
"stopping": "A parar a gravação",
"wait": "Aguarde enquanto guardamos a sua gravação",
"yes": "Sim"
},
"lockRoomPassword": "senha",
@@ -742,7 +747,6 @@
"newDeviceCameraTitle": "Nova câmara detetada",
"noiseSuppressionDesktopAudioDescription": "A supressão de ruído não pode ser ativada enquanto se partilha o áudio do ambiente de trabalho, por favor desative-o e tente novamente.",
"noiseSuppressionFailedTitle": "Falha ao iniciar a supressão de ruído",
"noiseSuppressionNoTrackDescription": "Por favor, ligue primeiro o seu microfone.",
"noiseSuppressionStereoDescription": "A supressão do ruído de áudio estéreo não é atualmente suportada.",
"oldElectronClientDescription1": "Parece estar a utilizar uma versão antiga do cliente Jitsi Meet que tem vulnerabilidades de segurança conhecidas. Por favor, certifique-se de que actualiza a nossa ",
"oldElectronClientDescription2": "compilação mais recente",
@@ -1073,13 +1077,14 @@
"links": "Links",
"privacy": "Privacidade",
"profileSection": "Perfil",
"sdkVersion": "Versão do SDK",
"serverURL": "URL do servidor",
"showAdvanced": "Mostrar definições avançadas",
"startCarModeInLowBandwidthMode": "Iniciar o modo de condução em modo de baixa largura de banda",
"startWithAudioMuted": "Iniciar sem áudio",
"startWithVideoMuted": "Iniciar sem vídeo",
"terms": "Termos",
"version": "Versão"
"version": "Versão da App"
},
"share": {
"dialInfoText": "\n\n=====\n\nDeseja apenas discar no seu telefone?\n\n{{defaultDialInNumber}}Clique neste link para ver os números de telefone para esta reunião\n{{dialInfoPageUrl}}",
@@ -1124,7 +1129,7 @@
"audioOnly": "Mudar para apenas áudio",
"audioRoute": "Selecionar o dispositivo de som",
"boo": "Vaia",
"breakoutRoom": "Entrar/Sair salas instantâneas",
"breakoutRoom": "Entrar/Sair da sala",
"callQuality": "Gerir a qualidade do vídeo",
"carmode": "Modo de condução",
"cc": "Mudar legendas",
@@ -1240,7 +1245,7 @@
"help": "Ajuda",
"hideWhiteboard": "Esconder quadro branco",
"invite": "Convidar pessoas",
"joinBreakoutRoom": "Entrar na sala",
"joinBreakoutRoom": "Entar na sala",
"laugh": "Risos",
"leaveBreakoutRoom": "Sair da sala",
"leaveConference": "Sair da reunião",

View File

@@ -11,7 +11,6 @@
"defaultEmail": "Seu email padrão",
"disabled": "Você não pode convidar pessoas.",
"failedToAdd": "Falha em adicionar participantes",
"footerText": "Discagem está desativada.",
"googleEmail": "Email Google",
"inviteMoreHeader": "Você é o único na reunião",
"inviteMoreMailSubject": "Entre na reunião {{appName}}",
@@ -31,6 +30,7 @@
},
"audioDevices": {
"bluetooth": "Bluetooth",
"car": "Áudio do carro",
"headphones": "Fones de ouvido",
"none": "Sem dispositivos de áudio disponível",
"phone": "Celular",
@@ -39,6 +39,25 @@
"audioOnly": {
"audioOnly": "Largura de banda baixa"
},
"breakoutRooms": {
"actions": {
"add": "Adicionar sala de apoio",
"autoAssign": "Atribuir a salas de apoio automaticamente",
"close": "Fechar",
"join": "Participar",
"leaveBreakoutRoom": "Sair da sala de apoio",
"more": "Mais",
"remove": "Remover",
"sendToBreakoutRoom": "Enviar participante para:"
},
"defaultName": "Sala de apoio #{{index}}",
"mainRoom": "Sala principal",
"notifications": {
"joined": "Entrando na sala de apoio \"{{name}}\"",
"joinedMainRoom": "Entrando na sala principal",
"joinedTitle": "Salas de apoio"
}
},
"calendarSync": {
"addMeetingURL": "Adicionar um link da reunião",
"confirmAddLink": "Gostaria de adicionar um link do Jitsi a esse evento?",
@@ -57,15 +76,27 @@
"refresh": "Atualizar calendário",
"today": "Hoje"
},
"carmode": {
"actions": {
"selectSoundDevice": "Selecionar dispositivo de som"
},
"labels": {
"buttonLabel": "Modo carro",
"title": "Modo carro",
"videoStopped": "Seu vídeo está parado"
}
},
"chat": {
"enter": "Entrar no bate-papo",
"error": "Erro: sua mensagem não foi enviada. Motivo: {{error}}",
"fieldPlaceHolder": "Digite sua mensagem aqui",
"lobbyChatMessageTo": "Mensagem para {{recipient}}",
"message": "Mensagem",
"messageAccessibleTitle": "{{user}} disse:",
"messageAccessibleTitleMe": "Você disse:",
"messageTo": "Mensagem privada para {{recipient}}",
"messagebox": "Digite uma mensagem",
"newMessages": "Novas mensagens",
"nickname": {
"popover": "Escolha um apelido",
"title": "Digite um apelido para usar o bate-papo",
@@ -85,6 +116,7 @@
},
"chromeExtensionBanner": {
"buttonText": "Instalar extensão do Chrome",
"buttonTextEdge": "Instalar extensão do Edge",
"close": "Fechar",
"dontShowAgain": "Não me mostre isso de novo",
"installExtensionText": "Instale a extensão para integrar com Google Calendar e Office 365"
@@ -115,6 +147,7 @@
"bridgeCount": "Servidores: ",
"codecs": "Codecs (A/V): ",
"connectedTo": "Conectado a:",
"e2eeVerified": "Verificado via E2EE",
"framerate": "Taxa de quadros:",
"less": "Mostrar menos",
"localaddress": "Endereço local:",
@@ -123,6 +156,7 @@
"localport_plural": "Portas locais:",
"maxEnabledResolution": "envio máx",
"more": "Mostrar mais",
"no": "não",
"packetloss": "Perda de pacote:",
"participant_id": "Id participante:",
"quality": {
@@ -141,7 +175,8 @@
"status": "Conexão:",
"transport": "Transporte:",
"transport_plural": "Transportes:",
"video_ssrc": "Video SSRC:"
"video_ssrc": "Video SSRC:",
"yes": "sim"
},
"dateUtils": {
"earlier": "Mais cedo",
@@ -151,14 +186,23 @@
"deepLinking": {
"appNotInstalled": "Você precisa do aplicativo móvel {{app}} para participar da reunião no seu telefone.",
"description": "Nada acontece? Estamos tentando iniciar sua reunião no aplicativo desktop {{app}}. Tente novamente ou inicie ele na aplicação web {{app}}.",
"descriptionNew": "Nada aconteceu? Tentamos iniciar sua reunião no aplicativo de desktop {{app}}. <br /><br /> Você pode tentar novamente ou abrir pela web.",
"descriptionWithoutWeb": "Nada aconteceu? Tentamos iniciar sua reunião no aplicativo de desktop {{app}}.",
"downloadApp": "Baixe o Aplicativo",
"downloadMobileApp": "Baixar o app",
"ifDoNotHaveApp": "Se você não tem o app ainda:",
"ifHaveApp": "Se você já tem o app:",
"joinInApp": "Entrar na reunião usando o app",
"joinInAppNew": "Entrar usando o app",
"joinInBrowser": "Entrar usando o navegador",
"launchMeetingLabel": "Como deseja entrar nesta reunião?",
"launchWebButton": "Iniciar na web",
"noMobileApp": "Não tem o app?",
"termsAndConditions": "Ao continuar você estará aceitando nossos <a href='{{termsAndConditionsLink}}' rel='noopener noreferrer' target='_blank'>termos e condições.</a>",
"title": "Iniciando sua reunião no {{app}}...",
"tryAgainButton": "Tente novamente no desktop"
"titleNew": "Iniciando sua reunião ...",
"tryAgainButton": "Tente novamente no desktop",
"unsupportedBrowser": "Parece que você está usando um navegador ao qual não temos suporte."
},
"defaultLink": "ex.: {{url}}",
"defaultNickname": "ex.: João Pedro",
@@ -169,11 +213,20 @@
"microphonePermission": "Erro ao obter permissão para o microfone"
},
"deviceSelection": {
"hid": {
"callControl": "Controle de chamada",
"connectedDevices": "Dispositivos conectados:",
"deleteDevice": "Remover dispositivo",
"pairDevice": "Emparelhar dispositivo"
},
"noPermission": "Permissão não concedida",
"previewUnavailable": "Visualização indisponível",
"selectADevice": "Selecione um dispositivo",
"testAudio": "Tocar um som de teste"
},
"dialIn": {
"screenTitle": "Sumário de discagem"
},
"dialOut": {
"statusMessage": "está agora {{status}}"
},
@@ -189,9 +242,13 @@
"WaitingForHostTitle": "Esperando o anfitrião...",
"Yes": "Sim",
"accessibilityLabel": {
"liveStreaming": "Transmissão ao vivo"
"close": "Fechar janela",
"liveStreaming": "Transmissão ao vivo",
"sharingTabs": "Opções de compartilhamento"
},
"add": "Adicionar",
"addMeetingNote": "Adicionar uma anotação para esta reunião",
"addOptionalNote": "Adicionar uma anotação (opcional):",
"allow": "Permitir",
"alreadySharedVideoMsg": "Outro participante já está compartilhando um vídeo. Esta conferência permite apenas um vídeo compartilhado por vez.",
"alreadySharedVideoTitle": "Somente um vídeo compartilhado é permitido por vez",
@@ -233,6 +290,7 @@
"gracefulShutdown": "Nosso serviço está em manutenção. Tente novamente mais tarde.",
"grantModeratorDialog": "Tem certeza que quer participar como moderador da reunião?",
"grantModeratorTitle": "Permitir moderador",
"hide": "Ocultar",
"hideShareAudioHelper": "Não mostre este diálogo novamente",
"incorrectPassword": "Usuário ou senha incorretos",
"incorrectRoomLockPassword": "Senha incorreta",
@@ -243,9 +301,10 @@
"kickParticipantDialog": "Tem certeza de que deseja remover este participante?",
"kickParticipantTitle": "Remover este participante?",
"kickTitle": "{{participantDisplayName}} removeu você da reunião",
"linkMeeting": "Vincular reunião",
"linkMeetingTitle": "Vincular reunião ao Salesforce",
"liveStreaming": "Transmissão ao Vivo",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Não é possível transmitir enquanto a gravação está ativa",
"liveStreamingDisabledTooltip": "Iniciar transmissão ao vivo desativada.",
"localUserControls": "Controles de usuários locais",
"lockMessage": "Falha ao bloquear a conferência.",
"lockRoom": "Adicionar reunião $t(lockRoomPasswordUppercase)",
@@ -279,11 +338,11 @@
"muteEveryonesVideoTitle": "Desativar a câmera de todos?",
"muteParticipantBody": "Você não está habilitado para tirar o mudo deles, mas eles podem tirar o mudo deles mesmos a qualquer tempo.",
"muteParticipantButton": "Mudo",
"muteParticipantDialog": "Tem certeza de que deseja silenciar este participante? Você não poderá desfazer isso, mas o participante pode reabilitar o áudio a qualquer momento.",
"muteParticipantTitle": "Deixar mudo este participante?",
"muteParticipantsVideoBody": "Você não poderá reativar posteriormente, mas o participante pode ativar sua própria câmera a qualquer momento.",
"muteParticipantsVideoBodyModerationOn": "Você e o participante não poderão reativar a câmera posteriormente.",
"muteParticipantsVideoButton": "Desativar a câmera",
"muteParticipantsVideoDialog": "Tem certeza de que deseja desativar a câmera deste participante? Você não poderá reativar posteriormente, mas o participante pode ativar sua própria câmera a qualquer momento.",
"muteParticipantsVideoDialogModerationOn": "Tem certeza de que deseja desativar a câmera deste participante? Você e o participante não poderão reativar posteriormente.",
"muteParticipantsVideoTitle": "Desativar a câmera deste participante?",
"noDropboxToken": "Nenhum token do Dropbox válido",
"password": "Senha",
@@ -297,9 +356,9 @@
"popupError": "Seu navegador está bloqueando janelas popup deste site. Habilite os popups nas configurações de segurança no seu navegador e tente novamente.",
"popupErrorTitle": "Popup bloqueado",
"readMore": "mais...",
"recentlyUsedObjects": "Seus objetos usados recentemente",
"recording": "Gravando",
"recordingDisabledBecauseOfActiveLiveStreamingTooltip": "Não é possível transmitir enquanto a gravação está ativa",
"recordingDisabledTooltip": "Iniciar gravação desativada.",
"rejoinNow": "Reconectar agora",
"remoteControlAllowedMessage": "{{user}} aceitou sua requisição de controle remoto!",
"remoteControlDeniedMessage": "{{user}} rejeitou sua requisição de controle remoto!",
@@ -319,6 +378,12 @@
"screenSharingFailed": "Oops! Alguma coisa de errado aconteceu, não é possível habilitar o compartilhamento de tela!",
"screenSharingFailedTitle": "Falha ao compartilhar a tela!",
"screenSharingPermissionDeniedError": "Oops! Alguma coisa está errada com suas permissões de compartilhamento de tela. Recarregue e tente de novo.",
"searchInSalesforce": "Pesquisar no Salesforce",
"searchResults": "Resultados da pesquisa({{count}})",
"searchResultsDetailsError": "Algo de errado ocorreu ao recuperar os dados do proprietário.",
"searchResultsError": "Algo de errado ocorreu ao recuperar os dados.",
"searchResultsNotFound": "A pesquisa não trouxe resultados.",
"searchResultsTryAgain": "Tente usar termos diferentes.",
"sendPrivateMessage": "Você enviou uma mensagem privada recentemente. Tem intenção de responder em privado, ou deseja enviar sua mensagem para o grupo?",
"sendPrivateMessageCancel": "Enviar para o grupo",
"sendPrivateMessageOk": "Enviar em privado",
@@ -341,7 +406,10 @@
"shareVideoTitle": "Compartilhar um vídeo",
"shareYourScreen": "Compartilhar sua tela",
"shareYourScreenDisabled": "Compartilhamento de tela desativada.",
"sharedVideoDialogError": "Erro: URL inválida",
"sharedVideoLinkPlaceholder": "link do YouTube ou link direto de vídeo",
"show": "Exibir",
"start": "Iniciar ",
"startLiveStreaming": "Iniciar transmissão ao vivo",
"startRecording": "Iniciar gravação",
"startRemoteControlErrorMessage": "Um erro ocorreu enquanto tentava iniciar uma sessão de controle remoto!",
@@ -359,6 +427,10 @@
"user": "Usuário",
"userIdentifier": "identificação do usuário",
"userPassword": "senha do usuário",
"verifyParticipantConfirm": "Coincidem",
"verifyParticipantDismiss": "Não coincidem",
"verifyParticipantQuestion": "EXPERIMENTAL: Pergunta ao participante {{participantName}} se ele vê o mesmo conteúdo na mesma ordem.",
"verifyParticipantTitle": "Verificação de usuário",
"videoLink": "Link do vídeo",
"viewUpgradeOptions": "Ver opções de atualização",
"viewUpgradeOptionsContent": "Para obter acesso ilimitado a recursos premium tais como gravação, transcrição, streaming RTMP e muito mais, você precisa atualizar seu plano.",
@@ -384,8 +456,14 @@
"veryBad": "Muito ruim",
"veryGood": "Muito boa"
},
"helpView": {
"title": "Centro de ajuda"
"filmstrip": {
"accessibilityLabel": {
"heading": "Miniaturas de vídeo"
}
},
"giphy": {
"noResults": "Nenhum resultado encontrado :(",
"search": "Buscar GIPHY"
},
"incomingCall": {
"answer": "Responder",
@@ -427,9 +505,11 @@
"noRoom": "Nenhuma sala foi especificada para entrar.",
"numbers": "Números de discagem",
"password": "$t(lockRoomPasswordUppercase):",
"reachedLimit": "Você atingiu o limite do seu plano.",
"sip": "endereço SIP",
"title": "Compartilhar",
"tooltip": "Compartilhar link e discagem para esta reunião"
"tooltip": "Compartilhar link e discagem para esta reunião",
"upgradeOptions": "Por favor, verifique as opções de upgrade em"
},
"inlineDialogFailure": {
"msg": "Tivemos um pequeno problema.",
@@ -450,6 +530,7 @@
"focusLocal": "Focar no seu vídeo",
"focusRemote": "Focar no vídeo de outro participante",
"fullScreen": "Entrar ou sair da tela cheia",
"giphyMenu": "Alternar menu do GIPHY",
"keyboardShortcuts": "Atalhos de teclado",
"localRecording": "Mostrar ou ocultar controles de gravação local",
"mute": "Deixar mudo ou não o microfone",
@@ -463,6 +544,10 @@
"toggleShortcuts": "Mostrar ou ocultar atalhos de teclado",
"videoMute": "Iniciar ou parar sua câmera"
},
"largeVideo": {
"screenIsShared": "Você está compartilhando sua tela",
"showMeWhatImSharing": "Me mostre o que estou compartilhando"
},
"liveStreaming": {
"busy": "Estamos trabalhando para liberar os recursos de transmissão. Tente novamente em alguns minutos.",
"busyTitle": "Todas as transmissões estão atualmente ocupadas",
@@ -479,6 +564,7 @@
"failedToStart": "Falha ao iniciar a transmissão ao vivo",
"getStreamKeyManually": "Não conseguimos buscar nenhuma transmissão ao vivo. Tente obter sua chave de transmissão ao vivo no YouTube.",
"googlePrivacyPolicy": "Política de Privacidade do Google",
"inProgress": "Gravação ou live streaming em andamento",
"invalidStreamKey": "A senha para transmissão ao vivo pode estar incorreta.",
"limitNotificationDescriptionNative": "Sua transmissão será limitada a {{limit}} minutos. Para transmissão ilimitada tente {{app}}.",
"limitNotificationDescriptionWeb": "Devido a alta demanda sua transmissão será limitada a {{limit}} minutos. Para transmissão ilimitada tente <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
@@ -488,6 +574,7 @@
"onBy": "{{name}} iniciou a transmissão ao vivo",
"pending": "Iniciando Transmissão ao Vivo...",
"serviceName": "Serviço de Transmissão ao Vivo",
"sessionAlreadyActive": "Esta sessão já está sendo gravada ou em live streaming.",
"signIn": "Faça login no Google",
"signInCTA": "Faça login ou insira sua chave de transmissão ao vivo do YouTube.",
"signOut": "Sair",
@@ -501,8 +588,8 @@
"lobby": {
"admit": "Aceitar",
"admitAll": "Aceitar todos",
"allow": "Permitir",
"backToKnockModeButton": "Sem senha, peça para se juntar",
"chat": "Chat",
"dialogTitle": "Modo sala de espera",
"disableDialogContent": "O modo sala de espera está habilitado. Este recurso evita que particpantes não convidados juntem-se à sua conferência. Deseja desabilitar?",
"disableDialogSubmit": "Desabilitar",
@@ -515,6 +602,7 @@
"errorMissingPassword": "Por favor informe a senha da conferência",
"invalidPassword": "Senha inválida",
"joinRejectedMessage": "Sua solicitação de participação foi rejeitada pelo moderador.",
"joinRejectedTitle": "Solicitação de participação recusada",
"joinTitle": "Junte-se à conferência",
"joinWithPasswordMessage": "Tentando entrar com a senha, por favor aguarde...",
"joiningMessage": "Você se juntará à conferência tão logo alguém aprove sua solicitação",
@@ -523,6 +611,8 @@
"knockButton": "Peça para participar",
"knockTitle": "Alguém deseja participar da conferência",
"knockingParticipantList": "Remover lista de participantes",
"lobbyChatStartedNotification": "{{moderator}} iniciou uma conversa na sala de espera com {{attendee}}",
"lobbyChatStartedTitle": "{{moderator}} iniciou uma conversa na sala de espera com você.",
"nameField": "Informe seu nome",
"notificationLobbyAccessDenied": "{{targetParticipantName}} foi rejeitado por {{originParticipantName}}",
"notificationLobbyAccessGranted": "{{targetParticipantName}} foi aceito por {{originParticipantName}}",
@@ -560,6 +650,7 @@
"no": "Não",
"participant": "Participante",
"participantStats": "Estatísticas dos Participantes",
"selectTabTitle": "🎥 Por favor selecione esta aba para gravar",
"sessionToken": "Token de Sessão",
"start": "Iniciar gravação",
"stop": "Parar a Gravação",
@@ -576,18 +667,39 @@
"OldElectronAPPTitle": "Vulnerabilidade de segurança!",
"allowAction": "Permitir",
"allowedUnmute": "Você pode religar seu microfone, ativar sua câmera ou compartilhar sua tela.",
"audioUnmuteBlockedDescription": "A liberação do microfone foi temporariamente bloqueada devido a limites do sistema.",
"audioUnmuteBlockedTitle": "Microfone bloqueado!",
"chatMessages": "Mensagens do chat",
"connectedOneMember": "{{name}} entrou na reunião",
"connectedThreePlusMembers": "{{name}} e outros {{count}} entraram na reunião",
"connectedTwoMembers": "{{first}} e {{second}} entraram na reunião",
"dataChannelClosed": "Qualidade do vídeo prejudicada",
"dataChannelClosedDescription": "O canal da ponte foi desconectado, assim a qualidade do vídeo foi limitada a sua configuração mais baixa.",
"disabledIframe": "Incorporação destina-se apenas a fins de demonstração, assim esta chamada será desconectada em {{timeout}} minutos.",
"disconnected": "desconectado",
"displayNotifications": "Exibir notificações para",
"dontRemindMe": "Não me lembrar",
"focus": "Foco da conferência",
"focusFail": "{{component}} não disponível - tente em {{ms}} seg",
"gifsMenu": "GIPHY",
"groupTitle": "Notificações",
"hostAskedUnmute": "O anfitrião deseja que você ative o som",
"invitedOneMember": "{{name}} foi convidado(a)",
"invitedThreePlusMembers": "{{name}} e {{count}} outros foram convidados",
"invitedTwoMembers": "{{first}} e {{second}} foram convidados",
"joinMeeting": "Participar",
"kickParticipant": "{{kicked}} foi removido por {{kicker}}",
"leftOneMember": "{{name}} saiu da reunião",
"leftThreePlusMembers": "{{name}} e muitos outros saíram da reunião",
"leftTwoMembers": "{{first}} e {{second}} saíram da reunião",
"linkToSalesforce": "Link para Salesforce",
"linkToSalesforceDescription": "Você pode vincular o sumário da reunião a um objeto do Salesforce.",
"linkToSalesforceError": "Falha ao vincular reunião ao Salesforce",
"linkToSalesforceKey": "Vincular esta reunião",
"linkToSalesforceProgress": "Vinculando reunião ao Salesforce...",
"linkToSalesforceSuccess": "A reunião foi vinculada ao Salesforce",
"localRecordingStarted": "{{name}} iniciou uma gravação local.",
"localRecordingStopped": "{{name}} parou uma gravação local.",
"me": "Eu",
"moderationInEffectCSDescription": "Levante a mão se quiser compartilhar seu vídeo",
"moderationInEffectCSTitle": "O compartilhamento de conteúdo foi desativado pelo moderador",
@@ -608,16 +720,27 @@
"newDeviceAction": "Usar",
"newDeviceAudioTitle": "Novo dispositivo de áudio detectado",
"newDeviceCameraTitle": "Nova câmera detectada",
"noiseSuppressionDesktopAudioDescription": "A supressão de ruído não pode ser habilitada enquanto compartilha o áudio da área de trabalho. Por favor desabilite e tente novamente.",
"noiseSuppressionFailedTitle": "Falha ao iniciar a supressão de ruído",
"noiseSuppressionNoTrackDescription": "Por favor ative o microfone antes.",
"noiseSuppressionStereoDescription": "Supressão de ruído de áudio estéreo não é suportado no momento.",
"oldElectronClientDescription1": "Você está usando um versão antiga do cliente Jitsi Meet que possui uma conhecida vulnerabilidade de segurança. Por favor tenha certeza de atulizar para a nossa ",
"oldElectronClientDescription2": "última versão",
"oldElectronClientDescription3": " agora!",
"participantWantsToJoin": "Deseja entrar na reunião",
"participantsWantToJoin": "Desejam entrar na reunião",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removido por outro participante",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) definido por outro participante",
"raiseHandAction": "Levantar a mão",
"raisedHand": "{{name}} gostaria de falar.",
"raisedHands": "{{participantName}} e {{raisedHands}} outras pessoas",
"reactionSounds": "Desabilitar sons",
"reactionSoundsForAll": "Desabilitar sons para todos",
"screenShareNoAudio": "O compartilhamento de áudio não foi selecionado na tela de escolha de janela.",
"screenShareNoAudioTitle": "Compartilhamento de áudio não selecionado",
"screenSharingAudioOnlyDescription": "Por favor perceba que ao compartilhar sua tela você estará afetando o modo de \"Melhor performance\" e irá usar mais banda de rede.",
"screenSharingAudioOnlyTitle": "Modo de \"Melhor performance\"",
"selfViewTitle": "Você pode habilitar a auto-visualização nas configurações a qualquer momento",
"somebody": "Alguém",
"startSilentDescription": "Volte à reunião para habilitar o áudio",
"startSilentTitle": "Você entrou sem saída de áudio!",
@@ -625,7 +748,11 @@
"suboptimalExperienceTitle": "Alerta do navegador",
"unmute": "Ativar som",
"videoMutedRemotelyDescription": "Você pode ativar sua câmera a qualquer momento.",
"videoMutedRemotelyTitle": "Sua câmera foi desativada por {{participantDisplayName}}!"
"videoMutedRemotelyTitle": "Sua câmera foi desativada por {{participantDisplayName}}!",
"videoUnmuteBlockedDescription": "A liberação da câmera e compartilhamento de tela foram temporariamente bloqueados devido a limites do sistema.",
"videoUnmuteBlockedTitle": "Câmera e compartilhamento de tela bloqueados!",
"viewLobby": "Ver sala de espera",
"waitingParticipants": "{{waitingParticipants}} pessoas"
},
"participantsPane": {
"actions": {
@@ -635,6 +762,9 @@
"audioModeration": "Reativarem seus sons",
"blockEveryoneMicCamera": "Bloquear microfone e câmera de todos",
"invite": "Convidar alguém",
"moreModerationActions": "Mais opções de moderação",
"moreModerationControls": "Mais controles de moderação",
"moreParticipantOptions": "Mais opções de participante",
"mute": "Silenciar",
"muteAll": "Silenciar todos",
"muteEveryoneElse": "Silenciar todos os demais",
@@ -647,17 +777,22 @@
"headings": {
"lobby": "Sala de espera ({{count}})",
"participantsList": "Participantes da reunião ({{count}})",
"visitors": "Visitantes ({{count}})",
"waitingLobby": "Aguardando na sala de espera ({{count}})"
},
"search": "Buscar participantes",
"title": "Participantes"
},
"passwordDigitsOnly": "Até {{number}} dígitos",
"passwordSetRemotely": "Definido por outro participante",
"pinParticipant": "{{participantName}} - Fixar",
"pinnedParticipant": "O participante está fixado",
"polls": {
"answer": {
"skip": "Desistir",
"submit": "Enviar"
},
"by": "Por {{ name }}",
"create": {
"addOption": "Adicionar opção",
"answerPlaceholder": "Opção {{index}}",
@@ -728,15 +863,18 @@
"initiated": "Chamada iniciada",
"joinAudioByPhone": "Participar com o áudio via ligação",
"joinMeeting": "Participar da reunião",
"joinMeetingInLowBandwidthMode": "Participar no modo de banda baixa",
"joinWithoutAudio": "Participar sem áudio",
"keyboardShortcuts": "Habilitar atalhos de teclado",
"linkCopied": "Link copiado para a área de transferência",
"lookGood": "Seu microfone está funcionando corretamente",
"or": "ou",
"premeeting": "Pré-reunião",
"proceedAnyway": "Prosseguir mesmo assim",
"screenSharingError": "Erro de compartilhamento de tela:",
"showScreen": "Habilitar tela pré-reunião",
"startWithPhone": "Iniciar com o áudio da ligação",
"unsafeRoomConsent": "Eu entendo os riscos, desejo ingressar na reunião",
"videoOnlyError": "Erro de vídeo:",
"videoTrackError": "Não é possível criar faixa de vídeo.",
"viewAllNumbers": "veja todos os números"
@@ -763,6 +901,19 @@
"title": "Perfil"
},
"raisedHand": "Gostaria de falar",
"raisedHandsLabel": "Número de mãos levantadas",
"record": {
"already": {
"linked": "A reunião já está vinculada a este objeto do Salesforce"
},
"type": {
"account": "Conta",
"contact": "Contato",
"lead": "Lead",
"opportunity": "Oportunidade",
"owner": "Proprietário"
}
},
"recording": {
"authDropboxText": "Enviar para o Dropbox",
"availableSpace": "Espaço disponível: {{spaceLeft}} MB (aproximadamente {{duration}} minutos de gravação)",
@@ -777,37 +928,66 @@
"expandedPending": "Iniciando gravação...",
"failedToStart": "Falha ao iniciar a gravação",
"fileSharingdescription": "Compartilhar gravação com participantes da reunião",
"highlight": "Destaque",
"highlightMoment": "Momento de destaque",
"highlightMomentDisabled": "Você pode destacar momentos quando a gravação começar",
"highlightMomentSuccess": "Momento destacado",
"highlightMomentSucessDescription": "Seu momento destacado será adicionado ao sumário da reunião.",
"inProgress": "Gravação ou live streaming em andamento",
"limitNotificationDescriptionNative": "Devido a demanda, sua gravação ficará limitada a {{limit}} minutos. Para gravação ilimitada tente <3>{{app}}</3>.",
"limitNotificationDescriptionWeb": "Devido a demanda, sua gravação ficará limitada a {{limit}} minutos. Para gravação ilimitada tente <a href={{url}} rel='noopener noreferrer' target='_blank'>{{app}}</a>.",
"linkGenerated": "Geramos um link para sua gravação.",
"live": "AOVIVO",
"localRecordingNoNotificationWarning": "A gravação não será anunciada aos outros participantes. Você precisará avisá-los que a reunião está sendo gravada.",
"localRecordingNoVideo": "O vídeo não está sendo gravado",
"localRecordingStartWarning": "Por favor, certifique-se de ter parado a gravação antes de sair da reunião para garantir que será salva.",
"localRecordingStartWarningTitle": "Parar a gravação para salvá-la",
"localRecordingVideoStop": "Ao parar o seu vídeo a gravação local também será parada. Tem certeza que deseja continuar?",
"localRecordingVideoWarning": "Para gravar o seu vídeo você precisa ativá-lo antes de inicar a gravação",
"localRecordingWarning": "Tenha certeza de selecionar a aba atual para usar o áudio e vídeo corretos. A gravação está atualmente limitada a 1GB, que corresponde a aproximadamente 100 minutos.",
"loggedIn": "Conectado como {{userName}}",
"noMicPermission": "Trilha para o microfone não pôde ser criada. Por favor conceda permissão para usar o microfone.",
"noStreams": "Nenhum fluxo de áudio ou vídeo detectado.",
"off": "Gravação parada",
"offBy": "{{name}} parou a gravação",
"on": "Gravando",
"onBy": "{{name}} iniciou a gravação",
"onlyRecordSelf": "Gravar apenas o meu fluxo de áudio e vídeo",
"pending": "Preparando para gravar a reunião...",
"rec": "REC",
"saveLocalRecording": "Salvar o arquivo de gravação localmente (Beta)",
"serviceDescription": "Sua gravação será salva pelo serviço de gravação",
"serviceDescriptionCloud": "Gravação na nuvem",
"serviceDescriptionCloudInfo": "Reuniões gravadas são removidas automaticamente 24h após a hora da gravação.",
"serviceName": "Serviço de gravação",
"sessionAlreadyActive": "Esta sessão já está sendo gravada ou em live streaming",
"signIn": "Entrar",
"signOut": "Sair",
"surfaceError": "Por favor selecione a aba atual.",
"title": "Gravando",
"unavailable": "Oops! O {{serviceName}} está indisponível. Estamos trabalhando para resolver o problema. Por favor, tente mais tarde.",
"unavailableTitle": "Gravação indisponível",
"uploadToCloud": "Enviar para a nuvem"
},
"screenshareDisplayName": "Tela: {{name}}",
"sectionList": {
"pullToRefresh": "Puxe para atualizar"
},
"security": {
"about": "Voce pode adicionar $t(lockRoomPassword) a sua reunião. Participantes terão que fornecer $t(lockRoomPassword) antes de entrar na reunião.",
"about": "Você pode adicionar $t(lockRoomPassword) a sua reunião. Participantes terão que fornecer $t(lockRoomPassword) antes de entrar na reunião.",
"aboutReadOnly": "Moderadores podem adicionar $t(lockRoomPassword) à reunião. Participantes terão que fornecer $t(lockRoomPassword) antes de entrar na reunião.",
"insecureRoomNameWarning": "A sala é insegura. Participantes não desejados podem entrar na reunião. Considere adicionar seguança no botão segurança.",
"securityOptions": "Opções de segurança"
"insecureRoomNameWarningNative": "O nome da sala é não é seguro. Participantes não desejados podem ingressar na reunião. {{recommendAction}} Saiba mais sobre como proteger sua reunião ",
"insecureRoomNameWarningWeb": "O nome da sala é não é seguro. Participantes não desejados podem ingressar na reunião. {{recommendAction}} Saiba mais sobre como proteger sua reunião <a href=\"{{securityUrl}}\" rel=\"security\" target=\"_blank\">aqui</a>.",
"title": "Opções de segurança",
"unsafeRoomActions": {
"meeting": "Considere usar o botão de segurança para proteger sua reunião.",
"prejoin": "Considere usar um nome de reunião não-comum.",
"welcome": "Considere usar um nome de reunião não-comum ou selecione um das sugestões."
}
},
"settings": {
"audio": "Áudio",
"buttonLabel": "Configurações",
"calendar": {
"about": "A integração do calendário {{appName}} é usada para acessar com segurança o seu calendário para que ele possa ler os próximos eventos.",
"disconnect": "Desconectar",
@@ -824,25 +1004,32 @@
"incomingMessage": "Mensagem recebida",
"language": "Idioma",
"loggedIn": "Conectado como {{name}}",
"maxStageParticipants": "Número máximo de participantes que podem ser fixados no palco principal (EXPERIMENTAL)",
"microphones": "Microfones",
"moderator": "Moderador",
"moderatorOptions": "Opções de moderador",
"more": "Mais",
"name": "Nome",
"noDevice": "Nenhum",
"participantJoined": "Participante Entrou",
"participantLeft": "Participante Saiu",
"notifications": "Notificações",
"participantJoined": "Participante entrou",
"participantKnocking": "Participante entrou na sala de espera",
"participantLeft": "Participante saiu",
"playSounds": "Tocar sons",
"reactions": "Reações da reunião",
"sameAsSystem": "Igual ao sistema ({{label}})",
"selectAudioOutput": "Saída de áudio",
"selectCamera": "Câmera",
"selectMic": "Microfone",
"sounds": "Sons",
"selfView": "Auto-visualização",
"shortcuts": "Atalhos",
"speakers": "Alto-faltantes",
"startAudioMuted": "Todos iniciam mudos",
"startReactionsMuted": "Silenciar sons de reações para todos",
"startVideoMuted": "Todos iniciam ocultos",
"talkWhileMuted": "Falar mesmo silenciado",
"title": "Configurações"
"title": "Configurações",
"video": "Vídeo"
},
"settingsView": {
"advanced": "Avançado",
@@ -857,13 +1044,21 @@
"disableCrashReportingWarning": "Tem certeza eue quer desabilitar o aviso de falha? A opção será habilitada após reiniciar o app.",
"disableP2P": "Desativar modo ponto a ponto",
"displayName": "Nome de exibição",
"displayNamePlaceholderText": "Ex: João Silva",
"email": "Email",
"emailPlaceholderText": "email@exemplo.com.br",
"goTo": "Ir para",
"header": "Configurações",
"help": "Ajuda",
"links": "Links",
"privacy": "Privacidade",
"profileSection": "Perfil",
"serverURL": "URL do servidor",
"showAdvanced": "Mostrar configurações avançadas",
"startCarModeInLowBandwidthMode": "Iniciar modo carro em modo de banda baixa",
"startWithAudioMuted": "Iniciar sem áudio",
"startWithVideoMuted": "Iniciar sem vídeo",
"terms": "Termos",
"version": "Versão"
},
"share": {
@@ -872,13 +1067,21 @@
},
"speaker": "Alto-falantes",
"speakerStats": {
"angry": "Zangado",
"disgusted": "Com nojo",
"displayEmotions": "Exibir emoções",
"fearful": "Com medo",
"happy": "Feliz",
"hours": "{{count}}h",
"minutes": "{{count}}m",
"name": "Nome",
"neutral": "Neutro",
"sad": "Triste",
"search": "Busca",
"seconds": "{{count}}s",
"speakerStats": "Estatísticas do Apresentador",
"speakerTime": "Tempo do Apresentador"
"speakerStats": "Estatísticas do apresentador",
"speakerTime": "Tempo do apresentador",
"surprised": "Surpreso"
},
"startupoverlay": {
"genericTitle": "A reunião precisa usar seu microfone e câmera.",
@@ -890,6 +1093,10 @@
"text": "Pressione o botão <i>Reentrar</i> para reconectar.",
"title": "Sua chamada de vídeo foi interrompida, porque seu computador foi dormir."
},
"termsView": {
"title": "Termos"
},
"toggleTopPanelLabel": "Alternar painel superior",
"toolbar": {
"Settings": "Configurações",
"accessibilityLabel": {
@@ -897,60 +1104,89 @@
"audioOnly": "Alternar para apenas áudio",
"audioRoute": "Selecionar o dispositivo de som",
"boo": "Vaia",
"breakoutRoom": "Entrar/sair da sala de apoio",
"callQuality": "Gerenciar qualidade do vídeo",
"carmode": "Modo carro",
"cc": "Alternar legendas",
"chat": "Alternar para janela de chat",
"clap": "Aplauso",
"closeChat": "Fechar chat",
"closeMoreActions": "Fechar o menu de mais ações",
"closeParticipantsPane": "Fechar painel de participantes",
"collapse": "Recolher",
"document": "Alternar para documento compartilhado",
"documentClose": "Fechar documento compartilhado",
"documentOpen": "Abrir documento compartilhado",
"download": "Baixe nossos aplicativos",
"embedMeeting": "Reunião em formato compacto",
"endConference": "Terminar para todos",
"enterFullScreen": "Ver em tela-cheia",
"enterTileView": "Entrar na visualização em blocos",
"exitFullScreen": "Sair da tela-cheia",
"exitTileView": "Sair da visualização em blocos",
"expand": "Expandir",
"feedback": "Deixar feedback",
"fullScreen": "Alternar para tela cheia",
"giphy": "Alternar menu do GIPHY",
"grantModerator": "Atribuir Moderador",
"hangup": "Sair da chamada",
"heading": "Barra de ferramentas",
"help": "Ajuda",
"hideWhiteboard": "Ocultar quadro branco",
"invite": "Convidar pessoas",
"kick": "Remover participante",
"laugh": "Risada",
"leaveConference": "Sair da reunião",
"like": "Gostei",
"linkToSalesforce": "Link com o Salesforce",
"lobbyButton": "Habilitar/desabilitar sala de espera",
"localRecording": "Alternar controles de gravação local",
"lockRoom": "Ativar/desativar senha de reunião",
"lowerHand": "Abaixar a mão",
"moreActions": "Alternar mais menu de ações",
"moreActionsMenu": "Menu de mais ações",
"moreOptions": "Mostrar mais opções",
"mute": "Alternar mudo do áudio",
"muteEveryone": "Silenciar todos",
"muteEveryoneElse": "Silenciar todos os demais",
"muteEveryoneElsesVideo": "Desativar a câmera de todos os demais",
"muteEveryonesVideo": "Desativar a câmera de todos",
"muteEveryoneElsesVideoStream": "Parar o vídeo de todos os outros",
"muteEveryonesVideoStream": "Parar o vídeo de todos",
"muteGUMPending": "Conectando seu microfone",
"noiseSuppression": "Supressão de ruído",
"openChat": "Abrir chat",
"participants": "Participantes",
"pip": "Alternar modo Picture-in-Picture",
"privateMessage": "Enviar mensagem privada",
"profile": "Editar seu perfil",
"raiseHand": "Alternar levantar a mão",
"reactions": "Reações",
"reactionsMenu": "Abrir / fechar menu de reações",
"recording": "Alternar gravação",
"remoteMute": "Silenciar participante",
"remoteVideoMute": "Desativar a câmera do participante",
"security": "Opções de segurança",
"selectBackground": "Selecionar Fundo",
"selfView": "Alternar auto-visualização",
"shareRoom": "Convidar alguém",
"shareYourScreen": "Alternar compartilhamento de tela",
"shareaudio": "Compartilhar áudio",
"sharedvideo": "Alternar compartilhamento de vídeo",
"shortcuts": "Alternar atalhos",
"show": "Mostrar no palco",
"showWhiteboard": "Exibir quadro branco",
"silence": "Silenciar",
"speakerStats": "Alternar estatísticas do apresentador",
"stopScreenSharing": "Parar de compartilhar sua tela",
"stopSharedVideo": "Parar vídeo",
"surprised": "Surpresa",
"tileView": "Alternar visualização em blocos",
"toggleCamera": "Alternar câmera",
"toggleFilmstrip": "Alterar tira de filme",
"unmute": "Ativar som",
"videoblur": "Alternar desfoque de vídeo",
"videomute": "Alternar mudo do vídeo"
"videomute": "Alternar mudo do vídeo",
"videomuteGUMPending": "Conectando sua câmera",
"videounmute": "Ativar câmera"
},
"addPeople": "Adicionar pessoas à sua chamada",
"audioOnlyOff": "Desabilitar modo de largura de banda baixa",
@@ -963,23 +1199,33 @@
"chat": "Abrir ou fechar o bate-papo",
"clap": "Aplauso",
"closeChat": "Fechar chat",
"closeParticipantsPane": "Fechar painel de participantes",
"closeReactionsMenu": "Fechar menu de reações",
"disableNoiseSuppression": "Desabilitar supressão de ruído",
"disableReactionSounds": "Você pode desabilitar os sons de reação para esta reunião",
"documentClose": "Fechar documento compartilhado",
"documentOpen": "Abrir documento compartilhado",
"download": "Baixe nossos aplicativos",
"e2ee": "Encriptação ponto a ponto",
"embedMeeting": "Reunião em formato compacto",
"enableNoiseSuppression": "Habilitar supressão de ruído",
"endConference": "Terminar para todos",
"enterFullScreen": "Ver em tela cheia",
"enterTileView": "Entrar em exibição de bloco",
"exitFullScreen": "Sair da tela cheia",
"exitTileView": "Sair de exibição de bloco",
"feedback": "Deixar feedback",
"giphy": "Alternar menu do GIPHY",
"hangup": "Sair",
"help": "Ajuda",
"hideWhiteboard": "Ocultar quadro branco",
"invite": "Convidar pessoas",
"joinBreakoutRoom": "Ingressar na sala de apoio",
"laugh": "Risada",
"leaveBreakoutRoom": "Sair da sala de apoio",
"leaveConference": "Sair da reunião",
"like": "Gostei",
"linkToSalesforce": "Link com o Salesforce",
"lobbyButtonDisable": "Desabilitar sala de espera",
"lobbyButtonEnable": "Habilitar sala de espera",
"login": "Iniciar sessão",
@@ -990,11 +1236,13 @@
"mute": "Mudo / Não mudo",
"muteEveryone": "Silenciar todos",
"muteEveryonesVideo": "Desativar a câmera de todos",
"muteGUMPending": "Conectando seu microfone",
"noAudioSignalDesc": "Se você não o desativou propositalmente das configurações do sistema ou do hardware, considere trocar o dispositivo.",
"noAudioSignalDescSuggestion": "Se você não o desativou propositalmente das configurações do sistema ou do hardware, considere trocar para o dispositivo sugerido.",
"noAudioSignalDialInDesc": "Você também pode discar usando:",
"noAudioSignalDialInLinkDesc": "Discar números",
"noAudioSignalTitle": "Não há entrada de áudio vindo do seu microfone!",
"noiseSuppression": "Supressão de ruído",
"noisyAudioInputDesc": "Parece que o microfone está fazendo barulho, considere silenciar ou alterar o dispositivo.",
"noisyAudioInputTitle": "O seu microfone parece estar barulhento!",
"openChat": "Abrir chat",
@@ -1011,12 +1259,14 @@
"reactionLike": "Enviar reação de gostei",
"reactionSilence": "Enviar reação de silêncio",
"reactionSurprised": "Enviar reação de surpresa",
"reactions": "Reações",
"security": "Opções de segurança",
"selectBackground": "Selecionar fundo",
"shareRoom": "Convidar alguém",
"shareaudio": "Compartilhar áudio",
"sharedvideo": "Compartilhar um vídeo",
"shortcuts": "Ver atalhos",
"showWhiteboard": "Exibir quadro branco",
"silence": "Silêncio",
"speakerStats": "Estatísticas do Apresentador",
"startScreenSharing": "Iniciar compart. de tela",
@@ -1029,8 +1279,11 @@
"talkWhileMutedPopup": "Tentando falar? Você está em mudo.",
"tileViewToggle": "Alternar visualização em blocos",
"toggleCamera": "Alternar câmera",
"unmute": "Ativar som",
"videoSettings": "Configurações de vídeo",
"videomute": "Iniciar ou parar a câmera"
"videomute": "Iniciar ou parar a câmera",
"videomuteGUMPending": "Conectando sua câmera",
"videounmute": "Ativar câmera"
},
"transcribing": {
"ccButtonTooltip": "Iniciar/parar legendas",
@@ -1040,10 +1293,15 @@
"labelToolTip": "A reunião esta sendo transcrita",
"off": "Transcrição parada",
"pending": "Preparando a transcrição da reunião...",
"sourceLanguageDesc": "No momento o idioma da reunião está definido para <b>{{sourceLanguage}}</b>. <br/> Você pode alterar-lo ",
"sourceLanguageHere": "aqui",
"start": "Exibir legendas",
"stop": "Não exibir legendas",
"subtitles": "Legendas",
"subtitlesOff": "Desativadas",
"tr": "TR"
},
"unpinParticipant": "{{participantName}} - Desafixar",
"userMedia": {
"androidGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
"chromeGrantPermissions": "Selecione <b><i>Permitir</i></b> quando seu navegador perguntar pelas permissões.",
@@ -1067,20 +1325,26 @@
"pending": "{{displayName}} foi convidado"
},
"videoStatus": {
"adjustFor": "Ajustar para:",
"audioOnly": "AUD",
"audioOnlyExpanded": "Você está em modo de banda baixa. Neste modo, se recebe somente áudio e compartilhamento de tela.",
"bestPerformance": "Melhor performance",
"callQuality": "Qualidade de vídeo",
"hd": "HD",
"hdTooltip": "Ver vídeo em alta definição",
"highDefinition": "Alta definição (HD)",
"highestQuality": "Alta qualidade",
"labelTooiltipNoVideo": "Sem vídeo",
"labelTooltipAudioOnly": "Modo de largura de banda baixa habilitada",
"ld": "LD",
"ldTooltip": "Ver vídeo em baixa definição",
"lowDefinition": "Baixa definição (LD)",
"performanceSettings": "Configurações de performance",
"recording": "Gravação em andamento",
"sd": "SD",
"sdTooltip": "Ver vídeo em definição padrão",
"standardDefinition": "Definição padrão"
"standardDefinition": "Definição padrão",
"streaming": "Streaming em andamento"
},
"videothumbnail": {
"connectionInfo": "Informações da Conexão",
@@ -1090,12 +1354,19 @@
"domuteVideoOfOthers": "Desativar a câmera de todos os demais",
"flip": "Inverter",
"grantModerator": "Atribuir Moderador",
"hideSelfView": "Ocultar auto-visualização",
"kick": "Remover",
"mirrorVideo": "Espelhar meu vídeo",
"moderator": "Moderador",
"mute": "Participante está mudo",
"muted": "Mudo",
"pinToStage": "Fixar no palco",
"remoteControl": "Controle remoto",
"screenSharing": "O participante está compartilhando sua tela",
"show": "Mostrar no palco",
"showSelfView": "Exibir auto-visualização",
"unpinFromStage": "Desafixar",
"verify": "Verificar participante",
"videoMuted": "Câmera desativada",
"videomute": "O participante parou a câmera"
},
@@ -1120,7 +1391,16 @@
"slightBlur": "Desfoque suave",
"title": "Fundos virtuais",
"uploadedImage": "Imagem enviada {{index}}",
"webAssemblyWarning": "Não há suporte para WebAssembly"
"webAssemblyWarning": "Não há suporte para WebAssembly",
"webAssemblyWarningDescription": "WebAssembly desativado ou não suportado neste navegador"
},
"visitors": {
"chatIndicator": "(visitante)",
"labelTooltip": "Número de visitantes: {{count}}",
"notification": {
"description": "Para participar levante a mão",
"title": "Você é um visitante na reunião"
}
},
"volumeSlider": "Controle de volume",
"welcomepage": {
@@ -1154,6 +1434,7 @@
"microsoftLogo": "Logo da Microsoft",
"policyLogo": "Logo da Política de Privacidade"
},
"meetingsAccessibilityLabel": "Reuniões",
"mobileDownLoadLinkAndroid": "Baixar aplicativo móvel para Android",
"mobileDownLoadLinkFDroid": "Baixar aplicativo móvel para F-Droid",
"mobileDownLoadLinkIos": "Baixar aplicativo móvel para iOS",
@@ -1162,13 +1443,21 @@
"recentList": "Recente",
"recentListDelete": "Remover",
"recentListEmpty": "Sua lista recente está vazia. As reuniões que você realizar serão exibidas aqui.",
"recentMeetings": "Suas reuniões recentes",
"reducedUIText": "Bem-vindo ao {{app}}!",
"roomNameAllowedChars": "Nome da reunião não deve conter qualquer um destes caracteres: ?. &, :, ', \", %, #.",
"roomname": "Digite o nome da sala",
"roomnameHint": "Digite o nome ou a URL da sala que você deseja entrar. Você pode digitar um nome, e apenas deixe para as pessoas que você quer se reunir digitem o mesmo nome.",
"sendFeedback": "Enviar comentários",
"settings": "Configurações",
"startMeeting": "Iniciar reunião",
"terms": "Termos",
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas"
"title": "Videoconferências mais seguras, flexíveis e totalmente gratuitas",
"upcomingMeetings": "Suas próximas reuniões"
},
"whiteboard": {
"accessibilityLabel": {
"heading": "Quadro branco"
}
}
}

View File

@@ -63,6 +63,8 @@
"leaveBreakoutRoom": "Leave breakout room",
"more": "More",
"remove": "Remove",
"rename": "Rename",
"renameBreakoutRoom": "Rename breakout room",
"sendToBreakoutRoom": "Send participant to:"
},
"defaultName": "Breakout room #{{index}}",
@@ -386,6 +388,8 @@
"removePassword": "Remove $t(lockRoomPassword)",
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
"removeSharedVideoTitle": "Remove shared video",
"renameBreakoutRoomLabel": "Room name",
"renameBreakoutRoomTitle": "Rename breakout room",
"reservationError": "Reservation system error",
"reservationErrorMsg": "Error code: {{code}}, message: {{msg}}",
"retry": "Retry",
@@ -743,7 +747,6 @@
"newDeviceCameraTitle": "New camera detected",
"noiseSuppressionDesktopAudioDescription": "Noise suppression can't be enabled while sharing desktop audio, please disable it and try again.",
"noiseSuppressionFailedTitle": "Failed to start noise suppression",
"noiseSuppressionNoTrackDescription": "Please unmute your microphone first.",
"noiseSuppressionStereoDescription": "Stereo audio noise suppression is not currently supported.",
"oldElectronClientDescription1": "You appear to be using an old version of the Jitsi Meet client which has known security vulnerabilities. Please make sure you update to our ",
"oldElectronClientDescription2": "latest build",

View File

@@ -2002,14 +2002,16 @@ class API {
* Notify external application ( if API is enabled) that a participant menu button was clicked.
*
* @param {string} key - The key of the participant menu button.
* @param {string} participantId - The ID of the participant for with the participant menu button was clicked.
* @param {string} participantId - The ID of the participant for whom the participant menu button was clicked.
* @param {boolean} preventExecution - Whether execution of the button click was prevented or not.
* @returns {void}
*/
notifyParticipantMenuButtonClicked(key, participantId) {
notifyParticipantMenuButtonClicked(key, participantId, preventExecution) {
this._sendEvent({
name: 'participant-menu-button-clicked',
key,
participantId
participantId,
preventExecution
});
}

View File

@@ -8,8 +8,15 @@ import { parseURLParams } from '../../react/features/base/util/parseURLParams';
/**
* JitsiMeetExternalAPI id - unique for a webpage.
* TODO: This shouldn't be computed here.
*/
export const API_ID = parseURLParams(window.location).jitsi_meet_external_api_id;
let _apiID;
try {
_apiID = parseURLParams(window.location).jitsi_meet_external_api_id;
} catch (_) { /* Ignore. */ }
export const API_ID = _apiID;
/**
* The payload name for the datachannel/endpoint text message event.

82
package-lock.json generated
View File

@@ -18,7 +18,7 @@
"@giphy/react-native-sdk": "2.3.0",
"@hapi/bourne": "2.0.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.0.5",
"@jitsi/js-utils": "2.0.6",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.5.1",
@@ -60,7 +60,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -83,7 +83,7 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.5.0",
"react-native-pager-view": "5.4.9",
@@ -3110,9 +3110,9 @@
}
},
"node_modules/@jitsi/js-utils": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.5.tgz",
"integrity": "sha512-Aa7lt/sGsDymWnKJtM1RePmR2b2J5TwY3QLv5iOmzMDYR+5RE0NyYc/vKW51JeatDVSkj+LT7kpUDvtJua0rmQ==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.6.tgz",
"integrity": "sha512-Z5cy1d8JmymAK5O0AOtNQFDqbKictcnafFzDTRxYK7Dcf62GSVlxoHizAhoUowilb+HcZC0W3YA3+NqLtMM//w==",
"dependencies": {
"bowser": "2.7.0",
"js-md5": "0.7.3"
@@ -5526,6 +5526,11 @@
"yarn": ">= 1.3.2"
}
},
"node_modules/@testrtc/watchrtc-sdk": {
"version": "1.36.3",
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -7109,6 +7114,11 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"node_modules/async-es": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
},
"node_modules/async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@@ -12760,8 +12770,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"integrity": "sha512-kTHkhQiuLn28wrDhzcgDwJTJ106bgWMgARxgVZpFM1FyRjgJt2e9pn6mZtjEKT5FZeUcUtpy9cDuU6+yLusZEw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -12769,7 +12779,8 @@
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "3.2.3",
"@testrtc/watchrtc-sdk": "1.36.3",
"async-es": "3.2.4",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"jquery": "3.6.1",
@@ -15807,12 +15818,12 @@
"resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz",
"integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g=="
},
"node_modules/react-native-immersive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",
"integrity": "sha512-9TL05nTHN/x9sN1wbUlBoGyzH4NCuZ/7WEEUp5CvOoKuUABvdYosov0O0SAMbm/5J913RRoy98VB6tGNi9lRSw==",
"node_modules/react-native-immersive-mode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.1.tgz",
"integrity": "sha512-2wlL7VIHl4rr4gwgnUp9K1UvsN7J5VCGqoAvBWQXvB4xn7XaoDEl6z9vqaqOiEdC6aAh2d/7zqcJz+dfcR2ELw==",
"peerDependencies": {
"react-native": ">=0.47.0"
"react-native": ">=0.60.5"
}
},
"node_modules/react-native-keep-awake": {
@@ -19717,9 +19728,9 @@
"integrity": "sha512-rCPf3AakAAgvapnbYVvG2bQyI3g6EDbPpjDJ72fdAu+XTzB1qvX4ZC6OnZ0I2+thaspjTb+8KwdyhdBl8Lt/QA=="
},
"node_modules/word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true,
"engines": {
"node": ">=0.10.0"
@@ -22020,9 +22031,9 @@
"integrity": "sha512-iK7p7i6qJFOkjTVZhWDvurDW1u+eMoOhAVgpab9CZEqCTX+W4Ih4AOPrUpf+mjaAHK5XqmQZSc5nVEpMg+xIGQ=="
},
"@jitsi/js-utils": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.5.tgz",
"integrity": "sha512-Aa7lt/sGsDymWnKJtM1RePmR2b2J5TwY3QLv5iOmzMDYR+5RE0NyYc/vKW51JeatDVSkj+LT7kpUDvtJua0rmQ==",
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-2.0.6.tgz",
"integrity": "sha512-Z5cy1d8JmymAK5O0AOtNQFDqbKictcnafFzDTRxYK7Dcf62GSVlxoHizAhoUowilb+HcZC0W3YA3+NqLtMM//w==",
"requires": {
"bowser": "2.7.0",
"js-md5": "0.7.3"
@@ -23720,6 +23731,11 @@
"seedrandom": "2.4.3"
}
},
"@testrtc/watchrtc-sdk": {
"version": "1.36.3",
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
},
"@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -24989,6 +25005,11 @@
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"async-es": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@@ -29268,14 +29289,15 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"integrity": "sha512-kTHkhQiuLn28wrDhzcgDwJTJ106bgWMgARxgVZpFM1FyRjgJt2e9pn6mZtjEKT5FZeUcUtpy9cDuU6+yLusZEw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "3.2.3",
"@testrtc/watchrtc-sdk": "1.36.3",
"async-es": "3.2.4",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"jquery": "3.6.1",
@@ -31577,10 +31599,10 @@
"resolved": "https://registry.npmjs.org/react-native-gradle-plugin/-/react-native-gradle-plugin-0.0.7.tgz",
"integrity": "sha512-+4JpbIx42zGTONhBTIXSyfyHICHC29VTvhkkoUOJAh/XHPEixpuBduYgf6Y4y9wsN1ARlQhBBoptTvXvAFQf5g=="
},
"react-native-immersive": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-immersive/-/react-native-immersive-2.0.0.tgz",
"integrity": "sha512-9TL05nTHN/x9sN1wbUlBoGyzH4NCuZ/7WEEUp5CvOoKuUABvdYosov0O0SAMbm/5J913RRoy98VB6tGNi9lRSw=="
"react-native-immersive-mode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/react-native-immersive-mode/-/react-native-immersive-mode-2.0.1.tgz",
"integrity": "sha512-2wlL7VIHl4rr4gwgnUp9K1UvsN7J5VCGqoAvBWQXvB4xn7XaoDEl6z9vqaqOiEdC6aAh2d/7zqcJz+dfcR2ELw=="
},
"react-native-keep-awake": {
"version": "4.0.0",
@@ -34415,9 +34437,9 @@
"integrity": "sha512-rCPf3AakAAgvapnbYVvG2bQyI3g6EDbPpjDJ72fdAu+XTzB1qvX4ZC6OnZ0I2+thaspjTb+8KwdyhdBl8Lt/QA=="
},
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
"dev": true
},
"wrap-ansi": {

View File

@@ -2,6 +2,7 @@
"name": "jitsi-meet",
"version": "0.0.0",
"description": "A sample app for the Jitsi Videobridge",
"private": true,
"repository": {
"type": "git",
"url": "git://github.com/jitsi/jitsi-meet"
@@ -23,7 +24,7 @@
"@giphy/react-native-sdk": "2.3.0",
"@hapi/bourne": "2.0.0",
"@jitsi/excalidraw": "https://github.com/jitsi/excalidraw/releases/download/v0.0.14/jitsi-excalidraw-0.0.14.tgz",
"@jitsi/js-utils": "2.0.5",
"@jitsi/js-utils": "2.0.6",
"@jitsi/logger": "2.0.0",
"@jitsi/rnnoise-wasm": "0.1.0",
"@jitsi/rtcstats": "9.5.1",
@@ -65,7 +66,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -88,7 +89,7 @@
"react-native-dialog": "https://github.com/jitsi/react-native-dialog/releases/download/v9.2.2-jitsi.1/react-native-dialog-9.2.2.tgz",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "1.5.0",
"react-native-pager-view": "5.4.9",

View File

@@ -1,19 +0,0 @@
diff --git a/node_modules/react-native-immersive/index.js b/node_modules/react-native-immersive/index.js
index 55dab57..110260b 100644
--- a/node_modules/react-native-immersive/index.js
+++ b/node_modules/react-native-immersive/index.js
@@ -18,7 +18,13 @@ const Immersive = Platform.OS === 'android' ? {
isListenerEnabled = true
RNImmersive.addImmersiveListener()
},
- removeImmersiveListener: (listener) => DeviceEventEmitter.removeListener('@@IMMERSIVE_STATE_CHANGED', listener)
+ removeImmersiveListener: (listener) => {
+ const immersiveListener = DeviceEventEmitter.addListener('@@IMMERSIVE_STATE_CHANGED', listener);
+
+ return () => {
+ immersiveListener.remove();
+ }
+ }
} : {
on: unSupportedError,
off: unSupportedError,

View File

@@ -2,20 +2,23 @@
## Installation
Inside your project, run `npm i @jitsi/react-native-sdk`.<br/><br/>Additionally, if not already installed, some dependencies will need to be added.
Inside your project, run;
```console
npm i @jitsi/react-native-sdk
```
<br/><br/>Additionally, if not already installed, some dependencies will need to be added.
This can be done by running the following script:
```
```console
node node_modules/@jitsi/react-native-sdk/update_dependencies.js
```
This will check and update all your dependencies.<br/><br/>
[comment]: # (These deps definitely need to be added manually, more could be neccesary)
Because of SVG use in react native, you need to update metro.config your project's file:
```
```javascript
const { getDefaultConfig } = require('metro-config');
module.exports = (async () => {
@@ -56,22 +59,31 @@ module.exports = (async () => {
- Audio
- Voice over IP
- Background fetch
- Add Copy Sounds step:
1. Open XCode, go to Build Phases and add this step and the script below.
Run;
```console
cd ios && pod install && cd ..
```
SOUNDS_DIR="${PROJECT_DIR}/../node_modules/@jitsi/react-native-sdk/sounds"
cp $SOUNDS_DIR/* ${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/
```
Run `cd ios && pod install && cd ..`
### Android
- In your build.gradle have at least `minSdkVersion = 24`
- In `android/app/src/debug/AndroidManifest.xml` and `android/app/src/main/AndroidManifest.xml`, under the `</application>` tag, include
```
```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
```
If you want to test all the steps before applying them to your app, you can check our React Native SDK sample app here:
https://github.com/jitsi/jitsi-meet-sdk-samples/tree/master/react-native
### Using JWT tokens
- If you are planning to use tokens or another domain you can do that by updating the following props, as shown below.
- For example:
```javascript
<JitsiMeeting
room={'ThisIsNotATestRoomName'}
serverURL={'https://meet.jit.si/'}
token={'dkhalhfajhflahlfaahalhfahfsl'} />
```

View File

@@ -138,3 +138,19 @@ if (isNewArchitectureEnabled()) {
codegenJavaPackageName = "org.jitsi.meet.sdk"
}
}
// Copy sounds to assets directory
android.libraryVariants.all { def variant ->
def mergeAssetsTask = variant.mergeAssetsProvider.get()
def mergeResourcesTask = variant.mergeResourcesProvider.get()
mergeAssetsTask.doLast {
def assetsDir = mergeAssetsTask.outputDir.get()
def soundsDir = "${projectDir}/../sounds"
copy {
from("${soundsDir}")
include("*.wav")
include("*.mp3")
into("${assetsDir}/sounds")
}
}
}

View File

@@ -1,47 +1,67 @@
/* eslint-disable lines-around-comment, no-undef, no-unused-vars */
import 'react-native-gesture-handler';
// Apply all necessary polyfills as early as possible
// to make sure anything imported henceforth sees them.
import 'react-native-get-random-values';
import './react/features/mobile/polyfills';
// NB: This import must always come first.
import './react/bootstrap.native';
// @ts-ignore
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { View } from 'react-native';
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState
} from 'react';
import { View, ViewStyle } from 'react-native';
import { appNavigate } from './react/features/app/actions.native';
import { App } from './react/features/app/components/App.native';
import { setAudioMuted, setVideoMuted } from './react/features/base/media/actions';
// @ts-ignore
import JitsiThemePaperProvider from './react/features/base/ui/components/JitsiThemeProvider.native';
interface IEventListeners {
onConferenceBlurred?: Function;
onConferenceFocused?: Function;
onConferenceJoined?: Function;
onConferenceLeft?: Function;
onConferenceWillJoin?: Function;
onEnterPictureInPicture?: Function;
onParticipantJoined?: Function;
onReadyToClose?: Function;
}
interface IUserInfo {
avatarURL: string;
displayName: string;
email: string;
}
interface IAppProps {
flags: [];
meetingOptions: {
domain: string;
roomName: string;
onReadyToClose?: Function;
onConferenceJoined?: Function;
onConferenceWillJoin?: Function;
onConferenceLeft?: Function;
onParticipantJoined?: Function;
settings?: {
startWithAudioMuted?: boolean;
startAudioOnly?: boolean;
startWithVideoMuted?: boolean;
}
};
config: object;
eventListeners?: IEventListeners;
flags?: object;
room: string;
serverURL?: string;
style?: Object;
token?: string;
userInfo?: IUserInfo;
}
/**
* Main React Native SDK component that displays a Jitsi Meet conference and gets all required params as props
*/
export const JitsiMeeting = forwardRef(({ flags, meetingOptions, style }: IAppProps, ref) => {
export const JitsiMeeting = forwardRef((props: IAppProps, ref) => {
const [ appProps, setAppProps ] = useState({});
const app = useRef(null);
const {
config,
eventListeners,
flags,
room,
serverURL,
style,
token,
userInfo
} = props;
// eslint-disable-next-line arrow-body-style
useImperativeHandle(ref, () => ({
@@ -64,33 +84,63 @@ export const JitsiMeeting = forwardRef(({ flags, meetingOptions, style }: IAppPr
useEffect(
() => {
const url = `${meetingOptions.domain}/${meetingOptions.roomName}`;
const urlObj = {
config,
jwt: token
};
let urlProps;
if (room.includes('://')) {
urlProps = {
...urlObj,
url: room
};
} else {
urlProps = {
...urlObj,
room,
serverURL
};
}
setAppProps({
'url': {
url,
config: meetingOptions.settings
},
'flags': flags,
'rnSdkHandlers': {
onReadyToClose: meetingOptions.onReadyToClose,
onConferenceJoined: meetingOptions.onConferenceJoined,
onConferenceWillJoin: meetingOptions.onConferenceWillJoin,
onConferenceLeft: meetingOptions.onConferenceLeft,
onParticipantJoined: meetingOptions.onParticipantJoined
onConferenceBlurred: eventListeners?.onConferenceBlurred,
onConferenceFocused: eventListeners?.onConferenceFocused,
onConferenceJoined: eventListeners?.onConferenceJoined,
onConferenceWillJoin: eventListeners?.onConferenceWillJoin,
onConferenceLeft: eventListeners?.onConferenceLeft,
onEnterPictureInPicture: eventListeners?.onEnterPictureInPicture,
onParticipantJoined: eventListeners?.onParticipantJoined,
onReadyToClose: eventListeners?.onReadyToClose
},
'flags': { ...flags }
'url': urlProps,
'userInfo': userInfo
});
}, []
);
// eslint-disable-next-line arrow-body-style
useLayoutEffect(() => {
/**
* When you close the component you need to reset it.
* In some cases it needs to be added as the parent component may have been destroyed.
* Without this change the call remains active without having the jitsi screen.
*/
return () => {
const dispatch = app.current?.state?.store?.dispatch;
dispatch && dispatch(appNavigate(undefined));
};
}, []);
return (
<View style = { style }>
<JitsiThemePaperProvider>
{/* @ts-ignore */}
<App
{ ...appProps }
ref = { app } />
</JitsiThemePaperProvider>
<View style = { style as ViewStyle }>
<App
{ ...appProps }
ref = { app } />
</View>
);
});

View File

@@ -22,4 +22,14 @@ Pod::Spec.new do |s|
s.dependency 'react-native-webrtc'
s.dependency 'ObjectiveDropboxOfficial', '6.2.3'
s.script_phase = {
:name => 'Copy Sound Files',
:script => '
SOURCE_PATH="${PODS_TARGET_SRCROOT}/sounds"
TARGET_PATH="$(dirname ${CONFIGURATION_BUILD_DIR})"
PROJECT_NAME=$(basename $(dirname $(dirname ${PROJECT_DIR}))).app
cp -R "${PODS_TARGET_SRCROOT}/sounds/" "${TARGET_PATH}/${PROJECT_NAME}"
',
}
end

View File

@@ -1,12 +1,12 @@
{
"name": "@jitsi/react-native-sdk",
"version": "0.1.0",
"version": "0.2.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@jitsi/react-native-sdk",
"version": "0.1.0",
"version": "0.2.2",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -28,7 +28,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -68,7 +68,7 @@
"react-native-device-info": "8.4.8",
"react-native-gesture-handler": "2.9.0",
"react-native-get-random-values": "1.7.2",
"react-native-immersive": "2.0.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-orientation-locker": "https://git@github.com/wonday/react-native-orientation-locker#f483520ea6b64b97002374a9e9f053a5299a062a",
"react-native-pager-view": "5.4.9",
@@ -823,6 +823,11 @@
"@svgr/core": "*"
}
},
"node_modules/@testrtc/watchrtc-sdk": {
"version": "1.36.3",
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
},
"node_modules/@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -960,10 +965,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
"node_modules/async-es": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
},
"node_modules/at-least-node": {
"version": "1.0.0",
@@ -2443,8 +2448,8 @@
},
"node_modules/lib-jitsi-meet": {
"version": "0.0.0",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"resolved": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"integrity": "sha512-kTHkhQiuLn28wrDhzcgDwJTJ106bgWMgARxgVZpFM1FyRjgJt2e9pn6mZtjEKT5FZeUcUtpy9cDuU6+yLusZEw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -2452,7 +2457,8 @@
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "3.2.3",
"@testrtc/watchrtc-sdk": "1.36.3",
"async-es": "3.2.4",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"jquery": "3.6.1",
@@ -4200,6 +4206,11 @@
"svgo": "^2.8.0"
}
},
"@testrtc/watchrtc-sdk": {
"version": "1.36.3",
"resolved": "https://registry.npmjs.org/@testrtc/watchrtc-sdk/-/watchrtc-sdk-1.36.3.tgz",
"integrity": "sha512-JtcTvvh20t553n8q5ZHpWQeSUTENkQrZbGNvQ05jD8SA2V5PHBok/7my1ZDuA44sgT0y1OfUA8VT7dhcs0VxWg=="
},
"@trysound/sax": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
@@ -4318,10 +4329,10 @@
"is-string": "^1.0.7"
}
},
"async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
"async-es": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/async-es/-/async-es-3.2.4.tgz",
"integrity": "sha512-GFHAAfmW7GzEiHFR6DvE5WMm6+js9pb+RLm+m1UpsyMX+I4j/R4QVw2Te664q+fvDVOz7Y0bORPDNvQS7BJ3Hw=="
},
"at-least-node": {
"version": "1.0.0",
@@ -5349,14 +5360,15 @@
}
},
"lib-jitsi-meet": {
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"integrity": "sha512-kTBN3NlI3RLxeQeq5uppU557kJPQWwFMq1NweGr0CH8TCJdnKt30Lqx/X+DGP7Js6286JDmpg4EWZa9wW7lLXQ==",
"version": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"integrity": "sha512-kTHkhQiuLn28wrDhzcgDwJTJ106bgWMgARxgVZpFM1FyRjgJt2e9pn6mZtjEKT5FZeUcUtpy9cDuU6+yLusZEw==",
"requires": {
"@jitsi/js-utils": "2.0.0",
"@jitsi/logger": "2.0.0",
"@jitsi/sdp-interop": "git+https://github.com/jitsi/sdp-interop#3d49eb4aa26863a3f8d32d7581cdb4321244266b",
"@jitsi/sdp-simulcast": "0.4.0",
"async": "3.2.3",
"@testrtc/watchrtc-sdk": "1.36.3",
"async-es": "3.2.4",
"base64-js": "1.3.1",
"current-executing-script": "0.1.3",
"jquery": "3.6.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@jitsi/react-native-sdk",
"version": "0.1.0",
"version": "0.0.0",
"description": "React Native SDK for Jitsi Meet.",
"main": "index.tsx",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"js-md5": "0.6.1",
"js-sha512": "0.8.0",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1659.0.0+5d322ea5/lib-jitsi-meet.tgz",
"lib-jitsi-meet": "https://github.com/jitsi/lib-jitsi-meet/releases/download/v1670.0.0+10ebc843/lib-jitsi-meet.tgz",
"lodash": "4.17.21",
"moment": "2.29.4",
"moment-duration-format": "2.2.2",
@@ -69,7 +69,7 @@
"react-native-device-info": "8.4.8",
"react-native-get-random-values": "1.7.2",
"react-native-gesture-handler": "2.9.0",
"react-native-immersive": "2.0.0",
"react-native-immersive-mode": "2.0.1",
"react-native-keep-awake": "4.0.0",
"react-native-pager-view": "5.4.9",
"react-native-paper": "4.11.1",
@@ -98,4 +98,4 @@
"keywords": [
"react-native"
]
}
}

7
react/bootstrap.native.js vendored Normal file
View File

@@ -0,0 +1,7 @@
// https://github.com/software-mansion/react-native-gesture-handler/issues/320#issuecomment-443815828
import 'react-native-gesture-handler';
// Apply all necessary polyfills as early as possible to make sure anything imported henceforth
// sees them.
import 'react-native-get-random-values';
import './features/mobile/polyfills';

View File

@@ -1,5 +1,6 @@
import React, { ComponentType } from 'react';
import { NativeModules, Platform, StyleSheet, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import SplashScreen from 'react-native-splash-screen';
@@ -11,6 +12,7 @@ import { getFeatureFlag } from '../../base/flags/functions';
import { clientResized, setSafeAreaInsets } from '../../base/responsive-ui/actions';
import DimensionsDetector from '../../base/responsive-ui/components/DimensionsDetector.native';
import { updateSettings } from '../../base/settings/actions';
import JitsiThemePaperProvider from '../../base/ui/components/JitsiThemeProvider.native';
import { _getRouteToRender } from '../getRouteToRender.native';
import logger from '../logger';
@@ -36,7 +38,7 @@ interface IProps extends AbstractAppProps {
/**
* An object with the feature flags.
*/
flags: Object;
flags: any;
/**
* An object with user information (display name, email, avatar URL).
@@ -88,6 +90,20 @@ export class App extends AbstractApp<IProps> {
logger.info(`Loaded SDK ${AppInfo.sdkVersion}${liteTxt}`);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<JitsiThemePaperProvider>
{ super.render() }
</JitsiThemePaperProvider>
);
}
/**
* Initializes feature flags and updates settings.
*
@@ -95,7 +111,13 @@ export class App extends AbstractApp<IProps> {
*/
async _extraInit() {
const { dispatch, getState } = this.state.store ?? {};
const { flags } = this.props;
const { flags = {} } = this.props;
// CallKit does not work on the simulator, make sure we disable it.
if (Platform.OS === 'ios' && DeviceInfo.isEmulatorSync()) {
flags['call-integration.enabled'] = false;
logger.info('Disabling CallKit because this is a simulator');
}
// We set these early enough so then we avoid any unnecessary re-renders.
dispatch?.(updateFlags(flags));

View File

@@ -1,6 +1,7 @@
// @ts-expect-error
import { generateRoomWithoutSeparator } from '@jitsi/js-utils/random';
import { getTokenAuthUrl } from '../authentication/functions';
import { IStateful } from '../base/app/types';
import { isRoomValid } from '../base/conference/functions';
import { isSupportedBrowser } from '../base/environment/environment';
@@ -36,11 +37,24 @@ export function _getRouteToRender(stateful: IStateful) {
* @returns {Promise|undefined}
*/
function _getWebConferenceRoute(state: IReduxState) {
if (!isRoomValid(state['features/base/conference'].room)) {
const room = state['features/base/conference'].room;
if (!isRoomValid(room)) {
return;
}
const route = _getEmptyRoute();
const config = state['features/base/config'];
// if we have auto redirect enabled, and we have previously logged in successfully
// let's redirect to the auth url to get the token and login again
if (config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt && room) {
route.href = getTokenAuthUrl(config, room);
return Promise.resolve(route);
}
// Update the location if it doesn't match. This happens when a room is
// joined from the welcome page. The reason for doing this instead of using

View File

@@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
import { createConnectionEvent } from '../analytics/AnalyticsEvents';
import { sendAnalytics } from '../analytics/functions';
import { appWillNavigate } from '../base/app/actions';
import { SET_ROOM } from '../base/conference/actionTypes';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { getURLWithoutParams } from '../base/connection/utils';
@@ -146,11 +147,15 @@ function _isMaybeSplitBrainError(getState: IStore['getState'], action: AnyAction
* @private
* @returns {void}
*/
function _navigate({ getState }: IStore) {
function _navigate({ dispatch, getState }: IStore) {
const state = getState();
const { app } = state['features/base/app'];
_getRouteToRender(state).then(route => app._navigate(route));
_getRouteToRender(state).then((route: Object) => {
dispatch(appWillNavigate(app, route));
return app._navigate(route);
});
}
/**

View File

@@ -26,6 +26,16 @@ export const LOGIN = 'LOGIN';
*/
export const LOGOUT = 'LOGOUT';
/**
* The type of (redux) action which signals that we have authenticated successful when
* tokenAuthUrl is set.
*
* {
* type: SET_TOKEN_AUTH_URL_SUCCESS
* }
*/
export const SET_TOKEN_AUTH_URL_SUCCESS = 'SET_TOKEN_AUTH_URL_SUCCESS';
/**
* The type of (redux) action which signals that the cyclic operation of waiting
* for conference owner has been aborted.

View File

@@ -4,6 +4,7 @@ import { IJitsiConference } from '../base/conference/reducer';
import { hideDialog, openDialog } from '../base/dialog/actions';
import {
SET_TOKEN_AUTH_URL_SUCCESS,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
UPGRADE_ROLE_STARTED, WAIT_FOR_OWNER
@@ -185,3 +186,16 @@ export function waitForOwner() {
export function openLoginDialog() {
return openDialog(LoginDialog);
}
/**
* Updates the config with new options.
*
* @param {boolean} value - The new value.
* @returns {Function}
*/
export function setTokenAuthUrlSuccess(value: boolean) {
return {
type: SET_TOKEN_AUTH_URL_SUCCESS,
value
};
}

View File

@@ -55,9 +55,13 @@ export function cancelWaitForOwner() {
// clients/consumers need an event.
const { authRequired } = getState()['features/base/conference'];
authRequired && dispatch(conferenceLeft(authRequired));
if (authRequired) {
dispatch(conferenceLeft(authRequired));
dispatch(appNavigate(undefined));
// in case we are showing lobby and on top of it wait for owner
// we do not want to navigate away from the conference
dispatch(appNavigate(undefined));
}
};
}

View File

@@ -24,13 +24,17 @@ export function cancelLogin() {
/**
* Cancels authentication, closes {@link WaitForOwnerDialog}
* and navigates back to the welcome page.
* and navigates back to the welcome page only in the case of authentication required error.
* We can be showing the dialog while lobby is enabled and participant is still waiting there and hiding this dialog
* should do nothing.
*
* @returns {Function}
*/
export function cancelWaitForOwner() {
return (dispatch: IStore['dispatch']) => {
dispatch(maybeRedirectToWelcomePage());
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const { authRequired } = getState()['features/base/conference'];
authRequired && dispatch(maybeRedirectToWelcomePage());
};
}

View File

@@ -5,12 +5,12 @@ import { connect as reduxConnect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { IJitsiConference } from '../../../base/conference/reducer';
import { IConfig } from '../../../base/config/configType';
import { connect } from '../../../base/connection/actions.web';
import { toJid } from '../../../base/connection/functions';
import { translate, translateToHTML } from '../../../base/i18n/functions';
import { JitsiConnectionErrors } from '../../../base/lib-jitsi-meet';
import Dialog from '../../../base/ui/components/web/Dialog';
import Input from '../../../base/ui/components/web/Input';
import { joinConference } from '../../../prejoin/actions.web';
import {
authenticateAndUpgradeRole,
cancelLogin
@@ -134,7 +134,9 @@ class LoginDialog extends Component<IProps, IState> {
if (conference) {
dispatch(authenticateAndUpgradeRole(jid, password, conference));
} else {
dispatch(connect(jid, password));
// dispatch(connect(jid, password));
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
dispatch(joinConference(undefined, false, jid, password));
}
}

View File

@@ -1,6 +1,4 @@
import { IConfig } from '../base/config/configType';
import JitsiMeetJS from '../base/lib-jitsi-meet';
/**
* Checks if the token for authentication is available.
@@ -12,13 +10,30 @@ export const isTokenAuthEnabled = (config: IConfig) =>
typeof config.tokenAuthUrl === 'string'
&& config.tokenAuthUrl.length;
/**
* Token url.
* Creates the URL pointing to JWT token authentication service. It is
* formatted from the 'urlPattern' argument which can contain the following
* constants:
* '{room}' - name of the conference room passed as <tt>roomName</tt>
* argument to this method.
* '{roleUpgrade}' - will contain 'true' if the URL will be used for
* the role upgrade scenario, where user connects from anonymous domain and
* then gets upgraded to the moderator by logging-in from the popup window.
*
* @param {Object} config - Configuration state object from store.
* @returns {string}
* @param {Object} config - Configuration state object from store. A URL pattern pointing to the login service.
* @param {string} roomName - The name of the conference room for which the user will be authenticated.
*
* @returns {string|undefined} - The URL pointing to JWT login service or
* <tt>undefined</tt> if the pattern stored in config is not a string and the URL can not be
* constructed.
*/
export const getTokenAuthUrl = (config: IConfig) =>
JitsiMeetJS.util.AuthUtil.getTokenAuthUrl.bind(null,
config.tokenAuthUrl);
export const getTokenAuthUrl = (config: IConfig, roomName: string) => {
const url = config.tokenAuthUrl;
if (typeof url !== 'string' || !roomName) {
return undefined;
}
return url.replace('{room}', roomName);
};

View File

@@ -1,9 +1,11 @@
import { IStore } from '../app/types';
import { APP_WILL_NAVIGATE } from '../base/app/actionTypes';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT
} from '../base/conference/actionTypes';
import { isRoomValid } from '../base/conference/functions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../base/connection/actionTypes';
import { hangup } from '../base/connection/actions';
import { hideDialog } from '../base/dialog/actions';
@@ -31,8 +33,10 @@ import {
openLoginDialog,
openWaitForOwnerDialog,
redirectToDefaultLocation,
setTokenAuthUrlSuccess,
stopWaitForOwner,
waitForOwner } from './actions';
waitForOwner
} from './actions';
import { LoginDialog, WaitForOwnerDialog } from './components';
import { getTokenAuthUrl, isTokenAuthEnabled } from './functions';
@@ -89,8 +93,11 @@ MiddlewareRegistry.register(store => next => action => {
// CONFERENCE_FAILED caused by
// JitsiConferenceErrors.AUTHENTICATION_REQUIRED.
let recoverable;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED) {
if (error.name === JitsiConferenceErrors.AUTHENTICATION_REQUIRED
|| (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR && lobbyWaitingForHost)) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
@@ -104,12 +111,25 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case CONFERENCE_JOINED:
case CONFERENCE_JOINED: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// auto redirect is turned on and we have succesfully logged in
// let's mark that
dispatch(setTokenAuthUrlSuccess(true));
}
if (_isWaitingForOwner(store)) {
store.dispatch(stopWaitForOwner());
}
store.dispatch(hideLoginDialog());
break;
}
case CONFERENCE_LEFT:
store.dispatch(stopWaitForOwner());
@@ -143,15 +163,53 @@ MiddlewareRegistry.register(store => next => action => {
}
case LOGOUT: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
const { conference } = store.getState()['features/base/conference'];
if (!conference) {
break;
}
store.dispatch(openLogoutDialog(() =>
conference.room.moderator.logout(() => store.dispatch(hangup(true)))
));
dispatch(openLogoutDialog(() => {
const logoutUrl = config.tokenLogoutUrl;
if (isTokenAuthEnabled(config)
&& config.tokenAuthUrlAutoRedirect
&& state['features/base/jwt'].jwt) {
// user is logging out remove auto redirect indication
dispatch(setTokenAuthUrlSuccess(false));
}
if (logoutUrl) {
window.location.href = logoutUrl;
return;
}
conference.room.moderator.logout(() => dispatch(hangup(true)));
}));
break;
}
case APP_WILL_NAVIGATE: {
const { dispatch, getState } = store;
const state = getState();
const config = state['features/base/config'];
const room = state['features/base/conference'].room;
if (isRoomValid(room)
&& config.tokenAuthUrl && config.tokenAuthUrlAutoRedirect
&& state['features/authentication'].tokenAuthUrlSuccessful
&& !state['features/base/jwt'].jwt) {
// if we have auto redirect enabled, and we have previously logged in successfully
// we will redirect to the auth url to get the token and login again
// we want to mark token auth success to false as if login is unsuccessful
// the participant can join anonymously and not go in login loop
dispatch(setTokenAuthUrlSuccess(false));
}
break;
}
@@ -236,9 +294,18 @@ function _handleLogin({ dispatch, getState }: IStore) {
return;
}
if (!room) {
return;
}
// FIXME: This method will not preserve the other URL params that were originally passed.
// redirectToTokenAuthService
window.location.href = getTokenAuthUrl(config)(room, false);
const tokenAuthServiceUrl = getTokenAuthUrl(config, room);
if (tokenAuthServiceUrl) {
// we have already shown the prejoin screen, so no need to show it again after obtaining the token
window.location.href = `${tokenAuthServiceUrl}${
tokenAuthServiceUrl.includes('#') ? '&' : '#'}skipPrejoin=true`;
}
} else {
dispatch(openLoginDialog());
}

View File

@@ -1,8 +1,10 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import { assign } from '../base/redux/functions';
import {
CANCEL_LOGIN,
SET_TOKEN_AUTH_URL_SUCCESS,
STOP_WAIT_FOR_OWNER,
UPGRADE_ROLE_FINISHED,
UPGRADE_ROLE_STARTED,
@@ -15,9 +17,17 @@ export interface IAuthenticationState {
thenableWithCancel?: {
cancel: Function;
};
tokenAuthUrlSuccessful?: boolean;
waitForOwnerTimeoutID?: number;
}
/**
* Sets up the persistence of the feature {@code authentication}.
*/
PersistenceRegistry.register('features/authentication', {
tokenAuthUrlSuccessful: true
});
/**
* Listens for actions which change the state of the authentication feature.
*
@@ -35,6 +45,10 @@ ReducerRegistry.register<IAuthenticationState>('features/authentication',
progress: undefined,
thenableWithCancel: undefined
});
case SET_TOKEN_AUTH_URL_SUCCESS:
return assign(state, {
tokenAuthUrlSuccessful: action.value
});
case STOP_WAIT_FOR_OWNER:
return assign(state, {

View File

@@ -19,3 +19,15 @@ export const APP_WILL_MOUNT = 'APP_WILL_MOUNT';
* }
*/
export const APP_WILL_UNMOUNT = 'APP_WILL_UNMOUNT';
/**
* The type of (redux) action which signals that a specific App will navigate using a route (in
* React terms).
*
* {
* type: APP_WILL_NAVIGATE,
* app: App,
* route: Route
* }
*/
export const APP_WILL_NAVIGATE = 'APP_WILL_NAVIGATE';

View File

@@ -1,6 +1,10 @@
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from './actionTypes';
import {
APP_WILL_MOUNT,
APP_WILL_NAVIGATE,
APP_WILL_UNMOUNT
} from './actionTypes';
/**
* Signals that a specific App will mount (in the terms of React).
@@ -44,3 +48,22 @@ export function appWillUnmount(app: Object) {
app
};
}
/**
* Signals that a specific App will navigate (in the terms of React).
*
* @param {App} app - The App which will navigate.
* @param {Object} route - The route which will be used.
* @returns {{
* type: APP_WILL_NAVIGATE,
* app: App,
* route: Object
* }}
*/
export function appWillNavigate(app: Object, route: Object) {
return {
type: APP_WILL_NAVIGATE,
app,
route
};
}

View File

@@ -53,6 +53,25 @@ export const CONFERENCE_JOIN_IN_PROGRESS = 'CONFERENCE_JOIN_IN_PROGRESS';
*/
export const CONFERENCE_LEFT = 'CONFERENCE_LEFT';
/**
* The type of (redux) action which signals that the conference is out of focus.
* For example, if the user navigates to the Chat screen.
*
* {
* type: CONFERENCE_BLURRED,
* }
*/
export const CONFERENCE_BLURRED = 'CONFERENCE_BLURRED';
/**
* The type of (redux) action which signals that the conference is in focus.
*
* {
* type: CONFERENCE_FOCUSED,
* }
*/
export const CONFERENCE_FOCUSED = 'CONFERENCE_FOCUSED';
/**
* The type of (redux) action, which indicates conference local subject changes.
*

View File

@@ -95,9 +95,14 @@ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore[
// Dispatches into features/base/conference follow:
conference.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin)));
// we want to ignore this event in case of tokenAuthUrl config
// we are deprecating this and at some point will get rid of it
if (!state['features/base/config'].tokenAuthUrl) {
conference.on(
JitsiConferenceEvents.AUTH_STATUS_CHANGED,
(authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin)));
}
conference.on(
JitsiConferenceEvents.CONFERENCE_FAILED,
(err: string, ...args: any[]) => dispatch(conferenceFailed(conference, err, ...args)));

View File

@@ -304,10 +304,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
// and the visitor will be redirected back to a vnode from jicofo
if (config.oldConfig && username) {
return {
hosts: {
domain: config.oldConfig.hosts.domain,
muc: config.oldConfig.hosts.muc
},
hosts: config.oldConfig.hosts,
focusUserJid: focusJid,
disableLocalStats: false,
bosh: config.oldConfig.bosh && appendURLParam(config.oldConfig.bosh, 'customusername', username),
@@ -323,8 +320,7 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
const oldConfig = {
hosts: {
domain: config.hosts.domain,
muc: config.hosts.muc
domain: ''
},
focusUserJid: config.focusUserJid,
bosh: config.bosh,
@@ -332,6 +328,9 @@ export function getVisitorOptions(stateful: IStateful, params: Array<string>) {
websocket: config.websocket
};
// copy original hosts, to make sure we do not use a modified one later
Object.assign(oldConfig.hosts, config.hosts);
const domain = `${vnode}.meet.jitsi`;
return {

View File

@@ -24,7 +24,7 @@ import { overwriteConfig } from '../config/actions';
import { CONNECTION_ESTABLISHED, CONNECTION_FAILED } from '../connection/actionTypes';
import { connect, connectionDisconnected, disconnect } from '../connection/actions';
import { validateJwt } from '../jwt/functions';
import { JitsiConferenceErrors } from '../lib-jitsi-meet';
import { JitsiConferenceErrors, JitsiConnectionErrors } from '../lib-jitsi-meet';
import { PARTICIPANT_UPDATED, PIN_PARTICIPANT } from '../participants/actionTypes';
import { PARTICIPANT_ROLE } from '../participants/constants';
import {
@@ -35,6 +35,7 @@ import {
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
import { TRACK_ADDED, TRACK_REMOVED } from '../tracks/actionTypes';
import { destroyLocalTracks } from '../tracks/actions.any';
import { getLocalTracks } from '../tracks/functions.any';
import {
CONFERENCE_FAILED,
@@ -48,6 +49,7 @@ import {
SET_ROOM
} from './actionTypes';
import {
authStatusChanged,
conferenceFailed,
conferenceWillInit,
conferenceWillLeave,
@@ -204,10 +206,19 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
if (newConfig) {
dispatch(overwriteConfig(newConfig)) // @ts-ignore
.then(dispatch(conferenceWillLeave(conference)))
.then(conference.leave())
.then(dispatch(disconnect()))
.then(dispatch(connect()));
.then(() => dispatch(conferenceWillLeave(conference)))
.then(() => conference.leave())
.then(() => dispatch(disconnect()))
.then(() => dispatch(connect()))
.then(() => {
// FIXME: Workaround for the web version. To be removed once we get rid of conference.js
if (typeof APP !== 'undefined') {
const localTracks = getLocalTracks(getState()['features/base/tracks']);
const jitsiTracks = localTracks.map((t: any) => t.jitsiTrack);
APP.conference.startConference(jitsiTracks).catch(logger.error);
}
});
}
break;
@@ -341,9 +352,23 @@ function _conferenceJoined({ dispatch, getState }: IStore, next: Function, actio
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
async function _connectionEstablished({ dispatch }: IStore, next: Function, action: AnyAction) {
async function _connectionEstablished({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
const result = next(action);
const { tokenAuthUrl = false } = getState()['features/base/config'];
// if there is token auth URL defined and local participant is using jwt
// this means it is logged in when connection is established, so we can change the state
if (tokenAuthUrl) {
let email;
if (getState()['features/base/jwt'].jwt) {
email = getLocalParticipant(getState())?.email;
}
dispatch(authStatusChanged(true, email || ''));
}
// FIXME: Workaround for the web version. Currently, the creation of the
// conference is handled by /conference.js.
if (typeof APP === 'undefined') {
@@ -392,18 +417,22 @@ function _logJwtErrors(message: string, state: IReduxState) {
function _connectionFailed({ dispatch, getState }: IStore, next: Function, action: AnyAction) {
_logJwtErrors(action.error.message, getState());
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
const { connection, error } = action;
// do not show the notification when we will prompt the user
// for username and password
if (error.name === JitsiConnectionErrors.PASSWORD_REQUIRED
&& getState()['features/base/jwt'].jwt) {
dispatch(showErrorNotification({
descriptionKey: 'dialog.tokenAuthFailed',
titleKey: 'dialog.tokenAuthFailedTitle'
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
const result = next(action);
_removeUnloadHandler(getState);
const { connection } = action;
const { error } = action;
forEachConference(getState, conference => {
// TODO: revisit this
// It feels that it would make things easier if JitsiConference

View File

@@ -68,6 +68,33 @@ type ButtonsWithNotifyClick = 'camera' |
'add-passcode' |
'__end';
type ParticipantMenuButtonsWithNotifyClick = 'allow-video' |
'ask-unmute' |
'conn-status' |
'flip-local-video' |
'grant-moderator' |
'hide-self-view' |
'kick' |
'mute' |
'mute-others' |
'mute-others-video' |
'mute-video' |
'pinToStage' |
'privateMessage' |
'remote-control' |
'send-participant-to-room' |
'verify';
type NotifyClickButtonKey = string |
ButtonsWithNotifyClick |
ParticipantMenuButtonsWithNotifyClick;
export type NotifyClickButton = NotifyClickButtonKey |
{
key: NotifyClickButtonKey;
preventExecution: boolean;
};
export type Sounds = 'ASKED_TO_UNMUTE_SOUND' |
'E2EE_OFF_SOUND' |
'E2EE_ON_SOUND' |
@@ -126,6 +153,25 @@ export interface INoiseSuppressionConfig {
};
}
export interface IWatchRTCConfiguration {
allowBrowserLogCollection?: boolean;
collectionInterval?: number;
console?: {
level: string;
override: boolean;
};
debug?: boolean;
keys?: any;
logGetStats?: boolean;
proxyUrl?: string;
rtcApiKey: string;
rtcPeerId?: string;
rtcRoomId?: string;
rtcTags?: string[];
rtcToken?: string;
wsUrl?: string;
}
export interface IConfig {
_desktopSharingSourceDevice?: string;
_immediateReloadThreshold?: string;
@@ -145,6 +191,7 @@ export interface IConfig {
rtcstatsStoreLogs?: boolean;
rtcstatsUseLegacy?: boolean;
scriptURLs?: Array<string>;
watchRTCEnabled?: boolean;
whiteListedEvents?: string[];
};
apiLogLevels?: Array<'warn' | 'log' | 'error' | 'info' | 'debug'>;
@@ -283,6 +330,7 @@ export interface IConfig {
disableThirdPartyRequests?: boolean;
disableTileEnlargement?: boolean;
disableTileView?: boolean;
disableVirtualBackground?: boolean;
disabledNotifications?: Array<string>;
disabledSounds?: Array<Sounds>;
doNotFlipLocalVideo?: boolean;
@@ -447,6 +495,10 @@ export interface IConfig {
mobileCodecPreferenceOrder?: Array<string>;
stunServers?: Array<{ urls: string; }>;
};
participantMenuButtonsWithNotifyClick?: Array<string | ParticipantMenuButtonsWithNotifyClick | {
key: string | ParticipantMenuButtonsWithNotifyClick;
preventExecution: boolean;
}>;
participantsPane?: {
hideModeratorSettingsTab?: boolean;
hideMoreActionsButton?: boolean;
@@ -524,6 +576,8 @@ export interface IConfig {
numberOfVisibleTiles?: number;
};
tokenAuthUrl?: string;
tokenAuthUrlAutoRedirect?: string;
tokenLogoutUrl?: string;
toolbarButtons?: Array<ToolbarButtons>;
toolbarConfig?: {
alwaysVisible?: boolean;
@@ -559,6 +613,7 @@ export interface IConfig {
mobileCodecPreferenceOrder?: Array<string>;
persist?: boolean;
};
watchRTCConfigParams?: IWatchRTCConfiguration;
webhookProxyUrl?: string;
webrtcIceTcpDisable?: boolean;
webrtcIceUdpDisable?: boolean;

View File

@@ -15,6 +15,7 @@ export default [
'_peerConnStatusRtcMuteTimeout',
'analytics.disabled',
'analytics.rtcstatsEnabled',
'analytics.watchRTCEnabled',
'audioLevelsInterval',
'audioQuality',
'autoKnockLobby',
@@ -193,6 +194,7 @@ export default [
'openSharedDocumentOnJoin',
'opusMaxAverageBitrate',
'p2p',
'participantMenuButtonsWithNotifyClick',
'participantsPane',
'pcStatsInterval',
'prejoinConfig',
@@ -227,6 +229,7 @@ export default [
'useHostPageLocalStorage',
'useTurnUdp',
'videoQuality',
'watchRTCConfigParams',
'webrtcIceTcpDisable',
'webrtcIceUdpDisable',
'whiteboard.enabled'

View File

@@ -27,8 +27,10 @@ export function _cleanupConfig(config: IConfig) {
delete config.analytics?.rtcstatsSendSdp;
delete config.analytics?.rtcstatsUseLegacy;
delete config.analytics?.obfuscateRoomName;
delete config.analytics?.watchRTCEnabled;
delete config.callStatsID;
delete config.callStatsSecret;
delete config.watchRTCConfigParams;
config.giphy = { enabled: false };
}
}

View File

@@ -1,7 +1,14 @@
import { IReduxState } from '../../app/types';
import JitsiMeetJS from '../../base/lib-jitsi-meet';
import { NOTIFY_CLICK_MODE } from '../../toolbox/constants';
import { IConfig, IDeeplinkingConfig, IDeeplinkingMobileConfig, IDeeplinkingPlatformConfig } from './configType';
import {
IConfig,
IDeeplinkingConfig,
IDeeplinkingMobileConfig,
IDeeplinkingPlatformConfig,
NotifyClickButton
} from './configType';
import { TOOLBAR_BUTTONS } from './constants';
export * from './functions.any';
@@ -120,14 +127,21 @@ export function _setDeeplinkingDefaults(deeplinking: IDeeplinkingConfig) {
}
/**
* Returns the list of buttons that have that notify the api when clicked.
* Common logic to gather buttons that have to notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Array} - The list of buttons.
* @param {Array} buttonsWithNotifyClick - The array of systme buttons that need to notify the api.
* @param {Array} customButtons - The custom buttons.
* @returns {Array}
*/
export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: string; preventExecution: boolean; }> {
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
const customButtons = customToolbarButtons?.map(({ id }) => {
const buildButtonsArray = (
buttonsWithNotifyClick?: NotifyClickButton[],
customButtons?: {
icon: string;
id: string;
text: string;
}[]
): NotifyClickButton[] => {
const customButtonsWithNotifyClick = customButtons?.map(({ id }) => {
return {
key: id,
preventExecution: false
@@ -135,13 +149,69 @@ export function getButtonsWithNotifyClick(state: IReduxState): Array<{ key: stri
});
const buttons = Array.isArray(buttonsWithNotifyClick)
? buttonsWithNotifyClick as Array<{ key: string; preventExecution: boolean; }>
? buttonsWithNotifyClick as NotifyClickButton[]
: [];
if (customButtons) {
buttons.push(...customButtons);
if (customButtonsWithNotifyClick) {
buttons.push(...customButtonsWithNotifyClick);
}
return buttons;
};
/**
* Returns the list of toolbar buttons that have to notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Array} - The list of buttons.
*/
export function getButtonsWithNotifyClick(
state: IReduxState
): NotifyClickButton[] {
const { buttonsWithNotifyClick, customToolbarButtons } = state['features/base/config'];
return buildButtonsArray(
buttonsWithNotifyClick,
customToolbarButtons
);
}
/**
* Returns the list of participant menu buttons that have that notify the api when clicked.
*
* @param {Object} state - The redux state.
* @returns {Array} - The list of participant menu buttons.
*/
export function getParticipantMenuButtonsWithNotifyClick(
state: IReduxState
): NotifyClickButton[] {
const { participantMenuButtonsWithNotifyClick, customParticipantMenuButtons } = state['features/base/config'];
return buildButtonsArray(
participantMenuButtonsWithNotifyClick,
customParticipantMenuButtons
);
}
/**
* Returns the notify mode for the specified button.
*
* @param {string} buttonKey - The button key.
* @param {Array} buttonsWithNotifyClick - The buttons with notify click.
* @returns {string|undefined}
*/
export const getButtonNotifyMode = (
buttonKey: string,
buttonsWithNotifyClick?: NotifyClickButton[]
): string | undefined => {
const notify = buttonsWithNotifyClick?.find(
(btn: NotifyClickButton) =>
(typeof btn === 'string' && btn === buttonKey) || (typeof btn === 'object' && btn.key === buttonKey)
);
if (notify) {
return typeof notify === 'string' || notify.preventExecution
? NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
: NOTIFY_CLICK_MODE.ONLY_NOTIFY;
}
};

View File

@@ -1,7 +1,6 @@
import _ from 'lodash';
import { IReduxState, IStore } from '../../app/types';
import { setPrejoinDisplayNameRequired } from '../../prejoin/actions.any';
import { conferenceLeft, conferenceWillLeave } from '../conference/actions';
import { getCurrentConference } from '../conference/functions';
import JitsiMeetJS, { JitsiConnectionEvents } from '../lib-jitsi-meet';
@@ -232,15 +231,6 @@ export function _connectInternal(id?: string, password?: string) {
JitsiConnectionEvents.CONNECTION_FAILED,
_onConnectionFailed);
/**
* Marks the display name for the prejoin screen as required.
* This can happen if a user tries to join a room with lobby enabled.
*/
connection.addEventListener(
JitsiConnectionEvents.DISPLAY_NAME_REQUIRED,
() => dispatch(setPrejoinDisplayNameRequired())
);
/**
* Unsubscribe the connection instance from
* {@code CONNECTION_DISCONNECTED} and {@code CONNECTION_FAILED} events.

View File

@@ -35,7 +35,7 @@ export function connect(id?: string, password?: string) {
}
})
.then(j => j && dispatch(setJWT(j)))
.then(() => _connectInternal(id, password));
.then(() => dispatch(_connectInternal(id, password)));
}
// used by jibri

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -25,6 +25,7 @@ export { default as IconDeviceHeadphone } from './headset.svg';
export { default as IconDotsHorizontal } from './dots-horizontal.svg';
export { default as IconDownload } from './download.svg';
export { default as IconE2EE } from './e2ee.svg';
export { default as IconEdit } from './edit.svg';
export { default as IconEnlarge } from './enlarge.svg';
export { default as IconEnterFullscreen } from './enter-fullscreen.svg';
export { default as IconEnvelope } from './envelope.svg';

View File

@@ -72,6 +72,7 @@ function setupJitsiLocalStorage() {
logger.error('Can\'t parse localStorageContent.', error);
}
jitsiLocalStorage.setLocalStorageDisabled(true);
jitsiLocalStorage.on('changed', onFakeLocalStorageChanged);
}
}

View File

@@ -153,6 +153,13 @@ function _setJWT(store: IStore, next: Function, action: AnyAction) {
_overwriteLocalParticipant(
store, { ...newUser,
features: context.features });
} else if (jwtPayload.name || jwtPayload.picture || jwtPayload.email) {
// there are some tokens (firebase) having picture and name on the main level.
_overwriteLocalParticipant(store, {
avatarURL: jwtPayload.picture,
name: jwtPayload.name,
email: jwtPayload.email
});
}
}
} else if (typeof APP === 'undefined') {

View File

@@ -337,12 +337,12 @@ export default class AbstractButton<P extends IProps, S=any> extends Component<P
* @private
* @returns {void}
*/
_onClick(e?: React.MouseEvent<HTMLElement> | GestureResponderEvent) {
const { afterClick, handleClick, notifyMode, buttonKey } = this.props;
_onClick(e?: React.MouseEvent | GestureResponderEvent) {
const { afterClick, buttonKey, handleClick, notifyMode } = this.props;
if (typeof APP !== 'undefined' && notifyMode) {
APP.API.notifyToolbarButtonClicked(
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
buttonKey, notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}

View File

@@ -1,4 +1,5 @@
import { IStore } from '../../app/types';
import { NoiseSuppressionEffect } from '../../stream-effects/noise-suppression/NoiseSuppressionEffect';
import { createVirtualBackgroundEffect } from '../../stream-effects/virtual-background';
import logger from './logger';
@@ -12,6 +13,9 @@ import logger from './logger';
export default function loadEffects(store: IStore): Promise<any> {
const state = store.getState();
const virtualBackground = state['features/virtual-background'];
const noiseSuppression = state['features/noise-suppression'];
const { noiseSuppression: nsOptions } = state['features/base/config'];
const backgroundPromise = virtualBackground.backgroundEffectEnabled
? createVirtualBackgroundEffect(virtualBackground)
@@ -22,5 +26,9 @@ export default function loadEffects(store: IStore): Promise<any> {
})
: Promise.resolve();
return Promise.all([ backgroundPromise ]);
const noiseSuppressionPromise = noiseSuppression?.enabled
? Promise.resolve(new NoiseSuppressionEffect(nsOptions))
: Promise.resolve();
return Promise.all([ backgroundPromise, noiseSuppressionPromise ]);
}

View File

@@ -170,13 +170,13 @@ const BaseDialog = ({
if (e.key === 'Enter' && !disableEnter) {
submit?.();
}
}, []);
}, [ disableEnter, onClose, submit ]);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, []);
}, [ handleKeyDown ]);
return (
<div className = { cx(classes.container, isUnmounting && 'unmount') }>

View File

@@ -21,6 +21,7 @@ export default function getUnsafeRoomText(state: IReduxState, t: Function, conte
const link = React.createElement(Link, {
url: securityUrl,
children: 'here',
key: 'support-link',
style: { color: BaseTheme.palette.action01 } });
const options = {

View File

@@ -77,6 +77,25 @@ export function closeBreakoutRoom(roomId: string) {
};
}
/**
* Action to rename a breakout room.
*
* @param {string} breakoutRoomJid - The jid of the breakout room to rename.
* @param {string} name - New name / subject for the breakout room.
* @returns {Function}
*/
export function renameBreakoutRoom(breakoutRoomJid: string, name = '') {
return (_dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const trimmedName = name.trim();
if (trimmedName.length !== 0) {
sendAnalytics(createBreakoutRoomsEvent('rename'));
getCurrentConference(getState)?.getBreakoutRooms()
?.renameBreakoutRoom(breakoutRoomJid, trimmedName);
}
};
}
/**
* Action to remove a breakout room.
*

View File

@@ -2,3 +2,8 @@
* Key for this feature.
*/
export const FEATURE_KEY = 'features/breakout-rooms';
/**
* Feature to rename breakout rooms.
*/
export const BREAKOUT_ROOMS_RENAME_FEATURE = 'rename';

View File

@@ -1,5 +1,5 @@
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import React, { useCallback } from 'react';
import {
BackHandler,
NativeModules,
@@ -10,10 +10,11 @@ import {
ViewStyle
} from 'react-native';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { connect } from 'react-redux';
import { connect, useDispatch } from 'react-redux';
import { appNavigate } from '../../../app/actions';
import { IReduxState, IStore } from '../../../app/types';
import { CONFERENCE_BLURRED, CONFERENCE_FOCUSED } from '../../../base/conference/actionTypes';
import { FULLSCREEN_ENABLED, PIP_ENABLED } from '../../../base/flags/constants';
import { getFeatureFlag } from '../../../base/flags/functions';
import { getParticipantCount } from '../../../base/participants/functions';
@@ -60,7 +61,6 @@ import TitleBar from './TitleBar';
import { EXPANDED_LABEL_TIMEOUT } from './constants';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link Conference}.
*/
@@ -443,7 +443,7 @@ class Conference extends AbstractConference<IProps, State> {
)
}
<LonelyMeetingExperience />
{ !_shouldDisplayTileView && <LonelyMeetingExperience /> }
{
_shouldDisplayTileView
@@ -608,18 +608,17 @@ function _mapStateToProps(state: IReduxState, _ownProps: any) {
}
export default withSafeAreaInsets(connect(_mapStateToProps)(props => {
const isFocused = useIsFocused();
const dispatch = useDispatch();
useEffect(() => {
if (isFocused) {
setPictureInPictureEnabled(true);
} else {
useFocusEffect(useCallback(() => {
dispatch({ type: CONFERENCE_FOCUSED });
setPictureInPictureEnabled(true);
return () => {
dispatch({ type: CONFERENCE_BLURRED });
setPictureInPictureEnabled(false);
}
// We also need to disable PiP when we are back on the WelcomePage
return () => setPictureInPictureEnabled(false);
}, [ isFocused ]);
};
}, []));
return ( // @ts-ignore
<Conference { ...props } />

View File

@@ -5,12 +5,10 @@ export default {
alignSelf: 'center',
backgroundColor: BaseTheme.palette.ui01,
borderRadius: BaseTheme.shape.borderRadius,
paddingHorizontal: BaseTheme.spacing[3],
paddingVertical: BaseTheme.spacing[1]
padding: 6
},
displayNamePadding: {
padding: BaseTheme.spacing[1],
paddingRight: 6
},

View File

@@ -36,4 +36,4 @@ export const SET_PASSWORD_JOIN_FAILED = 'SET_PASSWORD_JOIN_FAILED';
/**
* Action type to remove chattingWithModerator field
*/
export const REMOVE_LOBBY_CHAT_WITH_MODERATOR = 'REMOVE_LOBBY_CHAT_WITH_MODERATOR';
export const REMOVE_LOBBY_CHAT_WITH_MODERATOR = 'REMOVE_LOBBY_CHAT_WITH_MODERATOR';

View File

@@ -8,6 +8,7 @@ import { LOBBY_CHAT_MESSAGE } from '../chat/constants';
import { handleLobbyMessageReceived } from '../chat/middleware';
import { hideNotification, showNotification } from '../notifications/actions';
import { LOBBY_NOTIFICATION_ID } from '../notifications/constants';
import { joinConference } from '../prejoin/actions';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
@@ -205,6 +206,18 @@ export function startKnocking() {
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
const state = getState();
const { membersOnly } = state['features/base/conference'];
if (!membersOnly) {
// no membersOnly, this means we got lobby screen shown as someone
// tried to join a conference that has lobby enabled without setting display name
// join conference should trigger the lobby/member_only path after setting the display name
// this is possible only for web, where we can join without a prejoin screen
dispatch(joinConference());
return;
}
const localParticipant = getLocalParticipant(state);
dispatch(conferenceWillJoin(membersOnly));
@@ -406,3 +419,4 @@ export function setLobbyMessageListener() {
});
};
}

View File

@@ -27,6 +27,11 @@ export interface IProps {
*/
_deviceStatusVisible: boolean;
/**
* Indicates whether the message that display name is required is shown.
*/
_isDisplayNameRequiredActive: boolean;
/**
* True if moderator initiated a chat session with the participant.
*/
@@ -435,7 +440,7 @@ export function _mapStateToProps(state: IReduxState) {
const participantId = localParticipant?.id;
const inviteEnabledFlag = getFeatureFlag(state, INVITE_ENABLED, true);
const { disableInviteFunctions } = state['features/base/config'];
const { knocking, passwordJoinFailed } = state['features/lobby'];
const { isDisplayNameRequiredError, knocking, passwordJoinFailed } = state['features/lobby'];
const { iAmSipGateway } = state['features/base/config'];
const { disableLobbyPassword } = getSecurityUiConfig(state);
const showCopyUrlButton = inviteEnabledFlag || !disableInviteFunctions;
@@ -445,6 +450,7 @@ export function _mapStateToProps(state: IReduxState) {
return {
_deviceStatusVisible: deviceStatusVisible,
_isDisplayNameRequiredActive: Boolean(isDisplayNameRequiredError),
_knocking: knocking,
_lobbyChatMessages: messages,
_lobbyMessageRecipient: lobbyMessageRecipient?.name,

View File

@@ -153,16 +153,25 @@ class LobbyScreen extends AbstractLobbyScreen<IProps> {
*/
_renderParticipantInfo() {
const { displayName } = this.state;
const { t } = this.props;
const { _isDisplayNameRequiredActive, t } = this.props;
const showError = _isDisplayNameRequiredActive && !displayName;
return (
<Input
className = 'lobby-prejoin-input'
id = 'lobby-name-field'
onChange = { this._onChangeDisplayName }
placeholder = { t('lobby.nameField') }
testId = 'lobby.nameField'
value = { displayName } />
<>
<Input
autoFocus = { true }
className = 'lobby-prejoin-input'
error = { showError }
id = 'lobby-name-field'
onChange = { this._onChangeDisplayName }
placeholder = { t('lobby.nameField') }
testId = 'lobby.nameField'
value = { displayName } />
{ showError && <div
className = 'lobby-prejoin-error'
data-testid = 'lobby.errorMessage'>{t('prejoin.errorMissingName')}</div>}
</>
);
}

View File

@@ -41,7 +41,7 @@ import {
import { INotificationProps } from '../notifications/types';
import { open as openParticipantsPane } from '../participants-pane/actions';
import { getParticipantsPaneOpen } from '../participants-pane/functions';
import { shouldAutoKnock } from '../prejoin/functions';
import { isPrejoinPageVisible, shouldAutoKnock } from '../prejoin/functions';
import {
KNOCKING_PARTICIPANT_ARRIVED_OR_UPDATED,
@@ -267,17 +267,23 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
const state = getState();
const { membersOnly } = state['features/base/conference'];
const nonFirstFailure = Boolean(membersOnly);
const { isDisplayNameRequiredError } = state['features/lobby'];
const { prejoinConfig } = state['features/base/config'];
if (error.name === JitsiConferenceErrors.MEMBERS_ONLY_ERROR) {
if (typeof error.recoverable === 'undefined') {
error.recoverable = true;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ _lobbyJid, lobbyWaitingForHost ] = error.params;
const result = next(action);
dispatch(openLobbyScreen());
if (shouldAutoKnock(state)) {
// if there was an error about display name and pre-join is not enabled
if (shouldAutoKnock(state) || (isDisplayNameRequiredError && !prejoinConfig?.enabled) || lobbyWaitingForHost) {
dispatch(startKnocking());
}
@@ -288,6 +294,18 @@ function _conferenceFailed({ dispatch, getState }: IStore, next: Function, actio
dispatch(setPasswordJoinFailed(nonFirstFailure));
return result;
} else if (error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
const [ isLobbyEnabled ] = error.params;
const result = next(action);
// if the error is due to required display name because lobby is enabled for the room
// if not showing the prejoin page then show lobby UI
if (isLobbyEnabled && !isPrejoinPageVisible(state)) {
dispatch(openLobbyScreen());
}
return result;
}

View File

@@ -1,4 +1,10 @@
import { CONFERENCE_JOINED, CONFERENCE_LEFT, SET_PASSWORD } from '../base/conference/actionTypes';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
SET_PASSWORD
} from '../base/conference/actionTypes';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
@@ -14,6 +20,7 @@ import {
import { IKnockingParticipant } from './types';
const DEFAULT_STATE = {
isDisplayNameRequiredError: false,
knocking: false,
knockingParticipants: [],
lobbyEnabled: false,
@@ -22,6 +29,12 @@ const DEFAULT_STATE = {
};
export interface ILobbyState {
/**
* A conference error when we tried to join into a room with no display name
* when lobby is enabled in the room.
*/
isDisplayNameRequiredError: boolean;
knocking: boolean;
knockingParticipants: IKnockingParticipant[];
lobbyEnabled: boolean;
@@ -39,6 +52,16 @@ export interface ILobbyState {
*/
ReducerRegistry.register<ILobbyState>('features/lobby', (state = DEFAULT_STATE, action): ILobbyState => {
switch (action.type) {
case CONFERENCE_FAILED: {
if (action.error.name === JitsiConferenceErrors.DISPLAY_NAME_REQUIRED) {
return {
...state,
isDisplayNameRequiredError: true
};
}
return state;
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
return {

View File

@@ -1,5 +1,4 @@
import { AppState } from 'react-native';
import { AnyAction } from 'redux';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
@@ -23,12 +22,12 @@ MiddlewareRegistry.register(store => next => action => {
case APP_WILL_MOUNT: {
const { dispatch } = store;
_setAppStateListener(store, next, action, _onAppStateChange.bind(undefined, dispatch));
_setAppStateListener(store, _onAppStateChange.bind(undefined, dispatch));
break;
}
case APP_WILL_UNMOUNT:
_setAppStateListener(store, next, action, undefined);
_setAppStateListener(store, undefined);
break;
}
@@ -55,20 +54,14 @@ function _onAppStateChange(dispatch: IStore['dispatch'], appState: string) {
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code _SET_IMMERSIVE_LISTENER}
* which is being dispatched in the specified store.
* @param {any} listener - Listener for app state status.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setAppStateListener({ dispatch, getState }: IStore, next: Function, action: AnyAction, listener: any) {
function _setAppStateListener({ dispatch, getState }: IStore, listener: any) {
const { subscription } = getState()['features/background'];
const result = next(action);
subscription?.remove();
listener && dispatch(_setAppStateSubscription(AppState.addEventListener('change', listener)));
return result;
dispatch(_setAppStateSubscription(listener ? AppState.addEventListener('change', listener) : undefined));
}

View File

@@ -10,7 +10,9 @@ import { appNavigate } from '../../app/actions.native';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
import {
CONFERENCE_BLURRED,
CONFERENCE_FAILED,
CONFERENCE_FOCUSED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
@@ -32,12 +34,14 @@ import { getURLWithoutParams } from '../../base/connection/utils';
import {
JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
import { SET_AUDIO_MUTED, SET_VIDEO_MUTED } from '../../base/media/actionTypes';
import { MEDIA_TYPE } from '../../base/media/constants';
import { toggleCameraFacingMode } from '../../base/media/actions';
import { MEDIA_TYPE, VIDEO_TYPE } from '../../base/media/constants';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants/actionTypes';
import {
getLocalParticipant,
getParticipantById,
getRemoteParticipants
getRemoteParticipants,
isScreenShareParticipantById
} from '../../base/participants/functions';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
@@ -150,6 +154,14 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
_registerForEndpointTextMessages(store);
break;
case CONFERENCE_BLURRED:
sendEvent(store, CONFERENCE_BLURRED, {});
break;
case CONFERENCE_FOCUSED:
sendEvent(store, CONFERENCE_FOCUSED, {});
break;
case CONNECTION_DISCONNECTED: {
// FIXME: This is a hack. See the description in the JITSI_CONNECTION_CONFERENCE_KEY constant definition.
// Check if this connection was attached to any conference.
@@ -198,7 +210,9 @@ externalAPIEnabled && MiddlewareRegistry.register(store => next => action => {
const { participant } = action;
if (participant?.isVirtualScreenshareParticipant) {
const isVirtualScreenshareParticipant = isScreenShareParticipantById(store.getState(), participant.id);
if (isVirtualScreenshareParticipant) {
break;
}
@@ -253,7 +267,7 @@ externalAPIEnabled && StateListenerRegistry.register(
/* listener */ debounce((tracks: ITrack[], store: IStore) => {
const oldScreenShares = store.getState()['features/mobile/external-api'].screenShares || [];
const newScreenShares = tracks
.filter(track => track.mediaType === 'video' && track.videoType === 'desktop')
.filter(track => track.mediaType === MEDIA_TYPE.SCREENSHARE || track.videoType === VIDEO_TYPE.DESKTOP)
.map(track => track.participantId);
oldScreenShares.forEach(participantId => {
@@ -368,6 +382,10 @@ function _registerForNativeEvents(store: IStore) {
eventEmitter.addListener(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED, ({ enabled }: any) => {
dispatch(setRequestingSubtitles(enabled));
});
eventEmitter.addListener(ExternalAPI.TOGGLE_CAMERA, () => {
dispatch(toggleCameraFacingMode());
});
}
/**
@@ -387,6 +405,7 @@ function _unregisterForNativeEvents() {
eventEmitter.removeAllListeners(ExternalAPI.CLOSE_CHAT);
eventEmitter.removeAllListeners(ExternalAPI.SEND_CHAT_MESSAGE);
eventEmitter.removeAllListeners(ExternalAPI.SET_CLOSED_CAPTIONS_ENABLED);
eventEmitter.removeAllListeners(ExternalAPI.TOGGLE_CAMERA);
}
/**

View File

@@ -1,12 +1,12 @@
/**
* The type of (redux) action to set the react-native-immersive's change event
* listener.
* subscription.
*
* {
* type: _SET_IMMERSIVE_LISTENER,
* listener: Function
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: Function
* }
*
* @protected
*/
export const _SET_IMMERSIVE_LISTENER = '_SET_IMMERSIVE_LISTENER';
export const _SET_IMMERSIVE_SUBSCRIPTION = '_SET_IMMERSIVE_SUBSCRIPTION';

View File

@@ -1,19 +1,21 @@
import { _SET_IMMERSIVE_LISTENER } from './actionTypes';
import { NativeEventSubscription } from 'react-native';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
/**
* Sets the change event listener to be used with react-native-immersive's API.
*
* @param {Function} [listener] - The function to be used with
* @param {Function} subscription - The function to be used with
* react-native-immersive's API as the change event listener.
* @protected
* @returns {{
* type: _SET_IMMERSIVE_LISTENER,
* listener: ?Function
* type: _SET_IMMERSIVE_SUBSCRIPTION,
* subscription: ?NativeEventSubscription
* }}
*/
export function _setImmersiveListener(listener?: Function) {
export function _setImmersiveSubscription(subscription?: NativeEventSubscription) {
return {
type: _SET_IMMERSIVE_LISTENER,
listener
type: _SET_IMMERSIVE_SUBSCRIPTION,
subscription
};
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../../base/logging/functions';
export default getLogger('features/full-screen');

View File

@@ -1,6 +1,4 @@
// @ts-expect-error
import { Immersive } from 'react-native-immersive';
import { AnyAction } from 'redux';
import ImmersiveMode from 'react-native-immersive-mode';
import { IStore } from '../../app/types';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app/actionTypes';
@@ -8,12 +6,18 @@ import { getCurrentConference } from '../../base/conference/functions';
import { isAnyDialogOpen } from '../../base/dialog/functions';
import { FULLSCREEN_ENABLED } from '../../base/flags/constants';
import { getFeatureFlag } from '../../base/flags/functions';
import Platform from '../../base/react/Platform.native';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import StateListenerRegistry from '../../base/redux/StateListenerRegistry';
import { _SET_IMMERSIVE_LISTENER } from './actionTypes';
import { _setImmersiveListener as _setImmersiveListenerA } from './actions';
import { _setImmersiveSubscription } from './actions';
import logger from './logger';
type BarVisibilityType = {
navigationBottomBar: boolean;
statusBar: boolean;
};
type ImmersiveListener = (visibility: BarVisibilityType) => void;
/**
* Middleware that captures conference actions and activates or deactivates the
@@ -28,20 +32,14 @@ import { _setImmersiveListener as _setImmersiveListenerA } from './actions';
*/
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case _SET_IMMERSIVE_LISTENER:
return _setImmersiveListenerF(store, next, action);
case APP_WILL_MOUNT: {
const result = next(action);
_setImmersiveListener(store, _onImmersiveChange.bind(undefined, store));
store.dispatch(
_setImmersiveListenerA(_onImmersiveChange.bind(undefined, store)));
return result;
break;
}
case APP_WILL_UNMOUNT:
store.dispatch(_setImmersiveListenerA(undefined));
_setImmersiveListener(store, undefined);
break;
}
@@ -95,11 +93,9 @@ function _onImmersiveChange({ getState }: IStore) {
* @returns {void}
*/
function _setFullScreen(fullScreen: boolean) {
// XXX The React Native module Immersive is only implemented on Android and
// throws on other platforms.
if (Platform.OS === 'android') {
fullScreen ? Immersive.on() : Immersive.off();
}
logger.info(`Setting full-screen mode: ${fullScreen}`);
ImmersiveMode.fullLayout(fullScreen);
ImmersiveMode.setBarMode(fullScreen ? 'Full' : 'Normal');
}
/**
@@ -109,29 +105,14 @@ function _setFullScreen(fullScreen: boolean) {
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@code _SET_IMMERSIVE_LISTENER}
* which is being dispatched in the specified store.
* @param {Function} listener - Listener for immersive state.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setImmersiveListenerF({ getState }: IStore, next: Function, action: AnyAction) {
// XXX The React Native module Immersive is only implemented on Android and
// throws on other platforms.
if (Platform.OS === 'android') {
// Remove the old Immersive listener and add the new one.
const { listener: oldListener } = getState()['features/full-screen'];
const result = next(action);
const { listener: newListener } = getState()['features/full-screen'];
function _setImmersiveListener({ dispatch, getState }: IStore, listener?: ImmersiveListener) {
const { subscription } = getState()['features/full-screen'];
if (oldListener !== newListener) {
oldListener && Immersive.removeImmersiveListener(oldListener);
newListener && Immersive.addImmersiveListener(newListener);
}
subscription?.remove();
return result;
}
return next(action);
dispatch(_setImmersiveSubscription(listener ? ImmersiveMode.addEventListener(listener) : undefined));
}

View File

@@ -1,17 +1,19 @@
import { NativeEventSubscription } from 'react-native';
import ReducerRegistry from '../../base/redux/ReducerRegistry';
import { _SET_IMMERSIVE_LISTENER } from './actionTypes';
import { _SET_IMMERSIVE_SUBSCRIPTION } from './actionTypes';
export interface IFullScreenState {
listener?: Function;
subscription?: NativeEventSubscription;
}
ReducerRegistry.register<IFullScreenState>('features/full-screen', (state = {}, action): IFullScreenState => {
switch (action.type) {
case _SET_IMMERSIVE_LISTENER:
case _SET_IMMERSIVE_SUBSCRIPTION:
return {
...state,
listener: action.listener
subscription: action.subscription
};
}

View File

@@ -33,9 +33,11 @@ export function enterPictureInPicture() {
new Error('Picture-in-Picture not supported'))
: Promise.resolve();
p.then(
() => dispatch({ type: ENTER_PICTURE_IN_PICTURE }),
(e: string) => logger.warn(`Error entering PiP mode: ${e}`));
p.catch((e: string) => logger.warn(`Error entering PiP mode: ${e}`));
// We should still dispatch ENTER_PICTURE_IN_PICTURE for cases where
// the external app needs to handle the event (ie. react-native-sdk)
p.finally(() => dispatch({ type: ENTER_PICTURE_IN_PICTURE }));
}
};
}

View File

@@ -1,5 +1,7 @@
import { getAppProp } from '../../base/app/functions';
import {
CONFERENCE_BLURRED,
CONFERENCE_FOCUSED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN
@@ -8,6 +10,7 @@ import { PARTICIPANT_JOINED } from '../../base/participants/actionTypes';
import MiddlewareRegistry from '../../base/redux/MiddlewareRegistry';
import { READY_TO_CLOSE } from '../external-api/actionTypes';
import { participantToParticipantInfo } from '../external-api/functions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture/actionTypes';
import { isExternalAPIAvailable } from './functions';
@@ -22,17 +25,23 @@ const externalAPIEnabled = isExternalAPIAvailable();
const rnSdkHandlers = getAppProp(store, 'rnSdkHandlers');
switch (type) {
case READY_TO_CLOSE:
rnSdkHandlers?.onReadyToClose && rnSdkHandlers?.onReadyToClose();
case CONFERENCE_BLURRED:
rnSdkHandlers?.onConferenceBlurred && rnSdkHandlers?.onConferenceBlurred();
break;
case CONFERENCE_FOCUSED:
rnSdkHandlers?.onConferenceFocused && rnSdkHandlers?.onConferenceFocused();
break;
case CONFERENCE_JOINED:
rnSdkHandlers?.onConferenceJoined && rnSdkHandlers?.onConferenceJoined();
break;
case CONFERENCE_LEFT:
// Props are torn down at this point, perhaps need to leave this one out
break;
case CONFERENCE_WILL_JOIN:
rnSdkHandlers?.onConferenceWillJoin && rnSdkHandlers?.onConferenceWillJoin();
break;
case CONFERENCE_LEFT:
// Props are torn down at this point, perhaps need to leave this one out
case ENTER_PICTURE_IN_PICTURE:
rnSdkHandlers?.onEnterPictureInPicture && rnSdkHandlers?.onEnterPictureInPicture();
break;
case PARTICIPANT_JOINED: {
const { participant } = action;
@@ -41,10 +50,11 @@ const externalAPIEnabled = isExternalAPIAvailable();
rnSdkHandlers?.onParticipantJoined && rnSdkHandlers?.onParticipantJoined(participantInfo);
break;
}
case READY_TO_CLOSE:
rnSdkHandlers?.onReadyToClose && rnSdkHandlers?.onReadyToClose();
break;
}
return result;
}
);

View File

@@ -56,8 +56,22 @@ export function setNoiseSuppressionEnabled(enabled: boolean): any {
logger.info(`Attempting to set noise suppression enabled state: ${enabled}`);
if (enabled === noiseSuppressionEnabled) {
logger.warn(`Noise suppression enabled state already: ${enabled}`);
return;
}
// If there is no local audio, simply set the enabled state. Once an audio track is created
// the effects list will be applied.
if (!localAudio) {
dispatch(setNoiseSuppressionEnabledState(enabled));
return;
}
try {
if (enabled && !noiseSuppressionEnabled) {
if (enabled) {
if (!canEnableNoiseSuppression(state, dispatch, localAudio)) {
return;
}
@@ -66,12 +80,10 @@ export function setNoiseSuppressionEnabled(enabled: boolean): any {
dispatch(setNoiseSuppressionEnabledState(true));
logger.info('Noise suppression enabled.');
} else if (!enabled && noiseSuppressionEnabled) {
} else {
await localAudio.setEffect(undefined);
dispatch(setNoiseSuppressionEnabledState(false));
logger.info('Noise suppression disabled.');
} else {
logger.warn(`Noise suppression enabled state already: ${enabled}`);
}
} catch (error) {
logger.error(

View File

@@ -22,15 +22,6 @@ export function isNoiseSuppressionEnabled(state: IReduxState): boolean {
* @returns {boolean}
*/
export function canEnableNoiseSuppression(state: IReduxState, dispatch: IStore['dispatch'], localAudio: any): boolean {
if (!localAudio) {
dispatch(showWarningNotification({
titleKey: 'notify.noiseSuppressionFailedTitle',
descriptionKey: 'notify.noiseSuppressionNoTrackDescription'
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
return false;
}
const { channelCount } = localAudio.track.getSettings();
// Sharing screen audio implies an effect being applied to the local track, because currently we don't support

View File

@@ -1,3 +1,4 @@
import PersistenceRegistry from '../base/redux/PersistenceRegistry';
import ReducerRegistry from '../base/redux/ReducerRegistry';
import {
@@ -8,14 +9,18 @@ export interface INoiseSuppressionState {
enabled: boolean;
}
const STORE_NAME = 'features/noise-suppression';
const DEFAULT_STATE = {
enabled: false
};
PersistenceRegistry.register(STORE_NAME);
/**
* Reduces the Redux actions of the feature features/noise-suppression.
*/
ReducerRegistry.register<INoiseSuppressionState>('features/noise-suppression',
ReducerRegistry.register<INoiseSuppressionState>(STORE_NAME,
(state = DEFAULT_STATE, action): INoiseSuppressionState => {
const { enabled } = action;

View File

@@ -16,6 +16,19 @@ const NotificationsTransition = ({ children }: { children: ReactElement[]; }) =>
const toMount = children?.filter(child =>
childrenToRender.findIndex(c => c.props.uid === child.props.uid) === -1) ?? [];
/**
* Update current notifications.
* In some cases the UID is the same but the other props change.
* This way we make sure the notification displays the latest info.
*/
children.forEach(child => {
const index = childrenToRender.findIndex(c => c.props.uid === child.props.uid);
if (index !== -1) {
childrenToRender[index] = child;
}
});
if (toUnmount.length > 0) {
const ids = new Map(timeoutIds);

View File

@@ -6,28 +6,43 @@ import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../../../analytics/functions';
import { hideSheet } from '../../../../../base/dialog/actions';
import { hideSheet, openDialog } from '../../../../../base/dialog/actions';
import BottomSheet from '../../../../../base/dialog/components/native/BottomSheet';
import Icon from '../../../../../base/icons/components/Icon';
import { IconCloseLarge, IconRingGroup } from '../../../../../base/icons/svg';
import { IconCloseLarge, IconEdit, IconRingGroup } from '../../../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { getBreakoutRoomsConfig } from '../../../../../breakout-rooms/functions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { isBreakoutRoomRenameAllowed } from '../../../../functions';
import { BREAKOUT_CONTEXT_MENU_ACTIONS as ACTIONS } from '../../../../types';
import styles from '../../../native/styles';
import BreakoutRoomNamePrompt from './BreakoutRoomNamePrompt';
/**
* An array with all possible breakout rooms actions.
*/
const ALL_ACTIONS = [ ACTIONS.JOIN, ACTIONS.REMOVE, ACTIONS.RENAME ];
interface IProps {
/**
* The actions that will be displayed.
*/
actions: Array<ACTIONS>;
/**
* The room for which the menu is open.
*/
room: IRoom;
}
const BreakoutRoomContextMenu = ({ room }: IProps) => {
const BreakoutRoomContextMenu = ({ room, actions = ALL_ACTIONS }: IProps) => {
const dispatch = useDispatch();
const isLocalModerator = useSelector(isLocalParticipantModerator);
const { hideJoinRoomButton } = useSelector(getBreakoutRoomsConfig);
const _isBreakoutRoomRenameAllowed = useSelector(isBreakoutRoomRenameAllowed);
const { t } = useTranslation();
const onJoinRoom = useCallback(() => {
@@ -41,6 +56,14 @@ const BreakoutRoomContextMenu = ({ room }: IProps) => {
dispatch(hideSheet());
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
breakoutRoomJid: room.jid,
initialRoomName: room.name
}));
dispatch(hideSheet());
}, [ dispatch, room ]);
const onCloseBreakoutRoom = useCallback(() => {
dispatch(closeBreakoutRoom(room.id));
dispatch(hideSheet());
@@ -51,7 +74,7 @@ const BreakoutRoomContextMenu = ({ room }: IProps) => {
addScrollViewPadding = { false }
showSlidingView = { true }>
{
!hideJoinRoomButton && (
!hideJoinRoomButton && actions.includes(ACTIONS.JOIN) && (
<TouchableOpacity
onPress = { onJoinRoom }
style = { styles.contextMenuItem as ViewStyle }>
@@ -62,7 +85,17 @@ const BreakoutRoomContextMenu = ({ room }: IProps) => {
</TouchableOpacity>
)
}
{!room?.isMainRoom && isLocalModerator
{!room?.isMainRoom && actions.includes(ACTIONS.RENAME) && _isBreakoutRoomRenameAllowed
&& <TouchableOpacity
onPress = { onRenameBreakoutRoom }
style = { styles.contextMenuItem as ViewStyle }>
<Icon
size = { 24 }
src = { IconEdit } />
<Text style = { styles.contextMenuItemText }>{t('breakoutRooms.actions.rename')}</Text>
</TouchableOpacity>
}
{!room?.isMainRoom && isLocalModerator && actions.includes(ACTIONS.REMOVE)
&& (room?.participants && Object.keys(room.participants).length > 0
? <TouchableOpacity
onPress = { onCloseBreakoutRoom }

View File

@@ -0,0 +1,34 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import InputDialog from '../../../../../base/dialog/components/native/InputDialog';
import { renameBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { IBreakoutRoomNamePromptProps as IProps } from '../../../../types';
/**
* Implements a component to render a breakout room name prompt.
*
* @param {IProps} props - The props of the component.
* @returns {JSX.Element}
*/
export default function BreakoutRoomNamePrompt({ breakoutRoomJid, initialRoomName }: IProps) {
const dispatch = useDispatch();
const onSubmit = useCallback((roomName: string) => {
const formattedRoomName = roomName?.trim();
if (formattedRoomName) {
dispatch(renameBreakoutRoom(formattedRoomName, roomName));
return true;
}
return false;
}, [ breakoutRoomJid, dispatch ]);
return (
<InputDialog
descriptionKey = 'dialog.renameBreakoutRoomTitle'
initialValue = { initialRoomName?.trim() }
onSubmit = { onSubmit } />
);
}

View File

@@ -0,0 +1,47 @@
import React, { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import Dialog from '../../../../../base/ui/components/web/Dialog';
import Input from '../../../../../base/ui/components/web/Input';
import { renameBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { IBreakoutRoomNamePromptProps as IProps } from '../../../../types';
/**
* Implements a React {@code Component} for displaying a dialog with an field
* for setting a breakout room's name.
*
* @param {IProps} props - The props of the component.
* @returns {JSX.Element}
*/
export default function BreakoutRoomNamePrompt({ breakoutRoomJid, initialRoomName }: IProps) {
const [ roomName, setRoomName ] = useState(initialRoomName?.trim());
const { t } = useTranslation();
const okDisabled = !roomName;
const dispatch = useDispatch();
const onBreakoutRoomNameChange = useCallback((newRoomName: string) => {
setRoomName(newRoomName?.trim());
}, [ setRoomName ]);
const onSubmit = useCallback(() => {
dispatch(renameBreakoutRoom(breakoutRoomJid, roomName));
}, [ breakoutRoomJid, dispatch, roomName ]);
return (<Dialog
ok = {{
disabled: okDisabled,
translationKey: 'dialog.Ok'
}}
onSubmit = { onSubmit }
titleKey = 'dialog.renameBreakoutRoomTitle'>
<Input
autoFocus = { true }
className = 'dialog-bottom-margin'
id = 'breakout-rooms-name-input'
label = { t('dialog.renameBreakoutRoomLabel') }
name = 'breakoutRoomName'
onChange = { onBreakoutRoomNameChange }
type = 'text'
value = { roomName } />
</Dialog>);
}

View File

@@ -0,0 +1,53 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import { openDialog } from '../../../../../base/dialog/actions';
import { IconEdit } from '../../../../../base/icons/svg';
import BreakoutRoomNamePrompt from './BreakoutRoomNamePrompt';
const useStyles = makeStyles()(_theme => {
return {
container: {
position: 'absolute',
cursor: 'pointer',
marginTop: 2,
marginLeft: 5
}
};
});
interface IProps {
breakoutRoomJid: string;
name?: string;
}
/**
* Implements the rename button component which is displayed only for renaming a breakout room which is joined by the
* user.
*
* @param {IProps} props - The props of the component.
* @returns {JSX.Element}
*/
export default function RenameButton({ breakoutRoomJid, name }: IProps) {
const dispatch = useDispatch();
const { classes, cx } = useStyles();
const onRename = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
breakoutRoomJid,
initialRoomName: name
}));
}, [ dispatch, breakoutRoomJid, name ]);
return (
<span
className = { cx('jitsi-icon jitsi-icon-default', classes.container) }
onClick = { onRename }>
<IconEdit
height = { 16 }
key = { 1 }
width = { 16 } />
</span>
);
}

View File

@@ -4,13 +4,18 @@ import { useDispatch, useSelector } from 'react-redux';
import { createBreakoutRoomsEvent } from '../../../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../../../analytics/functions';
import { IconCloseLarge, IconRingGroup } from '../../../../../base/icons/svg';
import { openDialog } from '../../../../../base/dialog/actions';
import { IconCloseLarge, IconEdit, IconRingGroup } from '../../../../../base/icons/svg';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import ContextMenu from '../../../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup';
import { closeBreakoutRoom, moveToRoom, removeBreakoutRoom } from '../../../../../breakout-rooms/actions';
import { IRoom } from '../../../../../breakout-rooms/types';
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
import { isBreakoutRoomRenameAllowed } from '../../../../functions';
import BreakoutRoomNamePrompt from './BreakoutRoomNamePrompt';
interface IProps {
@@ -50,6 +55,7 @@ export const RoomContextMenu = ({
const dispatch = useDispatch();
const { t } = useTranslation();
const isLocalModerator = useSelector(isLocalParticipantModerator);
const _isBreakoutRoomRenameAllowed = useSelector(isBreakoutRoomRenameAllowed);
const _overflowDrawer = useSelector(showOverflowDrawer);
const onJoinRoom = useCallback(() => {
@@ -61,6 +67,13 @@ export const RoomContextMenu = ({
dispatch(removeBreakoutRoom(room?.jid ?? ''));
}, [ dispatch, room ]);
const onRenameBreakoutRoom = useCallback(() => {
dispatch(openDialog(BreakoutRoomNamePrompt, {
breakoutRoomJid: room?.jid,
initialRoomName: room?.name
}));
}, [ dispatch, room ]);
const onCloseBreakoutRoom = useCallback(() => {
dispatch(closeBreakoutRoom(room?.id ?? ''));
}, [ dispatch, room ]);
@@ -74,6 +87,13 @@ export const RoomContextMenu = ({
onClick: onJoinRoom,
text: t('breakoutRooms.actions.join')
} : null,
!room?.isMainRoom && _isBreakoutRoomRenameAllowed ? {
accessibilityLabel: t('breakoutRooms.actions.rename'),
icon: IconEdit,
id: `rename-room-${room?.id}`,
onClick: onRenameBreakoutRoom,
text: t('breakoutRooms.actions.rename')
} : null,
!room?.isMainRoom && isLocalModerator ? {
accessibilityLabel: isRoomEmpty ? t('breakoutRooms.actions.remove') : t('breakoutRooms.actions.close'),
icon: IconCloseLarge,

View File

@@ -4,12 +4,18 @@ import { useSelector } from 'react-redux';
import { makeStyles } from 'tss-react/mui';
import Avatar from '../../../../../base/avatar/components/Avatar';
import {
getButtonNotifyMode,
getParticipantMenuButtonsWithNotifyClick
} from '../../../../../base/config/functions.web';
import { isLocalParticipantModerator } from '../../../../../base/participants/functions';
import ContextMenu from '../../../../../base/ui/components/web/ContextMenu';
import ContextMenuItemGroup from '../../../../../base/ui/components/web/ContextMenuItemGroup';
import { getBreakoutRooms } from '../../../../../breakout-rooms/functions';
import { NOTIFY_CLICK_MODE } from '../../../../../toolbox/constants';
import { showOverflowDrawer } from '../../../../../toolbox/functions.web';
import SendToRoomButton from '../../../../../video-menu/components/web/SendToRoomButton';
import { PARTICIPANT_MENU_BUTTONS as BUTTONS } from '../../../../../video-menu/constants';
import { AVATAR_SIZE } from '../../../../constants';
@@ -72,11 +78,30 @@ export const RoomParticipantContextMenu = ({
const lowerMenu = useCallback(() => onSelect(true), [ onSelect ]);
const rooms: Object = useSelector(getBreakoutRooms);
const overflowDrawer = useSelector(showOverflowDrawer);
const buttonsWithNotifyClick = useSelector(getParticipantMenuButtonsWithNotifyClick);
const notifyClick = useCallback(
(buttonKey: string, participantId?: string) => {
const notifyMode = getButtonNotifyMode(buttonKey, buttonsWithNotifyClick);
if (!notifyMode) {
return;
}
APP.API.notifyParticipantMenuButtonClicked(
buttonKey,
participantId,
notifyMode === NOTIFY_CLICK_MODE.PREVENT_AND_NOTIFY
);
}, [ buttonsWithNotifyClick, getButtonNotifyMode ]);
const breakoutRoomsButtons = useMemo(() => Object.values(rooms || {}).map((room: any) => {
if (room.id !== entity?.room?.id) {
return (<SendToRoomButton
key = { room.id }
// eslint-disable-next-line react/jsx-no-bind
notifyClick = { () => notifyClick(BUTTONS.SEND_PARTICIPANT_TO_ROOM, entity?.jid) }
notifyMode = { getButtonNotifyMode(BUTTONS.SEND_PARTICIPANT_TO_ROOM, buttonsWithNotifyClick) }
onClick = { lowerMenu }
participantID = { entity?.jid ?? '' }
room = { room } />);

View File

@@ -4,6 +4,7 @@ import { FlatList, Text } from 'react-native';
import { connect } from 'react-redux';
import { IReduxState, IStore } from '../../../app/types';
import { openSheet } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import Icon from '../../../base/icons/components/Icon';
import { IconAddUser } from '../../../base/icons/svg';
@@ -24,9 +25,12 @@ import {
import { doInvitePeople } from '../../../invite/actions.native';
import { getInviteOthersControl } from '../../../share-room/functions';
import {
isCurrentRoomRenamable,
participantMatchesSearch,
shouldRenderInviteButton
} from '../../functions';
import { BREAKOUT_CONTEXT_MENU_ACTIONS } from '../../types';
import BreakoutRoomContextMenu from '../breakout-rooms/components/native/BreakoutRoomContextMenu';
import CollapsibleList from './CollapsibleList';
import MeetingParticipantItem from './MeetingParticipantItem';
@@ -50,6 +54,11 @@ interface IProps extends WithTranslation {
*/
_isAddPeopleFeatureEnabled: boolean;
/**
* Indicates whether the room that is currently joined can be renamed.
*/
_isCurrentRoomRenamable: boolean;
/**
* The local participant.
*/
@@ -126,6 +135,7 @@ class MeetingParticipantList extends PureComponent<IProps> {
this._keyExtractor = this._keyExtractor.bind(this);
this._onInvite = this._onInvite.bind(this);
this._openContextMenu = this._openContextMenu.bind(this);
this._renderParticipant = this._renderParticipant.bind(this);
this._onSearchStringChange = this._onSearchStringChange.bind(this);
}
@@ -185,6 +195,18 @@ class MeetingParticipantList extends PureComponent<IProps> {
this.props.setSearchString(text);
}
/**
* Opens the context menu to rename the current breakout room.
*
* @returns {void}
*/
_openContextMenu() {
this.props.dispatch(openSheet(BreakoutRoomContextMenu, {
room: this.props._currentRoom,
actions: [ BREAKOUT_CONTEXT_MENU_ACTIONS.RENAME ]
}));
}
/**
* Implements React's {@link Component#render()}.
*
@@ -194,6 +216,7 @@ class MeetingParticipantList extends PureComponent<IProps> {
render() {
const {
_currentRoom,
_isCurrentRoomRenamable,
_inviteOthersControl,
_localParticipant,
_participantsCount,
@@ -229,6 +252,7 @@ class MeetingParticipantList extends PureComponent<IProps> {
const _visitorsLabelText = _visitorsCount > 0
? t('participantsPane.headings.visitors', { count: _visitorsCount })
: undefined;
const onLongPress = _isCurrentRoomRenamable ? this._openContextMenu : undefined;
return (
<>
@@ -236,7 +260,8 @@ class MeetingParticipantList extends PureComponent<IProps> {
}
<CollapsibleList
containerStyle = { finalContainerStyle }
title = { title } >
onLongPress = { onLongPress }
title = { title }>
{
_showInviteButton
&& <Button
@@ -298,6 +323,7 @@ function _mapStateToProps(state: IReduxState) {
return {
_currentRoom,
_isAddPeopleFeatureEnabled,
_isCurrentRoomRenamable: isCurrentRoomRenamable(state),
_inviteOthersControl,
_participantsCount,
_remoteParticipants,

View File

@@ -16,8 +16,9 @@ import { normalizeAccents } from '../../../base/util/strings.web';
import { getBreakoutRooms, getCurrentRoomId, isInBreakoutRoom } from '../../../breakout-rooms/functions';
import { showOverflowDrawer } from '../../../toolbox/functions.web';
import { muteRemote } from '../../../video-menu/actions.web';
import { getSortedParticipantIds, shouldRenderInviteButton } from '../../functions';
import { getSortedParticipantIds, isCurrentRoomRenamable, shouldRenderInviteButton } from '../../functions';
import { useParticipantDrawer } from '../../hooks';
import RenameButton from '../breakout-rooms/components/web/RenameButton';
import { InviteButton } from './InviteButton';
import MeetingParticipantContextMenu from './MeetingParticipantContextMenu';
@@ -50,7 +51,10 @@ const useStyles = makeStyles()(theme => {
});
interface IProps {
currentRoom?: { name: string; };
currentRoom?: {
jid: string;
name: string;
};
overflowDrawer?: boolean;
participantsCount?: number;
searchString: string;
@@ -102,6 +106,7 @@ function MeetingParticipants({
const youText = t('chat.you');
const isBreakoutRoom = useSelector(isInBreakoutRoom);
const visitorsCount = useSelector((state: IReduxState) => state['features/visitors'].count || 0);
const _isCurrentRoomRenamable = useSelector(isCurrentRoomRenamable);
const { classes: styles, cx } = useStyles();
@@ -122,6 +127,10 @@ function MeetingParticipants({
{currentRoom?.name
? `${currentRoom.name} (${participantsCount})`
: t('participantsPane.headings.participantsList', { count: participantsCount })}
{ currentRoom?.name && _isCurrentRoomRenamable
&& <RenameButton
breakoutRoomJid = { currentRoom?.jid }
name = { currentRoom?.name } /> }
</div>
{showInviteButton && <InviteButton />}
<Input

View File

@@ -6,6 +6,7 @@ import {
isSupported
} from '../av-moderation/functions';
import { IStateful } from '../base/app/types';
import { getCurrentConference } from '../base/conference/functions';
import { INVITE_ENABLED } from '../base/flags/constants';
import { getFeatureFlag } from '../base/flags/functions';
import { MEDIA_TYPE, type MediaType } from '../base/media/constants';
@@ -20,6 +21,7 @@ import {
import { IParticipant } from '../base/participants/types';
import { toState } from '../base/redux/functions';
import { normalizeAccents } from '../base/util/strings';
import { BREAKOUT_ROOMS_RENAME_FEATURE } from '../breakout-rooms/constants';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
import { MEDIA_STATE, QUICK_ACTION_BUTTON, REDUCER_KEY } from './constants';
@@ -282,3 +284,28 @@ export const isMuteAllVisible = (state: IReduxState) => {
return inBreakoutRoom ? false : !hideMuteAllButton && isLocalModerator;
};
/**
* Returns true if reanming the currently joined breakout room is allowed and false otherwise.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if reanming the currently joined breakout room is allowed and false otherwise.
*/
export function isCurrentRoomRenamable(state: IReduxState) {
return isInBreakoutRoom(state) && isBreakoutRoomRenameAllowed(state);
}
/**
* Returns true if renaming a breakout room is allowed and false otherwise.
*
* @param {IReduxState} state - The redux state.
* @returns {boolean} - True if renaming a breakout room is allowed and false otherwise.
*/
export function isBreakoutRoomRenameAllowed(state: IReduxState) {
const isLocalModerator = isLocalParticipantModerator(state);
const conference = getCurrentConference(state);
const isRenameBreakoutRoomsSupported
= conference?.getBreakoutRooms().isFeatureSupported(BREAKOUT_ROOMS_RENAME_FEATURE);
return isLocalModerator && isRenameBreakoutRoomsSupported;
}

View File

@@ -0,0 +1,36 @@
/**
* The type of the React {@code Component} props of {@link BreakoutRoomNamePrompt}.
*/
export interface IBreakoutRoomNamePromptProps {
/**
* The jid of the breakout room to rename.
*/
breakoutRoomJid: string;
/**
* The initial breakout room name.
*/
initialRoomName: string;
}
/**
* The available actions for breakout rooms context menu.
*/
export enum BREAKOUT_CONTEXT_MENU_ACTIONS {
/**
* Join breakout room.
*/
JOIN = 1,
/**
* Rename breakout room.
*/
RENAME = 2,
/**
* Remove breakout room.
*/
REMOVE = 3
}

View File

@@ -19,11 +19,6 @@ export const SET_DEVICE_STATUS = 'SET_DEVICE_STATUS';
*/
export const SET_SKIP_PREJOIN_RELOAD = 'SET_SKIP_PREJOIN_RELOAD';
/**
* Action type used to set the mandatory stance of the prejoin display name.
*/
export const SET_PREJOIN_DISPLAY_NAME_REQUIRED = 'SET_PREJOIN_DISPLAY_NAME_REQUIRED';
/**
* Action type to set the country to dial out to.
*/

View File

@@ -1,14 +0,0 @@
import {
SET_PREJOIN_DISPLAY_NAME_REQUIRED
} from './actionTypes';
/**
* Action used to set the stance of the display name.
*
* @returns {Object}
*/
export function setPrejoinDisplayNameRequired() {
return {
type: SET_PREJOIN_DISPLAY_NAME_REQUIRED
};
}

Some files were not shown because too many files have changed in this diff Show More