mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-05-25 09:37:48 +00:00
Compare commits
60 Commits
3392
...
util-lua-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ed9d5893b | ||
|
|
07b7f03aa7 | ||
|
|
7ce44f85ca | ||
|
|
41e0d782ce | ||
|
|
2a8fafdd36 | ||
|
|
faee1c139e | ||
|
|
eb644987ce | ||
|
|
de60a70daf | ||
|
|
2904dfa794 | ||
|
|
d51cf7c581 | ||
|
|
f25e6c6a5d | ||
|
|
5fb9422513 | ||
|
|
d01cfc8466 | ||
|
|
fa3888991f | ||
|
|
ded355a807 | ||
|
|
b655c8d54a | ||
|
|
42a6e6faaf | ||
|
|
c7954c284d | ||
|
|
251da1861a | ||
|
|
9712804040 | ||
|
|
fecbef0aff | ||
|
|
d65b71b584 | ||
|
|
579d291bca | ||
|
|
871026f4ba | ||
|
|
9a8a070c62 | ||
|
|
7cf4c7bd78 | ||
|
|
72a1def571 | ||
|
|
0dad99c3b7 | ||
|
|
840c0190c4 | ||
|
|
e0fdeea69b | ||
|
|
e3612929f8 | ||
|
|
70921bb6ef | ||
|
|
371ca4eef1 | ||
|
|
54fdb7066f | ||
|
|
85bcb0c757 | ||
|
|
d387cbe5bd | ||
|
|
1bc28e4904 | ||
|
|
cb3419ba2a | ||
|
|
a2f8e156da | ||
|
|
a4cf79c161 | ||
|
|
9352517705 | ||
|
|
47d5163c52 | ||
|
|
def22b01bb | ||
|
|
9445cf99fd | ||
|
|
96b226de24 | ||
|
|
5101f69e4e | ||
|
|
61b66e0edf | ||
|
|
700051f809 | ||
|
|
d16e10baec | ||
|
|
11e5c14f83 | ||
|
|
ded58d77d1 | ||
|
|
8642c372c4 | ||
|
|
edbf591059 | ||
|
|
ded4291d6a | ||
|
|
a14fead0f3 | ||
|
|
466e1e3eb8 | ||
|
|
8a90f0dab1 | ||
|
|
d7e0aa3f61 | ||
|
|
37b343a797 | ||
|
|
149485905c |
@@ -10,6 +10,7 @@ MVN_HTTP=0
|
||||
DEFAULT_SDK_VERSION=$(grep sdkVersion ${THIS_DIR}/../gradle.properties | cut -d"=" -f2)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
RN_VERSION=$(jq -r '.dependencies."react-native"' ${THIS_DIR}/../../package.json)
|
||||
DO_GIT_TAG=${GIT_TAG:-0}
|
||||
|
||||
if [[ $THE_MVN_REPO == http* ]]; then
|
||||
MVN_HTTP=1
|
||||
@@ -64,13 +65,11 @@ pushd ${THIS_DIR}/../
|
||||
./gradlew clean assembleRelease publish
|
||||
popd
|
||||
|
||||
if [[ $MVN_HTTP == 0 ]]; then
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
# The artifacts are now on the Maven repo, commit them
|
||||
pushd ${MVN_REPO_PATH}
|
||||
if [[ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" == "true" ]]; then
|
||||
git add -A .
|
||||
git commit -m "Jitsi Meet SDK + dependencies"
|
||||
fi
|
||||
git add -A .
|
||||
git commit -m "Jitsi Meet SDK + dependencies: ${SDK_VERSION}"
|
||||
popd
|
||||
|
||||
# Tag the release
|
||||
|
||||
@@ -53,6 +53,7 @@ dependencies {
|
||||
|
||||
implementation project(':react-native-background-timer')
|
||||
implementation project(':react-native-calendar-events')
|
||||
implementation project(':react-native-community-async-storage')
|
||||
implementation(project(':react-native-fast-image')) {
|
||||
exclude group: 'com.android.support'
|
||||
}
|
||||
@@ -63,7 +64,6 @@ dependencies {
|
||||
implementation project(':react-native-vector-icons')
|
||||
implementation project(':react-native-webrtc')
|
||||
implementation project(':react-native-webview')
|
||||
implementation project(':@react-native-community_async-storage')
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
}
|
||||
@@ -207,8 +207,7 @@ publishing {
|
||||
def groupId = it.moduleGroup
|
||||
def artifactId = it.moduleName
|
||||
|
||||
if ((artifactId.startsWith('react-native-') || artifactId.startsWith('@react-native-community'))
|
||||
&& groupId.equals('jitsi-meet')) {
|
||||
if (artifactId.startsWith('react-native-') && groupId.equals('jitsi-meet')) {
|
||||
groupId = rootProject.ext.moduleGroupId
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,17 @@ class AmplitudeModule
|
||||
Amplitude.getInstance(instanceName).initialize(getCurrentActivity(), apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user ID for an Amplitude instance.
|
||||
*
|
||||
* @param instanceName The name of the Amplitude instance.
|
||||
* @param userId The new value for the user ID.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void setUserId(String instanceName, String userId) {
|
||||
Amplitude.getInstance(instanceName).setUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user properties for an Amplitude instance.
|
||||
*
|
||||
|
||||
@@ -40,6 +40,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
* Room name.
|
||||
*/
|
||||
private String room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
private String subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -70,6 +74,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
public static class Builder {
|
||||
private URL serverURL;
|
||||
private String room;
|
||||
private String subject;
|
||||
private String token;
|
||||
|
||||
private Bundle colorScheme;
|
||||
@@ -105,6 +110,17 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the conference subject.
|
||||
* @param subject - Subject for the conference.
|
||||
* @return - The {@link Builder} object itself so the method calls can be chained.
|
||||
*/
|
||||
public Builder setSubject(String subject) {
|
||||
this.subject = subject;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the JWT token to be used for authentication when joining a conference.
|
||||
* @param token - The JWT token to be used for authentication.
|
||||
@@ -185,6 +201,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
options.serverURL = this.serverURL;
|
||||
options.room = this.room;
|
||||
options.subject = this.subject;
|
||||
options.token = this.token;
|
||||
options.colorScheme = this.colorScheme;
|
||||
options.audioMuted = this.audioMuted;
|
||||
@@ -201,6 +218,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
|
||||
private JitsiMeetConferenceOptions(Parcel in) {
|
||||
room = in.readString();
|
||||
subject = in.readString();
|
||||
token = in.readString();
|
||||
colorScheme = in.readBundle();
|
||||
byte tmpAudioMuted = in.readByte();
|
||||
@@ -238,6 +256,9 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
if (videoMuted != null) {
|
||||
config.putBoolean("startWithVideoMuted", videoMuted);
|
||||
}
|
||||
if (subject != null) {
|
||||
config.putString("subject", subject);
|
||||
}
|
||||
|
||||
Bundle urlProps = new Bundle();
|
||||
|
||||
@@ -281,6 +302,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(room);
|
||||
dest.writeString(subject);
|
||||
dest.writeString(token);
|
||||
dest.writeBundle(colorScheme);
|
||||
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
|
||||
|
||||
@@ -66,7 +66,7 @@ class ReactInstanceManagerHolder {
|
||||
}
|
||||
|
||||
try {
|
||||
Class<?> amplitudeModuleClass = Class.forName("AmplitudeModule");
|
||||
Class<?> amplitudeModuleClass = Class.forName("org.jitsi.meet.sdk.AmplitudeModule");
|
||||
Constructor constructor = amplitudeModuleClass.getConstructor(ReactApplicationContext.class);
|
||||
nativeModules.add((NativeModule)constructor.newInstance(reactContext));
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -5,6 +5,8 @@ include ':react-native-background-timer'
|
||||
project(':react-native-background-timer').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-background-timer/android')
|
||||
include ':react-native-calendar-events'
|
||||
project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android')
|
||||
include ':react-native-community-async-storage'
|
||||
project(':react-native-community-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
|
||||
include ':react-native-fast-image'
|
||||
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fast-image/android')
|
||||
include ':react-native-google-signin'
|
||||
@@ -23,5 +25,3 @@ include ':react-native-webrtc'
|
||||
project(':react-native-webrtc').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webrtc/android')
|
||||
include ':react-native-webview'
|
||||
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
|
||||
include ':@react-native-community_async-storage'
|
||||
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android')
|
||||
|
||||
@@ -51,6 +51,8 @@ import {
|
||||
import {
|
||||
checkAndNotifyForNewDevice,
|
||||
getAvailableDevices,
|
||||
notifyCameraError,
|
||||
notifyMicError,
|
||||
setAudioOutputDeviceId,
|
||||
updateDeviceList
|
||||
} from './react/features/base/devices';
|
||||
@@ -486,10 +488,13 @@ class ConferenceConnector {
|
||||
* call in hangup() to resolve when all operations are finished.
|
||||
*/
|
||||
function disconnect() {
|
||||
connection.disconnect();
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
const onDisconnected = () => {
|
||||
APP.API.notifyConferenceLeft(APP.conference.roomName);
|
||||
|
||||
return Promise.resolve();
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return connection.disconnect().then(onDisconnected, onDisconnected);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -694,13 +699,14 @@ export default {
|
||||
// If both requests for 'audio' + 'video' and 'audio'
|
||||
// only failed, we assume that there are some problems
|
||||
// with user's microphone and show corresponding dialog.
|
||||
APP.UI.showMicErrorNotification(audioOnlyError);
|
||||
APP.UI.showCameraErrorNotification(videoOnlyError);
|
||||
APP.store.dispatch(notifyMicError(audioOnlyError));
|
||||
APP.store.dispatch(notifyCameraError(videoOnlyError));
|
||||
} else {
|
||||
// If request for 'audio' + 'video' failed, but request
|
||||
// for 'audio' only was OK, we assume that we had
|
||||
// problems with camera and show corresponding dialog.
|
||||
APP.UI.showCameraErrorNotification(audioAndVideoError);
|
||||
APP.store.dispatch(
|
||||
notifyCameraError(audioAndVideoError));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,6 +792,13 @@ export default {
|
||||
this.recorder = new Recorder();
|
||||
}
|
||||
|
||||
if (config.startSilent) {
|
||||
APP.store.dispatch(showNotification({
|
||||
descriptionKey: 'notify.startSilentDescription',
|
||||
titleKey: 'notify.startSilentTitle'
|
||||
}));
|
||||
}
|
||||
|
||||
// XXX The API will take care of disconnecting from the XMPP
|
||||
// server (and, thus, leaving the room) on unload.
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -839,7 +852,7 @@ export default {
|
||||
|
||||
if (!this.localAudio && !mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.UI.showMicErrorNotification(error);
|
||||
showUI && APP.store.dispatch(notifyMicError(error));
|
||||
};
|
||||
|
||||
createLocalTracksF({ devices: [ 'audio' ] }, false)
|
||||
@@ -902,7 +915,7 @@ export default {
|
||||
|
||||
if (!this.localVideo && !mute) {
|
||||
const maybeShowErrorDialog = error => {
|
||||
showUI && APP.UI.showCameraErrorNotification(error);
|
||||
showUI && APP.store.dispatch(notifyCameraError(error));
|
||||
};
|
||||
|
||||
// Try to create local video if there wasn't any.
|
||||
@@ -2109,7 +2122,7 @@ export default {
|
||||
this._updateVideoDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.UI.showCameraErrorNotification(err);
|
||||
APP.store.dispatch(notifyCameraError(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -2142,7 +2155,7 @@ export default {
|
||||
this._updateAudioDeviceId();
|
||||
})
|
||||
.catch(err => {
|
||||
APP.UI.showMicErrorNotification(err);
|
||||
APP.store.dispatch(notifyMicError(err));
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -2402,17 +2415,26 @@ export default {
|
||||
// Let's handle unknown/non-preferred devices
|
||||
const newAvailDevices
|
||||
= APP.store.getState()['features/base/devices'].availableDevices;
|
||||
let newAudioDevices = [];
|
||||
let oldAudioDevices = [];
|
||||
|
||||
if (typeof newDevices.audiooutput === 'undefined') {
|
||||
APP.store.dispatch(
|
||||
checkAndNotifyForNewDevice(newAvailDevices.audioOutput, oldDevices.audioOutput));
|
||||
newAudioDevices = newAvailDevices.audioOutput;
|
||||
oldAudioDevices = oldDevices.audioOutput;
|
||||
}
|
||||
|
||||
if (!requestedInput.audio) {
|
||||
APP.store.dispatch(
|
||||
checkAndNotifyForNewDevice(newAvailDevices.audioInput, oldDevices.audioInput));
|
||||
newAudioDevices = newAudioDevices.concat(newAvailDevices.audioInput);
|
||||
oldAudioDevices = oldAudioDevices.concat(oldDevices.audioInput);
|
||||
}
|
||||
|
||||
// check for audio
|
||||
if (newAudioDevices.length > 0) {
|
||||
APP.store.dispatch(
|
||||
checkAndNotifyForNewDevice(newAudioDevices, oldAudioDevices));
|
||||
}
|
||||
|
||||
// check for video
|
||||
if (!requestedInput.video) {
|
||||
APP.store.dispatch(
|
||||
checkAndNotifyForNewDevice(newAvailDevices.videoInput, oldDevices.videoInput));
|
||||
@@ -2591,8 +2613,7 @@ export default {
|
||||
leaveRoomAndDisconnect() {
|
||||
APP.store.dispatch(conferenceWillLeave(room));
|
||||
|
||||
return room.leave()
|
||||
.then(disconnect, disconnect);
|
||||
return room.leave().then(disconnect, disconnect);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
13
config.js
13
config.js
@@ -90,6 +90,10 @@ var config = {
|
||||
// applied locally. FIXME: having these 2 options is confusing.
|
||||
// startWithAudioMuted: false,
|
||||
|
||||
// Enabling it (with #params) will disable local audio output of remote
|
||||
// participants and to enable it back a reload is needed.
|
||||
// startSilent: false
|
||||
|
||||
// Video
|
||||
|
||||
// Sets the preferred resolution (height) for local video. Defaults to 720.
|
||||
@@ -269,6 +273,10 @@ var config = {
|
||||
// Enable lock room for all moderators, even when userRolesBasedOnToken is enabled and participants are guests.
|
||||
// lockRoomGuestEnabled: false,
|
||||
|
||||
// When enabled the password used for locking a room is restricted to up to the number of digits specified
|
||||
// roomPasswordNumberOfDigits: 10,
|
||||
// default: roomPasswordNumberOfDigits: false,
|
||||
|
||||
// Message to show the users. Example: 'The service will be down for
|
||||
// maintenance at 01:00 AM GMT,
|
||||
// noticeMessage: '',
|
||||
@@ -410,6 +418,10 @@ var config = {
|
||||
// use only.
|
||||
// _desktopSharingSourceDevice: 'sample-id-or-label'
|
||||
|
||||
// A property to disable the right click context menu for localVideo
|
||||
// the menu has option to flip the locally seen video for local presentations
|
||||
// disableLocalVideoFlip: false
|
||||
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
_immediateReloadThreshold
|
||||
@@ -424,7 +436,6 @@ var config = {
|
||||
dialOutCodesUrl
|
||||
disableRemoteControl
|
||||
displayJids
|
||||
enableLocalVideoFlip
|
||||
etherpad_base
|
||||
externalConnectUrl
|
||||
firefox_fake_device
|
||||
|
||||
@@ -514,6 +514,7 @@
|
||||
}
|
||||
#dominantSpeakerAvatar {
|
||||
background-color: #000000;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.dynamic-shadow {
|
||||
@@ -525,12 +526,21 @@
|
||||
transition: box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
.avatar-container {
|
||||
@include maxSize(60px);
|
||||
@include absoluteAligning();
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50%;
|
||||
overflow: hidden;
|
||||
width: auto;
|
||||
|
||||
.userAvatar {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
#videoNotAvailableScreen {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/**
|
||||
* Let the avatar grow with the tile.
|
||||
*/
|
||||
.userAvatar {
|
||||
.avatar-container {
|
||||
max-height: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
28
doc/api.md
28
doc/api.md
@@ -222,6 +222,11 @@ api.executeCommand('toggleChat');
|
||||
api.executeCommand('toggleShareScreen');
|
||||
```
|
||||
|
||||
* **toggleTileView** - Enter / exit tile view layout mode. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('toggleTileView');
|
||||
```
|
||||
|
||||
* **hangup** - Hangups the call. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('hangup');
|
||||
@@ -259,6 +264,14 @@ The `event` parameter is a String object with the name of the event.
|
||||
The `listener` parameter is a Function object with one argument that will be notified when the event occurs with data related to the event.
|
||||
|
||||
The following events are currently supported:
|
||||
* **cameraError** - event notifications about Jitsi-Meet having failed to access the camera. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
type: string, // A constant representing the overall type of the error.
|
||||
message: string // Additional information about the error.
|
||||
}
|
||||
```
|
||||
|
||||
* **avatarChanged** - event notifications about avatar
|
||||
changes. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
@@ -282,6 +295,14 @@ changes. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **micError** - event notifications about Jitsi-Meet having failed to access the mic. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
type: string, // A constant representing the overall type of the error.
|
||||
message: string // Additional information about the error.
|
||||
}
|
||||
```
|
||||
|
||||
* **screenSharingStatusChanged** - receives event notifications about turning on/off the local user screen sharing. The listener will receive object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
@@ -296,6 +317,13 @@ changes. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **tileViewChanged** - event notifications about tile view layout mode being entered or exited. The listener will receive object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
enabled: boolean, // whether tile view is not displayed or not
|
||||
}
|
||||
```
|
||||
|
||||
* **incomingMessage** - Event notifications about incoming
|
||||
messages. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
|
||||
@@ -167,13 +167,7 @@ var interfaceConfig = {
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
RECENT_LIST_ENABLED: true,
|
||||
|
||||
/**
|
||||
* A UX mode where the last screen share participant is automatically
|
||||
* pinned. Note: this mode is experimental and subject to breakage.
|
||||
*/
|
||||
AUTO_PIN_LATEST_SCREEN_SHARE: true
|
||||
RECENT_LIST_ENABLED: true
|
||||
|
||||
/**
|
||||
* How many columns the tile view can expand to. The respected range is
|
||||
@@ -201,6 +195,12 @@ var interfaceConfig = {
|
||||
*/
|
||||
// ANDROID_APP_PACKAGE: 'org.jitsi.meet',
|
||||
|
||||
/**
|
||||
* A UX mode where the last screen share participant is automatically
|
||||
* pinned. Note: this mode is experimental and subject to breakage.
|
||||
*/
|
||||
// AUTO_PIN_LATEST_SCREEN_SHARE: false,
|
||||
|
||||
/**
|
||||
* Override the behavior of some notifications to remain displayed until
|
||||
* explicitly dismissed through a user action. The value is how long, in
|
||||
|
||||
@@ -92,7 +92,7 @@ Leaves the currently active conference.
|
||||
|
||||
#### Universal / deep linking
|
||||
|
||||
In order to support Universal / deep linking, `JitsiMeetView` offers 2 class
|
||||
In order to support Universal / deep linking, `JitsiMeet` offers 2 class
|
||||
methods that you app's delegate should call in order for the app to follow those
|
||||
links.
|
||||
|
||||
@@ -104,7 +104,7 @@ is useful when the host application uses other SDKs which also use linking.
|
||||
continueUserActivity:(NSUserActivity *)userActivity
|
||||
restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler
|
||||
{
|
||||
return [JitsiMeetView application:application
|
||||
return [[JitsiMeet sharedInstance] application:application
|
||||
continueUserActivity:userActivity
|
||||
restorationHandler:restorationHandler];
|
||||
}
|
||||
@@ -117,7 +117,7 @@ And also one of the following:
|
||||
- (BOOL)application:(UIApplication *)app
|
||||
openURL:(NSURL *)url
|
||||
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
||||
return [JitsiMeetView application:app
|
||||
return [[JitsiMeet sharedInstance] application:app
|
||||
openURL:url
|
||||
options: options];
|
||||
}
|
||||
|
||||
@@ -737,7 +737,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEAD_CODE_STRIPPING = NO;
|
||||
DEVELOPMENT_TEAM = FC967L3QRG;
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"../../node_modules/react-native-webrtc/ios",
|
||||
|
||||
@@ -42,11 +42,11 @@
|
||||
|
||||
- (void)_onJitsiMeetViewDelegateEvent:(NSString *)name
|
||||
withData:(NSDictionary *)data {
|
||||
#if DEBUG
|
||||
NSLog(
|
||||
@"[%s:%d] JitsiMeetViewDelegate %@ %@",
|
||||
__FILE__, __LINE__, name, data);
|
||||
|
||||
#if DEBUG
|
||||
NSAssert(
|
||||
[NSThread isMainThread],
|
||||
@"JitsiMeetViewDelegate %@ method invoked on a non-main thread",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -14,6 +14,7 @@
|
||||
<objects>
|
||||
<controller title="Meetings" id="AgC-eL-Hgc" customClass="InterfaceController" customModule="JitsiMeetCompanion" customModuleProvider="target">
|
||||
<items>
|
||||
<label alignment="left" textAlignment="left" numberOfLines="0" id="OQN-sx-tDt"/>
|
||||
<table alignment="left" id="gpO-ql-Xsr">
|
||||
<items>
|
||||
<tableRow identifier="MeetingRowType" id="GGl-av-xeJ" customClass="MeetingRowController" customModule="JitsiMeetCompanion_Extension">
|
||||
@@ -39,6 +40,7 @@
|
||||
</table>
|
||||
</items>
|
||||
<connections>
|
||||
<outlet property="infoLabel" destination="OQN-sx-tDt" id="Juv-tb-SNj"/>
|
||||
<outlet property="table" destination="gpO-ql-Xsr" id="aVV-iZ-z3l"/>
|
||||
</connections>
|
||||
</controller>
|
||||
|
||||
@@ -16,17 +16,31 @@
|
||||
*/
|
||||
|
||||
import WatchKit
|
||||
import WatchConnectivity
|
||||
import Foundation
|
||||
|
||||
|
||||
class InterfaceController: WKInterfaceController {
|
||||
|
||||
@IBOutlet var infoLabel: WKInterfaceLabel!
|
||||
@IBOutlet var table: WKInterfaceTable!
|
||||
|
||||
override func didAppear(){
|
||||
self.updateUI(ExtensionDelegate.currentJitsiMeetContext)
|
||||
}
|
||||
|
||||
func updateUI(_ newContext:JitsiMeetContext) {
|
||||
if let recentURLsArray = newContext.recentURLs {
|
||||
updateRecents(withRecents: recentURLsArray, currentContext: newContext)
|
||||
}
|
||||
if (newContext.recentURLs == nil || newContext.recentURLs!.count == 0) {
|
||||
infoLabel.setText("There are no recent meetings. Please use the app on the phone to start a new call.")
|
||||
|
||||
table.setHidden(true)
|
||||
infoLabel.setHidden(false)
|
||||
} else {
|
||||
updateRecents(withRecents: newContext.recentURLs!, currentContext: newContext)
|
||||
|
||||
table.setHidden(false)
|
||||
infoLabel.setHidden(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRecents(withRecents recents: NSArray, currentContext: JitsiMeetContext) {
|
||||
@@ -68,7 +82,7 @@ class InterfaceController: WKInterfaceController {
|
||||
override func contextForSegue(withIdentifier segueIdentifier: String, in table: WKInterfaceTable, rowIndex: Int) -> Any? {
|
||||
let controller = table.rowController(at: rowIndex) as! MeetingRowController
|
||||
let currentContext = ExtensionDelegate.currentJitsiMeetContext
|
||||
|
||||
|
||||
// Copy the current context and add the joinConferenceURL to trigger the command when the in-call screen is displayed
|
||||
let actionContext = JitsiMeetContext(jmContext: currentContext)
|
||||
actionContext.joinConferenceURL = controller.roomUrl
|
||||
|
||||
@@ -7,6 +7,7 @@ PROJECT_REPO=$(realpath ${THIS_DIR}/../..)
|
||||
RELEASE_REPO=$(realpath ${THIS_DIR}/../../../jitsi-meet-ios-sdk-releases)
|
||||
DEFAULT_SDK_VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" ${THIS_DIR}/../sdk/src/Info.plist)
|
||||
SDK_VERSION=${OVERRIDE_SDK_VERSION:-${DEFAULT_SDK_VERSION}}
|
||||
DO_GIT_TAG=${GIT_TAG:-0}
|
||||
|
||||
|
||||
echo "Releasing Jitsi Meet SDK ${SDK_VERSION}"
|
||||
@@ -25,7 +26,9 @@ popd
|
||||
pushd ${PROJECT_REPO}
|
||||
rm -rf ios/sdk/JitsiMeet.framework
|
||||
xcodebuild -workspace ios/jitsi-meet.xcworkspace -scheme JitsiMeet -destination='generic/platform=iOS' -configuration Release archive
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
git tag ios-sdk-${SDK_VERSION}
|
||||
fi
|
||||
popd
|
||||
|
||||
pushd ${RELEASE_REPO}
|
||||
@@ -39,9 +42,11 @@ xcrun bitcode_strip -r Frameworks/JitsiMeet.framework/JitsiMeet -o Frameworks/Ji
|
||||
xcrun bitcode_strip -r Frameworks/WebRTC.framework/WebRTC -o Frameworks/WebRTC.framework/WebRTC
|
||||
|
||||
# Add all files to git
|
||||
git add -A .
|
||||
git commit -m "${SDK_VERSION}"
|
||||
git tag ${SDK_VERSION}
|
||||
if [[ $DO_GIT_TAG == 1 ]]; then
|
||||
git add -A .
|
||||
git commit -m "${SDK_VERSION}"
|
||||
git tag ${SDK_VERSION}
|
||||
fi
|
||||
|
||||
popd
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
|
||||
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */; };
|
||||
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */ = {isa = PBXBuildFile; fileRef = DEAFA778229EAD520033A7FA /* RNRootView.m */; };
|
||||
DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */; };
|
||||
DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; };
|
||||
DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535521FB2E8300011A3A /* ReactUtils.m */; };
|
||||
@@ -96,6 +97,8 @@
|
||||
DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetConferenceOptions.h; sourceTree = "<group>"; };
|
||||
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetConferenceOptions.m; sourceTree = "<group>"; };
|
||||
DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetConferenceOptions+Private.h"; sourceTree = "<group>"; };
|
||||
DEAFA777229EAD3B0033A7FA /* RNRootView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNRootView.h; sourceTree = "<group>"; };
|
||||
DEAFA778229EAD520033A7FA /* RNRootView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNRootView.m; sourceTree = "<group>"; };
|
||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocaleDetector.m; sourceTree = "<group>"; };
|
||||
DEFE535321FB1BF800011A3A /* JitsiMeet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeet.m; sourceTree = "<group>"; };
|
||||
DEFE535521FB2E8300011A3A /* ReactUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactUtils.m; sourceTree = "<group>"; };
|
||||
@@ -173,6 +176,8 @@
|
||||
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */,
|
||||
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
|
||||
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
|
||||
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
|
||||
DEAFA778229EAD520033A7FA /* RNRootView.m */,
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */,
|
||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */,
|
||||
DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */,
|
||||
@@ -478,6 +483,7 @@
|
||||
files = (
|
||||
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
|
||||
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
|
||||
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */,
|
||||
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */,
|
||||
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
|
||||
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
|
||||
@@ -634,7 +640,7 @@
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
DYLIB_CURRENT_VERSION = 1;
|
||||
DYLIB_INSTALL_NAME_BASE = "@rpath";
|
||||
ENABLE_BITCODE = YES;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = src/Info.plist;
|
||||
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
|
||||
#import "JitsiMeet.h"
|
||||
|
||||
@interface JitsiMeet ()
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
* Room name.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *room;
|
||||
/**
|
||||
* Conference subject.
|
||||
*/
|
||||
@property (nonatomic, copy, nullable) NSString *subject;
|
||||
/**
|
||||
* JWT token used for authentication.
|
||||
*/
|
||||
@@ -56,7 +60,9 @@
|
||||
@interface JitsiMeetConferenceOptions : NSObject
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSURL *serverURL;
|
||||
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *room;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *subject;
|
||||
@property (nonatomic, copy, nullable, readonly) NSString *token;
|
||||
|
||||
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
if (self = [super init]) {
|
||||
_serverURL = nil;
|
||||
_room = nil;
|
||||
_subject = nil;
|
||||
_token = nil;
|
||||
|
||||
_colorScheme = nil;
|
||||
@@ -138,6 +139,7 @@
|
||||
if (self = [super init]) {
|
||||
_serverURL = builder.serverURL;
|
||||
_room = builder.room;
|
||||
_subject = builder.subject;
|
||||
_token = builder.token;
|
||||
|
||||
_colorScheme = builder.colorScheme;
|
||||
@@ -183,6 +185,9 @@
|
||||
if (_videoMuted != nil) {
|
||||
config[@"startWithVideoMuted"] = @(self.videoMuted);
|
||||
}
|
||||
if (_subject != nil) {
|
||||
config[@"subject"] = self.subject;
|
||||
}
|
||||
|
||||
NSMutableDictionary *urlProps = [[NSMutableDictionary alloc] init];
|
||||
|
||||
|
||||
@@ -17,12 +17,11 @@
|
||||
|
||||
#include <mach/mach_time.h>
|
||||
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#import "JitsiMeet+Private.h"
|
||||
#import "JitsiMeetConferenceOptions+Private.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
#import "ReactUtils.h"
|
||||
#import "RNRootView.h"
|
||||
|
||||
|
||||
@implementation JitsiMeetView {
|
||||
@@ -36,7 +35,7 @@
|
||||
/**
|
||||
* React Native view where the entire content will be rendered.
|
||||
*/
|
||||
RCTRootView *rootView;
|
||||
RNRootView *rootView;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,9 +144,9 @@ static void initializeViewsMap() {
|
||||
} else {
|
||||
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
|
||||
rootView
|
||||
= [[RCTRootView alloc] initWithBridge:bridge
|
||||
moduleName:@"App"
|
||||
initialProperties:props];
|
||||
= [[RNRootView alloc] initWithBridge:bridge
|
||||
moduleName:@"App"
|
||||
initialProperties:props];
|
||||
rootView.backgroundColor = self.backgroundColor;
|
||||
|
||||
// Add rootView as a subview which completely covers this one.
|
||||
|
||||
20
ios/sdk/src/RNRootView.h
Normal file
20
ios/sdk/src/RNRootView.h
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
@interface RNRootView : RCTRootView
|
||||
@end
|
||||
45
ios/sdk/src/RNRootView.m
Normal file
45
ios/sdk/src/RNRootView.m
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright @ 2019-present 8x8, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTRootContentView.h>
|
||||
#import <React/RCTLog.h>
|
||||
|
||||
#import "RNRootView.h"
|
||||
|
||||
@implementation RNRootView
|
||||
|
||||
// Monkey-patch RCTRootView.runApplication to avoid logging initial props.
|
||||
- (void)runApplication:(RCTBridge *)bridge
|
||||
{
|
||||
NSString *moduleName = [self valueForKey:@"_moduleName"] ?: @"";
|
||||
RCTRootContentView *_contentView = [self valueForKey:@"_contentView"];
|
||||
NSNumber *reactTag = [_contentView valueForKey:@"reactTag"];
|
||||
|
||||
NSDictionary *appParameters = @{
|
||||
@"rootTag": reactTag,
|
||||
@"initialProps": self.appProperties ?: @{},
|
||||
};
|
||||
#if DEBUG
|
||||
RCTLogInfo(@"Running application %@ (%@)", moduleName, appParameters);
|
||||
#endif
|
||||
|
||||
[bridge enqueueJSCall:@"AppRegistry"
|
||||
method:@"runApplication"
|
||||
args:@[moduleName, appParameters]
|
||||
completion:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -33,6 +33,10 @@ RCT_EXPORT_METHOD(init:(NSString*)instanceName API_KEY:(NSString*)apiKey) {
|
||||
[[Amplitude instanceWithName:instanceName] initializeApiKey:apiKey];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setUserId:(NSString*)instanceName userId: (NSString *) userId) {
|
||||
[[Amplitude instanceWithName:instanceName] setUserId:userId];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(setUserProperties:(NSString*)instanceName userPropsString:(NSDictionary*)userProps) {
|
||||
if (userProps != nil) {
|
||||
[[Amplitude instanceWithName:instanceName] setUserProperties:userProps];
|
||||
|
||||
@@ -21,6 +21,7 @@ import Foundation
|
||||
internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
|
||||
private let listeners = NSMutableArray()
|
||||
private var pendingMuteActions = Set<UUID>()
|
||||
|
||||
internal override init() {}
|
||||
|
||||
@@ -36,6 +37,12 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
// MARK: - Add mute action
|
||||
|
||||
func addMuteAction(_ actionUUID: UUID) {
|
||||
pendingMuteActions.insert(actionUUID)
|
||||
}
|
||||
|
||||
// MARK: - CXProviderDelegate
|
||||
|
||||
func providerDidReset(_ provider: CXProvider) {
|
||||
@@ -43,6 +50,7 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
let listener = $0 as! JMCallKitListener
|
||||
listener.providerDidReset?()
|
||||
}
|
||||
pendingMuteActions.removeAll()
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
|
||||
@@ -64,9 +72,20 @@ internal final class JMCallKitEmitter: NSObject, CXProviderDelegate {
|
||||
}
|
||||
|
||||
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
||||
listeners.forEach {
|
||||
let listener = $0 as! JMCallKitListener
|
||||
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
|
||||
let uuid = pendingMuteActions.remove(action.uuid)
|
||||
|
||||
// Avoid mute actions ping-pong: if the mute action was caused by
|
||||
// the JS side (we requested a transaction) don't call the delegate
|
||||
// method. If it was called by the provder itself (when the user presses
|
||||
// the mute button in the CallKit view) then call the delegate method.
|
||||
//
|
||||
// NOTE: don't try to be clever and remove this. Been there, done that.
|
||||
// Won't work.
|
||||
if (uuid == nil) {
|
||||
listeners.forEach {
|
||||
let listener = $0 as! JMCallKitListener
|
||||
listener.performSetMutedCall?(UUID: action.callUUID, isMuted: action.isMuted)
|
||||
}
|
||||
}
|
||||
|
||||
action.fulfill()
|
||||
|
||||
@@ -160,6 +160,14 @@ import Foundation
|
||||
completion: @escaping (Error?) -> Swift.Void) {
|
||||
guard enabled else { return }
|
||||
|
||||
// XXX keep track of muted actions to avoid "ping-pong"ing. See
|
||||
// JMCallKitEmitter for details on the CXSetMutedCallAction handling.
|
||||
for action in transaction.actions {
|
||||
if (action as? CXSetMutedCallAction) != nil {
|
||||
emitter.addMuteAction(action.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
callController.request(transaction, completion: completion)
|
||||
}
|
||||
|
||||
@@ -187,3 +195,4 @@ import Foundation
|
||||
return update
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"en": "Английски",
|
||||
"af": "",
|
||||
"az": "",
|
||||
"bg": "Български",
|
||||
"cs": "",
|
||||
"de": "Немски",
|
||||
"el": "",
|
||||
"eo": "Есперанто",
|
||||
"es": "Испански",
|
||||
"fr": "Френски",
|
||||
"hy": "Арменски",
|
||||
"it": "Италиански",
|
||||
"ja": "",
|
||||
"ko": "",
|
||||
"nb": "Норвежки букмол",
|
||||
"oc": "Окситански",
|
||||
"pl": "Полски",
|
||||
"ptBR": "Португалски (Бразилия)",
|
||||
@@ -14,7 +22,6 @@
|
||||
"sl": "Словенски",
|
||||
"sv": "Шведски",
|
||||
"tr": "Турски",
|
||||
"zhCN": "Китайски (Китай)",
|
||||
"nb": "Норвежки букмол",
|
||||
"eo": "Есперанто"
|
||||
"vi": "",
|
||||
"zhCN": "Китайски (Китай)"
|
||||
}
|
||||
@@ -1,11 +1,18 @@
|
||||
{
|
||||
"en": "Englisch",
|
||||
"az": "",
|
||||
"bg": "Bulgarisch",
|
||||
"cs": "",
|
||||
"de": "Deutsch",
|
||||
"el": "",
|
||||
"eo": "Esperanto",
|
||||
"es": "Spanisch",
|
||||
"fr": "Französisch",
|
||||
"hy": "Armenisch",
|
||||
"it": "Italienisch",
|
||||
"ja": "",
|
||||
"ko": "",
|
||||
"nb": "Norwegisch (Bokmal)",
|
||||
"oc": "Okzitanisch",
|
||||
"pl": "Polnisch",
|
||||
"ptBR": "Portugiesisch (Brasilien)",
|
||||
@@ -14,7 +21,6 @@
|
||||
"sl": "Slowenisch",
|
||||
"sv": "Schwedisch",
|
||||
"tr": "Türkisch",
|
||||
"zhCN": "Chinesisch (China)",
|
||||
"nb": "Norwegisch (Bokmal)",
|
||||
"eo": "Esperanto"
|
||||
"vi": "",
|
||||
"zhCN": "Chinesisch (China)"
|
||||
}
|
||||
27
lang/languages-hr.json
Normal file
27
lang/languages-hr.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"en": "",
|
||||
"af": "",
|
||||
"az": "",
|
||||
"bg": "",
|
||||
"cs": "",
|
||||
"de": "",
|
||||
"el": "",
|
||||
"eo": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"ja": "",
|
||||
"ko": "",
|
||||
"nb": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"vi": "",
|
||||
"zhCN": ""
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"en": "Anglés",
|
||||
"af": "Afrikaans",
|
||||
"az": "Azèri",
|
||||
"bg": "Bulgar",
|
||||
"cs": "Chèc",
|
||||
"de": "Aleman",
|
||||
"el": "Grèc",
|
||||
"eo": "Esperanto",
|
||||
"es": "Castelhan",
|
||||
"fr": "Francés",
|
||||
"hy": "Armenian",
|
||||
"it": "Italian",
|
||||
"ja": "Japonés",
|
||||
"ko": "Corean",
|
||||
"nb": "Norvegian Bokmål",
|
||||
"oc": "Occitan",
|
||||
"pl": "Polonés",
|
||||
"ptBR": "Portugués (Brasil)",
|
||||
@@ -14,7 +22,6 @@
|
||||
"sl": "Eslovèn",
|
||||
"sv": "Suedés",
|
||||
"tr": "Turc",
|
||||
"zhCN": "Chinés (China)",
|
||||
"nb": "Norvegian Bokmål",
|
||||
"eo": "Esperanto"
|
||||
"vi": "Vietnamian",
|
||||
"zhCN": "Chinés (China)"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"en": "Inglês",
|
||||
"af": "Africâner",
|
||||
"az": "Azerbaijanês",
|
||||
"bg": "Búlgaro",
|
||||
"cs": "Checo",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
{
|
||||
"en": "Tiếng Anh",
|
||||
"af": "Tiếng Afrika",
|
||||
"az": "Tiếng Azecbaizan",
|
||||
"bg": "Tiếng Bulgaria",
|
||||
"cs": "Tiếng Séc",
|
||||
"de": "Tiếng Đức",
|
||||
"el": "Tiếng Nhật",
|
||||
"eo": "Tiếng Esperanto",
|
||||
"es": "Tiếng Tây Ban Nha",
|
||||
"fr": "Tiếng Pháp",
|
||||
"hy": "Tiếng Acmenia",
|
||||
"it": "Tiếng Ý",
|
||||
"ja": "Tiếng Nhật",
|
||||
"ko": "Tiếng Hàn",
|
||||
"nb": "Tiếng Na Uy",
|
||||
"oc": "Tiếng Occitan",
|
||||
"pl": "Tiếng Ba Lan",
|
||||
"ptBR": "Tiếng Bồ Đào Nha (Brazil)",
|
||||
@@ -14,7 +22,6 @@
|
||||
"sl": "Tiếng Slovenia",
|
||||
"sv": "Tiếng Thụy Điển",
|
||||
"tr": "Tiếng Thổ Nhĩ Kỳ",
|
||||
"zhCN": "Tiếng Hoa (Trung Quốc)",
|
||||
"nb": "Tiếng Na Uy",
|
||||
"eo": "Tiếng Esperanto"
|
||||
"vi": "Tiếng Việt",
|
||||
"zhCN": "Tiếng Hoa (Trung Quốc)"
|
||||
}
|
||||
1096
lang/main-bg.json
1096
lang/main-bg.json
File diff suppressed because it is too large
Load Diff
@@ -35,31 +35,43 @@
|
||||
"raiseHand": "Hand erheben",
|
||||
"pushToTalk": "Drücken um zu sprechen",
|
||||
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln",
|
||||
"toggleFilmstrip": "Videos anzeigen oder verbergen",
|
||||
"toggleShortcuts": "Hilfe-Menu anzeigen oder verdecken",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "Lokales Video fokussieren",
|
||||
"focusRemote": "Auf das Video eines anderen Teilnehmers fokussieren",
|
||||
"toggleChat": "Chat öffnen oder schliessen",
|
||||
"mute": "Stummschaltung aktivieren oder deaktivieren",
|
||||
"fullScreen": "Vollbildmodus aktivieren / deaktivieren",
|
||||
"videoMute": "Kamera starten oder stoppen",
|
||||
"showSpeakerStats": "Statistiken für Sprecher anzeigen"
|
||||
"showSpeakerStats": "Statistiken für Sprecher anzeigen",
|
||||
"localRecording": ""
|
||||
},
|
||||
"\u0005keyboardShortcuts": {},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
"join": "",
|
||||
"roomname": "Konferenzname eingeben"
|
||||
},
|
||||
"appDescription": "Auf geht's! Beginne eine Videokonferenz mit dem ganzen Team. Oder eigentlich, lade alle ein die du kennst. __app__ ist eine vollständig verschlüsselte, aus 100% Open-Source-Software bestehende Videokonferenzlösung die du den ganzen Tag kostenlos verwenden kannst — ohne Registrierung.",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "Sprache",
|
||||
"video": "Video"
|
||||
},
|
||||
"calendar": "Kalender",
|
||||
"connectCalendarText": "",
|
||||
"connectCalendarButton": "",
|
||||
"enterRoomTitle": "",
|
||||
"go": "Los",
|
||||
"join": "Beitreten",
|
||||
"privacy": "Privatsphäre",
|
||||
"recentList": "",
|
||||
"recentListDelete": "",
|
||||
"recentListEmpty": "",
|
||||
"roomname": "Konferenzname eingeben",
|
||||
"roomnameHint": "Name oder URL der Konferenz der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden damit sie der gleichen Konferenz beitreten.",
|
||||
"sendFeedback": "Senden Sie uns Ihr Feedback",
|
||||
"terms": "Bedingungen",
|
||||
"title": "Sichere, flexible und vollständig freie Videokonferenzen"
|
||||
"title": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
@@ -71,8 +83,41 @@
|
||||
"rejoinKeyTitle": "Erneut teilnehmen"
|
||||
},
|
||||
"toolbar": {
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "",
|
||||
"audioRoute": "",
|
||||
"callQuality": "Qualitätseinstellungen",
|
||||
"chat": "",
|
||||
"cc": "",
|
||||
"document": "Geteiltes Dokument schliessen",
|
||||
"feedback": "Feedback hinterlasen",
|
||||
"fullScreen": "",
|
||||
"hangup": "",
|
||||
"invite": "Teilnehmer einladen",
|
||||
"localRecording": "",
|
||||
"lockRoom": "",
|
||||
"moreActions": "",
|
||||
"moreActionsMenu": "",
|
||||
"mute": "",
|
||||
"pip": "",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "",
|
||||
"recording": "",
|
||||
"Settings": "",
|
||||
"sharedvideo": "",
|
||||
"shareRoom": "",
|
||||
"shareYourScreen": "",
|
||||
"shortcuts": "",
|
||||
"speakerStats": "",
|
||||
"toggleCamera": "",
|
||||
"tileView": "",
|
||||
"videomute": ""
|
||||
},
|
||||
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
|
||||
"audioonly": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
|
||||
"audioOnlyOn": "Nur-Audio-Modus aktivieren/deaktivieren (spart Bandbreite)",
|
||||
"audioOnlyOff": "",
|
||||
"audioRoute": "",
|
||||
"callQuality": "Qualitätseinstellungen",
|
||||
"enterFullScreen": "Vollbildmodus",
|
||||
"exitFullScreen": "Vollbildmodus verlassen",
|
||||
@@ -86,8 +131,8 @@
|
||||
"etherpad": "Geteiltes Dokument öffnen / schliessen",
|
||||
"documentOpen": "Geteiltes Dokument öffnen",
|
||||
"documentClose": "Geteiltes Dokument schliessen",
|
||||
"shareRoom": "",
|
||||
"sharedvideo": "YouTube-Video teilen",
|
||||
"sharescreen": "Bildschirmfreigabe",
|
||||
"stopSharedVideo": "YouTube Video stoppen",
|
||||
"fullscreen": "Vollbildmodus aktivieren / deaktivieren",
|
||||
"sip": "SIP Nummer anrufen",
|
||||
@@ -96,25 +141,40 @@
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden",
|
||||
"sharedVideoMutedPopup": "Das freigegebene Video wurde stumm geschaltet um mit den anderen Teilnehmern zu sprechen.",
|
||||
"toggleCamera": "",
|
||||
"micMutedPopup": "Das Mikrofon wurde stumm geschaltet um das freigegebene Video geniessen zu können.",
|
||||
"talkWhileMutedPopup": "Versuchen sie zu sprechen? Ihr Mikrofon ist stummgeschaltet.",
|
||||
"unableToUnmutePopup": "Die Stummschaltung kann nicht aufgehoben werden während das geteilte Video abgespielt wird.",
|
||||
"cameraDisabled": "Keine Kamera verfügbar",
|
||||
"micDisabled": "Kein Mikrofon verfügbar",
|
||||
"filmstrip": "Videos anzeigen / verbergen",
|
||||
"pip": "",
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "Hand erheben",
|
||||
"shortcuts": "Tastenkürzel anzeigen",
|
||||
"speakerStats": "Sprecher-Statistiken"
|
||||
"speakerStats": "Sprecher-Statistiken",
|
||||
"tileViewToggle": "",
|
||||
"invite": "Teilnehmer einladen"
|
||||
},
|
||||
"\u0005toolbar": {
|
||||
"accessibilityLabel": {}
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "Name eingeben",
|
||||
"popover": "Name"
|
||||
},
|
||||
"error": "",
|
||||
"messagebox": "Text eingeben..."
|
||||
},
|
||||
"settings": {
|
||||
"calendar": {
|
||||
"about": "",
|
||||
"disconnect": "Getrennt",
|
||||
"microsoftSignIn": "",
|
||||
"signedIn": "",
|
||||
"title": "Kalender"
|
||||
},
|
||||
"title": "Einstellungen",
|
||||
"update": "Aktualisieren",
|
||||
"name": "Name",
|
||||
@@ -124,11 +184,18 @@
|
||||
"selectMic": "Mikrofon",
|
||||
"selectAudioOutput": "Audioausgabe",
|
||||
"followMe": "Follow-me für alle Teilnehmer",
|
||||
"language": "",
|
||||
"loggedIn": "",
|
||||
"noDevice": "Kein",
|
||||
"cameraAndMic": "Kamera und Mikrofon",
|
||||
"moderator": "MODERATOR",
|
||||
"moderator": "Moderator",
|
||||
"more": "",
|
||||
"password": "PASSWORT SETZEN",
|
||||
"audioVideo": "AUDIO UND VIDEO"
|
||||
"audioVideo": "AUDIO UND VIDEO",
|
||||
"devices": ""
|
||||
},
|
||||
"\u0005settings": {
|
||||
"calendar": {}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profil",
|
||||
@@ -148,7 +215,9 @@
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "Verbindungsdaten",
|
||||
"connectedTo": "",
|
||||
"bitrate": "Bitrate:",
|
||||
"bridgeCount": "",
|
||||
"packetloss": "Paketverlust:",
|
||||
"resolution": "Auflösung:",
|
||||
"framerate": "Bildwiederholrate:",
|
||||
@@ -187,7 +256,6 @@
|
||||
"focus": "Konferenz-Organisator",
|
||||
"focusFail": "__component__ ist im Moment nicht verfügbar - wiederholen in __ms__ Sekunden",
|
||||
"grantedTo": "Moderatorenrechte an __to__ vergeben.",
|
||||
"grantedToUnknown": "Moderatorenrechte an $t(notify.somebody) vergeben.",
|
||||
"muted": "Der Konferenz wurde stumm beigetreten.",
|
||||
"mutedTitle": "Stummschaltung aktiv!",
|
||||
"raisedHand": "Möchte sprechen.",
|
||||
@@ -195,8 +263,13 @@
|
||||
"suboptimalExperienceDescription": "Tut uns leid, aber die Konferenz wird mit __appName__ kein grossartiges Erlebnis. Wir versuchen immer die Situation zu verbessern, bis dahin empfehlen wir aber die Verwendung einer der <a href=\"static/recommendedBrowsers.html\" target=\"_blank\">vollständig unterstützen Browser</a>."
|
||||
},
|
||||
"dialog": {
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": "Livestream:"
|
||||
},
|
||||
"allow": "Erlauben",
|
||||
"confirm": "",
|
||||
"kickMessage": "Oh! Sie wurden aus der Konferenz ausgeschlossen.",
|
||||
"kickTitle": "",
|
||||
"popupErrorTitle": "Popup blockiert",
|
||||
"popupError": "Ihr Browser blockiert Popups von dieser Website. Bitte aktivieren Sie Popups in den Sicherheitseinstellungen des Browsers und versuchen Sie es erneut.",
|
||||
"passwordErrorTitle": "Passwort-Fehler",
|
||||
@@ -219,6 +292,7 @@
|
||||
"rejoinNow": "Jetzt erneut beitreten",
|
||||
"maxUsersLimitReachedTitle": "Maximale Anzahl Teilnehmer ist erreicht",
|
||||
"maxUsersLimitReached": "Das Limit der maximalen Anzahl Teilnehmer wurde erreicht. Die Konferenz ist voll. Bitte benachrichtigen Sie den Konferenzorganisator oder versuchen Sie es später noch einmal.",
|
||||
"lockRoom": "",
|
||||
"lockTitle": "Sperren fehlgeschlagen",
|
||||
"lockMessage": "Die Konferenz konnte nicht gesperrt werden.",
|
||||
"warning": "Warnung",
|
||||
@@ -266,6 +340,7 @@
|
||||
"reservationError": "Fehler im Reservationssystem",
|
||||
"reservationErrorMsg": "Fehler, Nummer: __code__, Nachricht: __msg__",
|
||||
"password": "Passwort eingeben",
|
||||
"unlockRoom": "",
|
||||
"userPassword": "Benutzerpasswort",
|
||||
"token": "Token",
|
||||
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
|
||||
@@ -278,7 +353,7 @@
|
||||
"sorryFeedback": "Tut uns leid. Möchten Sie uns mehr mitteilen?",
|
||||
"liveStreaming": "Live-Streaming",
|
||||
"streamKey": "Name/Schlüssel für den Stream",
|
||||
"startLiveStreaming": "Live-Streaming starten",
|
||||
"startLiveStreaming": "Einen Livestream starten",
|
||||
"startRecording": "Aufnahme starten",
|
||||
"stopStreamingWarning": "Sind Sie sicher dass Sie das Live-Streaming stoppen möchten?",
|
||||
"stopRecordingWarning": "Sind Sie sicher dass Sie die Aufnahme stoppen möchten?",
|
||||
@@ -312,6 +387,10 @@
|
||||
"muteParticipantTitle": "Diesen Teilnehmer stummschalten?",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Teilnehmer nicht aufheben, aber ein Teilnehmer kann seine eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"liveStreamingDisabledTooltip": "",
|
||||
"liveStreamingDisabledForGuestTooltip": "",
|
||||
"recordingDisabledTooltip": "",
|
||||
"recordingDisabledForGuestTooltip": "",
|
||||
"remoteControlTitle": "Fernsteuerung",
|
||||
"remoteControlRequestMessage": "Möchten Sie __user__ erlauben den Computer fernzusteuern?",
|
||||
"remoteControlShareScreenWarning": "Achtung, wenn Sie die Anfrage genehmigen starten Sie die Bildschirmfreigabe!",
|
||||
@@ -322,10 +401,15 @@
|
||||
"remoteControlStopMessage": "Die Fernsteuerung wurde beendet.",
|
||||
"close": "Schliessen",
|
||||
"shareYourScreen": "Bildschirm freigeben",
|
||||
"shareYourScreenDisabled": "",
|
||||
"shareYourScreenDisabledForGuest": "",
|
||||
"yourEntireScreen": "Ganzer Bildschirm",
|
||||
"applicationWindow": "Anwendungsfenster"
|
||||
"applicationWindow": "Anwendungsfenster",
|
||||
"transcribing": ""
|
||||
},
|
||||
"\u0005dialog": {
|
||||
"accessibilityLabel": {}
|
||||
},
|
||||
"\u0005dialog": {},
|
||||
"email": {
|
||||
"sharedKey": [
|
||||
"Diese Konferenz ist passwortgeschützt. Bitte verwenden Sie diese PIN zum Beitreten:",
|
||||
@@ -357,6 +441,10 @@
|
||||
],
|
||||
"and": "und"
|
||||
},
|
||||
"share": {
|
||||
"mainText": "",
|
||||
"dialInfoText": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "Fehler",
|
||||
"CONNECTING": "Verbindung wird hergestellt",
|
||||
@@ -370,18 +458,44 @@
|
||||
"ATTACHED": "Angehängt"
|
||||
},
|
||||
"recording": {
|
||||
"beta": "",
|
||||
"busy": "Es werden Resourcen für eine Aufnahme bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
|
||||
"busyTitle": "Alle Aufnahme-Instanzen sind in Gebrauch",
|
||||
"buttonTooltip": "Aufnahme starten / stoppen",
|
||||
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"expandedOff": "",
|
||||
"expandedOn": "",
|
||||
"expandedPending": "",
|
||||
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
|
||||
"live": "",
|
||||
"off": "Aufnahme gestoppt",
|
||||
"on": "Aufnahme",
|
||||
"pending": "Die Aufnahme wartet auf den Beitritt eines Teilnehmers...",
|
||||
"pending": "",
|
||||
"rec": "",
|
||||
"authDropboxText": "",
|
||||
"serviceName": "Aufnahmedienst",
|
||||
"signOut": "",
|
||||
"signIn": "",
|
||||
"loggedIn": "",
|
||||
"availableSpace": "",
|
||||
"startRecordingBody": "Sind Sie sicher dass Sie die Aufnahme stoppen möchten?",
|
||||
"unavailable": "Oh! Der __serviceName__ ist aktuell nicht verfügbar. Wir arbeiten an der Behebung des Problems. Bitte versuchen Sie es später noch einmal.",
|
||||
"unavailableTitle": "Aufnahme nicht verfügbar"
|
||||
},
|
||||
"\u0005recording": {},
|
||||
"transcribing": {
|
||||
"pending": "",
|
||||
"off": "",
|
||||
"error": "Die Aufzeichnung ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"expandedLabel": "",
|
||||
"failedToStart": "",
|
||||
"tr": "",
|
||||
"labelToolTip": "",
|
||||
"ccButtonTooltip": "",
|
||||
"start": "",
|
||||
"stop": ""
|
||||
},
|
||||
"\u0005transcribing": {},
|
||||
"liveStreaming": {
|
||||
"busy": "Es werden Resourcen zum Streamen bereitgestellt. Bitte in ein paar Minuten erneut versuchen.",
|
||||
"busyTitle": "Alle Streaming-Instanzen sind in Gebrauch",
|
||||
@@ -392,17 +506,24 @@
|
||||
"enterStreamKey": "Name/Schlüssel für den YouTube Livestream hier eingeben.",
|
||||
"error": "Das Live-Streaming ist fehlgeschlagen. Bitte versuchen Sie es erneut.",
|
||||
"errorAPI": "Beim abrufen der YouTube Livestreams ist ein Fehler aufgetreten. Bitte versuchen Sie sich erneut anzumelden.",
|
||||
"errorLiveStreamNotEnabled": "",
|
||||
"expandedOff": "",
|
||||
"expandedOn": "",
|
||||
"expandedPending": "",
|
||||
"failedToStart": "Live-Streaming konnte nicht gestartet werden",
|
||||
"off": "Live-Streaming gestoppt",
|
||||
"on": "Live-Streaming",
|
||||
"pending": "Live-Stream wird gestartet...",
|
||||
"serviceName": "Live Streaming-Dienst",
|
||||
"signedInAs": "",
|
||||
"signIn": "Mit Google anmelden",
|
||||
"signOut": "",
|
||||
"signInCTA": "Anmelden oder den Name/Schlüssel des YouTube Livestreams eingeben.",
|
||||
"start": "Einen Livestream starten",
|
||||
"streamIdHelp": "Was ist das?",
|
||||
"unavailableTitle": "Live-Streaming nicht verfügbar"
|
||||
},
|
||||
"\u0005liveStreaming": {},
|
||||
"videoSIPGW": {
|
||||
"busy": "Es stehen keine freien Ressourcen zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"busyTitle": "Keine freien Ressourcen",
|
||||
@@ -428,9 +549,11 @@
|
||||
"noPermission": "Berechtigungen nicht erteilt",
|
||||
"previewUnavailable": "Keine Vorschau verfügbar",
|
||||
"selectADevice": "Ein Gerät wählen",
|
||||
"testAudio": "Audio testen"
|
||||
"testAudio": ""
|
||||
},
|
||||
"videoStatus": {
|
||||
"audioOnly": "",
|
||||
"audioOnlyExpanded": "",
|
||||
"callQuality": "Konferenzqualität",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Video wird in HD angezeigt",
|
||||
@@ -458,7 +581,8 @@
|
||||
"add": "Einladen",
|
||||
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
|
||||
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
|
||||
"disabled": "",
|
||||
"disabled": "Sie können keine Teilnehmer einladen.",
|
||||
"footerText": "",
|
||||
"invite": "Einladen",
|
||||
"loading": "Suche nach Teilnehmern und Telefonnummern",
|
||||
"loadingNumber": "Telefonnummer wird überprüft",
|
||||
@@ -495,6 +619,7 @@
|
||||
"veryGood": "Sehr gut"
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "",
|
||||
"addPassword": "Passwort hinzufügen",
|
||||
"cancelPassword": "Password abbrechen",
|
||||
"conferenceURL": "Link:",
|
||||
@@ -516,7 +641,7 @@
|
||||
"numbers": "Einwählnummern",
|
||||
"password": "Passwort:",
|
||||
"title": "Teilen",
|
||||
"tooltip": "Zugriffsinformationen über die Konferenz abrufen"
|
||||
"tooltip": ""
|
||||
},
|
||||
"settingsView": {
|
||||
"alertOk": "OK",
|
||||
@@ -532,17 +657,22 @@
|
||||
"startWithVideoMuted": "Ohne Video beitreten"
|
||||
},
|
||||
"calendarSync": {
|
||||
"later": "Später",
|
||||
"next": "Folgend",
|
||||
"addMeetingURL": "",
|
||||
"confirmAddLink": "",
|
||||
"confirmAddLinkTitle": "Kalender",
|
||||
"join": "",
|
||||
"joinTooltip": "",
|
||||
"nextMeeting": "Nächste Konferenz",
|
||||
"now": "Jetzt",
|
||||
"noEvents": "",
|
||||
"ongoingMeeting": "",
|
||||
"permissionButton": "Einstellungen öffnen",
|
||||
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen."
|
||||
"permissionMessage": "Die App benötigt Zugriff auf den Kalender um die Termine und Konferenzen anzuzeigen.",
|
||||
"refresh": "",
|
||||
"today": "Heute"
|
||||
},
|
||||
"\u0005calendarSync": {},
|
||||
"recentList": {
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"earlier": "Früher"
|
||||
"joinPastMeeting": ""
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Ziehen um zu aktualisieren"
|
||||
@@ -555,5 +685,62 @@
|
||||
"appNotInstalled": "Sie benötigen die __app__ App um der Konferenz auf dem Smartphone beizutreten.",
|
||||
"downloadApp": "App herunterladen",
|
||||
"openApp": "In der App fortfahren"
|
||||
}
|
||||
},
|
||||
"presenceStatus": {
|
||||
"invited": "Einladen",
|
||||
"ringing": "",
|
||||
"calling": "",
|
||||
"initializingCall": "",
|
||||
"connected": "Verbunden",
|
||||
"connecting": "Verbindung wird hergestellt",
|
||||
"connecting2": "",
|
||||
"disconnected": "Getrennt",
|
||||
"busy": "",
|
||||
"rejected": "",
|
||||
"ignored": "",
|
||||
"expired": ""
|
||||
},
|
||||
"\u0005presenceStatus": {},
|
||||
"dateUtils": {
|
||||
"today": "Heute",
|
||||
"yesterday": "Gestern",
|
||||
"earlier": "Früher"
|
||||
},
|
||||
"incomingCall": {
|
||||
"answer": "",
|
||||
"audioCallTitle": "",
|
||||
"decline": "OK",
|
||||
"productLabel": "",
|
||||
"videoCallTitle": ""
|
||||
},
|
||||
"localRecording": {
|
||||
"localRecording": "",
|
||||
"dialogTitle": "",
|
||||
"start": "Aufnahme starten",
|
||||
"stop": "Aufnahme stoppen",
|
||||
"moderator": "Moderator",
|
||||
"me": "Ich",
|
||||
"duration": "",
|
||||
"durationNA": "",
|
||||
"encoding": "",
|
||||
"participantStats": "",
|
||||
"participant": "Teilnehmer",
|
||||
"sessionToken": "",
|
||||
"clientState": {
|
||||
"on": "",
|
||||
"off": "",
|
||||
"unknown": ""
|
||||
},
|
||||
"messages": {
|
||||
"engaged": "",
|
||||
"finished": "",
|
||||
"finishedModerator": "",
|
||||
"notModerator": ""
|
||||
},
|
||||
"yes": "Ja",
|
||||
"no": "",
|
||||
"label": "",
|
||||
"labelToolTip": ""
|
||||
},
|
||||
"\u0005localRecording": {}
|
||||
}
|
||||
746
lang/main-hr.json
Normal file
746
lang/main-hr.json
Normal file
@@ -0,0 +1,746 @@
|
||||
{
|
||||
"addPeople": {
|
||||
"add": "",
|
||||
"countryNotSupported": "",
|
||||
"countryReminder": "",
|
||||
"disabled": "",
|
||||
"failedToAdd": "",
|
||||
"footerText": "",
|
||||
"invite": "",
|
||||
"loading": "",
|
||||
"loadingNumber": "",
|
||||
"loadingPeople": "",
|
||||
"noResults": "",
|
||||
"notAvailable": "",
|
||||
"noValidNumbers": "",
|
||||
"searchNumbers": "",
|
||||
"searchPeople": "",
|
||||
"searchPeopleAndNumbers": "",
|
||||
"telephone": "",
|
||||
"title": ""
|
||||
},
|
||||
"audioDevices": {
|
||||
"bluetooth": "",
|
||||
"headphones": "",
|
||||
"phone": "",
|
||||
"speaker": ""
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "",
|
||||
"featureToggleDisabled": ""
|
||||
},
|
||||
"calendarSync": {
|
||||
"addMeetingURL": "",
|
||||
"confirmAddLink": "",
|
||||
"confirmAddLinkTitle": "",
|
||||
"error": {
|
||||
"appConfiguration": "",
|
||||
"generic": "",
|
||||
"notSignedIn": ""
|
||||
},
|
||||
"join": "",
|
||||
"joinTooltip": "",
|
||||
"nextMeeting": "",
|
||||
"noEvents": "",
|
||||
"ongoingMeeting": "",
|
||||
"permissionButton": "",
|
||||
"permissionMessage": "",
|
||||
"refresh": "",
|
||||
"today": ""
|
||||
},
|
||||
"chat": {
|
||||
"error": "",
|
||||
"messagebox": "",
|
||||
"nickname": {
|
||||
"popover": "",
|
||||
"title": ""
|
||||
},
|
||||
"title": ""
|
||||
},
|
||||
"connection": {
|
||||
"ATTACHED": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"CONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ERROR": "",
|
||||
"RECONNECTING": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"address": "",
|
||||
"bandwidth": "",
|
||||
"bitrate": "",
|
||||
"bridgeCount": "",
|
||||
"connectedTo": "",
|
||||
"framerate": "",
|
||||
"header": "",
|
||||
"less": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural_2": "",
|
||||
"localport": "",
|
||||
"localport_plural_2": "",
|
||||
"more": "",
|
||||
"na": "",
|
||||
"packetloss": "",
|
||||
"quality": {
|
||||
"good": "",
|
||||
"inactive": "",
|
||||
"lost": "",
|
||||
"nonoptimal": "",
|
||||
"poor": ""
|
||||
},
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural_2": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural_2": "",
|
||||
"resolution": "",
|
||||
"status": "",
|
||||
"transport": "",
|
||||
"transport_plural_2": "",
|
||||
"turn": ""
|
||||
},
|
||||
"contactlist_plural": "",
|
||||
"dateUtils": {
|
||||
"earlier": "",
|
||||
"today": "",
|
||||
"yesterday": ""
|
||||
},
|
||||
"deepLinking": {
|
||||
"appNotInstalled": "",
|
||||
"description": "",
|
||||
"downloadApp": "",
|
||||
"launchWebButton": "",
|
||||
"openApp": "",
|
||||
"title": "",
|
||||
"tryAgainButton": ""
|
||||
},
|
||||
"defaultLink": "",
|
||||
"defaultNickname": "",
|
||||
"deviceError": {
|
||||
"cameraError": "",
|
||||
"cameraPermission": "",
|
||||
"microphoneError": "",
|
||||
"microphonePermission": ""
|
||||
},
|
||||
"deviceSelection": {
|
||||
"deviceSettings": "",
|
||||
"noPermission": "",
|
||||
"previewUnavailable": "",
|
||||
"selectADevice": "",
|
||||
"testAudio": ""
|
||||
},
|
||||
"dialog": {
|
||||
"accessibilityLabel": {
|
||||
"liveStreaming": ""
|
||||
},
|
||||
"allow": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"alreadySharedVideoTitle": "",
|
||||
"applicationWindow": "",
|
||||
"Back": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraNotSendingData": "",
|
||||
"cameraNotSendingDataTitle": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"Cancel": "",
|
||||
"close": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"confirm": "",
|
||||
"confirmNo": "",
|
||||
"confirmYes": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"connecting": "",
|
||||
"contactSupport": "",
|
||||
"copy": "",
|
||||
"currentPassword": "",
|
||||
"defaultError": "",
|
||||
"detectext": "",
|
||||
"dismiss": "",
|
||||
"displayNameRequired": "",
|
||||
"done": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"enterDisplayName": "",
|
||||
"error": "",
|
||||
"externalInstallationMsg": "",
|
||||
"externalInstallationTitle": "",
|
||||
"failedpermissions": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"goToStore": "",
|
||||
"gracefulShutdown": "",
|
||||
"hungUp": "",
|
||||
"IamHost": "",
|
||||
"incorrectPassword": "",
|
||||
"inlineInstallationMsg": "",
|
||||
"inlineInstallExtension": "",
|
||||
"internalError": "",
|
||||
"internalErrorTitle": "",
|
||||
"joinAgain": "",
|
||||
"kickMessage": "",
|
||||
"kickParticipantButton": "",
|
||||
"kickParticipantDialog": "",
|
||||
"kickParticipantTitle": "",
|
||||
"kickTitle": "",
|
||||
"liveStreaming": "",
|
||||
"liveStreamingDisabledForGuestTooltip": "",
|
||||
"liveStreamingDisabledTooltip": "",
|
||||
"lockMessage": "",
|
||||
"lockRoom": "",
|
||||
"lockTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"logoutTitle": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"maxUsersLimitReachedTitle": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micNotSendingData": "",
|
||||
"micNotSendingDataTitle": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micUnknownError": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "",
|
||||
"muteParticipantDialog": "",
|
||||
"muteParticipantTitle": "",
|
||||
"Ok": "",
|
||||
"oops": "",
|
||||
"password": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordLabel": "",
|
||||
"passwordNotSupported": "",
|
||||
"passwordNotSupportedTitle": "",
|
||||
"passwordRequired": "",
|
||||
"permissionDenied": "",
|
||||
"popupError": "",
|
||||
"popupErrorTitle": "",
|
||||
"recording": "",
|
||||
"recordingDisabledForGuestTooltip": "",
|
||||
"recordingDisabledTooltip": "",
|
||||
"recordingToken": "",
|
||||
"rejoinNow": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"remoteControlRequestMessage": "",
|
||||
"remoteControlShareScreenWarning": "",
|
||||
"remoteControlStopMessage": "",
|
||||
"remoteControlTitle": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"retry": "",
|
||||
"Save": "",
|
||||
"screenSharingFailedToInstall": "",
|
||||
"screenSharingFailedToInstallTitle": "",
|
||||
"screenSharingFirefoxPermissionDeniedError": "",
|
||||
"screenSharingFirefoxPermissionDeniedTitle": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"serviceUnavailable": "",
|
||||
"sessTerminated": "",
|
||||
"Share": "",
|
||||
"shareVideoLinkError": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareYourScreen": "",
|
||||
"shareYourScreenDisabled": "",
|
||||
"shareYourScreenDisabledForGuest": "",
|
||||
"SLDFailure": "",
|
||||
"sorryFeedback": "",
|
||||
"SRDFailure": "",
|
||||
"startLiveStreaming": "",
|
||||
"startRecording": "",
|
||||
"startRemoteControlErrorMessage": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopStreamingWarning": "",
|
||||
"streamKey": "",
|
||||
"Submit": "",
|
||||
"thankYou": "",
|
||||
"token": "",
|
||||
"tokenAuthFailed": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"transcribing": "",
|
||||
"unableToSwitch": "",
|
||||
"unlockRoom": "",
|
||||
"userPassword": "",
|
||||
"WaitForHostMsg": "",
|
||||
"WaitForHostMsgWOk": "",
|
||||
"WaitingForHost": "",
|
||||
"warning": "",
|
||||
"Yes": "",
|
||||
"yourEntireScreen": ""
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": ""
|
||||
},
|
||||
"email": {
|
||||
"and": "",
|
||||
"body": "",
|
||||
"sharedKey": "",
|
||||
"subject": ""
|
||||
},
|
||||
"feedback": {
|
||||
"average": "",
|
||||
"bad": "",
|
||||
"detailsLabel": "",
|
||||
"good": "",
|
||||
"rateExperience": "",
|
||||
"veryBad": "",
|
||||
"veryGood": ""
|
||||
},
|
||||
"incomingCall": {
|
||||
"answer": "",
|
||||
"audioCallTitle": "",
|
||||
"decline": "",
|
||||
"productLabel": "",
|
||||
"videoCallTitle": ""
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "",
|
||||
"addPassword": "",
|
||||
"cancelPassword": "",
|
||||
"conferenceURL": "",
|
||||
"country": "",
|
||||
"dialANumber": "",
|
||||
"dialInConferenceID": "",
|
||||
"dialInNotSupported": "",
|
||||
"dialInNumber": "",
|
||||
"dialInTollFree": "",
|
||||
"genericError": "",
|
||||
"inviteLiveStream": "",
|
||||
"invitePhone": "",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURL": "",
|
||||
"liveStreamURL": "",
|
||||
"moreNumbers": "",
|
||||
"noNumbers": "",
|
||||
"noPassword": "",
|
||||
"noRoom": "",
|
||||
"numbers": "",
|
||||
"password": "",
|
||||
"title": "",
|
||||
"tooltip": "",
|
||||
"label": ""
|
||||
},
|
||||
"inviteDialog": {
|
||||
"alertOk": "",
|
||||
"alertText": "",
|
||||
"alertTitle": "",
|
||||
"header": "",
|
||||
"searchCallOnlyPlaceholder": "",
|
||||
"searchPeopleOnlyPlaceholder": "",
|
||||
"searchPlaceholder": "",
|
||||
"send": ""
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "",
|
||||
"retry": "",
|
||||
"support": "",
|
||||
"supportMsg": ""
|
||||
},
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"keyboardShortcuts": {
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"fullScreen": "",
|
||||
"keyboardShortcuts": "",
|
||||
"localRecording": "",
|
||||
"mute": "",
|
||||
"pushToTalk": "",
|
||||
"raiseHand": "",
|
||||
"showSpeakerStats": "",
|
||||
"toggleChat": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleShortcuts": "",
|
||||
"videoMute": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"changeSignIn": "",
|
||||
"choose": "",
|
||||
"chooseCTA": "",
|
||||
"enterStreamKey": "",
|
||||
"error": "",
|
||||
"errorAPI": "",
|
||||
"errorLiveStreamNotEnabled": "",
|
||||
"expandedOff": "",
|
||||
"expandedOn": "",
|
||||
"expandedPending": "",
|
||||
"failedToStart": "",
|
||||
"getStreamKeyManually": "",
|
||||
"invalidStreamKey": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"signedInAs": "",
|
||||
"signIn": "",
|
||||
"signInCTA": "",
|
||||
"signOut": "",
|
||||
"start": "",
|
||||
"streamIdHelp": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"localRecording": {
|
||||
"clientState": {
|
||||
"off": "",
|
||||
"on": "",
|
||||
"unknown": ""
|
||||
},
|
||||
"dialogTitle": "",
|
||||
"duration": "",
|
||||
"durationNA": "",
|
||||
"encoding": "",
|
||||
"label": "",
|
||||
"labelToolTip": "",
|
||||
"localRecording": "",
|
||||
"me": "",
|
||||
"messages": {
|
||||
"engaged": "",
|
||||
"finished": "",
|
||||
"finishedModerator": "",
|
||||
"notModerator": ""
|
||||
},
|
||||
"moderator": "",
|
||||
"no": "",
|
||||
"participant": "",
|
||||
"participantStats": "",
|
||||
"sessionToken": "",
|
||||
"start": "",
|
||||
"stop": "",
|
||||
"yes": ""
|
||||
},
|
||||
"me": "",
|
||||
"notify": {
|
||||
"connectedOneMember": "",
|
||||
"connectedThreePlusMembers": "",
|
||||
"connectedTwoMembers": "",
|
||||
"disconnected": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"me": "",
|
||||
"moderator": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": "",
|
||||
"somebody": "",
|
||||
"suboptimalExperienceDescription": "",
|
||||
"suboptimalExperienceTitle": ""
|
||||
},
|
||||
"passwordSetRemotely": "",
|
||||
"poweredby": "",
|
||||
"presenceStatus": {
|
||||
"busy": "",
|
||||
"calling": "",
|
||||
"connected": "",
|
||||
"connecting": "",
|
||||
"connecting2": "",
|
||||
"disconnected": "",
|
||||
"expired": "",
|
||||
"ignored": "",
|
||||
"initializingCall": "",
|
||||
"invited": "",
|
||||
"rejected": "",
|
||||
"ringing": ""
|
||||
},
|
||||
"profile": {
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailInput": "",
|
||||
"setEmailLabel": "",
|
||||
"title": ""
|
||||
},
|
||||
"raisedHand": "",
|
||||
"recentList": {
|
||||
"joinPastMeeting": ""
|
||||
},
|
||||
"recording": {
|
||||
"authDropboxText": "",
|
||||
"availableSpace": "",
|
||||
"beta": "",
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"expandedOff": "",
|
||||
"expandedOn": "",
|
||||
"expandedPending": "",
|
||||
"failedToStart": "",
|
||||
"live": "",
|
||||
"loggedIn": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"rec": "",
|
||||
"serviceDescription": "",
|
||||
"serviceName": "",
|
||||
"signIn": "",
|
||||
"signOut": "",
|
||||
"startRecordingBody": "",
|
||||
"unavailable": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": ""
|
||||
},
|
||||
"settings": {
|
||||
"audioVideo": "",
|
||||
"calendar": {
|
||||
"about": "",
|
||||
"disconnect": "",
|
||||
"microsoftSignIn": "",
|
||||
"signedIn": "",
|
||||
"title": ""
|
||||
},
|
||||
"cameraAndMic": "",
|
||||
"devices": "",
|
||||
"followMe": "",
|
||||
"language": "",
|
||||
"loggedIn": "",
|
||||
"moderator": "",
|
||||
"more": "",
|
||||
"name": "",
|
||||
"noDevice": "",
|
||||
"password": "",
|
||||
"selectAudioOutput": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"title": "",
|
||||
"update": ""
|
||||
},
|
||||
"settingsView": {
|
||||
"alertOk": "",
|
||||
"alertTitle": "",
|
||||
"alertURLText": "",
|
||||
"conferenceSection": "",
|
||||
"displayName": "",
|
||||
"email": "",
|
||||
"header": "",
|
||||
"profileSection": "",
|
||||
"serverURL": "",
|
||||
"startWithAudioMuted": "",
|
||||
"startWithVideoMuted": ""
|
||||
},
|
||||
"share": {
|
||||
"dialInfoText": "",
|
||||
"mainText": ""
|
||||
},
|
||||
"speaker": "",
|
||||
"speakerStats": {
|
||||
"hours": "",
|
||||
"minutes": "",
|
||||
"name": "",
|
||||
"seconds": "",
|
||||
"speakerStats": "",
|
||||
"speakerTime": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"rejoinKeyTitle": "",
|
||||
"text": "",
|
||||
"title": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"accessibilityLabel": {
|
||||
"audioOnly": "",
|
||||
"audioRoute": "",
|
||||
"callQuality": "",
|
||||
"cc": "",
|
||||
"chat": "",
|
||||
"document": "",
|
||||
"feedback": "",
|
||||
"fullScreen": "",
|
||||
"hangup": "",
|
||||
"invite": "",
|
||||
"kick": "",
|
||||
"localRecording": "",
|
||||
"lockRoom": "",
|
||||
"moreActions": "",
|
||||
"moreActionsMenu": "",
|
||||
"mute": "",
|
||||
"pip": "",
|
||||
"profile": "",
|
||||
"raiseHand": "",
|
||||
"recording": "",
|
||||
"remoteMute": "",
|
||||
"Settings": "",
|
||||
"sharedvideo": "",
|
||||
"shareRoom": "",
|
||||
"shareYourScreen": "",
|
||||
"shortcuts": "",
|
||||
"speakerStats": "",
|
||||
"tileView": "",
|
||||
"toggleCamera": "",
|
||||
"videomute": ""
|
||||
},
|
||||
"addPeople": "",
|
||||
"audioonly": "",
|
||||
"audioOnlyOff": "",
|
||||
"audioOnlyOn": "",
|
||||
"audioRoute": "",
|
||||
"authenticate": "",
|
||||
"callQuality": "",
|
||||
"cameraDisabled": "",
|
||||
"chat": "",
|
||||
"closeChat": "",
|
||||
"documentClose": "",
|
||||
"documentOpen": "",
|
||||
"enterFullScreen": "",
|
||||
"enterTileView": "",
|
||||
"etherpad": "",
|
||||
"exitFullScreen": "",
|
||||
"exitTileView": "",
|
||||
"feedback": "",
|
||||
"filmstrip": "",
|
||||
"fullscreen": "",
|
||||
"hangup": "",
|
||||
"invite": "",
|
||||
"lock": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"lowerYourHand": "",
|
||||
"micDisabled": "",
|
||||
"micMutedPopup": "",
|
||||
"moreActions": "",
|
||||
"mute": "",
|
||||
"openChat": "",
|
||||
"pip": "",
|
||||
"profile": "",
|
||||
"raiseHand": "",
|
||||
"raiseYourHand": "",
|
||||
"Settings": "",
|
||||
"sharedvideo": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"shareRoom": "",
|
||||
"shortcuts": "",
|
||||
"sip": "",
|
||||
"speakerStats": "",
|
||||
"startScreenSharing": "",
|
||||
"startSubtitles": "",
|
||||
"stopScreenSharing": "",
|
||||
"stopSubtitles": "",
|
||||
"stopSharedVideo": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"tileViewToggle": "",
|
||||
"toggleCamera": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"videomute": ""
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "",
|
||||
"error": "",
|
||||
"expandedLabel": "",
|
||||
"failedToStart": "",
|
||||
"labelToolTip": "",
|
||||
"off": "",
|
||||
"pending": "",
|
||||
"start": "",
|
||||
"stop": "",
|
||||
"tr": ""
|
||||
},
|
||||
"userMedia": {
|
||||
"androidGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"edgeGrantPermissions": "",
|
||||
"electronGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"nwjsGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"react-nativeGrantPermissions": "",
|
||||
"safariGrantPermissions": ""
|
||||
},
|
||||
"videoSIPGW": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"errorAlreadyInvited": "",
|
||||
"errorInvite": "",
|
||||
"errorInviteFailed": "",
|
||||
"errorInviteFailedTitle": "",
|
||||
"errorInviteTitle": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"videoStatus": {
|
||||
"audioOnly": "",
|
||||
"audioOnlyExpanded": "",
|
||||
"callQuality": "",
|
||||
"hd": "",
|
||||
"hdTooltip": "",
|
||||
"highDefinition": "",
|
||||
"labelTooiltipNoVideo": "",
|
||||
"labelTooltipAudioOnly": "",
|
||||
"labelTooltipVideo": "",
|
||||
"ld": "",
|
||||
"ldTooltip": "",
|
||||
"lowDefinition": "",
|
||||
"onlyAudioAvailable": "",
|
||||
"onlyAudioSupported": "",
|
||||
"p2pEnabled": "",
|
||||
"p2pVideoQualityDescription": "",
|
||||
"qualityButtonTip": "",
|
||||
"recHighDefinitionOnly": "",
|
||||
"sd": "",
|
||||
"sdTooltip": "",
|
||||
"standardDefinition": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"kick": "",
|
||||
"moderator": "",
|
||||
"mute": "",
|
||||
"muted": "",
|
||||
"remoteControl": "",
|
||||
"videomute": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
"join": "",
|
||||
"roomname": ""
|
||||
},
|
||||
"appDescription": "",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "",
|
||||
"video": ""
|
||||
},
|
||||
"calendar": "",
|
||||
"connectCalendarButton": "",
|
||||
"connectCalendarText": "",
|
||||
"enterRoomTitle": "",
|
||||
"go": "",
|
||||
"join": "",
|
||||
"privacy": "",
|
||||
"recentList": "",
|
||||
"recentListDelete": "",
|
||||
"recentListEmpty": "",
|
||||
"roomname": "",
|
||||
"roomnameHint": "",
|
||||
"sendFeedback": "",
|
||||
"terms": "",
|
||||
"title": ""
|
||||
}
|
||||
}
|
||||
1185
lang/main-oc.json
1185
lang/main-oc.json
File diff suppressed because it is too large
Load Diff
1260
lang/main-ptBR.json
1260
lang/main-ptBR.json
File diff suppressed because it is too large
Load Diff
1204
lang/main-vi.json
1204
lang/main-vi.json
File diff suppressed because it is too large
Load Diff
@@ -478,14 +478,16 @@
|
||||
"mutedTitle": "You're muted!",
|
||||
"raisedHand": "__name__ would like to speak.",
|
||||
"somebody": "Somebody",
|
||||
"startSilentTitle": "You joined with no audio output!",
|
||||
"startSilentDescription": "Rejoin the meeting to enable audio",
|
||||
"suboptimalExperienceDescription": "Eer... we are afraid your experience with __appName__ isn't going to be that great here. We are looking for ways to improve this but, until then, please try using one of the <a href='static/recommendedBrowsers.html' target='_blank'>fully supported browsers</a>.",
|
||||
"suboptimalExperienceTitle": "Browser Warning",
|
||||
"newDeviceCameraTitle": "New camera detected",
|
||||
"newDeviceMicTitle": "New microphone detected",
|
||||
"newDeviceCameraTitle": "New audio output detected",
|
||||
"newDeviceAudioTitle": "New audio device detected",
|
||||
"newDeviceAction": "Use"
|
||||
},
|
||||
"passwordSetRemotely": "set by another member",
|
||||
"passwordDigitsOnly": "Up to __number__ digits",
|
||||
"poweredby": "powered by",
|
||||
"presenceStatus": {
|
||||
"busy": "Busy",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { setSubject } from '../../react/features/base/conference';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import { invite } from '../../react/features/invite';
|
||||
import { toggleTileView } from '../../react/features/video-layout';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import { API_ID } from './constants';
|
||||
@@ -97,9 +98,14 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('screen.sharing.toggled'));
|
||||
toggleScreenSharing();
|
||||
},
|
||||
'video-hangup': () => {
|
||||
'toggle-tile-view': () => {
|
||||
sendAnalytics(createApiEvent('tile-view.toggled'));
|
||||
|
||||
APP.store.dispatch(toggleTileView());
|
||||
},
|
||||
'video-hangup': (showFeedbackDialog = true) => {
|
||||
sendAnalytics(createApiEvent('video.hangup'));
|
||||
APP.conference.hangup(true);
|
||||
APP.conference.hangup(showFeedbackDialog);
|
||||
},
|
||||
'email': email => {
|
||||
sendAnalytics(createApiEvent('email.changed'));
|
||||
@@ -553,6 +559,38 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of an unexpected camera-related error having
|
||||
* occurred.
|
||||
*
|
||||
* @param {string} type - The type of the camera error.
|
||||
* @param {string} message - Additional information about the error.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyOnCameraError(type: string, message: string) {
|
||||
this._sendEvent({
|
||||
name: 'camera-error',
|
||||
type,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application of an unexpected mic-related error having
|
||||
* occurred.
|
||||
*
|
||||
* @param {string} type - The type of the mic error.
|
||||
* @param {string} message - Additional information about the error.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyOnMicError(type: string, message: string) {
|
||||
this._sendEvent({
|
||||
name: 'mic-error',
|
||||
type,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that conference feedback
|
||||
* has been submitted. Intended to be used in conjunction with the
|
||||
@@ -622,6 +660,21 @@ class API {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify external application (if API is enabled) that tile view has been
|
||||
* entered or exited.
|
||||
*
|
||||
* @param {string} enabled - True if tile view is currently displayed, false
|
||||
* otherwise.
|
||||
* @returns {void}
|
||||
*/
|
||||
notifyTileViewChanged(enabled: boolean) {
|
||||
this._sendEvent({
|
||||
name: 'tile-view-changed',
|
||||
enabled
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the allocated resources.
|
||||
*
|
||||
|
||||
6
modules/API/external/external_api.js
vendored
6
modules/API/external/external_api.js
vendored
@@ -39,6 +39,7 @@ const commands = {
|
||||
toggleChat: 'toggle-chat',
|
||||
toggleFilmStrip: 'toggle-film-strip',
|
||||
toggleShareScreen: 'toggle-share-screen',
|
||||
toggleTileView: 'toggle-tile-view',
|
||||
toggleVideo: 'toggle-video'
|
||||
};
|
||||
|
||||
@@ -50,6 +51,7 @@ const events = {
|
||||
'avatar-changed': 'avatarChanged',
|
||||
'audio-availability-changed': 'audioAvailabilityChanged',
|
||||
'audio-mute-status-changed': 'audioMuteStatusChanged',
|
||||
'camera-error': 'cameraError',
|
||||
'device-list-changed': 'deviceListChanged',
|
||||
'display-name-change': 'displayNameChange',
|
||||
'email-change': 'emailChange',
|
||||
@@ -57,6 +59,7 @@ const events = {
|
||||
'feedback-prompt-displayed': 'feedbackPromptDisplayed',
|
||||
'filmstrip-display-changed': 'filmstripDisplayChanged',
|
||||
'incoming-message': 'incomingMessage',
|
||||
'mic-error': 'micError',
|
||||
'outgoing-message': 'outgoingMessage',
|
||||
'participant-joined': 'participantJoined',
|
||||
'participant-left': 'participantLeft',
|
||||
@@ -67,7 +70,8 @@ const events = {
|
||||
'video-availability-changed': 'videoAvailabilityChanged',
|
||||
'video-mute-status-changed': 'videoMuteStatusChanged',
|
||||
'screen-sharing-status-changed': 'screenSharingStatusChanged',
|
||||
'subject-change': 'subjectChange'
|
||||
'subject-change': 'subjectChange',
|
||||
'tile-view-changed': 'tileViewChanged'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
104
modules/UI/UI.js
104
modules/UI/UI.js
@@ -13,16 +13,12 @@ import SharedVideoManager from './shared_video/SharedVideo';
|
||||
import VideoLayout from './videolayout/VideoLayout';
|
||||
import Filmstrip from './videolayout/Filmstrip';
|
||||
|
||||
import { JitsiTrackErrors } from '../../react/features/base/lib-jitsi-meet';
|
||||
import { getLocalParticipant } from '../../react/features/base/participants';
|
||||
import { toggleChat } from '../../react/features/chat';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import { setFilmstripVisible } from '../../react/features/filmstrip';
|
||||
import {
|
||||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
} from '../../react/features/notifications';
|
||||
import { setNotificationsEnabled } from '../../react/features/notifications';
|
||||
import {
|
||||
dockToolbox,
|
||||
setToolboxEnabled,
|
||||
@@ -40,39 +36,6 @@ UI.eventEmitter = eventEmitter;
|
||||
let etherpadManager;
|
||||
let sharedVideoManager;
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
microphone: {},
|
||||
camera: {}
|
||||
};
|
||||
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]
|
||||
= 'dialog.cameraUnsupportedResolutionError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.GENERAL]
|
||||
= 'dialog.cameraUnknownError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.PERMISSION_DENIED]
|
||||
= 'dialog.cameraPermissionDeniedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.NOT_FOUND]
|
||||
= 'dialog.cameraNotFoundError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[JitsiTrackErrors.CONSTRAINT_FAILED]
|
||||
= 'dialog.cameraConstraintFailedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
|
||||
= 'dialog.cameraNotSendingData';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.GENERAL]
|
||||
= 'dialog.micUnknownError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.PERMISSION_DENIED]
|
||||
= 'dialog.micPermissionDeniedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[JitsiTrackErrors.NOT_FOUND]
|
||||
= 'dialog.micNotFoundError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.CONSTRAINT_FAILED]
|
||||
= 'dialog.micConstraintFailedError';
|
||||
JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.NO_DATA_FROM_SOURCE]
|
||||
= 'dialog.micNotSendingData';
|
||||
|
||||
const UIListeners = new Map([
|
||||
[
|
||||
UIEvents.ETHERPAD_CLICKED,
|
||||
@@ -295,12 +258,6 @@ UI.addLocalStream = track => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Removed remote stream from UI.
|
||||
* @param {JitsiTrack} track stream to remove
|
||||
*/
|
||||
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
@@ -774,65 +731,6 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a notifications about the passed in microphone error.
|
||||
*
|
||||
* @param {JitsiTrackError} micError - An error object related to using or
|
||||
* acquiring an audio stream.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.showMicErrorNotification = function(micError) {
|
||||
if (!micError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, name } = micError;
|
||||
|
||||
const micJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
|
||||
const micErrorMsg = micJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.GENERAL];
|
||||
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
APP.store.dispatch(showWarningNotification({
|
||||
description: additionalMicErrorMsg,
|
||||
descriptionKey: micErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.microphonePermission'
|
||||
: 'deviceError.microphoneError'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows a notifications about the passed in camera error.
|
||||
*
|
||||
* @param {JitsiTrackError} cameraError - An error object related to using or
|
||||
* acquiring a video stream.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.showCameraErrorNotification = function(cameraError) {
|
||||
if (!cameraError) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, name } = cameraError;
|
||||
|
||||
const cameraJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.GENERAL];
|
||||
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
APP.store.dispatch(showWarningNotification({
|
||||
description: additionalCameraErrorMsg,
|
||||
descriptionKey: cameraErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.cameraPermission' : 'deviceError.cameraError'
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Shows error dialog that informs the user that no data is received from the
|
||||
* device.
|
||||
|
||||
@@ -361,6 +361,12 @@ const Filmstrip = {
|
||||
'min-width': `${local.thumbWidth}px`,
|
||||
width: `${local.thumbWidth}px`
|
||||
});
|
||||
|
||||
const avatarSize = local.thumbHeight / 2;
|
||||
|
||||
thumbs.localThumb.find('.avatar-container')
|
||||
.height(avatarSize)
|
||||
.width(avatarSize);
|
||||
}
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
@@ -371,6 +377,12 @@ const Filmstrip = {
|
||||
'min-width': `${remote.thumbWidth}px`,
|
||||
width: `${remote.thumbWidth}px`
|
||||
});
|
||||
|
||||
const avatarSize = remote.thumbHeight / 2;
|
||||
|
||||
thumbs.remoteThumbs.find('.avatar-container')
|
||||
.height(avatarSize)
|
||||
.width(avatarSize);
|
||||
}
|
||||
|
||||
const currentLayout = getCurrentLayout(APP.store.getState());
|
||||
|
||||
@@ -32,7 +32,7 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
|
||||
|
||||
this.localVideoId = null;
|
||||
this.bindHoverHandler();
|
||||
if (config.enableLocalVideoFlip) {
|
||||
if (!config.disableLocalVideoFlip) {
|
||||
this._buildContextMenu();
|
||||
}
|
||||
this.isLocal = true;
|
||||
|
||||
@@ -357,6 +357,11 @@ RemoteVideo.prototype.removeRemoteStreamElement = function(stream) {
|
||||
logger.info(`${isVideo ? 'Video' : 'Audio'
|
||||
} removed ${this.id}`, select);
|
||||
|
||||
|
||||
if (stream === this.videoStream) {
|
||||
this.videoStream = null;
|
||||
}
|
||||
|
||||
this.updateView();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
/* global $, APP, config, interfaceConfig */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React from 'react';
|
||||
@@ -199,6 +199,8 @@ SmallVideo.createStreamElement = function(stream) {
|
||||
|
||||
if (isVideo) {
|
||||
element.setAttribute('muted', 'true');
|
||||
} else if (config.startSilent) {
|
||||
element.muted = true;
|
||||
}
|
||||
|
||||
element.autoplay = true;
|
||||
|
||||
@@ -348,10 +348,6 @@ const VideoLayout = {
|
||||
remoteVideo.removeRemoteStreamElement(stream);
|
||||
}
|
||||
|
||||
if (stream.isVideoTrack()) {
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
|
||||
this.updateMutedForNoTracks(id, stream.getType());
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* global APP, JitsiMeetJS */
|
||||
|
||||
import { getAudioOutputDeviceId } from '../../react/features/base/devices';
|
||||
import {
|
||||
getAudioOutputDeviceId,
|
||||
notifyCameraError,
|
||||
notifyMicError
|
||||
} from '../../react/features/base/devices';
|
||||
import {
|
||||
getUserSelectedCameraDeviceId,
|
||||
getUserSelectedMicDeviceId,
|
||||
@@ -176,11 +180,11 @@ export default {
|
||||
]))
|
||||
.then(tracks => {
|
||||
if (audioTrackError) {
|
||||
APP.UI.showMicErrorNotification(audioTrackError);
|
||||
APP.store.dispatch(notifyMicError(audioTrackError));
|
||||
}
|
||||
|
||||
if (videoTrackError) {
|
||||
APP.UI.showCameraErrorNotification(videoTrackError);
|
||||
APP.store.dispatch(notifyCameraError(videoTrackError));
|
||||
}
|
||||
|
||||
return tracks.filter(t => typeof t !== 'undefined');
|
||||
@@ -205,7 +209,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
audioTrackError = err;
|
||||
showError && APP.UI.showMicErrorNotification(err);
|
||||
showError && APP.store.disptach(notifyMicError(err));
|
||||
|
||||
return [];
|
||||
}));
|
||||
@@ -223,7 +227,7 @@ export default {
|
||||
})
|
||||
.catch(err => {
|
||||
videoTrackError = err;
|
||||
showError && APP.UI.showCameraErrorNotification(err);
|
||||
showError && APP.store.dispatch(notifyCameraError(err));
|
||||
|
||||
return [];
|
||||
}));
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -8927,8 +8927,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"from": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"version": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
|
||||
"from": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"jsrsasign": "8.0.12",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#23503b0efd4b97989af7b10696ec9ae790aec7bb",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.19.4",
|
||||
|
||||
@@ -372,6 +372,28 @@ export function createLiveStreamingDialogEvent(dialogName, buttonName) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event with the local tracks duration.
|
||||
*
|
||||
* @param {Object} duration - The object with the duration of the local tracks.
|
||||
* @returns {Object} The event in a format suitable for sending via
|
||||
* sendAnalytics.
|
||||
*/
|
||||
export function createLocalTracksDurationEvent(duration) {
|
||||
const { audio, video, conference } = duration;
|
||||
const { camera, desktop } = video;
|
||||
|
||||
return {
|
||||
action: 'local.tracks.durations',
|
||||
attributes: {
|
||||
audio: audio.value,
|
||||
camera: camera.value,
|
||||
conference: conference.value,
|
||||
desktop: desktop.value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an event which indicates that an action related to recording has
|
||||
* occured.
|
||||
|
||||
11
react/features/analytics/actionTypes.js
Normal file
11
react/features/analytics/actionTypes.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals that local media duration has changed.
|
||||
*
|
||||
* {
|
||||
* type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
* localTracksDuration: Object
|
||||
* }
|
||||
*/
|
||||
export const UPDATE_LOCAL_TRACKS_DURATION = 'UPDATE_LOCAL_TRACKS_DURATION';
|
||||
@@ -15,7 +15,7 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
const { amplitudeAPPKey, host } = options;
|
||||
const { amplitudeAPPKey, host, user } = options;
|
||||
|
||||
if (!amplitudeAPPKey) {
|
||||
throw new Error('Failed to initialize Amplitude handler, no APP key');
|
||||
@@ -28,6 +28,10 @@ export default class AmplitudeHandler extends AbstractHandler {
|
||||
};
|
||||
|
||||
amplitude.getInstance(this._amplitudeOptions).init(amplitudeAPPKey);
|
||||
|
||||
if (user) {
|
||||
amplitude.getInstance(this._amplitudeOptions).setUserId(user);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,6 +31,16 @@ class Amplitude {
|
||||
AmplitudeNative.init(this._instanceName, apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an identifier for the current user.
|
||||
*
|
||||
* @param {string} userId - The new user id.
|
||||
* @returns {void}
|
||||
*/
|
||||
setUserId(userId) {
|
||||
AmplitudeNative.setUserId(this._instanceName, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets user properties for the current user.
|
||||
*
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './AnalyticsEvents';
|
||||
export * from './functions';
|
||||
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
|
||||
@@ -1,8 +1,74 @@
|
||||
import { SET_ROOM } from '../base/conference';
|
||||
// @flow
|
||||
|
||||
import {
|
||||
CONFERENCE_JOINED,
|
||||
CONFERENCE_WILL_LEAVE,
|
||||
SET_ROOM
|
||||
} from '../base/conference';
|
||||
import { SET_CONFIG } from '../base/config';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import {
|
||||
getLocalAudioTrack,
|
||||
getLocalVideoTrack,
|
||||
TRACK_ADDED,
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from '../base/tracks';
|
||||
|
||||
import { initAnalytics, resetAnalytics } from './functions';
|
||||
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
|
||||
import { createLocalTracksDurationEvent } from './AnalyticsEvents';
|
||||
import { initAnalytics, resetAnalytics, sendAnalytics } from './functions';
|
||||
|
||||
/**
|
||||
* Calculates the duration of the local tracks.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {Object} - The local tracks duration.
|
||||
*/
|
||||
function calculateLocalTrackDuration(state) {
|
||||
const now = Date.now();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const { conference } = state['features/base/conference'];
|
||||
const { audio, video } = localTracksDuration;
|
||||
const { camera, desktop } = video;
|
||||
const tracks = state['features/base/tracks'];
|
||||
const audioTrack = getLocalAudioTrack(tracks);
|
||||
const videoTrack = getLocalVideoTrack(tracks);
|
||||
const newDuration = { ...localTracksDuration };
|
||||
|
||||
if (!audioTrack || audioTrack.muted || !conference) {
|
||||
newDuration.audio = {
|
||||
startedTime: -1,
|
||||
value: audio.value + (audio.startedTime === -1 ? 0 : now - audio.startedTime)
|
||||
};
|
||||
} else if (audio.startedTime === -1) {
|
||||
newDuration.audio.startedTime = now;
|
||||
}
|
||||
|
||||
if (!videoTrack || videoTrack.muted || !conference) {
|
||||
newDuration.video = {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: camera.value + (camera.startedTime === -1 ? 0 : now - camera.startedTime)
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: desktop.value + (desktop.startedTime === -1 ? 0 : now - desktop.startedTime)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const { videoType } = videoTrack;
|
||||
|
||||
if (video[videoType].startedTime === -1) {
|
||||
newDuration.video[videoType].startedTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...localTracksDuration,
|
||||
...newDuration
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware which intercepts config actions to handle evaluating analytics
|
||||
@@ -12,25 +78,81 @@ import { initAnalytics, resetAnalytics } from './functions';
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case SET_CONFIG: {
|
||||
if (action.type === SET_CONFIG) {
|
||||
if (navigator.product === 'ReactNative') {
|
||||
// Reseting the analytics is currently not needed for web because
|
||||
// the user will be redirected to another page and new instance of
|
||||
// Analytics will be created and initialized.
|
||||
resetAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
const result = next(action);
|
||||
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: Date.now(),
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_WILL_LEAVE: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
const newLocalTracksDuration = {
|
||||
...calculateLocalTrackDuration(state),
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: Date.now() - localTracksDuration.conference.startedTime
|
||||
}
|
||||
};
|
||||
|
||||
sendAnalytics(createLocalTracksDurationEvent(newLocalTracksDuration));
|
||||
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: newLocalTracksDuration
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SET_ROOM: {
|
||||
const result = next(action);
|
||||
|
||||
initAnalytics(store);
|
||||
break;
|
||||
}
|
||||
case TRACK_ADDED:
|
||||
case TRACK_REMOVED:
|
||||
case TRACK_UPDATED: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { localTracksDuration } = state['features/analytics'];
|
||||
|
||||
return result;
|
||||
if (localTracksDuration.conference.startedTime === -1) {
|
||||
// We don't want to track the media duration if the conference is not joined yet because otherwise we won't
|
||||
// be able to compare them with the conference duration (from conference join to conference will leave).
|
||||
break;
|
||||
}
|
||||
dispatch({
|
||||
type: UPDATE_LOCAL_TRACKS_DURATION,
|
||||
localTracksDuration: {
|
||||
...localTracksDuration,
|
||||
...calculateLocalTrackDuration(state)
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
return result;
|
||||
});
|
||||
|
||||
51
react/features/analytics/reducer.js
Normal file
51
react/features/analytics/reducer.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../base/redux';
|
||||
|
||||
import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes';
|
||||
|
||||
/**
|
||||
* Initial state.
|
||||
*/
|
||||
const DEFAULT_STATE = {
|
||||
localTracksDuration: {
|
||||
audio: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
video: {
|
||||
camera: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
},
|
||||
desktop: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
},
|
||||
conference: {
|
||||
startedTime: -1,
|
||||
value: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Listen for actions which changes the state of the analytics feature.
|
||||
*
|
||||
* @param {Object} state - The Redux state of the feature features/analytics.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @returns {Object}
|
||||
*/
|
||||
ReducerRegistry.register('features/analytics', (state = DEFAULT_STATE, action) => {
|
||||
switch (action.type) {
|
||||
case UPDATE_LOCAL_TRACKS_DURATION:
|
||||
return {
|
||||
...state,
|
||||
localTracksDuration: action.localTracksDuration
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
});
|
||||
@@ -10,8 +10,9 @@ import {
|
||||
setConfig,
|
||||
storeConfig
|
||||
} from '../base/config';
|
||||
import { setLocationURL } from '../base/connection';
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { createDesiredLocalTracks } from '../base/tracks';
|
||||
import { parseURIString, toURLString } from '../base/util';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
@@ -58,6 +59,12 @@ export function appNavigate(uri: ?string) {
|
||||
const { contextRoot, host, room } = location;
|
||||
const locationURL = new URL(location.toString());
|
||||
|
||||
// Disconnect from any current conference.
|
||||
// FIXME: unify with web.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
dispatch(disconnect());
|
||||
}
|
||||
|
||||
dispatch(configWillLoad(locationURL, room));
|
||||
|
||||
let protocol = location.protocol.toLowerCase();
|
||||
@@ -74,25 +81,40 @@ export function appNavigate(uri: ?string) {
|
||||
|
||||
let config;
|
||||
|
||||
try {
|
||||
config = await loadConfig(url);
|
||||
dispatch(storeConfig(baseURL, config));
|
||||
} catch (error) {
|
||||
// Avoid (re)loading the config when there is no room.
|
||||
if (!room) {
|
||||
config = restoreConfig(baseURL);
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
if (!config) {
|
||||
try {
|
||||
config = await loadConfig(url);
|
||||
dispatch(storeConfig(baseURL, config));
|
||||
} catch (error) {
|
||||
config = restoreConfig(baseURL);
|
||||
|
||||
return;
|
||||
if (!config) {
|
||||
dispatch(loadConfigError(error, locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (getState()['features/base/config'].locationURL === locationURL) {
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
} else {
|
||||
if (getState()['features/base/config'].locationURL !== locationURL) {
|
||||
dispatch(loadConfigError(new Error('Config no longer needed!'), locationURL));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setLocationURL(locationURL));
|
||||
dispatch(setConfig(config));
|
||||
dispatch(setRoom(room));
|
||||
|
||||
// FIXME: unify with web, currently the connection and track creation happens in conference.js.
|
||||
if (room && navigator.product === 'ReactNative') {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import React from 'react';
|
||||
import { DialogContainer } from '../../base/dialog';
|
||||
import '../../base/responsive-ui';
|
||||
import '../../chat';
|
||||
import '../../external-api';
|
||||
import '../../room-lock';
|
||||
import '../../video-layout';
|
||||
|
||||
|
||||
@@ -64,12 +64,14 @@ class WaitForOwnerDialog extends Component<Props> {
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
cancelKey = 'dialog.Cancel'
|
||||
contentKey = {
|
||||
{
|
||||
key: 'dialog.WaitForHostMsgWOk',
|
||||
params: { room }
|
||||
}
|
||||
}
|
||||
okKey = 'dialog.Ok'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onLogin } />
|
||||
);
|
||||
|
||||
@@ -17,11 +17,6 @@ declare var APP;
|
||||
*/
|
||||
export function appWillMount(app: Object) {
|
||||
return (dispatch: Dispatch<any>) => {
|
||||
dispatch({
|
||||
type: APP_WILL_MOUNT,
|
||||
app
|
||||
});
|
||||
|
||||
// TODO There was a redux action creator appInit which I did not like
|
||||
// because we already had the redux action creator appWillMount and,
|
||||
// respectively, the redux action APP_WILL_MOUNT. So I set out to remove
|
||||
@@ -30,6 +25,11 @@ export function appWillMount(app: Object) {
|
||||
// API module into its own feature yet so we're bound to work on that in
|
||||
// the future.
|
||||
typeof APP === 'object' && APP.API.init();
|
||||
|
||||
dispatch({
|
||||
type: APP_WILL_MOUNT,
|
||||
app
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ function _addConferenceListeners(conference, dispatch) {
|
||||
conference.on(
|
||||
JitsiConferenceEvents.CONFERENCE_LEFT,
|
||||
(...args) => dispatch(conferenceLeft(conference, ...args)));
|
||||
conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
||||
(...args) => dispatch(conferenceSubjectChanged(...args)));
|
||||
|
||||
conference.on(
|
||||
JitsiConferenceEvents.KICKED,
|
||||
@@ -757,10 +759,6 @@ export function setSubject(subject: string = '') {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
|
||||
if (conference) {
|
||||
dispatch({
|
||||
type: SET_PENDING_SUBJECT_CHANGE,
|
||||
subject: undefined
|
||||
});
|
||||
conference.setSubject(subject);
|
||||
} else {
|
||||
dispatch({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -155,10 +157,10 @@ export function forEachConference(
|
||||
export function getConferenceName(stateful: Function | Object): string {
|
||||
const state = toState(stateful);
|
||||
const { callee } = state['features/base/jwt'];
|
||||
const { callDisplayName } = state['features/base/config'];
|
||||
const { pendingSubjectChange, room, subject } = state['features/base/conference'];
|
||||
|
||||
return state['features/base/config'].callDisplayName
|
||||
|| (callee && callee.name)
|
||||
|| state['features/base/conference'].room;
|
||||
return pendingSubjectChange || subject || callDisplayName || (callee && callee.name) || _.startCase(room);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,6 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
|
||||
|
||||
import {
|
||||
conferenceFailed,
|
||||
conferenceLeft,
|
||||
conferenceWillLeave,
|
||||
createConference,
|
||||
setLastN,
|
||||
@@ -38,6 +37,7 @@ import {
|
||||
DATA_CHANNEL_OPENED,
|
||||
SET_AUDIO_ONLY,
|
||||
SET_LASTN,
|
||||
SET_PENDING_SUBJECT_CHANGE,
|
||||
SET_ROOM
|
||||
} from './actionTypes';
|
||||
import {
|
||||
@@ -329,10 +329,17 @@ function _connectionFailed({ dispatch, getState }, next, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _conferenceSubjectChanged({ getState }, next, action) {
|
||||
function _conferenceSubjectChanged({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
const { subject } = getState()['features/base/conference'];
|
||||
|
||||
if (subject) {
|
||||
dispatch({
|
||||
type: SET_PENDING_SUBJECT_CHANGE,
|
||||
subject: undefined
|
||||
});
|
||||
}
|
||||
|
||||
typeof APP === 'object' && APP.API.notifySubjectChanged(subject);
|
||||
|
||||
return result;
|
||||
@@ -567,48 +574,30 @@ function _setReceiverVideoConstraint(conference, preferred, max) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature {@code base/conference} that the redix action
|
||||
* {@link SET_ROOM} is being dispatched within a specific redux store.
|
||||
* Notifies the feature base/conference that the action
|
||||
* {@code SET_ROOM} is being dispatched within a specific
|
||||
* redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} to the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ROOM} which is being
|
||||
* dispatched in the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code SET_ROOM}
|
||||
* which is being dispatched in the specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _setRoom({ dispatch, getState }, next, action) {
|
||||
const result = next(action);
|
||||
|
||||
// By the time SET_ROOM is dispatched, base/connection's locationURL and
|
||||
// base/conference's leaving should be the only conference-related sources
|
||||
// of truth.
|
||||
const state = getState();
|
||||
const { leaving } = state['features/base/conference'];
|
||||
const { locationURL } = state['features/base/connection'];
|
||||
const dispatchConferenceLeft = new Set();
|
||||
const { subject } = state['features/base/config'];
|
||||
const { room } = action;
|
||||
|
||||
// Figure out which of the JitsiConferences referenced by base/conference
|
||||
// have not dispatched or are not likely to dispatch CONFERENCE_FAILED and
|
||||
// CONFERENCE_LEFT.
|
||||
forEachConference(state, (conference, url) => {
|
||||
if (conference !== leaving && url && url !== locationURL) {
|
||||
dispatchConferenceLeft.add(conference);
|
||||
}
|
||||
|
||||
return true; // All JitsiConference instances are to be examined.
|
||||
});
|
||||
|
||||
// Dispatch CONFERENCE_LEFT for the JitsiConferences referenced by
|
||||
// base/conference which have not dispatched or are not likely to dispatch
|
||||
// CONFERENCE_FAILED or CONFERENCE_LEFT.
|
||||
for (const conference of dispatchConferenceLeft) {
|
||||
dispatch(conferenceLeft(conference));
|
||||
if (room) {
|
||||
// Set the stored subject.
|
||||
dispatch(setSubject(subject));
|
||||
}
|
||||
|
||||
return result;
|
||||
return next(action);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,6 +21,7 @@ const WHITELISTED_KEYS = [
|
||||
'_peerConnStatusOutOfLastNTimeout',
|
||||
'_peerConnStatusRtcMuteTimeout',
|
||||
'abTesting',
|
||||
'analytics.disabled',
|
||||
'autoRecord',
|
||||
'autoRecordToken',
|
||||
'avgRtpStatsN',
|
||||
@@ -101,7 +102,7 @@ const WHITELISTED_KEYS = [
|
||||
'enableDisplayNameInStats',
|
||||
'enableLayerSuspension',
|
||||
'enableLipSync',
|
||||
'enableLocalVideoFlip',
|
||||
'disableLocalVideoFlip',
|
||||
'enableRemb',
|
||||
'enableStatsID',
|
||||
'enableTalkWhileMuted',
|
||||
@@ -131,10 +132,12 @@ const WHITELISTED_KEYS = [
|
||||
'startAudioMuted',
|
||||
'startAudioOnly',
|
||||
'startBitrate',
|
||||
'startSilent',
|
||||
'startScreenSharing',
|
||||
'startVideoMuted',
|
||||
'startWithAudioMuted',
|
||||
'startWithVideoMuted',
|
||||
'subject',
|
||||
'testing',
|
||||
'useIPv6',
|
||||
'useNicks',
|
||||
|
||||
@@ -370,10 +370,7 @@ export function disconnect() {
|
||||
if (connection_) {
|
||||
promise = promise.then(() => connection_.disconnect());
|
||||
} else {
|
||||
// FIXME: We have no connection! Fake a disconnect. Because of how the current disconnec is implemented
|
||||
// (by doing the diconnect() in the Conference component unmount) we have lost the location URL already.
|
||||
// Oh well, at least send the event.
|
||||
promise.then(() => dispatch(_connectionDisconnected({}, '')));
|
||||
logger.info('No connection found while disconnecting.');
|
||||
}
|
||||
|
||||
return promise;
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a camera.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_CAMERA_ERROR = 'NOTIFY_CAMERA_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that an error occurred while obtaining
|
||||
* a microphone.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }
|
||||
*/
|
||||
export const NOTIFY_MIC_ERROR = 'NOTIFY_MIC_ERROR';
|
||||
|
||||
/**
|
||||
* The type of Redux action which signals that the currently used audio
|
||||
* input device should be changed.
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import {
|
||||
ADD_PENDING_DEVICE_REQUEST,
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
REMOVE_PENDING_DEVICE_REQUESTS,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE,
|
||||
@@ -21,6 +23,28 @@ import {
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Maps the WebRTC string for device type to the keys used to store configure,
|
||||
* within redux, which devices should be used by default.
|
||||
*/
|
||||
const DEVICE_TYPE_TO_SETTINGS_KEYS = {
|
||||
audioInput: {
|
||||
currentDeviceId: 'micDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedMicDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedMicDeviceLabel'
|
||||
},
|
||||
audioOutput: {
|
||||
currentDeviceId: 'audioOutputDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedAudioOutputDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedAudioOutputDeviceLabel'
|
||||
},
|
||||
videoInput: {
|
||||
currentDeviceId: 'audioOutputDeviceId',
|
||||
userSelectedDeviceId: 'userSelectedCameraDeviceId',
|
||||
userSelectedDeviceLabel: 'userSelectedCameraDeviceLabel'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a pending device request.
|
||||
*
|
||||
@@ -70,19 +94,19 @@ export function configureInitialDevices() {
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const newSettings = {};
|
||||
const devicesKeysToSettingsKeys = {
|
||||
audioInput: 'micDeviceId',
|
||||
audioOutput: 'audioOutputDeviceId',
|
||||
videoInput: 'cameraDeviceId'
|
||||
};
|
||||
|
||||
Object.keys(deviceLabels).forEach(key => {
|
||||
const label = deviceLabels[key];
|
||||
const deviceId = getDeviceIdByLabel(state, label, key);
|
||||
|
||||
if (deviceId) {
|
||||
newSettings[devicesKeysToSettingsKeys[key]] = deviceId;
|
||||
const settingsTranslationMap = DEVICE_TYPE_TO_SETTINGS_KEYS[key];
|
||||
|
||||
newSettings[settingsTranslationMap.currentDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceId] = deviceId;
|
||||
newSettings[settingsTranslationMap.userSelectedDeviceLabel] = label;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -126,6 +150,43 @@ export function getAvailableDevices() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a camera.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {string} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_CAMERA_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyCameraError(error) {
|
||||
return {
|
||||
type: NOTIFY_CAMERA_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that an error occurred while trying to obtain a track from a mic.
|
||||
*
|
||||
* @param {Object} error - The device error, as provided by lib-jitsi-meet.
|
||||
* @param {Object} error.name - The constant for the type of the error.
|
||||
* @param {string} error.message - Optional additional information about the
|
||||
* error.
|
||||
* @returns {{
|
||||
* type: NOTIFY_MIC_ERROR,
|
||||
* error: Object
|
||||
* }}
|
||||
*/
|
||||
export function notifyMicError(error) {
|
||||
return {
|
||||
type: NOTIFY_MIC_ERROR,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all pending device requests.
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CONFERENCE_JOINED } from '../conference';
|
||||
import { processExternalDeviceRequest } from '../../device-selection';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
|
||||
import {
|
||||
removePendingDeviceRequests,
|
||||
@@ -12,15 +13,35 @@ import {
|
||||
} from './actions';
|
||||
import {
|
||||
CHECK_AND_NOTIFY_FOR_NEW_DEVICE,
|
||||
NOTIFY_CAMERA_ERROR,
|
||||
NOTIFY_MIC_ERROR,
|
||||
SET_AUDIO_INPUT_DEVICE,
|
||||
SET_VIDEO_INPUT_DEVICE
|
||||
} from './actionTypes';
|
||||
import { showNotification } from '../../notifications';
|
||||
import { showNotification, showWarningNotification } from '../../notifications';
|
||||
import { updateSettings } from '../settings';
|
||||
import { setAudioOutputDeviceId } from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
const JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP = {
|
||||
microphone: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.micConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.micUnknownError',
|
||||
[JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.micNotSendingData',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.micNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.micPermissionDeniedError'
|
||||
},
|
||||
camera: {
|
||||
[JitsiTrackErrors.CONSTRAINT_FAILED]: 'dialog.cameraConstraintFailedError',
|
||||
[JitsiTrackErrors.GENERAL]: 'dialog.cameraUnknownError',
|
||||
[JitsiTrackErrors.NO_DATA_FROM_SOURCE]: 'dialog.cameraNotSendingData',
|
||||
[JitsiTrackErrors.NOT_FOUND]: 'dialog.cameraNotFoundError',
|
||||
[JitsiTrackErrors.PERMISSION_DENIED]: 'dialog.cameraPermissionDeniedError',
|
||||
[JitsiTrackErrors.UNSUPPORTED_RESOLUTION]: 'dialog.cameraUnsupportedResolutionError'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements the middleware of the feature base/devices.
|
||||
*
|
||||
@@ -32,6 +53,53 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case CONFERENCE_JOINED:
|
||||
return _conferenceJoined(store, next, action);
|
||||
case NOTIFY_CAMERA_ERROR: {
|
||||
if (typeof APP !== 'object' || !action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const cameraJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.camera[name];
|
||||
const cameraErrorMsg = cameraJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.camera[JitsiTrackErrors.GENERAL];
|
||||
const additionalCameraErrorMsg = cameraJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalCameraErrorMsg,
|
||||
descriptionKey: cameraErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.cameraPermission' : 'deviceError.cameraError'
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
case NOTIFY_MIC_ERROR: {
|
||||
if (typeof APP !== 'object' || !action.error) {
|
||||
break;
|
||||
}
|
||||
|
||||
const { message, name } = action.error;
|
||||
|
||||
const micJitsiTrackErrorMsg
|
||||
= JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP.microphone[name];
|
||||
const micErrorMsg = micJitsiTrackErrorMsg
|
||||
|| JITSI_TRACK_ERROR_TO_MESSAGE_KEY_MAP
|
||||
.microphone[JitsiTrackErrors.GENERAL];
|
||||
const additionalMicErrorMsg = micJitsiTrackErrorMsg ? null : message;
|
||||
|
||||
store.dispatch(showWarningNotification({
|
||||
description: additionalMicErrorMsg,
|
||||
descriptionKey: micErrorMsg,
|
||||
titleKey: name === JitsiTrackErrors.PERMISSION_DENIED
|
||||
? 'deviceError.microphonePermission'
|
||||
: 'deviceError.microphoneError'
|
||||
}));
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_AUDIO_INPUT_DEVICE:
|
||||
APP.UI.emitEvent(UIEvents.AUDIO_DEVICE_CHANGED, action.deviceId);
|
||||
break;
|
||||
@@ -79,7 +147,9 @@ function _conferenceJoined({ dispatch, getState }, next, action) {
|
||||
|
||||
/**
|
||||
* Finds a new device by comparing new and old array of devices and dispatches
|
||||
* notification with the new device.
|
||||
* notification with the new device. For new devices with same groupId only one
|
||||
* notification will be shown, this is so to avoid showing multiple notifications
|
||||
* for audio input and audio output devices.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
@@ -97,7 +167,25 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
nDevice => !oldDevices.find(
|
||||
device => device.deviceId === nDevice.deviceId));
|
||||
|
||||
onlyNewDevices.forEach(newDevice => {
|
||||
// we group devices by groupID which normally is the grouping by physical device
|
||||
// plugging in headset we provide normally two device, one input and one output
|
||||
// and we want to show only one notification for this physical audio device
|
||||
const devicesGroupBy = onlyNewDevices.reduce((accumulated, value) => {
|
||||
accumulated[value.groupId] = accumulated[value.groupId] || [];
|
||||
accumulated[value.groupId].push(value);
|
||||
|
||||
return accumulated;
|
||||
}, {});
|
||||
|
||||
Object.values(devicesGroupBy).forEach(devicesArray => {
|
||||
|
||||
if (devicesArray.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// let's get the first device as a reference, we will use it for
|
||||
// label and type
|
||||
const newDevice = devicesArray[0];
|
||||
|
||||
// we want to strip any device details that are not very
|
||||
// user friendly, like usb ids put in brackets at the end
|
||||
@@ -115,12 +203,9 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
titleKey = 'notify.newDeviceCameraTitle';
|
||||
break;
|
||||
}
|
||||
case 'audioinput': {
|
||||
titleKey = 'notify.newDeviceMicTitle';
|
||||
break;
|
||||
}
|
||||
case 'audioinput' :
|
||||
case 'audiooutput': {
|
||||
titleKey = 'notify.newDeviceCameraTitle';
|
||||
titleKey = 'notify.newDeviceAudioTitle';
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +214,7 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
description,
|
||||
titleKey,
|
||||
customActionNameKey: 'notify.newDeviceAction',
|
||||
customActionHandler: _useDevice.bind(undefined, store, newDevice)
|
||||
customActionHandler: _useDevice.bind(undefined, store, devicesArray)
|
||||
}));
|
||||
});
|
||||
}
|
||||
@@ -139,47 +224,49 @@ function _checkAndNotifyForNewDevice(store, newDevices, oldDevices) {
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {MediaDeviceInfo} device - The device to save.
|
||||
* @param {Array<MediaDeviceInfo|InputDeviceInfo>} devices - The devices to save.
|
||||
* @returns {boolean} - Returns true in order notifications to be dismissed.
|
||||
* @private
|
||||
*/
|
||||
function _useDevice({ dispatch }, device) {
|
||||
switch (device.kind) {
|
||||
case 'videoinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: device.deviceId,
|
||||
userSelectedCameraDeviceLabel: device.label
|
||||
}));
|
||||
function _useDevice({ dispatch }, devices) {
|
||||
devices.forEach(device => {
|
||||
switch (device.kind) {
|
||||
case 'videoinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedCameraDeviceId: device.deviceId,
|
||||
userSelectedCameraDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setVideoInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audioinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: device.deviceId,
|
||||
userSelectedMicDeviceLabel: device.label
|
||||
}));
|
||||
dispatch(setVideoInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audioinput': {
|
||||
dispatch(updateSettings({
|
||||
userSelectedMicDeviceId: device.deviceId,
|
||||
userSelectedMicDeviceLabel: device.label
|
||||
}));
|
||||
|
||||
dispatch(setAudioInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audiooutput': {
|
||||
setAudioOutputDeviceId(
|
||||
device.deviceId,
|
||||
dispatch,
|
||||
true,
|
||||
device.label)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
'Failed to change audio output device.',
|
||||
'Default or previously set audio output device will',
|
||||
' be used instead.',
|
||||
err);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(setAudioInputDevice(device.deviceId));
|
||||
break;
|
||||
}
|
||||
case 'audiooutput': {
|
||||
setAudioOutputDeviceId(
|
||||
device.deviceId,
|
||||
dispatch,
|
||||
true,
|
||||
device.label)
|
||||
.then(() => logger.log('changed audio output device'))
|
||||
.catch(err => {
|
||||
logger.warn(
|
||||
'Failed to change audio output device.',
|
||||
'Default or previously set audio output device will',
|
||||
' be used instead.',
|
||||
err);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, { PureComponent, type Node } from 'react';
|
||||
import { SafeAreaView, View } from 'react-native';
|
||||
import { Platform, SafeAreaView, ScrollView, View } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../color-scheme';
|
||||
import { SlidingView } from '../../../react';
|
||||
@@ -36,21 +36,6 @@ type Props = {
|
||||
* A component emulating Android's BottomSheet.
|
||||
*/
|
||||
class BottomSheet extends PureComponent<Props> {
|
||||
/**
|
||||
* Assembles a style for the BottomSheet container.
|
||||
*
|
||||
* @private
|
||||
* @returns {StyleType}
|
||||
*/
|
||||
_getContainerStyle() {
|
||||
const { _styles } = this.props;
|
||||
|
||||
return {
|
||||
...styles.container,
|
||||
backgroundColor: _styles.sheet.backgroundColor
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
@@ -65,15 +50,46 @@ class BottomSheet extends PureComponent<Props> {
|
||||
onHide = { this.props.onCancel }
|
||||
position = 'bottom'
|
||||
show = { true }>
|
||||
<SafeAreaView
|
||||
style = { this._getContainerStyle() }>
|
||||
<View style = { _styles.sheet }>
|
||||
{ this.props.children }
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetContainer }>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { styles.sheetAreaCover } />
|
||||
<View
|
||||
style = { [
|
||||
styles.sheetItemContainer,
|
||||
_styles.sheet
|
||||
] }>
|
||||
<ScrollView bounces = { false }>
|
||||
{ this._getWrappedContent() }
|
||||
</ScrollView>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</SlidingView>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the content when needed (iOS 11 and above), or just returns the original children.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_getWrappedContent() {
|
||||
if (Platform.OS === 'ios') {
|
||||
const majorVersionIOS = parseInt(Platform.Version, 10);
|
||||
|
||||
if (majorVersionIOS > 10) {
|
||||
return (
|
||||
<SafeAreaView>
|
||||
{ this.props.children }
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,7 +32,12 @@ type Props = BaseProps & {
|
||||
|
||||
t: Function,
|
||||
|
||||
textInputProps: ?Object
|
||||
textInputProps: ?Object,
|
||||
|
||||
/**
|
||||
* Validating of the input.
|
||||
*/
|
||||
validateInput: ?Function
|
||||
}
|
||||
|
||||
type State = {
|
||||
@@ -118,6 +123,12 @@ class InputDialog extends BaseDialog<Props, State> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_onChangeText(fieldValue) {
|
||||
|
||||
if (this.props.validateInput
|
||||
&& !this.props.validateInput(fieldValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
fieldValue
|
||||
});
|
||||
|
||||
@@ -27,17 +27,28 @@ export const PLACEHOLDER_COLOR = ColorPalette.lightGrey;
|
||||
* been implemented as per the Material Design guidelines:
|
||||
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
|
||||
*/
|
||||
export const bottomSheetStyles = createStyleSheet({
|
||||
export const bottomSheetStyles = {
|
||||
sheetAreaCover: {
|
||||
backgroundColor: ColorPalette.transparent,
|
||||
flex: 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the container of the sheet.
|
||||
*/
|
||||
container: {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
right: 0
|
||||
sheetContainer: {
|
||||
alignItems: 'stretch',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end'
|
||||
},
|
||||
|
||||
sheetItemContainer: {
|
||||
flex: -1,
|
||||
maxHeight: '60%',
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const brandedDialog = createStyleSheet({
|
||||
|
||||
@@ -137,10 +148,7 @@ ColorSchemeRegistry.register('BottomSheet', {
|
||||
* Bottom sheet's base style.
|
||||
*/
|
||||
sheet: {
|
||||
backgroundColor: schemeColor('background'),
|
||||
flex: 1,
|
||||
paddingHorizontal: MD_ITEM_MARGIN_PADDING,
|
||||
paddingVertical: 8
|
||||
backgroundColor: schemeColor('background')
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,9 @@ export function createLocalTrack(type: string, deviceId: string) {
|
||||
* otherwise.
|
||||
*/
|
||||
export function isAnalyticsEnabled(stateful: Function | Object) {
|
||||
return !toState(stateful)['features/base/config'].disableThirdPartyRequests;
|
||||
const { disableThirdPartyRequests, analytics = {} } = toState(stateful)['features/base/config'];
|
||||
|
||||
return !disableThirdPartyRequests && !analytics.disabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -260,6 +260,25 @@ function _getAllParticipants(stateful) {
|
||||
: toState(stateful)['features/base/participants'] || []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all of the meeting participants are moderators.
|
||||
*
|
||||
* @param {Object|Function} stateful -Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEveryoneModerator(stateful: Object | Function) {
|
||||
const participants = _getAllParticipants(stateful);
|
||||
|
||||
for (const participant of participants) {
|
||||
if (participant.role !== PARTICIPANT_ROLE.MODERATOR) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current local participant is a moderator in the
|
||||
* conference.
|
||||
|
||||
@@ -119,12 +119,22 @@ function _initSettings(featureState) {
|
||||
let settings = featureState;
|
||||
|
||||
// Old Settings.js values
|
||||
// FIXME: Let's remove this after a predefined time (e.g. by July 2018) to
|
||||
// avoid garbage in the source.
|
||||
const displayName = _.escape(window.localStorage.getItem('displayname'));
|
||||
const email = _.escape(window.localStorage.getItem('email'));
|
||||
// FIXME: jibri uses old settings.js local storage values to set its display
|
||||
// name and email. Provide another way for jibri to set these values, update
|
||||
// jibri, and remove the old settings.js values.
|
||||
const savedDisplayName = window.localStorage.getItem('displayname');
|
||||
const savedEmail = window.localStorage.getItem('email');
|
||||
let avatarID = _.escape(window.localStorage.getItem('avatarId'));
|
||||
|
||||
// The helper _.escape will convert null to an empty strings. The empty
|
||||
// string will be saved in settings. On app re-load, because an empty string
|
||||
// is a defined value, it will override any value found in local storage.
|
||||
// The workaround is sidestepping _.escape when the value is not set in
|
||||
// local storage.
|
||||
const displayName
|
||||
= savedDisplayName === null ? undefined : _.escape(savedDisplayName);
|
||||
const email = savedEmail === null ? undefined : _.escape(savedEmail);
|
||||
|
||||
if (!avatarID) {
|
||||
// if there is no avatar id, we generate a unique one and use it forever
|
||||
avatarID = randomHexString(32);
|
||||
|
||||
@@ -15,7 +15,6 @@ import UIEvents from '../../../../service/UI/UIEvents';
|
||||
import { createLocalTracksA } from './actions';
|
||||
import {
|
||||
TOGGLE_SCREENSHARING,
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import { getLocalTrack, setTrackMuted } from './functions';
|
||||
@@ -92,14 +91,6 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
break;
|
||||
|
||||
case TRACK_REMOVED:
|
||||
// TODO Remove this middleware case once all UI interested in tracks
|
||||
// being removed are converted to react and listening for store changes.
|
||||
if (typeof APP !== 'undefined' && !action.track.local) {
|
||||
APP.UI.removeRemoteStream(action.track.jitsiTrack);
|
||||
}
|
||||
break;
|
||||
|
||||
case TRACK_UPDATED:
|
||||
// TODO Remove the following calls to APP.UI once components interested
|
||||
// in track mute changes are moved into React and/or redux.
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { BackHandler, SafeAreaView, StatusBar, View } from 'react-native';
|
||||
import { BackHandler, NativeModules, SafeAreaView, StatusBar, View } from 'react-native';
|
||||
|
||||
import { appNavigate } from '../../../app';
|
||||
import { connect, disconnect } from '../../../base/connection';
|
||||
import { getAppProp } from '../../../base/app';
|
||||
import { getParticipantCount } from '../../../base/participants';
|
||||
import { Container, LoadingIndicator, TintedView } from '../../../base/react';
|
||||
import { connect as reactReduxConnect } from '../../../base/redux';
|
||||
import { connect } from '../../../base/redux';
|
||||
import {
|
||||
isNarrowAspectRatio,
|
||||
makeAspectRatioAware
|
||||
} from '../../../base/responsive-ui';
|
||||
import { TestConnectionInfo } from '../../../base/testing';
|
||||
import { createDesiredLocalTracks } from '../../../base/tracks';
|
||||
import { ConferenceNotification } from '../../../calendar-sync';
|
||||
import { Chat } from '../../../chat';
|
||||
import { DisplayNameLabel } from '../../../display-name';
|
||||
@@ -66,40 +64,6 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_largeVideoParticipantId: string,
|
||||
|
||||
/**
|
||||
* Current conference's full URL.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_locationURL: URL,
|
||||
|
||||
/**
|
||||
* The handler which dispatches the (redux) action connect.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onConnect: Function,
|
||||
|
||||
/**
|
||||
* The handler which dispatches the (redux) action disconnect.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisconnect: Function,
|
||||
|
||||
/**
|
||||
* Handles a hardware button press for back navigation. Leaves the
|
||||
* associated {@code Conference}.
|
||||
*
|
||||
* @private
|
||||
* @returns {boolean} As the associated conference is unconditionally left
|
||||
* and exiting the app while it renders a {@code Conference} is undesired,
|
||||
* {@code true} is always returned.
|
||||
*/
|
||||
_onHardwareBackPress: Function,
|
||||
|
||||
/**
|
||||
* The number of participants in the conference.
|
||||
*
|
||||
@@ -107,6 +71,13 @@ type Props = AbstractProps & {
|
||||
*/
|
||||
_participantCount: number,
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_pictureInPictureEnabled: boolean,
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the UI is reduced (to accommodate
|
||||
* smaller display areas).
|
||||
@@ -138,7 +109,12 @@ type Props = AbstractProps & {
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_toolboxAlwaysVisible: boolean
|
||||
_toolboxAlwaysVisible: boolean,
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Function
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -156,6 +132,8 @@ class Conference extends AbstractConference<Props, *> {
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onClick = this._onClick.bind(this);
|
||||
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
|
||||
this._setToolboxVisible = this._setToolboxVisible.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,17 +144,11 @@ class Conference extends AbstractConference<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidMount() {
|
||||
this.props._onConnect();
|
||||
|
||||
BackHandler.addEventListener(
|
||||
'hardwareBackPress',
|
||||
this.props._onHardwareBackPress);
|
||||
BackHandler.addEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
|
||||
// Show the toolbox if we are the only participant; otherwise, the whole
|
||||
// UI looks too unpopulated the LargeVideo visible.
|
||||
const { _participantCount, _setToolboxVisible } = this.props;
|
||||
|
||||
_participantCount === 1 && _setToolboxVisible(true);
|
||||
this.props._participantCount === 1 && this._setToolboxVisible(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,34 +156,23 @@ class Conference extends AbstractConference<Props, *> {
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate(pevProps: Props) {
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const {
|
||||
_locationURL: oldLocationURL,
|
||||
_participantCount: oldParticipantCount,
|
||||
_room: oldRoom
|
||||
} = pevProps;
|
||||
_participantCount: oldParticipantCount
|
||||
} = prevProps;
|
||||
const {
|
||||
_locationURL: newLocationURL,
|
||||
_participantCount: newParticipantCount,
|
||||
_room: newRoom,
|
||||
_setToolboxVisible,
|
||||
_toolboxVisible
|
||||
} = this.props;
|
||||
|
||||
// If the location URL changes we need to reconnect.
|
||||
oldLocationURL !== newLocationURL && newRoom && this.props._onDisconnect();
|
||||
|
||||
// Start the connection process when there is a (valid) room.
|
||||
oldRoom !== newRoom && newRoom && this.props._onConnect();
|
||||
|
||||
if (oldParticipantCount === 1
|
||||
&& newParticipantCount > 1
|
||||
&& _toolboxVisible) {
|
||||
_setToolboxVisible(false);
|
||||
this._setToolboxVisible(false);
|
||||
} else if (oldParticipantCount > 1
|
||||
&& newParticipantCount === 1
|
||||
&& !_toolboxVisible) {
|
||||
_setToolboxVisible(true);
|
||||
this._setToolboxVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,11 +186,7 @@ class Conference extends AbstractConference<Props, *> {
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
// Tear handling any hardware button presses for back navigation down.
|
||||
BackHandler.removeEventListener(
|
||||
'hardwareBackPress',
|
||||
this.props._onHardwareBackPress);
|
||||
|
||||
this.props._onDisconnect();
|
||||
BackHandler.removeEventListener('hardwareBackPress', this._onHardwareBackPress);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -338,9 +295,33 @@ class Conference extends AbstractConference<Props, *> {
|
||||
return;
|
||||
}
|
||||
|
||||
const toolboxVisible = !this.props._toolboxVisible;
|
||||
this._setToolboxVisible(!this.props._toolboxVisible);
|
||||
}
|
||||
|
||||
this.props._setToolboxVisible(toolboxVisible);
|
||||
_onHardwareBackPress: () => boolean;
|
||||
|
||||
/**
|
||||
* Handles a hardware button press for back navigation. Enters Picture-in-Picture mode
|
||||
* (if supported) or leaves the associated {@code Conference} otherwise.
|
||||
*
|
||||
* @returns {boolean} Exiting the app is undesired, so {@code true} is always returned.
|
||||
*/
|
||||
_onHardwareBackPress() {
|
||||
let p;
|
||||
|
||||
if (this.props._pictureInPictureEnabled) {
|
||||
const { PictureInPicture } = NativeModules;
|
||||
|
||||
p = PictureInPicture.enterPictureInPicture();
|
||||
} else {
|
||||
p = Promise.reject(new Error('PiP not enabled'));
|
||||
}
|
||||
|
||||
p.catch(() => {
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -388,70 +369,20 @@ class Conference extends AbstractConference<Props, *> {
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps dispatching of some action to React component props.
|
||||
*
|
||||
* @param {Function} dispatch - Redux action dispatcher.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _onConnect: Function,
|
||||
* _onDisconnect: Function,
|
||||
* _onHardwareBackPress: Function,
|
||||
* _setToolboxVisible: Function
|
||||
* }}
|
||||
*/
|
||||
function _mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
/**
|
||||
* Dispatches actions to create the desired local tracks and for
|
||||
* connecting to the conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onConnect() {
|
||||
dispatch(createDesiredLocalTracks());
|
||||
dispatch(connect());
|
||||
},
|
||||
_setToolboxVisible: (boolean) => void;
|
||||
|
||||
/**
|
||||
* Dispatches an action disconnecting from the conference.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onDisconnect() {
|
||||
dispatch(disconnect());
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles a hardware button press for back navigation. Leaves the
|
||||
* associated {@code Conference}.
|
||||
*
|
||||
* @returns {boolean} As the associated conference is unconditionally
|
||||
* left and exiting the app while it renders a {@code Conference} is
|
||||
* undesired, {@code true} is always returned.
|
||||
*/
|
||||
_onHardwareBackPress() {
|
||||
dispatch(appNavigate(undefined));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatches an action changing the visibility of the {@link Toolbox}.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} visible - Pass {@code true} to show the
|
||||
* {@code Toolbox} or {@code false} to hide it.
|
||||
* @returns {void}
|
||||
*/
|
||||
_setToolboxVisible(visible) {
|
||||
dispatch(setToolboxVisible(visible));
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Dispatches an action changing the visibility of the {@link Toolbox}.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} visible - Pass {@code true} to show the
|
||||
* {@code Toolbox} or {@code false} to hide it.
|
||||
* @returns {void}
|
||||
*/
|
||||
_setToolboxVisible(visible) {
|
||||
this.props.dispatch(setToolboxVisible(visible));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -462,8 +393,7 @@ function _mapDispatchToProps(dispatch) {
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
const { connecting, connection, locationURL }
|
||||
= state['features/base/connection'];
|
||||
const { connecting, connection } = state['features/base/connection'];
|
||||
const {
|
||||
conference,
|
||||
joining,
|
||||
@@ -508,14 +438,6 @@ function _mapStateToProps(state) {
|
||||
*/
|
||||
_largeVideoParticipantId: state['features/large-video'].participantId,
|
||||
|
||||
/**
|
||||
* Current conference's full URL.
|
||||
*
|
||||
* @private
|
||||
* @type {URL}
|
||||
*/
|
||||
_locationURL: locationURL,
|
||||
|
||||
/**
|
||||
* The number of participants in the conference.
|
||||
*
|
||||
@@ -524,6 +446,14 @@ function _mapStateToProps(state) {
|
||||
*/
|
||||
_participantCount: getParticipantCount(state),
|
||||
|
||||
/**
|
||||
* Whether Picture-in-Picture is enabled.
|
||||
*
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
_pictureInPictureEnabled: getAppProp(state, 'pictureInPictureEnabled'),
|
||||
|
||||
/**
|
||||
* The indicator which determines whether the UI is reduced (to
|
||||
* accommodate smaller display areas).
|
||||
@@ -551,5 +481,4 @@ function _mapStateToProps(state) {
|
||||
};
|
||||
}
|
||||
|
||||
export default reactReduxConnect(_mapStateToProps, _mapDispatchToProps)(
|
||||
makeAspectRatioAware(Conference));
|
||||
export default connect(_mapStateToProps)(makeAspectRatioAware(Conference));
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { SafeAreaView, Text, View } from 'react-native';
|
||||
import LinearGradient from 'react-native-linear-gradient';
|
||||
@@ -82,7 +81,7 @@ class NavigationBar extends Component<Props> {
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_meetingName: _.startCase(getConferenceName(state)),
|
||||
_meetingName: getConferenceName(state),
|
||||
_visible: isToolboxVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._openDesktopApp = this._openDesktopApp.bind(this);
|
||||
this._onLaunchWeb = this._onLaunchWeb.bind(this);
|
||||
this._onTryAgain = this._onTryAgain.bind(this);
|
||||
}
|
||||
@@ -61,7 +60,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
this._openDesktopApp();
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'displayed', 'DeepLinkingDesktop', { isMobileBrowser: false }));
|
||||
@@ -133,17 +131,6 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
);
|
||||
}
|
||||
|
||||
_openDesktopApp: () => {}
|
||||
|
||||
/**
|
||||
* Dispatches the <tt>openDesktopApp</tt> action.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_openDesktopApp() {
|
||||
this.props.dispatch(openDesktopApp());
|
||||
}
|
||||
|
||||
_onTryAgain: () => {}
|
||||
|
||||
/**
|
||||
@@ -155,7 +142,7 @@ class DeepLinkingDesktopPage<P : Props> extends Component<P> {
|
||||
sendAnalytics(
|
||||
createDeepLinkingPageEvent(
|
||||
'clicked', 'tryAgainButton', { isMobileBrowser: false }));
|
||||
this._openDesktopApp();
|
||||
this.props.dispatch(openDesktopApp());
|
||||
}
|
||||
|
||||
_onLaunchWeb: () => {}
|
||||
|
||||
@@ -8,28 +8,7 @@ import {
|
||||
DeepLinkingMobilePage,
|
||||
NoMobileApp
|
||||
} from './components';
|
||||
import { _shouldShowDeepLinkingDesktopPage }
|
||||
from './shouldShowDeepLinkingDesktopPage';
|
||||
|
||||
/**
|
||||
* Promise that resolves when the window load event is received.
|
||||
*
|
||||
* @type {Promise<void>}
|
||||
*/
|
||||
const windowLoadedPromise = new Promise(resolve => {
|
||||
/**
|
||||
* Handler for the window load event.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function onWindowLoad() {
|
||||
resolve();
|
||||
window.removeEventListener('load', onWindowLoad);
|
||||
}
|
||||
|
||||
window.addEventListener('load', onWindowLoad);
|
||||
});
|
||||
|
||||
import { _openDesktopApp } from './openDesktopApp';
|
||||
|
||||
/**
|
||||
* Generates a deep linking URL based on the current window URL.
|
||||
@@ -96,23 +75,17 @@ export function getDeepLinkingPage(state) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return _shouldShowDeepLinkingDesktopPage().then(
|
||||
return _openDesktopApp().then(
|
||||
// eslint-disable-next-line no-confusing-arrow
|
||||
show => show ? DeepLinkingDesktopPage : undefined);
|
||||
result => result ? DeepLinkingDesktopPage : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the desktop app.
|
||||
*
|
||||
* @returns {void}
|
||||
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
|
||||
* with false otherwise.
|
||||
*/
|
||||
export function openDesktopApp() {
|
||||
windowLoadedPromise.then(() => {
|
||||
// If the code for opening the deep link is executed before the window
|
||||
// load event, something with the internal chrome state goes wrong. The
|
||||
// result is that no window load event is received which is the cause
|
||||
// for some permission prompts to not be displayed. In our case the GUM
|
||||
// prompt wasn't displayed which causes the GUM call to never finish.
|
||||
window.location.href = generateDeepLinkingURL();
|
||||
});
|
||||
return _openDesktopApp();
|
||||
}
|
||||
|
||||
9
react/features/deep-linking/openDesktopApp.js
Normal file
9
react/features/deep-linking/openDesktopApp.js
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Opens the desktop app.
|
||||
*
|
||||
* @returns {Promise<boolean>} - Resolves with true if the attempt to open the desktop app was successful and resolves
|
||||
* with false otherwise.
|
||||
*/
|
||||
export function _openDesktopApp() {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
/**
|
||||
* Resolves with <tt>true</tt> if the deep linking page should be shown and with
|
||||
* <tt>false</tt> otherwise.
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export function _shouldShowDeepLinkingDesktopPage() {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
1
react/features/external-api/index.js
Normal file
1
react/features/external-api/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import './middleware';
|
||||
30
react/features/external-api/middleware.js
Normal file
30
react/features/external-api/middleware.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @flow
|
||||
|
||||
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
* The middleware of the feature {@code external-api}.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register((/* store */) => next => action => {
|
||||
switch (action.type) {
|
||||
case NOTIFY_CAMERA_ERROR:
|
||||
if (action.error) {
|
||||
APP.API.notifyOnCameraError(
|
||||
action.error.name, action.error.message);
|
||||
}
|
||||
break;
|
||||
|
||||
case NOTIFY_MIC_ERROR:
|
||||
if (action.error) {
|
||||
APP.API.notifyOnMicError(action.error.name, action.error.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { Audio, MEDIA_TYPE } from '../../../base/media';
|
||||
import {
|
||||
PARTICIPANT_ROLE,
|
||||
ParticipantView,
|
||||
isEveryoneModerator,
|
||||
isLocalParticipantModerator,
|
||||
pinParticipant
|
||||
} from '../../../base/participants';
|
||||
@@ -38,6 +39,11 @@ type Props = {
|
||||
*/
|
||||
_audioTrack: Object,
|
||||
|
||||
/**
|
||||
* True if everone in the meeting is moderator.
|
||||
*/
|
||||
_isEveryoneModerator: boolean,
|
||||
|
||||
/**
|
||||
* True if the local participant is a moderator.
|
||||
*/
|
||||
@@ -117,6 +123,7 @@ class Thumbnail extends Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
_audioTrack: audioTrack,
|
||||
_isEveryoneModerator,
|
||||
_isModerator,
|
||||
_largeVideo: largeVideo,
|
||||
_onClick,
|
||||
@@ -172,7 +179,7 @@ class Thumbnail extends Component<Props> {
|
||||
|
||||
{ renderDisplayName && <DisplayNameLabel participantId = { participantId } /> }
|
||||
|
||||
{ participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
{ !_isEveryoneModerator && participant.role === PARTICIPANT_ROLE.MODERATOR
|
||||
&& <View style = { styles.moderatorIndicatorContainer }>
|
||||
<ModeratorIndicator />
|
||||
</View> }
|
||||
@@ -275,6 +282,7 @@ function _mapStateToProps(state, ownProps) {
|
||||
|
||||
return {
|
||||
_audioTrack: audioTrack,
|
||||
_isEveryoneModerator: isEveryoneModerator(state),
|
||||
_isModerator: isLocalParticipantModerator(state),
|
||||
_largeVideo: largeVideo,
|
||||
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
|
||||
|
||||
@@ -55,6 +55,7 @@ export default {
|
||||
backgroundColor: 'rgb(240, 243, 247)',
|
||||
borderBottomRightRadius: 10,
|
||||
borderTopRightRadius: 10,
|
||||
color: DARK_GREY,
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
paddingVertical: 7
|
||||
|
||||
@@ -40,6 +40,11 @@ type Props = {
|
||||
*/
|
||||
_conferenceName: string,
|
||||
|
||||
/**
|
||||
* The number of digits to be used in the password.
|
||||
*/
|
||||
_passwordNumberOfDigits: ?number,
|
||||
|
||||
/**
|
||||
* The current url of the conference to be copied onto the clipboard.
|
||||
*/
|
||||
@@ -245,7 +250,8 @@ class InfoDialog extends Component<Props, State> {
|
||||
editEnabled = { this.state.passwordEditEnabled }
|
||||
locked = { this.props._locked }
|
||||
onSubmit = { this._onPasswordSubmit }
|
||||
password = { this.props._password } />
|
||||
password = { this.props._password }
|
||||
passwordNumberOfDigits = { this.props._passwordNumberOfDigits } />
|
||||
</div>
|
||||
<div className = 'info-dialog-action-links'>
|
||||
<div className = 'info-dialog-action-link'>
|
||||
@@ -591,6 +597,7 @@ function _mapStateToProps(state) {
|
||||
_canEditPassword: isLocalParticipantModerator(state, state['features/base/config'].lockRoomGuestEnabled),
|
||||
_conference: conference,
|
||||
_conferenceName: room,
|
||||
_passwordNumberOfDigits: state['features/base/config'].roomPasswordNumberOfDigits,
|
||||
_inviteURL: getInviteURL(state),
|
||||
_localParticipant: getLocalParticipant(state),
|
||||
_locationURL: state['features/base/connection'].locationURL,
|
||||
|
||||
@@ -32,6 +32,11 @@ type Props = {
|
||||
*/
|
||||
password: string,
|
||||
|
||||
/**
|
||||
* The number of digits to be used in the password.
|
||||
*/
|
||||
passwordNumberOfDigits: boolean,
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
@@ -117,6 +122,14 @@ class PasswordForm extends Component<Props, State> {
|
||||
*/
|
||||
_renderPasswordField() {
|
||||
if (this.props.editEnabled) {
|
||||
let digitPattern, placeHolderText;
|
||||
|
||||
if (this.props.passwordNumberOfDigits) {
|
||||
placeHolderText = this.props.t('passwordDigitsOnly', {
|
||||
number: this.props.passwordNumberOfDigits });
|
||||
digitPattern = '\\d*';
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className = 'info-password-form'
|
||||
@@ -125,7 +138,10 @@ class PasswordForm extends Component<Props, State> {
|
||||
<input
|
||||
autoFocus = { true }
|
||||
className = 'info-password-input'
|
||||
maxLength = { this.props.passwordNumberOfDigits }
|
||||
onChange = { this._onEnteredPasswordChange }
|
||||
pattern = { digitPattern }
|
||||
placeholder = { placeHolderText }
|
||||
spellCheck = { 'false' }
|
||||
type = 'text'
|
||||
value = { this.state.enteredPassword } />
|
||||
|
||||
@@ -4,11 +4,12 @@ import _ from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
import { NativeModules, Text, TouchableHighlight, View } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry } from '../../../base/color-scheme';
|
||||
import { hideDialog, BottomSheet } from '../../../base/dialog';
|
||||
import { translate } from '../../../base/i18n';
|
||||
import { Icon } from '../../../base/font-icons';
|
||||
import { connect } from '../../../base/redux';
|
||||
import { ColorPalette } from '../../../base/styles';
|
||||
import { ColorPalette, type StyleType } from '../../../base/styles';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
@@ -44,6 +45,11 @@ type Device = {
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Style of the bottom sheet feature.
|
||||
*/
|
||||
_bottomSheetStyles: StyleType,
|
||||
|
||||
/**
|
||||
* Used for hiding the dialog when the selection was completed.
|
||||
*/
|
||||
@@ -203,6 +209,7 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderDevice(device: Device) {
|
||||
const { _bottomSheetStyles } = this.props;
|
||||
const { iconName, selected, text } = device;
|
||||
const selectedStyle = selected ? styles.selectedText : {};
|
||||
|
||||
@@ -214,8 +221,8 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
<View style = { styles.deviceRow } >
|
||||
<Icon
|
||||
name = { iconName }
|
||||
style = { [ styles.deviceIcon, selectedStyle ] } />
|
||||
<Text style = { [ styles.deviceText, selectedStyle ] } >
|
||||
style = { [ styles.deviceIcon, _bottomSheetStyles.iconStyle, selectedStyle ] } />
|
||||
<Text style = { [ styles.deviceText, _bottomSheetStyles.labelStyle, selectedStyle ] } >
|
||||
{ text }
|
||||
</Text>
|
||||
</View>
|
||||
@@ -244,10 +251,22 @@ class AudioRoutePickerDialog extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
return {
|
||||
_bottomSheetStyles: ColorSchemeRegistry.get(state, 'BottomSheet')
|
||||
};
|
||||
}
|
||||
|
||||
// Only export the dialog if we have support for getting / setting audio devices
|
||||
// in AudioMode.
|
||||
if (AudioMode.getAudioDevices && AudioMode.setAudioDevice) {
|
||||
AudioRoutePickerDialog_ = translate(connect()(AudioRoutePickerDialog));
|
||||
AudioRoutePickerDialog_ = translate(connect(_mapStateToProps)(AudioRoutePickerDialog));
|
||||
}
|
||||
|
||||
export default AudioRoutePickerDialog_;
|
||||
|
||||
@@ -25,7 +25,11 @@ export function beginRoomLockRequest(conference: ?Object) {
|
||||
conference = getState()['features/base/conference'].conference;
|
||||
}
|
||||
if (conference) {
|
||||
dispatch(openDialog(RoomLockPrompt, { conference }));
|
||||
const passwordNumberOfDigits = getState()['features/base/config'].roomPasswordNumberOfDigits;
|
||||
|
||||
dispatch(openDialog(RoomLockPrompt, {
|
||||
conference,
|
||||
passwordNumberOfDigits }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -28,10 +28,15 @@ type Props = {
|
||||
*/
|
||||
conference: Object,
|
||||
|
||||
/**
|
||||
* The number of digits to be used in the password.
|
||||
*/
|
||||
passwordNumberOfDigits: ?number,
|
||||
|
||||
/**
|
||||
* Redux store dispatch function.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
dispatch: Dispatch<any>
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -51,6 +56,7 @@ class RoomLockPrompt extends Component<Props> {
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onSubmit = this._onSubmit.bind(this);
|
||||
this._validateInput = this._validateInput.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,12 +66,23 @@ class RoomLockPrompt extends Component<Props> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
let textInputProps = _TEXT_INPUT_PROPS;
|
||||
|
||||
if (this.props.passwordNumberOfDigits) {
|
||||
textInputProps = {
|
||||
...textInputProps,
|
||||
keyboardType: 'number-pad',
|
||||
maxLength: this.props.passwordNumberOfDigits
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<InputDialog
|
||||
contentKey = 'dialog.passwordLabel'
|
||||
onCancel = { this._onCancel }
|
||||
onSubmit = { this._onSubmit }
|
||||
textInputProps = { _TEXT_INPUT_PROPS } />
|
||||
textInputProps = { textInputProps }
|
||||
validateInput = { this._validateInput } />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -100,6 +117,28 @@ class RoomLockPrompt extends Component<Props> {
|
||||
|
||||
return false; // Do not hide.
|
||||
}
|
||||
|
||||
_validateInput: (string) => boolean;
|
||||
|
||||
/**
|
||||
* Verifies input in case only digits are required.
|
||||
*
|
||||
* @param {string|undefined} value - The submitted value.
|
||||
* @private
|
||||
* @returns {boolean} False when the value is not valid and True otherwise.
|
||||
*/
|
||||
_validateInput(value: string) {
|
||||
|
||||
// we want only digits, but both number-pad and numeric add ',' and '.' as symbols
|
||||
if (this.props.passwordNumberOfDigits
|
||||
&& value.length > 0
|
||||
&& !/^\d+$/.test(value)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(RoomLockPrompt);
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
import { toState } from '../base/redux';
|
||||
import { parseStandardURIString } from '../base/util';
|
||||
import { i18next, DEFAULT_LANGUAGE, LANGUAGES } from '../base/i18n';
|
||||
import { getLocalParticipant, PARTICIPANT_ROLE } from '../base/participants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
isLocalParticipantModerator
|
||||
} from '../base/participants';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
@@ -83,14 +86,12 @@ export function getMoreTabProps(stateful: Object | Function) {
|
||||
} = state['features/base/conference'];
|
||||
const followMeActive = Boolean(state['features/follow-me'].moderator);
|
||||
const configuredTabs = interfaceConfig.SETTINGS_SECTIONS || [];
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
|
||||
|
||||
// The settings sections to display.
|
||||
const showModeratorSettings = Boolean(
|
||||
conference
|
||||
&& configuredTabs.includes('moderator')
|
||||
&& localParticipant.role === PARTICIPANT_ROLE.MODERATOR);
|
||||
&& isLocalParticipantModerator(state));
|
||||
|
||||
return {
|
||||
currentLanguage: language,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @flow
|
||||
|
||||
import _ from 'lodash';
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../analytics';
|
||||
import { appNavigate } from '../../app';
|
||||
@@ -26,10 +27,33 @@ type Props = AbstractButtonProps & {
|
||||
* @extends AbstractHangupButton
|
||||
*/
|
||||
class HangupButton extends AbstractHangupButton<Props, *> {
|
||||
_hangup: Function;
|
||||
|
||||
accessibilityLabel = 'toolbar.accessibilityLabel.hangup';
|
||||
label = 'toolbar.hangup';
|
||||
tooltip = 'toolbar.hangup';
|
||||
|
||||
/**
|
||||
* Initializes a new HangupButton instance.
|
||||
*
|
||||
* @param {Props} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this._hangup = _.once(() => {
|
||||
sendAnalytics(createToolbarEvent('hangup'));
|
||||
|
||||
// FIXME: these should be unified.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
} else {
|
||||
this.props.dispatch(disconnect(true));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to perform the actual hangup action.
|
||||
*
|
||||
@@ -38,14 +62,7 @@ class HangupButton extends AbstractHangupButton<Props, *> {
|
||||
* @returns {void}
|
||||
*/
|
||||
_doHangup() {
|
||||
sendAnalytics(createToolbarEvent('hangup'));
|
||||
|
||||
// FIXME: these should be unified.
|
||||
if (navigator.product === 'ReactNative') {
|
||||
this.props.dispatch(appNavigate(undefined));
|
||||
} else {
|
||||
this.props.dispatch(disconnect(true));
|
||||
}
|
||||
this._hangup();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import {
|
||||
SCREEN_SHARE_PARTICIPANTS_UPDATED,
|
||||
SET_TILE_VIEW
|
||||
@@ -39,3 +41,17 @@ export function setTileView(enabled: boolean) {
|
||||
enabled
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a (redux) action which signals either to exit tile view if currently
|
||||
* enabled or enter tile view if currently disabled.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function toggleTileView() {
|
||||
return (dispatch: Dispatch<any>, getState: Function) => {
|
||||
const { tileViewEnabled } = getState()['features/video-layout'];
|
||||
|
||||
dispatch(setTileView(!tileViewEnabled));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
getParticipantById
|
||||
} from '../base/participants';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
import { TRACK_ADDED } from '../base/tracks';
|
||||
import { TRACK_ADDED, TRACK_REMOVED } from '../base/tracks';
|
||||
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
|
||||
|
||||
import './middleware.any';
|
||||
@@ -83,6 +83,12 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
|
||||
}
|
||||
|
||||
break;
|
||||
case TRACK_REMOVED:
|
||||
if (!action.track.local) {
|
||||
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { selectParticipant } from '../large-video';
|
||||
import { shouldDisplayTileView } from './functions';
|
||||
import { setParticipantsWithScreenShare } from './actions';
|
||||
|
||||
declare var APP: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
@@ -35,6 +36,10 @@ StateListenerRegistry.register(
|
||||
_updateAutoPinnedParticipant(store);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof APP === 'object') {
|
||||
APP.API.notifyTileViewChanged(displayTileView);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user