Compare commits

...

39 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
4c506ae5fd android: fix crash if UserInfo is not set
Fixes: https://github.com/jitsi/jitsi-meet-sdk-samples/issues/11
2019-07-16 07:31:45 +01:00
Saúl Ibarra Corretgé
54e8d2c1a8 android: raise SDK version 2019-07-16 07:31:25 +01:00
Saúl Ibarra Corretgé
d27a94b3b5 android: raise SDK version 2019-07-03 13:29:21 +02:00
Saúl Ibarra Corretgé
e1710eaa38 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:29:21 +02:00
Saúl Ibarra Corretgé
f4467206d7 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:29:21 +02:00
paweldomas
97e8b31cee fix: update LJM to fix reload on hangup
Updates LJM to c0af82a215d0893f1999df299cfdfcbc9ce9e72a
2019-06-26 22:11:12 +02:00
Saúl Ibarra Corretgé
55218de779 rn: add a reduced UI mode for the welcome page
The only way to render the welcome page in reduced UI mode currently is to
hangup a call from the Android ongoing notification while in PiP mode.
2019-06-26 21:45:27 +02:00
Saúl Ibarra Corretgé
714e0e045d android: add notification while there is an ongoing meeting
The notification is posted by a foreground service, which also has the nice
side-effect of keeping the app alive for a long time.
2019-06-26 21:45:27 +02:00
Saúl Ibarra Corretgé
0bc369afb4 android: add ability to get the current Activity running RN
This helper method gets the current Activity attached to React Native (via the
ReactContext). This is useful for modules which need access to it, without being
actual React Native modules.
2019-06-26 21:45:27 +02:00
Saúl Ibarra Corretgé
f71ec55170 android: add ability to keep track of the current ongoing conference 2019-06-26 21:45:27 +02:00
yanas
760885437a Merge pull request #4369 from jitsi/quality-text-change
Changes call quality strings to video quality.
2019-06-26 17:24:34 +01:00
damencho
f77976b742 Notify for detecting suspend. 2019-06-26 17:10:34 +01:00
Saúl Ibarra Corretgé
9e95e7cd97 rn: bump SDK version to 2.2.0 2019-06-26 12:04:17 +02:00
Leonard Kim
9d94257e79 ref(api): move conference join notification to middleware 2019-06-26 10:47:18 +02:00
Leonard Kim
13cfd61c83 fix(index): set reload link as attribute, not raw html 2019-06-25 12:48:31 -07:00
Leonard Kim
fa818bc386 feat(screenshare): allow auto-pin remote only 2019-06-25 11:21:23 +01:00
Saúl Ibarra Corretgé
a73a642c64 rn: fix CallKit failure screen flash when kicked
Pretend we have left instead of triggering a call failure. The user was already
told in the dialog.
2019-06-25 10:25:33 +02:00
Leonard Kim
94b3f6410d ref(api): move local participant name change 2019-06-24 13:15:02 -07:00
Leonard Kim
3d30f6e9cd ref(api): move filmstrip display notification 2019-06-24 13:15:02 -07:00
Leonard Kim
40c16f0bac ref(api): move tile view subscriber to api dir 2019-06-24 13:15:02 -07:00
Leonard Kim
a1db63a8c2 ref(api): move feedbackSubmitted notification to api middleware 2019-06-24 13:15:02 -07:00
Leonard Kim
59a9c2d947 fix(notifications): show notifications after reducers update to get name
Otherwise the participant will not have been added yet
to state so the participant name will not display in
the notification.
2019-06-24 10:51:17 -07:00
Yana Stamcheva
fc897b9bac Changes call quality to video quality. 2019-06-24 14:39:22 +01:00
damencho
96f013c549 Fixes few undefined errors on standby. 2019-06-24 12:06:27 +01:00
damencho
742905e05a Removes duplicated text. 2019-06-24 12:06:27 +01:00
damencho
bde44a94e8 Changes display name prompt to non modal, so people can mute/unmute. 2019-06-24 12:06:27 +01:00
Saúl Ibarra Corretgé
1786bfadce android: make some activity attributes private
They are only meant to be used internally. This reduces the risk of SDK users
sending garbage when we expect a specific type.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
b2e840636a sounds: lower log severity
When native SDK users end a meeting the view gets disposed and detached from
React, and then the entire app gets destroyed and these errors get printed at
the error level, throwing some people off.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
ddaa22048f android: make sure we left the current meeting when the activity is destroyed
If the user swipes the activity from the recents list there is no other chance,
onDestroy gets called directly.
2019-06-21 21:48:46 +02:00
Saúl Ibarra Corretgé
3e77890387 android: fix not calling super in onNewIntent 2019-06-21 21:48:46 +02:00
Leonard Kim
1e39c12963 fix(suboptimal): allow checks for chromium based browsers 2019-06-21 09:55:43 -07:00
Saúl Ibarra Corretgé
243fdba80f android: fix exception when sending events without URL
ENTER_PIP_MODE, for example, does not have it.
2019-06-21 15:42:10 +02:00
Дамян Минков
08c4933c1b Fixes inviting more than one participant (#4352)
* Fixes inviting more than one participant.

* Shows a notification when participants are invited.

* Adds support for both .id and .user_id props for people query results.
2019-06-21 13:17:47 +01:00
Leonard Kim
d5e0dea469 fix(suboptimal): ignore mobile browsers in whitelists/blacklists 2019-06-21 11:01:40 +01:00
paweldomas
033aa0dd6e fix(base/conference/reducer): check if room is defined
This was hit on a corner case when ConnectionService will deny
the request to start the call. I am not sure, but it could have been
that the conference object has been disposed or closed or something
else, but the fact is that 'conference.room' was not defined and things
crashed. It is not safe to access conference's private field 'room'. It
is true JitsiConference doesn't follow the practice of marking this
field as private with the underscore '_', but it is not a public field.
2019-06-20 13:19:04 -05:00
Saúl Ibarra Corretgé
803870ef8f android: add a uncaught exception handler
Its main task is to cleanup conferences (specially the connection services
stuff) to make sure the system is left in a working state even when the
unexpected happens.
2019-06-20 17:03:24 +02:00
virtuacoplenny
bf67a4a675 Configurable (sub)optimal and unsupported browsers (#4351)
* Configurable (sub)optimal and unsupported browsers

* maybe this helps somehow
2019-06-20 14:02:15 +01:00
Bettenbuk Zoltan
ee2036a2a7 feat: show on stage function 2019-06-20 11:05:34 +02:00
Bettenbuk Zoltan
4c3ed190f3 feat: add ability to toggle toolbox from tile view 2019-06-20 11:05:34 +02:00
70 changed files with 1050 additions and 279 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 960 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -18,4 +18,4 @@
# org.gradle.parallel=true
appVersion=19.2.0
sdkVersion=2.1.0
sdkVersion=2.2.2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -341,7 +341,7 @@ public class JitsiMeetConferenceOptions implements Parcelable {
dest.writeString(token);
dest.writeBundle(colorScheme);
dest.writeBundle(featureFlags);
dest.writeBundle(userInfo.asBundle());
dest.writeBundle(userInfo != null ? userInfo.asBundle() : new Bundle());
dest.writeByte((byte) (audioMuted == null ? 0 : audioMuted ? 1 : 2));
dest.writeByte((byte) (audioOnly == null ? 0 : audioOnly ? 1 : 2));
dest.writeByte((byte) (videoMuted == null ? 0 : videoMuted ? 1 : 2));

View File

@@ -0,0 +1,109 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
* responsible for presenting an ongoing notification when a conference is in progress.
* The service will help keep the app running while in the background.
*
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service
implements OngoingConferenceTracker.OngoingConferenceListener {
private static final String TAG = JitsiMeetOngoingConferenceService.class.getSimpleName();
static final class Actions {
static final String START = TAG + ":START";
static final String HANGUP = TAG + ":HANGUP";
}
static void launch(Context context) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(Actions.START);
ComponentName componentName = context.startService(intent);
if (componentName == null) {
Log.w(TAG, "Ongoing conference service not started");
}
}
static void abort(Context context) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
context.stopService(intent);
}
@Override
public void onCreate() {
super.onCreate();
OngoingConferenceTracker.getInstance().addListener(this);
}
@Override
public void onDestroy() {
OngoingConferenceTracker.getInstance().removeListener(this);
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (action.equals(Actions.START)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
Log.i(TAG, "Service started");
} else if (action.equals(Actions.HANGUP)) {
Log.i(TAG, "Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
stopSelf();
} else {
Log.w(TAG, "Unknown action received: " + action);
stopSelf();
}
return START_NOT_STICKY;
}
@Override
public void onCurrentConferenceChanged(String conferenceUrl) {
if (conferenceUrl == null) {
stopSelf();
Log.i(TAG, "Service stopped");
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright @ 2018-present 8x8, Inc.
* Copyright @ 2017-2018 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.util.Log;
class JitsiMeetUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
private final Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
public static void register() {
Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
JitsiMeetUncaughtExceptionHandler uncaughtExceptionHandler
= new JitsiMeetUncaughtExceptionHandler(defaultUncaughtExceptionHandler);
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);
}
private JitsiMeetUncaughtExceptionHandler(Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler) {
this.defaultUncaughtExceptionHandler = defaultUncaughtExceptionHandler;
}
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e(this.getClass().getSimpleName(), "FATAL ERROR", e);
// Abort all ConnectionService ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
if (defaultUncaughtExceptionHandler != null) {
defaultUncaughtExceptionHandler.uncaughtException(t, e);
}
}
}

View File

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

View File

@@ -0,0 +1,99 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.ReadableMap;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
/**
* Helper class to keep track of what the current conference is.
*/
class OngoingConferenceTracker {
private static final OngoingConferenceTracker instance = new OngoingConferenceTracker();
private static final String CONFERENCE_WILL_JOIN = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_TERMINATED = "CONFERENCE_TERMINATED";
private final Collection<OngoingConferenceListener> listeners =
Collections.synchronizedSet(new HashSet<OngoingConferenceListener>());
private String currentConference;
public OngoingConferenceTracker() {
}
public static OngoingConferenceTracker getInstance() {
return instance;
}
/**
* Gets the current active conference URL.
*
* @return - The current conference URL as a String.
*/
synchronized String getCurrentConference() {
return currentConference;
}
synchronized void onExternalAPIEvent(String name, ReadableMap data) {
if (!data.hasKey("url")) {
return;
}
String url = data.getString("url");
if (url == null) {
return;
}
switch(name) {
case CONFERENCE_WILL_JOIN:
currentConference = url;
updateListeners();
break;
case CONFERENCE_TERMINATED:
if (url.equals(currentConference)) {
currentConference = null;
updateListeners();
}
break;
}
}
void addListener(OngoingConferenceListener listener) {
listeners.add(listener);
}
void removeListener(OngoingConferenceListener listener) {
listeners.remove(listener);
}
private void updateListeners() {
synchronized (listeners) {
for (OngoingConferenceListener listener : listeners) {
listener.onCurrentConferenceChanged(currentConference);
}
}
}
public interface OngoingConferenceListener {
void onCurrentConferenceChanged(String conferenceUrl);
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright @ 2019-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.meet.sdk;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
* and to hangup from within the notification itself.
*/
class OngoingNotification {
private static final String TAG = OngoingNotification.class.getSimpleName();
private static final String CHANNEL_ID = "JitsiNotificationChannel";
private static final String CHANNEL_NAME = "Ongoing Conference Notifications";
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
Log.w(TAG, "Cannot create notification channel: no current context");
return;
}
NotificationManager notificationManager
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel(CHANNEL_ID);
if (channel != null) {
// The channel was already created, no need to do it again.
return;
}
channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT);
channel.enableLights(false);
channel.enableVibration(false);
channel.setShowBadge(false);
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification() {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
Log.w(TAG, "Cannot create notification: no current context");
return null;
}
Intent notificationIntent = new Intent(context, context.getClass());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new NotificationCompat.Builder(context, CHANNEL_ID);
} else {
builder = new NotificationCompat.Builder(context);
}
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
.setContentTitle(context.getString(R.string.ongoing_notification_title))
.setContentText(context.getString(R.string.ongoing_notification_text))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setOngoing(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
// Add a "hang-up" action only if we are using ConnectionService.
if (AudioModeModule.useConnectionService()) {
Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
PendingIntent hangupPendingIntent
= PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
builder.addAction(hangupAction);
}
return builder.build();
}
}

View File

@@ -17,6 +17,7 @@
package org.jitsi.meet.sdk;
import android.app.Activity;
import android.app.Application;
import android.support.annotation.Nullable;
@@ -120,6 +121,18 @@ class ReactInstanceManagerHolder {
? reactContext.getNativeModule(nativeModuleClass) : null;
}
/**
* Gets the current {@link Activity} linked to React Native.
*
* @return An activity attached to React Native.
*/
static Activity getCurrentActivity() {
ReactContext reactContext
= reactInstanceManager != null
? reactInstanceManager.getCurrentReactContext() : null;
return reactContext != null ? reactContext.getCurrentActivity() : null;
}
static ReactInstanceManager getReactInstanceManager() {
return reactInstanceManager;
}
@@ -182,5 +195,8 @@ class ReactInstanceManagerHolder {
if (devSettings != null) {
devSettings.setBundleDeltasEnabled(false);
}
// Register our uncaught exception handler.
JitsiMeetUncaughtExceptionHandler.register();
}
}

