Compare commits

..

1 Commits

Author SHA1 Message Date
Aaron van Meerten
4ed9d5893b util updates 2019-09-09 08:42:00 -05:00
257 changed files with 2439 additions and 5621 deletions

1
.gitignore vendored
View File

@@ -73,7 +73,6 @@ buck-out/
# Build artifacts
*.jsbundle
*.framework
android/app/debug
android/app/release
# precommit-hook

View File

@@ -45,8 +45,6 @@ deploy-appbundle:
$(OUTPUT_DIR)/analytics-ga.js \
$(BUILD_DIR)/analytics-ga.min.js \
$(BUILD_DIR)/analytics-ga.min.map \
$(BUILD_DIR)/video-blur-effect.min.js \
$(BUILD_DIR)/video-blur-effect.min.map \
$(DEPLOY_DIR)
deploy-lib-jitsi-meet:

View File

@@ -207,6 +207,24 @@ public class MainActivity extends FragmentActivity implements JitsiMeetActivityI
</details>
Starting with SDK version 1.22, a Glide module must be provided by the host app.
This makes it possible to use the Glide image processing library from both the
SDK and the host app itself.
You can use the code in `JitsiGlideModule.java` and adjust the package name.
When building, add the following code in your `app/build.gradle` file, adjusting
the Glide version to match the one in https://github.com/jitsi/jitsi-meet/blob/master/android/build.gradle
```
// Glide
implementation("com.github.bumptech.glide:glide:${glideVersion}") {
exclude group: "com.android.support", module: "glide"
}
implementation("com.github.bumptech.glide:annotations:${glideVersion}") {
exclude group: "com.android.support", module: "annotations"
}
```
### JitsiMeetActivity
This class encapsulates a high level API in the form of an Android `FragmentActivity`

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>app</name>
<comment>Project app created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@@ -1,2 +0,0 @@
connection.project.dir=..
eclipse.preferences.version=1

View File

