Compare commits

...

34 Commits
4635 ... 4652

Author SHA1 Message Date
Дамян Минков
6689aa3700 feat: Detects shard changed when using websockets.
* feat: Detects shard changed when using websockets. (#1462)

1009693f2e...cb484cf48c
2021-01-15 09:51:43 -06:00
Avram Tudor
13bc9863cb Merge pull request #8426 from jitsi/tavram/gradient-condition
fix(subject) remove gradient if no info in topbar
2021-01-15 16:26:19 +02:00
Tudor-Ovidiu Avram
7ff332b2bb fix(subject) remove gradient if no info in topbar 2021-01-15 16:02:50 +02:00
Mihai-Andrei Uscat
8aae2065dc fix(Toolbox): Fix toolbox display when accessing it via keyboard 2021-01-15 13:43:09 +02:00
Jaya Allamsetty
b65e61f633 feat: Add new codec selection mechanism.
When an endpoint that doesn't support the preferred codec (VP9) joins a conference, all the other endpoints fallback to VP8 until the endpoint leaves the call.
2021-01-14 18:01:38 -05:00
Saúl Ibarra Corretgé
88f1c218eb fix(rn,stats) fix incorrect bitrate calculation on mobile
Stats timestamps were incorrectly formatted, fixed upstream.

Fixes: https://github.com/jitsi/jitsi-meet/issues/8367
2021-01-14 14:59:22 +01:00
Boris Grozev
f6df76ab10 fix: Fix broken postinst reported by @wsldankers. 2021-01-14 07:55:50 -06:00
Saúl Ibarra Corretgé
85f1701393 fix(tile-view) avoid covering the logo
Reverts a8b2e6ffb3
2021-01-13 20:29:20 +01:00
damencho
2f7ff37472 fix: Fixes #8396, wrong ssi includes in offline static page. 2021-01-13 09:11:10 -06:00
Mihai-Andrei Uscat
c752ea13f1 feat(overflow): Add responsive drawer at small screen width.
* Implement opening toolbar and participant overflows as drawers when below certain width.
* Fix dial-in copy button displaying incorrectly.
2021-01-13 16:07:22 +01:00
tmoldovan8x8
5ef60c3a7d [WIP] adds BroadcastService (#8336)
feat(external_api) exposes more events from JS to native and adds the ability to send actions from native to JS.
2021-01-13 15:48:29 +02:00
hmuresan
1196ede961 feat(external-api) set privateMessage flag on incoming-message 2021-01-13 13:33:55 +01:00
Saúl Ibarra Corretgé
12877c7fce fix(settings) remove legacy compatibility code 2021-01-13 13:32:54 +01:00
Avram Tudor
c6bb600d4c Merge pull request #8400 from jitsi/tavram/shortcuts
feat(external_api) allow shortcuts to be disabled
2021-01-13 12:39:27 +02:00
Avram Tudor
845e23a947 Merge pull request #8399 from jitsi/tavram/overflowmenu
fix(menu) do not display overflow menu button if no items
2021-01-13 12:39:11 +02:00
Tudor-Ovidiu Avram
55ebb60f85 feat(external_api) allow shortcuts to be disabled 2021-01-13 12:10:27 +02:00
Tudor-Ovidiu Avram
8d3d94f568 fix(menu) do not display overflow menu button if no items 2021-01-13 11:55:38 +02:00
Avram Tudor
be24772e57 Merge pull request #8397 from jitsi/tavram/disable-jaas-directory
fix(jaas) disable directory integration
2021-01-13 11:38:05 +02:00
Tudor-Ovidiu Avram
a807f804a9 fix(jaas) disable directory integration 2021-01-13 11:13:04 +02:00
Hristo Terezov
db48dc3ed3 fix(Thumbnail): volume & audioLevel default values 2021-01-12 13:42:31 -06:00
Hristo Terezov
9bae7099dd fix(filmstrip): Import from base/tracks. 2021-01-12 13:42:31 -06:00
Hristo Terezov
e990f6984a fix(Thumbnail): Improve naming. 2021-01-12 13:42:31 -06:00
Hristo Terezov
9f321c988e style(isVideoPlayable): improve readability. 2021-01-12 13:42:31 -06:00
Hristo Terezov
2e5e9a3f79 fix(AudioTrack):Add check for NaN value for volume 2021-01-12 13:42:31 -06:00
Hristo Terezov
fdb8f76b90 fix(Thumbnail): imports. 2021-01-12 13:42:31 -06:00
Hristo Terezov
3d97bef308 style(Thumbnail): improve readability
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
5a55c7b965 style(AudioTrack): improve readability
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
e161cbc4bd fix(SmallVideo): computeDisplayModeInput
Co-authored-by: Saúl Ibarra Corretgé <saghul@jitsi.org>
2021-01-12 13:42:31 -06:00
Hristo Terezov
51e381a0b1 ref(Thumbnail): Create React component. 2021-01-12 13:42:31 -06:00
bgrozev
d8dd644f38 make sure extra plugin paths are enabled (#8390)
* fix: Make sure extra plugin_paths are not commented out.

* fix: Do not use "-e" as backup suffix.
2021-01-12 13:25:20 -06:00
Saúl Ibarra Corretgé
e30b2e14a5 fix(rn) stop room name generator when field is focused
Fixes: https://github.com/jitsi/jitsi-meet/issues/8307
2021-01-12 17:36:09 +01:00
Avram Tudor
7f1894dd57 Merge pull request #8379 from horymury/hmuresan/broadcast-screenshare
feat(external_api) add command and event listener for CS
2021-01-12 17:49:06 +02:00
hmuresan
4dda508708 feat(external_api) add command and event listener for CS 2021-01-12 17:23:40 +02:00
damencho
69c6463476 chore(deps) lib-jitsi-meet@latest
* fix: Disabling lobby when using tenant.

87c6e37475...7896fc8b92
2021-01-12 08:22:14 -06:00
78 changed files with 2365 additions and 1129 deletions

View File

@@ -38,7 +38,7 @@ import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Map;
import java.util.HashMap;
/**
* The one and only Activity that the Jitsi Meet app needs. The
@@ -183,8 +183,8 @@ public class MainActivity extends JitsiMeetActivity {
}
@Override
public void onConferenceTerminated(Map<String, Object> data) {
Log.d(TAG, "Conference terminated: " + data);
protected void onConferenceTerminated(HashMap<String, Object> extraData) {
Log.d(TAG, "Conference terminated: " + extraData);
}
// Activity lifecycle method overrides

View File

@@ -36,6 +36,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.fragment:fragment:1.2.5'
implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
//noinspection GradleDynamicVersion

View File

@@ -99,6 +99,7 @@ public abstract class BaseReactView<ListenerT>
* The listener (e.g. {@link JitsiMeetViewListener}) instance for reporting
* events occurring in Jitsi Meet.
*/
@Deprecated
private ListenerT listener;
/**
@@ -167,6 +168,7 @@ public abstract class BaseReactView<ListenerT>
*
* @return The listener set on this {@code BaseReactView}.
*/
@Deprecated
public ListenerT getListener() {
return listener;
}
@@ -179,8 +181,10 @@ public abstract class BaseReactView<ListenerT>
* @param data - The details of the event associated with/specific to the
* specified {@code name}.
*/
@Deprecated
protected abstract void onExternalAPIEvent(String name, ReadableMap data);
@Deprecated
protected void onExternalAPIEvent(
Map<String, Method> listenerMethods,
String name, ReadableMap data) {
@@ -215,6 +219,7 @@ public abstract class BaseReactView<ListenerT>
*
* @param listener The listener to set on this {@code BaseReactView}.
*/
@Deprecated
public void setListener(ListenerT listener) {
this.listener = listener;
}

View File

@@ -0,0 +1,84 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.bridge.WritableNativeMap;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* Wraps the name and extra data for events that were broadcasted locally.
*/
public class BroadcastAction {
private static final String TAG = BroadcastAction.class.getSimpleName();
private final Type type;
private final HashMap<String, Object> data;
public BroadcastAction(Intent intent) {
this.type = Type.buildTypeFromAction(intent.getAction());
this.data = buildDataFromBundle(intent.getExtras());
}
public Type getType() {
return this.type;
}
public HashMap<String, Object> getData() {
return this.data;
}
public WritableNativeMap getDataAsWritableNativeMap() {
WritableNativeMap nativeMap = new WritableNativeMap();
for (String key : this.data.keySet()) {
try {
// TODO add support for different types of objects
nativeMap.putString(key, this.data.get(key).toString());
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data in event", e);
}
}
return nativeMap;
}
private static HashMap<String, Object> buildDataFromBundle(Bundle bundle) {
HashMap<String, Object> map = new HashMap<>();
if (bundle != null) {
for (String key : bundle.keySet()) {
map.put(key, bundle.get(key));
}
}
return map;
}
enum Type {
SET_AUDIO_MUTED("org.jitsi.meet.SET_AUDIO_MUTED"),
HANG_UP("org.jitsi.meet.HANG_UP");
private final String action;
Type(String action) {
this.action = action;
}
public String getAction() {
return action;
}
private static Type buildTypeFromAction(String action) {
for (Type type : Type.values()) {
if (type.action.equalsIgnoreCase(action)) {
return type;
}
}
return null;
}
}
}

View File

@@ -0,0 +1,30 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.bridge.ReadableMap;
/**
* Class used to emit events through the LocalBroadcastManager, called when events
* from JS occurred. Takes an action name from JS, builds and broadcasts the {@link BroadcastEvent}
*/
public class BroadcastEmitter {
private final LocalBroadcastManager localBroadcastManager;
public BroadcastEmitter(Context context) {
localBroadcastManager = LocalBroadcastManager.getInstance(context);
}
public void sendBroadcast(String name, ReadableMap data) {
BroadcastEvent event = new BroadcastEvent(name, data);
Intent intent = event.buildIntent();
if (intent != null) {
localBroadcastManager.sendBroadcast(intent);
}
}
}

View File

@@ -0,0 +1,130 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
import android.os.Bundle;
import com.facebook.react.bridge.ReadableMap;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
/**
* Wraps the name and extra data for the events that occur on the JS side and are
* to be broadcasted.
*/
public class BroadcastEvent {
private static final String TAG = BroadcastEvent.class.getSimpleName();
private final Type type;
private final HashMap<String, Object> data;
public BroadcastEvent(String name, ReadableMap data) {
this.type = Type.buildTypeFromName(name);
this.data = data.toHashMap();
}
public BroadcastEvent(Intent intent) {
this.type = Type.buildTypeFromAction(intent.getAction());
this.data = buildDataFromBundle(intent.getExtras());
}
public Type getType() {
return this.type;
}
public HashMap<String, Object> getData() {
return this.data;
}
public Intent buildIntent() {
if (type != null && type.action != null) {
Intent intent = new Intent(type.action);
for (String key : this.data.keySet()) {
try {
intent.putExtra(key, this.data.get(key).toString());
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data in event", e);
}
}
return intent;
}
return null;
}
private static HashMap<String, Object> buildDataFromBundle(Bundle bundle) {
if (bundle != null) {
try {
HashMap<String, Object> map = new HashMap<>();
for (String key : bundle.keySet()) {
map.put(key, bundle.get(key));
}
return map;
} catch (Exception e) {
JitsiMeetLogger.w(TAG + " invalid extra data", e);
}
}
return null;
}
public enum Type {
CONFERENCE_JOINED("org.jitsi.meet.CONFERENCE_JOINED"),
CONFERENCE_TERMINATED("org.jitsi.meet.CONFERENCE_TERMINATED"),
CONFERENCE_WILL_JOIN("org.jitsi.meet.CONFERENCE_WILL_JOIN"),
AUDIO_MUTED_CHANGED("org.jitsi.meet.AUDIO_MUTED_CHANGED"),
PARTICIPANT_JOINED("org.jitsi.meet.PARTICIPANT_JOINED"),
PARTICIPANT_LEFT("org.jitsi.meet.PARTICIPANT_LEFT");
private static final String CONFERENCE_WILL_JOIN_NAME = "CONFERENCE_WILL_JOIN";
private static final String CONFERENCE_JOINED_NAME = "CONFERENCE_JOINED";
private static final String CONFERENCE_TERMINATED_NAME = "CONFERENCE_TERMINATED";
private static final String AUDIO_MUTED_CHANGED_NAME = "AUDIO_MUTED_CHANGED";
private static final String PARTICIPANT_JOINED_NAME = "PARTICIPANT_JOINED";
private static final String PARTICIPANT_LEFT_NAME = "PARTICIPANT_LEFT";
private final String action;
Type(String action) {
this.action = action;
}
public String getAction() {
return action;
}
private static Type buildTypeFromAction(String action) {
for (Type type : Type.values()) {
if (type.action.equalsIgnoreCase(action)) {
return type;
}
}
return null;
}
private static Type buildTypeFromName(String name) {
switch (name) {
case CONFERENCE_WILL_JOIN_NAME:
return CONFERENCE_WILL_JOIN;
case CONFERENCE_JOINED_NAME:
return CONFERENCE_JOINED;
case CONFERENCE_TERMINATED_NAME:
return CONFERENCE_TERMINATED;
case AUDIO_MUTED_CHANGED_NAME:
return AUDIO_MUTED_CHANGED;
case PARTICIPANT_JOINED_NAME:
return PARTICIPANT_JOINED;
case PARTICIPANT_LEFT_NAME:
return PARTICIPANT_LEFT;
}
return null;
}
}
}

View File

@@ -0,0 +1,15 @@
package org.jitsi.meet.sdk;
import android.content.Intent;
public class BroadcastIntentHelper {
public static Intent buildSetAudioMutedIntent(boolean muted) {
Intent intent = new Intent(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
intent.putExtra("muted", muted);
return intent;
}
public static Intent buildHangUpIntent() {
return new Intent(BroadcastAction.Type.HANG_UP.getAction());
}
}

View File

@@ -0,0 +1,32 @@
package org.jitsi.meet.sdk;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
/**
* Listens for {@link BroadcastAction}s on LocalBroadcastManager. When one occurs,
* it emits it to JS.
*/
public class BroadcastReceiver extends android.content.BroadcastReceiver {
public BroadcastReceiver(Context context) {
LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(context);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
intentFilter.addAction(BroadcastAction.Type.HANG_UP.getAction());
localBroadcastManager.registerReceiver(this, intentFilter);
}
@Override
public void onReceive(Context context, Intent intent) {
BroadcastAction action = new BroadcastAction(intent);
String actionName = action.getType().getAction();
ReactInstanceManagerHolder.emitEvent(actionName, action.getDataAsWritableNativeMap());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-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.
@@ -24,6 +24,9 @@ import com.facebook.react.module.annotations.ReactModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
import java.util.Map;
/**
* Module implementing an API for sending events from JavaScript to native code.
*/
@@ -35,6 +38,9 @@ class ExternalAPIModule
private static final String TAG = NAME;
private final BroadcastEmitter broadcastEmitter;
private final BroadcastReceiver broadcastReceiver;
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the app.
@@ -44,6 +50,9 @@ class ExternalAPIModule
*/
public ExternalAPIModule(ReactApplicationContext reactContext) {
super(reactContext);
broadcastEmitter = new BroadcastEmitter(reactContext);
broadcastReceiver = new BroadcastReceiver(reactContext);
}
/**
@@ -56,6 +65,22 @@ class ExternalAPIModule
return NAME;
}
/**
* Gets a mapping with the constants this module is exporting.
*
* @return a {@link Map} mapping the constants to be exported with their
* values.
*/
@Override
public Map<String, Object> getConstants() {
Map<String, Object> constants = new HashMap<>();
constants.put("SET_AUDIO_MUTED", BroadcastAction.Type.SET_AUDIO_MUTED.getAction());
constants.put("HANG_UP", BroadcastAction.Type.HANG_UP.getAction());
return constants;
}
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link BaseReactView}'s listener.
@@ -79,7 +104,8 @@ class ExternalAPIModule
JitsiMeetLogger.d(TAG + " Sending event: " + name + " with data: " + data);
try {
view.onExternalAPIEvent(name, data);
} catch(Exception e) {
broadcastEmitter.sendBroadcast(name, data);
} catch (Exception e) {
JitsiMeetLogger.e(e, TAG + " onExternalAPIEvent: error sending event");
}
}

View File

@@ -16,33 +16,41 @@
package org.jitsi.meet.sdk;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.facebook.react.modules.core.PermissionListener;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Map;
import java.util.HashMap;
/**
* A base activity for SDK users to embed. It uses {@link JitsiMeetFragment} to do the heavy
* lifting and wires the remaining Activity lifecycle methods so it works out of the box.
*/
public class JitsiMeetActivity extends FragmentActivity
implements JitsiMeetActivityInterface, JitsiMeetViewListener {
implements JitsiMeetActivityInterface {
protected static final String TAG = JitsiMeetActivity.class.getSimpleName();
private static final String ACTION_JITSI_MEET_CONFERENCE = "org.jitsi.meet.CONFERENCE";
private static final String JITSI_MEET_CONFERENCE_OPTIONS = "JitsiMeetConferenceOptions";
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onBroadcastReceived(intent);
}
};
// Helpers for starting the activity
//
@@ -68,8 +76,7 @@ public class JitsiMeetActivity extends FragmentActivity
setContentView(R.layout.activity_jitsi_meet);
// Listen for conference events.
getJitsiView().setListener(this);
registerForBroadcastMessages();
if (!extraInitialize()) {
initialize();
@@ -91,6 +98,8 @@ public class JitsiMeetActivity extends FragmentActivity
}
JitsiMeetOngoingConferenceService.abort(this);
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver);
super.onDestroy();
}
@@ -113,8 +122,8 @@ public class JitsiMeetActivity extends FragmentActivity
public void join(@Nullable String url) {
JitsiMeetConferenceOptions options
= new JitsiMeetConferenceOptions.Builder()
.setRoom(url)
.build();
.setRoom(url)
.build();
join(options);
}
@@ -138,7 +147,8 @@ public class JitsiMeetActivity extends FragmentActivity
}
}
private @Nullable JitsiMeetConferenceOptions getConferenceOptions(Intent intent) {
private @Nullable
JitsiMeetConferenceOptions getConferenceOptions(Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_VIEW.equals(action)) {
@@ -157,7 +167,7 @@ public class JitsiMeetActivity extends FragmentActivity
* Helper function called during activity initialization. If {@code true} is returned, the
* initialization is delayed and the {@link JitsiMeetActivity#initialize()} method is not
* called. In this case, it's up to the subclass to call the initialize method when ready.
*
* <p>
* This is mainly required so we do some extra initialization in the Jitsi Meet app.
*
* @return {@code true} if the initialization will be delayed, {@code false} otherwise.
@@ -172,6 +182,37 @@ public class JitsiMeetActivity extends FragmentActivity
join(getConferenceOptions(getIntent()));
}
protected void onConferenceJoined(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference joined: " + extraData);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
}
protected void onConferenceTerminated(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference terminated: " + extraData);
finish();
}
protected void onConferenceWillJoin(HashMap<String, Object> extraData) {
JitsiMeetLogger.i("Conference will join: " + extraData);
}
protected void onParticipantJoined(HashMap<String, Object> extraData) {
try {
JitsiMeetLogger.i("Participant joined: ", extraData);
} catch (Exception e) {
JitsiMeetLogger.w("Invalid participant joined extraData", e);
}
}
protected void onParticipantLeft(HashMap<String, Object> extraData) {
try {
JitsiMeetLogger.i("Participant left: ", extraData);
} catch (Exception e) {
JitsiMeetLogger.w("Invalid participant left extraData", e);
}
}
// Activity lifecycle methods
//
@@ -223,24 +264,38 @@ public class JitsiMeetActivity extends FragmentActivity
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
// JitsiMeetViewListener
//
private void registerForBroadcastMessages() {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_JOINED.getAction());
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_WILL_JOIN.getAction());
intentFilter.addAction(BroadcastEvent.Type.CONFERENCE_TERMINATED.getAction());
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_JOINED.getAction());
intentFilter.addAction(BroadcastEvent.Type.PARTICIPANT_LEFT.getAction());
@Override
public void onConferenceJoined(Map<String, Object> data) {
JitsiMeetLogger.i("Conference joined: " + data);
// Launch the service for the ongoing notification.
JitsiMeetOngoingConferenceService.launch(this);
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onConferenceTerminated(Map<String, Object> data) {
JitsiMeetLogger.i("Conference terminated: " + data);
finish();
}
private void onBroadcastReceived(Intent intent) {
if (intent != null) {
BroadcastEvent event = new BroadcastEvent(intent);
@Override
public void onConferenceWillJoin(Map<String, Object> data) {
JitsiMeetLogger.i("Conference will join: " + data);
switch (event.getType()) {
case CONFERENCE_JOINED:
onConferenceJoined(event.getData());
break;
case CONFERENCE_WILL_JOIN:
onConferenceWillJoin(event.getData());
break;
case CONFERENCE_TERMINATED:
onConferenceTerminated(event.getData());
break;
case PARTICIPANT_JOINED:
onParticipantJoined(event.getData());
break;
case PARTICIPANT_LEFT:
onParticipantLeft(event.getData());
break;
}
}
}
}

View File

@@ -21,11 +21,13 @@ import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.IBinder;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
/**
* This class implements an Android {@link Service}, a foreground one specifically, and it's
@@ -35,19 +37,18 @@ import org.jitsi.meet.sdk.log.JitsiMeetLogger;
* See: https://developer.android.com/guide/components/services
*/
public class JitsiMeetOngoingConferenceService extends Service
implements OngoingConferenceTracker.OngoingConferenceListener {
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";
}
private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver();
private boolean isAudioMuted;
static void launch(Context context) {
OngoingNotification.createOngoingConferenceNotificationChannel();
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(Actions.START);
intent.setAction(Action.START.getName());
ComponentName componentName;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -70,11 +71,16 @@ public class JitsiMeetOngoingConferenceService extends Service
super.onCreate();
OngoingConferenceTracker.getInstance().addListener(this);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BroadcastEvent.Type.AUDIO_MUTED_CHANGED.getAction());
LocalBroadcastManager.getInstance(getApplicationContext()).registerReceiver(broadcastReceiver, intentFilter);
}
@Override
public void onDestroy() {
OngoingConferenceTracker.getInstance().removeListener(this);
LocalBroadcastManager.getInstance(getApplicationContext()).unregisterReceiver(broadcastReceiver);
super.onDestroy();
}
@@ -86,26 +92,37 @@ public class JitsiMeetOngoingConferenceService extends Service
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
final String action = intent.getAction();
if (Actions.START.equals(action)) {
Notification notification = OngoingNotification.buildOngoingConferenceNotification();
if (notification == null) {
final String actionName = intent.getAction();
final Action action = Action.fromName(actionName);
switch (action) {
case UNMUTE:
case MUTE:
Intent muteBroadcastIntent = BroadcastIntentHelper.buildSetAudioMutedIntent(action == Action.MUTE);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(muteBroadcastIntent);
break;
case START:
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
break;
case HANGUP:
JitsiMeetLogger.i(TAG + " Hangup requested");
Intent hangupBroadcastIntent = BroadcastIntentHelper.buildHangUpIntent();
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(hangupBroadcastIntent);
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
} else if (Actions.HANGUP.equals(action)) {
JitsiMeetLogger.i(TAG + " Hangup requested");
// Abort all ongoing calls
if (AudioModeModule.useConnectionService()) {
ConnectionService.abortConnections();
}
stopSelf();
} else {
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
stopSelf();
break;
default:
JitsiMeetLogger.w(TAG + " Unknown action received: " + action);
stopSelf();
break;
}
return START_NOT_STICKY;
@@ -118,4 +135,46 @@ public class JitsiMeetOngoingConferenceService extends Service
JitsiMeetLogger.i(TAG + "Service stopped");
}
}
public enum Action {
START(TAG + ":START"),
HANGUP(TAG + ":HANGUP"),
MUTE(TAG + ":MUTE"),
UNMUTE(TAG + ":UNMUTE");
private final String name;
Action(String name) {
this.name = name;
}
public static Action fromName(String name) {
for (Action action : Action.values()) {
if (action.name.equalsIgnoreCase(name)) {
return action;
}
}
return null;
}
public String getName() {
return name;
}
}
private class BroadcastReceiver extends android.content.BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
isAudioMuted = Boolean.parseBoolean(intent.getStringExtra("muted"));
Notification notification = OngoingNotification.buildOngoingConferenceNotification(isAudioMuted);
if (notification == null) {
stopSelf();
JitsiMeetLogger.w(TAG + " Couldn't start service, notification is null");
} else {
startForeground(OngoingNotification.NOTIFICATION_ID, notification);
JitsiMeetLogger.i(TAG + " Service started");
}
}
}
}