View File

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

View File

@@ -79,7 +79,6 @@ import {
import { showNotification } from './react/features/notifications';
import {
dominantSpeakerChanged,
getAvatarURLByParticipantId,
getLocalParticipant,
getNormalizedDisplayName,
getParticipantById,
@@ -1999,6 +1998,8 @@ export default {
this.localAudio.dispose();
this.localAudio = null;
}
APP.API.notifySuspendDetected();
});
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
@@ -2278,18 +2279,6 @@ export default {
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
APP.API.notifyConferenceJoined(
this.roomName,
this._room.myUserId(),
{
displayName,
formattedDisplayName: appendSuffix(
displayName,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME),
avatarURL: getAvatarURLByParticipantId(
APP.store.getState(), this._room.myUserId())
}
);
},
/**
@@ -2753,14 +2742,6 @@ export default {
displayName: formattedNickname
}));
APP.API.notifyDisplayNameChanged(id, {
displayName: formattedNickname,
formattedDisplayName:
appendSuffix(
formattedNickname,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME)
});
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}

View File

@@ -25,6 +25,9 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-enlarge:before {
content: "\e90a";
}
.icon-signal_cellular_0:before {
content: "\e901";
}

View File

@@ -437,6 +437,8 @@ The listener will receive an object with the following structure:
}
```
* **suspendDetected** - event notifications about detecting suspend event in host computer.
You can also add multiple event listeners by using `addEventListeners`.
This method requires one argument of type Object. The object argument must
have the names of the events as keys and the listeners of the events as values.

Binary file not shown.

View File

@@ -37,6 +37,7 @@
<glyph unicode="&#xe907;" glyph-name="signal_cellular_2" d="M86 86l852 852v-852h-852z" />
<glyph unicode="&#xe908;" glyph-name="share-doc" d="M554 640h236l-236 234v-234zM682 426v86h-340v-86h340zM682 256v86h-340v-86h340zM598 938l256-256v-512c0-46-40-84-86-84h-512c-46 0-86 38-86 84l2 684c0 46 38 84 84 84h342z" />
<glyph unicode="&#xe909;" glyph-name="ninja" d="M330.667 469.333c-0.427 14.933 6.4 29.44 17.92 39.253 32-6.827 61.867-20.053 88.747-39.253 0-29.013-23.893-52.907-53.333-52.907s-52.907 23.467-53.333 52.907zM586.667 469.333c26.88 18.773 56.747 32 88.747 38.827 11.52-9.813 18.347-24.32 17.92-38.827 0-29.867-23.893-53.76-53.333-53.76s-53.333 23.893-53.333 53.76v0zM512 640c-118.187 1.707-234.667-27.733-338.347-85.333l-2.987-42.667c0-52.48 12.373-104.107 35.84-151.040 101.12 15.36 203.093 23.040 305.493 23.040s204.373-7.68 305.493-23.040c23.467 46.933 35.84 98.56 35.84 151.040l-2.987 42.667c-103.68 57.6-220.16 87.040-338.347 85.333zM512 938.667c235.641 0 426.667-191.025 426.667-426.667s-191.025-426.667-426.667-426.667c-235.641 0-426.667 191.025-426.667 426.667s191.025 426.667 426.667 426.667z" />
<glyph unicode="&#xe90a;" glyph-name="enlarge" d="M896 212v600h-768v-600h768zM896 896q34 0 60-26t26-60v-596q0-34-26-60t-60-26h-768q-34 0-60 26t-26 60v596q0 34 26 60t60 26h768zM598 342l-86-108-86 108h172zM256 598v-172l-106 86zM768 598l106-86-106-86v172zM512 790l86-108h-172z" />
<glyph unicode="&#xe90b;" glyph-name="full-screen" d="M598 810h212v-212h-84v128h-128v84zM726 298v128h84v-212h-212v84h128zM214 598v212h212v-84h-128v-128h-84zM298 426v-128h128v-84h-212v212h84z" />
<glyph unicode="&#xe90c;" glyph-name="exit-full-screen" d="M682 682h128v-84h-212v212h84v-128zM598 214v212h212v-84h-128v-128h-84zM342 682v128h84v-212h-212v84h128zM214 342v84h212v-212h-84v128h-128z" />
<glyph unicode="&#xe90d;" glyph-name="security" d="M768 170v428h-512v-428h512zM768 682c46 0 86-38 86-84v-428c0-46-40-84-86-84h-512c-46 0-86 38-86 84v428c0 46 40 84 86 84h388v86c0 72-60 132-132 132s-132-60-132-132h-82c0 118 96 214 214 214s214-96 214-214v-86h42zM512 298c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86z" />

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

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

View File

@@ -167,7 +167,27 @@ var interfaceConfig = {
*
* @type {boolean}
*/
RECENT_LIST_ENABLED: true
RECENT_LIST_ENABLED: true,
// Names of browsers which should show a warning stating the current browser
// has a suboptimal experience. Browsers which are not listed as optimal or
// unsupported are considered suboptimal. Valid values are:
// chrome, chromium, edge, electron, firefox, nwjs, opera, safari
OPTIMAL_BROWSERS: [ 'chrome', 'chromium', 'firefox', 'nwjs', 'electron' ],
// Browsers, in addition to those which do not fully support WebRTC, that
// are not supported and should show the unsupported browser page.
UNSUPPORTED_BROWSERS: [],
/**
* A UX mode where the last screen share participant is automatically
* pinned. Valid values are the string "remote-only" so remote participants
* get pinned but not local, otherwise any truthy value for all participants,
* and any falsy value to disable the feature.
*
* Note: this mode is experimental and subject to breakage.
*/
AUTO_PIN_LATEST_SCREEN_SHARE: 'remote-only'
/**
* How many columns the tile view can expand to. The respected range is
@@ -195,12 +215,6 @@ var interfaceConfig = {
*/
// ANDROID_APP_PACKAGE: 'org.jitsi.meet',
/**
* A UX mode where the last screen share participant is automatically
* pinned. Note: this mode is experimental and subject to breakage.
*/
// AUTO_PIN_LATEST_SCREEN_SHARE: false,
/**
* Override the behavior of some notifications to remain displayed until
* explicitly dismissed through a user action. The value is how long, in

View File

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

View File

@@ -472,6 +472,9 @@
"focus": "Conference focus",
"focusFail": "__component__ not available - retry in __ms__ sec",
"grantedTo": "Moderator rights granted to __to__!",
"invitedOneMember": "__name__ has been invited",
"invitedThreePlusMembers": "__name__ and __count__ others have been invited",
"invitedTwoMembers": "__first__ and __second__ have been invited",
"kickParticipant": "__kicked__ was kicked by __kicker__",
"me": "Me",
"moderator": "Moderator rights granted!",
@@ -614,7 +617,7 @@
"accessibilityLabel": {
"audioOnly": "Toggle audio only",
"audioRoute": "Select the sound device",
"callQuality": "Manage call quality",
"callQuality": "Manage video quality",
"cc": "Toggle subtitles",
"chat": "Toggle chat window",
"document": "Toggle shared document",
@@ -638,6 +641,7 @@
"shareRoom": "Invite someone",
"shareYourScreen": "Toggle screenshare",
"shortcuts": "Toggle shortcuts",
"show": "Show on stage",
"speakerStats": "Toggle speaker statistics",
"tileView": "Toggle tile view",
"toggleCamera": "Toggle camera",
@@ -649,7 +653,7 @@
"audioOnlyOn": "Enable audio only mode",
"audioRoute": "Select the sound device",
"authenticate": "Authenticate",
"callQuality": "Manage call quality",
"callQuality": "Manage video quality",
"cameraDisabled": "Camera is not available",
"chat": "Open / Close chat",
"closeChat": "Close chat",
@@ -735,7 +739,7 @@
"videoStatus": {
"audioOnly": "AUD",
"audioOnlyExpanded": "You are in audio only mode. This mode saves bandwidth but you won't see videos of others.",
"callQuality": "Call Quality",
"callQuality": "Video Quality",
"hd": "HD",
"hdTooltip": "Viewing high definition video",
"highDefinition": "High definition",
@@ -748,7 +752,7 @@
"onlyAudioAvailable": "Only audio is available",
"onlyAudioSupported": "We only support audio in this browser.",
"p2pEnabled": "Peer to Peer Enabled",
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
"p2pVideoQualityDescription": "In peer to peer mode, received video quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
"qualityButtonTip": "Change received video quality",
"recHighDefinitionOnly": "Will prefer high definition.",
"sd": "SD",
@@ -763,6 +767,7 @@
"mute": "Member is muted",
"muted": "Muted",
"remoteControl": "Remote control",
"show": "Show on stage",
"videomute": "Member has stopped the camera"
},
"welcomepage": {
@@ -786,6 +791,7 @@
"recentList": "Recent",
"recentListDelete": "Delete",
"recentListEmpty": "Your recent list is currently empty. Chat with your team and you will find all your recent meetings here.",
"reducedUIText": "Welcome to __app__!",
"roomname": "Enter room name",
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
"sendFeedback": "Send feedback",

View File

@@ -509,6 +509,15 @@ class API {
this._sendEvent({ name: 'video-ready-to-close' });
}
/**
* Notify external application (if API is enabled) that a suspend event in host computer.
*
* @returns {void}
*/
notifySuspendDetected() {
this._sendEvent({ name: 'suspend-detected' });
}
/**
* Notify external application (if API is enabled) for audio muted status
* changed.

View File

@@ -73,6 +73,7 @@ const events = {
'video-mute-status-changed': 'videoMuteStatusChanged',
'screen-sharing-status-changed': 'screenSharingStatusChanged',
'subject-change': 'subjectChange',
'suspend-detected': 'suspendDetected',
'tile-view-changed': 'tileViewChanged'
};
@@ -537,6 +538,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
* {{
* on: on //whether screen sharing is on
* }}
* {@code suspendDetected} - receives event notifications about detecting suspend event in host computer.
* {@code readyToClose} - all hangup operations are completed and Jitsi Meet
* is ready to be disposed.
* @returns {void}

View File

@@ -637,7 +637,7 @@ const VideoLayout = {
*/
onVideoMute(id, value) {
if (APP.conference.isLocalId(id)) {
localVideoThumbnail.setVideoMutedView(value);
localVideoThumbnail && localVideoThumbnail.setVideoMutedView(value);
} else {
const remoteVideo = remoteVideos[id];
@@ -985,7 +985,7 @@ const VideoLayout = {
} else if (currentId) {
const currentSmallVideo = this.getSmallVideo(currentId);
currentSmallVideo.updateView();
currentSmallVideo && currentSmallVideo.updateView();
}
},

4
package-lock.json generated
View File

@@ -8946,8 +8946,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"from": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"version": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"from": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"requires": {
"@jitsi/sdp-interop": "0.1.14",
"@jitsi/sdp-simulcast": "0.2.1",

View File

@@ -52,7 +52,7 @@
"js-utils": "github:jitsi/js-utils#73a67a7a60d52f8e895f50939c8fcbd1f20fe7b5",
"jsrsasign": "8.0.12",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#ee1854c1b12a229f70f351acaebe6686f38f6a85",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#c0af82a215d0893f1999df299cfdfcbc9ce9e72a",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.11",
"moment": "2.19.4",

View File

@@ -213,7 +213,8 @@ function _conferenceJoined(state, { conference }) {
// i.e. password-protected is private to lib-jitsi-meet. However, the
// library does not fire LOCK_STATE_CHANGED upon joining a JitsiConference
// with a password.
const locked = conference.room.locked ? LOCKED_REMOTELY : undefined;
// FIXME Technically JitsiConference.room is a private field.
const locked = conference.room && conference.room.locked ? LOCKED_REMOTELY : undefined;
return assign(state, {
authRequired: undefined,

View File

@@ -3,15 +3,74 @@
import JitsiMeetJS from '../lib-jitsi-meet';
import { Platform } from '../react';
import { isBlacklistedEnvironment } from './isBlacklistedEnvironment';
const { browser } = JitsiMeetJS.util;
const DEFAULT_OPTIMAL_BROWSERS = [
'chrome',
'electron',
'firefox',
'nwjs'
];
const DEFAULT_UNSUPPORTED_BROWSERS = [];
const browserNameToCheck = {
chrome: browser.isChrome.bind(browser),
chromium: browser.isChromiumBased.bind(browser),
edge: browser.isEdge.bind(browser),
electron: browser.isElectron.bind(browser),
firefox: browser.isFirefox.bind(browser),
nwjs: browser.isNWJS.bind(browser),
opera: browser.isOpera.bind(browser),
safari: browser.isSafari.bind(browser)
};
declare var interfaceConfig: Object;
/**
* Returns whether or not jitsi is optimized and targeted for the provided
* browser name.
*
* @param {string} browserName - The name of the browser to check.
* @returns {boolean}
*/
export function isBrowsersOptimal(browserName: string) {
return (interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS)
.includes(browserName);
}
/**
* Returns whether or not the current browser or the list of passed in browsers
* is considered suboptimal. Suboptimal means it is a supported browser but has
* not been explicitly listed as being optimal, possibly due to functionality
* issues.
*
* @param {Array<string>} [browsers] - A list of browser names to check. Will
* default to a whitelist.
* @returns {boolean}
*/
export function isSuboptimalBrowser() {
const optimalBrowsers
= interfaceConfig.OPTIMAL_BROWSERS || DEFAULT_OPTIMAL_BROWSERS;
return !_isCurrentBrowserInList(optimalBrowsers) && isSupportedBrowser();
}
/**
* Returns whether or not the current browser should allow the app to display.
* A supported browser is assumed to be able to support WebRtc.
*
* @returns {boolean}
*/
export function isSupportedBrowser() {
if (navigator.product === 'ReactNative' || isBlacklistedEnvironment()) {
if (navigator.product === 'ReactNative') {
return false;
}
// Blacklists apply to desktop browsers only right now.
if (!_isMobileBrowser() && _isCurrentBrowserInList(
interfaceConfig.UNSUPPORTED_BROWSERS || DEFAULT_UNSUPPORTED_BROWSERS
)) {
return false;
}
@@ -19,7 +78,32 @@ export function isSupportedBrowser() {
// - the WelcomePage is mobile ready;
// - if the URL points to a conference then deep-linking will take
// care of it.
return Platform.OS === 'android'
|| Platform.OS === 'ios'
|| JitsiMeetJS.isWebRtcSupported();
return _isMobileBrowser() || JitsiMeetJS.isWebRtcSupported();
}
/**
* Runs various browser checks to know if the current browser is found within
* the list.
*
* @param {Array<string>} list - Browser names to check. The names should be
* keys in {@link browserNameToCheck}.
* @private
* @returns {boolean}
*/
function _isCurrentBrowserInList(list) {
return Boolean(list.find(browserName => {
const checkFunction = browserNameToCheck[browserName];
return checkFunction ? checkFunction.call(browser) : false;
}));
}
/**
* Returns whether or not the current environment is a mobile device.
*
* @private
* @returns {boolean}
*/
function _isMobileBrowser() {
return Platform.OS === 'android' || Platform.OS === 'ios';
}

View File

@@ -1,11 +0,0 @@
/**
* Returns whether or not the current browser is supported for showing meeting
* based on any custom overrides. This file should be overridden with branding
* as needed to fit deployment needs.
*
* @returns {boolean} True the browser is unsupported due to being blacklisted
* by the logic within this function.
*/
export function isBlacklistedEnvironment() {
return false;
}

View File

@@ -118,13 +118,15 @@ 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 _participantJoinedOrUpdated(store, next, action);
return result;
}
case PARTICIPANT_LEFT:

View File

@@ -96,10 +96,7 @@ function _addOrRemoveAudioElement(state, action) {
}));
}
} else {
const actionName
= isAddAction ? '_ADD_AUDIO_ELEMENT' : '_REMOVE_AUDIO_ELEMENT';
logger.error(`${actionName}: no sound for id: ${soundId}`);
logger.warn(`${action.type}: no sound for id: ${soundId}`);
}
return nextState;

View File

@@ -111,13 +111,6 @@ type Props = AbstractProps & {
*/
_toolboxVisible: boolean,
/**
* The indicator which determines whether the Toolbox is always visible.
*
* @private
*/
_toolboxAlwaysVisible: boolean,
/**
* The redux {@code dispatch} function.
*/
@@ -298,10 +291,6 @@ class Conference extends AbstractConference<Props, *> {
* @returns {void}
*/
_onClick() {
if (this.props._toolboxAlwaysVisible) {
return;
}
this._setToolboxVisible(!this.props._toolboxVisible);
}
@@ -407,7 +396,7 @@ function _mapStateToProps(state) {
leaving
} = state['features/base/conference'];
const { reducedUI } = state['features/base/responsive-ui'];
const { alwaysVisible, visible } = state['features/toolbox'];
const { visible } = state['features/toolbox'];
// XXX There is a window of time between the successful establishment of the
// XMPP connection and the subsequent commencement of joining the MUC during
@@ -484,15 +473,7 @@ function _mapStateToProps(state) {
* @private
* @type {boolean}
*/
_toolboxVisible: visible,
/**
* The indicator which determines whether the Toolbox is always visible.
*
* @private
* @type {boolean}
*/
_toolboxAlwaysVisible: alwaysVisible
_toolboxVisible: visible
};
}

View File

@@ -1,5 +1,5 @@
import { translateToHTML } from '../base/i18n';
import { browser } from '../base/lib-jitsi-meet';
import { isSuboptimalBrowser } from '../base/environment';
import { toState } from '../base/redux';
import { getName } from '../app';
@@ -17,16 +17,7 @@ import { getOverlayToRender } from '../overlay';
* @returns {void}
*/
export function maybeShowSuboptimalExperienceNotification(dispatch, t) {
if (!browser.isChrome()
&& !browser.isFirefox()
&& !browser.isNWJS()
&& !browser.isElectron()
// Adding react native to the list of recommended browsers is not
// necessary for now because the function won't be executed at all
// in this case but I'm adding it for completeness.
&& !browser.isReactNative()
) {
if (isSuboptimalBrowser()) {
dispatch(
showWarningNotification(
{

View File

@@ -5,12 +5,11 @@ import {
CONFERENCE_JOINED,
KICKED_OUT,
VIDEO_QUALITY_LEVELS,
conferenceFailed,
conferenceLeft,
getCurrentConference,
setPreferredReceiverVideoQuality
} from '../base/conference';
import { hideDialog, isDialogOpen } from '../base/dialog';
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet';
import { pinParticipant } from '../base/participants';
import { SET_REDUCED_UI } from '../base/responsive-ui';
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
@@ -46,8 +45,7 @@ MiddlewareRegistry.register(store => next => action => {
dispatch(notifyKickedOut(
action.participant,
() => {
dispatch(
conferenceFailed(action.conference, JitsiConferenceEvents.KICKED));
dispatch(conferenceLeft(action.conference));
dispatch(appNavigate(undefined));
}
));

View File

@@ -56,7 +56,7 @@ class DisplayNamePrompt extends AbstractDisplayNamePrompt<State> {
render() {
return (
<Dialog
isModal = { true }
isModal = { false }
onSubmit = { this._onSubmit }
titleKey = 'dialog.displayNameRequired'
width = 'small'>

View File

@@ -1,3 +1,5 @@
// @flow
/**
* Appends a suffix to the display name.
*
@@ -5,7 +7,7 @@
* @param {string} suffix - Suffix that will be appended.
* @returns {string} The formatted display name.
*/
export function appendSuffix(displayName, suffix) {
export function appendSuffix(displayName: string, suffix: string) {
return `${displayName || suffix || ''}${
displayName && suffix && displayName !== suffix ? ` (${suffix})` : ''}`;
}

View File

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

View File

@@ -1,18 +1,28 @@
// @flow
import { CONFERENCE_FAILED } from '../base/conference';
import { CONFERENCE_FAILED, CONFERENCE_JOINED } from '../base/conference';
import { NOTIFY_CAMERA_ERROR, NOTIFY_MIC_ERROR } from '../base/devices';
import { JitsiConferenceErrors } from '../base/lib-jitsi-meet';
import {
getAvatarURLByParticipantId,
getLocalParticipant
} from '../base/participants';
import { MiddlewareRegistry } from '../base/redux';
import { appendSuffix } from '../display-name';
import { SUBMIT_FEEDBACK } from '../feedback';
import { SET_FILMSTRIP_VISIBLE } from '../filmstrip';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* The middleware of the feature {@code external-api}.
*
* @returns {Function}
*/
MiddlewareRegistry.register((/* store */) => next => action => {
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_FAILED: {
if (action.conference
@@ -22,6 +32,27 @@ MiddlewareRegistry.register((/* store */) => next => action => {
break;
}
case CONFERENCE_JOINED: {
const state = store.getState();
const { room } = state['features/base/conference'];
const { name, id } = getLocalParticipant(state);
APP.API.notifyConferenceJoined(
room,
id,
{
displayName: name,
formattedDisplayName: appendSuffix(
name,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME
),
avatarURL: getAvatarURLByParticipantId(state, id)
}
);
break;
}
case NOTIFY_CAMERA_ERROR:
if (action.error) {
APP.API.notifyOnCameraError(
@@ -34,7 +65,15 @@ MiddlewareRegistry.register((/* store */) => next => action => {
APP.API.notifyOnMicError(action.error.name, action.error.message);
}
break;
case SET_FILMSTRIP_VISIBLE:
APP.API.notifyFilmstripDisplayChanged(action.visible);
break;
case SUBMIT_FEEDBACK:
APP.API.notifyFeedbackSubmitted();
break;
}
return next(action);
return result;
});

View File

@@ -0,0 +1,39 @@
// @flow
import { getLocalParticipant } from '../base/participants';
import { StateListenerRegistry } from '../base/redux';
import { appendSuffix } from '../display-name';
import { shouldDisplayTileView } from '../video-layout';
declare var APP: Object;
declare var interfaceConfig: Object;
/**
* StateListenerRegistry provides a reliable way of detecting changes to
* preferred layout state and dispatching additional actions.
*/
StateListenerRegistry.register(
/* selector */ state => shouldDisplayTileView(state),
/* listener */ displayTileView => {
APP.API.notifyTileViewChanged(displayTileView);
});
StateListenerRegistry.register(
/* selector */ state => state['features/base/settings'].displayName,
/* listener */ (displayName, store) => {
const localParticipant = getLocalParticipant(store.getState());
// Initial setting of the display name occurs happens on app
// initialization, before the local participant is ready. The initial
// settings is not desired to be fired anyways, only changes.
if (localParticipant) {
const { id } = localParticipant;
APP.API.notifyDisplayNameChanged(id, {
displayName,
formattedDisplayName: appendSuffix(
displayName,
interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME)
});
}
});

View File

@@ -2,5 +2,4 @@ export * from './actions';
export * from './actionTypes';
export * from './components';
import './middleware';
import './reducer';

View File

@@ -1,26 +0,0 @@
/* @flow */
import { MiddlewareRegistry } from '../base/redux';
import { SUBMIT_FEEDBACK } from './actionTypes';
declare var APP: Object;
/**
* Implements the middleware of the feature feedback.
*
* @param {Store} store - The redux store.
* @returns {Function}
*/
// eslint-disable-next-line no-unused-vars
MiddlewareRegistry.register(store => next => action => {
switch (action.type) {
case SUBMIT_FEEDBACK:
if (typeof APP === 'object') {
APP.API.notifyFeedbackSubmitted();
}
break;
}
return next(action);
});

View File

@@ -11,7 +11,6 @@ import {
PARTICIPANT_ROLE,
ParticipantView,
isEveryoneModerator,
isLocalParticipantModerator,
pinParticipant
} from '../../../base/participants';
import { Container } from '../../../base/react';
@@ -21,6 +20,7 @@ import { getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayNameLabel } from '../../../display-name';
import { RemoteVideoMenu } from '../../../remote-video-menu';
import { toggleToolboxVisible } from '../../../toolbox';
import AudioMutedIndicator from './AudioMutedIndicator';
import DominantSpeakerIndicator from './DominantSpeakerIndicator';
@@ -44,11 +44,6 @@ type Props = {
*/
_isEveryoneModerator: boolean,
/**
* True if the local participant is a moderator.
*/
_isModerator: boolean,
/**
* The Redux representation of the state "features/large-video".
*/
@@ -74,12 +69,6 @@ type Props = {
*/
_videoTrack: Object,
/**
* If true, tapping on the thumbnail will not pin the participant to large
* video. By default tapping does pin the participant.
*/
disablePin?: boolean,
/**
* If true, there will be no color overlay (tint) on the thumbnail
* indicating the participant associated with the thumbnail is displayed on
@@ -105,7 +94,12 @@ type Props = {
/**
* Optional styling to add or override on the Thumbnail component root.
*/
styleOverrides?: Object
styleOverrides?: Object,
/**
* If true, it tells the thumbnail that it needs to behave differently. E.g. react differently to a single tap.
*/
tileView?: boolean
};
/**
@@ -124,16 +118,15 @@ class Thumbnail extends Component<Props> {
const {
_audioTrack: audioTrack,
_isEveryoneModerator,
_isModerator,
_largeVideo: largeVideo,
_onClick,
_onShowRemoteVideoMenu,
_styles,
_videoTrack: videoTrack,
disablePin,
disableTint,
participant,
renderDisplayName
renderDisplayName,
tileView
} = this.props;
// We don't render audio in any of the following:
@@ -148,17 +141,14 @@ class Thumbnail extends Component<Props> {
const participantInLargeVideo
= participantId === largeVideo.participantId;
const videoMuted = !videoTrack || videoTrack.muted;
const showRemoteVideoMenu = _isModerator && !participant.local;
return (
<Container
onClick = { disablePin ? undefined : _onClick }
onLongPress = {
showRemoteVideoMenu
? _onShowRemoteVideoMenu : undefined }
onClick = { _onClick }
onLongPress = { participant.local ? undefined : _onShowRemoteVideoMenu }
style = { [
styles.thumbnail,
participant.pinned && !disablePin
participant.pinned && !tileView
? _styles.thumbnailPinned : null,
this.props.styleOverrides || null
] }
@@ -234,10 +224,13 @@ function _mapDispatchToProps(dispatch: Function, ownProps): Object {
* @returns {void}
*/
_onClick() {
const { participant } = ownProps;
const { participant, tileView } = ownProps;
dispatch(
pinParticipant(participant.pinned ? null : participant.id));
if (tileView) {
dispatch(toggleToolboxVisible());
} else {
dispatch(pinParticipant(participant.pinned ? null : participant.id));
}
},
/**
@@ -262,7 +255,6 @@ function _mapDispatchToProps(dispatch: Function, ownProps): Object {
* @param {Props} ownProps - Properties of component.
* @returns {{
* _audioTrack: Track,
* _isModerator: boolean,
* _largeVideo: Object,
* _styles: StyleType,
* _videoTrack: Track
@@ -283,7 +275,6 @@ function _mapStateToProps(state, ownProps) {
return {
_audioTrack: audioTrack,
_isEveryoneModerator: isEveryoneModerator(state),
_isModerator: isLocalParticipantModerator(state),
_largeVideo: largeVideo,
_styles: ColorSchemeRegistry.get(state, 'Thumbnail'),
_videoTrack: videoTrack

View File

@@ -298,12 +298,12 @@ class TileView extends Component<Props, State> {
return this._getSortedParticipants()
.map(participant => (
<Thumbnail
disablePin = { true }
disableTint = { true }
key = { participant.id }
participant = { participant }
renderDisplayName = { true }
styleOverrides = { styleOverrides } />));
styleOverrides = { styleOverrides }
tileView = { true } />));
}
/**

View File

@@ -11,6 +11,10 @@ import {
isAddPeopleEnabled,
isDialOutEnabled
} from '../../functions';
import {
NOTIFICATION_TIMEOUT,
showNotification
} from '../../../notifications';
const logger = require('jitsi-meet-logger').getLogger(__filename);
@@ -21,6 +25,11 @@ export type Props = {
*/
_addPeopleEnabled: boolean,
/**
* Whether or not call flows are enabled.
*/
_callFlowsEnabled: boolean,
/**
* The URL for validating if a phone number can be called.
*/
@@ -115,7 +124,7 @@ export default class AbstractAddPeopleDialog<P: Props, S: State>
addToCallInProgress: true
});
const { dispatch } = this.props;
const { _callFlowsEnabled, dispatch } = this.props;
return dispatch(invite(invitees))
.then(invitesLeftToSend => {
@@ -140,6 +149,39 @@ export default class AbstractAddPeopleDialog<P: Props, S: State>
this.setState({
addToCallError: true
});
} else if (!_callFlowsEnabled) {
const invitedCount = invitees.length;
let notificationProps;
if (invitedCount >= 3) {
notificationProps = {
titleArguments: {
name: invitees[0].name,
count: invitedCount - 1
},
titleKey: 'notify.invitedThreePlusMembers'
};
} else if (invitedCount === 2) {
notificationProps = {
titleArguments: {
first: invitees[0].name,
second: invitees[1].name
},
titleKey: 'notify.invitedTwoMembers'
};
} else if (invitedCount) {
notificationProps = {
titleArguments: {
name: invitees[0].name
},
titleKey: 'notify.invitedOneMember'
};
}
if (notificationProps) {
dispatch(
showNotification(notificationProps, NOTIFICATION_TIMEOUT));
}
}
return invitesLeftToSend;
@@ -206,6 +248,7 @@ export default class AbstractAddPeopleDialog<P: Props, S: State>
*/
export function _mapStateToProps(state: Object) {
const {
callFlowsEnabled,
dialOutAuthUrl,
peopleSearchQueryTypes,
peopleSearchUrl
@@ -213,6 +256,7 @@ export function _mapStateToProps(state: Object) {
return {
_addPeopleEnabled: isAddPeopleEnabled(state),
_callFlowsEnabled: callFlowsEnabled,
_dialOutAuthUrl: dialOutAuthUrl,
_dialOutEnabled: isDialOutEnabled(state),
_jwt: state['features/base/jwt'].jwt,

View File

@@ -212,7 +212,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
* @returns {string}
*/
_keyExtractor(item) {
return item.type === 'user' ? item.user_id : item.number;
return item.type === 'user' ? item.id || item.user_id : item.number;
}
_onCloseAddPeopleDialog: () => void
@@ -315,8 +315,9 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
&& !inviteItems.find(
_.matchesProperty('number', result.number));
case 'user':
return result.user_id && !inviteItems.find(
_.matchesProperty('user_id', result.user_id));
return !inviteItems.find(
(result.user_id && _.matchesProperty('id', result.id))
|| (result.user_id && _.matchesProperty('user_id', result.user_id)));
default:
return false;
}
@@ -367,10 +368,12 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
break;
case 'user':
selected
= inviteItems.find(_.matchesProperty('user_id', item.user_id));
= item.id
? inviteItems.find(_.matchesProperty('id', item.id))
: inviteItems.find(_.matchesProperty('user_id', item.user_id));
renderableItem = {
avatar: item.avatar,
key: item.user_id,
key: item.id || item.user_id,
title: item.name
};
break;

View File

@@ -254,10 +254,10 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
if (invitesLeftToSend.length) {
const unsentInviteIDs
= invitesLeftToSend.map(invitee =>
invitee.id || invitee.number);
invitee.id || invitee.user_id || invitee.number);
const itemsToSelect
= inviteItems.filter(({ item }) =>
unsentInviteIDs.includes(item.id || item.number));
unsentInviteIDs.includes(item.id || item.user_id || item.number));
if (this._multiselect) {
this._multiselect.setSelectedItems(itemsToSelect);
@@ -296,7 +296,7 @@ class AddPeopleDialog extends AbstractAddPeopleDialog<Props, State> {
size = 'xsmall'
src = { user.avatar } />
},
value: user.id
value: user.id || user.user_id
};
});

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { translate, translateToHTML } from '../../../base/i18n';
import { translate } from '../../../base/i18n';
import AbstractSuspendedOverlay from './AbstractSuspendedOverlay';
import OverlayFrame from './OverlayFrame';
@@ -31,11 +31,6 @@ class SuspendedOverlay extends AbstractSuspendedOverlay {
className = 'inlay__title'>
{ t('suspendedoverlay.title') }
</h3>
<span className = 'inlay__text'>
{
translateToHTML(t, 'suspendedoverlay.title')
}
</span>
<ReloadButton textKey = 'suspendedoverlay.rejoinKeyTitle' />
</div>
</OverlayFrame>

View File

@@ -1,14 +1,21 @@
// @flow
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractKickButton from '../AbstractKickButton';
/**
* We don't need any further implementation for this on mobile, but we keep it
* here for clarity and consistency with web. Once web uses the
* {@code AbstractButton} base class, we can remove all these and just use
* the {@code AbstractKickButton} as {@KickButton}.
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
export default translate(connect()(AbstractKickButton));
function _mapStateToProps(state) {
return {
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractKickButton));

View File

@@ -1,14 +1,23 @@
// @flow
import { translate } from '../../../base/i18n';
import { isLocalParticipantModerator } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractMuteButton, { _mapStateToProps } from '../AbstractMuteButton';
import AbstractMuteButton, { _mapStateToProps as _abstractMapStateToProps } from '../AbstractMuteButton';
/**
* We don't need any further implementation for this on mobile, but we keep it
* here for clarity and consistency with web. Once web uses the
* {@code AbstractButton} base class, we can remove all these and just use
* the {@code AbstractMuteButton} as {@MuteButton}.
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - Properties of component.
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
return {
..._abstractMapStateToProps(state, ownProps),
visible: isLocalParticipantModerator(state)
};
}
export default translate(connect(_mapStateToProps)(AbstractMuteButton));

View File

@@ -0,0 +1,65 @@
// @flow
import { translate } from '../../../base/i18n';
import { pinParticipant } from '../../../base/participants';
import { connect } from '../../../base/redux';
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox';
export type Props = AbstractButtonProps & {
/**
* True if tile view is currently enabled.
*/
_tileViewEnabled: boolean,
/**
* The redux {@code dispatch} function.
*/
dispatch: Function,
/**
* The ID of the participant that this button is supposed to pin.
*/
participantID: string,
/**
* The function to be used to translate i18n labels.
*/
t: Function
};
/**
* A remote video menu button which pins a participant and exist the tile view.
*/
class PinButton extends AbstractButton<Props, *> {
accessibilityLabel = 'toolbar.accessibilityLabel.show';
iconName = 'icon-enlarge';
label = 'videothumbnail.show';
/**
* Handles clicking / pressing the button, and kicks the participant.
*
* @private
* @returns {void}
*/
_handleClick() {
const { dispatch } = this.props;
// Pin participant, it will automatically exit the tile view
dispatch(pinParticipant(this.props.participantID));
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function _mapStateToProps(state) {
return {
visible: state['features/video-layout'].tileViewEnabled
};
}
export default translate(connect(_mapStateToProps)(PinButton));

View File

@@ -19,6 +19,7 @@ import { hideRemoteVideoMenu } from '../../actions';
import KickButton from './KickButton';
import MuteButton from './MuteButton';
import PinButton from './PinButton';
import styles from './styles';
/**
@@ -94,6 +95,7 @@ class RemoteVideoMenu extends Component<Props> {
</View>
<MuteButton { ...buttonProps } />
<KickButton { ...buttonProps } />
<PinButton { ...buttonProps } />
</BottomSheet>
);
}

View File

@@ -102,3 +102,12 @@ export const SET_TOOLBOX_TIMEOUT_MS = 'SET_TOOLBOX_TIMEOUT_MS';
* }
*/
export const SET_TOOLBOX_VISIBLE = 'SET_TOOLBOX_VISIBLE';
/**
* The type of the redux action which toggles the toolbox visibility regardless of it's current state.
*
* {
* type: TOGGLE_TOOLBOX_VISIBLE
* }
*/
export const TOGGLE_TOOLBOX_VISIBLE = 'TOGGLE_TOOLBOX_VISIBLE';

View File

@@ -8,7 +8,8 @@ import {
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
@@ -144,3 +145,16 @@ export function setToolboxVisible(visible: boolean): Object {
visible
};
}
/**
* Action to toggle the toolbox visibility.
*
* @returns {{
* type: TOGGLE_TOOLBOX_VISIBLE
* }}
*/
export function toggleToolboxVisible() {
return {
type: TOGGLE_TOOLBOX_VISIBLE
};
}

View File

@@ -11,7 +11,8 @@ import {
SET_TOOLBOX_ENABLED,
SET_TOOLBOX_TIMEOUT,
SET_TOOLBOX_TIMEOUT_MS,
SET_TOOLBOX_VISIBLE
SET_TOOLBOX_VISIBLE,
TOGGLE_TOOLBOX_VISIBLE
} from './actionTypes';
declare var interfaceConfig: Object;
@@ -165,7 +166,10 @@ ReducerRegistry.register(
};
case SET_TOOLBOX_VISIBLE:
return set(state, 'visible', action.visible);
return set(state, 'visible', state.alwaysVisible || action.visible);
case TOGGLE_TOOLBOX_VISIBLE:
return set(state, 'visible', state.alwaysVisible || !state.visible);
}
return state;

View File

@@ -2,6 +2,7 @@
import React, { Component } from 'react';
import { isBrowsersOptimal } from '../../base/environment';
import { translate } from '../../base/i18n';
import { CHROME, FIREFOX } from './browserLinks';
@@ -47,14 +48,26 @@ class UnsupportedDesktopBrowser extends Component<Props> {
Please try again with the latest version of&nbsp;
<a
className = { `${_SNS}__link` }
href = { CHROME } >Chrome</a> and&nbsp;
<a
className = { `${_SNS}__link` }
href = { FIREFOX }>Firefox</a>
href = { CHROME } >Chrome</a>&nbsp;
{
this._showFirefox() && <>and <a
className = { `${_SNS}__link` }
href = { FIREFOX }>Firefox</a></>
}
</p>
</div>
);
}
/**
* Returns whether or not a link to download Firefox is displayed.
*
* @private
* @returns {boolean}
*/
_showFirefox() {
return isBrowsersOptimal('firefox');
}
}
export default translate(UnsupportedDesktopBrowser);

View File

@@ -75,7 +75,6 @@ MiddlewareRegistry.register(store => next => action => {
case SET_FILMSTRIP_VISIBLE:
VideoLayout.resizeVideoArea(true, false);
APP.API.notifyFilmstripDisplayChanged(action.visible);
break;
case TRACK_ADDED:

View File

@@ -36,10 +36,6 @@ StateListenerRegistry.register(
_updateAutoPinnedParticipant(store);
}
}
if (typeof APP === 'object') {
APP.API.notifyTileViewChanged(displayTileView);
}
}
);
@@ -59,7 +55,13 @@ StateListenerRegistry.register(
= store.getState()['features/video-layout'].screenShares || [];
const knownSharingParticipantIds = tracks.reduce((acc, track) => {
if (track.mediaType === 'video' && track.videoType === 'desktop') {
acc.push(track.participantId);
const skipTrack
= interfaceConfig.AUTO_PIN_LATEST_SCREEN_SHARE === 'remote-only'
&& track.local;
if (!skipTrack) {
acc.push(track.participantId);
}
}
return acc;

View File

@@ -9,6 +9,8 @@ import {
View
} from 'react-native';
import { getName } from '../../app';
import { ColorSchemeRegistry } from '../../base/color-scheme';
import { translate } from '../../base/i18n';
import { Icon } from '../../base/font-icons';
@@ -23,11 +25,12 @@ import {
import { DialInSummary } from '../../invite';
import { SettingsView } from '../../settings';
import { setSideBarVisible } from '../actions';
import {
AbstractWelcomePage,
_mapStateToProps as _abstractMapStateToProps
} from './AbstractWelcomePage';
import { setSideBarVisible } from '../actions';
import LocalVideoTrackUnderlay from './LocalVideoTrackUnderlay';
import styles, { PLACEHOLDER_TEXT_COLOR } from './styles';
import VideoSwitch from './VideoSwitch';
@@ -95,52 +98,13 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {ReactElement}
*/
render() {
const roomnameAccLabel = 'welcomepage.accessibilityLabel.roomname';
const { _headerStyles, t } = this.props;
const { _reducedUI } = this.props;
return (
<LocalVideoTrackUnderlay style = { styles.welcomePage }>
<View style = { _headerStyles.page }>
<Header style = { styles.header }>
<TouchableOpacity onPress = { this._onShowSideBar } >
<Icon
name = 'menu'
style = { _headerStyles.headerButtonIcon } />
</TouchableOpacity>
<VideoSwitch />
</Header>
<SafeAreaView style = { styles.roomContainer } >
<View style = { styles.joinControls } >
<TextInput
accessibilityLabel = { t(roomnameAccLabel) }
autoCapitalize = 'none'
autoComplete = 'off'
autoCorrect = { false }
autoFocus = { false }
onBlur = { this._onFieldBlur }
onChangeText = { this._onRoomChange }
onFocus = { this._onFieldFocus }
onSubmitEditing = { this._onJoin }
placeholder = { t('welcomepage.roomname') }
placeholderTextColor = {
PLACEHOLDER_TEXT_COLOR
}
returnKeyType = { 'go' }
style = { styles.textInput }
underlineColorAndroid = 'transparent'
value = { this.state.room } />
{
this._renderHintBox()
}
</View>
</SafeAreaView>
<WelcomePageLists disabled = { this.state._fieldFocused } />
<SettingsView />
<DialInSummary />
</View>
<WelcomePageSideBar />
</LocalVideoTrackUnderlay>
);
if (_reducedUI) {
return this._renderReducedUI();
}
return this._renderFullUI();
}
/**
@@ -272,20 +236,92 @@ class WelcomePage extends AbstractWelcomePage {
</TouchableHighlight>
);
}
/**
* Renders the full welcome page.
*
* @returns {ReactElement}
*/
_renderFullUI() {
const roomnameAccLabel = 'welcomepage.accessibilityLabel.roomname';
const { _headerStyles, t } = this.props;
return (
<LocalVideoTrackUnderlay style = { styles.welcomePage }>
<View style = { _headerStyles.page }>
<Header style = { styles.header }>
<TouchableOpacity onPress = { this._onShowSideBar } >
<Icon
name = 'menu'
style = { _headerStyles.headerButtonIcon } />
</TouchableOpacity>
<VideoSwitch />
</Header>
<SafeAreaView style = { styles.roomContainer } >
<View style = { styles.joinControls } >
<TextInput
accessibilityLabel = { t(roomnameAccLabel) }
autoCapitalize = 'none'
autoComplete = 'off'
autoCorrect = { false }
autoFocus = { false }
onBlur = { this._onFieldBlur }
onChangeText = { this._onRoomChange }
onFocus = { this._onFieldFocus }
onSubmitEditing = { this._onJoin }
placeholder = { t('welcomepage.roomname') }
placeholderTextColor = {
PLACEHOLDER_TEXT_COLOR
}
returnKeyType = { 'go' }
style = { styles.textInput }
underlineColorAndroid = 'transparent'
value = { this.state.room } />
{
this._renderHintBox()
}
</View>
</SafeAreaView>
<WelcomePageLists disabled = { this.state._fieldFocused } />
<SettingsView />
<DialInSummary />
</View>
<WelcomePageSideBar />
</LocalVideoTrackUnderlay>
);
}
/**
* Renders a "reduced" version of the welcome page.
*
* @returns {ReactElement}
*/
_renderReducedUI() {
const { t } = this.props;
return (
<View style = { styles.reducedUIContainer }>
<Text style = { styles.reducedUIText }>
{ t('welcomepage.reducedUIText', { app: getName() }) }
</Text>
</View>
);
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _headerStyles: Object
* }}
* @returns {Object}
*/
function _mapStateToProps(state) {
const { reducedUI } = state['features/base/responsive-ui'];
return {
..._abstractMapStateToProps(state),
_headerStyles: ColorSchemeRegistry.get(state, 'Header')
_headerStyles: ColorSchemeRegistry.get(state, 'Header'),
_reducedUI: reducedUI
};
}

View File

@@ -163,6 +163,21 @@ export default createStyleSheet({
flexDirection: 'column'
},
/**
* The styles for reduced UI mode.
*/
reducedUIContainer: {
alignItems: 'center',
backgroundColor: ColorPalette.blue,
flex: 1,
justifyContent: 'center'
},
reducedUIText: {
color: TEXT_COLOR,
fontSize: 12
},
/**
* Container for room name input box and 'join' button.
*/