@@ -88,6 +88,15 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'
// Glide
implementation("com.github.bumptech.glide:glide:${rootProject.ext.glideVersion}") {
exclude group: "com.android.support", module: "glide"
}
implementation("com.github.bumptech.glide:annotations:${rootProject.ext.glideVersion}") {
exclude group: "com.android.support", module: "annotations"
}
annotationProcessor "com.github.bumptech.glide:compiler:${rootProject.ext.glideVersion}"
}
gradle.projectsEvaluated {

View File

@@ -62,6 +62,17 @@
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# FastImage + Glide
-keep public class com.dylanvann.fastimage.* {*;}
-keep public class com.dylanvann.fastimage.** {*;}
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# WebRTC
-keep class org.webrtc.** { *; }

View File

@@ -0,0 +1,16 @@
package org.jitsi.meet;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
/**
* An AppGlideModule needs to be present for image loading events to work in
* react-native-fast-image. However, if this is defined by the SDK it will cause trouble with
* apps which are using Glide themselves.
*
* In order to avoid the problem, define a Jitsi Glide module here, so applications already using
* it are not in trouble.
*/
@GlideModule
public final class JitsiGlideModule extends AppGlideModule {
}

View File

@@ -28,7 +28,6 @@ import android.view.KeyEvent;
import org.jitsi.meet.sdk.JitsiMeet;
import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions;
import org.jitsi.meet.sdk.JitsiMeetUserInfo;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
@@ -107,7 +106,7 @@ public class MainActivity extends JitsiMeetActivity {
//
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == OVERLAY_PERMISSION_REQUEST_CODE
&& canRequestOverlayPermission()) {
if (Settings.canDrawOverlays(this)) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -164,8 +164,12 @@ ext {
mavenUser = System.env.MVN_USER ?: ""
mavenPassword = System.env.MVN_PASSWORD ?: ""
// Glide
excludeAppGlideModule = true
glideVersion = "4.7.1" // keep in sync with react-native-fast-image
// Libre build
libreBuild = (System.env.LIBRE_BUILD ?: "false").toBoolean()
libreBuild = (System.env.LIBRE_BUILD ?: "true").toBoolean()
}
// If Android SDK is not installed, accept its license so that it

View File

@@ -17,5 +17,5 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
appVersion=19.3.0
sdkVersion=2.2.1
appVersion=19.2.0
sdkVersion=2.1.0

View File

@@ -14,7 +14,6 @@ if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
exit 2
fi
else
adb reverse tcp:8081 tcp:8081
CMD="${THIS_DIR}/../../node_modules/react-native/scripts/launchPackager.command"
if [[ `uname` == "Darwin" ]]; then
open -g "${CMD}" || echo "Can't start packager automatically"

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>sdk</name>
<comment>Project sdk created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@@ -1,2 +0,0 @@
connection.project.dir=..
eclipse.preferences.version=1

View File

@@ -54,6 +54,9 @@ 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'
}
implementation project(':react-native-immersive')
implementation project(':react-native-keep-awake')
implementation project(':react-native-linear-gradient')

View File

@@ -12,7 +12,6 @@
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-feature
android:glEsVersion="0x00020000"
@@ -45,8 +44,6 @@
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<service android:name="org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService" />
</application>
</manifest>

View File

@@ -29,7 +29,6 @@ import com.facebook.react.bridge.ReadableMap;
import com.rnimmersive.RNImmersiveModule;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
@@ -78,15 +77,6 @@ public abstract class BaseReactView<ListenerT>
return null;
}
/**
* Gets all registered React views.
*
* @return An {@link ArrayList} containing all views currently held by React.
*/
static ArrayList<BaseReactView> getViews() {
return new ArrayList<>(views);
}
/**
* The unique identifier of this {@code BaseReactView} within the process
* for the purposes of {@link ExternalAPIModule}. The name scope was

View File

@@ -63,16 +63,6 @@ public class ConnectionService extends android.telecom.ConnectionService {
static private final HashMap<String, Promise> startCallPromises
= new HashMap<>();
/**
* Aborts all ongoing connections. This is a last resort mechanism which forces all resources to
* be freed on the system in case of fatal error.
*/
static void abortConnections() {
for (ConnectionImpl connection: getConnections()) {
connection.onAbort();
}
}
/**
* Adds {@link ConnectionImpl} to the list.
*

View File

@@ -67,16 +67,12 @@ class ExternalAPIModule
*/
@ReactMethod
public void sendEvent(String name, ReadableMap data, String scope) {
// Keep track of the current ongoing conference.
OngoingConferenceTracker.getInstance().onExternalAPIEvent(name, data);
// The JavaScript App needs to provide uniquely identifying information
// to the native ExternalAPI module so that the latter may match the
// former to the native BaseReactView which hosts it.
BaseReactView view = BaseReactView.findViewByExternalAPIScope(scope);
if (view != null) {
Log.d(TAG, "Sending event: " + name + " with data: " + data);
try {
view.onExternalAPIEvent(name, data);
} catch(Exception e) {

View File

@@ -36,15 +36,6 @@ public class JitsiMeet {
defaultConferenceOptions = options;
}
/**
* Returns the current conference URL as a string.
*
* @return the current conference URL.
*/
public static String getCurrentConference() {
return OngoingConferenceTracker.getInstance().getCurrentConference();
}
/**
* Helper to get the default conference options as a {@link Bundle}.
*

View File

@@ -38,8 +38,8 @@ public class JitsiMeetActivity extends FragmentActivity
protected static final String TAG = JitsiMeetActivity.class.getSimpleName();
private static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
private static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
public static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
public static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
// Helpers for starting the activity
//
@@ -71,24 +71,6 @@ public class JitsiMeetActivity extends FragmentActivity
}
}
@Override
public void onDestroy() {
// Here we are trying to handle the following corner case: an application using the SDK
// is using this Activity for displaying meetings, but there is another "main" Activity
// with other content. If this Activity is "swiped out" from the recent list we will get
// Activity#onDestroy() called without warning. At this point we can try to leave the
// current meeting, but when our view is detached from React the JS <-> Native bridge won't
// be operational so the external API won't be able to notify the native side that the
// conference terminated. Thus, try our best to clean up.
leave();
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
JitsiMeetOngoingConferenceService.abort(this);
super.onDestroy();
}
@Override
public void finish() {
leave();
@@ -161,11 +143,6 @@ public class JitsiMeetActivity extends FragmentActivity
// Activity lifecycle methods
//
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
JitsiMeetActivityDelegate.onActivityResult(this, requestCode, resultCode, data);
}
@Override
public void onBackPressed() {
JitsiMeetActivityDelegate.onBackPressed();
@@ -173,8 +150,6 @@ public class JitsiMeetActivity extends FragmentActivity
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
JitsiMeetConferenceOptions options;
if ((options = getConferenceOptions(intent)) != null) {
@@ -209,8 +184,6 @@ public class JitsiMeetActivity extends FragmentActivity
@Override
public void onConferenceJoined(Map<String, Object> data) {
Log.d(TAG, "Conference joined: " + data);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
}
@Override

View File

@@ -54,11 +54,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
*/
private Bundle colorScheme;
/**
* Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
*/
private Bundle featureFlags;
/**
* Set to {@code true} to join the conference with audio / video muted or to start in audio
* only mode respectively.
@@ -68,9 +63,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
private Boolean videoMuted;
/**
* USer information, to be used when no token is specified.
* Set to {@code true} to enable the welcome page. Typically SDK users won't need this enabled
* since the host application decides which meeting to join.
*/
private JitsiMeetUserInfo userInfo;
private Boolean welcomePageEnabled;
/**
* Class used to build the immutable {@link JitsiMeetConferenceOptions} object.
@@ -82,16 +78,14 @@ public class JitsiMeetConferenceOptions implements Parcelable {
private String token;
private Bundle colorScheme;
private Bundle featureFlags;
private Boolean audioMuted;
private Boolean audioOnly;
private Boolean videoMuted;
private JitsiMeetUserInfo userInfo;
private Boolean welcomePageEnabled;
public Builder() {
featureFlags = new Bundle();
}
/**\
@@ -192,31 +186,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
* @return - The {@link Builder} object itself so the method calls can be chained.
*/
public Builder setWelcomePageEnabled(boolean enabled) {
this.featureFlags.putBoolean("welcomepage.enabled", enabled);
return this;
}
public Builder setFeatureFlag(String flag, boolean value) {
this.featureFlags.putBoolean(flag, value);
return this;
}
public Builder setFeatureFlag(String flag, String value) {
this.featureFlags.putString(flag, value);
return this;
}
public Builder setFeatureFlag(String flag, int value) {
this.featureFlags.putInt(flag, value);
return this;
}
public Builder setUserInfo(JitsiMeetUserInfo userInfo) {
this.userInfo = userInfo;
this.welcomePageEnabled = enabled;
return this;
}
@@ -234,11 +204,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
options.subject = this.subject;
options.token = this.token;
options.colorScheme = this.colorScheme;
options.featureFlags = this.featureFlags;
options.audioMuted = this.audioMuted;
options.audioOnly = this.audioOnly;
options.videoMuted = this.videoMuted;
options.userInfo = this.userInfo;
options.welcomePageEnabled = this.welcomePageEnabled;
return options;
}
@@ -252,30 +221,30 @@ public class JitsiMeetConferenceOptions implements Parcelable {
subject = in.readString();
token = in.readString();
colorScheme = in.readBundle();
featureFlags = in.readBundle();
userInfo = new JitsiMeetUserInfo(in.readBundle());
byte tmpAudioMuted = in.readByte();
audioMuted = tmpAudioMuted == 0 ? null : tmpAudioMuted == 1;
byte tmpAudioOnly = in.readByte();
audioOnly = tmpAudioOnly == 0 ? null : tmpAudioOnly == 1;
byte tmpVideoMuted = in.readByte();
videoMuted = tmpVideoMuted == 0 ? null : tmpVideoMuted == 1;
byte tmpWelcomePageEnabled = in.readByte();
welcomePageEnabled = tmpWelcomePageEnabled == 0 ? null : tmpWelcomePageEnabled == 1;
}
Bundle asProps() {
Bundle props = new Bundle();
// Android always has the PiP flag set by default.
if (!featureFlags.containsKey("pip.enabled")) {
featureFlags.putBoolean("pip.enabled", true);
}
props.putBundle("flags", featureFlags);
if (colorScheme != null) {
props.putBundle("colorScheme", colorScheme);
}
if (welcomePageEnabled != null) {
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
}
// TODO: get rid of this.
props.putBoolean("pictureInPictureEnabled", true);
Bundle config = new Bundle();
if (audioMuted != null) {
@@ -309,10 +278,6 @@ public class JitsiMeetConferenceOptions implements Parcelable {
urlProps.putString("jwt", token);
}
if (token == null && userInfo != null) {
props.putBundle("userInfo", userInfo.asBundle());
}
urlProps.putBundle("config", config);
props.putBundle("url", urlProps);
@@ -340,11 +305,10 @@ public class JitsiMeetConferenceOptions implements Parcelable {
dest.writeString(subject);
dest.writeString(token);
dest.writeBundle(colorScheme);
dest.writeBundle(featureFlags);
dest.writeBundle(userInfo.asBundle());
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));
dest.writeByte((byte) (welcomePageEnabled == null ? 0 : welcomePageEnabled ? 1 : 2));
}
@Override

View File

@@ -26,6 +26,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.net.URL;
/**
* Base {@link Fragment} for applications integrating Jitsi Meet at a higher level. It
* contains all the required wiring between the {@code JitsiMeetView} and

View File

@@ -1,120 +0,0 @@
/*
* 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.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
* responsible for presenting an ongoing notification when a conference is in progress.
* The service will help keep the app running while in the background.
*
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service
implements OngoingConferenceTracker.OngoingConferenceListener {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
static final class Actions {
static final String START = TAG + ":START";
static final String HANGUP = TAG + ":HANGUP";
}
static void launch(Context context) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(Actions.START);
ComponentName componentName;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
componentName = context.startForegroundService(intent);
} else {
componentName = context.startService(intent);
}
if (componentName == null) {
Log.w(TAG, "Ongoing conference service not started");
}
}
static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}
@Override
public void onCreate() {
super.onCreate();
OngoingConferenceTracker.getInstance().addListener(this);
}
@Override
public void onDestroy() {
OngoingConferenceTracker.getInstance().removeListener(this);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (action.equals(Actions.START)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
if (notification == null) {
stopSelf();
Log.w(TAG, "Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
Log.i(TAG, "Service started");
}
} else if (action.equals(Actions.HANGUP)) {
Log.i(TAG, "Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
stopSelf();
} else {
Log.w(TAG, "Unknown action received: " + action);
stopSelf();
}
return START_NOT_STICKY;
}
@Override
public void onCurrentConferenceChanged(String conferenceUrl) {
if (conferenceUrl == null) {
stopSelf();
Log.i(TAG, "Service stopped");
}
}
}

View File

@@ -1,51 +0,0 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* 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.
*/
package org.jitsi.meet.sdk;
import android.util.Log;
class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public static void register() {
Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
JitsiMeetUncaughtExceptionHandler uncaughtExceptionHandler
= new JitsiMeetUncaughtExceptionHandler(defaultUncaughtExceptionHandler);
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
private JitsiMeetUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
// Abort all ConnectionService ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
if (defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(t, e);
}
}
}

View File

@@ -1,107 +0,0 @@
/*
* 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.
*/
package org.jitsi.meet.sdk;
import android.os.Bundle;
import java.net.MalformedURLException;
import java.net.URL;
/**
* This class represents user information to be passed to {@link JitsiMeetConferenceOptions} for
* identifying a user.
*/
public class JitsiMeetUserInfo {
/**
* User's display name.
*/
private String displayName;
/**
* User's email address.
*/
private String email;
/**
* User's avatar URL.
*/
private URL avatar;
public JitsiMeetUserInfo() {}
public JitsiMeetUserInfo(Bundle b) {
super();
if (b.containsKey("displayName")) {
displayName = b.getString("displayName");
}
if (b.containsKey("email")) {
email = b.getString("email");
}
if (b.containsKey("avatarURL")) {
String avatarURL = b.getString("avatarURL");
try {
avatar = new URL(avatarURL);
} catch (MalformedURLException e) {
}
}
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public URL getAvatar() {
return avatar;
}
public void setAvatar(URL avatar) {
this.avatar = avatar;
}
Bundle asBundle() {
Bundle b = new Bundle();
if (displayName != null) {
b.putString("displayName", displayName);
}
if (email != null) {
b.putString("email", email);
}
if (avatar != null) {
b.putString("avatarURL", avatar.toString());
}
return b;
}
}

View File

@@ -29,8 +29,7 @@ import java.lang.reflect.Method;
import java.util.Map;
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
implements OngoingConferenceTracker.OngoingConferenceListener {
public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener> {
/**
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
@@ -107,14 +106,6 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
if (!(context instanceof JitsiMeetActivityInterface)) {
throw new RuntimeException("Enclosing Activity must implement JitsiMeetActivityInterface");
}
OngoingConferenceTracker.getInstance().addListener(this);
}
@Override
public void dispose() {
OngoingConferenceTracker.getInstance().removeListener(this);
super.dispose();
}
/**
@@ -182,17 +173,27 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
}
/**
* Handler for {@link OngoingConferenceTracker} events.
* @param conferenceUrl
* The internal processing for the URL of the current conference set on the
* associated {@link JitsiMeetView}.
*
* @param eventName the name of the external API event to be processed
* @param eventData the details/specifics of the event to process determined
* by/associated with the specified {@code eventName}.
*/
@Override
public void onCurrentConferenceChanged(String conferenceUrl) {
// This property was introduced in order to address
// an exception in the Picture-in-Picture functionality which arose
// because of delays related to bridging between JavaScript and Java. To
// reduce these delays do not wait for the call to be transferred to the
// UI thread.
this.url = conferenceUrl;
private void maybeSetViewURL(String eventName, ReadableMap eventData) {
String url = eventData.getString("url");
switch(eventName) {
case "CONFERENCE_WILL_JOIN":
this.url = url;
break;
case "CONFERENCE_TERMINATED":
if (url != null && url.equals(this.url)) {
this.url = null;
}
break;
}
}
/**
@@ -204,6 +205,13 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
*/
@Override
protected void onExternalAPIEvent(String name, ReadableMap data) {
// XXX The JitsiMeetView property URL was introduced in order to address
// an exception in the Picture-in-Picture functionality which arose
// because of delays related to bridging between JavaScript and Java. To
// reduce these delays do not wait for the call to be transferred to the
// UI thread.
maybeSetViewURL(name, data);
onExternalAPIEvent(LISTENER_METHODS, name, data);
}
}

View File

@@ -1,99 +0,0 @@
/*
* 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.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.ReadableMap;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
/**
* Helper class to keep track of what the current conference is.
*/
class OngoingConferenceTracker {
private static final OngoingConferenceTracker instance = new OngoingConferenceTracker();
private static final String CONFERENCE_WILL_JOIN = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_TERMINATED = "CONFERENCE_TERMINATED";
private final Collection<OngoingConferenceListener> listeners =
Collections.synchronizedSet(new HashSet<OngoingConferenceListener>());
private String currentConference;
public OngoingConferenceTracker() {
}
public static OngoingConferenceTracker getInstance() {
return instance;
}
/**
* Gets the current active conference URL.
*
* @return - The current conference URL as a String.
*/
synchronized String getCurrentConference() {
return currentConference;
}
synchronized void onExternalAPIEvent(String name, ReadableMap data) {
if (!data.hasKey("url")) {
return;
}
String url = data.getString("url");
if (url == null) {
return;
}
switch(name) {
case CONFERENCE_WILL_JOIN:
currentConference = url;
updateListeners();
break;
case CONFERENCE_TERMINATED:
if (url.equals(currentConference)) {
currentConference = null;
updateListeners();
}
break;
}
}
void addListener(OngoingConferenceListener listener) {
listeners.add(listener);
}
void removeListener(OngoingConferenceListener listener) {
listeners.remove(listener);
}
private void updateListeners() {
synchronized (listeners) {
for (OngoingConferenceListener listener : listeners) {
listener.onCurrentConferenceChanged(currentConference);
}
}
}
public interface OngoingConferenceListener {
void onCurrentConferenceChanged(String conferenceUrl);
}
}

View File

@@ -1,118 +0,0 @@
/*
* 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.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
* and to hangup from within the notification itself.
*/
class OngoingNotification {
private static final String TAG = OngoingNotification.class.getSimpleName();
private static final String CHANNEL_ID = "JitsiNotificationChannel";
private static final String CHANNEL_NAME = "Ongoing Conference Notifications";
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
Log.w(TAG, "Cannot create notification channel: no current context");
return;
}
NotificationManager notificationManager
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel(CHANNEL_ID);
if (channel != null) {
// The channel was already created, no need to do it again.
return;
}
channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification() {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
Log.w(TAG, "Cannot create notification: no current context");
return null;
}
Intent notificationIntent = new Intent(context, context.getClass());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new NotificationCompat.Builder(context, CHANNEL_ID);
} else {
builder = new NotificationCompat.Builder(context);
}
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
.setContentTitle(context.getString(R.string.ongoing_notification_title))
.setContentText(context.getString(R.string.ongoing_notification_text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
// Add a "hang-up" action only if we are using ConnectionService.
if (AudioModeModule.useConnectionService()) {
Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
PendingIntent hangupPendingIntent
= PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
builder.addAction(hangupAction);
}
return builder.build();
}
}

View File

@@ -17,7 +17,6 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Application;
import android.support.annotation.Nullable;
@@ -121,18 +120,6 @@ class ReactInstanceManagerHolder {
? reactContext.getNativeModule(nativeModuleClass) : null;
}
/**
* Gets the current {@link Activity} linked to React Native.
*
* @return An activity attached to React Native.
*/
static Activity getCurrentActivity() {
ReactContext reactContext
= reactInstanceManager != null
? reactInstanceManager.getCurrentReactContext() : null;
return reactContext != null ? reactContext.getCurrentActivity() : null;
}
static ReactInstanceManager getReactInstanceManager() {
return reactInstanceManager;
}
@@ -155,6 +142,7 @@ class ReactInstanceManagerHolder {
new com.BV.LinearGradient.LinearGradientPackage(),
new com.calendarevents.CalendarEventsPackage(),
new com.corbt.keepawake.KCKeepAwakePackage(),
new com.dylanvann.fastimage.FastImageViewPackage(),
new com.facebook.react.shell.MainReactPackage(),
new com.oblador.vectoricons.VectorIconsPackage(),
new com.ocetnik.timer.BackgroundTimerPackage(),
@@ -194,8 +182,5 @@ class ReactInstanceManagerHolder {
if (devSettings != null) {
devSettings.setBundleDeltasEnabled(false);
}
// Register our uncaught exception handler.
JitsiMeetUncaughtExceptionHandler.register();
}
}

View File

@@ -1,6 +1,4 @@
<resources>
<string name="app_name">Jitsi Meet SDK</string>
<string name="dropbox_app_key"></string>
<string name="ongoing_notification_title">Ongoing meeting</string>
<string name="ongoing_notification_text">You are currently in a meeting. Tap to return to it.</string>
</resources>

View File

@@ -7,6 +7,8 @@ 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'
project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android')
include ':react-native-immersive'

View File

@@ -16,15 +16,13 @@ import * as JitsiMeetConferenceEvents from './ConferenceEvents';
import {
createDeviceChangedEvent,
createStartSilentEvent,
createScreenSharingEvent,
createStreamSwitchDelayEvent,
createTrackMutedEvent,
sendAnalytics
} from './react/features/analytics';
import {
maybeRedirectToWelcomePage,
redirectToStaticPage,
redirectWithStoredParams,
reloadWithStoredParams
} from './react/features/app';
@@ -44,7 +42,6 @@ import {
conferenceWillJoin,
conferenceWillLeave,
dataChannelOpened,
kickedOut,
lockStateChanged,
onStartMutedPolicyChanged,
p2pStatusChanged,
@@ -78,20 +75,16 @@ import {
setVideoAvailable,
setVideoMuted
} from './react/features/base/media';
import {
hideNotification,
showNotification
} from './react/features/notifications';
import { showNotification } from './react/features/notifications';
import {
dominantSpeakerChanged,
getAvatarURLByParticipantId,
getLocalParticipant,
getNormalizedDisplayName,
getParticipantById,
localParticipantConnectionStatusChanged,
localParticipantRoleChanged,
participantConnectionStatusChanged,
participantKicked,
participantMutedUs,
participantPresenceChanged,
participantRoleChanged,
participantUpdated
@@ -101,12 +94,14 @@ import {
createLocalTracksF,
destroyLocalTracks,
isLocalTrackMuted,
isUserInteractionRequiredForUnmute,
replaceLocalTrack,
trackAdded,
trackRemoved
} from './react/features/base/tracks';
import { getJitsiMeetGlobalNS } from './react/features/base/util';
import {
getLocationContextRoot,
getJitsiMeetGlobalNS
} from './react/features/base/util';
import { addMessage } from './react/features/chat';
import { showDesktopPicker } from './react/features/desktop-picker';
import { appendSuffix } from './react/features/display-name';
@@ -213,6 +208,77 @@ function muteLocalVideo(muted) {
APP.store.dispatch(setVideoMuted(muted));
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {object} options used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog
* @param {boolean} options.showThankYou - whether we should
* show thank you dialog
* @param {boolean} options.feedbackSubmitted - whether feedback was submitted
*/
function maybeRedirectToWelcomePage(options) {
// if close page is enabled redirect to it, without further action
if (config.enableClosePage) {
const { isGuest } = APP.store.getState()['features/base/jwt'];
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', isGuest);
redirectToStaticPage(`static/${
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`);
return;
}
// else: show thankYou dialog only if there is no feedback
if (options.showThankYou) {
APP.store.dispatch(showNotification({
titleArguments: { appName: interfaceConfig.APP_NAME },
titleKey: 'dialog.thankYou'
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (config.enableWelcomePage) {
setTimeout(
() => {
APP.store.dispatch(redirectWithStoredParams('/'));
},
options.showThankYou ? 3000 : 500);
}
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @return {void}
*/
function redirectToStaticPage(pathname) {
const windowLocation = window.location;
let newPathname = pathname;
if (!newPathname.startsWith('/')) {
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
newPathname.startsWith('./')
&& (newPathname = newPathname.substring(2));
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
windowLocation.pathname = newPathname;
}
/**
* A queue for the async replaceLocalTrack action so that multiple audio
* replacements cannot happen simultaneously. This solves the issue where
@@ -278,7 +344,7 @@ class ConferenceConnector {
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
// let's show some auth not allowed page
APP.store.dispatch(redirectToStaticPage('static/authError.html'));
redirectToStaticPage('static/authError.html');
break;
}
@@ -309,6 +375,13 @@ class ConferenceConnector {
APP.UI.notifyGracefulShutdown();
break;
case JitsiConferenceErrors.JINGLE_FATAL_ERROR: {
const [ error ] = params;
APP.UI.notifyInternalError(error);
break;
}
case JitsiConferenceErrors.CONFERENCE_DESTROYED: {
const [ reason ] = params;
@@ -330,7 +403,6 @@ class ConferenceConnector {
case JitsiConferenceErrors.FOCUS_LEFT:
case JitsiConferenceErrors.VIDEOBRIDGE_NOT_AVAILABLE:
case JitsiConferenceErrors.OFFER_ANSWER_FAILED:
APP.store.dispatch(conferenceWillLeave(room));
// FIXME the conference should be stopped by the library and not by
@@ -560,7 +632,8 @@ export default {
// Resolve with no tracks
tryCreateLocalTracks = Promise.resolve([]);
} else {
tryCreateLocalTracks = createLocalTracksF({ devices: initialDevices }, true)
tryCreateLocalTracks = createLocalTracksF(
{ devices: initialDevices }, true)
.catch(err => {
if (requestedAudio && requestedVideo) {
@@ -656,7 +729,6 @@ export default {
// based on preferred devices, loose label matching can be done in
// cases where the exact ID match is no longer available, such as
// when the camera device has switched USB ports.
// when in startSilent mode we want to start with audio muted
this._initDeviceList()
.catch(error => logger.warn(
'initial device list initialization failed', error))
@@ -664,11 +736,8 @@ export default {
options.roomName, {
startAudioOnly: config.startAudioOnly,
startScreenSharing: config.startScreenSharing,
startWithAudioMuted: config.startWithAudioMuted
|| config.startSilent
|| isUserInteractionRequiredForUnmute(APP.store.getState()),
startWithAudioMuted: config.startWithAudioMuted,
startWithVideoMuted: config.startWithVideoMuted
|| isUserInteractionRequiredForUnmute(APP.store.getState())
}))
.then(([ tracks, con ]) => {
tracks.forEach(track => {
@@ -724,7 +793,6 @@ export default {
}
if (config.startSilent) {
sendAnalytics(createStartSilentEvent());
APP.store.dispatch(showNotification({
descriptionKey: 'notify.startSilentDescription',
titleKey: 'notify.startSilentTitle'
@@ -769,13 +837,6 @@ export default {
* dialogs in case of media permissions error.
*/
muteAudio(mute, showUI = true) {
if (!mute
&& isUserInteractionRequiredForUnmute(APP.store.getState())) {
logger.error('Unmuting audio requires user interaction');
return;
}
// Not ready to modify track's state yet
if (!this._localTracksInitialized) {
// This will only modify base/media.audio.muted which is then synced
@@ -839,13 +900,6 @@ export default {
* dialogs in case of media permissions error.
*/
muteVideo(mute, showUI = true) {
if (!mute
&& isUserInteractionRequiredForUnmute(APP.store.getState())) {
logger.error('Unmuting video requires user interaction');
return;
}
// If not ready to modify track's state yet adjust the base/media
if (!this._localTracksInitialized) {
// This will only modify base/media.video.muted which is then synced
@@ -965,15 +1019,17 @@ export default {
* Returns the connection times stored in the library.
*/
getConnectionTimes() {
return room.getConnectionTimes();
return this._room.getConnectionTimes();
},
// used by torture currently
isJoined() {
return room && room.isJoined();
return this._room
&& this._room.isJoined();
},
getConnectionState() {
return room && room.getConnectionState();
return this._room
&& this._room.getConnectionState();
},
/**
@@ -982,7 +1038,8 @@ export default {
* P2P connection
*/
getP2PConnectionState() {
return room && room.getP2PConnectionState();
return this._room
&& this._room.getP2PConnectionState();
},
/**
@@ -991,7 +1048,7 @@ export default {
*/
_startP2P() {
try {
room && room.startP2PSession();
this._room && this._room.startP2PSession();
} catch (error) {
logger.error('Start P2P failed', error);
throw error;
@@ -1004,7 +1061,7 @@ export default {
*/
_stopP2P() {
try {
room && room.stopP2PSession();
this._room && this._room.stopP2PSession();
} catch (error) {
logger.error('Stop P2P failed', error);
throw error;
@@ -1019,7 +1076,7 @@ export default {
* false otherwise.
*/
isConnectionInterrupted() {
return room.isConnectionInterrupted();
return this._room.isConnectionInterrupted();
},
/**
@@ -1080,7 +1137,7 @@ export default {
},
getMyUserId() {
return room && room.myUserId();
return this._room && this._room.myUserId();
},
/**
@@ -1103,7 +1160,7 @@ export default {
* least one track.
*/
getNumberOfParticipantsWithTracks() {
return room.getParticipants()
return this._room.getParticipants()
.filter(p => p.getTracks().length > 0)
.length;
},
@@ -1247,26 +1304,11 @@ export default {
}
options.applicationName = interfaceConfig.APP_NAME;
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
options.getWiFiStatsMethod = getJitsiMeetGlobalNS().getWiFiStats;
return options;
},
/**
* Returns the result of getWiFiStats from the global NS or does nothing
* (returns empty result).
* Fixes a concurrency problem where we need to pass a function when creating
* JitsiConference, but that method is added to the context later.
*
* @returns {Promise}
* @private
*/
_getWiFiStatsMethod() {
const gloabalNS = getJitsiMeetGlobalNS();
return gloabalNS.getWiFiStats ? gloabalNS.getWiFiStats() : Promise.resolve('{}');
},
/**
* Start using provided video stream.
* Stops previous video stream.
@@ -1282,7 +1324,7 @@ export default {
this.localVideo = newStream;
this._setSharingScreen(newStream);
if (newStream) {
APP.UI.addLocalVideoStream(newStream);
APP.UI.addLocalStream(newStream);
}
this.setVideoMuteStatus(this.isLocalVideoMuted());
})
@@ -1333,6 +1375,9 @@ export default {
replaceLocalTrack(this.localAudio, newStream, room))
.then(() => {
this.localAudio = newStream;
if (newStream) {
APP.UI.addLocalStream(newStream);
}
this.setAudioMuteStatus(this.isLocalAudioMuted());
})
.then(resolve)
@@ -1737,6 +1782,11 @@ export default {
logger.log(`USER ${id} LEFT:`, user);
APP.API.notifyUserLeft(id);
APP.UI.messageHandler.participantNotification(
user.getDisplayName(),
'notify.somebody',
'disconnected',
'notify.disconnected');
APP.UI.onSharedVideoStop(id);
});
@@ -1805,29 +1855,8 @@ export default {
APP.UI.setAudioLevel(id, newLvl);
});
// we store the last start muted notification id that we showed,
// so we can hide it when unmuted mic is detected
let lastNotificationId;
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track, participantThatMutedUs) => {
if (participantThatMutedUs) {
APP.store.dispatch(participantMutedUs(participantThatMutedUs));
}
if (lastNotificationId && track.isAudioTrack() && track.isLocal() && !track.isMuted()) {
APP.store.dispatch(hideNotification(lastNotificationId));
lastNotificationId = undefined;
}
});
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
const action = APP.store.dispatch(showNotification({
titleKey: 'toolbar.talkWhileMutedPopup',
customActionNameKey: 'notify.unmute',
customActionHandler: muteLocalAudio.bind(this, false)
}));
lastNotificationId = action.uid;
APP.UI.showToolbar(6000);
});
room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
@@ -1926,17 +1955,13 @@ export default {
}
});
room.on(JitsiConferenceEvents.KICKED, participant => {
room.on(JitsiConferenceEvents.KICKED, () => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
APP.UI.notifyKicked();
// FIXME close
});
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
APP.store.dispatch(participantKicked(kicker, kicked));
});
room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
APP.store.dispatch(suspendDetected());
@@ -1964,8 +1989,6 @@ export default {
this.localAudio.dispose();
this.localAudio = null;
}
APP.API.notifySuspendDetected();
});
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
@@ -2234,8 +2257,7 @@ export default {
APP.keyboardshortcut.init();
if (config.requireDisplayName
&& !APP.conference.getLocalDisplayName()
&& !room.isHidden()) {
&& !APP.conference.getLocalDisplayName()) {
APP.UI.promptDisplayName();
}
@@ -2245,6 +2267,18 @@ export default {
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
APP.API.notifyConferenceJoined(
this.roomName,
this._room.myUserId(),
{
displayName,
formattedDisplayName: appendSuffix(
displayName,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME),
avatarURL: getAvatarURLByParticipantId(
APP.store.getState(), this._room.myUserId())
}
);
},
/**
@@ -2567,7 +2601,7 @@ export default {
room = undefined;
APP.API.notifyReadyToClose();
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
maybeRedirectToWelcomePage(values[0]);
});
},
@@ -2708,6 +2742,14 @@ export default {
displayName: formattedNickname
}));
APP.API.notifyDisplayNameChanged(id, {
displayName: formattedNickname,
formattedDisplayName:
appendSuffix(
formattedNickname,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME)
});
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}

View File

@@ -1,31 +0,0 @@
.avatar {
align-items: center;
background-color: #AAA;
display: flex;
border-radius: 50%;
color: rgba(255, 255, 255, 0.6);
font-weight: 100;
justify-content: center;
object-fit: cover;
}
.avatar-foreign {
align-items: center;
bottom: 0;
display: flex;
font-size: 40pt;
justify-content: center;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.avatar-svg {
height: 100%;
width: 100%;
}
.defaultAvatar {
opacity: 0.6
}

View File

@@ -1,12 +1,12 @@
@font-face {
font-family: 'jitsi';
src: url('../fonts/jitsi.eot?icrce1');
src: url('../fonts/jitsi.eot?icrce1#iefix') format('embedded-opentype'),
url('../fonts/jitsi.ttf?icrce1') format('truetype'),
url('../fonts/jitsi.woff?icrce1') format('woff'),
url('../fonts/jitsi.svg?icrce1#jitsi') format('svg');
font-weight: normal;
font-style: normal;
font-family: 'jitsi';
src: url('../fonts/jitsi.eot?3vw865');
src: url('../fonts/jitsi.eot?3vw865#iefix') format('embedded-opentype'),
url('../fonts/jitsi.ttf?3vw865') format('truetype'),
url('../fonts/jitsi.woff?3vw865') format('woff'),
url('../fonts/jitsi.svg?3vw865#jitsi') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="icon-"], [class*=" icon-"] {
@@ -25,9 +25,89 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-blur-background:before {
content: "\e90f";
color: #a4b8d1;
.icon-signal_cellular_0:before {
content: "\e901";
}
.icon-signal_cellular_1:before {
content: "\e902";
}
.icon-signal_cellular_2:before {
content: "\e907";
}
.icon-phone:before {
content: "\e0cd";
}
.icon-radio_button_unchecked:before {
content: "\e836";
}
.icon-radio_button_checked:before {
content: "\e837";
}
.icon-search:before {
content: "\e8b6";
}
.icon-chat-unread:before {
content: "\e0b7";
}
.icon-closed_caption:before {
content: "\e930";
}
.icon-tiles-many:before {
content: "\e92e";
}
.icon-close:before {
content: "\e5cd";
}
.icon-open_in_new:before {
content: "\e89e";
}
.icon-restore:before {
content: "\e8b3";
}
.icon-navigate_next:before {
content: "\e409";
}
.icon-menu:before {
content: "\e5d2";
}
.icon-arrow_back:before {
content: "\e5c4";
}
.icon-public:before {
content: "\e80b";
}
.icon-event_note:before {
content: "\e616";
}
.icon-bluetooth:before {
content: "\e1aa";
}
.icon-headset:before {
content: "\e310";
}
.icon-phone-talk:before {
content: "\e61d";
}
.icon-thumb-menu:before {
content: "\e5d4";
}
.icon-ninja:before {
content: "\e909";
}
.icon-invite:before {
content: "\e145";
}
.icon-add:before {
content: "\e146";
}
.icon-play:before {
content: "\f04b";
}
.icon-stop:before {
content: "\f04d";
}
.icon-dominant-speaker:before {
content: "\f0a1";
}
.icon-speaker:before {
content: "\e92d";
@@ -137,90 +217,3 @@
.icon-visibility-off:before {
content: "\e924";
}
.icon-enlarge:before {
content: "\e90a";
}
.icon-signal_cellular_0:before {
content: "\e901";
}
.icon-signal_cellular_1:before {
content: "\e902";
}
.icon-signal_cellular_2:before {
content: "\e907";
}
.icon-phone:before {
content: "\e0cd";
}
.icon-radio_button_unchecked:before {
content: "\e836";
}
.icon-radio_button_checked:before {
content: "\e837";
}
.icon-search:before {
content: "\e8b6";
}
.icon-chat-unread:before {
content: "\e0b7";
}
.icon-closed_caption:before {
content: "\e930";
}
.icon-tiles-many:before {
content: "\e92e";
}
.icon-close:before {
content: "\e5cd";
}
.icon-open_in_new:before {
content: "\e89e";
}
.icon-restore:before {
content: "\e8b3";
}
.icon-navigate_next:before {
content: "\e409";
}
.icon-menu:before {
content: "\e5d2";
}
.icon-arrow_back:before {
content: "\e5c4";
}
.icon-public:before {
content: "\e80b";
}
.icon-event_note:before {
content: "\e616";
}
.icon-bluetooth:before {
content: "\e1aa";
}
.icon-headset:before {
content: "\e310";
}
.icon-phone-talk:before {
content: "\e61d";
}
.icon-thumb-menu:before {
content: "\e5d4";
}
.icon-ninja:before {
content: "\e909";
}
.icon-invite:before {
content: "\e145";
}
.icon-add:before {
content: "\e146";
}
.icon-play:before {
content: "\f04b";
}
.icon-stop:before {
content: "\f04d";
}
.icon-dominant-speaker:before {
content: "\f0a1";
}

View File

@@ -116,9 +116,9 @@
}
}
i.disabled, .disabled i {
cursor: initial !important;
color: #fff !important;
background-color: #a4b8d1 !important;
cursor: initial;
color: #fff;
background-color: #a4b8d1;
}
.icon-mic-disabled, .icon-microphone, .icon-camera-disabled, .icon-camera {

View File

@@ -23,9 +23,14 @@
top: 0;
width: 100%;
#largeVideoBackground {
min-height: 100%;
min-width: 100%;
&.fit-full-height #largeVideoBackground {
height: 100%;
width: auto;
}
.fit-full-width #largeVideoBackground {
height: auto;
width: 100%;
}
}
#largeVideoBackgroundContainer {
@@ -493,6 +498,7 @@
}
#dominantSpeakerAvatarContainer,
#dominantSpeakerAvatar,
.dynamic-shadow {
width: 200px;
height: 200px;
@@ -502,9 +508,14 @@
top: 50px;
margin: auto;
position: relative;
border-radius: 100px;
overflow: hidden;
visibility: inherit;
}
#dominantSpeakerAvatar {
background-color: #000000;
object-fit: cover;
}
.dynamic-shadow {
border-radius: 50%;
@@ -518,6 +529,7 @@
.avatar-container {
@include maxSize(60px);
@include absoluteAligning();
border-radius: 50%;
display: flex;
justify-content: center;
height: 50%;
@@ -534,17 +546,13 @@
#videoNotAvailableScreen {
text-align: center;
#avatarContainer {
border-radius: 50%;
display: inline-block;
height: 50vh;
display:inline-block;
margin-top: 25vh;
overflow: hidden;
width: 50vh;
#avatar {
border-radius: 50%;
height: 100%;
object-fit: cover;
width: 100%;
}
}
}

View File

@@ -86,6 +86,5 @@ $flagsImagePath: "../images/";
@import 'navigate_section_list';
@import 'third-party-branding/google';
@import 'third-party-branding/microsoft';
@import 'avatar';
/* Modules END */

View File

@@ -192,11 +192,6 @@ The `command` parameter is String object with the name of the command. The follo
api.executeCommand('displayName', 'New Nickname');
```
* **password** - Sets the password for the room. This command requires one argument - the password name to be set.
```javascript
api.executeCommand('password', 'The Password');
```
* **subject** - Sets the subject of the conference. This command requires one argument - the new subject to be set.
```javascript
api.executeCommand('subject', 'New Conference Subject');
@@ -387,19 +382,6 @@ changes. The listener will receive an object with the following structure:
}
```
* **participantKickedOut** - event notifications about a participants being removed from the room. The listener will receive an object with the following structure:
```javascript
{
kicked: {
id: string, // the id of the participant removed from the room
local: boolean // whether or not the participant is the local particiapnt
},
kicker: {
id: string // the id of the participant who kicked out the other participant
}
}
```
* **participantLeft** - event notifications about participants that leave the room. The listener will receive an object with the following structure:
```javascript
{
@@ -407,8 +389,6 @@ changes. The listener will receive an object with the following structure:
}
```
* **passwordRequired** - event notifications fired when failing to join a room because it has a password.
* **videoConferenceJoined** - event notifications fired when the local user has joined the video conference. The listener will receive an object with the following structure:
```javascript
{
@@ -450,8 +430,6 @@ The listener will receive an object with the following structure:
}
```
* **suspendDetected** - event notifications about detecting suspend event in host computer.
You can also add multiple event listeners by using `addEventListeners`.
This method requires one argument of type Object. The object argument must
have the names of the events as keys and the listeners of the events as values.

Binary file not shown.

View File

@@ -37,12 +37,10 @@
<glyph unicode="&#xe907;" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
<glyph unicode="&#xe908;" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
<glyph unicode="&#xe909;" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
<glyph unicode="&#xe90a;" glyph-name="enlarge" d="M896 212v600h-768v-600h768zM896 896q34 0 60-26t26-60v-596q0-34-26-60t-60-26h-768q-34 0-60 26t-26 60v596q0 34 26 60t60 26h768zM598 342l-86-108-86 108h172zM256 598v-172l-106 86zM768 598l106-86-106-86v172zM512 790l86-108h-172z" />
<glyph unicode="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe90c;" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe90d;" glyph-name="security" d="M768 170v428h-512v-428h512zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h388v86c0 72-60 132-132 132s-132-60-132-132h-82c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe90e;" glyph-name="security-locked" d="M768 170v428h-512v-428h512zM380 768v-86h264v86c0 72-60 132-132 132s-132-60-132-132zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h42v86c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe90f;" glyph-name="blur-background" d="M469.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM725.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM469.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM426.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM426.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM725.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333z" />
<glyph unicode="&#xe910;" glyph-name="microphone" d="M738 554h72c0-146-116-266-256-286v-140h-84v140c-140 20-256 140-256 286h72c0-128 108-216 226-216s226 88 226 216zM512 426c-70 0-128 58-128 128v256c0 70 58 128 128 128s128-58 128-128v-256c0-70-58-128-128-128z" />
<glyph unicode="&#xe912;" glyph-name="mic-disabled" d="M182 896l714-714-54-54-178 178c-32-20-72-32-110-38v-140h-84v140c-140 20-256 140-256 286h72c0-128 108-216 226-216 34 0 68 8 98 22l-70 70c-8-2-18-4-28-4-70 0-128 58-128 128v32l-256 256zM640 548l-256 254v8c0 70 58 128 128 128s128-58 128-128v-262zM810 554c0-50-14-98-38-140l-52 54c12 26 18 54 18 86h72z" />
<glyph unicode="&#xe913;" glyph-name="link" d="M640 426c114 0 342-56 342-170v-86h-684v86c0 114 228 170 342 170zM256 598h128v-86h-128v-128h-86v128h-128v86h128v128h86v-128zM640 512c-94 0-170 76-170 170s76 172 170 172 170-78 170-172-76-170-170-170z" />

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -93,15 +93,12 @@
+ "font-size:small;"
+ "cursor: pointer'>" + showMoreText + "</a>"
+ "&nbsp;&nbsp;&nbsp;"
+ "<a id ='reloadLink' style='"
+ "<a href='" + href + "' style='"
+ "text-decoration: underline;"
+ "font-size:small;"
+ "'>reload now</a>"
+ "</div>";
var reloadLink = document.getElementById('reloadLink');
reloadLink.setAttribute('href', href);
var showMoreElem = document.getElementById("showMore");
showMoreElem.addEventListener('click', function () {
var moreInfoElem

View File

@@ -50,7 +50,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview', 'videobackgroundblur'
'tileview'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
@@ -167,27 +167,7 @@ var interfaceConfig = {
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true,
// Names of browsers which should show a warning stating the current browser
// has a suboptimal experience. Browsers which are not listed as optimal or
// unsupported are considered suboptimal. Valid values are:
// chrome, chromium, edge, electron, firefox, nwjs, opera, safari
OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'nwjs', 'electron' ],
// Browsers, in addition to those which do not fully support WebRTC, that
// are not supported and should show the unsupported browser page.
UNSUPPORTED_BROWSERS: [],
/**
* A UX mode where the last screen share participant is automatically
* pinned. Valid values are the string "remote-only" so remote participants
* get pinned but not local, otherwise any truthy value for all participants,
* and any falsy value to disable the feature.
*
* Note: this mode is experimental and subject to breakage.
*/
AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only'
RECENT_LIST_ENABLED: true
/**
* How many columns the tile view can expand to. The respected range is
@@ -215,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

View File

@@ -39,6 +39,7 @@ target 'JitsiMeet' do
pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer'
pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events'
pod 'react-native-fast-image', :path => '../node_modules/react-native-fast-image'
pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake'
pod 'react-native-webview', :path => '../node_modules/react-native-webview'
pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc'

View File

@@ -35,6 +35,7 @@ PODS:
- FirebaseCore (~> 5.2)
- GoogleUtilities/Environment (~> 5.2)
- GoogleUtilities/UserDefaults (~> 5.2)
- FLAnimatedImage (1.0.12)
- Folly (2018.10.22.00):
- boost-for-react-native
- DoubleConversion
@@ -89,9 +90,14 @@ PODS:
- React
- react-native-calendar-events (1.6.4):
- React
- react-native-fast-image (5.1.1):
- FLAnimatedImage
- React
- SDWebImage/Core
- SDWebImage/GIF
- react-native-keep-awake (4.0.0):
- React
- react-native-webrtc (1.69.2):
- react-native-webrtc (1.69.1):
- React
- react-native-webview (5.8.1):
- React
@@ -144,8 +150,8 @@ PODS:
- React/RCTBlob
- RNCAsyncStorage (1.3.4):
- React
- RNGoogleSignin (2.0.0):
- GoogleSignIn (~> 4.4.0)
- RNGoogleSignin (1.0.2):
- GoogleSignIn
- React
- RNSound (0.10.12):
- React/Core
@@ -156,6 +162,10 @@ PODS:
- React
- RNWatch (0.2.0):
- React
- SDWebImage/Core (4.4.6)
- SDWebImage/GIF (4.4.6):
- FLAnimatedImage (~> 1.0)
- SDWebImage/Core
- yoga (0.59.8.React)
DEPENDENCIES:
@@ -171,6 +181,7 @@ DEPENDENCIES:
- ObjectiveDropboxOfficial (~> 3.9.4)
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- react-native-calendar-events (from `../node_modules/react-native-calendar-events`)
- react-native-fast-image (from `../node_modules/react-native-fast-image`)
- react-native-keep-awake (from `../node_modules/react-native-keep-awake`)
- react-native-webrtc (from `../node_modules/react-native-webrtc`)
- react-native-webview (from `../node_modules/react-native-webview`)
@@ -203,6 +214,7 @@ SPEC REPOS:
- FirebaseCore
- FirebaseDynamicLinks
- FirebaseInstanceID
- FLAnimatedImage
- GoogleAppMeasurement
- GoogleSignIn
- GoogleToolboxForMac
@@ -210,6 +222,7 @@ SPEC REPOS:
- GTMSessionFetcher
- nanopb
- ObjectiveDropboxOfficial
- SDWebImage
EXTERNAL SOURCES:
BVLinearGradient:
@@ -226,6 +239,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-background-timer"
react-native-calendar-events:
:path: "../node_modules/react-native-calendar-events"
react-native-fast-image:
:path: "../node_modules/react-native-fast-image"
react-native-keep-awake:
:path: "../node_modules/react-native-keep-awake"
react-native-webrtc:
@@ -258,6 +273,7 @@ SPEC CHECKSUMS:
FirebaseCore: 52f851b30e11360f1e67cf04b1edfebf0a47a2d3
FirebaseDynamicLinks: f209c3caccd82102caa0e91d393e3ccc593501fd
FirebaseInstanceID: bd6fc5a258884e206fd5c474ebe4f5b00e21770e
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
Folly: de497beb10f102453a1afa9edbf8cf8a251890de
glog: aefd1eb5dda2ab95ba0938556f34b98e2da3a60d
GoogleAppMeasurement: 6cf307834da065863f9faf4c0de0a936d81dd832
@@ -270,16 +286,18 @@ SPEC CHECKSUMS:
React: 76e6aa2b87d05eb6cccb6926d72685c9a07df152
react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2
react-native-calendar-events: ee9573e355711ac679e071be70789542431f4ce3
react-native-fast-image: 47487b71169aea34868e7b38bf870b6b3f2157c5
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-webrtc: 1415d2a54b2246dd85ba95eb3e4bf2b66533f951
react-native-webrtc: 90a847d19deb2d7323fef8cc89ca12b8995fbc90
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
RNGoogleSignin: d030c6c6591db24c3cee649f64c7babf0a1699a0
RNGoogleSignin: 361174d9a3090d295b06257162b560d8efc8a6ed
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
SDWebImage: 3f3f0c02f09798048c47a5ed0a13f17b063572d8
yoga: 92b2102c3d373d1a790db4ab761d2b0ffc634f64
PODFILE CHECKSUM: d540f088d564bfe3b8ca3d13eec4cc0ce9c6e4bc
PODFILE CHECKSUM: b55338cc43312051ed83f8d9c6aadbd8c9402e6a
COCOAPODS: 1.7.2
COCOAPODS: 1.6.1

View File

@@ -46,12 +46,6 @@
jitsiMeet.defaultConferenceOptions = [JitsiMeetConferenceOptions fromBuilder:^(JitsiMeetConferenceOptionsBuilder *builder) {
builder.serverURL = [NSURL URLWithString:@"https://meet.jit.si"];
builder.welcomePageEnabled = YES;
// Apple rejected our app because they claim requiring a
// Dropbox account for recording is not acceptable.
#if DEBUG
[builder setFeatureFlag:@"ios.recording.enabled" withBoolean:YES];
#endif
}];
[jitsiMeet application:application didFinishLaunchingWithOptions:launchOptions];

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>19.3.0</string>
<string>19.2.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

@@ -96,10 +96,4 @@
[self _onJitsiMeetViewDelegateEvent:@"CONFERENCE_WILL_JOIN" withData:data];
}
#if 0
- (void)enterPictureInPicture:(NSDictionary *)data {
[self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data];
}
#endif
@end

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>19.3.0</string>
<string>19.2.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>19.3.0</string>
<string>19.2.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

@@ -42,8 +42,6 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */; settings = {ATTRIBUTES = (Public, ); }; };
DE762DB622AFDE8D000DEBD6 /* JitsiMeetUserInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */; };
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 */; };
@@ -96,9 +94,6 @@
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetUserInfo.h; sourceTree = "<group>"; };
DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetUserInfo.m; sourceTree = "<group>"; };
DE762DB722AFE166000DEBD6 /* JitsiMeetUserInfo+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetUserInfo+Private.h"; sourceTree = "<group>"; };
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>"; };
@@ -179,9 +174,6 @@
DEAD3224220C497000E93636 /* JitsiMeetConferenceOptions.h */,
DEAD3228220C734300E93636 /* JitsiMeetConferenceOptions+Private.h */,
DEAD3225220C497000E93636 /* JitsiMeetConferenceOptions.m */,
DE762DB322AFDE76000DEBD6 /* JitsiMeetUserInfo.h */,
DE762DB722AFE166000DEBD6 /* JitsiMeetUserInfo+Private.h */,
DE762DB522AFDE8D000DEBD6 /* JitsiMeetUserInfo.m */,
0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
DEAFA777229EAD3B0033A7FA /* RNRootView.h */,
@@ -267,7 +259,6 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
DE762DB422AFDE76000DEBD6 /* JitsiMeetUserInfo.h in Headers */,
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -493,7 +484,6 @@
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
DEAFA779229EAD520033A7FA /* RNRootView.m in Sources */,
DE762DB622AFDE8D000DEBD6 /* JitsiMeetUserInfo.m in Sources */,
DEAD3227220C497000E93636 /* JitsiMeetConferenceOptions.m in Sources */,
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,

View File

@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.2.0</string>
<string>2.1.0</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>

View File

@@ -42,7 +42,7 @@
*/
@property (nonatomic, nullable) JitsiMeetConferenceOptions *defaultConferenceOptions;
#pragma mark - This class is a singleton
#pragma mak - This class is a singleton
+ (instancetype _Nonnull)sharedInstance;

View File

@@ -16,9 +16,6 @@
#import <Foundation/Foundation.h>
#import "JitsiMeetUserInfo.h"
@interface JitsiMeetConferenceOptionsBuilder : NSObject
/**
@@ -44,11 +41,6 @@
*/
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
/**
* Feature flags. See: https://github.com/jitsi/jitsi-meet/blob/master/react/features/base/flags/constants.js
*/
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
/**
* Set to YES to join the conference with audio / video muted or to start in audio
* only mode respectively.
@@ -63,14 +55,6 @@
*/
@property (nonatomic) BOOL welcomePageEnabled;
/**
* Information about the local user. It will be used in absence of a token.
*/
@property (nonatomic, nullable) JitsiMeetUserInfo *userInfo;
- (void)setFeatureFlag:(NSString *_Nonnull)flag withBoolean:(BOOL)value;
- (void)setFeatureFlag:(NSString *_Nonnull)flag withValue:(id _Nonnull)value;
@end
@interface JitsiMeetConferenceOptions : NSObject
@@ -82,7 +66,6 @@
@property (nonatomic, copy, nullable, readonly) NSString *token;
@property (nonatomic, copy, nullable) NSDictionary *colorScheme;
@property (nonatomic, readonly, nonnull) NSDictionary *featureFlags;
@property (nonatomic, readonly) BOOL audioOnly;
@property (nonatomic, readonly) BOOL audioMuted;
@@ -90,8 +73,6 @@
@property (nonatomic, readonly) BOOL welcomePageEnabled;
@property (nonatomic, nullable) JitsiMeetUserInfo *userInfo;
+ (instancetype _Nonnull)fromBuilder:(void (^_Nonnull)(JitsiMeetConferenceOptionsBuilder *_Nonnull))initBlock;
- (instancetype _Nonnull)init NS_UNAVAILABLE;

View File

@@ -17,19 +17,12 @@
#import <React/RCTUtils.h>
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetUserInfo+Private.h"
/**
* Backwards compatibility: turn the boolean property into a feature flag.
*/
static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
@implementation JitsiMeetConferenceOptionsBuilder {
NSNumber *_audioOnly;
NSNumber *_audioMuted;
NSNumber *_videoMuted;
NSMutableDictionary *_featureFlags;
NSNumber *_welcomePageEnabled;
}
@dynamic audioOnly;
@@ -45,26 +38,17 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
_token = nil;
_colorScheme = nil;
_featureFlags = [[NSMutableDictionary alloc] init];
_audioOnly = nil;
_audioMuted = nil;
_videoMuted = nil;
_userInfo = nil;
_welcomePageEnabled = nil;
}
return self;
}
- (void)setFeatureFlag:(NSString *)flag withBoolean:(BOOL)value {
[self setFeatureFlag:flag withValue:[NSNumber numberWithBool:value]];
}
- (void)setFeatureFlag:(NSString *)flag withValue:(id)value {
_featureFlags[flag] = value;
}
#pragma mark - Dynamic properties
- (void)setAudioOnly:(BOOL)audioOnly {
@@ -92,14 +76,11 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
}
- (void)setWelcomePageEnabled:(BOOL)welcomePageEnabled {
[self setFeatureFlag:WelcomePageEnabledFeatureFlag
withBoolean:welcomePageEnabled];
_welcomePageEnabled = [NSNumber numberWithBool:welcomePageEnabled];
}
- (BOOL)welcomePageEnabled {
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
return n != nil ? [n boolValue] : NO;
return _welcomePageEnabled && [_welcomePageEnabled boolValue];
}
#pragma mark - Private API
@@ -116,13 +97,17 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
return _videoMuted;
}
- (NSNumber *)getWelcomePageEnabled {
return _welcomePageEnabled;
}
@end
@implementation JitsiMeetConferenceOptions {
NSNumber *_audioOnly;
NSNumber *_audioMuted;
NSNumber *_videoMuted;
NSDictionary *_featureFlags;
NSNumber *_welcomePageEnabled;
}
@dynamic audioOnly;
@@ -145,9 +130,7 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
}
- (BOOL)welcomePageEnabled {
NSNumber *n = _featureFlags[WelcomePageEnabledFeatureFlag];
return n != nil ? [n boolValue] : NO;
return _welcomePageEnabled && [_welcomePageEnabled boolValue];
}
#pragma mark - Internal initializer
@@ -165,9 +148,7 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
_audioMuted = [builder getAudioMuted];
_videoMuted = [builder getVideoMuted];
_featureFlags = [NSDictionary dictionaryWithDictionary:builder.featureFlags];
_userInfo = builder.userInfo;
_welcomePageEnabled = [builder getWelcomePageEnabled];
}
return self;
@@ -186,12 +167,14 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
- (NSDictionary *)asProps {
NSMutableDictionary *props = [[NSMutableDictionary alloc] init];
props[@"flags"] = [NSMutableDictionary dictionaryWithDictionary:_featureFlags];
if (_colorScheme != nil) {
props[@"colorScheme"] = self.colorScheme;
}
if (_welcomePageEnabled != nil) {
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
}
NSMutableDictionary *config = [[NSMutableDictionary alloc] init];
if (_audioOnly != nil) {
config[@"startAudioOnly"] = @(self.audioOnly);
@@ -225,10 +208,6 @@ static NSString *const WelcomePageEnabledFeatureFlag = @"welcomepage.enabled";
urlProps[@"jwt"] = _token;
}
if (_token == nil && _userInfo != nil) {
props[@"userInfo"] = [self.userInfo asDict];
}
urlProps[@"config"] = config;
props[@"url"] = urlProps;

View File

@@ -1,23 +0,0 @@
/*
* 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 "JitsiMeetUserInfo.h"
@interface JitsiMeetUserInfo ()
- (NSMutableDictionary *)asDict;
@end

View File

@@ -1,38 +0,0 @@
/*
* 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 <Foundation/Foundation.h>
@interface JitsiMeetUserInfo : NSObject
/**
* User display name.
*/
@property (nonatomic, copy, nullable) NSString *displayName;
/**
* User e-mail.
*/
@property (nonatomic, copy, nullable) NSString *email;
/**
* URL for the user avatar.
*/
@property (nonatomic, copy, nullable) NSURL *avatar;
- (instancetype _Nullable)initWithDisplayName:(NSString *_Nullable)displayName
andEmail:(NSString *_Nullable)email
andAvatar:(NSURL *_Nullable) avatar;
@end

View File

@@ -1,55 +0,0 @@
/*
* 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 "JitsiMeetUserInfo+Private.h"
@implementation JitsiMeetUserInfo
- (instancetype)initWithDisplayName:(NSString *)displayName
andEmail:(NSString *)email
andAvatar:(NSURL *_Nullable) avatar {
self = [super init];
if (self) {
self.displayName = displayName;
self.email = email;
self.avatar = avatar;
}
return self;
}
- (NSDictionary *)asDict {
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
if (self.displayName != nil) {
dict[@"displayName"] = self.displayName;
}
if (self.email != nil) {
dict[@"email"] = self.email;
}
if (self.avatar != nil) {
NSString *avatarURL = [self.avatar absoluteString];
if (avatarURL != nil) {
dict[@"avatarURL"] = avatarURL;
}
}
return dict;
}
@end

View File

@@ -24,12 +24,6 @@
#import "RNRootView.h"
/**
* Backwards compatibility: turn the boolean prop into a feature flag.
*/
static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
@implementation JitsiMeetView {
/**
* The unique identifier of this `JitsiMeetView` within the process for the
@@ -128,16 +122,12 @@ static void initializeViewsMap() {
- (void)setProps:(NSDictionary *_Nonnull)newProps {
NSMutableDictionary *props = mergeProps([[JitsiMeet sharedInstance] getDefaultProps], newProps);
// Set the PiP flag if it wasn't manually set.
NSMutableDictionary *featureFlags = props[@"flags"];
if (featureFlags[PiPEnabledFeatureFlag] == nil) {
featureFlags[PiPEnabledFeatureFlag]
= [NSNumber numberWithBool:
self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]];
}
props[@"externalAPIScope"] = externalAPIScope;
// TODO: put this in some 'flags' field
props[@"pictureInPictureEnabled"]
= @(self.delegate && [self.delegate respondsToSelector:@selector(enterPictureInPicture:)]);
// This method is supposed to be imperative i.e. a second
// invocation with one and the same URL is expected to join the respective
// conference again if the first invocation was followed by leaving the

View File

@@ -74,12 +74,6 @@ RCT_EXPORT_METHOD(endCall:(NSString *)callUUID
#endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
CXEndCallAction *action
= [[CXEndCallAction alloc] initWithCallUUID:callUUID_];
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
@@ -97,12 +91,6 @@ RCT_EXPORT_METHOD(setMuted:(NSString *)callUUID
#endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
CXSetMutedCallAction *action
= [[CXSetMutedCallAction alloc] initWithCallUUID:callUUID_ muted:muted];
[self requestTransaction:[[CXTransaction alloc] initWithAction:action]
@@ -135,13 +123,6 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
NSLog(@"[RNCallKit][startCall] callUUID = %@", callUUID);
#endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
// Don't start a new call if there's an active call for the specified
// callUUID. JitsiMeetView was configured for an incoming call.
if ([JMCallKitProxy hasActiveCallForUUID:callUUID]) {
@@ -151,6 +132,7 @@ RCT_EXPORT_METHOD(startCall:(NSString *)callUUID
CXHandle *handle_
= [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
CXStartCallAction *action
= [[CXStartCallAction alloc] initWithCallUUID:callUUID_
handle:handle_];
@@ -164,12 +146,6 @@ RCT_EXPORT_METHOD(reportCallFailed:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
[JMCallKitProxy reportCallWith:callUUID_
endedAt:nil
reason:CXCallEndedReasonFailed];
@@ -181,12 +157,6 @@ RCT_EXPORT_METHOD(reportConnectedOutgoingCall:(NSString *)callUUID
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject) {
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
[JMCallKitProxy reportOutgoingCallWith:callUUID_
connectedAt:nil];
resolve(nil);
@@ -205,12 +175,6 @@ RCT_EXPORT_METHOD(updateCall:(NSString *)callUUID
#endif
NSUUID *callUUID_ = [[NSUUID alloc] initWithUUIDString:callUUID];
if (!callUUID_) {
reject(nil, [NSString stringWithFormat:@"Invalid UUID: %@", callUUID], nil);
return;
}
NSString *displayName = options[@"displayName"];
BOOL hasVideo = [(NSNumber*)options[@"hasVideo"] boolValue];

View File

@@ -37,15 +37,6 @@ public class PiPViewCoordinator {
}
}
public enum Position {
case lowerRightCorner
case upperRightCorner
case lowerLeftCorner
case upperLeftCorner
}
public var initialPositionInSuperview = Position.lowerRightCorner
/// The size ratio of the view when in PiP mode
public var pipSizeRatio: CGFloat = {
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
@@ -214,21 +205,9 @@ public class PiPViewCoordinator {
let adjustedBounds = bounds.inset(by: dragBoundInsets)
let size = CGSize(width: bounds.size.width * pipSizeRatio,
height: bounds.size.height * pipSizeRatio)
let origin = initialPositionFor(pipSize: size, bounds: adjustedBounds)
return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height)
}
private func initialPositionFor(pipSize size: CGSize, bounds: CGRect) -> CGPoint {
switch initialPositionInSuperview {
case .lowerLeftCorner:
return CGPoint(x: bounds.minX, y: bounds.maxY - size.height)
case .lowerRightCorner:
return CGPoint(x: bounds.maxX - size.width, y: bounds.maxY - size.height)
case .upperLeftCorner:
return CGPoint(x: bounds.minX, y: bounds.minY)
case .upperRightCorner:
return CGPoint(x: bounds.maxX - size.width, y: bounds.minY)
}
let x: CGFloat = adjustedBounds.maxX - size.width
let y: CGFloat = adjustedBounds.maxY - size.height
return CGRect(x: x, y: y, width: size.width, height: size.height)
}
// MARK: - Animation helpers

View File

@@ -4,7 +4,7 @@
"countryNotSupported": "We do not support this destination yet.",
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
"disabled": "You can't invite people.",
"failedToAdd": "Failed to add participants",
"failedToAdd": "Failed to add members",
"footerText": "Dialing out is disabled.",
"invite": "Invite",
"loading": "Searching for people and phone numbers",
@@ -112,6 +112,7 @@
"transport_plural": "Transports:",
"turn": " (turn)"
},
"contactlist_plural": "__count__ Members",
"dateUtils": {
"earlier": "Earlier",
"today": "Today",
@@ -120,7 +121,6 @@
"deepLinking": {
"appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
"description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.",
"descriptionWithoutWeb": "Nothing happened? We tried launching your meeting in the __app__ desktop app.",
"downloadApp": "Download the app",
"launchWebButton": "Launch in web",
"openApp": "Continue to the app",
@@ -147,7 +147,7 @@
"liveStreaming": "Live Stream"
},
"allow": "Allow",
"alreadySharedVideoMsg": "Another participant is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoMsg": "Another member is already sharing a video. This conference allows only one shared video at a time.",
"alreadySharedVideoTitle": "Only one shared video is allowed at a time",
"applicationWindow": "Application window",
"Back": "Back",
@@ -172,13 +172,14 @@
"connecting": "Connecting",
"contactSupport": "Contact support",
"copy": "Copy",
"currentPassword": "The current password is",
"defaultError": "There was some kind of error",
"detectext": "Error when trying to detect desktopsharing extension.",
"dismiss": "Dismiss",
"displayNameRequired": "Hi! Whats your name?",
"displayNameRequired": "Display name is required",
"done": "Done",
"doNotShowMessageAgain": "Don't show this message again",
"enterDisplayName": "Please enter your name here",
"enterDisplayName": "Please enter your display name",
"error": "Error",
"externalInstallationMsg": "You need to install our desktop sharing extension.",
"externalInstallationTitle": "Extension required",
@@ -189,44 +190,47 @@
"gracefulShutdown": "Our service is currently down for maintenance. Please try again later.",
"hungUp": "You hung up",
"IamHost": "I am the host",
"incorrectRoomLockPassword": "Incorrect password",
"incorrectPassword": "Incorrect username or password",
"inlineInstallationMsg": "You need to install our desktop sharing extension.",
"inlineInstallExtension": "Install now",
"internalError": "Oops! Something went wrong. The following error occurred: __error__",
"internalErrorTitle": "Internal error",
"joinAgain": "Join again",
"kickMessage": "You can contact __participantDisplayName__ for more details.",
"kickMessage": "Ouch! You have been kicked out of the meet!",
"kickParticipantButton": "Kick",
"kickParticipantDialog": "Are you sure you want to kick this participant?",
"kickParticipantTitle": "Kick this participant?",
"kickTitle": "Ouch! __participantDisplayName__ kicked you out of the meeting",
"kickParticipantTitle": "Kick this member?",
"kickTitle": "Kicked from meeting",
"liveStreaming": "Live Streaming",
"liveStreamingDisabledForGuestTooltip": "Guests can't start live streaming.",
"liveStreamingDisabledTooltip": "Start live stream disabled.",
"lockMessage": "Failed to lock the conference.",
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)",
"lockRoom": "Add meeting password",
"lockTitle": "Lock failed",
"logoutQuestion": "Are you sure you want to logout and stop the conference?",
"logoutTitle": "Logout",
"maxUsersLimitReached": "The limit for maximum number of participants has been reached. The conference is full. Please contact the meeting owner or try again later!",
"maxUsersLimitReachedTitle": "Maximum participants limit reached",
"maxUsersLimitReached": "The limit for maximum number of members has been reached. The conference is full. Please contact the meeting owner or try again later!",
"maxUsersLimitReachedTitle": "Maximum members limit reached",
"micConstraintFailedError": "Your microphone does not satisfy some of the required constraints.",
"micNotFoundError": "Microphone was not found.",
"micNotSendingData": "Go to your computer's settings to unmute your mic and adjust its level",
"micNotSendingDataTitle": "Your mic is muted by your system settings",
"micNotSendingData": "We are unable to access your microphone. Please select another device from the settings menu or try to reload the application.",
"micNotSendingDataTitle": "Unable to access microphone",
"micPermissionDeniedError": "You have not granted permission to use your microphone. You can still join the conference but others won't hear you. Use the camera button in the address bar to fix this.",
"micUnknownError": "Cannot use microphone for an unknown reason.",
"muteParticipantBody": "You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantButton": "Mute",
"muteParticipantDialog": "Are you sure you want to mute this participant? You won't be able to unmute them, but they can unmute themselves at any time.",
"muteParticipantTitle": "Mute this participant?",
"muteParticipantTitle": "Mute this member?",
"Ok": "Ok",
"oops": "Oops!",
"passwordLabel": "$t(lockRoomPasswordUppercase)",
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) not supported",
"passwordRequired": "$t(lockRoomPasswordUppercase) required",
"password": "Enter password",
"passwordError": "This conversation is currently protected by a password. Only the owner of the conference can set a password.",
"passwordError2": "This conversation isn't currently protected by a password. Only the owner of the conference can set a password.",
"passwordErrorTitle": "Password Error",
"passwordLabel": "Password",
"passwordNotSupported": "Setting a meeting password is not supported.",
"passwordNotSupportedTitle": "Password not supported",
"passwordRequired": "Password required",
"permissionDenied": "Permission Denied",
"popupError": "Your browser is blocking pop-up windows from this site. Please enable pop-ups in your browser's security settings and try again.",
"popupErrorTitle": "Pop-up blocked",
@@ -243,7 +247,7 @@
"remoteControlStopMessage": "The remote control session ended!",
"remoteControlTitle": "Remote desktop control",
"Remove": "Remove",
"removePassword": "Remove $t(lockRoomPassword)",
"removePassword": "Remove password",
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
"removeSharedVideoTitle": "Remove shared video",
"reservationError": "Reservation system error",
@@ -281,7 +285,7 @@
"tokenAuthFailedTitle": "Authentication failed",
"transcribing": "Transcribing",
"unableToSwitch": "Unable to switch video stream.",
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
"unlockRoom": "Remove meeting password",
"userPassword": "user password",
"WaitForHostMsg": "The conference <b>__room__</b> has not yet started. If you are the host then please authenticate. Otherwise, please wait for the host to arrive.",
"WaitForHostMsgWOk": "The conference <b>__room__</b> has not yet started. If you are the host then please press Ok to authenticate. Otherwise, please wait for the host to arrive.",
@@ -293,6 +297,34 @@
"dialOut": {
"statusMessage": "is now __status__"
},
"email": {
"and": "and",
"body": [
" Note that __appName__ is currently only supported by __supportedBrowsers__, so you need to be using one of these browsers.",
"",
"",
"",
"",
"",
"",
"",
"",
"Hey there, I%27d like to invite you to a __appName__ conference I%27ve just set up.",
"Please click on the following link in order to join the conference.",
"Talk to you in a sec!",
"__roomUrl__",
"__sharedKeyText__"
],
"sharedKey": [
"",
"",
"",
"",
"This conference is password-protected. Please use the following pin when joining:",
"__sharedKey__"
],
"subject": "Invitation to a __appName__ (__conferenceName__)"
},
"feedback": {
"average": "Average",
"bad": "Bad",
@@ -311,8 +343,8 @@
},
"info": {
"accessibilityLabel": "Show info",
"addPassword": "Add $t(lockRoomPassword)",
"cancelPassword": "Cancel $t(lockRoomPassword)",
"addPassword": "Add password",
"cancelPassword": "Cancel password",
"conferenceURL": "Link:",
"country": "Country",
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
@@ -323,18 +355,19 @@
"dialInTollFree": "Toll Free",
"genericError": "Whoops, something went wrong.",
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
"invitePhone": "To join by phone instead, tap this: __number__,,__conferenceID__#\n",
"invitePhoneAlternatives": "Looking for a different dial-in number?\nSee meeting dial-in numbers: __url__\n\n\nIf also dialing-in through a room phone, join without connecting to audio: __silentUrl__",
"invitePhone": "One tap audio Dial In: __number__,,__conferenceID__#",
"invitePhoneAlternatives": "Looking for a different dial in number? Please see: __url__",
"inviteURLFirstPartGeneral": "You are invited to join a meeting.",
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.\n",
"inviteURLSecondPart": "\nJoin the meeting:\n__url__\n",
"inviteURLFirstPartPersonal": "__name__ is inviting you to a meeting.",
"inviteURLSecondPart": "\n__moreInfo__\nJoin meeting: __url__\n",
"inviteURLMoreInfo": "Meeting ID: __conferenceID__#\n",
"liveStreamURL": "Live stream:",
"moreNumbers": "More numbers",
"noNumbers": "No dial-in numbers.",
"noPassword": "None",
"noRoom": "No room was specified to dial-in into.",
"numbers": "Dial-in Numbers",
"password": "$t(lockRoomPasswordUppercase):",
"password": "Password:",
"title": "Share",
"tooltip": "Share link and dial-in info for this meeting",
"label": "Meeting info"
@@ -430,8 +463,6 @@
"stop": "Stop Recording",
"yes": "Yes"
},
"lockRoomPassword": "password",
"lockRoomPasswordUppercase": "Password",
"me": "me",
"notify": {
"connectedOneMember": "__name__ joined the meeting",
@@ -441,30 +472,21 @@
"focus": "Conference focus",
"focusFail": "__component__ not available - retry in __ms__ sec",
"grantedTo": "Moderator rights granted to __to__!",
"invitedOneMember": "__name__ has been invited",
"invitedThreePlusMembers": "__name__ and __count__ others have been invited",
"invitedTwoMembers": "__first__ and __second__ have been invited",
"kickParticipant": "__kicked__ was kicked by __kicker__",
"me": "Me",
"moderator": "Moderator rights granted!",
"muted": "You have started the conversation muted.",
"mutedTitle": "You're muted!",
"mutedRemotelyTitle": "You have been muted by __participantDisplayName__!",
"mutedRemotelyDescription": "You can always unmute when you're ready to speak. Mute back when you're done to keep noise away from the meeting.",
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) removed by another participant",
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) set by another participant",
"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",
"unmute": "Unmute",
"newDeviceCameraTitle": "New camera detected",
"newDeviceAudioTitle": "New audio device detected",
"newDeviceAction": "Use"
},
"passwordSetRemotely": "set by another participant",
"passwordSetRemotely": "set by another member",
"passwordDigitsOnly": "Up to __number__ digits",
"poweredby": "powered by",
"presenceStatus": {
@@ -539,6 +561,7 @@
"more": "More",
"name": "Name",
"noDevice": "None",
"password": "SET PASSWORD",
"selectAudioOutput": "Audio output",
"selectCamera": "Camera",
"selectMic": "Microphone",
@@ -588,7 +611,7 @@
"accessibilityLabel": {
"audioOnly": "Toggle audio only",
"audioRoute": "Select the sound device",
"callQuality": "Manage video quality",
"callQuality": "Manage call quality",
"cc": "Toggle subtitles",
"chat": "Toggle chat window",
"document": "Toggle shared document",
@@ -612,12 +635,10 @@
"shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare",
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"speakerStats": "Toggle speaker statistics",
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
"videomute": "Toggle mute video",
"videoblur": "Toggle video blur"
"videomute": "Toggle mute video"
},
"addPeople": "Add people to your call",
"audioonly": "Enable / Disable audio only mode",
@@ -625,7 +646,7 @@
"audioOnlyOn": "Enable audio only mode",
"audioRoute": "Select the sound device",
"authenticate": "Authenticate",
"callQuality": "Manage video quality",
"callQuality": "Manage call quality",
"cameraDisabled": "Camera is not available",
"chat": "Open / Close chat",
"closeChat": "Close chat",
@@ -656,7 +677,7 @@
"raiseYourHand": "Raise your hand",
"Settings": "Settings",
"sharedvideo": "Share a YouTube video",
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other participants.",
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
"shareRoom": "Invite someone",
"shortcuts": "View shortcuts",
"sip": "Call SIP number",
@@ -670,9 +691,7 @@
"tileViewToggle": "Toggle tile view",
"toggleCamera": "Toggle camera",
"unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
"videomute": "Start / Stop camera",
"startvideoblur": "Blur my background",
"stopvideoblur": "Disable background blur"
"videomute": "Start / Stop camera"
},
"transcribing": {
"ccButtonTooltip": "Start / Stop subtitles",
@@ -713,7 +732,7 @@
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "You are in audio only mode. This mode saves bandwidth but you won't see videos of others.",
"callQuality": "Video Quality",
"callQuality": "Call Quality",
"hd": "HD",
"hdTooltip": "Viewing high definition video",
"highDefinition": "High definition",
@@ -726,7 +745,7 @@
"onlyAudioAvailable": "Only audio is available",
"onlyAudioSupported": "We only support audio in this browser.",
"p2pEnabled": "Peer to Peer Enabled",
"p2pVideoQualityDescription": "In peer to peer mode, received video quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
"qualityButtonTip": "Change received video quality",
"recHighDefinitionOnly": "Will prefer high definition.",
"sd": "SD",
@@ -738,11 +757,10 @@
"flip": "Flip",
"kick": "Kick out",
"moderator": "Moderator",
"mute": "Participant is muted",
"mute": "Member is muted",
"muted": "Muted",
"remoteControl": "Remote control",
"show": "Show on stage",
"videomute": "Participant has stopped the camera"
"videomute": "Member has stopped the camera"
},
"welcomepage": {
"accessibilityLabel": {
@@ -765,7 +783,6 @@
"recentList": "Recent",
"recentListDelete": "Delete",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"reducedUIText": "Welcome to __app__!",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",

View File

@@ -5,7 +5,7 @@ import {
createApiEvent,
sendAnalytics
} from '../../react/features/analytics';
import { setPassword, setSubject } from '../../react/features/base/conference';
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';
@@ -65,28 +65,6 @@ function initCommands() {
sendAnalytics(createApiEvent('display.name.changed'));
APP.conference.changeLocalDisplayName(displayName);
},
'password': password => {
const { conference, passwordRequired }
= APP.store.getState()['features/base/conference'];
if (passwordRequired) {
sendAnalytics(createApiEvent('submit.password'));
APP.store.dispatch(setPassword(
passwordRequired,
passwordRequired.join,
password
));
} else {
sendAnalytics(createApiEvent('password.changed'));
APP.store.dispatch(setPassword(
conference,
conference.lock,
password
));
}
},
'proxy-connection-event': event => {
APP.conference.onProxyConnectionEvent(event);
},
@@ -509,15 +487,6 @@ class API {
this._sendEvent({ name: 'video-ready-to-close' });
}
/**
* Notify external application (if API is enabled) that a suspend event in host computer.
*
* @returns {void}
*/
notifySuspendDetected() {
this._sendEvent({ name: 'suspend-detected' });
}
/**
* Notify external application (if API is enabled) for audio muted status
* changed.
@@ -658,34 +627,6 @@ class API {
});
}
/**
* Notify external application of a participant, remote or local, being
* removed from the conference by another participant.
*
* @param {string} kicked - The ID of the participant removed from the
* conference.
* @param {string} kicker - The ID of the participant that removed the
* other participant.
* @returns {void}
*/
notifyKickedOut(kicked: Object, kicker: Object) {
this._sendEvent({
name: 'participant-kicked-out',
kicked,
kicker
});
}
/**
* Notify external application of the current meeting requiring a password
* to join.
*
* @returns {void}
*/
notifyOnPasswordRequired() {
this._sendEvent({ name: 'password-required' });
}
/**
* Notify external application (if API is enabled) that the screen sharing
* has been turned on/off.

View File

@@ -33,7 +33,6 @@ const commands = {
displayName: 'display-name',
email: 'email',
hangup: 'video-hangup',
password: 'password',
subject: 'subject',
submitFeedback: 'submit-feedback',
toggleAudio: 'toggle-audio',
@@ -63,9 +62,7 @@ const events = {
'mic-error': 'micError',
'outgoing-message': 'outgoingMessage',
'participant-joined': 'participantJoined',
'participant-kicked-out': 'participantKickedOut',
'participant-left': 'participantLeft',
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
@@ -74,7 +71,6 @@ const events = {
'video-mute-status-changed': 'videoMuteStatusChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged'
};
@@ -539,7 +535,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* {{
* on: on //whether screen sharing is on
* }}
* {@code suspendDetected} - receives event notifications about detecting suspend event in host computer.
* {@code readyToClose} - all hangup operations are completed and Jitsi Meet
* is ready to be disposed.
* @returns {void}
@@ -562,7 +557,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
this.emit('_willDispose');
this._transport.dispose();
this.removeAllListeners();
if (this._frame && this._frame.parentNode) {
if (this._frame) {
this._frame.parentNode.removeChild(this._frame);
}
}

View File

@@ -99,6 +99,17 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Notify user that he has been kicked from the server.
*/
UI.notifyKicked = function() {
messageHandler.showError({
hideErrorSupportLink: true,
descriptionKey: 'dialog.kickMessage',
titleKey: 'dialog.kickTitle'
});
};
/**
* Notify user that conference was destroyed.
* @param reason {string} the reason text
@@ -229,11 +240,22 @@ UI.unbindEvents = () => {
};
/**
* Show local video stream on UI.
* Show local stream on UI.
* @param {JitsiTrack} track stream to show
*/
UI.addLocalVideoStream = track => {
VideoLayout.changeLocalVideo(track);
UI.addLocalStream = track => {
switch (track.getType()) {
case 'audio':
// Local audio is not rendered so no further action is needed at this
// point.
break;
case 'video':
VideoLayout.changeLocalVideo(track);
break;
default:
logger.error(`Unknown stream type: ${track.getType()}`);
break;
}
};
/**
@@ -499,8 +521,8 @@ UI.dockToolbar = dock => APP.store.dispatch(dockToolbox(dock));
* @param {string} avatarURL - The URL to avatar image to display.
* @returns {void}
*/
UI.refreshAvatarDisplay = function(id) {
VideoLayout.changeUserAvatar(id);
UI.refreshAvatarDisplay = function(id, avatarURL) {
VideoLayout.changeUserAvatar(id, avatarURL);
};
/**
@@ -709,6 +731,24 @@ UI.showExtensionInlineInstallationDialog = function(callback) {
});
};
/**
* Shows error dialog that informs the user that no data is received from the
* device.
*
* @param {boolean} isAudioTrack - Whether or not the dialog is for an audio
* track error.
* @returns {void}
*/
UI.showTrackNotWorkingDialog = function(isAudioTrack) {
messageHandler.showError({
descriptionKey: isAudioTrack
? 'dialog.micNotSendingData' : 'dialog.cameraNotSendingData',
titleKey: isAudioTrack
? 'dialog.micNotSendingDataTitle'
: 'dialog.cameraNotSendingDataTitle'
});
};
UI.updateDevicesAvailability = function(id, devices) {
VideoLayout.setDeviceAvailabilityIcons(id, devices);
};

View File

@@ -22,6 +22,7 @@ export default function SharedVideoThumb(participant, videoType, VideoLayout) {
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
this.container.ondblclick = this._onContainerDoubleClick;
}
SharedVideoThumb.prototype = Object.create(SmallVideo.prototype);
SharedVideoThumb.prototype.constructor = SharedVideoThumb;
@@ -33,7 +34,7 @@ SharedVideoThumb.prototype.constructor = SharedVideoThumb;
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
// eslint-disable-next-line no-empty-function
SharedVideoThumb.prototype.initializeAvatar = function() {};
SharedVideoThumb.prototype.avatarChanged = function() {};
SharedVideoThumb.prototype.createContainer = function(spanId) {
const container = document.createElement('span');

View File

@@ -5,8 +5,11 @@ import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { Avatar } from '../../../react/features/base/avatar';
import { i18next } from '../../../react/features/base/i18n';
import {
Avatar,
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { PresenceLabel } from '../../../react/features/presence-status';
/* eslint-enable no-unused-vars */
@@ -211,7 +214,8 @@ export default class LargeVideoManager {
container.setStream(id, stream, videoType);
// change the avatar url on large
this.updateAvatar();
this.updateAvatar(
getAvatarURLByParticipantId(APP.store.getState(), id));
// If the user's connection is disrupted then the avatar will be
// displayed in case we have no video image cached. That is if
@@ -402,16 +406,18 @@ export default class LargeVideoManager {
/**
* Updates the src of the dominant speaker avatar
*/
updateAvatar() {
ReactDOM.render(
<Provider store = { APP.store }>
updateAvatar(avatarUrl) {
if (avatarUrl) {
ReactDOM.render(
<Avatar
id = "dominantSpeakerAvatar"
participantId = { this.id }
size = { 200 } />
</Provider>,
this._dominantSpeakerAvatarContainer
);
uri = { avatarUrl } />,
this._dominantSpeakerAvatarContainer
);
} else {
ReactDOM.unmountComponentAtNode(
this._dominantSpeakerAvatarContainer);
}
}
/**

View File

@@ -7,6 +7,9 @@ import { Provider } from 'react-redux';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import {
getAvatarURLByParticipantId
} from '../../../react/features/base/participants';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
@@ -52,12 +55,14 @@ function LocalVideo(VideoLayout, emitter, streamEndedCallback) {
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.avatarChanged(
getAvatarURLByParticipantId(APP.store.getState(), this.id));
this.addAudioLevelIndicator();
this.updateIndicators();
this.container.onclick = this._onContainerClick;
this.container.ondblclick = this._onContainerDoubleClick;
}
LocalVideo.prototype = Object.create(SmallVideo.prototype);

View File

@@ -89,6 +89,7 @@ function RemoteVideo(user, VideoLayout, emitter) {
this._stopRemoteControl = this._stopRemoteControl.bind(this);
this.container.onclick = this._onContainerClick;
this.container.ondblclick = this._onContainerDoubleClick;
}
RemoteVideo.prototype = Object.create(SmallVideo.prototype);
@@ -164,10 +165,7 @@ RemoteVideo.prototype._generatePopupContent = function() {
const initialVolumeValue
= this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange = APP.store.getState()['features/base/config'].startSilent
? undefined : this._setAudioVolume;
const onVolumeChange = this._setAudioVolume;
const { isModerator } = APP.conference;
const participantID = this.id;

View File

@@ -10,8 +10,9 @@ import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { AudioLevelIndicator }
from '../../../react/features/audio-level-indicator';
import { Avatar as AvatarDisplay } from '../../../react/features/base/avatar';
import {
Avatar as AvatarDisplay,
getAvatarURLByParticipantId,
getPinnedParticipant,
pinParticipant
} from '../../../react/features/base/participants';
@@ -143,6 +144,7 @@ function SmallVideo(VideoLayout) {
this.updateView = this.updateView.bind(this);
this._onContainerClick = this._onContainerClick.bind(this);
this._onContainerDoubleClick = this._onContainerDoubleClick.bind(this);
}
/**
@@ -569,7 +571,8 @@ SmallVideo.prototype.updateView = function() {
if (!this.hasAvatar) {
if (this.id) {
// Init avatar
this.initializeAvatar();
this.avatarChanged(
getAvatarURLByParticipantId(APP.store.getState(), this.id));
} else {
logger.error('Unable to init avatar - no id', this);
@@ -607,22 +610,19 @@ SmallVideo.prototype.updateView = function() {
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @param {string} avatarUrl - The uri to the avatar image.
* @returns {void}
*/
SmallVideo.prototype.initializeAvatar = function() {
SmallVideo.prototype.avatarChanged = function(avatarUrl) {
const thumbnail = this.$avatar().get(0);
this.hasAvatar = true;
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
<AvatarDisplay
className = 'userAvatar'
uri = { avatarUrl } />,
thumbnail
);
}
@@ -859,6 +859,20 @@ SmallVideo.prototype.updateIndicators = function() {
);
};
/**
* Callback invoked when the thumbnail is double clicked. Will pin the
* participant if in tile view.
*
* @param {MouseEvent} event - The click event to intercept.
* @private
* @returns {void}
*/
SmallVideo.prototype._onContainerDoubleClick = function(event) {
if (this._pinningRequiresDoubleClick() && this._shouldTriggerPin(event)) {
APP.store.dispatch(pinParticipant(this.id));
}
};
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
@@ -868,7 +882,8 @@ SmallVideo.prototype.updateIndicators = function() {
* @returns {void}
*/
SmallVideo.prototype._onContainerClick = function(event) {
const triggerPin = this._shouldTriggerPin(event);
const triggerPin = this._shouldTriggerPin(event)
&& !this._pinningRequiresDoubleClick();
if (event.stopPropagation && triggerPin) {
event.stopPropagation();
@@ -919,6 +934,17 @@ SmallVideo.prototype.togglePin = function() {
APP.store.dispatch(pinParticipant(participantIdToPin));
};
/**
* Returns whether or not clicking to pin the participant needs to be a double
* click instead of a single click.
*
* @private
* @returns {boolean}
*/
SmallVideo.prototype._pinningRequiresDoubleClick = function() {
return shouldDisplayTileView(APP.store.getState());
};
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.

View File

@@ -53,6 +53,16 @@ function onLocalFlipXChanged(val) {
}
}
/**
* Returns the redux representation of all known users.
*
* @private
* @returns {Array}
*/
function getAllParticipants() {
return APP.store.getState()['features/base/participants'];
}
/**
* Returns an array of all thumbnails in the filmstrip.
*
@@ -76,6 +86,43 @@ function getLocalParticipant() {
return getLocalParticipantFromStore(APP.store.getState());
}
/**
* Returns the user ID of the remote participant that is current the dominant
* speaker.
*
* @private
* @returns {string|null}
*/
function getCurrentRemoteDominantSpeakerID() {
const dominantSpeaker = getAllParticipants()
.find(participant => participant.dominantSpeaker);
if (dominantSpeaker) {
return dominantSpeaker.local ? null : dominantSpeaker.id;
}
return null;
}
/**
* Returns the corresponding resource id to the given peer container
* DOM element.
*
* @return the corresponding resource id to the given peer container
* DOM element
*/
function getPeerContainerResourceId(containerElement) {
if (localVideoThumbnail.container === containerElement) {
return localVideoThumbnail.id;
}
const i = containerElement.id.indexOf('participant_');
if (i >= 0) {
return containerElement.id.substring(i + 12);
}
}
const VideoLayout = {
init(emitter) {
eventEmitter = emitter;
@@ -161,6 +208,10 @@ const VideoLayout = {
* and setting them assume the id is already set.
*/
mucJoined() {
if (largeVideo && !largeVideo.id) {
this.updateLargeVideo(getLocalParticipant().id, true);
}
// FIXME: replace this call with a generic update call once SmallVideo
// only contains a ReactElement. Then remove this call once the
// Filmstrip is fully in React.
@@ -196,6 +247,79 @@ const VideoLayout = {
localVideoThumbnail.setVisible(visible);
},
/**
* Checks if removed video is currently displayed and tries to display
* another one instead.
* Uses focusedID if any or dominantSpeakerID if any,
* otherwise elects new video, in this order.
*/
_updateAfterThumbRemoved(id) {
// Always trigger an update if large video is empty.
if (!largeVideo
|| (this.getLargeVideoID() && !this.isCurrentlyOnLarge(id))) {
return;
}
const pinnedId = this.getPinnedId();
let newId;
if (pinnedId) {
newId = pinnedId;
} else if (getCurrentRemoteDominantSpeakerID()) {
newId = getCurrentRemoteDominantSpeakerID();
} else { // Otherwise select last visible video
newId = this.electLastVisibleVideo();
}
this.updateLargeVideo(newId);
},
electLastVisibleVideo() {
// pick the last visible video in the row
// if nobody else is left, this picks the local video
const remoteThumbs = Filmstrip.getThumbs(true).remoteThumbs;
let thumbs = remoteThumbs.filter('[id!="mixedstream"]');
const lastVisible = thumbs.filter(':visible:last');
if (lastVisible.length) {
const id = getPeerContainerResourceId(lastVisible[0]);
if (remoteVideos[id]) {
logger.info(`electLastVisibleVideo: ${id}`);
return id;
}
// The RemoteVideo was removed (but the DOM elements may still
// exist).
}
logger.info('Last visible video no longer exists');
thumbs = Filmstrip.getThumbs().remoteThumbs;
if (thumbs.length) {
const id = getPeerContainerResourceId(thumbs[0]);
if (remoteVideos[id]) {
logger.info(`electLastVisibleVideo: ${id}`);
return id;
}
// The RemoteVideo was removed (but the DOM elements may
// still exist).
}
// Go with local video
logger.info('Fallback to local video...');
const { id } = getLocalParticipant();
logger.info(`electLastVisibleVideo: ${id}`);
return id;
},
onRemoteStreamAdded(stream) {
const id = stream.getParticipantId();
const remoteVideo = remoteVideos[id];
@@ -299,6 +423,23 @@ const VideoLayout = {
getAllThumbnails().forEach(thumbnail =>
thumbnail.focus(pinnedParticipantID === thumbnail.getId()));
if (pinnedParticipantID) {
this.updateLargeVideo(pinnedParticipantID);
} else {
const currentDominantSpeakerID
= getCurrentRemoteDominantSpeakerID();
if (currentDominantSpeakerID) {
this.updateLargeVideo(currentDominantSpeakerID);
} else {
// if there is no currentDominantSpeakerID, it can also be
// that local participant is the dominant speaker
// we should act as a participant has left and was on large
// and we should choose somebody (electLastVisibleVideo)
this.updateLargeVideo(this.electLastVisibleVideo());
}
}
},
/**
@@ -332,6 +473,19 @@ const VideoLayout = {
this.updateMutedForNoTracks(id, 'audio');
this.updateMutedForNoTracks(id, 'video');
const remoteVideosCount = Object.keys(remoteVideos).length;
if (remoteVideosCount === 1) {
window.setTimeout(() => {
const updatedRemoteVideosCount
= Object.keys(remoteVideos).length;
if (updatedRemoteVideosCount === 1 && remoteVideos[id]) {
this._maybePlaceParticipantOnLargeVideo(id);
}
}, 3000);
}
},
/**
@@ -358,14 +512,43 @@ const VideoLayout = {
// FIXME: what does this do???
remoteVideoActive(videoElement, resourceJid) {
logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails(
false, () => {
if (videoElement) {
$(videoElement).show();
}
});
this._updateLargeVideoIfDisplayed(resourceJid, true);
this._maybePlaceParticipantOnLargeVideo(resourceJid);
},
/**
* Update the large video to the last added video only if there's no current
* dominant, focused speaker or update it to the current dominant speaker.
*
* @params {string} resourceJid - The id of the user to maybe display on
* large video.
* @returns {void}
*/
_maybePlaceParticipantOnLargeVideo(resourceJid) {
const pinnedId = this.getPinnedId();
if ((!pinnedId
&& !getCurrentRemoteDominantSpeakerID()
&& this.isLargeContainerTypeVisible(VIDEO_CONTAINER_TYPE))
|| pinnedId === resourceJid
|| (!pinnedId && resourceJid
&& getCurrentRemoteDominantSpeakerID() === resourceJid)
/* Playback started while we're on the stage - may need to update
video source with the new stream */
|| this.isCurrentlyOnLarge(resourceJid)) {
this.updateLargeVideo(resourceJid, true);
}
},
/**
@@ -454,7 +637,7 @@ const VideoLayout = {
*/
onVideoMute(id, value) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail && localVideoThumbnail.setVideoMutedView(value);
localVideoThumbnail.setVideoMutedView(value);
} else {
const remoteVideo = remoteVideos[id];
@@ -463,8 +646,10 @@ const VideoLayout = {
}
}
// large video will show avatar instead of muted stream
this._updateLargeVideoIfDisplayed(id, true);
if (this.isCurrentlyOnLarge(id)) {
// large video will show avatar instead of muted stream
this.updateLargeVideo(id, true);
}
},
/**
@@ -492,6 +677,18 @@ const VideoLayout = {
onDominantSpeakerChanged(id) {
getAllThumbnails().forEach(thumbnail =>
thumbnail.showDominantSpeakerIndicator(id === thumbnail.getId()));
if (!remoteVideos[id]) {
return;
}
// Local video will not have container found, but that's ok
// since we don't want to switch to local video.
if (!interfaceConfig.filmStripOnly && !this.getPinnedId()
&& !this.getCurrentlyOnLargeContainer().stayOnStage()) {
this.updateLargeVideo(id);
}
},
/**
@@ -561,7 +758,9 @@ const VideoLayout = {
if (remoteVideo) {
remoteVideo.updateView();
this._updateLargeVideoIfDisplayed(id);
if (remoteVideo.isCurrentlyOnLargeVideo()) {
this.updateLargeVideo(id);
}
}
},
@@ -610,6 +809,7 @@ const VideoLayout = {
}
VideoLayout.resizeThumbnails();
VideoLayout._updateAfterThumbRemoved(id);
},
onVideoTypeChanged(id, newVideoType) {
@@ -635,7 +835,9 @@ const VideoLayout = {
}
smallVideo.setVideoType(newVideoType);
this._updateLargeVideoIfDisplayed(id, true);
if (this.isCurrentlyOnLarge(id)) {
this.updateLargeVideo(id, true);
}
},
/**
@@ -678,7 +880,7 @@ const VideoLayout = {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.initializeAvatar();
smallVideo.avatarChanged(avatarUrl);
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`
@@ -783,7 +985,7 @@ const VideoLayout = {
} else if (currentId) {
const currentSmallVideo = this.getSmallVideo(currentId);
currentSmallVideo && currentSmallVideo.updateView();
currentSmallVideo.updateView();
}
},

207
package-lock.json generated
View File

@@ -2563,102 +2563,6 @@
"component-url": "^0.2.1"
}
},
"@tensorflow-models/body-pix": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@tensorflow-models/body-pix/-/body-pix-1.1.1.tgz",
"integrity": "sha512-l9bd+b3QI7OzJjw/OuhEfeGRb5l2lRivgDHGMvQbT2Snn8nV7odHSRW55NzhU7Khl7vga00TWo5QDuVnkevQmQ=="
},
"@tensorflow/tfjs": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-1.2.2.tgz",
"integrity": "sha512-HfhSzL2eTWhlT0r/A5wmo+u3bHe+an16p5wsnFH3ujn21fQ8QtGpSfDHQZjWx1kVFaQnV6KBG+17MOrRHoHlLA==",
"requires": {
"@tensorflow/tfjs-converter": "1.2.2",
"@tensorflow/tfjs-core": "1.2.2",
"@tensorflow/tfjs-data": "1.2.2",
"@tensorflow/tfjs-layers": "1.2.2"
}
},
"@tensorflow/tfjs-converter": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.2.2.tgz",
"integrity": "sha512-NM2NcPRHpCNeJdBxHcYpmW9ZHTQ2lJFJgmgGpQ8CxSC9CtQB05bFONs3SKcwMNDE/69QBRVom5DYqLCVUg+A+g=="
},
"@tensorflow/tfjs-core": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.2.2.tgz",
"integrity": "sha512-2hCHMKjh3UNpLEjbAEaurrTGJyj/KpLtMSAraWgHA1vGY0kmk50BBSbgCDmXWUVm7lyh/SkCq4/GrGDZktEs3g==",
"requires": {
"@types/offscreencanvas": "~2019.3.0",
"@types/seedrandom": "2.4.27",
"@types/webgl-ext": "0.0.30",
"@types/webgl2": "0.0.4",
"node-fetch": "~2.1.2",
"rollup-plugin-visualizer": "~1.1.1",
"seedrandom": "2.4.3"
},
"dependencies": {
"node-fetch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
"integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
}
}
},
"@tensorflow/tfjs-data": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-1.2.2.tgz",
"integrity": "sha512-oHGBoGdnCl2RyouLKplQqo+iil0iJgPbi/aoHizhpO77UBuJXlKMblH8w5GbxVAw3hKxWlqzYpxPo6rVRgehNA==",
"requires": {
"@types/node-fetch": "^2.1.2",
"node-fetch": "~2.1.2"
},
"dependencies": {
"node-fetch": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
"integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
}
}
},
"@tensorflow/tfjs-layers": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-1.2.2.tgz",
"integrity": "sha512-yzWZaZrCVpEyTkSrzMe4OOP4aGUfaaROE/zR9fPsPGGF8wLlbLNZUJjeYUmjy3G3pXGaM0mQUbLR5Vd707CVtQ=="
},
"@types/node": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
"integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ=="
},
"@types/node-fetch": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.3.7.tgz",
"integrity": "sha512-+bKtuxhj/TYSSP1r4CZhfmyA0vm/aDRQNo7vbAgf6/cZajn0SAniGGST07yvI4Q+q169WTa2/x9gEHfJrkcALw==",
"requires": {
"@types/node": "*"
}
},
"@types/offscreencanvas": {
"version": "2019.3.0",
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="
},
"@types/seedrandom": {
"version": "2.4.27",
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz",
"integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE="
},
"@types/webgl-ext": {
"version": "0.0.30",
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
"integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg=="
},
"@types/webgl2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz",
"integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw=="
},
"@webassemblyjs/ast": {
"version": "1.7.11",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz",
@@ -2936,7 +2840,7 @@
"blueimp-md5": "^2.10.0",
"json3": "^3.3.2",
"lodash": "^4.17.4",
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f1"
"ua-parser-js": "github:amplitude/ua-parser-js#ed538f16f5c6ecd8357da989b617d4f156dcf35d"
},
"dependencies": {
"ua-parser-js": {
@@ -6846,8 +6750,7 @@
},
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"aproba": {
"version": "1.2.0",
@@ -6865,13 +6768,11 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"optional": true
"bundled": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6884,18 +6785,15 @@
},
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"optional": true
"bundled": true
},
"core-util-is": {
"version": "1.0.2",
@@ -6998,8 +6896,7 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true,
"optional": true
"bundled": true
},
"ini": {
"version": "1.3.5",
@@ -7009,7 +6906,6 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -7022,20 +6918,17 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
"bundled": true,
"optional": true
"bundled": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -7052,7 +6945,6 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -7125,8 +7017,7 @@
},
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"optional": true
"bundled": true
},
"object-assign": {
"version": "4.1.1",
@@ -7136,7 +7027,6 @@
"once": {
"version": "1.4.0",
"bundled": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -7212,8 +7102,7 @@
},
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"optional": true
"bundled": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -7243,7 +7132,6 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -7261,7 +7149,6 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -7300,13 +7187,11 @@
},
"wrappy": {
"version": "1.0.2",
"bundled": true,
"optional": true
"bundled": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"optional": true
"bundled": true
}
}
},
@@ -8834,8 +8719,8 @@
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
},
"js-utils": {
"version": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"from": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"version": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"from": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"requires": {
"bowser": "1.9.1",
"js-md5": "0.7.3"
@@ -9042,8 +8927,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#8a4d1f1b085131aca3cec5513fa7995cf7508fc2",
"from": "github:jitsi/lib-jitsi-meet#8a4d1f1b085131aca3cec5513fa7995cf7508fc2",
"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",
@@ -12309,10 +12194,15 @@
"jssha": "^2.2.0"
}
},
"react-native-fast-image": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-5.1.1.tgz",
"integrity": "sha512-kEzgZxbbXYhy27u5GnhrKitn+XDBFAHSDUJdYC6llMi5cDPjgcqhOAQABj0K+ga5pn+/xPZLmD882rrUGiwVVA=="
},
"react-native-google-signin": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-2.0.0.tgz",
"integrity": "sha512-9loM4lcCIdbco5BnmNio7yGaXQKCpCaY1VRmYiTSvC5NjuSf6Ui6jZRee46p/YdaU4yRnS3u5Vct6Psrvr0HNg=="
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-native-google-signin/-/react-native-google-signin-1.0.2.tgz",
"integrity": "sha512-4HPPSecI29gX0Pu7h2E7ZYXnKO4r+6eh5f+Unm67liE1RfvCQfOqoDliPbK96Mb/91VgHwqyxi0sUEC4j54/AQ=="
},
"react-native-immersive": {
"version": "2.0.0",
@@ -12391,8 +12281,8 @@
"integrity": "sha512-l3Quzbb+qa4in2U5RSt/lT0/pHrIpEChT1NnqrVAAXNrjkXjVOsxduaaEDdDhTzNJQEm/PcAcoyrFmgvGOohxw=="
},
"react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#fb4c734d1964d58310175677f6693f3854bb2eaa",
"from": "github:jitsi/react-native-webrtc#fb4c734d1964d58310175677f6693f3854bb2eaa",
"version": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
"from": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
"requires": {
"base64-js": "^1.1.2",
"event-target-shim": "^1.0.5",
@@ -13278,35 +13168,6 @@
"inherits": "^2.0.1"
}
},
"rollup-plugin-visualizer": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-1.1.1.tgz",
"integrity": "sha512-7xkSKp+dyJmSC7jg2LXqViaHuOnF1VvIFCnsZEKjrgT5ZVyiLLSbeszxFcQSfNJILphqgAEmWAUz0Z4xYScrRw==",
"optional": true,
"requires": {
"mkdirp": "^0.5.1",
"opn": "^5.4.0",
"source-map": "^0.7.3",
"typeface-oswald": "0.0.54"
},
"dependencies": {
"opn": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz",
"integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==",
"optional": true,
"requires": {
"is-wsl": "^1.1.0"
}
},
"source-map": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz",
"integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==",
"optional": true
}
}
},
"rsvp": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz",
@@ -13895,11 +13756,6 @@
"resolved": "https://registry.npmjs.org/sdp-transform/-/sdp-transform-2.3.0.tgz",
"integrity": "sha1-V6lXWUIEHYV3qGnXx01MOgvYiPY="
},
"seedrandom": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -15488,12 +15344,6 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typeface-oswald": {
"version": "0.0.54",
"resolved": "https://registry.npmjs.org/typeface-oswald/-/typeface-oswald-0.0.54.tgz",
"integrity": "sha512-U1WMNp4qfy4/3khIfHMVAIKnNu941MXUfs3+H9R8PFgnoz42Hh9pboSFztWr86zut0eXC8byalmVhfkiKON/8Q==",
"optional": true
},
"ua-parser-js": {
"version": "0.7.17",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz",
@@ -16949,11 +16799,6 @@
"string-width": "^1.0.2 || 2"
}
},
"windows-iana": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/windows-iana/-/windows-iana-3.1.0.tgz",
"integrity": "sha512-rCPf3AakAAgvapnbYVvG2bQyI3g6EDbPpjDJ72fdAu+XTzB1qvX4ZC6OnZ0I2+thaspjTb+8KwdyhdBl8Lt/QA=="
},
"wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",

