Compare commits

...

69 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
1e18af2d1a rn: fix not showing a prompt when permissions have been denied
This only shows up if we cannot prompt again, like when turning permissions off
in the Settings app in iOS.
2019-07-12 17:50:54 +02:00
Дамян Минков
eb1fd4fd88 Merge pull request #4438 from jitsi/cs-siteid
Passes confID when initializing JitsiConference.
2019-07-12 14:25:35 +01:00
Matthias Herzog
e0c8b6b3c0 fix welcome page title fixes #4273 2019-07-12 14:25:26 +01:00
Дамян Минков
9071cd4813 Listens for suspend events from jitsi-power-monitor on postis channel. (#4428)
* Listens for suspend events from jitsi-power-monitor on postis channel.

* Removes duplicated type and actions.

* Moves suspendDetected state from overlay to power-monitor feature.
2019-07-12 14:08:34 +01:00
Saúl Ibarra Corretgé
9bf650c700 android: keep okio classes
Fixes running profile builds.
2019-07-12 14:22:36 +02:00
Saúl Ibarra Corretgé
49e3b03885 android: custom initialization of the WebRTC module
Set our own audio device manager so we can tweak it if need be (enabling /
disabling the HW AEC on specific devices).

Switch to using the software video encoder / decoder. This may feel like a
downgrade, but it has advantages:

- simulcast is now working (on par with iOS)
- certain devices have broken VP8 HW encoders (I'm looking at you Samsung Galaxy
S7) so this fixes that
2019-07-12 14:22:36 +02:00
Saúl Ibarra Corretgé
31e996ac3f android: always run adb reverse when starting the packager
It tends to close, so always open the reverse tunnel.
2019-07-12 14:22:36 +02:00
Saúl Ibarra Corretgé
d1c447e97b deps: update react-native-webrtc
- Fix crash in PeerConnection.close (Android)
- Add ability to customize PeerConnectionFactory (Android)
2019-07-12 14:22:36 +02:00
damencho
8a34c0a80f Updates lib-jitsi-meet. 2019-07-12 11:17:53 +01:00
virtuacoplenny
2f626ea474 ref(api): move participant join and left to middleware (#4365) 2019-07-11 12:44:27 -07:00
Bettenbuk Zoltan
0a76eebca7 feat: central back button registry 2019-07-11 16:14:08 +02:00
damencho
0e9e695251 Passes confID when initializing JitsiConference.
The confID is domain.com/RoomName or domain.com/tenant/RoomName, adds the tenant if any and also keeps the room name case.
2019-07-11 14:58:14 +01:00
Leonard Kim
b86df7a8e3 fix(remote-control): do not assume failed query is missing support
Multiple requests for checkUserRemoteControlSupport can be in
flight simultaneously. Order of promise resolution is not
guaranteed. It is possible for Request A and Request B to be
in flight and then Request B's promise chain resolves first.
Request A could have encountered errors and then resolve. Then
what could happen is checkUserRemoteControlSupport returns true
for remote control support due to Request B and the UI updates.
But then checkUserRemoteControlSupport returns false for
remote control support due to Request A's error and the UI
updates to hide remote control.
2019-07-11 07:25:08 +01:00
Bettenbuk Zoltan
5b25e02e26 feat: use dialog instead of alert when inviting 2019-07-10 20:50:14 +02:00
Bettenbuk Zoltan
0e6f14bb7c fix: avoid false triggering CDU in SlidingView 2019-07-10 20:49:50 +02:00
Leonard Kim
57b9954d9c fix(api): support params with value of undefined 2019-07-10 11:09:11 -07:00
virtuacoplenny
249dd7b8b8 fix(invite): decode the meeting name (#4411)
* fix(invite): decode the meeting name

* squash: try to make mobile join same encoded meeting name as web

* Decodes and generated texts for share and copy meeting info.

Decodes in all cases except when it contains a space, as it will generate wrong links when pasted/shared in external applications.
2019-07-10 10:27:11 -07:00
Bettenbuk Zoltan
b31d7b4451 ref: remove createStyleSheet from dialog styles 2019-07-10 16:42:56 +02:00
Bettenbuk Zoltan
8dea3389ee fix: avoid clicking behind dialogs 2019-07-10 16:42:56 +02:00
virtuacoplenny
e7f9e8e7f7 fix(conference): require user interaction to unmute in iframe (#4429)
* fix(conference): require user interaction to unmute in iframe

* squash: explicitly whitelist react native
2019-07-10 12:02:27 +01:00
Bettenbuk Zoltan
a53a86ee7a feat: add room lock pwd error message 2019-07-10 11:01:49 +02:00
Saúl Ibarra Corretgé
598b6f0598 deps: update react-native-webrtc
WebRTC is now at M75
2019-07-09 17:22:32 +02:00
Saúl Ibarra Corretgé
b245945c4a android: be explicit about starting a foreground service
After calling startService we are supposed to have a bit of time before turning
the service into a foreground service, but certain devices seem to be more
spartan and we've seen the following failure:

Caused by java.lang.IllegalStateException: Not allowed to start service Intent { act=JitsiMeetOngoingConferenceService:START cmp=org.jitsi.meet/.sdk.JitsiMeetOngoingConferenceService }: app is in background uid UidRecord{f6778d5 u0a220 CAC  bg:+1m1s417ms idle change:idle procs:1 proclist:15604, seq(0,0,0)}
       at android.app.ContextImpl.startServiceCommon + 1600(ContextImpl.java:1600)
       at android.app.ContextImpl.startService + 1546(ContextImpl.java:1546)
       at android.content.ContextWrapper.startService + 669(ContextWrapper.java:669)
       at org.jitsi.meet.sdk.JitsiMeetOngoingConferenceService.launch + 50(JitsiMeetOngoingConferenceService.java:50)

Be expliocit and call startForegroundService, on supported platforms.
2019-07-09 16:14:39 +02:00
Bettenbuk Zoltan
301a8e319a fix: reduce avatar font size 2019-07-09 15:45:38 +02:00
Saúl Ibarra Corretgé
820abfd059 ios: sync Podfile.lock 2019-07-09 12:58:07 +02:00
Hristo Terezov
1d42420a36 fix(font): Bring back the missing icons. 2019-07-08 20:03:04 +01:00
Hristo Terezov
1b4bdb5142 fix(blur): on FF 2019-07-08 20:03:04 +01:00
Hristo Terezov
f030a3f1fb fix(blur): when switching video tracks. 2019-07-08 20:03:04 +01:00
Hristo Terezov
8f79779ca7 fix(blur): Disable for SS 2019-07-08 20:03:04 +01:00
Saúl Ibarra Corretgé
c5111bb359 rn: update default color scheme 2019-07-08 20:27:44 +02:00
Bettenbuk Zoltan
42814eac7d feat: add icon based avatar and icon for pstn 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
74d0013acc feat: use participant id for avatar color 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
88e4850c4d fix: remove locally generated avatar ID 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
87f9b1cf92 ref: remove unnecesary functions 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
fe1187d7b7 ref: remove unused libs 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
a35b36d6df feat: dynamic avatar font size 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
a04982fd96 fix: AlwaysOnTop avatar 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
658679f89e fix: undefined participant in avatar call 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
f90fc665f8 ui: bigger padding to default avatar 2019-07-08 16:53:30 +02:00
Bettenbuk Zoltan
0c8130af41 fix: default avatar relative url 2019-07-08 16:53:30 +02:00
paweldomas
e4c5968459 fix: bring back the add meeting URL button
Brings back the button for generating meeting URLs for calendar events.
2019-07-08 09:26:33 -05:00
damencho
a148cd41a4 Fixes loading wifi-stats when used from jitsi-meet electron utils. 2019-07-08 14:30:20 +01:00
Saúl Ibarra Corretgé
f8a049759d android: fix crash if notification is null
I cannot see a path leading to it being null, but Crashlytics demonstrated it's
possible (so far in a small subset of old devices). Be defensive then.
2019-07-05 10:11:00 +02:00
Hristo Terezov
4b4225e14f chore(lib-jitsi-meet): Update. 2019-07-04 06:26:32 -07:00
Hristo Terezov
3b0c5d0b6a fix(blur): many small issues. 2019-07-04 06:26:32 -07:00
Cristian Florin Ghita
3b750ddd5a Add video background blur 2019-07-04 06:26:32 -07:00
Saúl Ibarra Corretgé
6383d000a9 rn: raise version to 19.3 2019-07-03 21:09:15 +02:00
Bettenbuk Zoltan
a48d67bdc7 fix: inconsistent state when quickly closing a sliding view 2019-07-03 13:43:56 +02:00
Saúl Ibarra Corretgé
2f92e72858 android: raise SDK version 2019-07-03 13:28:31 +02:00
Saúl Ibarra Corretgé
d2c85ada1b android: fix deadlock in uncaught exception handler
The app is about to crash at that stage so it was a moot point to try to leave
the conference anyway.

Stopping ConnectionServers is still a good idea though, since a crash may leave
the device in a bad state otherwise.
2019-07-03 13:28:18 +02:00
Saúl Ibarra Corretgé
55b95c52d6 android: fix synchronized access to listeners set
Fixes this issue:

~~~
    java.util.ConcurrentModificationException
        at java.util.HashMap$HashIterator.nextNode(HashMap.java:1441)
        at java.util.HashMap$KeyIterator.next(HashMap.java:1465)
        at org.jitsi.meet.sdk.OngoingConferenceTracker.updateListeners(OngoingConferenceTracker.java:89)
        at org.jitsi.meet.sdk.OngoingConferenceTracker.onExternalAPIEvent(OngoingConferenceTracker.java:74)
        at org.jitsi.meet.sdk.ExternalAPIModule.sendEvent(ExternalAPIModule.java:71)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.facebook.react.bridge.JavaMethodWrapper.invoke(JavaMethodWrapper.java:372)
        at com.facebook.react.bridge.JavaModuleWrapper.invoke(JavaModuleWrapper.java:158)
        at com.facebook.react.bridge.queue.NativeRunnable.run(Native Method)
        at android.os.Handler.handleCallback(Handler.java:873)
        at android.os.Handler.dispatchMessage(Handler.java:99)
        at com.facebook.react.bridge.queue.MessageQueueThreadHandler.dispatchMessage(MessageQueueThreadHandler.java:29)
        at android.os.Looper.loop(Looper.java:214)
        at com.facebook.react.bridge.queue.MessageQueueThreadImpl$4.run(MessageQueueThreadImpl.java:232)
        at java.lang.Thread.run(Thread.java:764)
~~~
2019-07-03 13:28:18 +02:00
Chris Hansen
52362c4675 Fix spelling mistake on pragma mark
It causes a compiler error in Xcode 11 because it doesn't know how to parse it.
2019-07-02 14:11:01 -07:00
netaskd
8da0552541 microsoftCalendar: add showing calendar events in local timezone. 2019-07-02 21:20:00 +01:00
Hristo Terezov
7ce0def995 fix(mobile): After PR #4396 2019-07-02 10:36:37 -07:00
Hristo Terezov
48285e8a2d fix(conference): Don't use this._room.
this._room should be used only by jitsi-meet-torture and for test purposes. Also this._room is assigned later than room. This may cause some issues, for example conference.getMyUserId() may return undefined while the user id is already available beacuse this._room hasn't been assigned yet.
2019-07-02 07:38:57 -07:00
Hristo Terezov
21dcc41d31 ref(large-video): switch LargeVideo logic to react 2019-07-02 07:38:57 -07:00
Дамян Минков
625d268373 Room lock update (#4394)
* Adds a notification when remote lock happens.

* Updates translations.

Removes unused strings and extracts room password to separate translation, to be able to change it when deployment uses only digits.

* Formats the conference pin when showing it.

* Removes member from translation in favour of participant.

* Updates formatting of the pin.

* Adds a notification when password is remotely removed.
2019-07-02 14:14:58 +01:00
Дамян Минков
681782ed20 Adds back talk while muted notification. (#4392)
* Adds back talk while muted notification.

* Adds unmute button to the notification.
2019-07-02 12:59:25 +01:00
Saúl Ibarra Corretgé
1baa85b649 rn: hide invite button if the functionality is not available 2019-07-02 12:30:50 +02:00
Bettenbuk Zoltan
72137a2811 feat: initial based avatars 2019-07-01 23:59:16 +02:00
Leonard Kim
0734ce7ae3 feat(api): add notifications for kicked participants 2019-07-01 12:53:25 -07:00
Дамян Минков
2dc06c28e3 Adds option to be able to cancel locked rooms and leave. (#4391)
* Adds option to be able to cancel locked rooms and leave.

* Removes not needed operations when canceling password prompt.
2019-07-01 13:02:25 +01:00
paweldomas
5848669552 feat(analytics): time since last success in connection dropped
Update LJM to 9bcc2a26cc94683b8ed302418695a331b450df97 in order to bring
in the analytics update which will add a property indicating how much
time has passed since the last successful XMPP request came through.
2019-06-28 13:30:50 -05:00
Leonard Kim
c0376d238a ref(notifications): do not notify of local participant left
Join notifications are already supressed for the local
participant, so hide the left notification. For now
the notification is not being shown on mobile to keep
the same existing behavior and because a copy change
will be needed, but will be added once batching is
implemented.
2019-06-28 09:18:18 -07:00
Leonard Kim
979b773c3c ref(notifications): move join notification firing to notifications feature 2019-06-27 17:29:02 -07:00
Leonard Kim
3195a449ca ref(conference): web and native exercise same redux flow for kicked out 2019-06-27 09:34:05 -07:00
Bettenbuk Zoltan
d7483f07e3 feat: add possibility to clear invite search field 2019-06-27 18:17:37 +02:00
Дамян Минков
9c1b802997 Device selection now always shows the device that it managed to open. (#4384)
* Device selection now always shows the device that managed to open.

* Simplifies implementation, skips using state.
2019-06-27 17:04:47 +01:00
damencho
bb3a10b0fc Safe guard for removed parent node of the iframe. 2019-06-27 14:23:59 +01:00
165 changed files with 3525 additions and 1785 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@@ -42,6 +42,7 @@
<glyph unicode="&#xe90c;" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe90d;" glyph-name="security" d="M768 170v428h-512v-428h512zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h388v86c0 72-60 132-132 132s-132-60-132-132h-82c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe90e;" glyph-name="security-locked" d="M768 170v428h-512v-428h512zM380 768v-86h264v86c0 72-60 132-132 132s-132-60-132-132zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h42v86c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />
<glyph unicode="&#xe90f;" glyph-name="blur-background" d="M469.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM725.333 640c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM469.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333zM426.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 170.667c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM213.333 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 384c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM896 640c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM426.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM682.667 853.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667zM725.333 384c0-47.128-38.205-85.333-85.333-85.333s-85.333 38.205-85.333 85.333c0 47.128 38.205 85.333 85.333 85.333s85.333-38.205 85.333-85.333z" />
<glyph unicode="&#xe910;" glyph-name="microphone" d="M738 554h72c0-146-116-266-256-286v-140h-84v140c-140 20-256 140-256 286h72c0-128 108-216 226-216s226 88 226 216zM512 426c-70 0-128 58-128 128v256c0 70 58 128 128 128s128-58 128-128v-256c0-70-58-128-128-128z" />
<glyph unicode="&#xe912;" glyph-name="mic-disabled" d="M182 896l714-714-54-54-178 178c-32-20-72-32-110-38v-140h-84v140c-140 20-256 140-256 286h72c0-128 108-216 226-216 34 0 68 8 98 22l-70 70c-8-2-18-4-28-4-70 0-128 58-128 128v32l-256 256zM640 548l-256 254v8c0 70 58 128 128 128s128-58 128-128v-262zM810 554c0-50-14-98-38-140l-52 54c12 26 18 54 18 86h72z" />
<glyph unicode="&#xe913;" glyph-name="link" d="M640 426c114 0 342-56 342-170v-86h-684v86c0 114 228 170 342 170zM256 598h128v-86h-128v-128h-86v128h-128v86h128v128h86v-128zM640 512c-94 0-170 76-170 170s76 172 170 172 170-78 170-172-76-170-170-170z" />

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -50,7 +50,7 @@ var interfaceConfig = {
'fodeviceselection', 'hangup', 'profile', 'info', 'chat', 'recording',
'livestreaming', 'etherpad', 'sharedvideo', 'settings', 'raisehand',
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
'tileview'
'tileview', 'videobackgroundblur'
],
SETTINGS_SECTIONS: [ 'devices', 'language', 'moderator', 'profile', 'calendar' ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
"countryNotSupported": "We do not support this destination yet.",
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
"disabled": "You can't invite people.",
"failedToAdd": "Failed to add 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": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

View File

@@ -0,0 +1,4 @@
// @flow
export * from './components';
export * from './functions';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,3 @@
export { default as Avatar } from './Avatar';
// @flow
export { default as ParticipantView } from './ParticipantView';

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

View File

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

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

View 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