View File

@@ -197,6 +197,7 @@ public class JitsiMeetView extends BaseReactView<JitsiMeetViewListener>
* by/associated with the specified {@code name}.
*/
@Override
@Deprecated
protected void onExternalAPIEvent(String name, ReadableMap data) {
onExternalAPIEvent(LISTENER_METHODS, name, data);
}

View File

@@ -21,6 +21,7 @@ import java.util.Map;
/**
* Interface for listening to events coming from Jitsi Meet.
*/
@Deprecated
public interface JitsiMeetViewListener {
/**
* Called when a conference was joined.

View File

@@ -32,6 +32,7 @@ import java.util.regex.Pattern;
* Utility methods for helping with transforming {@link ExternalAPIModule}
* events into listener methods. Used with descendants of {@link BaseReactView}.
*/
@Deprecated
public final class ListenerUtils {
/**
* Extracts the methods defined in a listener and creates a mapping of this

View File

@@ -23,13 +23,13 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
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
@@ -43,7 +43,6 @@ class OngoingNotification {
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
@@ -56,7 +55,7 @@ class OngoingNotification {
}
NotificationManager notificationManager
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel(CHANNEL_ID);
@@ -73,7 +72,7 @@ class OngoingNotification {
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification() {
static Notification buildOngoingConferenceNotification(boolean isMuted) {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
@@ -83,12 +82,7 @@ class OngoingNotification {
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);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
@@ -99,21 +93,27 @@ class OngoingNotification {
.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);
NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, "Hang up");
builder.addAction(hangupAction);
}
JitsiMeetOngoingConferenceService.Action toggleAudioAction = isMuted
? JitsiMeetOngoingConferenceService.Action.UNMUTE : JitsiMeetOngoingConferenceService.Action.MUTE;
String toggleAudioTitle = isMuted ? "Unmute" : "Mute";
NotificationCompat.Action audioAction = createAction(context, toggleAudioAction, toggleAudioTitle);
builder.addAction(hangupAction);
builder.addAction(audioAction);
return builder.build();
}
private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, String title) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(action.getName());
PendingIntent pendingIntent
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Action(0, title, pendingIntent);
}
}

View File

@@ -2014,7 +2014,6 @@ export default {
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
@@ -2053,10 +2052,7 @@ export default {
(...args) => APP.store.dispatch(lockStateChanged(room, ...args)));
room.on(JitsiConferenceEvents.KICKED, participant => {
APP.UI.hideStats();
APP.store.dispatch(kickedOut(room, participant));
// FIXME close
});
room.on(JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker, kicked) => {
@@ -2386,14 +2382,11 @@ export default {
_onConferenceJoined() {
APP.UI.initConference();
APP.keyboardshortcut.init();
if (!config.disableShortcuts) {
APP.keyboardshortcut.init();
}
APP.store.dispatch(conferenceJoined(room));
const displayName
= APP.store.getState()['features/base/settings'].displayName;
APP.UI.changeDisplayName('localVideoContainer', displayName);
},
/**
@@ -2871,10 +2864,6 @@ export default {
APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}
},
/**

View File

@@ -336,6 +336,9 @@ var config = {
// will be joined when no room is specified.
enableWelcomePage: true,
// Disable app shortcuts that are registered upon joining a conference
// disableShortcuts: false,
// Disable initial browser getUserMedia requests.
// This is useful for scenarios where users might want to start a conference for screensharing only
// disableInitialGUM: false,

View File

@@ -48,3 +48,19 @@
.toolbox-button-wth-dialog .eYJELv {
max-height: initial;
}
/**
* Override @atlaskit/InlineDialog styling for the overflowmenu so it displays
* a scrollable list of elements at small screen widths.
*/
.sc-eNQAEJ {
overflow-y: auto;
}
/**
* Keep overflow menu within screen vertical bounds and make it scrollable.
*/
.toolbox-button-wth-dialog .sc-ckVGcZ.fdAqDG > :first-child {
max-height: calc(100vh - #{$newToolbarSizeWithPadding} - 16px);
overflow-y: auto;
}

124
css/_drawer.scss Normal file
View File

@@ -0,0 +1,124 @@
.drawer-portal {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: $drawerZ;
}
.drawer-menu {
padding: 12px 16px;
max-height: 50vh;
background: #242528;
border-radius: 16px 16px 0 0;
overflow-y: auto;
&.expanded {
max-height: 80vh;
}
.drawer-toggle {
display: flex;
justify-content: center;
align-items: center;
height: 44px;
cursor: pointer;
&:hover {
background-color: $overflowMenuItemHoverBG;
}
svg, path {
fill: #b8c7e0;
}
}
.popupmenu {
margin: auto;
width: 100%;
}
.popupmenu__item {
height: 48px;
}
&#{&} .overflow-menu {
margin: auto;
font-size: 1.2em;
list-style-type: none;
padding: 0;
.overflow-menu-item {
box-sizing: border-box;
height: 48px;
padding: 12px 16px;
align-items: center;
color: $overflowMenuItemColor;
cursor: pointer;
display: flex;
font-size: 14px;
div {
display: flex;
flex-direction: row;
align-items: center;
}
&:hover {
background-color: $overflowMenuItemHoverBG;
color: $overflowMenuItemHoverColor;
}
&.unclickable {
cursor: default;
}
&.unclickable:hover {
background: inherit;
}
&.disabled {
cursor: initial;
color: #3b475c;
}
}
.beta-tag {
background: $overflowMenuItemColor;
border-radius: 2px;
color: $overflowMenuBG;
font-size: 11px;
font-weight: bold;
margin-left: 8px;
padding: 0 6px;
}
.overflow-menu-item-icon {
margin-right: 10px;
i {
display: inline;
font-size: 24px;
}
i:hover {
background-color: initial;
}
img {
max-width: 24px;
max-height: 24px;
}
svg {
fill: #B8C7E0 !important;
}
}
.profile-text {
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View File

@@ -14,12 +14,15 @@
text-overflow: ellipsis;
box-sizing: border-box;
white-space: nowrap;
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
&.visible {
top: 0px;
}
&.gradient {
background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0));
}
&-text {
vertical-align: middle;
}

View File

@@ -121,6 +121,7 @@ $poweredByZ: 100;
$ringingZ: 300;
$sideToolbarContainerZ: 300;
$toolbarZ: 350;
$drawerZ: 351;
$tooltipsZ: 401;
$dropdownMaskZ: 900;
$dropdownZ: 901;

View File

@@ -22,7 +22,8 @@
display: none;
}
#remoteConnectionMessage {
#remoteConnectionMessage,
.watermark {
z-index: $filmstripVideosZ + 1;
}

View File

@@ -103,5 +103,6 @@ $flagsImagePath: "../images/";
@import 'e2ee';
@import 'responsive';
@import 'connection-status';
@import 'drawer';
/* Modules END */

View File

@@ -50,6 +50,12 @@
}
}
.dial-in-number {
display: flex;
justify-content: space-between;
padding-right: 8px;
}
.dial-in-numbers-list {
margin-top: 20px;
font-size: 12px;

View File

@@ -135,7 +135,6 @@
.dial-in-copy {
display: inline-block;
vertical-align: middle;
margin-left: 21px;
cursor: pointer;
}
}

View File

@@ -150,7 +150,14 @@ case "$1" in
# Component "focus.jitmeet.example.com" "client_proxy"
# target_address = "focus@auth.jitmeet.example.com"
if grep -q "Component \"focus.$JVB_HOSTNAME\"" $PROSODY_HOST_CONFIG && ! grep "Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"" $PROSODY_HOST_CONFIG ;then
sed -i -e "s/Component \"focus.$JVB_HOSTNAME\"/Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"\n target_address = \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\"/" $PROSODY_HOST_CONFIG
sed -i "s/Component \"focus.$JVB_HOSTNAME\"/Component \"focus.$JVB_HOSTNAME\" \"client_proxy\"\n target_address = \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\"/g" $PROSODY_HOST_CONFIG
PROSODY_CONFIG_PRESENT="false"
fi
# Old versions of jitsi-meet-prosody come with the extra plugin path commented out (https://github.com/jitsi/jitsi-meet/commit/e11d4d3101e5228bf956a69a9e8da73d0aee7949)
# Make sure it is uncommented, as it contains required modules.
if grep -q -- '--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }' $PROSODY_HOST_CONFIG ;then
sed -i 's#--plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }#g' $PROSODY_HOST_CONFIG
PROSODY_CONFIG_PRESENT="false"
fi

View File