View File

@@ -35,8 +35,6 @@
"@atlaskit/tooltip": "12.1.13",
"@microsoft/microsoft-graph-client": "1.1.0",
"@react-native-community/async-storage": "1.3.4",
"@tensorflow-models/body-pix": "^1.0.1",
"@tensorflow/tfjs": "^1.1.2",
"@webcomponents/url": "0.7.1",
"amplitude-js": "4.5.2",
"bc-css-flags": "3.0.0",
@@ -51,10 +49,10 @@
"jquery-contextmenu": "2.4.5",
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#8a4d1f1b085131aca3cec5513fa7995cf7508fc2",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0ee30bf12a549d10bb5d559e19bd557c3ed179eb",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.11",
"moment": "2.19.4",
@@ -69,7 +67,8 @@
"react-native-background-timer": "2.1.1",
"react-native-calendar-events": "1.6.4",
"react-native-callstats": "3.58.2",
"react-native-google-signin": "2.0.0",
"react-native-fast-image": "5.1.1",
"react-native-google-signin": "1.0.2",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.3",
@@ -77,7 +76,7 @@
"react-native-swipeout": "2.3.6",
"react-native-vector-icons": "6.0.2",
"react-native-watch-connectivity": "0.2.0",
"react-native-webrtc": "github:jitsi/react-native-webrtc#fb4c734d1964d58310175677f6693f3854bb2eaa",
"react-native-webrtc": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
"react-native-webview": "5.8.1",
"react-redux": "5.0.7",
"react-textarea-autosize": "7.1.0",
@@ -86,7 +85,6 @@
"redux-thunk": "2.2.0",
"styled-components": "3.4.9",
"uuid": "3.1.0",
"windows-iana": "^3.1.0",
"xmldom": "0.1.27"
},
"devDependencies": {

View File

@@ -2,11 +2,6 @@
import React, { Component } from 'react';
// We need to reference these files directly to avoid loading things that are not available
// in this environment (e.g. JitsiMeetJS or interfaceConfig)
import StatelessAvatar from '../base/avatar/components/web/StatelessAvatar';
import { getAvatarColor, getInitials } from '../base/avatar/functions';
import Toolbar from './Toolbar';
const { api } = window.alwaysOnTop;
@@ -22,9 +17,7 @@ const TOOLBAR_TIMEOUT = 4000;
type State = {
avatarURL: string,
displayName: string,
formattedDisplayName: string,
isVideoDisplayed: boolean,
userID: string,
visible: boolean
};
@@ -49,9 +42,7 @@ export default class AlwaysOnTop extends Component<*, State> {
this.state = {
avatarURL: '',
displayName: '',
formattedDisplayName: '',
isVideoDisplayed: true,
userID: '',
visible: true
};
@@ -87,15 +78,10 @@ export default class AlwaysOnTop extends Component<*, State> {
*
* @returns {void}
*/
_displayNameChangedListener({ displayname, formattedDisplayName, id }) {
_displayNameChangedListener({ formattedDisplayName, id }) {
if (api._getOnStageParticipant() === id
&& (formattedDisplayName !== this.state.formattedDisplayName
|| displayname !== this.state.displayName)) {
// I think the API has a typo using lowercase n for the displayname
this.setState({
displayName: displayname,
formattedDisplayName
});
&& formattedDisplayName !== this.state.displayName) {
this.setState({ displayName: formattedDisplayName });
}
}
@@ -126,16 +112,13 @@ export default class AlwaysOnTop extends Component<*, State> {
_largeVideoChangedListener() {
const userID = api._getOnStageParticipant();
const avatarURL = api.getAvatarURL(userID);
const displayName = api.getDisplayName(userID);
const formattedDisplayName = api._getFormattedDisplayName(userID);
const displayName = api._getFormattedDisplayName(userID);
const isVideoDisplayed = Boolean(api._getLargeVideo());
this.setState({
avatarURL,
displayName,
formattedDisplayName,
isVideoDisplayed,
userID
isVideoDisplayed
});
}
@@ -178,7 +161,7 @@ export default class AlwaysOnTop extends Component<*, State> {
* @returns {ReactElement}
*/
_renderVideoNotAvailableScreen() {
const { avatarURL, displayName, formattedDisplayName, isVideoDisplayed, userID } = this.state;
const { avatarURL, displayName, isVideoDisplayed } = this.state;
if (isVideoDisplayed) {
return null;
@@ -186,17 +169,19 @@ export default class AlwaysOnTop extends Component<*, State> {
return (
<div id = 'videoNotAvailableScreen'>
<div id = 'avatarContainer'>
<StatelessAvatar
color = { getAvatarColor(userID) }
id = 'avatar'
initials = { getInitials(displayName) }
url = { avatarURL } />)
</div>
{
avatarURL
? <div id = 'avatarContainer'>
<img
id = 'avatar'
src = { avatarURL } />
</div>
: null
}
<div
className = 'displayname'
id = 'displayname'>
{ formattedDisplayName }
{ displayName }
</div>
</div>
);

View File

@@ -271,18 +271,6 @@ export function createInviteDialogEvent(
};
}
/**
* Creates an "offer/answer failure" event.
*
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createOfferAnswerFailedEvent() {
return {
action: 'offer.answer.failure'
};
}
/**
* Creates a "page reload" event.
*
@@ -467,21 +455,6 @@ export function createRemoteVideoMenuButtonEvent(buttonName, attributes) {
};
}
/**
* Creates an event indicating that an action related to video blur
* occurred (e.g. It was started or stopped).
*
* @param {string} action - The action which occurred.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createVideoBlurEvent(action) {
return {
action,
actionSubject: 'video.blur'
};
}
/**
* Creates an event indicating that an action related to screen sharing
* occurred (e.g. It was started or stopped).
@@ -578,18 +551,6 @@ export function createStartAudioOnlyEvent(audioOnly) {
};
}
/**
* Creates an event which indicates the "start silent" configuration.
*
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createStartSilentEvent() {
return {
action: 'start.silent'
};
}
/**
* Creates an event which indicates the "start muted" configuration.
*

View File

@@ -13,18 +13,10 @@ import {
import { connect, disconnect, setLocationURL } from '../base/connection';
import { loadConfig } from '../base/lib-jitsi-meet';
import { createDesiredLocalTracks } from '../base/tracks';
import {
getLocationContextRoot,
parseURIString,
toURLString
} from '../base/util';
import { showNotification } from '../notifications';
import { parseURIString, toURLString } from '../base/util';
import { setFatalError } from '../overlay';
import {
getDefaultURL,
getName
} from './functions';
import { getDefaultURL } from './functions';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -144,34 +136,6 @@ export function redirectWithStoredParams(pathname: string) {
};
}
/**
* Assigns a specific pathname to window.location.pathname taking into account
* the context root of the Web app.
*
* @param {string} pathname - The pathname to assign to
* window.location.pathname. If the specified pathname is relative, the context
* root of the Web app will be prepended to the specified pathname before
* assigning it to window.location.pathname.
* @returns {Function}
*/
export function redirectToStaticPage(pathname: string) {
return () => {
const windowLocation = window.location;
let newPathname = pathname;
if (!newPathname.startsWith('/')) {
// A pathname equal to ./ specifies the current directory. It will be
// fine but pointless to include it because contextRoot is the current
// directory.
newPathname.startsWith('./')
&& (newPathname = newPathname.substring(2));
newPathname = getLocationContextRoot(windowLocation) + newPathname;
}
windowLocation.pathname = newPathname;
};
}
/**
* Reloads the page.
*
@@ -218,58 +182,3 @@ export function reloadWithStoredParams() {
}
};
}
/**
* Check if the welcome page is enabled and redirects to it.
* If requested show a thank you dialog before that.
* If we have a close page enabled, redirect to it without
* showing any other dialog.
*
* @param {Object} options - Used to decide which particular close page to show
* or if close page is disabled, whether we should show the thankyou dialog.
* @param {boolean} options.showThankYou - Whether we should
* show thank you dialog.
* @param {boolean} options.feedbackSubmitted - Whether feedback was submitted.
* @returns {Function}
*/
export function maybeRedirectToWelcomePage(options: Object = {}) {
return (dispatch: Dispatch<any>, getState: Function) => {
const {
enableClosePage
} = getState()['features/base/config'];
// if close page is enabled redirect to it, without further action
if (enableClosePage) {
const { isGuest } = getState()['features/base/jwt'];
// save whether current user is guest or not, before navigating
// to close page
window.sessionStorage.setItem('guest', isGuest);
dispatch(redirectToStaticPage(`static/${
options.feedbackSubmitted ? 'close.html' : 'close2.html'}`));
return;
}
// else: show thankYou dialog only if there is no feedback
if (options.showThankYou) {
dispatch(showNotification({
titleArguments: { appName: getName() },
titleKey: 'dialog.thankYou'
}));
}
// if Welcome page is enabled redirect to welcome page after 3 sec, if
// there is a thank you message to be shown, 0.5s otherwise.
if (getState()['features/base/config'].enableWelcomePage) {
setTimeout(
() => {
dispatch(redirectWithStoredParams('/'));
},
options.showThankYou ? 3000 : 500);
}
};
}

View File

@@ -6,14 +6,12 @@ import '../../analytics';
import '../../authentication';
import { setColorScheme } from '../../base/color-scheme';
import { DialogContainer } from '../../base/dialog';
import { updateFlags } from '../../base/flags';
import '../../base/jwt';
import { Platform } from '../../base/react';
import {
AspectRatioDetector,
ReducedUIDetector
} from '../../base/responsive-ui';
import { updateSettings } from '../../base/settings';
import '../../google-api';
import '../../mobile/audio-mode';
import '../../mobile/background';
@@ -49,14 +47,18 @@ type Props = AbstractAppProps & {
externalAPIScope: string,
/**
* An object with the feature flags.
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar button
* is rendered in the {@link Conference} view to afford entering
* Picture-in-Picture.
*/
flags: Object,
pictureInPictureEnabled: boolean,
/**
* An object with user information (display name, email, avatar URL).
* Whether the Welcome page is enabled. If {@code true}, the Welcome page is
* rendered when the {@link App} is not at a location (URL) identifying
* a Jitsi Meet conference/room.
*/
userInfo: ?Object
welcomePageEnabled: boolean
};
/**
@@ -94,12 +96,9 @@ export class App extends AbstractApp {
super.componentDidMount();
this._init.then(() => {
// We set these early enough so then we avoid any unnecessary re-renders.
const { dispatch } = this.state.store;
dispatch(setColorScheme(this.props.colorScheme));
dispatch(updateFlags(this.props.flags));
dispatch(updateSettings(this.props.userInfo || {}));
// We set the color scheme early enough so then we avoid any
// unnecessary re-renders.
this.state.store.dispatch(setColorScheme(this.props.colorScheme));
});
}

View File

@@ -4,7 +4,6 @@ import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React from 'react';
import { DialogContainer } from '../../base/dialog';
import '../../base/user-interaction';
import '../../base/responsive-ui';
import '../../chat';
import '../../external-api';

View File

@@ -4,8 +4,9 @@ import { generateRoomWithoutSeparator } from 'js-utils/random';
import type { Component } from 'react';
import { isRoomValid } from '../base/conference';
import JitsiMeetJS from '../base/lib-jitsi-meet';
import { Platform } from '../base/react';
import { toState } from '../base/redux';
import { isSupportedBrowser } from '../base/environment';
import { Conference } from '../conference';
import { getDeepLinkingPage } from '../deep-linking';
import { UnsupportedDesktopBrowser } from '../unsupported-browser';
@@ -39,109 +40,69 @@ export type Route = {
*/
export function _getRouteToRender(stateful: Function | Object): Promise<Route> {
const state = toState(stateful);
if (navigator.product === 'ReactNative') {
return _getMobileRoute(state);
}
return _getWebConferenceRoute(state) || _getWebWelcomePageRoute(state);
}
/**
* Returns the {@code Route} to display on the React Native app.
*
* @param {Object} state - The redux state.
* @returns {Promise<Route>}
*/
function _getMobileRoute(state): Promise<Route> {
const route = _getEmptyRoute();
if (isRoomValid(state['features/base/conference'].room)) {
route.component = Conference;
} else if (isWelcomePageAppEnabled(state)) {
route.component = WelcomePage;
} else {
route.component = BlankPage;
}
return Promise.resolve(route);
}
/**
* Returns the {@code Route} to display when trying to access a conference if
* a valid conference is being joined.
*
* @param {Object} state - The redux state.
* @returns {Promise<Route>|undefined}
*/
function _getWebConferenceRoute(state): ?Promise<Route> {
if (!isRoomValid(state['features/base/conference'].room)) {
return;
}
const route = _getEmptyRoute();
// Update the location if it doesn't match. This happens when a room is
// joined from the welcome page. The reason for doing this instead of using
// the history API is that we want to load the config.js which takes the
// room into account.
const { locationURL } = state['features/base/connection'];
if (window.location.href !== locationURL.href) {
route.href = locationURL.href;
return Promise.resolve(route);
}
return getDeepLinkingPage(state)
.then(deepLinkComponent => {
if (deepLinkComponent) {
route.component = deepLinkComponent;
} else if (isSupportedBrowser()) {
route.component = Conference;
} else {
route.component = UnsupportedDesktopBrowser;
}
return route;
});
}
/**
* Returns the {@code Route} to display when trying to access the welcome page.
*
* @param {Object} state - The redux state.
* @returns {Promise<Route>}
*/
function _getWebWelcomePageRoute(state): Promise<Route> {
const route = _getEmptyRoute();
if (isWelcomePageUserEnabled(state)) {
if (isSupportedBrowser()) {
route.component = WelcomePage;
} else {
route.component = UnsupportedDesktopBrowser;
}
} else {
// Web: if the welcome page is disabled, go directly to a random room.
let href = window.location.href;
href.endsWith('/') || (href += '/');
route.href = href + generateRoomWithoutSeparator();
}
return Promise.resolve(route);
}
/**
* Returns the default {@code Route}.
*
* @returns {Route}
*/
function _getEmptyRoute(): Route {
return {
const { room } = state['features/base/conference'];
const isMobileApp = navigator.product === 'ReactNative';
const isMobileBrowser
= !isMobileApp && (Platform.OS === 'android' || Platform.OS === 'ios');
const route: Route = {
component: BlankPage,
href: undefined
};
return new Promise(resolve => {
// First, check if the current endpoint supports WebRTC. We are
// intentionally not performing the check for mobile browsers because:
// - the WelcomePage is mobile ready;
// - if the URL points to a conference, getDeepLinkingPage will take
// care of it.
if (!isMobileBrowser && !JitsiMeetJS.isWebRtcSupported()) {
route.component = UnsupportedDesktopBrowser;
resolve(route);
return;
}
if (isRoomValid(room)) {
if (isMobileApp) {
route.component = Conference;
resolve(route);
} else {
// Update the location if it doesn't match. This happens when a
// room is joined from the welcome page. The reason for doing
// this instead of using the history API is that we want to load
// the config.js which takes the room into account.
const { locationURL } = state['features/base/connection'];
// eslint-disable-next-line no-negated-condition
if (window.location.href !== locationURL.href) {
route.href = locationURL.href;
resolve(route);
} else {
// Maybe show deep-linking, otherwise go to Conference.
getDeepLinkingPage(state).then(component => {
route.component = component || Conference;
resolve(route);
});
}
}
return;
}
if (!isWelcomePageUserEnabled(state)) {
// Web: if the welcome page is disabled, go directly to a random
// room.
let href = window.location.href;
href.endsWith('/') || (href += '/');
route.href = href + generateRoomWithoutSeparator();
} else if (isWelcomePageAppEnabled(state)) {
// Mobile: only go to the welcome page if enabled.
route.component = WelcomePage;
}
resolve(route);
});
}

View File

@@ -1,49 +0,0 @@
// @flow
import { PureComponent } from 'react';
export type Props = {
/**
* Color of the (initials based) avatar, if needed.
*/
color?: string,
/**
* Initials to be used to render the initials based avatars.
*/
initials?: string,
/**
* Callback to signal the failure of the loading of the URL.
*/
onAvatarLoadError?: Function,
/**
* Expected size of the avatar.
*/
size?: number;
/**
* The URL of the avatar to render.
*/
url?: ?string
};
/**
* Implements an abstract stateless avatar component that renders an avatar purely from what gets passed through
* props.
*/
export default class AbstractStatelessAvatar<P: Props> extends PureComponent<P> {
/**
* Parses an icon out of a specially constructed icon URL and returns the icon name.
*
* @param {string?} url - The url to parse.
* @returns {string?}
*/
_parseIconUrl(url: ?string): ?string {
const match = url && url.match(/icon:\/\/(.+)/i);
return (match && match[1]) || undefined;
}
}

View File

@@ -1,190 +0,0 @@
// @flow
import React, { PureComponent } from 'react';
import { getParticipantById } from '../../participants';
import { connect } from '../../redux';
import { getAvatarColor, getInitials } from '../functions';
import { StatelessAvatar } from '.';
export type Props = {
/**
* The string we base the initials on (this is generated from a list of precendences).
*/
_initialsBase: ?string,
/**
* An URL that we validated that it can be loaded.
*/
_loadableAvatarUrl: ?string,
/**
* A prop to maintain compatibility with web.
*/
className?: string,
/**
* A string to override the initials to generate a color of. This is handy if you don't want to make
* the background color match the string that the initials are generated from.
*/
colorBase?: string,
/**
* Display name of the entity to render an avatar for (if any). This is handy when we need
* an avatar for a non-participasnt entity (e.g. a recent list item).
*/
displayName?: string,
/**
* ID of the element, if any.
*/
id?: string,
/**
* The ID of the participant to render an avatar for (if it's a participant avatar).
*/
participantId?: string,
/**
* The size of the avatar.
*/
size: number,
/**
* URL of the avatar, if any.
*/
url: ?string,
}
type State = {
avatarFailed: boolean
}
export const DEFAULT_SIZE = 65;
/**
* Implements a class to render avatars in the app.
*/
class Avatar<P: Props> extends PureComponent<P, State> {
/**
* Instantiates a new {@code Component}.
*
* @inheritdoc
*/
constructor(props: P) {
super(props);
this.state = {
avatarFailed: false
};
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
}
/**
* Implements {@code Component#componentDidUpdate}.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: P) {
if (prevProps.url !== this.props.url) {
// URI changed, so we need to try to fetch it again.
// Eslint doesn't like this statement, but based on the React doc, it's safe if it's
// wrapped in a condition: https://reactjs.org/docs/react-component.html#componentdidupdate
// eslint-disable-next-line react/no-did-update-set-state
this.setState({
avatarFailed: false
});
}
}
/**
* Implements {@code Componenr#render}.
*
* @inheritdoc
*/
render() {
const {
_initialsBase,
_loadableAvatarUrl,
className,
colorBase,
id,
size,
url
} = this.props;
const { avatarFailed } = this.state;
const avatarProps = {
className,
color: undefined,
id,
initials: undefined,
onAvatarLoadError: undefined,
size,
url: undefined
};
// _loadableAvatarUrl is validated that it can be loaded, but uri (if present) is not, so
// we still need to do a check for that. And an explicitly provided URI is higher priority than
// an avatar URL anyhow.
const effectiveURL = (!avatarFailed && url) || _loadableAvatarUrl;
if (effectiveURL) {
avatarProps.onAvatarLoadError = this._onAvatarLoadError;
avatarProps.url = effectiveURL;
}
const initials = getInitials(_initialsBase);
if (initials) {
avatarProps.color = getAvatarColor(colorBase || _initialsBase);
avatarProps.initials = initials;
}
return (
<StatelessAvatar
{ ...avatarProps } />
);
}
_onAvatarLoadError: () => void;
/**
* Callback to handle the error while loading of the avatar URI.
*
* @returns {void}
*/
_onAvatarLoadError() {
this.setState({
avatarFailed: true
});
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {Props}
*/
export function _mapStateToProps(state: Object, ownProps: Props) {
const { colorBase, displayName, participantId, url } = ownProps;
const _participant = participantId && getParticipantById(state, participantId);
const _initialsBase = (_participant && _participant.name) || displayName;
return {
_initialsBase,
_loadableAvatarUrl: _participant && _participant.loadableAvatarUrl,
colorBase: !colorBase && _participant ? _participant.id : colorBase,
url: !url && _participant && _participant.isJigasi ? 'icon://phone' : url
};
}
export default connect(_mapStateToProps)(Avatar);

View File

@@ -1,4 +0,0 @@
// @flow
export * from './native';
export { default as Avatar } from './Avatar';

View File

@@ -1,4 +0,0 @@
// @flow
export * from './web';
export { default as Avatar } from './Avatar';

View File

@@ -1,143 +0,0 @@
// @flow
import React from 'react';
import { Image, Text, View } from 'react-native';
import { type StyleType } from '../../../styles';
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
import styles from './styles';
import { Icon } from '../../../font-icons';
type Props = AbstractProps & {
/**
* External style passed to the componant.
*/
style?: StyleType
};
const DEFAULT_AVATAR = require('../../../../../../images/avatar.png');
/**
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
* props.
*/
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { initials, size, style, url } = this.props;
let avatar;
const icon = this._parseIconUrl(url);
if (icon) {
avatar = this._renderIconAvatar(icon);
} else if (url) {
avatar = this._renderURLAvatar();
} else if (initials) {
avatar = this._renderInitialsAvatar();
} else {
avatar = this._renderDefaultAvatar();
}
return (
<View
style = { [
styles.avatarContainer(size),
style
] }>
{ avatar }
</View>
);
}
_parseIconUrl: ?string => ?string
/**
* Renders the default avatar.
*
* @returns {React$Element<*>}
*/
_renderDefaultAvatar() {
const { size } = this.props;
return (
<Image
source = { DEFAULT_AVATAR }
style = { [
styles.avatarContent(size),
styles.staticAvatar
] } />
);
}
/**
* Renders the initials-based avatar.
*
* @param {string} icon - The icon name to render.
* @returns {React$Element<*>}
*/
_renderIconAvatar(icon) {
const { color, size } = this.props;
return (
<View
style = { [
styles.initialsContainer,
{
backgroundColor: color
}
] }>
<Icon
name = { icon }
style = { styles.initialsText(size) } />
</View>
);
}
/**
* Renders the initials-based avatar.
*
* @returns {React$Element<*>}
*/
_renderInitialsAvatar() {
const { color, initials, size } = this.props;
return (
<View
style = { [
styles.initialsContainer,
{
backgroundColor: color
}
] }>
<Text style = { styles.initialsText(size) }> { initials } </Text>
</View>
);
}
/**
* Renders the url-based avatar.
*
* @returns {React$Element<*>}
*/
_renderURLAvatar() {
const { onAvatarLoadError, size, url } = this.props;
return (
<Image
defaultSource = { DEFAULT_AVATAR }
onError = { onAvatarLoadError }
resizeMode = 'cover'
source = {{ uri: url }}
style = { styles.avatarContent(size) } />
);
}
}

View File

@@ -1,3 +0,0 @@
// @flow
export { default as StatelessAvatar } from './StatelessAvatar';

View File

@@ -1,49 +0,0 @@
// @flow
import { ColorPalette } from '../../../styles';
const DEFAULT_SIZE = 65;
/**
* The styles of the feature base/participants.
*/
export default {
avatarContainer: (size: number = DEFAULT_SIZE) => {
return {
alignItems: 'center',
borderRadius: size / 2,
height: size,
justifyContent: 'center',
overflow: 'hidden',
width: size
};
},
avatarContent: (size: number = DEFAULT_SIZE) => {
return {
height: size,
width: size
};
},
initialsContainer: {
alignItems: 'center',
alignSelf: 'stretch',
flex: 1,
justifyContent: 'center'
},
initialsText: (size: number = DEFAULT_SIZE) => {
return {
color: 'rgba(255, 255, 255, 0.6)',
fontSize: size * 0.45,
fontWeight: '100'
};
},
staticAvatar: {
backgroundColor: ColorPalette.lightGrey,
opacity: 0.4
}
};

View File

@@ -1,123 +0,0 @@
// @flow
import React from 'react';
import AbstractStatelessAvatar, { type Props as AbstractProps } from '../AbstractStatelessAvatar';
type Props = AbstractProps & {
/**
* External class name passed through props.
*/
className?: string,
/**
* The default avatar URL if we want to override the app bundled one (e.g. AlwaysOnTop)
*/
defaultAvatar?: string,
/**
* ID of the component to be rendered.
*/
id?: string
};
/**
* Implements a stateless avatar component that renders an avatar purely from what gets passed through
* props.
*/
export default class StatelessAvatar extends AbstractStatelessAvatar<Props> {
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
render() {
const { initials, url } = this.props;
const icon = this._parseIconUrl(url);
if (icon) {
return (
<div
className = { this._getAvatarClassName() }
id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }>
<i className = { `icon-${icon}` } />
</div>
);
}
if (url) {
return (
<img
className = { this._getAvatarClassName() }
id = { this.props.id }
onError = { this.props.onAvatarLoadError }
src = { url }
style = { this._getAvatarStyle() } />
);
}
if (initials) {
return (
<div
className = { this._getAvatarClassName() }
id = { this.props.id }
style = { this._getAvatarStyle(this.props.color) }>
<svg
className = 'avatar-svg'
viewBox = '0 0 100 100'
xmlns = 'http://www.w3.org/2000/svg'
xmlnsXlink = 'http://www.w3.org/1999/xlink'>
<foreignObject
height = '100%'
width = '100%'>
<span
className = 'avatar-foreign'>
{ initials }
</span>
</foreignObject>
</svg>
</div>
);
}
// default avatar
return (
<img
className = { this._getAvatarClassName('defaultAvatar') }
id = { this.props.id }
src = { this.props.defaultAvatar || 'images/avatar.png' }
style = { this._getAvatarStyle() } />
);
}
/**
* Constructs a style object to be used on the avatars.
*
* @param {string?} color - The desired background color.
* @returns {Object}
*/
_getAvatarStyle(color) {
const { size } = this.props;
return {
backgroundColor: color || undefined,
fontSize: size ? size * 0.5 : '180%',
height: size || '100%',
width: size || '100%'
};
}
/**
* Constructs a list of class names required for the avatar component.
*
* @param {string} additional - Any additional class to add.
* @returns {string}
*/
_getAvatarClassName(additional) {
return `avatar ${additional || ''} ${this.props.className || ''}`;
}
_parseIconUrl: ?string => ?string
}

View File

@@ -1,3 +0,0 @@
// @flow
export { default as StatelessAvatar } from './StatelessAvatar';

View File

@@ -1,54 +0,0 @@
// @flow
import _ from 'lodash';
const AVATAR_COLORS = [
'232, 105, 156',
'255, 198, 115',
'128, 128, 255',
'105, 232, 194',
'234, 255, 128'
];
const AVATAR_OPACITY = 0.4;
/**
* Generates the background color of an initials based avatar.
*
* @param {string?} initials - The initials of the avatar.
* @returns {string}
*/
export function getAvatarColor(initials: ?string) {
let colorIndex = 0;
if (initials) {
let nameHash = 0;
for (const s of initials) {
nameHash += s.codePointAt(0);
}
colorIndex = nameHash % AVATAR_COLORS.length;
}
return `rgba(${AVATAR_COLORS[colorIndex]}, ${AVATAR_OPACITY})`;
}
/**
* Generates initials for a simple string.
*
* @param {string?} s - The string to generate initials for.
* @returns {string?}
*/
export function getInitials(s: ?string) {
// We don't want to use the domain part of an email address, if it is one
const initialsBasis = _.split(s, '@')[0];
const words = _.words(initialsBasis);
let initials = '';
for (const w of words) {
(initials.length < 2) && (initials += w.substr(0, 1).toUpperCase());
}
return initials;
}

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