mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-10 16:50:21 +00:00
Compare commits
69 Commits
android-sd
...
3523
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e18af2d1a | ||
|
|
eb1fd4fd88 | ||
|
|
e0c8b6b3c0 | ||
|
|
9071cd4813 | ||
|
|
9bf650c700 | ||
|
|
49e3b03885 | ||
|
|
31e996ac3f | ||
|
|
d1c447e97b | ||
|
|
8a34c0a80f | ||
|
|
2f626ea474 | ||
|
|
0a76eebca7 | ||
|
|
0e9e695251 | ||
|
|
b86df7a8e3 | ||
|
|
5b25e02e26 | ||
|
|
0e6f14bb7c | ||
|
|
57b9954d9c | ||
|
|
249dd7b8b8 | ||
|
|
b31d7b4451 | ||
|
|
8dea3389ee | ||
|
|
e7f9e8e7f7 | ||
|
|
a53a86ee7a | ||
|
|
598b6f0598 | ||
|
|
b245945c4a | ||
|
|
301a8e319a | ||
|
|
820abfd059 | ||
|
|
1d42420a36 | ||
|
|
1b4bdb5142 | ||
|
|
f030a3f1fb | ||
|
|
8f79779ca7 | ||
|
|
c5111bb359 | ||
|
|
42814eac7d | ||
|
|
74d0013acc | ||
|
|
88e4850c4d | ||
|
|
87f9b1cf92 | ||
|
|
fe1187d7b7 | ||
|
|
a35b36d6df | ||
|
|
a04982fd96 | ||
|
|
658679f89e | ||
|
|
f90fc665f8 | ||
|
|
0c8130af41 | ||
|
|
e4c5968459 | ||
|
|
a148cd41a4 | ||
|
|
f8a049759d | ||
|
|
4b4225e14f | ||
|
|
3b0c5d0b6a | ||
|
|
3b750ddd5a | ||
|
|
6383d000a9 | ||
|
|
a48d67bdc7 | ||
|
|
2f92e72858 | ||
|
|
d2c85ada1b | ||
|
|
55b95c52d6 | ||
|
|
52362c4675 | ||
|
|
8da0552541 | ||
|
|
7ce0def995 | ||
|
|
48285e8a2d | ||
|
|
21dcc41d31 | ||
|
|
625d268373 | ||
|
|
681782ed20 | ||
|
|
1baa85b649 | ||
|
|
72137a2811 | ||
|
|
0734ce7ae3 | ||
|
|
2dc06c28e3 | ||
|
|
5848669552 | ||
|
|
c0376d238a | ||
|
|
979b773c3c | ||
|
|
3195a449ca | ||
|
|
d7483f07e3 | ||
|
|
9c1b802997 | ||
|
|
bb3a10b0fc |
2
Makefile
2
Makefile
@@ -45,6 +45,8 @@ 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:
|
||||
|
||||
@@ -207,24 +207,6 @@ 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`
|
||||
|
||||
6
android/app/.classpath
Normal file
6
android/app/.classpath
Normal file
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
23
android/app/.project
Normal file
23
android/app/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
2
android/app/.settings/org.eclipse.buildship.core.prefs
Normal file
2
android/app/.settings/org.eclipse.buildship.core.prefs
Normal file
@@ -0,0 +1,2 @@
|
||||
connection.project.dir=..
|
||||
eclipse.preferences.version=1
|
||||
@@ -88,15 +88,6 @@ 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 {
|
||||
|
||||
12
android/app/proguard-rules.pro
vendored
12
android/app/proguard-rules.pro
vendored
@@ -60,19 +60,9 @@
|
||||
-keep class sun.misc.Unsafe { *; }
|
||||
-dontwarn java.nio.file.*
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
-keep class okio.** { *; }
|
||||
-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.** { *; }
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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 {
|
||||
}
|
||||
@@ -164,10 +164,6 @@ 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()
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
|
||||
appVersion=19.2.0
|
||||
sdkVersion=2.2.0
|
||||
appVersion=19.3.0
|
||||
sdkVersion=2.2.1
|
||||
|
||||
@@ -8,13 +8,14 @@ THIS_DIR=$(cd -P "$(dirname "$(readlink "${BASH_SOURCE[0]}" || echo "${BASH_SOUR
|
||||
export RCT_METRO_PORT="${RCT_METRO_PORT:=8081}"
|
||||
echo "export RCT_METRO_PORT=${RCT_METRO_PORT}" > "${THIS_DIR}/../../node_modules/react-native/scripts/.packager.env"
|
||||
|
||||
adb reverse tcp:8081 tcp:8081
|
||||
|
||||
if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then
|
||||
if ! curl -s "http://localhost:${RCT_METRO_PORT}/status" | grep -q "packager-status:running" ; then
|
||||
echo "Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly"
|
||||
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"
|
||||
|
||||
6
android/sdk/.classpath
Normal file
6
android/sdk/.classpath
Normal file
@@ -0,0 +1,6 @@
|
||||
<?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>
|
||||
23
android/sdk/.project
Normal file
23
android/sdk/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?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>
|
||||
2
android/sdk/.settings/org.eclipse.buildship.core.prefs
Normal file
2
android/sdk/.settings/org.eclipse.buildship.core.prefs
Normal file
@@ -0,0 +1,2 @@
|
||||
connection.project.dir=..
|
||||
eclipse.preferences.version=1
|
||||
@@ -54,9 +54,6 @@ 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')
|
||||
|
||||
@@ -21,6 +21,7 @@ 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;
|
||||
|
||||
@@ -47,7 +48,12 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
|
||||
intent.setAction(Actions.START);
|
||||
|
||||
ComponentName componentName = context.startService(intent);
|
||||
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");
|
||||
}
|
||||
@@ -82,8 +88,13 @@ public class JitsiMeetOngoingConferenceService extends Service
|
||||
final String action = intent.getAction();
|
||||
if (action.equals(Actions.START)) {
|
||||
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
|
||||
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
|
||||
Log.i(TAG, "Service started");
|
||||
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
|
||||
|
||||
@@ -39,13 +39,6 @@ class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandl
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
|
||||
|
||||
// Terminate all conferences
|
||||
for (BaseReactView view: BaseReactView.getViews()) {
|
||||
if (view instanceof JitsiMeetView) {
|
||||
((JitsiMeetView) view).leave();
|
||||
}
|
||||
}
|
||||
|
||||
// Abort all ConnectionService ongoing calls
|
||||
if (AudioModeModule.useConnectionService()) {
|
||||
ConnectionService.abortConnections();
|
||||
|
||||
@@ -86,8 +86,10 @@ class OngoingConferenceTracker {
|
||||
}
|
||||
|
||||
private void updateListeners() {
|
||||
for (OngoingConferenceListener listener : listeners) {
|
||||
listener.onCurrentConferenceChanged(currentConference);
|
||||
synchronized (listeners) {
|
||||
for (OngoingConferenceListener listener : listeners) {
|
||||
listener.onCurrentConferenceChanged(currentConference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,17 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.devsupport.DevInternalSettings;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.uimanager.ViewManager;
|
||||
import com.oney.WebRTCModule.RTCVideoViewManager;
|
||||
import com.oney.WebRTCModule.WebRTCModule;
|
||||
|
||||
import org.webrtc.SoftwareVideoDecoderFactory;
|
||||
import org.webrtc.SoftwareVideoEncoderFactory;
|
||||
import org.webrtc.VideoDecoderFactory;
|
||||
import org.webrtc.VideoEncoderFactory;
|
||||
import org.webrtc.audio.AudioDeviceModule;
|
||||
import org.webrtc.audio.JavaAudioDeviceModule;
|
||||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.ArrayList;
|
||||
@@ -47,8 +58,7 @@ class ReactInstanceManagerHolder {
|
||||
*/
|
||||
private static ReactInstanceManager reactInstanceManager;
|
||||
|
||||
private static List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
private static List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
List<NativeModule> nativeModules
|
||||
= new ArrayList<>(Arrays.<NativeModule>asList(
|
||||
new AndroidSettingsModule(reactContext),
|
||||
@@ -66,6 +76,21 @@ class ReactInstanceManagerHolder {
|
||||
nativeModules.add(new RNConnectionService(reactContext));
|
||||
}
|
||||
|
||||
// Initialize the WebRTC module by hand, since we want to override some
|
||||
// initialization options.
|
||||
WebRTCModule.Options options = new WebRTCModule.Options();
|
||||
|
||||
AudioDeviceModule adm = JavaAudioDeviceModule.builder(reactContext)
|
||||
.createAudioDeviceModule();
|
||||
VideoDecoderFactory videoDecoderFactory = new SoftwareVideoDecoderFactory();
|
||||
VideoEncoderFactory videoEncoderFactory = new SoftwareVideoEncoderFactory();
|
||||
|
||||
options.setAudioDeviceModule(adm);
|
||||
options.setVideoDecoderFactory(videoDecoderFactory);
|
||||
options.setVideoEncoderFactory(videoEncoderFactory);
|
||||
|
||||
nativeModules.add(new WebRTCModule(reactContext, options));
|
||||
|
||||
try {
|
||||
Class<?> amplitudeModuleClass = Class.forName("org.jitsi.meet.sdk.AmplitudeModule");
|
||||
Constructor constructor = amplitudeModuleClass.getConstructor(ReactApplicationContext.class);
|
||||
@@ -77,6 +102,13 @@ class ReactInstanceManagerHolder {
|
||||
return nativeModules;
|
||||
}
|
||||
|
||||
private static List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return Arrays.<ViewManager>asList(
|
||||
// WebRTC, see createNativeModules for details.
|
||||
new RTCVideoViewManager()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to send an event to JavaScript.
|
||||
*
|
||||
@@ -155,11 +187,9 @@ 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(),
|
||||
new com.oney.WebRTCModule.WebRTCModulePackage(),
|
||||
new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(),
|
||||
new com.reactnativecommunity.webview.RNCWebViewPackage(),
|
||||
new com.rnimmersive.RNImmersivePackage(),
|
||||
@@ -169,6 +199,10 @@ class ReactInstanceManagerHolder {
|
||||
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
||||
return ReactInstanceManagerHolder.createNativeModules(reactContext);
|
||||
}
|
||||
@Override
|
||||
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
||||
return ReactInstanceManagerHolder.createViewManagers(reactContext);
|
||||
}
|
||||
}));
|
||||
|
||||
try {
|
||||
|
||||
@@ -7,8 +7,6 @@ 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'
|
||||
|
||||
249
conference.js
249
conference.js
@@ -23,7 +23,8 @@ import {
|
||||
sendAnalytics
|
||||
} from './react/features/analytics';
|
||||
import {
|
||||
redirectWithStoredParams,
|
||||
maybeRedirectToWelcomePage,
|
||||
redirectToStaticPage,
|
||||
reloadWithStoredParams
|
||||
} from './react/features/app';
|
||||
|
||||
@@ -43,6 +44,7 @@ import {
|
||||
conferenceWillJoin,
|
||||
conferenceWillLeave,
|
||||
dataChannelOpened,
|
||||
kickedOut,
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
@@ -76,7 +78,10 @@ import {
|
||||
setVideoAvailable,
|
||||
setVideoMuted
|
||||
} from './react/features/base/media';
|
||||
import { showNotification } from './react/features/notifications';
|
||||
import {
|
||||
hideNotification,
|
||||
showNotification
|
||||
} from './react/features/notifications';
|
||||
import {
|
||||
dominantSpeakerChanged,
|
||||
getLocalParticipant,
|
||||
@@ -96,15 +101,12 @@ import {
|
||||
createLocalTracksF,
|
||||
destroyLocalTracks,
|
||||
isLocalTrackMuted,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
replaceLocalTrack,
|
||||
trackAdded,
|
||||
trackRemoved
|
||||
} from './react/features/base/tracks';
|
||||
import {
|
||||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { notifyKickedOut } from './react/features/conference';
|
||||
import { 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';
|
||||
@@ -112,10 +114,8 @@ import {
|
||||
maybeOpenFeedbackDialog,
|
||||
submitFeedback
|
||||
} from './react/features/feedback';
|
||||
import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
import { mediaPermissionPromptVisibilityChanged } from './react/features/overlay';
|
||||
import { suspendDetected } from './react/features/power-monitor';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { isButtonEnabled } from './react/features/toolbox';
|
||||
import { endpointMessageReceived } from './react/features/subtitles';
|
||||
@@ -211,77 +211,6 @@ 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
|
||||
@@ -347,7 +276,7 @@ class ConferenceConnector {
|
||||
|
||||
case JitsiConferenceErrors.NOT_ALLOWED_ERROR: {
|
||||
// let's show some auth not allowed page
|
||||
redirectToStaticPage('static/authError.html');
|
||||
APP.store.dispatch(redirectToStaticPage('static/authError.html'));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -629,8 +558,7 @@ 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) {
|
||||
|
||||
@@ -734,8 +662,11 @@ export default {
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted || config.startSilent,
|
||||
startWithAudioMuted: config.startWithAudioMuted
|
||||
|| config.startSilent
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState()),
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
|| isUserInteractionRequiredForUnmute(APP.store.getState())
|
||||
}))
|
||||
.then(([ tracks, con ]) => {
|
||||
tracks.forEach(track => {
|
||||
@@ -836,6 +767,13 @@ 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
|
||||
@@ -899,6 +837,13 @@ 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
|
||||
@@ -1018,17 +963,15 @@ export default {
|
||||
* Returns the connection times stored in the library.
|
||||
*/
|
||||
getConnectionTimes() {
|
||||
return this._room.getConnectionTimes();
|
||||
return room.getConnectionTimes();
|
||||
},
|
||||
|
||||
// used by torture currently
|
||||
isJoined() {
|
||||
return this._room
|
||||
&& this._room.isJoined();
|
||||
return room && room.isJoined();
|
||||
},
|
||||
getConnectionState() {
|
||||
return this._room
|
||||
&& this._room.getConnectionState();
|
||||
return room && room.getConnectionState();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1037,8 +980,7 @@ export default {
|
||||
* P2P connection
|
||||
*/
|
||||
getP2PConnectionState() {
|
||||
return this._room
|
||||
&& this._room.getP2PConnectionState();
|
||||
return room && room.getP2PConnectionState();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1047,7 +989,7 @@ export default {
|
||||
*/
|
||||
_startP2P() {
|
||||
try {
|
||||
this._room && this._room.startP2PSession();
|
||||
room && room.startP2PSession();
|
||||
} catch (error) {
|
||||
logger.error('Start P2P failed', error);
|
||||
throw error;
|
||||
@@ -1060,7 +1002,7 @@ export default {
|
||||
*/
|
||||
_stopP2P() {
|
||||
try {
|
||||
this._room && this._room.stopP2PSession();
|
||||
room && room.stopP2PSession();
|
||||
} catch (error) {
|
||||
logger.error('Stop P2P failed', error);
|
||||
throw error;
|
||||
@@ -1075,7 +1017,7 @@ export default {
|
||||
* false otherwise.
|
||||
*/
|
||||
isConnectionInterrupted() {
|
||||
return this._room.isConnectionInterrupted();
|
||||
return room.isConnectionInterrupted();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1136,7 +1078,7 @@ export default {
|
||||
},
|
||||
|
||||
getMyUserId() {
|
||||
return this._room && this._room.myUserId();
|
||||
return room && room.myUserId();
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1159,7 +1101,7 @@ export default {
|
||||
* least one track.
|
||||
*/
|
||||
getNumberOfParticipantsWithTracks() {
|
||||
return this._room.getParticipants()
|
||||
return room.getParticipants()
|
||||
.filter(p => p.getTracks().length > 0)
|
||||
.length;
|
||||
},
|
||||
@@ -1297,17 +1239,34 @@ export default {
|
||||
const options = config;
|
||||
|
||||
const nick = APP.store.getState()['features/base/settings'].displayName;
|
||||
const { locationURL } = APP.store.getState()['features/base/connection'];
|
||||
|
||||
if (nick) {
|
||||
options.displayName = nick;
|
||||
}
|
||||
|
||||
options.applicationName = interfaceConfig.APP_NAME;
|
||||
options.getWiFiStatsMethod = getJitsiMeetGlobalNS().getWiFiStats;
|
||||
options.getWiFiStatsMethod = this._getWiFiStatsMethod;
|
||||
options.confID = `${locationURL.host}${locationURL.pathname}`;
|
||||
|
||||
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.
|
||||
@@ -1323,7 +1282,7 @@ export default {
|
||||
this.localVideo = newStream;
|
||||
this._setSharingScreen(newStream);
|
||||
if (newStream) {
|
||||
APP.UI.addLocalStream(newStream);
|
||||
APP.UI.addLocalVideoStream(newStream);
|
||||
}
|
||||
this.setVideoMuteStatus(this.isLocalVideoMuted());
|
||||
})
|
||||
@@ -1374,9 +1333,6 @@ export default {
|
||||
replaceLocalTrack(this.localAudio, newStream, room))
|
||||
.then(() => {
|
||||
this.localAudio = newStream;
|
||||
if (newStream) {
|
||||
APP.UI.addLocalStream(newStream);
|
||||
}
|
||||
this.setAudioMuteStatus(this.isLocalAudioMuted());
|
||||
})
|
||||
.then(resolve)
|
||||
@@ -1757,14 +1713,7 @@ export default {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayName = user.getDisplayName();
|
||||
|
||||
logger.log(`USER ${id} connnected:`, user);
|
||||
APP.API.notifyUserJoined(id, {
|
||||
displayName,
|
||||
formattedDisplayName: appendSuffix(
|
||||
displayName || interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
|
||||
});
|
||||
APP.UI.addUser(user);
|
||||
|
||||
// check the roles for the new user and reflect them
|
||||
@@ -1780,12 +1729,7 @@ 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);
|
||||
});
|
||||
|
||||
@@ -1854,14 +1798,29 @@ export default {
|
||||
APP.UI.setAudioLevel(id, newLvl);
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.TRACK_MUTE_CHANGED, (_, participantThatMutedUs) => {
|
||||
// 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, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
const action = APP.store.dispatch(showNotification({
|
||||
titleKey: 'toolbar.talkWhileMutedPopup',
|
||||
customActionNameKey: 'notify.unmute',
|
||||
customActionHandler: muteLocalAudio.bind(this, false)
|
||||
}));
|
||||
|
||||
lastNotificationId = action.uid;
|
||||
});
|
||||
room.on(JitsiConferenceEvents.SUBJECT_CHANGED,
|
||||
subject => APP.store.dispatch(conferenceSubjectChanged(subject)));
|
||||
@@ -1962,7 +1921,7 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.KICKED, participant => {
|
||||
APP.UI.hideStats();
|
||||
APP.store.dispatch(notifyKickedOut(participant));
|
||||
APP.store.dispatch(kickedOut(room, participant));
|
||||
|
||||
// FIXME close
|
||||
});
|
||||
@@ -1973,33 +1932,6 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.SUSPEND_DETECTED, () => {
|
||||
APP.store.dispatch(suspendDetected());
|
||||
|
||||
// After wake up, we will be in a state where conference is left
|
||||
// there will be dialog shown to user.
|
||||
// We do not want video/audio as we show an overlay and after it
|
||||
// user need to rejoin or close, while waking up we can detect
|
||||
// camera wakeup as a problem with device.
|
||||
// We also do not care about device change, which happens
|
||||
// on resume after suspending PC.
|
||||
if (this.deviceChangeListener) {
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
}
|
||||
|
||||
// stop local video
|
||||
if (this.localVideo) {
|
||||
this.localVideo.dispose();
|
||||
this.localVideo = null;
|
||||
}
|
||||
|
||||
// stop local audio
|
||||
if (this.localAudio) {
|
||||
this.localAudio.dispose();
|
||||
this.localAudio = null;
|
||||
}
|
||||
|
||||
APP.API.notifySuspendDetected();
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
|
||||
@@ -2255,6 +2187,27 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanups local conference on suspend.
|
||||
*/
|
||||
onSuspendDetected() {
|
||||
// After wake up, we will be in a state where conference is left
|
||||
// there will be dialog shown to user.
|
||||
// We do not want video/audio as we show an overlay and after it
|
||||
// user need to rejoin or close, while waking up we can detect
|
||||
// camera wakeup as a problem with device.
|
||||
// We also do not care about device change, which happens
|
||||
// on resume after suspending PC.
|
||||
if (this.deviceChangeListener) {
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
}
|
||||
|
||||
this.localVideo = null;
|
||||
this.localAudio = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback invoked when the conference has been successfully joined.
|
||||
* Initializes the UI and various other features.
|
||||
@@ -2269,7 +2222,7 @@ export default {
|
||||
|
||||
if (config.requireDisplayName
|
||||
&& !APP.conference.getLocalDisplayName()
|
||||
&& !this._room.isHidden()) {
|
||||
&& !room.isHidden()) {
|
||||
APP.UI.promptDisplayName();
|
||||
}
|
||||
|
||||
@@ -2601,7 +2554,7 @@ export default {
|
||||
room = undefined;
|
||||
|
||||
APP.API.notifyReadyToClose();
|
||||
maybeRedirectToWelcomePage(values[0]);
|
||||
APP.store.dispatch(maybeRedirectToWelcomePage(values[0]));
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
31
css/_avatar.scss
Normal file
31
css/_avatar.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.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
|
||||
}
|
||||
192
css/_font.scss
192
css/_font.scss
@@ -1,12 +1,12 @@
|
||||
@font-face {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
@@ -25,92 +25,9 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.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";
|
||||
.icon-blur-background:before {
|
||||
content: "\e90f";
|
||||
color: #a4b8d1;
|
||||
}
|
||||
.icon-speaker:before {
|
||||
content: "\e92d";
|
||||
@@ -220,3 +137,90 @@
|
||||
.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";
|
||||
}
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
}
|
||||
|
||||
#dominantSpeakerAvatarContainer,
|
||||
#dominantSpeakerAvatar,
|
||||
.dynamic-shadow {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
@@ -503,14 +502,9 @@
|
||||
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%;
|
||||
@@ -524,7 +518,6 @@
|
||||
.avatar-container {
|
||||
@include maxSize(60px);
|
||||
@include absoluteAligning();
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 50%;
|
||||
|
||||
@@ -86,5 +86,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'navigate_section_list';
|
||||
@import 'third-party-branding/google';
|
||||
@import 'third-party-branding/microsoft';
|
||||
@import 'avatar';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
13
doc/api.md
13
doc/api.md
@@ -387,6 +387,19 @@ 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
|
||||
{
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -42,6 +42,7 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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: 22 KiB After Width: | Height: | Size: 24 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -50,7 +50,7 @@ var interfaceConfig = {
|
||||
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview'
|
||||
'tileview', 'videobackgroundblur'
|
||||
],
|
||||
|
||||
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],
|
||||
|
||||
@@ -39,7 +39,6 @@ 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'
|
||||
|
||||
@@ -35,7 +35,6 @@ 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
|
||||
@@ -90,14 +89,9 @@ 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.1):
|
||||
- react-native-webrtc (1.69.2):
|
||||
- React
|
||||
- react-native-webview (5.8.1):
|
||||
- React
|
||||
@@ -162,10 +156,6 @@ 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:
|
||||
@@ -181,7 +171,6 @@ 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`)
|
||||
@@ -214,7 +203,6 @@ SPEC REPOS:
|
||||
- FirebaseCore
|
||||
- FirebaseDynamicLinks
|
||||
- FirebaseInstanceID
|
||||
- FLAnimatedImage
|
||||
- GoogleAppMeasurement
|
||||
- GoogleSignIn
|
||||
- GoogleToolboxForMac
|
||||
@@ -222,7 +210,6 @@ SPEC REPOS:
|
||||
- GTMSessionFetcher
|
||||
- nanopb
|
||||
- ObjectiveDropboxOfficial
|
||||
- SDWebImage
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
BVLinearGradient:
|
||||
@@ -239,8 +226,6 @@ 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:
|
||||
@@ -273,7 +258,6 @@ SPEC CHECKSUMS:
|
||||
FirebaseCore: 52f851b30e11360f1e67cf04b1edfebf0a47a2d3
|
||||
FirebaseDynamicLinks: f209c3caccd82102caa0e91d393e3ccc593501fd
|
||||
FirebaseInstanceID: bd6fc5a258884e206fd5c474ebe4f5b00e21770e
|
||||
FLAnimatedImage: 4a0b56255d9b05f18b6dd7ee06871be5d3b89e31
|
||||
Folly: de497beb10f102453a1afa9edbf8cf8a251890de
|
||||
glog: aefd1eb5dda2ab95ba0938556f34b98e2da3a60d
|
||||
GoogleAppMeasurement: 6cf307834da065863f9faf4c0de0a936d81dd832
|
||||
@@ -286,18 +270,16 @@ 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: 90a847d19deb2d7323fef8cc89ca12b8995fbc90
|
||||
react-native-webrtc: 1415d2a54b2246dd85ba95eb3e4bf2b66533f951
|
||||
react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4
|
||||
RNCAsyncStorage: 8e31405a9f12fbf42c2bb330e4560bfd79c18323
|
||||
RNGoogleSignin: d030c6c6591db24c3cee649f64c7babf0a1699a0
|
||||
RNSound: e157320f503bdd4f4ee6d8542e948d54f90c3c3a
|
||||
RNVectorIcons: d819334932bcda3332deb3d2c8ea4d069e0b98f9
|
||||
RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42
|
||||
SDWebImage: 3f3f0c02f09798048c47a5ed0a13f17b063572d8
|
||||
yoga: 92b2102c3d373d1a790db4ab761d2b0ffc634f64
|
||||
|
||||
PODFILE CHECKSUM: b55338cc43312051ed83f8d9c6aadbd8c9402e6a
|
||||
PODFILE CHECKSUM: d540f088d564bfe3b8ca3d13eec4cc0ce9c6e4bc
|
||||
|
||||
COCOAPODS: 1.7.2
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>19.2.0</string>
|
||||
<string>19.3.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>19.2.0</string>
|
||||
<string>19.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>19.2.0</string>
|
||||
<string>19.3.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CLKComplicationPrincipalClass</key>
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
*/
|
||||
@property (nonatomic, nullable) JitsiMeetConferenceOptions *defaultConferenceOptions;
|
||||
|
||||
#pragma mak - This class is a singleton
|
||||
#pragma mark - This class is a singleton
|
||||
|
||||
+ (instancetype _Nonnull)sharedInstance;
|
||||
|
||||
|
||||
@@ -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 members",
|
||||
"failedToAdd": "Failed to add participants",
|
||||
"footerText": "Dialing out is disabled.",
|
||||
"invite": "Invite",
|
||||
"loading": "Searching for people and phone numbers",
|
||||
@@ -112,7 +112,6 @@
|
||||
"transport_plural": "Transports:",
|
||||
"turn": " (turn)"
|
||||
},
|
||||
"contactlist_plural": "__count__ Members",
|
||||
"dateUtils": {
|
||||
"earlier": "Earlier",
|
||||
"today": "Today",
|
||||
@@ -148,7 +147,7 @@
|
||||
"liveStreaming": "Live Stream"
|
||||
},
|
||||
"allow": "Allow",
|
||||
"alreadySharedVideoMsg": "Another member is already sharing a video. This conference allows only one shared video at a time.",
|
||||
"alreadySharedVideoMsg": "Another participant 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",
|
||||
@@ -173,7 +172,6 @@
|
||||
"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",
|
||||
@@ -191,6 +189,7 @@
|
||||
"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",
|
||||
@@ -200,18 +199,18 @@
|
||||
"kickMessage": "You can contact __participantDisplayName__ for more details.",
|
||||
"kickParticipantButton": "Kick",
|
||||
"kickParticipantDialog": "Are you sure you want to kick this participant?",
|
||||
"kickParticipantTitle": "Kick this member?",
|
||||
"kickParticipantTitle": "Kick this participant?",
|
||||
"kickTitle": "Ouch! __participantDisplayName__ kicked you out of the 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 password",
|
||||
"lockRoom": "Add meeting $t(lockRoomPasswordUppercase)",
|
||||
"lockTitle": "Lock failed",
|
||||
"logoutQuestion": "Are you sure you want to logout and stop the conference?",
|
||||
"logoutTitle": "Logout",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
@@ -221,17 +220,13 @@
|
||||
"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 member?",
|
||||
"muteParticipantTitle": "Mute this participant?",
|
||||
"Ok": "Ok",
|
||||
"oops": "Oops!",
|
||||
"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",
|
||||
"passwordLabel": "$t(lockRoomPasswordUppercase)",
|
||||
"passwordNotSupported": "Setting a meeting $t(lockRoomPassword) is not supported.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) not supported",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) 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",
|
||||
@@ -248,7 +243,7 @@
|
||||
"remoteControlStopMessage": "The remote control session ended!",
|
||||
"remoteControlTitle": "Remote desktop control",
|
||||
"Remove": "Remove",
|
||||
"removePassword": "Remove password",
|
||||
"removePassword": "Remove $t(lockRoomPassword)",
|
||||
"removeSharedVideoMsg": "Are you sure you would like to remove your shared video?",
|
||||
"removeSharedVideoTitle": "Remove shared video",
|
||||
"reservationError": "Reservation system error",
|
||||
@@ -286,7 +281,7 @@
|
||||
"tokenAuthFailedTitle": "Authentication failed",
|
||||
"transcribing": "Transcribing",
|
||||
"unableToSwitch": "Unable to switch video stream.",
|
||||
"unlockRoom": "Remove meeting password",
|
||||
"unlockRoom": "Remove meeting $t(lockRoomPassword)",
|
||||
"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.",
|
||||
@@ -298,34 +293,6 @@
|
||||
"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",
|
||||
@@ -344,8 +311,8 @@
|
||||
},
|
||||
"info": {
|
||||
"accessibilityLabel": "Show info",
|
||||
"addPassword": "Add password",
|
||||
"cancelPassword": "Cancel password",
|
||||
"addPassword": "Add $t(lockRoomPassword)",
|
||||
"cancelPassword": "Cancel $t(lockRoomPassword)",
|
||||
"conferenceURL": "Link:",
|
||||
"country": "Country",
|
||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter the pin.",
|
||||
@@ -367,15 +334,13 @@
|
||||
"noPassword": "None",
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "Password:",
|
||||
"password": "$t(lockRoomPasswordUppercase):",
|
||||
"title": "Share",
|
||||
"tooltip": "Share link and dial-in info for this meeting",
|
||||
"label": "Meeting info"
|
||||
},
|
||||
"inviteDialog": {
|
||||
"alertOk": "Ok",
|
||||
"alertText": "Failed to invite some participants.",
|
||||
"alertTitle": "Invite",
|
||||
"header": "Invite",
|
||||
"searchCallOnlyPlaceholder": "Enter phone number",
|
||||
"searchPeopleOnlyPlaceholder": "Search for participants",
|
||||
@@ -463,6 +428,8 @@
|
||||
"stop": "Stop Recording",
|
||||
"yes": "Yes"
|
||||
},
|
||||
"lockRoomPassword": "password",
|
||||
"lockRoomPasswordUppercase": "Password",
|
||||
"me": "me",
|
||||
"notify": {
|
||||
"connectedOneMember": "__name__ joined the meeting",
|
||||
@@ -482,17 +449,20 @@
|
||||
"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 member",
|
||||
"passwordSetRemotely": "set by another participant",
|
||||
"passwordDigitsOnly": "Up to __number__ digits",
|
||||
"poweredby": "powered by",
|
||||
"presenceStatus": {
|
||||
@@ -567,7 +537,6 @@
|
||||
"more": "More",
|
||||
"name": "Name",
|
||||
"noDevice": "None",
|
||||
"password": "SET PASSWORD",
|
||||
"selectAudioOutput": "Audio output",
|
||||
"selectCamera": "Camera",
|
||||
"selectMic": "Microphone",
|
||||
@@ -645,7 +614,8 @@
|
||||
"speakerStats": "Toggle speaker statistics",
|
||||
"tileView": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"videomute": "Toggle mute video"
|
||||
"videomute": "Toggle mute video",
|
||||
"videoblur": "Toggle video blur"
|
||||
},
|
||||
"addPeople": "Add people to your call",
|
||||
"audioonly": "Enable / Disable audio only mode",
|
||||
@@ -684,7 +654,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 members.",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other participants.",
|
||||
"shareRoom": "Invite someone",
|
||||
"shortcuts": "View shortcuts",
|
||||
"sip": "Call SIP number",
|
||||
@@ -698,7 +668,9 @@
|
||||
"tileViewToggle": "Toggle tile view",
|
||||
"toggleCamera": "Toggle camera",
|
||||
"unableToUnmutePopup": "You cannot un-mute while the shared video is on.",
|
||||
"videomute": "Start / Stop camera"
|
||||
"videomute": "Start / Stop camera",
|
||||
"startvideoblur": "Blur my background",
|
||||
"stopvideoblur": "Disable background blur"
|
||||
},
|
||||
"transcribing": {
|
||||
"ccButtonTooltip": "Start / Stop subtitles",
|
||||
@@ -764,11 +736,11 @@
|
||||
"flip": "Flip",
|
||||
"kick": "Kick out",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Member is muted",
|
||||
"mute": "Participant is muted",
|
||||
"muted": "Muted",
|
||||
"remoteControl": "Remote control",
|
||||
"show": "Show on stage",
|
||||
"videomute": "Member has stopped the camera"
|
||||
"videomute": "Participant has stopped the camera"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
|
||||
@@ -658,6 +658,24 @@ 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.
|
||||
|
||||
3
modules/API/external/external_api.js
vendored
3
modules/API/external/external_api.js
vendored
@@ -63,6 +63,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',
|
||||
@@ -561,7 +562,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
this.emit('_willDispose');
|
||||
this._transport.dispose();
|
||||
this.removeAllListeners();
|
||||
if (this._frame) {
|
||||
if (this._frame && this._frame.parentNode) {
|
||||
this._frame.parentNode.removeChild(this._frame);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +156,6 @@ UI.getSharedVideoManager = function() {
|
||||
* established, false - otherwise (for example in the case of welcome page)
|
||||
*/
|
||||
UI.start = function() {
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
|
||||
// Set the defaults for prompt dialogs.
|
||||
$.prompt.setDefaults({ persistent: false });
|
||||
|
||||
@@ -189,8 +187,6 @@ UI.start = function() {
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
UI.messageHandler.enablePopups(false);
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -229,22 +225,11 @@ UI.unbindEvents = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Show local stream on UI.
|
||||
* Show local video stream on UI.
|
||||
* @param {JitsiTrack} track stream to show
|
||||
*/
|
||||
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;
|
||||
}
|
||||
UI.addLocalVideoStream = track => {
|
||||
VideoLayout.changeLocalVideo(track);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -510,8 +495,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, avatarURL) {
|
||||
VideoLayout.changeUserAvatar(id, avatarURL);
|
||||
UI.refreshAvatarDisplay = function(id) {
|
||||
VideoLayout.changeUserAvatar(id);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,7 +33,7 @@ SharedVideoThumb.prototype.constructor = SharedVideoThumb;
|
||||
SharedVideoThumb.prototype.setDeviceAvailabilityIcons = function() {};
|
||||
|
||||
// eslint-disable-next-line no-empty-function
|
||||
SharedVideoThumb.prototype.avatarChanged = function() {};
|
||||
SharedVideoThumb.prototype.initializeAvatar = function() {};
|
||||
|
||||
SharedVideoThumb.prototype.createContainer = function(spanId) {
|
||||
const container = document.createElement('span');
|
||||
|
||||
@@ -5,11 +5,8 @@ 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 */
|
||||
|
||||
@@ -214,8 +211,7 @@ export default class LargeVideoManager {
|
||||
container.setStream(id, stream, videoType);
|
||||
|
||||
// change the avatar url on large
|
||||
this.updateAvatar(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), id));
|
||||
this.updateAvatar();
|
||||
|
||||
// If the user's connection is disrupted then the avatar will be
|
||||
// displayed in case we have no video image cached. That is if
|
||||
@@ -406,18 +402,16 @@ export default class LargeVideoManager {
|
||||
/**
|
||||
* Updates the src of the dominant speaker avatar
|
||||
*/
|
||||
updateAvatar(avatarUrl) {
|
||||
if (avatarUrl) {
|
||||
ReactDOM.render(
|
||||
updateAvatar() {
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<Avatar
|
||||
id = "dominantSpeakerAvatar"
|
||||
uri = { avatarUrl } />,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
);
|
||||
} else {
|
||||
ReactDOM.unmountComponentAtNode(
|
||||
this._dominantSpeakerAvatarContainer);
|
||||
}
|
||||
participantId = { this.id }
|
||||
size = { 200 } />
|
||||
</Provider>,
|
||||
this._dominantSpeakerAvatarContainer
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,9 +7,6 @@ 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';
|
||||
@@ -55,8 +52,7 @@ 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.avatarChanged(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), this.id));
|
||||
this.initializeAvatar();
|
||||
|
||||
this.addAudioLevelIndicator();
|
||||
this.updateIndicators();
|
||||
|
||||
@@ -10,9 +10,8 @@ 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';
|
||||
@@ -570,8 +569,7 @@ SmallVideo.prototype.updateView = function() {
|
||||
if (!this.hasAvatar) {
|
||||
if (this.id) {
|
||||
// Init avatar
|
||||
this.avatarChanged(
|
||||
getAvatarURLByParticipantId(APP.store.getState(), this.id));
|
||||
this.initializeAvatar();
|
||||
} else {
|
||||
logger.error('Unable to init avatar - no id', this);
|
||||
|
||||
@@ -609,19 +607,22 @@ 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.avatarChanged = function(avatarUrl) {
|
||||
SmallVideo.prototype.initializeAvatar = function() {
|
||||
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(
|
||||
<AvatarDisplay
|
||||
className = 'userAvatar'
|
||||
uri = { avatarUrl } />,
|
||||
<Provider store = { APP.store }>
|
||||
<AvatarDisplay
|
||||
className = 'userAvatar'
|
||||
participantId = { this.id } />
|
||||
</Provider>,
|
||||
thumbnail
|
||||
);
|
||||
}
|
||||
|
||||
@@ -53,16 +53,6 @@ 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.
|
||||
*
|
||||
@@ -86,43 +76,6 @@ 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;
|
||||
@@ -208,10 +161,6 @@ 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.
|
||||
@@ -247,79 +196,6 @@ 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];
|
||||
@@ -423,23 +299,6 @@ 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());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -473,19 +332,6 @@ 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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -512,43 +358,14 @@ 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._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);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(resourceJid, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -646,10 +463,8 @@ const VideoLayout = {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
// large video will show avatar instead of muted stream
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
// large video will show avatar instead of muted stream
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -677,18 +492,6 @@ 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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -758,9 +561,7 @@ const VideoLayout = {
|
||||
|
||||
if (remoteVideo) {
|
||||
remoteVideo.updateView();
|
||||
if (remoteVideo.isCurrentlyOnLargeVideo()) {
|
||||
this.updateLargeVideo(id);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(id);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -809,7 +610,6 @@ const VideoLayout = {
|
||||
}
|
||||
|
||||
VideoLayout.resizeThumbnails();
|
||||
VideoLayout._updateAfterThumbRemoved(id);
|
||||
},
|
||||
|
||||
onVideoTypeChanged(id, newVideoType) {
|
||||
@@ -835,9 +635,7 @@ const VideoLayout = {
|
||||
}
|
||||
smallVideo.setVideoType(newVideoType);
|
||||
|
||||
if (this.isCurrentlyOnLarge(id)) {
|
||||
this.updateLargeVideo(id, true);
|
||||
}
|
||||
this._updateLargeVideoIfDisplayed(id, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -880,7 +678,7 @@ const VideoLayout = {
|
||||
const smallVideo = VideoLayout.getSmallVideo(id);
|
||||
|
||||
if (smallVideo) {
|
||||
smallVideo.avatarChanged(avatarUrl);
|
||||
smallVideo.initializeAvatar();
|
||||
} else {
|
||||
logger.warn(
|
||||
`Missed avatar update - no small video yet for ${id}`
|
||||
@@ -1095,8 +893,10 @@ const VideoLayout = {
|
||||
* will be set.
|
||||
*/
|
||||
_setRemoteControlProperties(user, remoteVideo) {
|
||||
APP.remoteControl.checkUserRemoteControlSupport(user).then(result =>
|
||||
remoteVideo.setRemoteControlSupport(result));
|
||||
APP.remoteControl.checkUserRemoteControlSupport(user)
|
||||
.then(result => remoteVideo.setRemoteControlSupport(result))
|
||||
.catch(error =>
|
||||
logger.warn('could not get remote control properties', error));
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -91,9 +91,8 @@ class RemoteControl extends EventEmitter {
|
||||
* the user supports remote control and with false if not.
|
||||
*/
|
||||
checkUserRemoteControlSupport(user: Object) {
|
||||
return user.getFeatures().then(
|
||||
features => features.has(DISCO_REMOTE_CONTROL_FEATURE),
|
||||
() => false);
|
||||
return user.getFeatures()
|
||||
.then(features => features.has(DISCO_REMOTE_CONTROL_FEATURE));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
158
package-lock.json
generated
158
package-lock.json
generated
@@ -2563,6 +2563,102 @@
|
||||
"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",
|
||||
@@ -8738,8 +8834,8 @@
|
||||
"integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
|
||||
},
|
||||
"js-utils": {
|
||||
"version": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"from": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"version": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"from": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"requires": {
|
||||
"bowser": "1.9.1",
|
||||
"js-md5": "0.7.3"
|
||||
@@ -8946,8 +9042,8 @@
|
||||
}
|
||||
},
|
||||
"lib-jitsi-meet": {
|
||||
"version": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"from": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"version": "github:jitsi/lib-jitsi-meet#24bda8e941c346ccfde6bc1e1bb5dcdbd9450bab",
|
||||
"from": "github:jitsi/lib-jitsi-meet#24bda8e941c346ccfde6bc1e1bb5dcdbd9450bab",
|
||||
"requires": {
|
||||
"@jitsi/sdp-interop": "0.1.14",
|
||||
"@jitsi/sdp-simulcast": "0.2.1",
|
||||
@@ -12213,11 +12309,6 @@
|
||||
"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",
|
||||
@@ -12300,8 +12391,8 @@
|
||||
"integrity": "sha512-l3Quzbb+qa4in2U5RSt/lT0/pHrIpEChT1NnqrVAAXNrjkXjVOsxduaaEDdDhTzNJQEm/PcAcoyrFmgvGOohxw=="
|
||||
},
|
||||
"react-native-webrtc": {
|
||||
"version": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
|
||||
"from": "github:jitsi/react-native-webrtc#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
|
||||
"version": "github:jitsi/react-native-webrtc#e89a1d0b96375cb339513c1c71a6e8d7ff3b33ad",
|
||||
"from": "github:jitsi/react-native-webrtc#e89a1d0b96375cb339513c1c71a6e8d7ff3b33ad",
|
||||
"requires": {
|
||||
"base64-js": "^1.1.2",
|
||||
"event-target-shim": "^1.0.5",
|
||||
@@ -13187,6 +13278,35 @@
|
||||
"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",
|
||||
@@ -13775,6 +13895,11 @@
|
||||
"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",
|
||||
@@ -15363,6 +15488,12 @@
|
||||
"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",
|
||||
@@ -16818,6 +16949,11 @@
|
||||
"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",
|
||||
|
||||
10
package.json
10
package.json
@@ -35,6 +35,8 @@
|
||||
"@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",
|
||||
@@ -49,10 +51,10 @@
|
||||
"jquery-contextmenu": "2.4.5",
|
||||
"jquery-i18next": "1.2.0",
|
||||
"js-md5": "0.6.1",
|
||||
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
|
||||
"js-utils": "github:jitsi/js-utils#192b1c996e8c05530eb1f19e82a31069c3021e31",
|
||||
"jsrsasign": "8.0.12",
|
||||
"jwt-decode": "2.2.0",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
|
||||
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#24bda8e941c346ccfde6bc1e1bb5dcdbd9450bab",
|
||||
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.19.4",
|
||||
@@ -67,7 +69,6 @@
|
||||
"react-native-background-timer": "2.1.1",
|
||||
"react-native-calendar-events": "1.6.4",
|
||||
"react-native-callstats": "3.58.2",
|
||||
"react-native-fast-image": "5.1.1",
|
||||
"react-native-google-signin": "2.0.0",
|
||||
"react-native-immersive": "2.0.0",
|
||||
"react-native-keep-awake": "4.0.0",
|
||||
@@ -76,7 +77,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#4064c6f2db4f8b961daaaa8dafc6a896d7cfbc43",
|
||||
"react-native-webrtc": "github:jitsi/react-native-webrtc#e89a1d0b96375cb339513c1c71a6e8d7ff3b33ad",
|
||||
"react-native-webview": "5.8.1",
|
||||
"react-redux": "5.0.7",
|
||||
"react-textarea-autosize": "7.1.0",
|
||||
@@ -85,6 +86,7 @@
|
||||
"redux-thunk": "2.2.0",
|
||||
"styled-components": "3.4.9",
|
||||
"uuid": "3.1.0",
|
||||
"windows-iana": "^3.1.0",
|
||||
"xmldom": "0.1.27"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
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;
|
||||
@@ -17,7 +22,9 @@ const TOOLBAR_TIMEOUT = 4000;
|
||||
type State = {
|
||||
avatarURL: string,
|
||||
displayName: string,
|
||||
formattedDisplayName: string,
|
||||
isVideoDisplayed: boolean,
|
||||
userID: string,
|
||||
visible: boolean
|
||||
};
|
||||
|
||||
@@ -42,7 +49,9 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
this.state = {
|
||||
avatarURL: '',
|
||||
displayName: '',
|
||||
formattedDisplayName: '',
|
||||
isVideoDisplayed: true,
|
||||
userID: '',
|
||||
visible: true
|
||||
};
|
||||
|
||||
@@ -78,10 +87,15 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayNameChangedListener({ formattedDisplayName, id }) {
|
||||
_displayNameChangedListener({ displayname, formattedDisplayName, id }) {
|
||||
if (api._getOnStageParticipant() === id
|
||||
&& formattedDisplayName !== this.state.displayName) {
|
||||
this.setState({ displayName: formattedDisplayName });
|
||||
&& (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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,13 +126,16 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
_largeVideoChangedListener() {
|
||||
const userID = api._getOnStageParticipant();
|
||||
const avatarURL = api.getAvatarURL(userID);
|
||||
const displayName = api._getFormattedDisplayName(userID);
|
||||
const displayName = api.getDisplayName(userID);
|
||||
const formattedDisplayName = api._getFormattedDisplayName(userID);
|
||||
const isVideoDisplayed = Boolean(api._getLargeVideo());
|
||||
|
||||
this.setState({
|
||||
avatarURL,
|
||||
displayName,
|
||||
isVideoDisplayed
|
||||
formattedDisplayName,
|
||||
isVideoDisplayed,
|
||||
userID
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,7 +178,7 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderVideoNotAvailableScreen() {
|
||||
const { avatarURL, displayName, isVideoDisplayed } = this.state;
|
||||
const { avatarURL, displayName, formattedDisplayName, isVideoDisplayed, userID } = this.state;
|
||||
|
||||
if (isVideoDisplayed) {
|
||||
return null;
|
||||
@@ -169,19 +186,17 @@ export default class AlwaysOnTop extends Component<*, State> {
|
||||
|
||||
return (
|
||||
<div id = 'videoNotAvailableScreen'>
|
||||
{
|
||||
avatarURL
|
||||
? <div id = 'avatarContainer'>
|
||||
<img
|
||||
id = 'avatar'
|
||||
src = { avatarURL } />
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div id = 'avatarContainer'>
|
||||
<StatelessAvatar
|
||||
color = { getAvatarColor(userID) }
|
||||
id = 'avatar'
|
||||
initials = { getInitials(displayName) }
|
||||
url = { avatarURL } />)
|
||||
</div>
|
||||
<div
|
||||
className = 'displayname'
|
||||
id = 'displayname'>
|
||||
{ displayName }
|
||||
{ formattedDisplayName }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -467,6 +467,21 @@ 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).
|
||||
|
||||
@@ -13,10 +13,18 @@ import {
|
||||
import { connect, disconnect, setLocationURL } from '../base/connection';
|
||||
import { loadConfig } from '../base/lib-jitsi-meet';
|
||||
import { createDesiredLocalTracks } from '../base/tracks';
|
||||
import { parseURIString, toURLString } from '../base/util';
|
||||
import {
|
||||
getLocationContextRoot,
|
||||
parseURIString,
|
||||
toURLString
|
||||
} from '../base/util';
|
||||
import { showNotification } from '../notifications';
|
||||
import { setFatalError } from '../overlay';
|
||||
|
||||
import { getDefaultURL } from './functions';
|
||||
import {
|
||||
getDefaultURL,
|
||||
getName
|
||||
} from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -136,6 +144,34 @@ 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.
|
||||
*
|
||||
@@ -182,3 +218,58 @@ 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { updateSettings } from '../../base/settings';
|
||||
import '../../google-api';
|
||||
import '../../mobile/audio-mode';
|
||||
import '../../mobile/back-button';
|
||||
import '../../mobile/background';
|
||||
import '../../mobile/call-integration';
|
||||
import '../../mobile/external-api';
|
||||
|
||||
@@ -4,9 +4,11 @@ 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';
|
||||
import '../../power-monitor';
|
||||
import '../../room-lock';
|
||||
import '../../video-layout';
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// @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;
|
||||
}
|
||||
}
|
||||
190
react/features/base/avatar/components/Avatar.js
Normal file
190
react/features/base/avatar/components/Avatar.js
Normal file
@@ -0,0 +1,190 @@
|
||||
// @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);
|
||||
4
react/features/base/avatar/components/index.native.js
Normal file
4
react/features/base/avatar/components/index.native.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export * from './native';
|
||||
export { default as Avatar } from './Avatar';
|
||||
4
react/features/base/avatar/components/index.web.js
Normal file
4
react/features/base/avatar/components/index.web.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export * from './web';
|
||||
export { default as Avatar } from './Avatar';
|
||||
143
react/features/base/avatar/components/native/StatelessAvatar.js
Normal file
143
react/features/base/avatar/components/native/StatelessAvatar.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// @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) } />
|
||||
);
|
||||
}
|
||||
}
|
||||
3
react/features/base/avatar/components/native/index.js
Normal file
3
react/features/base/avatar/components/native/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as StatelessAvatar } from './StatelessAvatar';
|
||||
49
react/features/base/avatar/components/native/styles.js
Normal file
49
react/features/base/avatar/components/native/styles.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @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
|
||||
}
|
||||
};
|
||||
123
react/features/base/avatar/components/web/StatelessAvatar.js
Normal file
123
react/features/base/avatar/components/web/StatelessAvatar.js
Normal file
@@ -0,0 +1,123 @@
|
||||
// @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
|
||||
}
|
||||
3
react/features/base/avatar/components/web/index.js
Normal file
3
react/features/base/avatar/components/web/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
|
||||
export { default as StatelessAvatar } from './StatelessAvatar';
|
||||
54
react/features/base/avatar/functions.js
Normal file
54
react/features/base/avatar/functions.js
Normal file
@@ -0,0 +1,54 @@
|
||||
// @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;
|
||||
}
|
||||
4
react/features/base/avatar/index.js
Normal file
4
react/features/base/avatar/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// @flow
|
||||
|
||||
export * from './components';
|
||||
export * from './functions';
|
||||
@@ -1,23 +1,23 @@
|
||||
// @flow
|
||||
|
||||
import { ColorPalette, getRGBAFormat } from '../styles';
|
||||
import { ColorPalette } from '../styles';
|
||||
|
||||
/**
|
||||
* The default color scheme of the application.
|
||||
*/
|
||||
export default {
|
||||
'BottomSheet': {
|
||||
background: ColorPalette.blackBlue,
|
||||
icon: ColorPalette.white,
|
||||
label: ColorPalette.white
|
||||
background: 'rgb(255, 255, 255)',
|
||||
icon: '#1c2025',
|
||||
label: '#1c2025'
|
||||
},
|
||||
'Dialog': {
|
||||
background: ColorPalette.blackBlue,
|
||||
border: getRGBAFormat(ColorPalette.white, 0.2),
|
||||
background: 'rgb(255, 255, 255)',
|
||||
border: 'rgba(0, 3, 6, 0.6)',
|
||||
buttonBackground: ColorPalette.blue,
|
||||
buttonLabel: ColorPalette.white,
|
||||
icon: ColorPalette.white,
|
||||
text: ColorPalette.white
|
||||
icon: '#1c2025',
|
||||
text: '#1c2025'
|
||||
},
|
||||
'Header': {
|
||||
background: ColorPalette.blue,
|
||||
@@ -27,22 +27,21 @@ export default {
|
||||
text: ColorPalette.white
|
||||
},
|
||||
'LargeVideo': {
|
||||
background: ColorPalette.black
|
||||
background: 'rgb(42, 58, 75)'
|
||||
},
|
||||
'LoadConfigOverlay': {
|
||||
background: ColorPalette.black,
|
||||
text: ColorPalette.white
|
||||
background: 'rgb(249, 249, 249)',
|
||||
text: 'rgb(28, 32, 37)'
|
||||
},
|
||||
'Thumbnail': {
|
||||
activeParticipantHighlight: ColorPalette.blue,
|
||||
activeParticipantTint: ColorPalette.black,
|
||||
background: ColorPalette.black
|
||||
activeParticipantHighlight: 'rgb(81, 214, 170)',
|
||||
activeParticipantTint: 'rgba(49, 183, 106, 0.3)',
|
||||
background: 'rgb(94, 109, 122)'
|
||||
},
|
||||
'Toolbox': {
|
||||
button: getRGBAFormat(ColorPalette.white, 0.7),
|
||||
buttonToggled: getRGBAFormat(ColorPalette.buttonUnderlay, 0.7),
|
||||
buttonToggledBorder:
|
||||
getRGBAFormat(ColorPalette.buttonUnderlay, 0.7),
|
||||
hangup: ColorPalette.red
|
||||
button: 'rgb(255, 255, 255)',
|
||||
buttonToggled: 'rgba(255, 255, 255, 0)',
|
||||
buttonToggledBorder: '#a4b8d1',
|
||||
hangup: 'rgb(225, 45, 45)'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -393,7 +393,8 @@ export function createConference() {
|
||||
room.toLowerCase(), {
|
||||
...state['features/base/config'],
|
||||
applicationName: getName(),
|
||||
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats
|
||||
getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
|
||||
confID: `${locationURL.host}${locationURL.pathname}`
|
||||
});
|
||||
|
||||
connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;
|
||||
|
||||
@@ -160,7 +160,11 @@ export function getConferenceName(stateful: Function | Object): string {
|
||||
const { callDisplayName } = state['features/base/config'];
|
||||
const { pendingSubjectChange, room, subject } = state['features/base/conference'];
|
||||
|
||||
return pendingSubjectChange || subject || callDisplayName || (callee && callee.name) || _.startCase(room);
|
||||
return pendingSubjectChange
|
||||
|| subject
|
||||
|| callDisplayName
|
||||
|| (callee && callee.name)
|
||||
|| _.startCase(decodeURIComponent(room));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -42,9 +42,11 @@ export default function parseURLParams(
|
||||
|
||||
try {
|
||||
value = param[1];
|
||||
|
||||
if (!dontParse) {
|
||||
value
|
||||
= JSON.parse(decodeURIComponent(value).replace(/\\&/, '&'));
|
||||
const decoded = decodeURIComponent(value).replace(/\\&/, '&');
|
||||
|
||||
value = decoded === 'undefined' ? undefined : JSON.parse(decoded);
|
||||
}
|
||||
} catch (e) {
|
||||
reportError(
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import {
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
@@ -51,28 +52,29 @@ class BaseDialog<P: Props, S: State> extends AbstractDialog<P, S> {
|
||||
const { _dialogStyles, style } = this.props;
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { [
|
||||
styles.overlay,
|
||||
style
|
||||
] }>
|
||||
<TouchableWithoutFeedback>
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { [
|
||||
_dialogStyles.dialog,
|
||||
this.props.style
|
||||
styles.overlay,
|
||||
style
|
||||
] }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.closeWrapper }>
|
||||
<Icon
|
||||
name = 'close'
|
||||
style = { _dialogStyles.closeStyle } />
|
||||
</TouchableOpacity>
|
||||
{ this._renderContent() }
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { [
|
||||
_dialogStyles.dialog,
|
||||
this.props.style
|
||||
] }>
|
||||
<TouchableOpacity
|
||||
onPress = { this._onCancel }
|
||||
style = { styles.closeWrapper }>
|
||||
<Icon
|
||||
name = 'close'
|
||||
style = { _dialogStyles.closeStyle } />
|
||||
</TouchableOpacity>
|
||||
{ this._renderContent() }
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,16 @@ type Props = BaseProps & {
|
||||
*/
|
||||
contentKey: string,
|
||||
|
||||
/**
|
||||
* An optional initial value to initiate the field with.
|
||||
*/
|
||||
initialValue?: ?string,
|
||||
|
||||
/**
|
||||
* A message key to be shown for the user (e.g. an error that is defined after submitting the form).
|
||||
*/
|
||||
messageKey?: string,
|
||||
|
||||
t: Function,
|
||||
|
||||
textInputProps: ?Object,
|
||||
@@ -62,7 +72,7 @@ class InputDialog extends BaseDialog<Props, State> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
fieldValue: undefined
|
||||
fieldValue: props.initialValue
|
||||
};
|
||||
|
||||
this._onChangeText = this._onChangeText.bind(this);
|
||||
@@ -75,7 +85,7 @@ class InputDialog extends BaseDialog<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
_renderContent() {
|
||||
const { _dialogStyles, okDisabled, t } = this.props;
|
||||
const { _dialogStyles, messageKey, okDisabled, t } = this.props;
|
||||
|
||||
return (
|
||||
<View>
|
||||
@@ -93,6 +103,13 @@ class InputDialog extends BaseDialog<Props, State> {
|
||||
underlineColorAndroid = { FIELD_UNDERLINE }
|
||||
value = { this.state.fieldValue }
|
||||
{ ...this.props.textInputProps } />
|
||||
{ messageKey && (<Text
|
||||
style = { [
|
||||
styles.formMessage,
|
||||
_dialogStyles.text
|
||||
] }>
|
||||
{ t(messageKey) }
|
||||
</Text>) }
|
||||
</View>
|
||||
<View style = { brandedDialog.buttonWrapper }>
|
||||
<TouchableOpacity
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { StyleSheet } from 'react-native';
|
||||
|
||||
import { ColorSchemeRegistry, schemeColor } from '../../../color-scheme';
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
|
||||
import { BoxModel, ColorPalette } from '../../../styles';
|
||||
|
||||
import { PREFERRED_DIALOG_SIZE } from '../../constants';
|
||||
|
||||
@@ -50,7 +50,7 @@ export const bottomSheetStyles = {
|
||||
}
|
||||
};
|
||||
|
||||
export const brandedDialog = createStyleSheet({
|
||||
export const brandedDialog = {
|
||||
|
||||
/**
|
||||
* The style of bold {@code Text} rendered by the {@code Dialog}s of the
|
||||
@@ -95,8 +95,12 @@ export const brandedDialog = createStyleSheet({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
padding: 30
|
||||
},
|
||||
|
||||
overlayTouchable: {
|
||||
...StyleSheet.absoluteFillObject
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Reusable (colored) style for text in any branded dialogs.
|
||||
@@ -107,7 +111,7 @@ const brandedDialogText = {
|
||||
textAlign: 'center'
|
||||
};
|
||||
|
||||
export const inputDialog = createStyleSheet({
|
||||
export const inputDialog = {
|
||||
bottomField: {
|
||||
marginBottom: 0
|
||||
},
|
||||
@@ -115,8 +119,14 @@ export const inputDialog = createStyleSheet({
|
||||
fieldWrapper: {
|
||||
...brandedDialog.mainWrapper,
|
||||
paddingBottom: BoxModel.padding * 2
|
||||
},
|
||||
|
||||
formMessage: {
|
||||
alignSelf: 'flex-start',
|
||||
fontStyle: 'italic',
|
||||
margin: BoxModel.margin
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Default styles for the items of a {@code BottomSheet}-based menu.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
*
|
||||
@@ -65,6 +67,18 @@ export const PARTICIPANT_ID_CHANGED = 'PARTICIPANT_ID_CHANGED';
|
||||
*/
|
||||
export const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has been removed from a conference by
|
||||
* another participant.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_KICKED,
|
||||
* kicked: Object,
|
||||
* kicker: Object
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
|
||||
|
||||
/**
|
||||
* Action to handle case when participant lefts.
|
||||
*
|
||||
@@ -120,3 +134,17 @@ export const HIDDEN_PARTICIPANT_JOINED = 'HIDDEN_PARTICIPANT_JOINED';
|
||||
* }
|
||||
*/
|
||||
export const HIDDEN_PARTICIPANT_LEFT = 'HIDDEN_PARTICIPANT_LEFT';
|
||||
|
||||
/**
|
||||
* The type of Redux action which notifies the app that the loadable avatar URL has changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
loadableAvatarUrl: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import throttle from 'lodash/throttle';
|
||||
|
||||
import { set } from '../redux';
|
||||
import { NOTIFICATION_TIMEOUT, showNotification } from '../../notifications';
|
||||
|
||||
@@ -11,9 +9,11 @@ import {
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_KICKED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT
|
||||
PIN_PARTICIPANT,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
@@ -417,6 +417,12 @@ export function participantMutedUs(participant) {
|
||||
export function participantKicked(kicker, kicked) {
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({
|
||||
type: PARTICIPANT_KICKED,
|
||||
kicked: kicked.getId(),
|
||||
kicker: kicker.getId()
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
titleArguments: {
|
||||
kicked:
|
||||
@@ -451,71 +457,24 @@ export function pinParticipant(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of names of participants that have joined the conference. The array
|
||||
* is replaced with an empty array as notifications are displayed.
|
||||
* Creates an action which notifies the app that the loadable URL of the avatar of a participant got updated.
|
||||
*
|
||||
* @private
|
||||
* @type {string[]}
|
||||
*/
|
||||
let joinedParticipantsNames = [];
|
||||
|
||||
/**
|
||||
* A throttled internal function that takes the internal list of participant
|
||||
* names, {@code joinedParticipantsNames}, and triggers the display of a
|
||||
* notification informing of their joining.
|
||||
*
|
||||
* @private
|
||||
* @type {Function}
|
||||
*/
|
||||
const _throttledNotifyParticipantConnected = throttle(dispatch => {
|
||||
const joinedParticipantsCount = joinedParticipantsNames.length;
|
||||
|
||||
let notificationProps;
|
||||
|
||||
if (joinedParticipantsCount >= 3) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0],
|
||||
count: joinedParticipantsCount - 1
|
||||
},
|
||||
titleKey: 'notify.connectedThreePlusMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount === 2) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
first: joinedParticipantsNames[0],
|
||||
second: joinedParticipantsNames[1]
|
||||
},
|
||||
titleKey: 'notify.connectedTwoMembers'
|
||||
};
|
||||
} else if (joinedParticipantsCount) {
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: joinedParticipantsNames[0]
|
||||
},
|
||||
titleKey: 'notify.connectedOneMember'
|
||||
};
|
||||
}
|
||||
|
||||
if (notificationProps) {
|
||||
dispatch(
|
||||
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
|
||||
}
|
||||
|
||||
joinedParticipantsNames = [];
|
||||
|
||||
}, 500, { leading: false });
|
||||
|
||||
/**
|
||||
* Queues the display of a notification of a participant having connected to
|
||||
* the meeting. The notifications are batched so that quick consecutive
|
||||
* connection events are shown in one notification.
|
||||
*
|
||||
* @param {string} displayName - The name of the participant that connected.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function showParticipantJoinedNotification(displayName) {
|
||||
joinedParticipantsNames.push(displayName);
|
||||
|
||||
return dispatch => _throttledNotifyParticipantConnected(dispatch);
|
||||
* @param {string} participantId - The ID of the participant.
|
||||
* @param {string} url - The new URL.
|
||||
* @returns {{
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* loadableAvatarUrl: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function setLoadableAvatarUrl(participantId, url) {
|
||||
return {
|
||||
type: SET_LOADABLE_AVATAR_URL,
|
||||
participant: {
|
||||
id: participantId,
|
||||
loadableAvatarUrl: url
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component, Fragment, PureComponent } from 'react';
|
||||
import { Dimensions, Image, Platform, View } from 'react-native';
|
||||
import FastImage, {
|
||||
type CacheControls,
|
||||
type Priorities
|
||||
} from 'react-native-fast-image';
|
||||
|
||||
import { ColorPalette } from '../../styles';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The default image/source to be used in case none is specified or the
|
||||
* specified one fails to load.
|
||||
*
|
||||
* XXX The relative path to the default/stock (image) file is defined by the
|
||||
* {@code const} {@code DEFAULT_AVATAR_RELATIVE_PATH}. Unfortunately, the
|
||||
* packager of React Native cannot deal with it early enough for the following
|
||||
* {@code require} to succeed at runtime. Anyway, be sure to synchronize the
|
||||
* relative path on Web and mobile for the purposes of consistency.
|
||||
*
|
||||
* @private
|
||||
* @type {string}
|
||||
*/
|
||||
const _DEFAULT_SOURCE = require('../../../../../images/avatar.png');
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link Avatar}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The size for the {@link Avatar}.
|
||||
*/
|
||||
size: number,
|
||||
|
||||
|
||||
/**
|
||||
* The URI of the {@link Avatar}.
|
||||
*/
|
||||
uri: string
|
||||
};
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} state of {@link Avatar}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Background color for the locally generated avatar.
|
||||
*/
|
||||
backgroundColor: string,
|
||||
|
||||
/**
|
||||
* Error indicator for non-local avatars.
|
||||
*/
|
||||
error: boolean,
|
||||
|
||||
/**
|
||||
* Indicates if the non-local avatar was loaded or not.
|
||||
*/
|
||||
loaded: boolean,
|
||||
|
||||
/**
|
||||
* Source for the non-local avatar.
|
||||
*/
|
||||
source: {
|
||||
uri?: string,
|
||||
headers?: Object,
|
||||
priority?: Priorities,
|
||||
cache?: CacheControls,
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements a React Native/mobile {@link Component} wich renders the content
|
||||
* of an Avatar.
|
||||
*/
|
||||
class AvatarContent extends Component<Props, State> {
|
||||
/**
|
||||
* Initializes a new Avatar instance.
|
||||
*
|
||||
* @param {Props} props - The read-only React Component props with which
|
||||
* the new instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
// Set the image source. The logic for the character # below is as
|
||||
// follows:
|
||||
// - Technically, URI is supposed to start with a scheme and scheme
|
||||
// cannot contain the character #.
|
||||
// - Technically, the character # in a URI signals the start of the
|
||||
// fragment/hash.
|
||||
// - Technically, the fragment/hash does not imply a retrieval
|
||||
// action.
|
||||
// - Practically, the fragment/hash does not always mandate a
|
||||
// retrieval action. For example, an HTML anchor with an href that
|
||||
// starts with the character # does not cause a Web browser to
|
||||
// initiate a retrieval action.
|
||||
// So I'll use the character # at the start of URI to not initiate
|
||||
// an image retrieval action.
|
||||
const source = {};
|
||||
|
||||
if (props.uri && !props.uri.startsWith('#')) {
|
||||
source.uri = props.uri;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
backgroundColor: this._getBackgroundColor(props),
|
||||
error: false,
|
||||
loaded: false,
|
||||
source
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onAvatarLoaded = this._onAvatarLoaded.bind(this);
|
||||
this._onAvatarLoadError = this._onAvatarLoadError.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes if the default avatar (ie, locally generated) should be used
|
||||
* or not.
|
||||
*/
|
||||
get useDefaultAvatar() {
|
||||
const { error, loaded, source } = this.state;
|
||||
|
||||
return !source.uri || error || !loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash over the URI and returns a HSL background color. We use
|
||||
* 75% as lightness, for nice pastel style colors.
|
||||
*
|
||||
* @param {Object} props - The read-only React {@code Component} props from
|
||||
* which the background color is to be generated.
|
||||
* @private
|
||||
* @returns {string} - The HSL CSS property.
|
||||
*/
|
||||
_getBackgroundColor({ uri }) {
|
||||
if (!uri) {
|
||||
return ColorPalette.white;
|
||||
}
|
||||
|
||||
let hash = 0;
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
for (let i = 0; i < uri.length; i++) {
|
||||
hash = uri.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash |= 0; // Convert to 32-bit integer
|
||||
}
|
||||
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return `hsl(${hash % 360}, 100%, 75%)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper which computes the style for the {@code Image} / {@code FastImage}
|
||||
* component.
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getImageStyle() {
|
||||
const { size } = this.props;
|
||||
|
||||
return {
|
||||
...styles.avatar,
|
||||
borderRadius: size / 2,
|
||||
height: size,
|
||||
width: size
|
||||
};
|
||||
}
|
||||
|
||||
_onAvatarLoaded: () => void;
|
||||
|
||||
/**
|
||||
* Handler called when the remote image loading finishes. This doesn't
|
||||
* necessarily mean the load was successful.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoaded() {
|
||||
this.setState({ loaded: true });
|
||||
}
|
||||
|
||||
_onAvatarLoadError: () => void;
|
||||
|
||||
/**
|
||||
* Handler called when the remote image loading failed.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onAvatarLoadError() {
|
||||
this.setState({ error: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a default, locally generated avatar image.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderDefaultAvatar() {
|
||||
// When using a local image, react-native-fastimage falls back to a
|
||||
// regular Image, so we need to wrap it in a view to make it round.
|
||||
// https://github.com/facebook/react-native/issues/3198
|
||||
|
||||
const { backgroundColor } = this.state;
|
||||
const imageStyle = this._getImageStyle();
|
||||
const viewStyle = {
|
||||
...imageStyle,
|
||||
|
||||
backgroundColor,
|
||||
|
||||
// FIXME @lyubomir: Without the opacity below I feel like the
|
||||
// avatar colors are too strong. Besides, we use opacity for the
|
||||
// ToolbarButtons. That's where I copied the value from and we
|
||||
// may want to think about "standardizing" the opacity in the
|
||||
// app in a way similar to ColorPalette.
|
||||
opacity: 0.1,
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
return (
|
||||
<View style = { viewStyle }>
|
||||
<Image
|
||||
|
||||
// The Image adds a fade effect without asking, so lets
|
||||
// explicitly disable it. More info here:
|
||||
// https://github.com/facebook/react-native/issues/10194
|
||||
fadeDuration = { 0 }
|
||||
resizeMode = 'contain'
|
||||
source = { _DEFAULT_SOURCE }
|
||||
style = { imageStyle } />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an avatar using a remote image.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderAvatar() {
|
||||
const { source } = this.state;
|
||||
let extraStyle;
|
||||
|
||||
if (this.useDefaultAvatar) {
|
||||
// On Android, the image loading indicators don't work unless the
|
||||
// Glide image is actually created, so we cannot use display: none.
|
||||
// Instead, render it off-screen, which does the trick.
|
||||
if (Platform.OS === 'android') {
|
||||
const windowDimensions = Dimensions.get('window');
|
||||
|
||||
extraStyle = {
|
||||
bottom: -windowDimensions.height,
|
||||
right: -windowDimensions.width
|
||||
};
|
||||
} else {
|
||||
extraStyle = { display: 'none' };
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
onError = { this._onAvatarLoadError }
|
||||
onLoadEnd = { this._onAvatarLoaded }
|
||||
resizeMode = 'contain'
|
||||
source = { source }
|
||||
style = { [ this._getImageStyle(), extraStyle ] } />
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
const { source } = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{ source.uri && this._renderAvatar() }
|
||||
{ this.useDefaultAvatar && this._renderDefaultAvatar() }
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
|
||||
/**
|
||||
* Implements an avatar as a React Native/mobile {@link Component}.
|
||||
*
|
||||
* Note: we use `key` in order to trigger a new component creation in case
|
||||
* the URI changes.
|
||||
*/
|
||||
export default class Avatar extends PureComponent<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<AvatarContent
|
||||
key = { this.props.uri }
|
||||
{ ...this.props } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link Avatar}.
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The URI of the {@link Avatar}.
|
||||
*/
|
||||
uri: string
|
||||
};
|
||||
|
||||
/**
|
||||
* Implements an avatar as a React/Web {@link Component}.
|
||||
*/
|
||||
export default class Avatar extends Component<Props> {
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
render() {
|
||||
// Propagate all props of this Avatar but the ones consumed by this
|
||||
// Avatar to the img it renders.
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const { uri, ...props } = this.props;
|
||||
|
||||
return (
|
||||
<img
|
||||
{ ...props }
|
||||
src = { uri } />
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
|
||||
import { Avatar } from '../../avatar';
|
||||
import { translate } from '../../i18n';
|
||||
import { JitsiParticipantConnectionStatus } from '../../lib-jitsi-meet';
|
||||
import {
|
||||
@@ -16,13 +16,7 @@ import { StyleType } from '../../styles';
|
||||
import { TestHint } from '../../testing/components';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../../tracks';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import {
|
||||
getAvatarURL,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName,
|
||||
shouldRenderParticipantVideo
|
||||
} from '../functions';
|
||||
import { shouldRenderParticipantVideo } from '../functions';
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
@@ -30,14 +24,6 @@ import styles from './styles';
|
||||
*/
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* The source (e.g. URI, URL) of the avatar image of the participant with
|
||||
* {@link #participantId}.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_avatar: string,
|
||||
|
||||
/**
|
||||
* The connection status of the participant. Her video will only be rendered
|
||||
* if the connection status is 'active'; otherwise, the avatar will be
|
||||
@@ -192,7 +178,6 @@ class ParticipantView extends Component<Props> {
|
||||
*/
|
||||
render() {
|
||||
const {
|
||||
_avatar: avatar,
|
||||
_connectionStatus: connectionStatus,
|
||||
_renderVideo: renderVideo,
|
||||
_videoTrack: videoTrack,
|
||||
@@ -202,9 +187,6 @@ class ParticipantView extends Component<Props> {
|
||||
|
||||
const waitForVideoStarted = false;
|
||||
|
||||
// Is the avatar to be rendered?
|
||||
const renderAvatar = Boolean(!renderVideo && avatar);
|
||||
|
||||
// If the connection has problems, we will "tint" the video / avatar.
|
||||
const connectionProblem
|
||||
= connectionStatus !== JitsiParticipantConnectionStatus.ACTIVE;
|
||||
@@ -238,10 +220,12 @@ class ParticipantView extends Component<Props> {
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
|
||||
{ renderAvatar
|
||||
&& <Avatar
|
||||
size = { this.props.avatarSize }
|
||||
uri = { avatar } /> }
|
||||
{ !renderVideo
|
||||
&& <View style = { styles.avatarContainer }>
|
||||
<Avatar
|
||||
participantId = { this.props.participantId }
|
||||
size = { this.props.avatarSize } />
|
||||
</View> }
|
||||
|
||||
{ useTint
|
||||
|
||||
@@ -265,45 +249,14 @@ class ParticipantView extends Component<Props> {
|
||||
* @param {Object} ownProps - The React {@code Component} props passed to the
|
||||
* associated (instance of) {@code ParticipantView}.
|
||||
* @private
|
||||
* @returns {{
|
||||
* _avatar: string,
|
||||
* _connectionStatus: string,
|
||||
* _participantName: string,
|
||||
* _renderVideo: boolean,
|
||||
* _videoTrack: Track
|
||||
* }}
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state, ownProps) {
|
||||
const { participantId } = ownProps;
|
||||
const participant = getParticipantById(state, participantId);
|
||||
let avatar;
|
||||
let connectionStatus;
|
||||
let participantName;
|
||||
|
||||
if (participant) {
|
||||
avatar = getAvatarURL(participant);
|
||||
connectionStatus = participant.connectionStatus;
|
||||
participantName = getParticipantDisplayName(state, participant.id);
|
||||
|
||||
// Avatar (on React Native) now has the ability to generate an
|
||||
// automatically-colored default image when no URI/URL is specified or
|
||||
// when it fails to load. In order to make the coloring permanent(ish)
|
||||
// per participant, Avatar will need something permanent(ish) per
|
||||
// perticipant, obviously. A participant's ID is such a piece of data.
|
||||
// But the local participant changes her ID as she joins, leaves.
|
||||
// TODO @lyubomir: The participants may change their avatar URLs at
|
||||
// runtime which means that, if their old and new avatar URLs fail to
|
||||
// download, Avatar will change their automatically-generated colors.
|
||||
avatar || participant.local || (avatar = `#${participant.id}`);
|
||||
|
||||
// ParticipantView knows before Avatar that an avatar URL will be used
|
||||
// so it's advisable to prefetch here.
|
||||
avatar && !avatar.startsWith('#')
|
||||
&& FastImage.preload([ { uri: avatar } ]);
|
||||
}
|
||||
|
||||
return {
|
||||
_avatar: avatar,
|
||||
_connectionStatus:
|
||||
connectionStatus
|
||||
|| JitsiParticipantConnectionStatus.ACTIVE,
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export { default as Avatar } from './Avatar';
|
||||
// @flow
|
||||
|
||||
export { default as ParticipantView } from './ParticipantView';
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../styles';
|
||||
// @flow
|
||||
|
||||
import { BoxModel, ColorPalette } from '../../styles';
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default createStyleSheet({
|
||||
export default {
|
||||
/**
|
||||
* The style of the avatar of the participant.
|
||||
* Container for the avatar in the view.
|
||||
*/
|
||||
avatar: {
|
||||
alignSelf: 'center',
|
||||
flex: 0
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -42,4 +44,4 @@ export default createStyleSheet({
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,87 +1,66 @@
|
||||
// @flow
|
||||
import { getAvatarURL as _getAvatarURL } from 'js-utils/avatar';
|
||||
import { getGravatarURL } from 'js-utils/avatar';
|
||||
|
||||
import { toState } from '../redux';
|
||||
|
||||
import { JitsiParticipantConnectionStatus } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE, shouldRenderVideoTrack } from '../media';
|
||||
import { getTrackByMediaTypeAndParticipant } from '../tracks';
|
||||
import { createDeferred } from '../util';
|
||||
|
||||
import {
|
||||
DEFAULT_AVATAR_RELATIVE_PATH,
|
||||
LOCAL_PARTICIPANT_DEFAULT_ID,
|
||||
MAX_DISPLAY_NAME_LENGTH,
|
||||
PARTICIPANT_ROLE
|
||||
} from './constants';
|
||||
import { preloadImage } from './preloadImage';
|
||||
|
||||
declare var config: Object;
|
||||
declare var interfaceConfig: Object;
|
||||
|
||||
/**
|
||||
* Returns the URL of the image for the avatar of a specific participant.
|
||||
*
|
||||
* @param {Participant} [participant] - The participant to return the avatar URL
|
||||
* of.
|
||||
* @param {string} [participant.avatarID] - Participant's avatar ID.
|
||||
* @param {string} [participant.avatarURL] - Participant's avatar URL.
|
||||
* @param {string} [participant.email] - Participant's e-mail address.
|
||||
* @param {string} [participant.id] - Participant's ID.
|
||||
* @public
|
||||
* @returns {string} The URL of the image for the avatar of the specified
|
||||
* participant.
|
||||
* Temp structures for avatar urls to be checked/preloaded.
|
||||
*/
|
||||
export function getAvatarURL({ avatarID, avatarURL, email, id }: {
|
||||
avatarID: string,
|
||||
avatarURL: string,
|
||||
email: string,
|
||||
id: string
|
||||
}) {
|
||||
// If disableThirdPartyRequests disables third-party avatar services, we are
|
||||
// restricted to a stock image of ours.
|
||||
if (typeof config === 'object' && config.disableThirdPartyRequests) {
|
||||
return DEFAULT_AVATAR_RELATIVE_PATH;
|
||||
const AVATAR_QUEUE = [];
|
||||
const AVATAR_CHECKED_URLS = new Map();
|
||||
/* eslint-disable arrow-body-style */
|
||||
const AVATAR_CHECKER_FUNCTIONS = [
|
||||
participant => {
|
||||
return participant && participant.avatarURL ? participant.avatarURL : null;
|
||||
},
|
||||
participant => {
|
||||
return participant && participant.email ? getGravatarURL(participant.email) : null;
|
||||
}
|
||||
|
||||
// If an avatarURL is specified, then obviously there's nothing to generate.
|
||||
if (avatarURL) {
|
||||
return avatarURL;
|
||||
}
|
||||
|
||||
// The deployment is allowed to choose the avatar service which is to
|
||||
// generate the random avatars.
|
||||
const avatarService
|
||||
= typeof interfaceConfig === 'object'
|
||||
&& interfaceConfig.RANDOM_AVATAR_URL_PREFIX
|
||||
? {
|
||||
urlPrefix: interfaceConfig.RANDOM_AVATAR_URL_PREFIX,
|
||||
urlSuffix: interfaceConfig.RANDOM_AVATAR_URL_SUFFIX }
|
||||
: undefined;
|
||||
|
||||
// eslint-disable-next-line object-property-newline
|
||||
return _getAvatarURL({ avatarID, email, id }, avatarService);
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the avatarURL for the participant associated with the passed in
|
||||
* participant ID.
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {(Function|Object|Participant[])} stateful - The redux state
|
||||
* features/base/participants, the (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @param {string} id - The ID of the participant to retrieve.
|
||||
* @param {boolean} isLocal - An optional parameter indicating whether or not
|
||||
* the partcipant id is for the local user. If true, a different logic flow is
|
||||
* used find the local user, ignoring the id value as it can change through the
|
||||
* beginning and end of a call.
|
||||
* @returns {(string|undefined)}
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getAvatarURLByParticipantId(
|
||||
stateful: Object | Function,
|
||||
id: string = LOCAL_PARTICIPANT_DEFAULT_ID) {
|
||||
const participant = getParticipantById(stateful, id);
|
||||
export function getFirstLoadableAvatarUrl(participant: Object) {
|
||||
const deferred = createDeferred();
|
||||
const fullPromise = deferred.promise
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant))
|
||||
.then(src => {
|
||||
|
||||
return participant && getAvatarURL(participant);
|
||||
if (AVATAR_QUEUE.length) {
|
||||
const next = AVATAR_QUEUE.shift();
|
||||
|
||||
next.resolve();
|
||||
}
|
||||
|
||||
return src;
|
||||
});
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
AVATAR_QUEUE.push(deferred);
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return fullPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,7 +148,6 @@ export function getParticipantCountWithFake(stateful: Object | Function) {
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {string} id - The ID of the participant's display name to retrieve.
|
||||
* @private
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getParticipantDisplayName(
|
||||
@@ -346,3 +324,35 @@ export function shouldRenderParticipantVideo(
|
||||
&& shouldRenderVideoTrack(videoTrack, waitForVideoStarted);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @returns {?string}
|
||||
*/
|
||||
async function _getFirstLoadableAvatarUrl(participant) {
|
||||
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
|
||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant);
|
||||
|
||||
if (url) {
|
||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||
if (AVATAR_CHECKED_URLS.get(url)) {
|
||||
return url;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const finalUrl = await preloadImage(url);
|
||||
|
||||
AVATAR_CHECKED_URLS.set(finalUrl, true);
|
||||
|
||||
return finalUrl;
|
||||
} catch (e) {
|
||||
AVATAR_CHECKED_URLS.set(url, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
localParticipantLeft,
|
||||
participantLeft,
|
||||
participantUpdated,
|
||||
showParticipantJoinedNotification
|
||||
setLoadableAvatarUrl
|
||||
} from './actions';
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
@@ -38,8 +38,9 @@ import {
|
||||
PARTICIPANT_LEFT_SOUND_ID
|
||||
} from './constants';
|
||||
import {
|
||||
getAvatarURLByParticipantId,
|
||||
getFirstLoadableAvatarUrl,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getParticipantDisplayName
|
||||
} from './functions';
|
||||
@@ -118,15 +119,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
case PARTICIPANT_JOINED: {
|
||||
_maybePlaySounds(store, action);
|
||||
|
||||
const result = _participantJoinedOrUpdated(store, next, action);
|
||||
|
||||
const { participant: p } = action;
|
||||
|
||||
if (!p.local) {
|
||||
store.dispatch(showParticipantJoinedNotification(getParticipantDisplayName(store.getState, p.id)));
|
||||
}
|
||||
|
||||
return result;
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT:
|
||||
@@ -203,6 +196,13 @@ StateListenerRegistry.register(
|
||||
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
||||
(participant, propertyName, oldValue, newValue) => {
|
||||
switch (propertyName) {
|
||||
case 'features_jigasi':
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
isJigasi: newValue
|
||||
}));
|
||||
break;
|
||||
case 'features_screen-sharing':
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
@@ -323,8 +323,8 @@ function _maybePlaySounds({ getState, dispatch }, action) {
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _participantJoinedOrUpdated({ getState }, next, action) {
|
||||
const { participant: { id, local, raisedHand } } = action;
|
||||
function _participantJoinedOrUpdated({ dispatch, getState }, next, action) {
|
||||
const { participant: { avatarURL, email, id, local, name, raisedHand } } = action;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
@@ -339,26 +339,29 @@ function _participantJoinedOrUpdated({ getState }, next, action) {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const oldAvatarURL = getAvatarURLByParticipantId(getState(), id);
|
||||
// Allow the redux update to go through and compare the old avatar
|
||||
// to the new avatar and emit out change events if necessary.
|
||||
const result = next(action);
|
||||
|
||||
// Allow the redux update to go through and compare the old avatar
|
||||
// to the new avatar and emit out change events if necessary.
|
||||
const result = next(action);
|
||||
const newAvatarURL = getAvatarURLByParticipantId(getState(), id);
|
||||
if (avatarURL || email || id || name) {
|
||||
const participantId = !id && local ? getLocalParticipant(getState()).id : id;
|
||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||
|
||||
if (oldAvatarURL !== newAvatarURL) {
|
||||
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
||||
|
||||
APP.UI.refreshAvatarDisplay(currentKnownId, newAvatarURL);
|
||||
APP.API.notifyAvatarChanged(currentKnownId, newAvatarURL);
|
||||
}
|
||||
|
||||
return result;
|
||||
getFirstLoadableAvatarUrl(updatedParticipant)
|
||||
.then(url => {
|
||||
dispatch(setLoadableAvatarUrl(participantId, url));
|
||||
});
|
||||
}
|
||||
|
||||
return next(action);
|
||||
// Notify external listeners of potential avatarURL changes.
|
||||
if (typeof APP === 'object') {
|
||||
const currentKnownId = local ? APP.conference.getMyUserId() : id;
|
||||
|
||||
// Force update of local video getting a new id.
|
||||
APP.UI.refreshAvatarDisplay(currentKnownId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
16
react/features/base/participants/preloadImage.native.js
Normal file
16
react/features/base/participants/preloadImage.native.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
// @flow
|
||||
|
||||
import { Image } from 'react-native';
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string} src - Source of the avatar.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.prefetch(src).then(() => resolve(src), reject);
|
||||
});
|
||||
}
|
||||
24
react/features/base/participants/preloadImage.web.js
Normal file
24
react/features/base/participants/preloadImage.web.js
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
// @flow
|
||||
|
||||
declare var config: Object;
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string} src - Source of the avatar.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string): Promise<string> {
|
||||
if (typeof config === 'object' && config.disableThirdPartyRequests) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
image.onload = () => resolve(src);
|
||||
image.onerror = reject;
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { randomHexString } from 'js-utils/random';
|
||||
|
||||
import { ReducerRegistry, set } from '../redux';
|
||||
|
||||
import {
|
||||
@@ -10,7 +8,8 @@ import {
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT
|
||||
PIN_PARTICIPANT,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||
|
||||
@@ -65,6 +64,7 @@ const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
*/
|
||||
ReducerRegistry.register('features/base/participants', (state = [], action) => {
|
||||
switch (action.type) {
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case DOMINANT_SPEAKER_CHANGED:
|
||||
case PARTICIPANT_ID_CHANGED:
|
||||
case PARTICIPANT_UPDATED:
|
||||
@@ -133,6 +133,7 @@ function _participant(state: Object = {}, action) {
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { participant } = action; // eslint-disable-line no-shadow
|
||||
let { id } = participant;
|
||||
@@ -180,26 +181,24 @@ function _participant(state: Object = {}, action) {
|
||||
*/
|
||||
function _participantJoined({ participant }) {
|
||||
const {
|
||||
avatarID,
|
||||
avatarURL,
|
||||
botType,
|
||||
connectionStatus,
|
||||
dominantSpeaker,
|
||||
email,
|
||||
isFakeParticipant,
|
||||
isJigasi,
|
||||
loadableAvatarUrl,
|
||||
local,
|
||||
name,
|
||||
pinned,
|
||||
presence,
|
||||
role
|
||||
} = participant;
|
||||
let { avatarID, conference, id } = participant;
|
||||
let { conference, id } = participant;
|
||||
|
||||
if (local) {
|
||||
// avatarID
|
||||
//
|
||||
// TODO Get the avatarID of the local participant from localStorage.
|
||||
avatarID || (avatarID = randomHexString(32));
|
||||
|
||||
// conference
|
||||
//
|
||||
// XXX The local participant is not identified in association with a
|
||||
@@ -221,6 +220,8 @@ function _participantJoined({ participant }) {
|
||||
email,
|
||||
id,
|
||||
isFakeParticipant,
|
||||
isJigasi,
|
||||
loadableAvatarUrl,
|
||||
local: local || false,
|
||||
name,
|
||||
pinned: pinned || false,
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Text } from 'react-native';
|
||||
|
||||
import { Icon } from '../../../font-icons';
|
||||
import { Avatar } from '../../../participants';
|
||||
import { Avatar } from '../../../avatar';
|
||||
import { StyleType } from '../../../styles';
|
||||
|
||||
import { type Item } from '../../Types';
|
||||
@@ -70,44 +69,6 @@ export default class AvatarListItem extends Component<Props> {
|
||||
this._renderItemLine = this._renderItemLine.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to render the content in the avatar container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
_getAvatarContent() {
|
||||
const {
|
||||
avatarSize = AVATAR_SIZE,
|
||||
avatarTextStyle
|
||||
} = this.props;
|
||||
const { avatar, title } = this.props.item;
|
||||
const isAvatarURL = Boolean(avatar && avatar.match(/^http[s]*:\/\//i));
|
||||
|
||||
if (isAvatarURL) {
|
||||
return (
|
||||
<Avatar
|
||||
size = { avatarSize }
|
||||
uri = { avatar } />
|
||||
);
|
||||
}
|
||||
|
||||
if (avatar && !isAvatarURL) {
|
||||
return (
|
||||
<Icon name = { avatar } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
style = { [
|
||||
styles.avatarContent,
|
||||
avatarTextStyle
|
||||
] }>
|
||||
{ title.substr(0, 1).toUpperCase() }
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements {@code Component#render}.
|
||||
*
|
||||
@@ -118,26 +79,19 @@ export default class AvatarListItem extends Component<Props> {
|
||||
avatarSize = AVATAR_SIZE,
|
||||
avatarStyle
|
||||
} = this.props;
|
||||
const { colorBase, lines, title } = this.props.item;
|
||||
const avatarStyles = {
|
||||
...styles.avatar,
|
||||
...this._getAvatarColor(colorBase),
|
||||
...avatarStyle,
|
||||
borderRadius: avatarSize / 2,
|
||||
height: avatarSize,
|
||||
width: avatarSize
|
||||
};
|
||||
const { avatar, colorBase, lines, title } = this.props.item;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { this.props.onPress }
|
||||
style = { styles.listItem }
|
||||
underlayColor = { UNDERLAY_COLOR }>
|
||||
<Container style = { styles.avatarContainer }>
|
||||
<Container style = { avatarStyles }>
|
||||
{ this._getAvatarContent() }
|
||||
</Container>
|
||||
</Container>
|
||||
<Avatar
|
||||
colorBase = { colorBase }
|
||||
displayName = { title }
|
||||
size = { avatarSize }
|
||||
style = { avatarStyle }
|
||||
url = { avatar } />
|
||||
<Container style = { styles.listItemDetails }>
|
||||
<Text
|
||||
numberOfLines = { 1 }
|
||||
@@ -155,27 +109,6 @@ export default class AvatarListItem extends Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a style (color) based on the string that determines the color of
|
||||
* the avatar.
|
||||
*
|
||||
* @param {string} colorBase - The string that is the base of the color.
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_getAvatarColor(colorBase) {
|
||||
if (!colorBase) {
|
||||
return null;
|
||||
}
|
||||
let nameHash = 0;
|
||||
|
||||
for (let i = 0; i < colorBase.length; i++) {
|
||||
nameHash += colorBase.codePointAt(i);
|
||||
}
|
||||
|
||||
return styles[`avatarColor${(nameHash % 5) + 1}`];
|
||||
}
|
||||
|
||||
_renderItemLine: (string, number) => React$Node;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ColorPalette } from '../../../styles';
|
||||
import type { Item } from '../../Types';
|
||||
|
||||
import AvatarListItem from './AvatarListItem';
|
||||
import Container from './Container';
|
||||
import Text from './Text';
|
||||
import styles from './styles';
|
||||
|
||||
@@ -92,6 +93,24 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||
return lines && lines.length ? lines.map(this._renderItemLine) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the secondary action label.
|
||||
*
|
||||
* @private
|
||||
* @returns {React$Node}
|
||||
*/
|
||||
_renderSecondaryAction() {
|
||||
const { secondaryAction } = this.props;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { secondaryAction }
|
||||
style = { styles.secondaryActionContainer }>
|
||||
<Text style = { styles.secondaryActionLabel }>+</Text>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
@@ -124,7 +143,10 @@ export default class NavigateSectionListItem extends Component<Props> {
|
||||
right = { right }>
|
||||
<AvatarListItem
|
||||
item = { item }
|
||||
onPress = { this.props.onPress } />
|
||||
onPress = { this.props.onPress } >
|
||||
{ this.props.secondaryAction
|
||||
&& this._renderSecondaryAction() }
|
||||
</AvatarListItem>
|
||||
</Swipeout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
View
|
||||
} from 'react-native';
|
||||
|
||||
import { BackButtonRegistry } from '../../../../mobile/back-button';
|
||||
|
||||
import { type StyleType } from '../../../styles';
|
||||
|
||||
import styles from './slidingviewstyles';
|
||||
@@ -110,6 +112,7 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
this._onHardwareBackPress = this._onHardwareBackPress.bind(this);
|
||||
this._onHide = this._onHide.bind(this);
|
||||
}
|
||||
|
||||
@@ -119,6 +122,8 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidMount() {
|
||||
BackButtonRegistry.addListener(this._onHardwareBackPress, true);
|
||||
|
||||
this._mounted = true;
|
||||
this._setShow(this.props.show);
|
||||
}
|
||||
@@ -128,8 +133,12 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentDidUpdate() {
|
||||
this._setShow(this.props.show);
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { show } = this.props;
|
||||
|
||||
if (prevProps.show !== show) {
|
||||
this._setShow(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,6 +147,8 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
* @inheritdoc
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
BackButtonRegistry.removeListener(this._onHardwareBackPress);
|
||||
|
||||
this._mounted = false;
|
||||
}
|
||||
|
||||
@@ -211,6 +222,23 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
return style;
|
||||
}
|
||||
|
||||
_onHardwareBackPress: () => boolean;
|
||||
|
||||
/**
|
||||
* Callback to handle the hardware back button.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_onHardwareBackPress() {
|
||||
const { onHide } = this.props;
|
||||
|
||||
if (typeof onHide === 'function') {
|
||||
return onHide();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_onHide: () => void;
|
||||
|
||||
/**
|
||||
@@ -264,7 +292,9 @@ export default class SlidingView extends PureComponent<Props, State> {
|
||||
})
|
||||
.start(({ finished }) => {
|
||||
finished && this._mounted && !show
|
||||
&& this.setState({ showOverlay: false });
|
||||
&& this.setState({ showOverlay: false }, () => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
|
||||
|
||||
const AVATAR_OPACITY = 0.4;
|
||||
const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
|
||||
const SECONDARY_ACTION_BUTTON_SIZE = 30;
|
||||
|
||||
export const AVATAR_SIZE = 65;
|
||||
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
|
||||
@@ -92,40 +92,6 @@ const PAGED_LIST_STYLES = {
|
||||
};
|
||||
|
||||
const SECTION_LIST_STYLES = {
|
||||
/**
|
||||
* The style of the actual avatar.
|
||||
*/
|
||||
avatar: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(23, 160, 219, ${AVATAR_OPACITY})`,
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* List of styles of the avatar of a remote meeting (not the default
|
||||
* server). The number of colors are limited because they should match
|
||||
* nicely.
|
||||
*/
|
||||
avatarColor1: {
|
||||
backgroundColor: `rgba(232, 105, 156, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor2: {
|
||||
backgroundColor: `rgba(255, 198, 115, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor3: {
|
||||
backgroundColor: `rgba(128, 128, 255, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor4: {
|
||||
backgroundColor: `rgba(105, 232, 194, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
avatarColor5: {
|
||||
backgroundColor: `rgba(234, 255, 128, ${AVATAR_OPACITY})`
|
||||
},
|
||||
|
||||
/**
|
||||
* The style of the avatar container that makes the avatar rounded.
|
||||
*/
|
||||
@@ -217,6 +183,21 @@ const SECTION_LIST_STYLES = {
|
||||
color: OVERLAY_FONT_COLOR
|
||||
},
|
||||
|
||||
secondaryActionContainer: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: ColorPalette.blue,
|
||||
borderRadius: 3,
|
||||
height: SECONDARY_ACTION_BUTTON_SIZE,
|
||||
justifyContent: 'center',
|
||||
margin: BoxModel.margin * 0.5,
|
||||
marginRight: BoxModel.margin,
|
||||
width: SECONDARY_ACTION_BUTTON_SIZE
|
||||
},
|
||||
|
||||
secondaryActionLabel: {
|
||||
color: ColorPalette.white
|
||||
},
|
||||
|
||||
touchableView: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
|
||||
@@ -579,30 +579,9 @@ function _onCreateLocalTracksRejected({ gum }, device) {
|
||||
const { error } = gum;
|
||||
|
||||
if (error) {
|
||||
// FIXME For whatever reason (which is probably an
|
||||
// implementation fault), react-native-webrtc will give the
|
||||
// error in one of the following formats depending on whether it
|
||||
// is attached to a remote debugger or not. (The remote debugger
|
||||
// scenario suggests that react-native-webrtc is at fault
|
||||
// because the remote debugger is Google Chrome and then its
|
||||
// JavaScript engine will define DOMException. I suspect I wrote
|
||||
// react-native-webrtc to return the error in the alternative
|
||||
// format if DOMException is not defined.)
|
||||
let trackPermissionError;
|
||||
|
||||
switch (error.name) {
|
||||
case 'DOMException':
|
||||
trackPermissionError = error.message === 'NotAllowedError';
|
||||
break;
|
||||
|
||||
case 'NotAllowedError':
|
||||
trackPermissionError = error instanceof DOMException;
|
||||
break;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: TRACK_CREATE_ERROR,
|
||||
permissionDenied: trackPermissionError,
|
||||
permissionDenied: error.name === 'SecurityError',
|
||||
trackType: device
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* global APP */
|
||||
|
||||
import { getBlurEffect } from '../../blur';
|
||||
import JitsiMeetJS, { JitsiTrackErrors } from '../lib-jitsi-meet';
|
||||
import { MEDIA_TYPE } from '../media';
|
||||
import {
|
||||
@@ -50,37 +51,49 @@ export function createLocalTracksF(
|
||||
}
|
||||
}
|
||||
|
||||
const state = store.getState();
|
||||
const {
|
||||
constraints,
|
||||
desktopSharingFrameRate,
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
resolution
|
||||
} = store.getState()['features/base/config'];
|
||||
} = state['features/base/config'];
|
||||
const loadEffectsPromise = state['features/blur'].blurEnabled
|
||||
? getBlurEffect()
|
||||
.then(blurEffect => [ blurEffect ])
|
||||
.catch(error => {
|
||||
logger.error('Failed to obtain the blur effect instance with error: ', error);
|
||||
|
||||
return Promise.resolve([]);
|
||||
})
|
||||
: Promise.resolve([]);
|
||||
|
||||
return (
|
||||
JitsiMeetJS.createLocalTracks(
|
||||
{
|
||||
cameraDeviceId,
|
||||
constraints,
|
||||
desktopSharingExtensionExternalInstallation:
|
||||
options.desktopSharingExtensionExternalInstallation,
|
||||
desktopSharingFrameRate,
|
||||
desktopSharingSourceDevice:
|
||||
options.desktopSharingSourceDevice,
|
||||
desktopSharingSources: options.desktopSharingSources,
|
||||
loadEffectsPromise.then(effects =>
|
||||
JitsiMeetJS.createLocalTracks(
|
||||
{
|
||||
cameraDeviceId,
|
||||
constraints,
|
||||
desktopSharingExtensionExternalInstallation:
|
||||
options.desktopSharingExtensionExternalInstallation,
|
||||
desktopSharingFrameRate,
|
||||
desktopSharingSourceDevice:
|
||||
options.desktopSharingSourceDevice,
|
||||
desktopSharingSources: options.desktopSharingSources,
|
||||
|
||||
// Copy array to avoid mutations inside library.
|
||||
devices: options.devices.slice(0),
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
micDeviceId,
|
||||
resolution
|
||||
},
|
||||
firePermissionPromptIsShownEvent)
|
||||
.catch(err => {
|
||||
logger.error('Failed to create local tracks', options.devices, err);
|
||||
// Copy array to avoid mutations inside library.
|
||||
devices: options.devices.slice(0),
|
||||
effects,
|
||||
firefox_fake_device, // eslint-disable-line camelcase
|
||||
micDeviceId,
|
||||
resolution
|
||||
},
|
||||
firePermissionPromptIsShownEvent)
|
||||
.catch(err => {
|
||||
logger.error('Failed to create local tracks', options.devices, err);
|
||||
|
||||
return Promise.reject(err);
|
||||
}));
|
||||
return Promise.reject(err);
|
||||
})));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,6 +228,25 @@ export function isRemoteTrackMuted(tracks, mediaType, participantId) {
|
||||
return !track || track.muted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the current environment needs a user interaction with
|
||||
* the page before any unmute can occur.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isUserInteractionRequiredForUnmute(state) {
|
||||
const { browser } = JitsiMeetJS.util;
|
||||
|
||||
return !browser.isReactNative()
|
||||
&& !browser.isChrome()
|
||||
&& !browser.isChromiumBased()
|
||||
&& !browser.isElectron()
|
||||
&& window
|
||||
&& window.self !== window.top
|
||||
&& !state['features/base/user-interaction'].interacted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutes or unmutes a specific {@code JitsiLocalTrack}. If the muted state of
|
||||
* the specified {@code track} is already in accord with the specified
|
||||
|
||||
@@ -24,7 +24,12 @@ import {
|
||||
TRACK_REMOVED,
|
||||
TRACK_UPDATED
|
||||
} from './actionTypes';
|
||||
import { getLocalTrack, getTrackByJitsiTrack, setTrackMuted } from './functions';
|
||||
import {
|
||||
getLocalTrack,
|
||||
getTrackByJitsiTrack,
|
||||
isUserInteractionRequiredForUnmute,
|
||||
setTrackMuted
|
||||
} from './functions';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
@@ -50,6 +55,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
case SET_AUDIO_MUTED:
|
||||
if (!action.muted
|
||||
&& isUserInteractionRequiredForUnmute(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_setMuted(store, action, MEDIA_TYPE.AUDIO);
|
||||
break;
|
||||
|
||||
@@ -74,6 +84,11 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
}
|
||||
|
||||
case SET_VIDEO_MUTED:
|
||||
if (!action.muted
|
||||
&& isUserInteractionRequiredForUnmute(store.getState())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_setMuted(store, action, MEDIA_TYPE.VIDEO);
|
||||
break;
|
||||
|
||||
@@ -124,9 +139,7 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
} else {
|
||||
APP.UI.setVideoMuted(participantID, muted);
|
||||
}
|
||||
APP.UI.onPeerVideoTypeChanged(
|
||||
participantID,
|
||||
jitsiTrack.videoType);
|
||||
APP.UI.onPeerVideoTypeChanged(participantID, jitsiTrack.videoType);
|
||||
} else if (jitsiTrack.isLocal()) {
|
||||
APP.conference.setAudioMuteStatus(muted);
|
||||
} else {
|
||||
|
||||
20
react/features/base/user-interaction/actionTypes.js
Normal file
20
react/features/base/user-interaction/actionTypes.js
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* The type of (redux) action which signals that an event listener has been
|
||||
* added to listen for any user interaction with the page.
|
||||
*
|
||||
* {
|
||||
* type: SET_USER_INTERACTION_LISTENER,
|
||||
* userInteractionListener: Function
|
||||
* }
|
||||
*/
|
||||
export const SET_USER_INTERACTION_LISTENER = 'SET_USER_INTERACTION_LISTENER';
|
||||
|
||||
/**
|
||||
* The type of (redux) action which signals the user has interacted with the
|
||||
* page.
|
||||
*
|
||||
* {
|
||||
* type: USER_INTERACTION_RECEIVED,
|
||||
* }
|
||||
*/
|
||||
export const USER_INTERACTION_RECEIVED = 'USER_INTERACTION_RECEIVED';
|
||||
2
react/features/base/user-interaction/index.js
Normal file
2
react/features/base/user-interaction/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import './middleware';
|
||||
import './reducer';
|
||||
79
react/features/base/user-interaction/middleware.js
Normal file
79
react/features/base/user-interaction/middleware.js
Normal file
@@ -0,0 +1,79 @@
|
||||
// @flow
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
import { MiddlewareRegistry } from '../redux';
|
||||
|
||||
import {
|
||||
SET_USER_INTERACTION_LISTENER,
|
||||
USER_INTERACTION_RECEIVED
|
||||
} from './actionTypes';
|
||||
|
||||
/**
|
||||
* Implements the entry point of the middleware of the feature base/user-interaction.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_startListeningForUserInteraction(store);
|
||||
break;
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
case USER_INTERACTION_RECEIVED:
|
||||
_stopListeningForUserInteraction(store);
|
||||
break;
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listeners to notify redux of any user interaction with the page.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _startListeningForUserInteraction(store) {
|
||||
const userInteractionListener = event => {
|
||||
if (event.isTrusted) {
|
||||
store.dispatch({
|
||||
type: USER_INTERACTION_RECEIVED
|
||||
});
|
||||
|
||||
_stopListeningForUserInteraction(store);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousedown', userInteractionListener);
|
||||
window.addEventListener('keydown', userInteractionListener);
|
||||
|
||||
store.dispatch({
|
||||
type: SET_USER_INTERACTION_LISTENER,
|
||||
userInteractionListener
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Un-registers listeners intended to notify when the user has interacted with
|
||||
* the page.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _stopListeningForUserInteraction({ getState, dispatch }) {
|
||||
const { userInteractionListener } = getState()['features/base/app'];
|
||||
|
||||
if (userInteractionListener) {
|
||||
window.removeEventListener('mousedown', userInteractionListener);
|
||||
window.removeEventListener('keydown', userInteractionListener);
|
||||
|
||||
dispatch({
|
||||
type: SET_USER_INTERACTION_LISTENER,
|
||||
userInteractionListener: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
34
react/features/base/user-interaction/reducer.js
Normal file
34
react/features/base/user-interaction/reducer.js
Normal file
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
|
||||
import { ReducerRegistry } from '../redux';
|
||||
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app';
|
||||
import {
|
||||
SET_USER_INTERACTION_LISTENER,
|
||||
USER_INTERACTION_RECEIVED
|
||||
} from './actionTypes';
|
||||
|
||||
ReducerRegistry.register('features/base/user-interaction', (state = {}, action) => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
case APP_WILL_UNMOUNT: {
|
||||
return {
|
||||
...state,
|
||||
interacted: false
|
||||
};
|
||||
}
|
||||
case SET_USER_INTERACTION_LISTENER:
|
||||
return {
|
||||
...state,
|
||||
userInteractionListener: action.userInteractionListener
|
||||
};
|
||||
|
||||
case USER_INTERACTION_RECEIVED:
|
||||
return {
|
||||
...state,
|
||||
interacted: true
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
@@ -2,6 +2,41 @@
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Creates a deferred object.
|
||||
*
|
||||
* @returns {{promise, resolve, reject}}
|
||||
*/
|
||||
export function createDeferred(): Object {
|
||||
const deferred = {};
|
||||
|
||||
deferred.promise = new Promise((resolve, reject) => {
|
||||
deferred.resolve = resolve;
|
||||
deferred.reject = reject;
|
||||
});
|
||||
|
||||
return deferred;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base URL of the app.
|
||||
*
|
||||
* @param {Object} w - Window object to use instead of the built in one.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBaseUrl(w: Object = window) {
|
||||
const doc = w.document;
|
||||
const base = doc.querySelector('base');
|
||||
|
||||
if (base && base.href) {
|
||||
return base.href;
|
||||
}
|
||||
|
||||
const { protocol, host } = w.location;
|
||||
|
||||
return `${protocol}//${host}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace for all global variables, functions, etc that we need.
|
||||
*
|
||||
|
||||
21
react/features/blur/actionTypes.js
Normal file
21
react/features/blur/actionTypes.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched which represents that the blur
|
||||
* is enabled.
|
||||
*
|
||||
* {
|
||||
* type: BLUR_ENABLED
|
||||
* }
|
||||
*/
|
||||
export const BLUR_ENABLED = 'BLUR_ENABLED';
|
||||
|
||||
/**
|
||||
* The type of redux action dispatched which represents that the blur
|
||||
* is disabled.
|
||||
*
|
||||
* {
|
||||
* type: BLUR_DISABLED
|
||||
* }
|
||||
*/
|
||||
export const BLUR_DISABLED = 'BLUR_DISABLED';
|
||||
68
react/features/blur/actions.js
Normal file
68
react/features/blur/actions.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// @flow
|
||||
|
||||
import { getLocalVideoTrack } from '../../features/base/tracks';
|
||||
|
||||
import { BLUR_DISABLED, BLUR_ENABLED } from './actionTypes';
|
||||
import { getBlurEffect } from './functions';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
/**
|
||||
* Signals the local participant is switching between blurred or non blurred video.
|
||||
*
|
||||
* @param {boolean} enabled - If true enables video blur, false otherwise.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function toggleBlurEffect(enabled: boolean) {
|
||||
return function(dispatch: (Object) => Object, getState: () => any) {
|
||||
const state = getState();
|
||||
|
||||
if (state['features/blur'].blurEnabled !== enabled) {
|
||||
const { jitsiTrack } = getLocalVideoTrack(state['features/base/tracks']);
|
||||
|
||||
return getBlurEffect()
|
||||
.then(blurEffectInstance =>
|
||||
jitsiTrack.setEffect(enabled ? blurEffectInstance : undefined)
|
||||
.then(() => {
|
||||
enabled ? dispatch(blurEnabled()) : dispatch(blurDisabled());
|
||||
})
|
||||
.catch(error => {
|
||||
enabled ? dispatch(blurDisabled()) : dispatch(blurEnabled());
|
||||
logger.error('setEffect failed with error:', error);
|
||||
})
|
||||
)
|
||||
.catch(error => {
|
||||
dispatch(blurDisabled());
|
||||
logger.error('getBlurEffect failed with error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the local participant that the blur has been enabled.
|
||||
*
|
||||
* @returns {{
|
||||
* type: BLUR_ENABLED
|
||||
* }}
|
||||
*/
|
||||
export function blurEnabled() {
|
||||
return {
|
||||
type: BLUR_ENABLED
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals the local participant that the blur has been disabled.
|
||||
*
|
||||
* @returns {{
|
||||
* type: BLUR_DISABLED
|
||||
* }}
|
||||
*/
|
||||
export function blurDisabled() {
|
||||
return {
|
||||
type: BLUR_DISABLED
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user