@@ -292,7 +292,7 @@ PODS:
- React
- react-native-splash-screen (3.2.0):
- React
- react-native-webrtc (1.87.2):
- react-native-webrtc (1.87.3):
- React-Core
- react-native-webview (11.0.2):
- React-Core
@@ -563,7 +563,7 @@ SPEC CHECKSUMS:
react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae
react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a
react-native-splash-screen: 200d11d188e2e78cea3ad319964f6142b6384865
react-native-webrtc: e6fca8432542dd1c77afa6c59629f0176ed78ee6
react-native-webrtc: dc1208bdca2c4d091f7b57859e69332bff6f1986
react-native-webview: b2542d6fd424bcc3e3b2ec5f854f0abb4ec86c87
React-RCTActionSheet: bcbc311dc3b47bc8efb2737ff0940239a45789a9
React-RCTAnimation: 65f61080ce632f6dea23d52e354ffac9948396c6

View File

@@ -99,9 +99,22 @@
#if 0
- (void)enterPictureInPicture:(NSDictionary *)data {
[self _onJitsiMeetViewDelegateEvent:@"ENTER_PICTURE_IN_PICTURE" withData:data];
}
#endif
- (void)participantJoined:(NSDictionary *)data {
NSLog(@"%@%@", @"Participant joined: ", data[@"participantId"]);
}
- (void)participantLeft:(NSDictionary *)data {
NSLog(@"%@%@", @"Participant left: ", data[@"participantId"]);
}
- (void)audioMutedChanged:(NSDictionary *)data {
NSLog(@"%@%@", @"Audio muted changed: ", data[@"muted"]);
}
#pragma mark - Helpers
- (void)terminate {

View File

@@ -41,6 +41,7 @@
C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = C69EFA0B209A0F660027712B /* JMCallKitListener.swift */; };
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */; };
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
C8AFD2802462C613000293D2 /* InfoPlistUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */; };
DE438CDA2350934700DD541D /* JavaScriptSandbox.m in Sources */ = {isa = PBXBuildFile; fileRef = DE438CD82350934700DD541D /* JavaScriptSandbox.m */; };
@@ -105,6 +106,7 @@
C6A3425E204EF76800E062DD /* DragGestureController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DragGestureController.swift; sourceTree = "<group>"; };
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = "<group>"; };
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ExternalAPI.h; sourceTree = "<group>"; };
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InfoPlistUtil.h; sourceTree = "<group>"; };
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InfoPlistUtil.m; sourceTree = "<group>"; };
DE438CD82350934700DD541D /* JavaScriptSandbox.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JavaScriptSandbox.m; sourceTree = "<group>"; };
@@ -228,6 +230,7 @@
0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */,
C8AFD27D2462C613000293D2 /* InfoPlistUtil.h */,
C8AFD27E2462C613000293D2 /* InfoPlistUtil.m */,
C81E9AB825AC5AD800B134D9 /* ExternalAPI.h */,
);
path = src;
sourceTree = "<group>";
@@ -301,6 +304,7 @@
DE81A2D42316AC4D00AE1940 /* JitsiMeetLogger.h in Headers */,
DE65AACA2317FFCD00290BEC /* LogUtils.h in Headers */,
DEAD3226220C497000E93636 /* JitsiMeetConferenceOptions.h in Headers */,
C81E9AB925AC5AD800B134D9 /* ExternalAPI.h in Headers */,
C8AFD27F2462C613000293D2 /* InfoPlistUtil.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-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.

24
ios/sdk/src/ExternalAPI.h Normal file
View File

@@ -0,0 +1,24 @@
/* Copyright @ 2021-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
@interface ExternalAPI : RCTEventEmitter<RCTBridgeModule>
- (void)sendHangUp;
- (void)sendSetAudioMuted: (BOOL)muted;
@end

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-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.
@@ -14,17 +14,24 @@
* limitations under the License.
*/
#import <React/RCTBridgeModule.h>
#import "ExternalAPI.h"
#import "JitsiMeetView+Private.h"
@interface ExternalAPI : NSObject<RCTBridgeModule>
@end
// Events
static NSString * const hangUpEvent = @"org.jitsi.meet.HANG_UP";
static NSString * const setAudioMutedEvent = @"org.jitsi.meet.SET_AUDIO_MUTED";
@implementation ExternalAPI
RCT_EXPORT_MODULE();
- (NSDictionary *)constantsToExport {
return @{
@"HANG_UP": hangUpEvent,
@"SET_AUDIO_MUTED" : setAudioMutedEvent
};
};
/**
* Make sure all methods in this module are invoked on the main/UI thread.
*/
@@ -32,6 +39,14 @@ RCT_EXPORT_MODULE();
return dispatch_get_main_queue();
}
+ (BOOL)requiresMainQueueSetup {
return NO;
}
- (NSArray<NSString *> *)supportedEvents {
return @[ hangUpEvent, setAudioMutedEvent ];
}
/**
* Dispatches an event that occurred on JavaScript to the view's delegate.
*
@@ -87,4 +102,14 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
return methodName;
}
- (void)sendHangUp {
[self sendEventWithName:hangUpEvent body:nil];
}
- (void)sendSetAudioMuted: (BOOL)muted {
NSDictionary *data = @{ @"muted": [NSNumber numberWithBool:muted]};
[self sendEventWithName:setAudioMutedEvent body:data];
}
@end

View File

@@ -37,4 +37,8 @@
*/
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
@end

View File

@@ -17,6 +17,7 @@
#include <mach/mach_time.h>
#import "ExternalAPI.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
@@ -49,7 +50,6 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
* identifiers within the process).
*/
static NSMapTable<NSString *, JitsiMeetView *> *views;
/**
* This gets called automagically when the program starts.
*/
@@ -115,6 +115,16 @@ static void initializeViewsMap() {
[self setProps:@{}];
}
- (void)hangUp {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
[[bridge moduleForClass:ExternalAPI.class] sendHangUp];
}
- (void)setAudioMuted:(BOOL)muted {
RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge];
[[bridge moduleForClass:ExternalAPI.class] sendSetAudioMuted:muted];
}
#pragma mark Private methods
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-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.
@@ -55,4 +55,24 @@
*/
- (void)enterPictureInPicture:(NSDictionary *)data;
/**
* Called when a participant has joined the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has joined.
*/
- (void)participantJoined:(NSDictionary *)data;
/**
* Called when a participant has left the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has left.
*/
- (void)participantLeft:(NSDictionary *)data;
/**
* Called when audioMuted state changed.
*
* The `data` dictionary contains a `muted` key with state of the audioMuted for the localParticipant.
*/
- (void)audioMutedChanged:(NSDictionary *)data;
@end

View File

@@ -432,6 +432,15 @@ function initCommands() {
case 'is-sharing-screen':
callback(Boolean(APP.conference.isSharingScreen));
break;
case 'get-content-sharing-participants': {
const tracks = getState()['features/base/tracks'];
const sharingParticipantIds = tracks.filter(tr => tr.videoType === 'desktop').map(t => t.participantId);
callback({
sharingParticipantIds
});
break;
}
default:
return false;
}
@@ -583,8 +592,8 @@ class API {
* @returns {void}
*/
notifyReceivedChatMessage(
{ body, id, nick, ts }: {
body: *, id: string, nick: string, ts: *
{ body, id, nick, privateMessage, ts }: {
body: *, id: string, nick: string, privateMessage: boolean, ts: *
} = {}) {
if (APP.conference.isLocalId(id)) {
return;
@@ -595,6 +604,7 @@ class API {
from: id,
message: body,
nick,
privateMessage,
stamp: ts
});
}
@@ -675,6 +685,19 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that the list of sharing participants changed.
*
* @param {Object} data - The event data.
* @returns {void}
*/
notifySharingParticipantsChanged(data: Object) {
this._sendEvent({
name: 'content-sharing-participants-changed',
data
});
}
/**
* Notify external application (if API is enabled) that the device list has
* changed.

View File

@@ -64,6 +64,7 @@ const events = {
'audio-availability-changed': 'audioAvailabilityChanged',
'audio-mute-status-changed': 'audioMuteStatusChanged',
'camera-error': 'cameraError',
'content-sharing-participants-changed': 'contentSharingParticipantsChanged',
'device-list-changed': 'deviceListChanged',
'display-name-change': 'displayNameChange',
'email-change': 'emailChange',
@@ -725,6 +726,17 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
return getAvailableDevices(this._transport);
}
/**
* Gets a list of the currently sharing participant id's.
*
* @returns {Promise} - Resolves with the list of participant id's currently sharing.
*/
getContentSharingParticipants() {
return this._transport.sendRequest({
name: 'get-content-sharing-participants'
});
}
/**
* Returns Promise that resolves with current selected devices.
*

View File

@@ -7,7 +7,6 @@ import EventEmitter from 'events';
import Logger from 'jitsi-meet-logger';
import { isMobileBrowser } from '../../react/features/base/environment/utils';
import { getLocalParticipant } from '../../react/features/base/participants';
import { toggleChat } from '../../react/features/chat';
import { setDocumentUrl } from '../../react/features/etherpad';
import { setFilmstripVisible } from '../../react/features/filmstrip';
@@ -91,29 +90,11 @@ UI.notifyReservationError = function(code, msg) {
});
};
/**
* Change nickname for the user.
* @param {string} id user id
* @param {string} displayName new nickname
*/
UI.changeDisplayName = function(id, displayName) {
VideoLayout.onDisplayNameChanged(id, displayName);
};
/**
* Initialize conference UI.
*/
UI.initConference = function() {
const { getState } = APP.store;
const { id, name } = getLocalParticipant(getState);
UI.showToolbar();
const displayName = config.displayJids ? id : name;
if (displayName) {
UI.changeDisplayName('localVideoContainer', displayName);
}
};
/**
@@ -238,19 +219,12 @@ UI.getSharedDocumentManager = () => etherpadManager;
* @param {JitsiParticipant} user
*/
UI.addUser = function(user) {
const id = user.getId();
const displayName = user.getDisplayName();
const status = user.getStatus();
if (status) {
// FIXME: move updateUserStatus in participantPresenceChanged action
UI.updateUserStatus(user, status);
}
// set initial display name
if (displayName) {
UI.changeDisplayName(id, displayName);
}
};
/**
@@ -442,14 +416,6 @@ UI.handleLastNEndpoints = function(leavingIds, enteringIds) {
*/
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
/**
* Hide connection quality statistics from UI.
*/
UI.hideStats = function() {
VideoLayout.hideStats();
};
UI.notifyTokenAuthFailed = function() {
messageHandler.showError({
descriptionKey: 'dialog.tokenAuthFailed',

View File

@@ -1,10 +1,15 @@
/* global $ */
/* global $, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { Thumbnail } from '../../../react/features/filmstrip';
import SmallVideo from '../videolayout/SmallVideo';
const logger = Logger.getLogger(__filename);
/* eslint-enable no-unused-vars */
/**
*
@@ -13,28 +18,21 @@ export default class SharedVideoThumb extends SmallVideo {
/**
*
* @param {*} participant
* @param {*} videoType
* @param {*} VideoLayout
*/
constructor(participant, videoType, VideoLayout) {
super(VideoLayout);
constructor(participant) {
super();
this.id = participant.id;
this.isLocal = false;
this.url = participant.id;
this.videoSpanId = 'sharedVideoContainer';
this.container = this.createContainer(this.videoSpanId);
this.$container = $(this.container);
this.renderThumbnail();
this._setThumbnailSize();
this.bindHoverHandler();
this.updateDisplayName();
this.container.onclick = this._onContainerClick;
}
/**
*
*/
initializeAvatar() {} // eslint-disable-line no-empty-function
/**
*
* @param {*} spanId
@@ -45,18 +43,6 @@ export default class SharedVideoThumb extends SmallVideo {
container.id = spanId;
container.className = 'videocontainer';
// add the avatar
const avatar = document.createElement('img');
avatar.className = 'sharedVideoAvatar';
avatar.src = `https://img.youtube.com/vi/${this.url}/0.jpg`;
container.appendChild(avatar);
const displayNameContainer = document.createElement('div');
displayNameContainer.className = 'displayNameContainer';
container.appendChild(displayNameContainer);
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -68,21 +54,14 @@ export default class SharedVideoThumb extends SmallVideo {
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
}

View File

@@ -37,7 +37,6 @@ const Filmstrip = {
*/
resizeThumbnailsForTileView(width, height, forceUpdate = false) {
const thumbs = this._getThumbs(!forceUpdate);
const avatarSize = height / 2;
if (thumbs.localThumb) {
thumbs.localThumb.css({
@@ -58,11 +57,6 @@ const Filmstrip = {
width: `${width}px`
});
}
$('.avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
},
/**
@@ -77,7 +71,6 @@ const Filmstrip = {
if (thumbs.localThumb) {
const { height, width } = local;
const avatarSize = height / 2;
thumbs.localThumb.css({
height: `${height}px`,
@@ -85,15 +78,10 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#localVideoContainer > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
if (thumbs.remoteThumbs) {
const { height, width } = remote;
const avatarSize = height / 2;
thumbs.remoteThumbs.css({
height: `${height}px`,
@@ -101,10 +89,6 @@ const Filmstrip = {
'min-width': `${width}px`,
width: `${width}px`
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
},
@@ -126,10 +110,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#localVideoContainer > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
if (thumbs.remoteThumbs) {
@@ -142,10 +122,6 @@ const Filmstrip = {
'min-width': '',
'min-height': ''
});
$('#filmstripRemoteVideosContainer > span > .avatar-container').css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
}
},

View File

@@ -1,35 +1,34 @@
/* global $, config, interfaceConfig, APP */
/* global $, config, APP */
import Logger from 'jitsi-meet-logger';
/* eslint-disable no-unused-vars */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { I18nextProvider } from 'react-i18next';
import { Provider } from 'react-redux';
import { i18next } from '../../../react/features/base/i18n';
import { JitsiTrackEvents } from '../../../react/features/base/lib-jitsi-meet';
import { VideoTrack } from '../../../react/features/base/media';
import { updateSettings } from '../../../react/features/base/settings';
import { getLocalVideoTrack } from '../../../react/features/base/tracks';
import Thumbnail from '../../../react/features/filmstrip/components/web/Thumbnail';
import { shouldDisplayTileView } from '../../../react/features/video-layout';
/* eslint-enable no-unused-vars */
import UIEvents from '../../../service/UI/UIEvents';
import SmallVideo from './SmallVideo';
const logger = Logger.getLogger(__filename);
/**
*
*/
export default class LocalVideo extends SmallVideo {
/**
*
* @param {*} VideoLayout
* @param {*} emitter
* @param {*} streamEndedCallback
*/
constructor(VideoLayout, emitter, streamEndedCallback) {
super(VideoLayout);
constructor(emitter, streamEndedCallback) {
super();
this.videoSpanId = 'localVideoContainer';
this.streamEndedCallback = streamEndedCallback;
this.container = this.createContainer();
@@ -37,6 +36,7 @@ export default class LocalVideo extends SmallVideo {
this.isLocal = true;
this._setThumbnailSize();
this.updateDOMLocation();
this.renderThumbnail();
this.localVideoId = null;
this.bindHoverHandler();
@@ -44,7 +44,6 @@ export default class LocalVideo extends SmallVideo {
this._buildContextMenu();
}
this.emitter = emitter;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left top' : 'top center';
Object.defineProperty(this, 'id', {
get() {
@@ -53,18 +52,6 @@ export default class LocalVideo extends SmallVideo {
});
this.initBrowserSpecificProperties();
// Set default display name.
this.updateDisplayName();
// Initialize the avatar display with an avatar url selected from the redux
// state. Redux stores the local user with a hardcoded participant id of
// 'local' if no id has been assigned yet.
this.initializeAvatar();
this.addAudioLevelIndicator();
this.updateIndicators();
this.updateStatusBar();
this.container.onclick = this._onContainerClick;
}
@@ -77,38 +64,19 @@ export default class LocalVideo extends SmallVideo {
containerSpan.classList.add('videocontainer');
containerSpan.id = this.videoSpanId;
containerSpan.innerHTML = `
<div class = 'videocontainer__background'></div>
<span id = 'localVideoWrapper'></span>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>`;
return containerSpan;
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
* Renders the thumbnail.
*/
updateDisplayName() {
if (!this.container) {
logger.warn(
`Unable to set displayName - ${this.videoSpanId
} does not exist`);
return;
}
this._renderDisplayName({
allowEditing: !config.disableProfile,
displayNameSuffix: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
elementID: 'localDisplayName',
participantID: this.id
});
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>, this.container);
}
/**
@@ -116,9 +84,7 @@ export default class LocalVideo extends SmallVideo {
* @param {*} stream
*/
changeVideo(stream) {
this.videoStream = stream;
this.localVideoId = `localVideo_${stream.getId()}`;
this._updateVideoElement();
// eslint-disable-next-line eqeqeq
const isVideo = stream.videoType != 'desktop';
@@ -128,17 +94,6 @@ export default class LocalVideo extends SmallVideo {
this.setFlipX(isVideo ? settings.localFlipX : false);
const endedHandler = () => {
const localVideoContainer
= document.getElementById('localVideoWrapper');
// Only remove if there is no video and not a transition state.
// Previous non-react logic created a new video element with each track
// removal whereas react reuses the video component so it could be the
// stream ended but a new one is being used.
if (localVideoContainer && this.videoStream.isEnded()) {
ReactDOM.unmountComponentAtNode(localVideoContainer);
}
this._notifyOfStreamEnded();
stream.off(JitsiTrackEvents.LOCAL_TRACK_STOPPED, endedHandler);
};
@@ -254,35 +209,5 @@ export default class LocalVideo extends SmallVideo {
: document.getElementById('filmstripLocalVideoThumbnail');
appendTarget && appendTarget.appendChild(this.container);
this._updateVideoElement();
}
/**
* Renders the React Element for displaying video in {@code LocalVideo}.
*
*/
_updateVideoElement() {
const localVideoContainer = document.getElementById('localVideoWrapper');
const videoTrack
= getLocalVideoTrack(APP.store.getState()['features/base/tracks']);
ReactDOM.render(
<Provider store = { APP.store }>
<VideoTrack
id = 'localVideo_container'
videoTrack = { videoTrack } />
</Provider>,
localVideoContainer
);
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay. Also, set the playsinline attribute on the
// video element so that local video doesn't open in full screen by default
// in Safari browser on iOS.
const video = this.container.querySelector('video');
video && video.setAttribute('playsinline', 'true');
video && !config.testing?.noAutoPlayVideo && video.play();
}
}

View File

@@ -1,4 +1,4 @@
/* global $, APP, interfaceConfig */
/* global $, APP, config */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -15,6 +15,7 @@ import {
import { getParticipantById } from '../../../react/features/base/participants';
import { isTestModeEnabled } from '../../../react/features/base/testing';
import { updateLastTrackVideoMediaEvent } from '../../../react/features/base/tracks';
import { Thumbnail, isVideoPlayable } from '../../../react/features/filmstrip';
import { PresenceLabel } from '../../../react/features/presence-status';
import { stopController, requestRemoteControl } from '../../../react/features/remote-control';
import { RemoteVideoMenuTriggerButton } from '../../../react/features/remote-video-menu';
@@ -44,16 +45,6 @@ function createContainer(spanId) {
container.id = spanId;
container.className = 'videocontainer';
container.innerHTML = `
<div class = 'videocontainer__background'></div>
<div class = 'videocontainer__toptoolbar'></div>
<div class = 'videocontainer__toolbar'></div>
<div class = 'videocontainer__hoverOverlay'></div>
<div class = 'displayNameContainer'></div>
<div class = 'avatar-container'></div>
<div class ='presence-label-container'></div>
<span class = 'remotevideomenu'></span>`;
const remoteVideosContainer
= document.getElementById('filmstripRemoteVideosContainer');
const localVideoContainer
@@ -72,21 +63,16 @@ export default class RemoteVideo extends SmallVideo {
* Creates new instance of the <tt>RemoteVideo</tt>.
* @param user {JitsiParticipant} the user for whom remote video instance will
* be created.
* @param {VideoLayout} VideoLayout the video layout instance.
* @constructor
*/
constructor(user, VideoLayout) {
super(VideoLayout);
constructor(user) {
super();
this.user = user;
this.id = user.getId();
this.videoSpanId = `participant_${this.id}`;
this._audioStreamElement = null;
this.statsPopoverLocation = interfaceConfig.VERTICAL_FILMSTRIP ? 'left bottom' : 'top center';
this.addRemoteVideoContainer();
this.updateIndicators();
this.updateDisplayName();
this.bindHoverHandler();
this.flipX = false;
this.isLocal = false;
@@ -100,11 +86,6 @@ export default class RemoteVideo extends SmallVideo {
*/
this._canPlayEventReceived = false;
// Bind event handlers so they are only bound once for every instance.
// TODO The event handlers should be turned into actions so changes can be
// handled through reducers and middleware.
this._setAudioVolume = this._setAudioVolume.bind(this);
this.container.onclick = this._onContainerClick;
}
@@ -114,76 +95,23 @@ export default class RemoteVideo extends SmallVideo {
addRemoteVideoContainer() {
this.container = createContainer(this.videoSpanId);
this.$container = $(this.container);
this.initializeAvatar();
this.renderThumbnail();
this._setThumbnailSize();
this.initBrowserSpecificProperties();
this.updateRemoteVideoMenu();
this.updateStatusBar();
this.addAudioLevelIndicator();
this.addPresenceLabel();
return this.container;
}
/**
* Generates the popup menu content.
*
* @returns {Element|*} the constructed element, containing popup menu items
* @private
* Renders the thumbnail.
*/
_generatePopupContent() {
const remoteVideoMenuContainer
= this.container.querySelector('.remotevideomenu');
if (!remoteVideoMenuContainer) {
return;
}
const initialVolumeValue = this._audioStreamElement && this._audioStreamElement.volume;
// hide volume when in silent mode
const onVolumeChange
= APP.store.getState()['features/base/config'].startSilent ? undefined : this._setAudioVolume;
renderThumbnail(isHovered = false) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { initialVolumeValue }
onMenuDisplay
= {this._onRemoteVideoMenuDisplay.bind(this)}
onVolumeChange = { onVolumeChange }
participantID = { this.id } />
</AtlasKitThemeProvider>
<Thumbnail participantID = { this.id } isHovered = { isHovered } />
</I18nextProvider>
</Provider>,
remoteVideoMenuContainer);
}
/**
*
*/
_onRemoteVideoMenuDisplay() {
this.updateRemoteVideoMenu();
}
/**
* Change the remote participant's volume level.
*
* @param {int} newVal - The value to set the slider to.
*/
_setAudioVolume(newVal) {
if (this._audioStreamElement) {
this._audioStreamElement.volume = newVal;
}
}
/**
* Updates the remote video menu.
*/
updateRemoteVideoMenu() {
this._generatePopupContent();
</Provider>, this.container);
}
/**
@@ -199,7 +127,7 @@ export default class RemoteVideo extends SmallVideo {
}
const isVideo = stream.isVideoTrack();
const elementID = SmallVideo.getStreamElementID(stream);
const elementID = `remoteVideo_${stream.getId()}`;
const select = $(`#${elementID}`);
select.remove();
@@ -207,11 +135,7 @@ export default class RemoteVideo extends SmallVideo {
this._canPlayEventReceived = false;
}
logger.info(`${isVideo ? 'Video' : 'Audio'} removed ${this.id}`, select);
if (stream === this.videoStream) {
this.videoStream = null;
}
logger.info(`Video removed ${this.id}`, select);
this.updateView();
}
@@ -223,14 +147,7 @@ export default class RemoteVideo extends SmallVideo {
* @override
*/
isVideoPlayable() {
const participant = getParticipantById(APP.store.getState(), this.id);
const { connectionStatus } = participant || {};
return (
super.isVideoPlayable()
&& this._canPlayEventReceived
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE
);
return isVideoPlayable(APP.store.getState(), this.id) && this._canPlayEventReceived;
}
/**
@@ -245,9 +162,8 @@ export default class RemoteVideo extends SmallVideo {
* Removes RemoteVideo from the page.
*/
remove() {
ReactDOM.unmountComponentAtNode(this.container);
super.remove();
this.removePresenceLabel();
this.removeRemoteVideoMenu();
}
/**
@@ -295,19 +211,16 @@ export default class RemoteVideo extends SmallVideo {
const isVideo = stream.isVideoTrack();
if (isVideo) {
this.videoStream = stream;
} else {
this.audioStream = stream;
}
if (!stream.getOriginalStream()) {
logger.debug('Remote video stream has no original stream');
return;
}
let streamElement = SmallVideo.createStreamElement(stream);
let streamElement = document.createElement('video');
streamElement.autoplay = !config.testing?.noAutoPlayVideo;
streamElement.id = `remoteVideo_${stream.getId()}`;
// Put new stream element always in front
streamElement = UIUtils.prependChild(this.container, streamElement);
@@ -315,14 +228,7 @@ export default class RemoteVideo extends SmallVideo {
this.waitForPlayback(streamElement, stream);
stream.attach(streamElement);
if (!isVideo) {
this._audioStreamElement = streamElement;
// If the remote video menu was created before the audio stream was
// attached we need to update the menu in order to show the volume
// slider.
this.updateRemoteVideoMenu();
} else if (isTestModeEnabled(APP.store.getState())) {
if (isVideo && isTestModeEnabled(APP.store.getState())) {
const cb = name => APP.store.dispatch(updateLastTrackVideoMediaEvent(stream, name));
@@ -331,72 +237,4 @@ export default class RemoteVideo extends SmallVideo {
});
}
}
/**
* Triggers re-rendering of the display name using current instance state.
*
* @returns {void}
*/
updateDisplayName() {
if (!this.container) {
logger.warn(`Unable to set displayName - ${this.videoSpanId} does not exist`);
return;
}
this._renderDisplayName({
elementID: `${this.videoSpanId}_name`,
participantID: this.id
});
}
/**
* Removes remote video menu element from video element identified by
* given <tt>videoElementId</tt>.
*
* @param videoElementId the id of local or remote video element.
*/
removeRemoteVideoMenu() {
const menuSpan = this.$container.find('.remotevideomenu');
if (menuSpan.length) {
ReactDOM.unmountComponentAtNode(menuSpan.get(0));
menuSpan.remove();
}
}
/**
* Mounts the {@code PresenceLabel} for displaying the participant's current
* presence status.
*
* @return {void}
*/
addPresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<PresenceLabel
participantID = { this.id }
className = 'presence-label' />
</I18nextProvider>
</Provider>,
presenceLabelContainer);
}
}
/**
* Unmounts the {@code PresenceLabel} component.
*
* @return {void}
*/
removePresenceLabel() {
const presenceLabelContainer = this.container.querySelector('.presence-label-container');
if (presenceLabelContainer) {
ReactDOM.unmountComponentAtNode(presenceLabelContainer);
}
}
}

View File

@@ -1,4 +1,4 @@
/* global $, APP, config, interfaceConfig */
/* global $, APP, interfaceConfig */
/* eslint-disable no-unused-vars */
import { AtlasKitThemeProvider } from '@atlaskit/theme';
@@ -21,6 +21,7 @@ import {
pinParticipant
} from '../../../react/features/base/participants';
import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
isLocalTrackMuted,
isRemoteTrackMuted
@@ -30,7 +31,8 @@ import { DisplayName } from '../../../react/features/display-name';
import {
DominantSpeakerIndicator,
RaisedHandIndicator,
StatusIndicators
StatusIndicators,
isVideoPlayable
} from '../../../react/features/filmstrip';
import {
LAYOUTS,
@@ -89,37 +91,10 @@ export default class SmallVideo {
/**
* Constructor.
*/
constructor(VideoLayout) {
this.videoStream = null;
this.audioStream = null;
this.VideoLayout = VideoLayout;
constructor() {
this.videoIsHovered = false;
this.videoType = undefined;
/**
* Whether or not the connection indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showConnectionIndicator = !interfaceConfig.CONNECTION_INDICATOR_DISABLED;
/**
* Whether or not the dominant speaker indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showDominantSpeaker = false;
/**
* Whether or not the raised hand indicator should be displayed.
*
* @private
* @type {boolean}
*/
this._showRaisedHand = false;
// Bind event handlers so they are only bound once for every instance.
this.updateView = this.updateView.bind(this);
@@ -145,33 +120,6 @@ export default class SmallVideo {
return this.$container.is(':visible');
}
/**
* Creates an audio or video element for a particular MediaStream.
*/
static createStreamElement(stream) {
const isVideo = stream.isVideoTrack();
const element = isVideo ? document.createElement('video') : document.createElement('audio');
if (isVideo) {
element.setAttribute('muted', 'true');
element.setAttribute('playsInline', 'true'); /* for Safari on iOS to work */
} else if (config.startSilent) {
element.muted = true;
}
element.autoplay = !config.testing?.noAutoPlayVideo;
element.id = SmallVideo.getStreamElementID(stream);
return element;
}
/**
* Returns the element id for a particular MediaStream.
*/
static getStreamElementID(stream) {
return (stream.isVideoTrack() ? 'remoteVideo_' : 'remoteAudio_') + stream.getId();
}
/**
* Configures hoverIn/hoverOut handlers. Depends on connection indicator.
*/
@@ -180,103 +128,22 @@ export default class SmallVideo {
this.$container.hover(
() => {
this.videoIsHovered = true;
this.renderThumbnail(true);
this.updateView();
this.updateIndicators();
},
() => {
this.videoIsHovered = false;
this.renderThumbnail(false);
this.updateView();
this.updateIndicators();
}
);
}
/**
* Unmounts the ConnectionIndicator component.
* @returns {void}
*/
removeConnectionIndicator() {
this._showConnectionIndicator = false;
this.updateIndicators();
}
/**
* Create or updates the ReactElement for displaying status indicators about
* audio mute, video mute, and moderator status.
*
* @returns {void}
* Renders the thumbnail.
*/
updateStatusBar() {
const statusBarContainer = this.container.querySelector('.videocontainer__toolbar');
if (!statusBarContainer) {
return;
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<StatusIndicators
participantID = { this.id } />
</I18nextProvider>
</Provider>,
statusBarContainer);
}
/**
* Adds the element indicating the audio level of the participant.
*
* @returns {void}
*/
addAudioLevelIndicator() {
let audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
return;
}
audioLevelContainer = document.createElement('span');
audioLevelContainer.className = 'audioindicator-container';
this.container.appendChild(audioLevelContainer);
this.updateAudioLevelIndicator();
}
/**
* Removes the element indicating the audio level of the participant.
*
* @returns {void}
*/
removeAudioLevelIndicator() {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.unmountComponentAtNode(audioLevelContainer);
}
}
/**
* Updates the audio level for this small video.
*
* @param lvl the new audio level to set
* @returns {void}
*/
updateAudioLevelIndicator(lvl = 0) {
const audioLevelContainer = this._getAudioLevelContainer();
if (audioLevelContainer) {
ReactDOM.render(<AudioLevelIndicator audioLevel = { lvl }/>, audioLevelContainer);
}
}
/**
* Queries the component's DOM for the element that should be the parent to the
* AudioLevelIndicator.
*
* @returns {HTMLElement} The DOM element that holds the AudioLevelIndicator.
*/
_getAudioLevelContainer() {
return this.container.querySelector('.audioindicator-container');
renderThumbnail() {
// Should be implemented by in subclasses.
}
/**
@@ -293,62 +160,6 @@ export default class SmallVideo {
return $($(this.container).find('video')[0]);
}
/**
* Selects the HTML image element which displays user's avatar.
*
* @return {jQuery|HTMLElement} a jQuery selector pointing to the HTML image
* element which displays the user's avatar.
*/
$avatar() {
return this.$container.find('.avatar-container');
}
/**
* Returns the display name element, which appears on the video thumbnail.
*
* @return {jQuery} a jQuery selector pointing to the display name element of
* the video thumbnail
*/
$displayName() {
return this.$container.find('.displayNameContainer');
}
/**
* Creates or updates the participant's display name that is shown over the
* video preview.
*
* @param {Object} props - The React {@code Component} props to pass into the
* {@code DisplayName} component.
* @returns {void}
*/
_renderDisplayName(props) {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<DisplayName { ...props } />
</I18nextProvider>
</Provider>,
displayNameContainer);
}
}
/**
* Removes the component responsible for showing the participant's display name,
* if its container is present.
*
* @returns {void}
*/
removeDisplayName() {
const displayNameContainer = this.container.querySelector('.displayNameContainer');
if (displayNameContainer) {
ReactDOM.unmountComponentAtNode(displayNameContainer);
}
}
/**
* Enables / disables the css responsible for focusing/pinning a video
* thumbnail.
@@ -392,18 +203,7 @@ export default class SmallVideo {
* or <tt>false</tt> otherwise.
*/
isVideoPlayable() {
const state = APP.store.getState();
const tracks = state['features/base/tracks'];
const participant = this.id ? getParticipantById(state, this.id) : getLocalParticipant(state);
let isVideoMuted = true;
if (participant?.local) {
isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, this.id);
}
return this.videoStream && !isVideoMuted && !APP.conference.isAudioOnly();
return isVideoPlayable(APP.store.getState(), this.id);
}
/**
@@ -436,13 +236,15 @@ export default class SmallVideo {
let isScreenSharing = false;
let connectionStatus;
const state = APP.store.getState();
const participant = getParticipantById(state, this.id);
const id = this.id;
const participant = getParticipantById(state, id);
const isLocal = participant?.local ?? true;
const tracks = state['features/base/tracks'];
const videoTrack
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
if (typeof participant !== 'undefined' && !participant.isFakeParticipant && !participant.local) {
const tracks = state['features/base/tracks'];
const track = getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, this.id);
isScreenSharing = typeof track !== 'undefined' && track.videoType === 'desktop';
isScreenSharing = videoTrack?.videoType === 'desktop';
connectionStatus = participant.connectionStatus;
}
@@ -455,9 +257,9 @@ export default class SmallVideo {
hasVideo: Boolean(this.selectVideoElement().length),
connectionStatus,
canPlayEventReceived: this._canPlayEventReceived,
videoStream: Boolean(this.videoStream),
videoStream: Boolean(videoTrack),
isScreenSharing,
videoStreamMuted: this.videoStream ? this.videoStream.isMuted() : 'no stream'
videoStreamMuted: videoTrack ? videoTrack.muted : 'no stream'
};
}
@@ -527,43 +329,6 @@ export default class SmallVideo {
}
}
/**
* Updates the react component displaying the avatar with the passed in avatar
* url.
*
* @returns {void}
*/
initializeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
// Maybe add a special case for local participant, as on init of
// LocalVideo.js the id is set to "local" but will get updated later.
ReactDOM.render(
<Provider store = { APP.store }>
<AvatarDisplay
className = 'userAvatar'
participantId = { this.id } />
</Provider>,
thumbnail
);
}
}
/**
* Unmounts any attached react components (particular the avatar image) from
* the avatar container.
*
* @returns {void}
*/
removeAvatar() {
const thumbnail = this.$avatar().get(0);
if (thumbnail) {
ReactDOM.unmountComponentAtNode(thumbnail);
}
}
/**
* Shows or hides the dominant speaker indicator.
* @param show whether to show or hide.
@@ -580,30 +345,8 @@ export default class SmallVideo {
return;
}
if (this._showDominantSpeaker === show) {
return;
}
this._showDominantSpeaker = show;
this.$container.toggleClass('active-speaker', this._showDominantSpeaker);
this.updateIndicators();
this.updateView();
}
/**
* Shows or hides the raised hand indicator.
* @param show whether to show or hide.
*/
showRaisedHandIndicator(show) {
if (!this.container) {
logger.warn(`Unable to raised hand indication - ${
this.videoSpanId} does not exist`);
return;
}
this._showRaisedHand = show;
this.updateIndicators();
this.$container.toggleClass('active-speaker', show);
}
/**
@@ -634,19 +377,7 @@ export default class SmallVideo {
*/
remove() {
logger.log('Remove thumbnail', this.id);
this.removeAudioLevelIndicator();
const toolbarContainer
= this.container.querySelector('.videocontainer__toolbar');
if (toolbarContainer) {
ReactDOM.unmountComponentAtNode(toolbarContainer);
}
this.removeConnectionIndicator();
this.removeDisplayName();
this.removeAvatar();
this._unmountIndicators();
this._unmountThumbnail();
// Remove whole container
if (this.container.parentNode) {
@@ -661,76 +392,9 @@ export default class SmallVideo {
* @returns {void}
*/
rerender() {
this.updateIndicators();
this.updateStatusBar();
this.updateView();
}
/**
* Updates the React element responsible for showing connection status, dominant
* speaker, and raised hand icons. Uses instance variables to get the necessary
* state to display. Will create the React element if not already created.
*
* @private
* @returns {void}
*/
updateIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (!indicatorToolbar) {
return;
}
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
const iconSize = NORMAL;
const showConnectionIndicator = this.videoIsHovered || !interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED;
const state = APP.store.getState();
const currentLayout = getCurrentLayout(state);
const participantCount = getParticipantCount(state);
let statsPopoverPosition, tooltipPosition;
if (currentLayout === LAYOUTS.TILE_VIEW) {
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
} else if (currentLayout === LAYOUTS.VERTICAL_FILMSTRIP_VIEW) {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'left';
} else {
statsPopoverPosition = this.statsPopoverLocation;
tooltipPosition = 'top';
}
ReactDOM.render(
<Provider store = { APP.store }>
<I18nextProvider i18n = { i18next }>
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ this._showConnectionIndicator
? <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
iconSize = { iconSize }
isLocalVideo = { this.isLocal }
enableStatsDisplay = { true }
participantId = { this.id }
statsPopoverPosition = { statsPopoverPosition } />
: null }
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { this.id }
tooltipPosition = { tooltipPosition } />
{ this._showDominantSpeaker && participantCount > 2
? <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
: null }
</AtlasKitThemeProvider>
</div>
</I18nextProvider>
</Provider>,
indicatorToolbar
);
}
/**
* Callback invoked when the thumbnail is clicked and potentially trigger
* pinning of the participant.
@@ -788,18 +452,10 @@ export default class SmallVideo {
}
/**
* Removes the React element responsible for showing connection status, dominant
* speaker, and raised hand icons.
*
* @private
* @returns {void}
* Unmounts the thumbnail.
*/
_unmountIndicators() {
const indicatorToolbar = this.container.querySelector('.videocontainer__toptoolbar');
if (indicatorToolbar) {
ReactDOM.unmountComponentAtNode(indicatorToolbar);
}
_unmountThumbnail() {
ReactDOM.unmountComponentAtNode(this.container);
}
/**
@@ -813,10 +469,6 @@ export default class SmallVideo {
switch (layout) {
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
this.$container.css('padding-top', `${heightToWidthPercent}%`);
this.$avatar().css({
height: '50%',
width: `${heightToWidthPercent / 2}%`
});
break;
}
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
@@ -826,7 +478,6 @@ export default class SmallVideo {
if (typeof size !== 'undefined') {
const { height, width } = size;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -834,10 +485,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}
@@ -847,7 +494,6 @@ export default class SmallVideo {
if (typeof thumbnailSize !== 'undefined') {
const { height, width } = thumbnailSize;
const avatarSize = height / 2;
this.$container.css({
height: `${height}px`,
@@ -855,10 +501,6 @@ export default class SmallVideo {
'min-width': `${width}px`,
width: `${width}px`
});
this.$avatar().css({
height: `${avatarSize}px`,
width: `${avatarSize}px`
});
}
break;
}

View File

@@ -72,7 +72,6 @@ const VideoLayout = {
eventEmitter = emitter;
localVideoThumbnail = new LocalVideo(
VideoLayout,
emitter,
this._updateLargeVideoIfDisplayed.bind(this));
@@ -116,12 +115,6 @@ const VideoLayout = {
* @param lvl the new audio level to update to
*/
setAudioLevel(id, lvl) {
const smallVideo = this.getSmallVideo(id);
if (smallVideo) {
smallVideo.updateAudioLevelIndicator(lvl);
}
if (largeVideo && id === largeVideo.id) {
largeVideo.updateLargeVideoAudioLevel(lvl);
}
@@ -137,19 +130,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(localId);
},
/**
* Get's the localID of the conference and set it to the local video
* (small one). This needs to be called as early as possible, when muc is
* actually joined. Otherwise events can come with information like email
* and setting them assume the id is already set.
*/
mucJoined() {
// 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.
localVideoThumbnail.updateIndicators();
},
/**
* Shows/hides local video.
* @param {boolean} true to make the local video visible, false - otherwise
@@ -172,11 +152,8 @@ const VideoLayout = {
remoteVideo.addRemoteStreamElement(stream);
// Make sure track's muted state is reflected
if (stream.getType() !== 'audio') {
this.onVideoMute(id);
remoteVideo.updateView();
}
this.onVideoMute(id);
remoteVideo.updateView();
},
onRemoteStreamRemoved(stream) {
@@ -184,13 +161,12 @@ const VideoLayout = {
const remoteVideo = remoteVideos[id];
// Remote stream may be removed after participant left the conference.
if (remoteVideo) {
remoteVideo.removeRemoteStreamElement(stream);
remoteVideo.updateView();
}
this.updateMutedForNoTracks(id, stream.getType());
this.updateVideoMutedForNoTracks(id);
},
/**
@@ -199,19 +175,12 @@ const VideoLayout = {
*
* If participant has no tracks will make the UI display muted status.
* @param {string} participantId
* @param {string} mediaType 'audio' or 'video'
*/
updateMutedForNoTracks(participantId, mediaType) {
updateVideoMutedForNoTracks(participantId) {
const participant = APP.conference.getParticipantById(participantId);
if (participant && !participant.getTracksByMediaType(mediaType).length) {
if (mediaType === 'audio') {
APP.UI.setAudioMuted(participantId, true);
} else if (mediaType === 'video') {
APP.UI.setVideoMuted(participantId);
} else {
logger.error(`Unsupported media type: ${mediaType}`);
}
if (participant && !participant.getTracksByMediaType('video').length) {
APP.UI.setVideoMuted(participantId);
}
},
@@ -279,10 +248,7 @@ const VideoLayout = {
if (!participant || participant.local) {
return;
} else if (participant.isFakeParticipant) {
const sharedVideoThumb = new SharedVideoThumb(
participant,
SHARED_VIDEO_CONTAINER_TYPE,
VideoLayout);
const sharedVideoThumb = new SharedVideoThumb(participant);
this.addRemoteVideoContainer(participant.id, sharedVideoThumb);
@@ -291,12 +257,10 @@ const VideoLayout = {
const id = participant.id;
const jitsiParticipant = APP.conference.getParticipantById(id);
const remoteVideo = new RemoteVideo(jitsiParticipant, VideoLayout);
const remoteVideo = new RemoteVideo(jitsiParticipant);
this.addRemoteVideoContainer(id, remoteVideo);
this.updateMutedForNoTracks(id, 'audio');
this.updateMutedForNoTracks(id, 'video');
this.updateVideoMutedForNoTracks(id);
},
/**
@@ -331,22 +295,6 @@ const VideoLayout = {
this._updateLargeVideoIfDisplayed(id, true);
},
/**
* Display name changed.
*/
onDisplayNameChanged(id) {
if (id === 'localVideoContainer'
|| APP.conference.isLocalId(id)) {
localVideoThumbnail.updateDisplayName();
} else {
const remoteVideo = remoteVideos[id];
if (remoteVideo) {
remoteVideo.updateDisplayName();
}
}
},
/**
* On dominant speaker changed event.
*
@@ -413,20 +361,6 @@ const VideoLayout = {
}
},
/**
* Hides all the indicators
*/
hideStats() {
for (const video in remoteVideos) { // eslint-disable-line guard-for-in
const remoteVideo = remoteVideos[video];
if (remoteVideo) {
remoteVideo.removeConnectionIndicator();
}
}
localVideoThumbnail.removeConnectionIndicator();
},
removeParticipantContainer(id) {
// Unlock large video
if (this.getPinnedId() === id) {
@@ -477,15 +411,6 @@ const VideoLayout = {
},
changeUserAvatar(id, avatarUrl) {
const smallVideo = VideoLayout.getSmallVideo(id);
if (smallVideo) {
smallVideo.initializeAvatar();
} else {
logger.warn(
`Missed avatar update - no small video yet for ${id}`
);
}
if (this.isCurrentlyOnLarge(id)) {
largeVideo.updateAvatar(avatarUrl);
}

16
package-lock.json generated
View File

@@ -7017,9 +7017,9 @@
}
},
"cross-os": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/cross-os/-/cross-os-1.3.0.tgz",
"integrity": "sha512-9kViqCcAwlPLTeSDPlyC2FdMQ5UVPtGZUnGV8vYDcBA3olJ/hDR7H6IfrNJft2DlKONleHf8CMhD+7Uv2tBnEw=="
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/cross-os/-/cross-os-1.4.0.tgz",
"integrity": "sha512-nuARAXqbsizhsqo3qo1bpYW2S6ohxJICjo/5Q/mq5xQ9dn1uWoRQ855DZrBAWgVfg7liPLdatnY2KMZwZYuxsQ=="
},
"cross-spawn": {
"version": "6.0.5",
@@ -10816,8 +10816,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#87c6e374755718fdb0804c2c798ea4bc832f4fca",
"from": "github:jitsi/lib-jitsi-meet#87c6e374755718fdb0804c2c798ea4bc832f4fca",
"version": "github:jitsi/lib-jitsi-meet#cb484cf48ccd3d56cf9bb2e9d452bd7aa644ac8a",
"from": "github:jitsi/lib-jitsi-meet#cb484cf48ccd3d56cf9bb2e9d452bd7aa644ac8a",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@@ -14329,9 +14329,9 @@
"integrity": "sha512-iqdJ1KpZbR4XGahgVmaeibB7kDhyMT7wrylINgJaYBY38IAiI0LF32VX1umO4pko6n21YF5I/kSeNQ+OXGqqow=="
},
"react-native-webrtc": {
"version": "1.87.2",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.2.tgz",
"integrity": "sha512-bUMoMvfK17nT8S2w16bpL1uMMyDvDwOmhVjGrP6FDrCS7lAx/w2jVMUtZlNVS6zCJXN92wTkYJ6P3z+nr2hhNg==",
"version": "1.87.3",
"resolved": "https://registry.npmjs.org/react-native-webrtc/-/react-native-webrtc-1.87.3.tgz",
"integrity": "sha512-fWnaEHFCFD7YnPR95aaUqLQ5b4dY4av0qHjmwHXeLHGvGrVeWF1je9PNhet7PDHUIJa4GIYKB/8+co51SXm5dA==",
"requires": {
"base64-js": "^1.1.2",
"cross-os": "^1.3.0",

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#87c6e374755718fdb0804c2c798ea4bc832f4fca",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#cb484cf48ccd3d56cf9bb2e9d452bd7aa644ac8a",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -85,7 +85,7 @@
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",
"react-native-webrtc": "1.87.2",
"react-native-webrtc": "1.87.3",
"react-native-webview": "11.0.2",
"react-native-youtube-iframe": "1.2.3",
"react-redux": "7.1.0",

View File

@@ -39,8 +39,11 @@ class AudioLevelIndicator extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { audioLevel: passedAudioLevel } = this.props;
// First make sure we are sensitive enough.
const audioLevel = Math.min(this.props.audioLevel * 1.2, 1);
const audioLevel = typeof passedAudioLevel === 'number' && !isNaN(passedAudioLevel)
? Math.min(passedAudioLevel * 1.2, 1) : 0;
// Let's now stretch the audio level over the number of dots we have.
const stretchedAudioLevel = AUDIO_LEVEL_DOTS * audioLevel;

View File

@@ -128,9 +128,6 @@ function _conferenceFailed({ dispatch, getState }, next, action) {
titleKey: 'dialog.sessTerminated'
}));
if (typeof APP !== 'undefined') {
APP.UI.hideStats();
}
break;
}
case JitsiConferenceErrors.CONNECTION_ERROR: {

View File

@@ -90,6 +90,7 @@ export default [
'disableRemoteControl',
'disableRemoteMute',
'disableRtx',
'disableShortcuts',
'disableSimulcast',
'disableThirdPartyRequests',
'disableTileView',

View File

@@ -0,0 +1,217 @@
// @flow
import React, { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link AudioTrack}.
*/
type Props = {
/**
* The value of the id attribute of the audio element.
*/
id: string,
/**
* The audio track.
*/
audioTrack: ?Object,
/**
* Used to determine the value of the autoplay attribute of the underlying
* audio element.
*/
autoPlay: boolean,
/**
* Represents muted property of the underlying audio element.
*/
muted: ?Boolean,
/**
* Represents volume property of the underlying audio element.
*/
volume: ?number,
/**
* A function that will be executed when the reference to the underlying audio element changes in order to report
* the initial volume value.
*/
onInitialVolumeSet: Function
};
/**
* The React/Web {@link Component} which is similar to and wraps around {@code HTMLAudioElement}.
*/
export default class AudioTrack extends Component<Props> {
/**
* Reference to the HTML audio element, stored until the file is ready.
*/
_ref: ?HTMLAudioElement;
/**
* Default values for {@code AudioTrack} component's properties.
*
* @static
*/
static defaultProps = {
autoPlay: true,
id: ''
};
/**
* Creates new <code>Audio</code> element instance with given props.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once for every instance.
this._setRef = this._setRef.bind(this);
}
/**
* Attaches the audio track to the audio element and plays it.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._attachTrack(this.props.audioTrack);
if (this._ref) {
const { autoPlay, muted, volume } = this.props;
if (autoPlay) {
// Ensure the audio gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case the audio may not autoplay.
this._ref.play();
}
if (typeof volume === 'number') {
this._ref.volume = volume;
}
if (typeof muted === 'boolean') {
this._ref.muted = muted;
}
}
}
/**
* Remove any existing associations between the current audio track and the
* component's audio element.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._detachTrack(this.props.audioTrack);
}
/**
* This component's updating is blackboxed from React to prevent re-rendering of the audio
* element, as we set all the properties manually.
*
* @inheritdoc
* @returns {boolean} - False is always returned to blackbox this component
* from React.
*/
shouldComponentUpdate(nextProps: Props) {
const currentJitsiTrack = this.props.audioTrack?.jitsiTrack;
const nextJitsiTrack = nextProps.audioTrack?.jitsiTrack;
if (currentJitsiTrack !== nextJitsiTrack) {
this._detachTrack(this.props.audioTrack);
this._attachTrack(nextProps.audioTrack);
}
if (this._ref) {
const currentVolume = this._ref.volume;
const nextVolume = nextProps.volume;
if (typeof nextVolume === 'number' && !isNaN(nextVolume) && currentVolume !== nextVolume) {
this._ref.volume = nextVolume;
}
const currentMuted = this._ref.muted;
const nextMuted = nextProps.muted;
if (typeof nextMuted === 'boolean' && currentMuted !== nextVolume) {
this._ref.muted = nextMuted;
}
}
return false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { autoPlay, id } = this.props;
return (
<audio
autoPlay = { autoPlay }
id = { id }
ref = { this._setRef } />
);
}
/**
* Calls into the passed in track to associate the track with the component's audio element.
*
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_attachTrack(track) {
if (!track || !track.jitsiTrack) {
return;
}
track.jitsiTrack.attach(this._ref);
}
/**
* Removes the association to the component's audio element from the passed
* in redux representation of jitsi audio track.
*
* @param {Object} track - The redux representation of the {@code JitsiLocalTrack}.
* @private
* @returns {void}
*/
_detachTrack(track) {
if (this._ref && track && track.jitsiTrack) {
track.jitsiTrack.detach(this._ref);
}
}
_setRef: (?HTMLAudioElement) => void;
/**
* Sets the reference to the HTML audio element.
*
* @param {HTMLAudioElement} audioElement - The HTML audio element instance.
* @private
* @returns {void}
*/
_setRef(audioElement: ?HTMLAudioElement) {
this._ref = audioElement;
const { onInitialVolumeSet } = this.props;
if (this._ref && onInitialVolumeSet) {
onInitialVolumeSet(this._ref.volume);
}
}
}

View File

@@ -99,6 +99,13 @@ class Video extends Component<Props> {
}
this._attachTrack(this.props.videoTrack);
if (this._videoElement && this.props.autoPlay) {
// Ensure the video gets play() called on it. This may be necessary in the
// case where the local video container was moved and re-attached, in which
// case video does not autoplay.
this._videoElement.play();
}
}
/**

View File

@@ -3,6 +3,8 @@
import InlineDialog from '@atlaskit/inline-dialog';
import React, { Component } from 'react';
import { Drawer, DrawerPortal } from '../../../toolbox/components/web';
/**
* A map of dialog positions, relative to trigger, to css classes used to
* manipulate elements for handling mouse events.
@@ -66,6 +68,11 @@ type Props = {
*/
onPopoverOpen: Function,
/**
* Whether to display the Popover as a drawer.
*/
overflowDrawer: boolean,
/**
* From which side of the dialog trigger the dialog should display. The
* value will be passed to {@code InlineDialog}.
@@ -101,6 +108,11 @@ class Popover extends Component<Props, State> {
id: ''
};
/**
* Reference to the Popover that is meant to open as a drawer.
*/
_drawerContainerRef: Object;
/**
* Initializes a new {@code Popover} instance.
*
@@ -117,6 +129,51 @@ class Popover extends Component<Props, State> {
// Bind event handlers so they are only bound once for every instance.
this._onHideDialog = this._onHideDialog.bind(this);
this._onShowDialog = this._onShowDialog.bind(this);
this._drawerContainerRef = React.createRef();
}
/**
* Sets up an event listener to open a drawer when clicking, rather than entering the
* overflow area.
*
* TODO: This should be done by setting an {@code onClick} handler on the div, but for some
* reason that doesn't seem to work whatsoever.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
if (this._drawerContainerRef && this._drawerContainerRef.current) {
this._drawerContainerRef.current.addEventListener('click', this._onShowDialog);
}
}
/**
* Removes the listener set up in the {@code componentDidMount} method.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
if (this._drawerContainerRef && this._drawerContainerRef.current) {
this._drawerContainerRef.current.removeEventListener('click', this._onShowDialog);
}
}
/**
* Implements React Component's componentDidUpdate.
*
* @inheritdoc
*/
componentDidUpdate(prevProps: Props) {
if (prevProps.overflowDrawer !== this.props.overflowDrawer) {
// Make sure the listeners are set up when resizing the screen past the drawer threshold.
if (this.props.overflowDrawer) {
this.componentDidMount();
} else {
this.componentWillUnmount();
}
}
}
/**
@@ -126,17 +183,37 @@ class Popover extends Component<Props, State> {
* @returns {ReactElement}
*/
render() {
const { children, className, content, id, overflowDrawer, position } = this.props;
if (overflowDrawer) {
return (
<div
className = { className }
id = { id }
ref = { this._drawerContainerRef }>
{ children }
<DrawerPortal>
<Drawer
isOpen = { this.state.showDialog }
onClose = { this._onHideDialog }>
{ content }
</Drawer>
</DrawerPortal>
</div>
);
}
return (
<div
className = { this.props.className }
id = { this.props.id }
className = { className }
id = { id }
onMouseEnter = { this._onShowDialog }
onMouseLeave = { this._onHideDialog }>
<InlineDialog
content = { this._renderContent() }
isOpen = { this.state.showDialog }
position = { this.props.position }>
{ this.props.children }
position = { position }>
{ children }
</InlineDialog>
</div>
);
@@ -160,10 +237,12 @@ class Popover extends Component<Props, State> {
* Displays the {@code InlineDialog} and calls any registered onPopoverOpen
* callbacks.
*
* @param {MouseEvent} event - The mouse event to intercept.
* @private
* @returns {void}
*/
_onShowDialog() {
_onShowDialog(event) {
event.stopPropagation();
if (!this.props.disablePopover) {
this.setState({ showDialog: true });

View File

@@ -4,12 +4,10 @@ import { jitsiLocalStorage } from '@jitsi/js-utils';
import _ from 'lodash';
import { APP_WILL_MOUNT } from '../app/actionTypes';
import { browser } from '../lib-jitsi-meet';
import { PersistenceRegistry, ReducerRegistry } from '../redux';
import { assignIfDefined } from '../util';
import { SETTINGS_UPDATED } from './actionTypes';
import logger from './logger';
/**
* The default/initial redux state of the feature {@code base/settings}.
@@ -75,41 +73,10 @@ ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => {
return state;
});
/**
* Retrieves the legacy profile values regardless of it's being in pre or
* post-flattening format.
*
* FIXME: Let's remove this after a predefined time (e.g. By July 2018) to avoid
* garbage in the source.
*
* @private
* @returns {Object}
*/
function _getLegacyProfile() {
let persistedProfile = jitsiLocalStorage.getItem('features/base/profile');
if (persistedProfile) {
try {
persistedProfile = JSON.parse(persistedProfile);
if (persistedProfile && typeof persistedProfile === 'object') {
const preFlattenedProfile = persistedProfile.profile;
return preFlattenedProfile || persistedProfile;
}
} catch (e) {
logger.warn('Error parsing persisted legacy profile', e);
}
}
return {};
}
/**
* Inits the settings object based on what information we have available.
* Info taken into consideration:
* - Old Settings.js style data
* - Things that we stored in profile earlier but belong here.
* - Old Settings.js style data.
*
* @private
* @param {Object} featureState - The current state of the feature.
@@ -138,28 +105,5 @@ function _initSettings(featureState) {
email
}, settings);
if (!browser.isReactNative()) {
// Browser only
const localFlipX = JSON.parse(jitsiLocalStorage.getItem('localFlipX') || 'true');
const cameraDeviceId = jitsiLocalStorage.getItem('cameraDeviceId') || '';
const micDeviceId = jitsiLocalStorage.getItem('micDeviceId') || '';
// Currently audio output device change is supported only in Chrome and
// default output always has 'default' device ID
const audioOutputDeviceId = jitsiLocalStorage.getItem('audioOutputDeviceId') || 'default';
settings = assignIfDefined({
audioOutputDeviceId,
cameraDeviceId,
localFlipX,
micDeviceId
}, settings);
}
// Things we stored in profile earlier
const legacyProfile = _getLegacyProfile();
settings = assignIfDefined(legacyProfile, settings);
return settings;
}

View File

@@ -52,7 +52,7 @@ export default class AbstractAudioMuteButton<P: Props, S: *>
* Helper function to perform the actual setting of the audio mute / unmute
* action.
*
* @param {boolean} audioMuted - Whether video should be muted or not.
* @param {boolean} audioMuted - Whether audio should be muted or not.
* @protected
* @returns {void}
*/

View File

@@ -35,6 +35,8 @@ import {
setTrackMuted
} from './functions';
import './subscriber';
declare var APP: Object;
/**

View File

@@ -0,0 +1,24 @@
// @flow
import _ from 'lodash';
import { StateListenerRegistry } from '../../base/redux';
declare var APP: Object;
/**
* Notifies when the list of currently sharing participants changes.
*/
StateListenerRegistry.register(
/* selector */ state =>
state['features/base/tracks'].filter(tr => tr.videoType === 'desktop').map(t => t.participantId),
/* listener */ (participantIDs, store, previousParticipantIDs) => {
if (typeof APP !== 'object') {
return;
}
if (!_.isEqual(_.sortBy(participantIDs), _.sortBy(previousParticipantIDs))) {
APP.API.notifySharingParticipantsChanged(participantIDs);
}
}
);

View File

@@ -247,6 +247,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMe
body: message,
id,
nick: displayName,
privateMessage,
ts: timestamp
});

View File

@@ -57,9 +57,14 @@ class Subject extends Component<Props> {
*/
render() {
const { _hideConferenceTimer, _showParticipantCount, _showSubject, _subject, _visible } = this.props;
let className = `subject ${_visible ? 'visible' : ''}`;
if (!_hideConferenceTimer || _showParticipantCount || _showSubject) {
className += ' gradient';
}
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
<div className = { className }>
{ _showSubject && <span className = 'subject-text'>{ _subject }</span>}
{ _showParticipantCount && <ParticipantsCount /> }
{ !_hideConferenceTimer && <ConferenceTimer /> }

View File

@@ -0,0 +1,638 @@
// @flow
import { AtlasKitThemeProvider } from '@atlaskit/theme';
import React, { Component } from 'react';
import { AudioLevelIndicator } from '../../../audio-level-indicator';
import { Avatar } from '../../../base/avatar';
import JitsiMeetJS from '../../../base/lib-jitsi-meet/_';
import { MEDIA_TYPE, VideoTrack } from '../../../base/media';
import AudioTrack from '../../../base/media/components/web/AudioTrack';
import {
getLocalParticipant,
getParticipantById,
getParticipantCount
} from '../../../base/participants';
import { connect } from '../../../base/redux';
import { getLocalAudioTrack, getLocalVideoTrack, getTrackByMediaTypeAndParticipant } from '../../../base/tracks';
import { ConnectionIndicator } from '../../../connection-indicator';
import { DisplayName } from '../../../display-name';
import { StatusIndicators, RaisedHandIndicator, DominantSpeakerIndicator } from '../../../filmstrip';
import { PresenceLabel } from '../../../presence-status';
import { RemoteVideoMenuTriggerButton } from '../../../remote-video-menu';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
const JitsiTrackEvents = JitsiMeetJS.events.track;
declare var interfaceConfig: Object;
/**
* The type of the React {@code Component} state of {@link Thumbnail}.
*/
type State = {
/**
* The current audio level value for the Thumbnail.
*/
audioLevel: number,
/**
* The current volume setting for the Thumbnail.
*/
volume: ?number
};
/**
* The type of the React {@code Component} props of {@link Thumbnail}.
*/
type Props = {
/**
* The audio track related to the participant.
*/
_audioTrack: ?Object,
/**
* Disable/enable the auto hide functionality for the connection indicator.
*/
_connectionIndicatorAutoHideEnabled: boolean,
/**
* Disable/enable the connection indicator.
*/
_connectionIndicatorDisabled: boolean,
/**
* The current layout of the filmstrip.
*/
_currentLayout: string,
/**
* The default display name for the local participant.
*/
_defaultLocalDisplayName: string,
/**
* Indicates whether the profile functionality is disabled.
*/
_disableProfile: boolean,
/**
* The height of the Thumbnail.
*/
_height: number,
/**
* The aspect ratio of the Thumbnail in percents.
*/
_heightToWidthPercent: number,
/**
* Disable/enable the dominant speaker indicator.
*/
_isDominantSpeakerDisabled: boolean,
/**
* The size of the icon of indicators.
*/
_indicatorIconSize: number,
/**
* An object with information about the participant related to the thumbnaul.
*/
_participant: Object,
/**
* The number of participants in the call.
*/
_participantCount: number,
/**
* Indicates whether the "start silent" mode is enabled.
*/
_startSilent: Boolean,
/**
* The video track that will be displayed in the thumbnail.
*/
_videoTrack: ?Object,
/**
* The width of the thumbnail.
*/
_width: number,
/**
* The redux dispatch function.
*/
dispatch: Function,
/**
* Indicates whether the thumbnail is hovered or not.
*/
isHovered: ?boolean,
/**
* The ID of the participant related to the thumbnail.
*/
participantID: ?string
};
/**
* Implements a thumbnail.
*
* @extends Component
*/
class Thumbnail extends Component<Props, State> {
/**
* Initializes a new Thumbnail instance.
*
* @param {Object} props - The read-only React Component props with which
* the new instance is to be initialized.
*/
constructor(props: Props) {
super(props);
this.state = {
audioLevel: 0,
volume: undefined
};
this._updateAudioLevel = this._updateAudioLevel.bind(this);
this._onVolumeChange = this._onVolumeChange.bind(this);
this._onInitialVolumeSet = this._onInitialVolumeSet.bind(this);
}
/**
* Starts listening for audio level updates after the initial render.
*
* @inheritdoc
* @returns {void}
*/
componentDidMount() {
this._listenForAudioUpdates();
}
/**
* Stops listening for audio level updates on the old track and starts
* listening instead on the new track.
*
* @inheritdoc
* @returns {void}
*/
componentDidUpdate(prevProps: Props) {
if (prevProps._audioTrack !== this.props._audioTrack) {
this._stopListeningForAudioUpdates(prevProps._audioTrack);
this._listenForAudioUpdates();
this._updateAudioLevel(0);
}
}
/**
* Unsubscribe from audio level updates.
*
* @inheritdoc
* @returns {void}
*/
componentWillUnmount() {
this._stopListeningForAudioUpdates(this.props._audioTrack);
}
/**
* Starts listening for audio level updates from the library.
*
* @private
* @returns {void}
*/
_listenForAudioUpdates() {
const { _audioTrack } = this.props;
if (_audioTrack) {
const { jitsiTrack } = _audioTrack;
jitsiTrack && jitsiTrack.on(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
/**
* Stops listening to further updates from the passed track.
*
* @param {Object} audioTrack - The track.
* @private
* @returns {void}
*/
_stopListeningForAudioUpdates(audioTrack) {
if (audioTrack) {
const { jitsiTrack } = audioTrack;
jitsiTrack && jitsiTrack.off(JitsiTrackEvents.TRACK_AUDIO_LEVEL_CHANGED, this._updateAudioLevel);
}
}
_updateAudioLevel: (number) => void;
/**
* Updates the internal state of the last know audio level. The level should
* be between 0 and 1, as the level will be used as a percentage out of 1.
*
* @param {number} audioLevel - The new audio level for the track.
* @private
* @returns {void}
*/
_updateAudioLevel(audioLevel) {
this.setState({
audioLevel
});
}
/**
* Returns an object with the styles for thumbnail.
*
* @returns {Object} - The styles for the thumbnail.
*/
_getStyles(): Object {
const { _height, _heightToWidthPercent, _currentLayout } = this.props;
let styles;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const avatarSize = _height / 2;
styles = {
avatarContainer: {
height: `${avatarSize}px`,
width: `${avatarSize}px`
}
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW: {
styles = {
avatarContainer: {
height: '50%',
width: `${_heightToWidthPercent / 2}%`
}
};
break;
}
}
return styles;
}
/**
* Renders a fake participant (youtube video) thumbnail.
*
* @param {string} id - The id of the participant.
* @returns {ReactElement}
*/
_renderFakeParticipant() {
const { _participant } = this.props;
const { id } = _participant;
return (
<>
<img
className = 'sharedVideoAvatar'
src = { `https://img.youtube.com/vi/${id}/0.jpg` } />
<div className = 'displayNameContainer'>
<DisplayName
elementID = 'sharedVideoContainer_name'
participantID = { id } />
</div>
</>
);
}
/**
* Renders the top indicators of the thumbnail.
*
* @returns {Component}
*/
_renderTopIndicators() {
const {
_connectionIndicatorAutoHideEnabled,
_connectionIndicatorDisabled,
_currentLayout,
_isDominantSpeakerDisabled,
_indicatorIconSize: iconSize,
_participant,
_participantCount,
isHovered
} = this.props;
const showConnectionIndicator = isHovered || !_connectionIndicatorAutoHideEnabled;
const { id, local = false, dominantSpeaker = false } = _participant;
const showDominantSpeaker = !_isDominantSpeakerDisabled && dominantSpeaker;
let statsPopoverPosition, tooltipPosition;
switch (_currentLayout) {
case LAYOUTS.TILE_VIEW:
statsPopoverPosition = 'right top';
tooltipPosition = 'right';
break;
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
statsPopoverPosition = 'left top';
tooltipPosition = 'left';
break;
default:
statsPopoverPosition = 'top center';
tooltipPosition = 'top';
}
return (
<div>
<AtlasKitThemeProvider mode = 'dark'>
{ !_connectionIndicatorDisabled
&& <ConnectionIndicator
alwaysVisible = { showConnectionIndicator }
enableStatsDisplay = { true }
iconSize = { iconSize }
isLocalVideo = { local }
participantId = { id }
statsPopoverPosition = { statsPopoverPosition } />
}
<RaisedHandIndicator
iconSize = { iconSize }
participantId = { id }
tooltipPosition = { tooltipPosition } />
{ showDominantSpeaker && _participantCount > 2
&& <DominantSpeakerIndicator
iconSize = { iconSize }
tooltipPosition = { tooltipPosition } />
}
</AtlasKitThemeProvider>
</div>);
}
/**
* Renders the avatar.
*
* @returns {ReactElement}
*/
_renderAvatar() {
const { _participant } = this.props;
const { id } = _participant;
const styles = this._getStyles();
return (
<div
className = 'avatar-container'
style = { styles.avatarContainer }>
<Avatar
className = 'userAvatar'
participantId = { id } />
</div>
);
}
/**
* Renders the local participant's thumbnail.
*
* @returns {ReactElement}
*/
_renderLocalParticipant() {
const {
_defaultLocalDisplayName,
_disableProfile,
_participant,
_videoTrack
} = this.props;
const { id } = _participant || {};
const { audioLevel } = this.state;
return (
<>
<div className = 'videocontainer__background' />
<span id = 'localVideoWrapper'>
<VideoTrack
id = 'localVideo_container'
videoTrack = { _videoTrack } />
</span>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__toptoolbar'>
{ this._renderTopIndicators() }
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
allowEditing = { !_disableProfile }
displayNameSuffix = { _defaultLocalDisplayName }
elementID = 'localDisplayName'
participantID = { id } />
</div>
{ this._renderAvatar() }
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
/**
* Renders a remote participant's 'thumbnail.
*
* @returns {ReactElement}
*/
_renderRemoteParticipant() {
const {
_audioTrack,
_participant,
_startSilent
} = this.props;
const { id } = _participant;
const { audioLevel, volume } = this.state;
// hide volume when in silent mode
const onVolumeChange = _startSilent ? undefined : this._onVolumeChange;
const jitsiTrack = _audioTrack?.jitsiTrack;
const audioTrackId = jitsiTrack && jitsiTrack.getId();
return (
<>
{
_audioTrack
? <AudioTrack
audioTrack = { _audioTrack }
id = { `remoteAudio_${audioTrackId || ''}` }
muted = { _startSilent }
onInitialVolumeSet = { this._onInitialVolumeSet }
volume = { this.state.volume } />
: null
}
<div className = 'videocontainer__background' />
<div className = 'videocontainer__toptoolbar'>
{ this._renderTopIndicators() }
</div>
<div className = 'videocontainer__toolbar'>
<StatusIndicators participantID = { id } />
</div>
<div className = 'videocontainer__hoverOverlay' />
<div className = 'displayNameContainer'>
<DisplayName
elementID = { `participant_${id}_name` }
participantID = { id } />
</div>
{ this._renderAvatar() }
<div className = 'presence-label-container'>
<PresenceLabel
className = 'presence-label'
participantID = { id } />
</div>
<span className = 'remotevideomenu'>
<AtlasKitThemeProvider mode = 'dark'>
<RemoteVideoMenuTriggerButton
initialVolumeValue = { volume }
onVolumeChange = { onVolumeChange }
participantID = { id } />
</AtlasKitThemeProvider>
</span>
<span className = 'audioindicator-container'>
<AudioLevelIndicator audioLevel = { audioLevel } />
</span>
</>
);
}
_onInitialVolumeSet: Object => void;
/**
* A handler for the initial volume value of the audio element.
*
* @param {number} volume - Properties of the audio element.
* @returns {void}
*/
_onInitialVolumeSet(volume) {
if (this.state.volume !== volume) {
this.setState({ volume });
}
}
_onVolumeChange: number => void;
/**
* Handles volume changes.
*
* @param {number} value - The new value for the volume.
* @returns {void}
*/
_onVolumeChange(value) {
this.setState({ volume: value });
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const { _participant } = this.props;
if (!_participant) {
return null;
}
const { isFakeParticipant, local } = _participant;
if (local) {
return this._renderLocalParticipant();
}
if (isFakeParticipant) {
return this._renderFakeParticipant();
}
return this._renderRemoteParticipant();
}
}
/**
* Maps (parts of) the redux state to the associated props for this component.
*
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {Props}
*/
function _mapStateToProps(state, ownProps): Object {
const { participantID } = ownProps;
// Only the local participant won't have id for the time when the conference is not yet joined.
const participant = participantID ? getParticipantById(state, participantID) : getLocalParticipant(state);
const isLocal = participant?.local ?? true;
const _videoTrack = isLocal
? getLocalVideoTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.VIDEO, participantID);
const _audioTrack = isLocal
? getLocalAudioTrack(state['features/base/tracks'])
: getTrackByMediaTypeAndParticipant(state['features/base/tracks'], MEDIA_TYPE.AUDIO, participantID);
const _currentLayout = getCurrentLayout(state);
let size = {};
const { startSilent, disableProfile = false } = state['features/base/config'];
const { NORMAL = 8 } = interfaceConfig.INDICATOR_FONT_SIZES || {};
switch (_currentLayout) {
case LAYOUTS.HORIZONTAL_FILMSTRIP_VIEW: {
const {
horizontalViewDimensions = {
local: {},
remote: {}
}
} = state['features/filmstrip'];
const { local, remote } = horizontalViewDimensions;
const { width, height } = isLocal ? local : remote;
size = {
_width: width,
_height: height
};
break;
}
case LAYOUTS.VERTICAL_FILMSTRIP_VIEW:
size = {
_heightToWidthPercent: isLocal
? 100 / interfaceConfig.LOCAL_THUMBNAIL_RATIO
: 100 / interfaceConfig.REMOTE_THUMBNAIL_RATIO
};
break;
case LAYOUTS.TILE_VIEW: {
const { width, height } = state['features/filmstrip'].tileViewDimensions.thumbnailSize;
size = {
_width: width,
_height: height
};
break;
}
}
return {
_audioTrack,
_connectionIndicatorAutoHideEnabled: interfaceConfig.CONNECTION_INDICATOR_AUTO_HIDE_ENABLED,
_connectionIndicatorDisabled: interfaceConfig.CONNECTION_INDICATOR_DISABLED,
_currentLayout,
_defaultLocalDisplayName: interfaceConfig.DEFAULT_LOCAL_DISPLAY_NAME,
_disableProfile: disableProfile,
_isDominantSpeakerDisabled: interfaceConfig.DISABLE_DOMINANT_SPEAKER_INDICATOR,
_indicatorIconSize: NORMAL,
_participant: participant,
_participantCount: getParticipantCount(state),
_startSilent: Boolean(startSilent),
_videoTrack,
...size
};
}
export default connect(_mapStateToProps)(Thumbnail);

View File

@@ -7,3 +7,4 @@ export { default as ModeratorIndicator } from './ModeratorIndicator';
export { default as RaisedHandIndicator } from './RaisedHandIndicator';
export { default as StatusIndicators } from './StatusIndicators';
export { default as VideoMutedIndicator } from './VideoMutedIndicator';
export { default as Thumbnail } from './Thumbnail';

View File

@@ -9,3 +9,8 @@ export const FILMSTRIP_SIZE = 90;
* The aspect ratio of a tile in tile view.
*/
export const TILE_ASPECT_RATIO = 16 / 9;
/**
* Width below which the overflow menu(s) will be displayed as drawer(s).
*/
export const DISPLAY_DRAWER_THRESHOLD = 512;

View File

@@ -1,10 +1,20 @@
// @flow
import { JitsiParticipantConnectionStatus } from '../base/lib-jitsi-meet';
import { MEDIA_TYPE } from '../base/media';
import {
getLocalParticipant,
getParticipantById,
getParticipantCountWithFake,
getPinnedParticipant
} from '../base/participants';
import { toState } from '../base/redux';
import {
getLocalVideoTrack,
getTrackByMediaTypeAndParticipant,
isLocalTrackMuted,
isRemoteTrackMuted
} from '../base/tracks/functions';
import { TILE_ASPECT_RATIO } from './constants';
@@ -63,6 +73,40 @@ export function shouldRemoteVideosBeVisible(state: Object) {
|| state['features/base/config'].disable1On1Mode);
}
/**
* Checks whether there is a playable video stream available for the user associated with the passed ID.
*
* @param {Object | Function} stateful - The Object or Function that can be
* resolved to a Redux state object with the toState function.
* @param {string} id - The id of the participant.
* @returns {boolean} <tt>true</tt> if there is a playable video stream available
* or <tt>false</tt> otherwise.
*/
export function isVideoPlayable(stateful: Object | Function, id: String) {
const state = toState(stateful);
const tracks = state['features/base/tracks'];
const participant = id ? getParticipantById(state, id) : getLocalParticipant(state);
const isLocal = participant?.local ?? true;
const { connectionStatus } = participant || {};
const videoTrack
= isLocal ? getLocalVideoTrack(tracks) : getTrackByMediaTypeAndParticipant(tracks, MEDIA_TYPE.VIDEO, id);
const isAudioOnly = Boolean(state['features/base/audio-only'].enabled);
let isPlayable = false;
if (isLocal) {
const isVideoMuted = isLocalTrackMuted(tracks, MEDIA_TYPE.VIDEO);
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly;
} else if (!participant?.isFakeParticipant) { // remote participants excluding shared video
const isVideoMuted = isRemoteTrackMuted(tracks, MEDIA_TYPE.VIDEO, id);
isPlayable = Boolean(videoTrack) && !isVideoMuted && !isAudioOnly
&& connectionStatus === JitsiParticipantConnectionStatus.ACTIVE;
}
return isPlayable;
}
/**
* Calculates the size for thumbnails when in horizontal view layout.
*

View File

@@ -3,9 +3,11 @@
import Filmstrip from '../../../modules/UI/videolayout/Filmstrip';
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout';
import { StateListenerRegistry, equals } from '../base/redux';
import { setOverflowDrawer } from '../toolbox/actions.web';
import { getCurrentLayout, getTileViewGridDimensions, shouldDisplayTileView, LAYOUTS } from '../video-layout';
import { setHorizontalViewDimensions, setTileViewDimensions } from './actions.web';
import { DISPLAY_DRAWER_THRESHOLD } from './constants';
/**
* Listens for changes in the number of participants to calculate the dimensions of the tile view grid and the tiles.
@@ -123,3 +125,12 @@ StateListenerRegistry.register(
);
}
});
/**
* Listens for changes in the client width to determine whether the overflow menu(s) should be displayed as drawers.
*/
StateListenerRegistry.register(
/* selector */ state => state['features/base/responsive-ui'].clientWidth < DISPLAY_DRAWER_THRESHOLD,
/* listener */ (widthBelowThreshold, store) => {
store.dispatch(setOverflowDrawer(widthBelowThreshold));
});

View File

@@ -79,25 +79,27 @@ class DialInNumber extends Component<Props> {
return (
<div className = 'dial-in-number'>
<span className = 'phone-number'>
<span className = 'info-label'>
{ t('info.dialInNumber') }
<div>
<span className = 'phone-number'>
<span className = 'info-label'>
{ t('info.dialInNumber') }
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
{ phoneNumber }
</span>
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
{ phoneNumber }
<span className = 'conference-id'>
<span className = 'info-label'>
{ t('info.dialInConferenceID') }
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
{ `${_formatConferenceIDPin(conferenceID)}#` }
</span>
</span>
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'conference-id'>
<span className = 'info-label'>
{ t('info.dialInConferenceID') }
</span>
<span className = 'spacer'>&nbsp;</span>
<span className = 'info-value'>
{ `${_formatConferenceIDPin(conferenceID)}#` }
</span>
</span>
</div>
<a
className = 'dial-in-copy'
onClick = { this._onCopyText }>

View File

@@ -4,6 +4,7 @@ import { i18next } from '../base/i18n';
import { isLocalParticipantModerator } from '../base/participants';
import { toState } from '../base/redux';
import { doGetJSON, parseURIString } from '../base/util';
import { isVpaasMeeting } from '../billing-counter/functions';
import logger from './logger';
@@ -352,7 +353,7 @@ export function invitePeopleAndChatRooms( // eslint-disable-line max-params
export function isAddPeopleEnabled(state: Object): boolean {
const { peopleSearchUrl } = state['features/base/config'];
return state['features/base/jwt'].jwt && Boolean(peopleSearchUrl);
return state['features/base/jwt'].jwt && Boolean(peopleSearchUrl) && !isVpaasMeeting(state);
}
/**

View File

@@ -1,5 +1,9 @@
// @flow
import { NativeEventEmitter, NativeModules } from 'react-native';
import { appNavigate } from '../../app/actions';
import { APP_WILL_MOUNT } from '../../base/app/actionTypes';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
@@ -18,7 +22,10 @@ import {
JITSI_CONNECTION_URL_KEY,
getURLWithoutParams
} from '../../base/connection';
import { SET_AUDIO_MUTED } from '../../base/media/actionTypes';
import { PARTICIPANT_JOINED, PARTICIPANT_LEFT } from '../../base/participants';
import { MiddlewareRegistry } from '../../base/redux';
import { muteLocal } from '../../remote-video-menu/actions';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
import { sendEvent } from './functions';
@@ -29,6 +36,9 @@ import { sendEvent } from './functions';
*/
const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED';
const { ExternalAPI } = NativeModules;
const eventEmitter = new NativeEventEmitter(ExternalAPI);
/**
* Middleware that captures Redux actions and uses the ExternalAPI module to
* turn them into native events so the application knows about them.
@@ -41,6 +51,9 @@ MiddlewareRegistry.register(store => next => action => {
const { type } = action;
switch (type) {
case APP_WILL_MOUNT:
_registerForNativeEvents(store.dispatch);
break;
case CONFERENCE_FAILED: {
const { error, ...data } = action;
@@ -111,14 +124,56 @@ MiddlewareRegistry.register(store => next => action => {
break;
}
case PARTICIPANT_JOINED:
case PARTICIPANT_LEFT: {
const { participant } = action;
sendEvent(
store,
action.type,
/* data */ {
isLocal: participant.local,
email: participant.email,
name: participant.name,
participantId: participant.id
});
break;
}
case SET_ROOM:
_maybeTriggerEarlyConferenceWillJoin(store, action);
break;
case SET_AUDIO_MUTED:
sendEvent(
store,
'AUDIO_MUTED_CHANGED',
/* data */ {
muted: action.muted
});
break;
}
return result;
});
/**
* Registers for events sent from the native side via NativeEventEmitter.
*
* @param {Dispatch} dispatch - The Redux dispatch function.
* @private
* @returns {void}
*/
function _registerForNativeEvents(dispatch) {
eventEmitter.addListener(ExternalAPI.HANG_UP, () => {
dispatch(appNavigate(undefined));
});
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
dispatch(muteLocal(muted === 'true'));
});
}
/**
* Returns a {@code String} representation of a specific error {@code Object}.
*

View File

@@ -60,6 +60,11 @@ type Props = {
*/
_menuPosition: string,
/**
* Whether to display the Popover as a drawer.
*/
_overflowDrawer: boolean,
/**
* The current state of the participant's remote control session.
*/
@@ -75,12 +80,7 @@ type Props = {
* A value between 0 and 1 indicating the volume of the participant's
* audio element.
*/
initialVolumeValue: number,
/**
* Callback to invoke when the popover has been displayed.
*/
onMenuDisplay: Function,
initialVolumeValue: ?number,
/**
* Callback to invoke when changing the level of the participant's
@@ -111,19 +111,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
*/
_rootElement = null;
/**
* Initializes a new {#@code RemoteVideoMenuTriggerButton} instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Object) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onShowRemoteMenu = this._onShowRemoteMenu.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
@@ -140,7 +127,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
return (
<Popover
content = { content }
onPopoverOpen = { this._onShowRemoteMenu }
overflowDrawer = { this.props._overflowDrawer }
position = { this.props._menuPosition }>
<span
className = 'popover-trigger remote-video-menu-trigger'>
@@ -153,18 +140,6 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
);
}
_onShowRemoteMenu: () => void;
/**
* Opens the {@code RemoteVideoMenu}.
*
* @private
* @returns {void}
*/
_onShowRemoteMenu() {
this.props.onMenuDisplay();
}
/**
* Creates a new {@code RemoteVideoMenu} with buttons for interacting with
* the remote participant.
@@ -241,7 +216,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
participantID = { participantID } />
);
if (onVolumeChange) {
if (onVolumeChange && initialVolumeValue && !isNaN(initialVolumeValue)) {
buttons.push(
<VolumeSlider
initialValue = { initialVolumeValue }
@@ -268,14 +243,7 @@ class RemoteVideoMenuTriggerButton extends Component<Props> {
* @param {Object} state - The Redux state.
* @param {Object} ownProps - The own props of the component.
* @private
* @returns {{
* _isAudioMuted: boolean,
* _isModerator: boolean,
* _disableKick: boolean,
* _disableRemoteMute: boolean,
* _menuPosition: string,
* _remoteControlState: number
* }}
* @returns {Props}
*/
function _mapStateToProps(state, ownProps) {
const { participantID } = ownProps;
@@ -290,6 +258,7 @@ function _mapStateToProps(state, ownProps) {
const { active, controller } = state['features/remote-control'];
const { requestedParticipant, controlled } = controller;
const activeParticipant = requestedParticipant || controlled;
const { overflowDrawer } = state['features/toolbox'];
if (_supportsRemoteControl
&& ((!active && !_isRemoteControlSessionActive) || activeParticipant === participantID)) {
@@ -322,7 +291,8 @@ function _mapStateToProps(state, ownProps) {
_disableKick: Boolean(disableKick),
_disableRemoteMute: Boolean(disableRemoteMute),
_remoteControlState,
_menuPosition
_menuPosition,
_overflowDrawer: overflowDrawer
};
}

View File

@@ -29,6 +29,11 @@ export const FULL_SCREEN_CHANGED = 'FULL_SCREEN_CHANGED';
*/
export const SET_FULL_SCREEN = 'SET_FULL_SCREEN';
/**
* The type of the redux action that toggles whether the overflow menu(s) should be shown as drawers.
*/
export const SET_OVERFLOW_DRAWER = 'SET_OVERFLOW_DRAWER';
/**
* The type of the (redux) action which shows/hides the OverflowMenu.
*

View File

@@ -4,7 +4,8 @@ import type { Dispatch } from 'redux';
import {
FULL_SCREEN_CHANGED,
SET_FULL_SCREEN
SET_FULL_SCREEN,
SET_OVERFLOW_DRAWER
} from './actionTypes';
import {
clearToolboxTimeout,
@@ -143,3 +144,19 @@ export function showToolbox(timeout: number = 0): Object {
}
};
}
/**
* Signals a request to display overflow as drawer.
*
* @param {boolean} displayAsDrawer - True to display overflow as drawer, false to preserve original behaviour.
* @returns {{
* type: SET_OVERFLOW_DRAWER,
* displayAsDrawer: boolean
* }}
*/
export function setOverflowDrawer(displayAsDrawer: boolean) {
return {
type: SET_OVERFLOW_DRAWER,
displayAsDrawer
};
}

View File

@@ -0,0 +1,90 @@
// @flow
import React, { useEffect, useRef, useState } from 'react';
import { Icon, IconArrowUp, IconArrowDown } from '../../../base/icons';
type Props = {
/**
* Whether the drawer should have a button that expands its size or not.
*/
canExpand: ?boolean,
/**
* The component(s) to be displayed within the drawer menu.
*/
children: React$Node,
/**
Whether the drawer should be shown or not.
*/
isOpen: boolean,
/**
Function that hides the drawer.
*/
onClose: Function
};
/**
* Component that displays the mobile friendly drawer on web.
*
* @returns {ReactElement}
*/
function Drawer({
canExpand,
children,
isOpen,
onClose }: Props) {
const [ expanded, setExpanded ] = useState(false);
const drawerRef: Object = useRef(null);
/**
* Closes the drawer when clicking outside of it.
*
* @param {Event} event - Mouse down event object.
* @returns {void}
*/
function handleOutsideClick(event: MouseEvent) {
if (drawerRef.current && !drawerRef.current.contains(event.target)) {
onClose();
}
}
useEffect(() => {
window.addEventListener('mousedown', handleOutsideClick);
return () => {
window.removeEventListener('mousedown', handleOutsideClick);
};
}, [ drawerRef ]);
/**
* Toggles the menu state between expanded/collapsed.
*
* @returns {void}
*/
function toggleExpanded() {
setExpanded(!expanded);
}
return (
isOpen ? (
<div
className = { `drawer-menu${expanded ? ' expanded' : ''}` }
ref = { drawerRef }>
{canExpand && (
<div
className = 'drawer-toggle'
onClick = { toggleExpanded }>
<Icon src = { expanded ? IconArrowDown : IconArrowUp } />
</div>
)}
{children}
</div>
) : null
);
}
export default Drawer;

View File

@@ -0,0 +1,47 @@
// @flow
import { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
type Props = {
/**
* The component(s) to be displayed within the drawer portal.
*/
children: React$Node
};
/**
* Component meant to render a drawer at the bottom of the screen,
* by creating a portal containing the component's children.
*
* @returns {ReactElement}
*/
function DrawerPortal({ children }: Props) {
const [ portalTarget ] = useState(() => {
const portalDiv = document.createElement('div');
portalDiv.className = 'drawer-portal';
return portalDiv;
});
useEffect(() => {
if (document.body) {
document.body.appendChild(portalTarget);
}
return () => {
if (document.body) {
document.body.removeChild(portalTarget);
}
};
}, []);
return ReactDOM.createPortal(
children,
portalTarget
);
}
export default DrawerPortal;

View File

@@ -6,7 +6,10 @@ import React, { Component } from 'react';
import { createToolbarEvent, sendAnalytics } from '../../../analytics';
import { translate } from '../../../base/i18n';
import { IconMenuThumb } from '../../../base/icons';
import { connect } from '../../../base/redux';
import Drawer from './Drawer';
import DrawerPortal from './DrawerPortal';
import ToolbarButton from './ToolbarButton';
/**
@@ -29,6 +32,11 @@ type Props = {
*/
onVisibilityChange: Function,
/**
* Whether to display the OverflowMenu as a drawer.
*/
overflowDrawer: boolean,
/**
* Invoked to obtain translated strings.
*/
@@ -63,27 +71,58 @@ class OverflowMenuButton extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { children, isOpen, t } = this.props;
const { children, isOpen, overflowDrawer } = this.props;
return (
<div className = 'toolbox-button-wth-dialog'>
<InlineDialog
content = { children }
isOpen = { isOpen }
onClose = { this._onCloseDialog }
position = { 'top right' }>
<ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.moreActions') }
icon = { IconMenuThumb }
onClick = { this._onToggleDialogVisibility }
toggled = { isOpen }
tooltip = { t('toolbar.moreActions') } />
</InlineDialog>
{
overflowDrawer ? (
<>
{this._renderToolbarButton()}
<DrawerPortal>
<Drawer
canExpand = { true }
isOpen = { isOpen }
onClose = { this._onCloseDialog }>
{children}
</Drawer>
</DrawerPortal>
</>
) : (
<InlineDialog
content = { children }
isOpen = { isOpen }
onClose = { this._onCloseDialog }
position = { 'top right' }>
{this._renderToolbarButton()}
</InlineDialog>
)
}
</div>
);
}
_renderToolbarButton: () => React$Node;
/**
* Renders the actual toolbar overflow menu button.
*
* @returns {ReactElement}
*/
_renderToolbarButton() {
const { isOpen, t } = this.props;
return (
<ToolbarButton
accessibilityLabel =
{ t('toolbar.accessibilityLabel.moreActions') }
icon = { IconMenuThumb }
onClick = { this._onToggleDialogVisibility }
toggled = { isOpen }
tooltip = { t('toolbar.moreActions') } />
);
}
_onCloseDialog: () => void;
/**
@@ -113,4 +152,19 @@ class OverflowMenuButton extends Component<Props> {
}
}
export default translate(OverflowMenuButton);
/**
* Maps (parts of) the Redux state to the associated props for the
* {@code OverflowMenuButton} component.
*
* @param {Object} state - The Redux state.
* @returns {Props}
*/
function mapStateToProps(state) {
const { overflowDrawer } = state['features/toolbox'];
return {
overflowDrawer
};
}
export default translate(connect(mapStateToProps)(OverflowMenuButton));

View File

@@ -73,7 +73,8 @@ import {
import {
setFullScreen,
setOverflowMenuVisible,
setToolbarHovered
setToolbarHovered,
setToolboxVisible
} from '../../actions';
import { isToolboxVisible } from '../../functions';
import DownloadButton from '../DownloadButton';
@@ -238,6 +239,7 @@ class Toolbox extends Component<Props, State> {
this._onMouseOver = this._onMouseOver.bind(this);
this._onResize = this._onResize.bind(this);
this._onSetOverflowVisible = this._onSetOverflowVisible.bind(this);
this._onTabIn = this._onTabIn.bind(this);
this._onShortcutToggleChat = this._onShortcutToggleChat.bind(this);
this._onShortcutToggleFullScreen = this._onShortcutToggleFullScreen.bind(this);
@@ -368,6 +370,7 @@ class Toolbox extends Component<Props, State> {
<div
className = { rootClassNames }
id = 'new-toolbox'
onFocus = { this._onTabIn }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }>
<div className = 'toolbox-background' />
@@ -698,6 +701,19 @@ class Toolbox extends Component<Props, State> {
this._doToggleScreenshare();
}
_onTabIn: () => void;
/**
* Toggle the toolbar visibility when tabbing into it.
*
* @returns {void}
*/
_onTabIn() {
if (!this.props._visible) {
this.props.dispatch(setToolboxVisible(true));
}
}
_onToolbarOpenFeedback: () => void;
/**
@@ -1030,12 +1046,14 @@ class Toolbox extends Component<Props, State> {
key = 'fullscreen'
onClick = { this._onToolbarToggleFullScreen }
text = { _fullScreen ? t('toolbar.exitFullScreen') : t('toolbar.enterFullScreen') } />,
<LiveStreamButton
key = 'livestreaming'
showLabel = { true } />,
<RecordButton
key = 'record'
showLabel = { true } />,
this._shouldShowButton('livestreaming')
&& <LiveStreamButton
key = 'livestreaming'
showLabel = { true } />,
this._shouldShowButton('recording')
&& <RecordButton
key = 'record'
showLabel = { true } />,
this._shouldShowButton('sharedvideo')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.sharedvideo') }
@@ -1047,18 +1065,19 @@ class Toolbox extends Component<Props, State> {
&& <SharedDocumentButton
key = 'etherpad'
showLabel = { true } />,
<VideoBlurButton
key = 'videobackgroundblur'
showLabel = { true }
visible = { this._shouldShowButton('videobackgroundblur') && !_screensharing } />,
<SettingsButton
key = 'settings'
showLabel = { true }
visible = { this._shouldShowButton('settings') } />,
<MuteEveryoneButton
key = 'mute-everyone'
showLabel = { true }
visible = { this._shouldShowButton('mute-everyone') } />,
this._shouldShowButton('videobackgroundblur')
&& <VideoBlurButton
key = 'videobackgroundblur'
showLabel = { true }
visible = { !_screensharing } />,
this._shouldShowButton('settings')
&& <SettingsButton
key = 'settings'
showLabel = { true } />,
this._shouldShowButton('mute-everyone')
&& <MuteEveryoneButton
key = 'mute-everyone'
showLabel = { true } />,
this._shouldShowButton('stats')
&& <OverflowMenuItem
accessibilityLabel = { t('toolbar.accessibilityLabel.speakerStats') }
@@ -1224,7 +1243,7 @@ class Toolbox extends Component<Props, State> {
t
} = this.props;
const overflowMenuContent = this._renderOverflowMenuContent();
const overflowHasItems = Boolean(overflowMenuContent.length);
const overflowHasItems = overflowMenuContent.some(item => Boolean(item));
const toolbarAccLabel = 'toolbar.accessibilityLabel.moreActionsMenu';
const buttonsLeft = [];
const buttonsRight = [];

View File

@@ -2,3 +2,5 @@ export { default as AudioSettingsButton } from './AudioSettingsButton';
export { default as VideoSettingsButton } from './VideoSettingsButton';
export { default as ToolbarButton } from './ToolbarButton';
export { default as Toolbox } from './Toolbox';
export { default as Drawer } from './Drawer';
export { default as DrawerPortal } from './DrawerPortal';

View File

@@ -5,6 +5,7 @@ import { ReducerRegistry, set } from '../base/redux';
import {
CLEAR_TOOLBOX_TIMEOUT,
FULL_SCREEN_CHANGED,
SET_OVERFLOW_DRAWER,
SET_OVERFLOW_MENU_VISIBLE,
SET_TOOLBAR_HOVERED,
SET_TOOLBOX_ALWAYS_VISIBLE,
@@ -25,6 +26,7 @@ declare var interfaceConfig: Object;
* alwaysVisible: boolean,
* enabled: boolean,
* hovered: boolean,
* overflowDrawer: boolean,
* overflowMenuVisible: boolean,
* timeoutID: number,
* timeoutMS: number,
@@ -79,6 +81,13 @@ function _getInitialState() {
*/
hovered: false,
/**
* The indicator which determines whether the overflow menu(s) are to be displayed as drawers.
*
* @type {boolean}
*/
overflowDrawer: false,
/**
* The indicator which determines whether the OverflowMenu is visible.
*
@@ -103,7 +112,7 @@ function _getInitialState() {
timeoutMS,
/**
* The indicator which determines whether the Toolbox is visible.
* The indicator that determines whether the Toolbox is visible.
*
* @type {boolean}
*/
@@ -127,6 +136,12 @@ ReducerRegistry.register(
fullScreen: action.fullScreen
};
case SET_OVERFLOW_DRAWER:
return {
...state,
overflowDrawer: action.displayAsDrawer
};
case SET_OVERFLOW_MENU_VISIBLE:
return {
...state,

View File

@@ -1,7 +1,8 @@
// @flow
import VideoLayout from '../../../modules/UI/videolayout/VideoLayout.js';
import { CONFERENCE_JOINED, CONFERENCE_WILL_LEAVE } from '../base/conference';
import { CONFERENCE_WILL_LEAVE } from '../base/conference';
import { MEDIA_TYPE } from '../base/media';
import {
DOMINANT_SPEAKER_CHANGED,
PARTICIPANT_JOINED,
@@ -33,10 +34,6 @@ MiddlewareRegistry.register(store => next => action => {
const result = next(action);
switch (action.type) {
case CONFERENCE_JOINED:
VideoLayout.mucJoined();
break;
case CONFERENCE_WILL_LEAVE:
VideoLayout.reset();
break;
@@ -77,13 +74,13 @@ MiddlewareRegistry.register(store => next => action => {
break;
case TRACK_ADDED:
if (!action.track.local) {
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
VideoLayout.onRemoteStreamAdded(action.track.jitsiTrack);
}
break;
case TRACK_REMOVED:
if (!action.track.local) {
if (!action.track.local && action.track.mediaType !== MEDIA_TYPE.AUDIO) {
VideoLayout.onRemoteStreamRemoved(action.track.jitsiTrack);
}

View File

@@ -167,10 +167,17 @@ class WelcomePage extends AbstractWelcomePage {
* @returns {void}
*/
_onFieldFocusChange(focused) {
focused
&& this.setState({
_fieldFocused: true
if (focused) {
// Stop placeholder animation.
this._clearTimeouts();
this.setState({
_fieldFocused: true,
roomPlaceholder: ''
});
} else {
// Restart room placeholder animation.
this._updateRoomname();
}
Animated.timing(
this.state.hintBoxAnimation,

View File

@@ -2,14 +2,14 @@
<head>
<head>
<!--#include virtual="head.html" -->
<!--#include virtual="/head.html" -->
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!--#include virtual="base.html" -->
<!--#include virtual="/base.html" -->
<link rel="apple-touch-icon" href="images/apple-touch-icon.png">
<!--#include virtual="title.html" -->
<!--#include virtual="/title.html" -->
</head>
<style>
body,
@@ -61,4 +61,4 @@
</div>
</body>
</html>
</html>