mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-01-08 15:50:21 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f891fd060 | ||
|
|
a39905883f | ||
|
|
fe78f104bc | ||
|
|
9c13603489 | ||
|
|
61037b982b | ||
|
|
df21ec6f04 | ||
|
|
23574e9edc | ||
|
|
ec3130af0e | ||
|
|
3b692dc502 | ||
|
|
6689aa3700 | ||
|
|
13bc9863cb | ||
|
|
7ff332b2bb | ||
|
|
8aae2065dc | ||
|
|
b65e61f633 | ||
|
|
88f1c218eb | ||
|
|
f6df76ab10 | ||
|
|
85f1701393 | ||
|
|
2f7ff37472 | ||
|
|
c752ea13f1 | ||
|
|
5ef60c3a7d | ||
|
|
1196ede961 | ||
|
|
12877c7fce | ||
|
|
c6bb600d4c | ||
|
|
845e23a947 | ||
|
|
55ebb60f85 | ||
|
|
8d3d94f568 | ||
|
|
be24772e57 | ||
|
|
a807f804a9 | ||
|
|
db48dc3ed3 | ||
|
|
9bae7099dd | ||
|
|
e990f6984a | ||
|
|
9f321c988e | ||
|
|
2e5e9a3f79 | ||
|
|
fdb8f76b90 | ||
|
|
3d97bef308 | ||
|
|
5a55c7b965 | ||
|
|
e161cbc4bd | ||
|
|
51e381a0b1 | ||
|
|
d8dd644f38 | ||
|
|
e30b2e14a5 | ||
|
|
7f1894dd57 | ||
|
|
4dda508708 | ||
|
|
69c6463476 |
@@ -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
|
||||
|
||||
@@ -10,17 +10,17 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.0.2'
|
||||
classpath 'com.google.gms:google-services:4.3.3'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0'
|
||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||
classpath 'com.google.gms:google-services:4.3.4'
|
||||
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1'
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
buildToolsVersion = "29.0.3"
|
||||
compileSdkVersion = 29
|
||||
buildToolsVersion = "30.0.3"
|
||||
compileSdkVersion = 30
|
||||
minSdkVersion = 23
|
||||
targetSdkVersion = 29
|
||||
targetSdkVersion = 30
|
||||
supportLibVersion = "28.0.0"
|
||||
|
||||
// The Maven artifact groupdId of the third-party react-native modules which
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
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"),
|
||||
SEND_ENDPOINT_TEXT_MESSAGE("org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
134
android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java
Normal file
134
android/sdk/src/main/java/org/jitsi/meet/sdk/BroadcastEvent.java
Normal file
@@ -0,0 +1,134 @@
|
||||
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"),
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED("org.jitsi.meet.ENDPOINT_TEXT_MESSAGE_RECEIVED");
|
||||
|
||||
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 static final String ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME = "ENDPOINT_TEXT_MESSAGE_RECEIVED";
|
||||
|
||||
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;
|
||||
case ENDPOINT_TEXT_MESSAGE_RECEIVED_NAME:
|
||||
return ENDPOINT_TEXT_MESSAGE_RECEIVED;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
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());
|
||||
}
|
||||
|
||||
public static Intent buildSendEndpointTextMessageIntent(String to, String message) {
|
||||
Intent intent = new Intent(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
|
||||
intent.putExtra("to", to);
|
||||
intent.putExtra("message", message);
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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());
|
||||
intentFilter.addAction(BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.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());
|
||||
}
|
||||
}
|
||||
@@ -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,23 @@ 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());
|
||||
constants.put("SEND_ENDPOINT_TEXT_MESSAGE", BroadcastAction.Type.SEND_ENDPOINT_TEXT_MESSAGE.getAction());
|
||||
|
||||
return constants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||
* the specified {@link BaseReactView}'s listener.
|
||||
@@ -79,7 +105,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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,39 @@ 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());
|
||||
intentFilter.addAction(BroadcastEvent.Type.ENDPOINT_TEXT_MESSAGE_RECEIVED.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
124
css/_drawer.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -121,6 +121,7 @@ $poweredByZ: 100;
|
||||
$ringingZ: 300;
|
||||
$sideToolbarContainerZ: 300;
|
||||
$toolbarZ: 350;
|
||||
$drawerZ: 351;
|
||||
$tooltipsZ: 401;
|
||||
$dropdownMaskZ: 900;
|
||||
$dropdownZ: 901;
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
#remoteConnectionMessage {
|
||||
#remoteConnectionMessage,
|
||||
.watermark {
|
||||
z-index: $filmstripVideosZ + 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,5 +103,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'e2ee';
|
||||
@import 'responsive';
|
||||
@import 'connection-status';
|
||||
@import 'drawer';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -135,7 +135,6 @@
|
||||
.dial-in-copy {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 21px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
19
debian/jitsi-meet-prosody.postinst
vendored
19
debian/jitsi-meet-prosody.postinst
vendored
@@ -60,15 +60,6 @@ case "$1" in
|
||||
JICOFO_AUTH_PASSWORD="$RET"
|
||||
fi
|
||||
|
||||
db_get jicofo/jicofosecret
|
||||
if [ -z "$RET" ] ; then
|
||||
# if secret is missing generate it, and store it
|
||||
JICOFO_SECRET=`generateRandomPassword`
|
||||
db_set jicofo/jicofosecret "$JICOFO_SECRET"
|
||||
else
|
||||
JICOFO_SECRET="$RET"
|
||||
fi
|
||||
|
||||
JICOFO_AUTH_DOMAIN="auth.$JVB_HOSTNAME"
|
||||
|
||||
# detect dpkg-reconfigure, just delete old links
|
||||
@@ -107,7 +98,6 @@ case "$1" in
|
||||
mkdir -p /etc/prosody/conf.d/
|
||||
cp /usr/share/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusSecret/$JICOFO_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/focusUser/$JICOFO_AUTH_USER/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/__turnSecret__/$TURN_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
if [ ! -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua ]; then
|
||||
@@ -150,7 +140,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
|
||||
|
||||
|
||||
5
debian/jitsi-meet-prosody.templates
vendored
5
debian/jitsi-meet-prosody.templates
vendored
@@ -24,11 +24,6 @@ Type: password
|
||||
_Description: Jicofo user password:
|
||||
The secret used to connect to xmpp server as jicofo user.
|
||||
|
||||
Template: jicofo/jicofosecret
|
||||
Type: password
|
||||
_Description: Jicofo Component secret:
|
||||
The secret used to connect to xmpp server as component
|
||||
|
||||
Template: jitsi-meet-prosody/turn-secret
|
||||
Type: string
|
||||
_Description: The turn server secret
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -99,9 +99,26 @@
|
||||
#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"]);
|
||||
}
|
||||
|
||||
- (void)endpointTextMessageReceived:(NSDictionary *)data; {
|
||||
NSLog(@"%@%@", @"Endpoint text message received: ", data);
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (void)terminate {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
25
ios/sdk/src/ExternalAPI.h
Normal file
25
ios/sdk/src/ExternalAPI.h
Normal file
@@ -0,0 +1,25 @@
|
||||
/* 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;
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
|
||||
@end
|
||||
@@ -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,26 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTBridgeModule.h>
|
||||
|
||||
#import "ExternalAPI.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
|
||||
@interface ExternalAPI : NSObject<RCTBridgeModule>
|
||||
@end
|
||||
// Events
|
||||
static NSString * const hangUpAction = @"org.jitsi.meet.HANG_UP";
|
||||
static NSString * const setAudioMutedAction = @"org.jitsi.meet.SET_AUDIO_MUTED";
|
||||
static NSString * const sendEndpointTextMessageAction = @"org.jitsi.meet.SEND_ENDPOINT_TEXT_MESSAGE";
|
||||
|
||||
@implementation ExternalAPI
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
- (NSDictionary *)constantsToExport {
|
||||
return @{
|
||||
@"HANG_UP": hangUpAction,
|
||||
@"SET_AUDIO_MUTED" : setAudioMutedAction,
|
||||
@"SEND_ENDPOINT_TEXT_MESSAGE": sendEndpointTextMessageAction
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Make sure all methods in this module are invoked on the main/UI thread.
|
||||
*/
|
||||
@@ -32,6 +41,14 @@ RCT_EXPORT_MODULE();
|
||||
return dispatch_get_main_queue();
|
||||
}
|
||||
|
||||
+ (BOOL)requiresMainQueueSetup {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[ hangUpAction, setAudioMutedAction, sendEndpointTextMessageAction ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on JavaScript to the view's delegate.
|
||||
*
|
||||
@@ -87,4 +104,23 @@ RCT_EXPORT_METHOD(sendEvent:(NSString *)name
|
||||
return methodName;
|
||||
}
|
||||
|
||||
- (void)sendHangUp {
|
||||
[self sendEventWithName:hangUpAction body:nil];
|
||||
}
|
||||
|
||||
- (void)sendSetAudioMuted:(BOOL)muted {
|
||||
NSDictionary *data = @{ @"muted": [NSNumber numberWithBool:muted]};
|
||||
|
||||
[self sendEventWithName:setAudioMutedAction body:data];
|
||||
}
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
|
||||
NSDictionary *data = @{
|
||||
@"to": to,
|
||||
@"message": message
|
||||
};
|
||||
|
||||
[self sendEventWithName:sendEndpointTextMessageAction body:data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
|
||||
#import "ExternalAPI.h"
|
||||
#import "JitsiMeet.h"
|
||||
|
||||
@interface JitsiMeet ()
|
||||
|
||||
- (NSDictionary *)getDefaultProps;
|
||||
- (RCTBridge *)getReactBridge;
|
||||
- (ExternalAPI *)getExternalAPI;
|
||||
|
||||
@end
|
||||
|
||||
@@ -213,4 +213,8 @@
|
||||
return _bridgeWrapper.bridge;
|
||||
}
|
||||
|
||||
- (ExternalAPI *)getExternalAPI {
|
||||
return [_bridgeWrapper.bridge moduleForClass:ExternalAPI.class];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,4 +37,10 @@
|
||||
*/
|
||||
- (void)leave;
|
||||
|
||||
- (void)hangUp;
|
||||
|
||||
- (void)setAudioMuted:(BOOL)muted;
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
|
||||
|
||||
@end
|
||||
|
||||
@@ -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,21 @@ static void initializeViewsMap() {
|
||||
[self setProps:@{}];
|
||||
}
|
||||
|
||||
- (void)hangUp {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendHangUp];
|
||||
}
|
||||
|
||||
- (void)setAudioMuted:(BOOL)muted {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendSetAudioMuted:muted];
|
||||
}
|
||||
|
||||
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message {
|
||||
ExternalAPI *externalAPI = [[JitsiMeet sharedInstance] getExternalAPI];
|
||||
[externalAPI sendEndpointTextMessage:to :message];
|
||||
}
|
||||
|
||||
#pragma mark Private methods
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,32 @@
|
||||
*/
|
||||
- (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;
|
||||
|
||||
/**
|
||||
* Called when an endpoint text message is received.
|
||||
*
|
||||
* The `data` dictionary contains a `senderId` key with the participantId of the sender and a 'message' key with the content.
|
||||
*/
|
||||
- (void)endpointTextMessageReceived:(NSDictionary *)data;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,28 +8,28 @@
|
||||
"countryNotSupported": "Wir unterstützen dieses Land noch nicht.",
|
||||
"countryReminder": "Telefonnummer nicht in den USA? Bitte sicherstellen, dass die Telefonnummer mit dem Ländercode beginnt.",
|
||||
"defaultEmail": "Ihre Standard-E-Mail",
|
||||
"disabled": "Sie können keine Teilnehmer einladen.",
|
||||
"failedToAdd": "Fehler beim Hinzufügen von Teilnehmern",
|
||||
"disabled": "Sie können keine Personen einladen.",
|
||||
"failedToAdd": "Fehler beim Hinzufügen von Personen",
|
||||
"footerText": "Abgehender Ruf ist deaktiviert.",
|
||||
"googleEmail": "Google-E-Mail",
|
||||
"inviteMoreHeader": "Sie sind alleine in der Sitzung",
|
||||
"inviteMoreMailSubject": "An {{appName}} Meeting teilnehmen",
|
||||
"inviteMorePrompt": "Mehr Leute einladen",
|
||||
"linkCopied": "Link in die Zwischenablage kopiert",
|
||||
"loading": "Suche nach Teilnehmern und Telefonnummern",
|
||||
"loading": "Suche nach Personen und Telefonnummern",
|
||||
"loadingNumber": "Telefonnummer wird überprüft",
|
||||
"loadingPeople": "Suche nach einzuladenden Teilnehmern",
|
||||
"loadingPeople": "Suche nach einzuladenden Personen",
|
||||
"noResults": "Keine passenden Ergebnisse",
|
||||
"noValidNumbers": "Telefonnummer eingeben",
|
||||
"outlookEmail": "Outlook-E-Mail",
|
||||
"searchNumbers": "Telefonnummern hinzufügen",
|
||||
"searchPeople": "Nach Teilnehmern suchen",
|
||||
"searchPeopleAndNumbers": "Nach Teilnehmen suchen oder deren Telefonnummern hinzufügen",
|
||||
"searchPeople": "Nach Personen suchen",
|
||||
"searchPeopleAndNumbers": "Nach Personen suchen oder deren Telefonnummern hinzufügen",
|
||||
"shareInvite": "Einladung zur Versammlung teilen",
|
||||
"shareLink": "Teilen Sie den Konferenzlink, um andere einzuladen",
|
||||
"shareStream": "Den Livestreaminglink freigeben",
|
||||
"telephone": "Telefon: {{number}}",
|
||||
"title": "Teilnehmer zu dieser Konferenz einladen",
|
||||
"title": "Personen zu dieser Konferenz einladen",
|
||||
"yahooEmail": "Yahoo-E-Mail"
|
||||
},
|
||||
"audioDevices": {
|
||||
@@ -128,7 +128,7 @@
|
||||
"remoteport_plural": "Entfernte Ports:",
|
||||
"resolution": "Auflösung:",
|
||||
"savelogs": "Logs speichern",
|
||||
"participant_id": "Teilnehmer-ID:",
|
||||
"participant_id": "Personen-ID:",
|
||||
"status": "Verbindung:",
|
||||
"transport": "Protokoll:",
|
||||
"transport_plural": "Protokolle:",
|
||||
@@ -171,7 +171,7 @@
|
||||
},
|
||||
"add": "Hinzufügen",
|
||||
"allow": "Erlauben",
|
||||
"alreadySharedVideoMsg": "Ein anderer Teilnehmer gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
|
||||
"alreadySharedVideoMsg": "Eine andere Person gibt bereits ein Video weiter. Bei dieser Konferenz ist jeweils nur ein geteiltes Video möglich.",
|
||||
"alreadySharedVideoTitle": "Nur ein geteiltes Video gleichzeitig",
|
||||
"applicationWindow": "Anwendungsfenster",
|
||||
"Back": "Zurück",
|
||||
@@ -179,7 +179,7 @@
|
||||
"cameraNotFoundError": "Kamera nicht gefunden.",
|
||||
"cameraNotSendingData": "Die Kamera ist nicht verfügbar. Bitte prüfen, ob eine andere Applikation die Kamera verwendet, eine andere Kamera vom Einstellungs-Menü auswählen oder die Applikation neu laden.",
|
||||
"cameraNotSendingDataTitle": "Zugriff auf Kamera nicht möglich",
|
||||
"cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
|
||||
"cameraPermissionDeniedError": "Die Berechtigung zur Verwendung der Kamera wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Personen können Sie nicht sehen. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
|
||||
"cameraUnknownError": "Die Kamera kann aus einem unbekannten Grund nicht verwendet werden.",
|
||||
"cameraUnsupportedResolutionError": "Die Kamera unterstützt die erforderliche Auflösung nicht.",
|
||||
"Cancel": "Abbrechen",
|
||||
@@ -202,21 +202,21 @@
|
||||
"done": "Fertig",
|
||||
"e2eeDescription": "Ende-zu-Ende-Verschlüsselung ist derzeit noch EXPERIMENTELL. Bitte beachten Sie, dass das Aktivieren der Ende-zu-Ende-Verschlüsselung diverse serverseitige Funktionen deaktiviert: Aufnahmen, Livestreaming und Telefoneinwahl. Bitte beachten Sie außerdem, dass der Konferenz dann nur noch mit Browsern beigetreten werden kann, die Insertable Streams unterstützen.",
|
||||
"e2eeLabel": "Ende-zu-Ende-Verschlüsselung aktivieren",
|
||||
"e2eeWarning": "WARNUNG: Nicht alle Teilnehmer dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Teilnehmer nichts mehr sehen oder hören.",
|
||||
"e2eeWarning": "WARNUNG: Nicht alle Personen dieser Konferenz scheinen Ende-zu-Ende-Verschlüsselung zu unterstützen. Wenn Sie diese aktivieren, können die entsprechenden Personen nichts mehr sehen oder hören.",
|
||||
"enterDisplayName": "Bitte geben Sie hier Ihren Namen ein",
|
||||
"error": "Fehler",
|
||||
"gracefulShutdown": "Der Dienst steht momentan wegen Wartungsarbeiten nicht zur Verfügung. Bitte versuchen Sie es später noch einmal.",
|
||||
"grantModeratorDialog": "Möchten Sie diesen Teilnehmer wirklich zum Moderator machen?",
|
||||
"grantModeratorTitle": "Zum Moderator machen",
|
||||
"IamHost": "Ich bin der Organisator",
|
||||
"grantModeratorDialog": "Möchten Sie diesen Person wirklich zum Moderator:in machen?",
|
||||
"grantModeratorTitle": "Moderationsrechte vergeben",
|
||||
"IamHost": "Ich organisiere das Meeting",
|
||||
"incorrectRoomLockPassword": "Falsches Passwort",
|
||||
"incorrectPassword": "Benutzername oder Passwort ungültig",
|
||||
"incorrectPassword": "Name oder Passwort ungültig",
|
||||
"internalError": "Oh! Es hat etwas nicht funktioniert. Der folgende Fehler ist aufgetreten: {{error}}",
|
||||
"internalErrorTitle": "Interner Fehler",
|
||||
"kickMessage": "Sie können sich für mehr Details an {{participantDisplayName}} wenden.",
|
||||
"kickParticipantButton": "Entfernen",
|
||||
"kickParticipantDialog": "Wollen Sie diesen Teilnehmer wirklich entfernen?",
|
||||
"kickParticipantTitle": "Teilnehmer entfernen?",
|
||||
"kickParticipantDialog": "Wollen Sie diese Person wirklich entfernen?",
|
||||
"kickParticipantTitle": "Person entfernen?",
|
||||
"kickTitle": "Autsch! {{participantDisplayName}} hat Sie aus dem Meeting geworfen",
|
||||
"liveStreaming": "Livestreaming",
|
||||
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Während einer Aufnahme nicht möglich",
|
||||
@@ -227,13 +227,13 @@
|
||||
"lockTitle": "Sperren fehlgeschlagen",
|
||||
"logoutQuestion": "Sind Sie sicher, dass Sie sich abmelden und die Konferenz verlassen möchten?",
|
||||
"logoutTitle": "Abmelden",
|
||||
"maxUsersLimitReached": "Das Limit für die maximale Teilnehmerzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an den Besitzer des Meetings oder versuchen Sie es später noch einmal!",
|
||||
"maxUsersLimitReachedTitle": "Maximales Teilnehmerlimit erreicht",
|
||||
"maxUsersLimitReached": "Das Limit für die maximale Personenzahl ist erreicht. Die Konferenz ist voll. Bitte wenden Sie sich an die verwaltende Person des Meetings oder versuchen Sie es später noch einmal!",
|
||||
"maxUsersLimitReachedTitle": "Maximale Personenzahl erreicht",
|
||||
"micConstraintFailedError": "Ihr Mikrofon erfüllt die notwendigen Anforderungen nicht.",
|
||||
"micNotFoundError": "Mikrofon nicht gefunden.",
|
||||
"micNotSendingData": "Gehen Sie zu den Einstellungen Ihres Computers, um die Stummschaltung Ihres Mikrofons aufzuheben und seinen Pegel einzustellen",
|
||||
"micNotSendingDataTitle": "Ihr Mikrofon ist durch Ihre Systemeinstellungen stumm geschaltet",
|
||||
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Teilnehmer können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
|
||||
"micPermissionDeniedError": "Die Berechtigung zur Verwendung des Mikrofons wurde nicht erteilt. Sie können trotzdem an der Konferenz teilnehmen, aber die anderen Personen können Sie nicht hören. Verwenden Sie die Kamera-Schaltfläche in der Adressleiste, um die Berechtigungen zu erteilen.",
|
||||
"micUnknownError": "Das Mikrofon kann aus einem unbekannten Grund nicht verwendet werden.",
|
||||
"muteEveryoneElseDialog": "Einmal stummgeschaltet, können Sie deren Stummschaltung nicht mehr beenden, aber sie können ihre Stummschaltung jederzeit selbst beenden.",
|
||||
"muteEveryoneElseTitle": "Alle außer {{whom}} stummschalten?",
|
||||
@@ -241,12 +241,12 @@
|
||||
"muteEveryoneTitle": "Alle stummschalten?",
|
||||
"muteEveryoneSelf": "sich selbst",
|
||||
"muteEveryoneStartMuted": "Alle beginnen von jetzt an stummgeschaltet",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Teilnehmer nicht aufheben, aber ein Teilnehmer kann seine eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantBody": "Sie können die Stummschaltung anderer Personen nicht aufheben, aber eine Person kann ihre eigene Stummschaltung jederzeit beenden.",
|
||||
"muteParticipantButton": "Stummschalten",
|
||||
"muteParticipantDialog": "Wollen Sie diesen Teilnehmer wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, der Teilnehmer kann dies aber jederzeit selbst tun.",
|
||||
"muteParticipantTitle": "Teilnehmer stummschalten?",
|
||||
"muteParticipantDialog": "Wollen Sie diese Person wirklich stummschalten? Sie können die Stummschaltung nicht wieder aufheben, die Person kann dies aber jederzeit selbst tun.",
|
||||
"muteParticipantTitle": "Person stummschalten?",
|
||||
"Ok": "OK",
|
||||
"passwordLabel": "Dieses Meeting wurde von einem Teilnehmer gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
|
||||
"passwordLabel": "Dieses Meeting wurde gesichert. Bitte geben Sie das $t(lockRoomPasswordUppercase) ein, um dem Meeting beizutreten.",
|
||||
"passwordNotSupported": "Das Festlegen eines Konferenzpassworts wird nicht unterstützt.",
|
||||
"passwordNotSupportedTitle": "$t(lockRoomPasswordUppercase) nicht unterstützt",
|
||||
"passwordRequired": "$t(lockRoomPasswordUppercase) erforderlich",
|
||||
@@ -303,10 +303,10 @@
|
||||
"tokenAuthFailedTitle": "Authentifizierung fehlgeschlagen",
|
||||
"transcribing": "Wird transkribiert",
|
||||
"unlockRoom": "Konferenz$t(lockRoomPassword) entfernen",
|
||||
"userPassword": "Benutzerpasswort",
|
||||
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, authentifizieren Sie sich. Warten Sie andernfalls, bis der Organisator erscheint.",
|
||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Wenn Sie der Organisator sind, drücken Sie zum Authentifizieren auf OK. Warten Sie andernfalls, bis der Organisator erscheint.",
|
||||
"WaitingForHost": "Warten auf den Organisator …",
|
||||
"userPassword": "Passwort",
|
||||
"WaitForHostMsg": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls sie die Konferenz organisieren, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitForHostMsgWOk": "Die Konferenz <b>{{room}}</b> wurde noch nicht gestartet. Falls sie die Konferenz organisieren, authentifizieren Sie sich bitte. Warten Sie andernfalls, bis die Konferenz gestartet wird.",
|
||||
"WaitingForHost": "Warten auf den Beginn der Konferenz …",
|
||||
"Yes": "Ja",
|
||||
"yourEntireScreen": "Ganzer Bildschirm"
|
||||
},
|
||||
@@ -317,7 +317,7 @@
|
||||
"title": "Freigegebenes Dokument"
|
||||
},
|
||||
"e2ee": {
|
||||
"labelToolTip": "Audio- und Videodaten dieser Unterhaltung sind jetzt zwischen den Teilnehmern verschlüsselt"
|
||||
"labelToolTip": "Audio- und Videodaten dieser Unterhaltung sind jetzt zwischen den Personen verschlüsselt"
|
||||
},
|
||||
"embedMeeting": {
|
||||
"title": "Diese Konferenz einbetten"
|
||||
@@ -369,11 +369,11 @@
|
||||
"label": "Einwahlinformationen"
|
||||
},
|
||||
"inviteDialog": {
|
||||
"alertText": "Die Einladung einiger Teilnehmer ist fehlgeschlagen.",
|
||||
"alertText": "Die Einladung einiger Personen ist fehlgeschlagen.",
|
||||
"header": "Einladen",
|
||||
"searchCallOnlyPlaceholder": "Telefonnummer eingeben",
|
||||
"searchPeopleOnlyPlaceholder": "Nach Teilnehmern suchen",
|
||||
"searchPlaceholder": "Teilnehmer oder Telefonnummer",
|
||||
"searchPeopleOnlyPlaceholder": "Nach Personen suchen",
|
||||
"searchPlaceholder": "Personen oder Telefonnummer",
|
||||
"send": "Senden"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
@@ -384,14 +384,14 @@
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"focusLocal": "Lokales Video fokussieren",
|
||||
"focusRemote": "Auf das Video eines anderen Teilnehmers fokussieren",
|
||||
"focusRemote": "Auf das Video einer anderen Person fokussieren",
|
||||
"fullScreen": "Vollbildmodus aktivieren oder deaktivieren",
|
||||
"keyboardShortcuts": "Tastenkürzel",
|
||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein- oder ausblenden",
|
||||
"mute": "Stummschaltung aktivieren oder deaktivieren",
|
||||
"pushToTalk": "Push-to-Talk (Sprechtaste)",
|
||||
"raiseHand": "Hand erheben",
|
||||
"showSpeakerStats": "Sprecherstatistik anzeigen",
|
||||
"showSpeakerStats": "Sprechstatistik anzeigen",
|
||||
"toggleChat": "Chat öffnen oder schließen",
|
||||
"toggleFilmstrip": "Video-Miniaturansichten ein- oder ausblenden",
|
||||
"toggleScreensharing": "Zwischen Kamera und Bildschirmfreigabe wechseln",
|
||||
@@ -449,14 +449,14 @@
|
||||
"me": "Ich",
|
||||
"messages": {
|
||||
"engaged": "Lokale Aufzeichnung ist aktiviert.",
|
||||
"finished": "Aufzeichnung der Sitzung {{token}} ist beendet. Senden Sie die aufgezeichnete Datei an den Moderator.",
|
||||
"finishedModerator": "Aufzeichnung der Sitzung {{token}} ist beendet. Die Aufzeichnung des lokalen Verlaufs wurde gespeichert. Bitten Sie die anderen Teilnehmer, ihre Aufzeichnungen zu übermitteln.",
|
||||
"notModerator": "Sie sind nicht der Moderator. Sie können die lokale Aufzeichnung nicht starten oder stoppen."
|
||||
"finished": "Aufzeichnung der Sitzung {{token}} ist beendet. Senden Sie die aufgezeichnete Datei an die Moderation.",
|
||||
"finishedModerator": "Aufzeichnung der Sitzung {{token}} ist beendet. Die Aufzeichnung des lokalen Verlaufs wurde gespeichert. Bitten Sie die anderen Personen, ihre Aufzeichnungen zu übermitteln.",
|
||||
"notModerator": "Sie moderieren nicht. Sie können die lokale Aufzeichnung nicht starten oder stoppen."
|
||||
},
|
||||
"moderator": "Moderator",
|
||||
"moderator": "Moderator:in",
|
||||
"no": "Nein",
|
||||
"participant": "Teilnehmer",
|
||||
"participantStats": "Teilnehmerstatistik",
|
||||
"participant": "Person",
|
||||
"participantStats": "Personenstatistik",
|
||||
"sessionToken": "Sitzungs-Token",
|
||||
"start": "Aufnahme starten",
|
||||
"stop": "Aufnahme stoppen",
|
||||
@@ -470,21 +470,21 @@
|
||||
"connectedThreePlusMembers": "{{name}} und {{count}} andere Personen nehmen am Meeting teil",
|
||||
"connectedTwoMembers": "{{first}} und {{second}} nehmen am Meeting teil",
|
||||
"disconnected": "getrennt",
|
||||
"focus": "Konferenz-Organisator",
|
||||
"focus": "Konferenz-Organisator:in",
|
||||
"focusFail": "{{component}} ist im Moment nicht verfügbar - wiederholen in {{ms}} Sekunden",
|
||||
"grantedTo": "Moderatorenrechte an {{to}} vergeben!",
|
||||
"grantedTo": "Moderationsrechte an {{to}} vergeben!",
|
||||
"invitedOneMember": "{{name}} wurde eingeladen",
|
||||
"invitedThreePlusMembers": "{{name}} und {{count}} andere wurden eingeladen",
|
||||
"invitedTwoMembers": "{{first}} und {{second}} wurden eingeladen",
|
||||
"kickParticipant": "{{kicked}} wurde von {{kicker}} ausgewiesen",
|
||||
"me": "Ich",
|
||||
"moderator": "Moderatorenrechte vergeben!",
|
||||
"moderator": "Moderationsrechte vergeben!",
|
||||
"muted": "Der Konferenz wurde stumm beigetreten.",
|
||||
"mutedTitle": "Stummschaltung aktiv!",
|
||||
"mutedRemotelyTitle": "Sie wurden von {{participantDisplayName}} stummgeschaltet!",
|
||||
"mutedRemotelyDescription": "Sie können jederzeit die Stummschaltung aufheben, wenn Sie bereit sind zu sprechen. Wenn Sie fertig sind, können Sie sich wieder stummschalten, um Geräusche vom Meeting fernzuhalten.",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einem anderen Teilnehmer entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einem anderen Teilnehmer gesetzt",
|
||||
"passwordRemovedRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person entfernt",
|
||||
"passwordSetRemotely": "$t(lockRoomPasswordUppercase) von einer anderen Person gesetzt",
|
||||
"raisedHand": "{{name}} möchte sprechen.",
|
||||
"somebody": "Jemand",
|
||||
"startSilentTitle": "Sie sind ohne Audioausgabe beigetreten!",
|
||||
@@ -500,7 +500,7 @@
|
||||
"oldElectronClientDescription2": "aktuelle Version",
|
||||
"oldElectronClientDescription3": "!"
|
||||
},
|
||||
"passwordSetRemotely": "von einem anderen Teilnehmer gesetzt",
|
||||
"passwordSetRemotely": "von einer anderen Person gesetzt",
|
||||
"passwordDigitsOnly": "Bis zu {{number}} Ziffern",
|
||||
"poweredby": "Betrieben von",
|
||||
"prejoin": {
|
||||
@@ -525,7 +525,7 @@
|
||||
"goodQuality": "Großartig! Ihre Bild- und Tonqualität sollte super sein.",
|
||||
"noMediaConnectivity": "Es konnte für diesen Test keine Medienverbindung hergestellt werden. Das wird gewöhnlich durch eine Firewall oder ein NAT ausgelöst.",
|
||||
"noVideo": "Ihr Bild wird wahrscheinlich eine schlechte Qualität haben.",
|
||||
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte den Entwickler der Webanwendung.",
|
||||
"undetectable": "Wenn Sie mit Ihrem Browser weiterhin Probleme in Konferenzen haben, sollten Sie die Verbindung und Funktion Ihrer Lautsprecher, Ihres Mikrofons und Ihrer Kamera überprüfen. Stellen Sie außerdem sicher, dass Ihr Browser die erforderlichen Rechte hat, auf das Mikrofon und die Kamera zuzugreifen, und dass Sie die neuste Browserversion installiert haben. Sollten Sie immer noch Probleme haben, kontaktieren Sie bitte die Entwickler:innen der Webanwendung.",
|
||||
"veryPoorConnection": "Ihre Konferenzqualität wird wahrscheinlich sehr schlecht sein.",
|
||||
"videoFreezing": "Ihr Bild wird wahrscheinlich einfrieren, schwarz werden und eine geringe Auflösung haben.",
|
||||
"videoHighQuality": "Ihr Bild sollte sehr gut aussehen.",
|
||||
@@ -594,7 +594,7 @@
|
||||
"expandedOn": "Das Meeting wird momentan aufgezeichnet.",
|
||||
"expandedPending": "Aufzeichnung wird gestartet…",
|
||||
"failedToStart": "Die Aufnahme konnte nicht gestartet werden",
|
||||
"fileSharingdescription": "Aufzeichnung mit Konferenzteilnehmer teilen",
|
||||
"fileSharingdescription": "Aufzeichnung mit den Personen der Konferenz teilen",
|
||||
"live": "LIVE",
|
||||
"loggedIn": "Als {{userName}} angemeldet",
|
||||
"off": "Aufnahme gestoppt",
|
||||
@@ -614,9 +614,9 @@
|
||||
"pullToRefresh": "Ziehen, um zu aktualisieren"
|
||||
},
|
||||
"security": {
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Moderatoren können die Konferenz mit einem Passwort sichern. Teilnehmer müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Teilnehmer könnten Ihrer Konferenz beitreten",
|
||||
"about": "Sie können Ihre Konferenz mit einem Passwort sichern. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"aboutReadOnly": "Mit Moderationsrechten kann die Konferenz mit einem Passwort gesichert werden. Personen müssen dieses eingeben, bevor sie an der Sitzung teilnehmen dürfen.",
|
||||
"insecureRoomNameWarning": "Der Raumname ist unsicher. Unerwünschte Personen könnten Ihrer Konferenz beitreten",
|
||||
"securityOptions": "Sicherheitsoptionen"
|
||||
},
|
||||
"settings": {
|
||||
@@ -628,11 +628,11 @@
|
||||
"title": "Kalender"
|
||||
},
|
||||
"devices": "Geräte",
|
||||
"followMe": "Follow-me für alle Teilnehmer",
|
||||
"followMe": "Follow-me für alle Personen",
|
||||
"language": "Sprache",
|
||||
"loggedIn": "Als {{name}} angemeldet",
|
||||
"microphones": "Mikrofon",
|
||||
"moderator": "Moderator",
|
||||
"moderator": "Moderator:in",
|
||||
"more": "Mehr",
|
||||
"name": "Name",
|
||||
"noDevice": "Kein",
|
||||
@@ -640,8 +640,8 @@
|
||||
"selectCamera": "Kamera",
|
||||
"selectMic": "Mikrofon",
|
||||
"speakers": "Lautsprecher",
|
||||
"startAudioMuted": "Alle Teilnehmer treten stumm geschaltet bei",
|
||||
"startVideoMuted": "Alle Teilnehmer treten ohne Video bei",
|
||||
"startAudioMuted": "Alle Personen treten stumm geschaltet bei",
|
||||
"startVideoMuted": "Alle Personen treten ohne Video bei",
|
||||
"title": "Einstellungen"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -670,14 +670,14 @@
|
||||
"dialInfoText": "\n\n=====\n\nWollen Sie sich nur auf Ihrem Telefon einwählen?\n\n{{defaultDialInNumber}}Klicken Sie auf diesen Link, um die eingewählten Telefonnummern für dieses Meeting zu sehen\n{{dialInfoPageUrl}}",
|
||||
"mainText": "Klicken Sie auf den folgenden Link, um dem Meeting beizutreten:\n{{roomUrl}}"
|
||||
},
|
||||
"speaker": "Sprecher",
|
||||
"speaker": "Sprecher:in",
|
||||
"speakerStats": {
|
||||
"hours": "{{count}}h",
|
||||
"minutes": "{{count}}m",
|
||||
"name": "Name",
|
||||
"seconds": "{{count}}s",
|
||||
"speakerStats": "Sprecherstatistik",
|
||||
"speakerTime": "Sprecherzeit"
|
||||
"speakerStats": "Sprechstatistik",
|
||||
"speakerTime": "Sprechzeit"
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": " ",
|
||||
@@ -700,11 +700,11 @@
|
||||
"embedMeeting": "Konferenz einbetten",
|
||||
"feedback": "Feedback hinterlassen",
|
||||
"fullScreen": "Vollbildmodus ein-/ausschalten",
|
||||
"grantModerator": "Zum Moderator machen",
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"hangup": "Anruf beenden",
|
||||
"help": "Hilfe",
|
||||
"invite": "Teilnehmer einladen",
|
||||
"kick": "Teilnehmer entfernen",
|
||||
"invite": "Person einladen",
|
||||
"kick": "Person entfernen",
|
||||
"lobbyButton": "Lobbymodus ein-/ausschalten",
|
||||
"localRecording": "Lokale Aufzeichnungssteuerelemente ein-/ausschalten",
|
||||
"lockRoom": "Konferenzpasswort ein-/ausschalten",
|
||||
@@ -718,7 +718,7 @@
|
||||
"profile": "Profil bearbeiten",
|
||||
"raiseHand": "„Melden“ ein-/ausschalten",
|
||||
"recording": "Aufzeichnung ein-/ausschalten",
|
||||
"remoteMute": "Teilnehmer stummschalten",
|
||||
"remoteMute": "Personen stummschalten",
|
||||
"security": "Sicherheitsoptionen",
|
||||
"Settings": "Einstellungen ein-/ausschalten",
|
||||
"sharedvideo": "YouTube-Videofreigabe ein-/ausschalten",
|
||||
@@ -726,14 +726,14 @@
|
||||
"shareYourScreen": "Bildschirmfreigabe ein-/ausschalten",
|
||||
"shortcuts": "Tastenkombinationen ein-/ausblenden",
|
||||
"show": "Im Vordergrund anzeigen",
|
||||
"speakerStats": "Sprecherstatistik ein-/ausblenden",
|
||||
"speakerStats": "Sprechstatistik ein-/ausblenden",
|
||||
"tileView": "Kachelansicht ein-/ausschalten",
|
||||
"toggleCamera": "Kamera wechseln",
|
||||
"toggleFilmstrip": "Miniaturansichten ein-/ausschalten",
|
||||
"videomute": "„Video stummschalten“ ein-/ausschalten",
|
||||
"videoblur": "Video-Unschärfe ein-/ausschalten"
|
||||
},
|
||||
"addPeople": "Teilnehmer zur Konferenz hinzufügen",
|
||||
"addPeople": "Personen zur Konferenz hinzufügen",
|
||||
"audioOnlyOff": "Modus „Nur Audio“ deaktivieren",
|
||||
"audioOnlyOn": "Modus „Nur Audio“ aktivieren",
|
||||
"audioRoute": "Audiogerät auswählen",
|
||||
@@ -753,7 +753,7 @@
|
||||
"feedback": "Feedback hinterlassen",
|
||||
"hangup": "Verlassen",
|
||||
"help": "Hilfe",
|
||||
"invite": "Teilnehmer einladen",
|
||||
"invite": "Personen einladen",
|
||||
"lobbyButtonDisable": "Lobbymodus deaktivieren",
|
||||
"lobbyButtonEnable": "Lobbymodus aktivieren",
|
||||
"login": "Anmelden",
|
||||
@@ -781,7 +781,7 @@
|
||||
"sharedvideo": "YouTube-Video teilen",
|
||||
"shareRoom": "Person einladen",
|
||||
"shortcuts": "Tastenkürzel anzeigen",
|
||||
"speakerStats": "Sprecherstatistik",
|
||||
"speakerStats": "Sprechstatistik",
|
||||
"startScreenSharing": "Bildschirmfreigabe starten",
|
||||
"startSubtitles": "Untertitel einschalten",
|
||||
"stopScreenSharing": "Bildschirmfreigabe stoppen",
|
||||
@@ -850,14 +850,14 @@
|
||||
"domute": "Stummschalten",
|
||||
"domuteOthers": "Alle anderen stummschalten",
|
||||
"flip": "Spiegeln",
|
||||
"grantModerator": "Zum Moderator machen",
|
||||
"grantModerator": "Moderationsrechte vergeben",
|
||||
"kick": "Hinauswerfen",
|
||||
"moderator": "Moderator",
|
||||
"mute": "Teilnehmer ist stumm geschaltet",
|
||||
"moderator": "Moderator:in",
|
||||
"mute": "Person ist stumm geschaltet",
|
||||
"muted": "Stummgeschaltet",
|
||||
"remoteControl": "Fernsteuerung",
|
||||
"show": "Im Vordergrund anzeigen",
|
||||
"videomute": "Teilnehmer hat die Kamera angehalten"
|
||||
"videomute": "Person hat die Kamera angehalten"
|
||||
},
|
||||
"welcomepage": {
|
||||
"accessibilityLabel": {
|
||||
@@ -878,7 +878,7 @@
|
||||
"goSmall": "Los",
|
||||
"info": "Einwahlinformationen",
|
||||
"join": "ERSTELLEN / BEITRETEN",
|
||||
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, unter der Sie der einzige Moderator sind.",
|
||||
"moderatedMessage": "Oder <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservieren Sie sich eine Konferenz-URL</a>, die nur Sie moderieren.",
|
||||
"privacy": "Datenschutz",
|
||||
"recentList": "Verlauf",
|
||||
"recentListDelete": "Eintrag löschen",
|
||||
@@ -886,7 +886,7 @@
|
||||
"reducedUIText": "Willkommen bei {{app}}!",
|
||||
"roomNameAllowedChars": "Der Konferenzname sollte keines der folgenden Zeichen enthalten: ?, &, :, ', \", %, #.",
|
||||
"roomname": "Konferenzname eingeben",
|
||||
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Teilnehmern übermittelt werden, damit diese der gleichen Konferenz beitreten.",
|
||||
"roomnameHint": "Name oder URL der Konferenz, der Sie beitreten möchten. Sie können einen Namen erfinden, er muss nur den anderen Personen übermittelt werden, damit diese der gleichen Konferenz beitreten.",
|
||||
"sendFeedback": "Feedback senden",
|
||||
"terms": "AGB",
|
||||
"title": "Sichere, voll funktionale und komplett kostenlose Videokonferenzen"
|
||||
@@ -899,7 +899,7 @@
|
||||
"header": "Hilfecenter"
|
||||
},
|
||||
"lobby": {
|
||||
"knockingParticipantList": "Liste anklopfender Teilnehmer",
|
||||
"knockingParticipantList": "Liste anklopfender Personen",
|
||||
"allow": "Annehmen",
|
||||
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
|
||||
"dialogTitle": "Lobbymodus",
|
||||
@@ -908,13 +908,13 @@
|
||||
"emailField": "E-Mail-Adresse eingeben",
|
||||
"enableDialogPasswordField": "Passwort setzen (optional)",
|
||||
"enableDialogSubmit": "Aktivieren",
|
||||
"enableDialogText": "Mit dem Lobbymodus schützen Sie Ihre Konferenz, da nur von einem Moderator angenommene Teilnehmer beitreten können.",
|
||||
"enableDialogText": "Mit dem Lobbymodus schützen Sie Ihre Konferenz, damit der Beitritt von Ihnen moderiert werden kann.",
|
||||
"enterPasswordButton": "Konferenzpasswort eingeben",
|
||||
"enterPasswordTitle": "Passwort zum Beitreten benutzen",
|
||||
"invalidPassword": "Ungültiges Passwort",
|
||||
"joiningMessage": "Sie treten der Konferenz bei, sobald jemand Ihre Anfrage annimmt.",
|
||||
"joinWithPasswordMessage": "Beitrittsversuch mit Passwort, bitte warten …",
|
||||
"joinRejectedMessage": "Ihr Beitrittsanfrage wurde von einem Moderator abgelehnt.",
|
||||
"joinRejectedMessage": "Ihre Beitrittsanfrage wurde von der Moderation abgelehnt.",
|
||||
"joinTitle": "Konferenz beitreten",
|
||||
"joiningTitle": "Beitritt anfragen …",
|
||||
"joiningWithPasswordTitle": "Mit Passwort beitreten …",
|
||||
|
||||
@@ -602,6 +602,7 @@
|
||||
"rec": "REC",
|
||||
"serviceDescription": "La tua registrazione verrà salvata dal servizio di registrazione che hai scelto",
|
||||
"serviceName": "Servizio di registrazione",
|
||||
"serviceDescriptionCloud": "Registrazione in rete",
|
||||
"signIn": "Entra",
|
||||
"signOut": "Esci",
|
||||
"unavailable": "Ops! Il {{serviceName}} non è al momento disponibile. Stiamo lavorando per risolvere il problema. Riprova più tardi.",
|
||||
@@ -628,6 +629,7 @@
|
||||
"followMe": "Tutti mi seguono",
|
||||
"language": "Lingua",
|
||||
"loggedIn": "Connesso come {{name}}",
|
||||
"microphones": "Microfoni",
|
||||
"moderator": "Moderatore",
|
||||
"more": "Altro",
|
||||
"name": "Nome",
|
||||
@@ -635,6 +637,7 @@
|
||||
"selectAudioOutput": "Uscita audio",
|
||||
"selectCamera": "Videocamera",
|
||||
"selectMic": "Microfono",
|
||||
"speakers": "Altoparlanti",
|
||||
"startAudioMuted": "Tutti cominciano con il microfono disattivato",
|
||||
"startVideoMuted": "Tutti cominciano con il video disattivato",
|
||||
"title": "Impostazioni"
|
||||
@@ -841,6 +844,7 @@
|
||||
"standardDefinition": "Definizione standard"
|
||||
},
|
||||
"videothumbnail": {
|
||||
"connectionInfo": "Info connessione",
|
||||
"domute": "Disattiva audio",
|
||||
"domuteOthers": "Zittisci tutti gli altri",
|
||||
"flip": "Rifletti",
|
||||
|
||||
@@ -546,8 +546,8 @@
|
||||
"errorValidation": "Validação do número falhou",
|
||||
"iWantToDialIn": "Eu quere discar",
|
||||
"joinAudioByPhone": "Participar com o áudio via ligação",
|
||||
"joinMeeting": "Particar da reunião",
|
||||
"joinWithoutAudio": "Particar sem áudio",
|
||||
"joinMeeting": "Participar da reunião",
|
||||
"joinWithoutAudio": "Participar sem áudio",
|
||||
"initiated": "Chamada iniciada",
|
||||
"linkCopied": "Link copiado para a área de transferência",
|
||||
"lookGood": "Seu microfone está funcionando corretamente",
|
||||
|
||||
@@ -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.
|
||||
|
||||
12
modules/API/external/external_api.js
vendored
12
modules/API/external/external_api.js
vendored
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}%`
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -3325,9 +3325,9 @@
|
||||
}
|
||||
},
|
||||
"@jitsi/js-utils": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.3.tgz",
|
||||
"integrity": "sha512-m6mZz7R716mHP21lTKQffyM0nNFu3Fe/EHCaOVLFY/vdPsaUl9DhypJqtPIYzRUfPnmnugdaxcxrUeSZQXQzVA==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@jitsi/js-utils/-/js-utils-1.0.5.tgz",
|
||||
"integrity": "sha512-1APQyuqQaYDR+W7cdgzsaBo6x8dpF8sfelcBf3ngNU3Jd+DzuuwUvCMTbr2+cCuy6w59ZAuQ7e2ixCnnOXOW4Q==",
|
||||
"requires": {
|
||||
"bowser": "2.7.0",
|
||||
"js-md5": "0.7.3"
|
||||
@@ -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#94ac35ae818093896e639e74f5fc389b488206a0",
|
||||
"from": "github:jitsi/lib-jitsi-meet#94ac35ae818093896e639e74f5fc389b488206a0",
|
||||
"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",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@atlaskit/theme": "7.0.2",
|
||||
"@atlaskit/toggle": "5.0.14",
|
||||
"@atlaskit/tooltip": "12.1.13",
|
||||
"@jitsi/js-utils": "1.0.3",
|
||||
"@jitsi/js-utils": "1.0.5",
|
||||
"@microsoft/microsoft-graph-client": "1.1.0",
|
||||
"@react-native-async-storage/async-storage": "1.13.2",
|
||||
"@react-native-community/google-signin": "3.0.1",
|
||||
@@ -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#94ac35ae818093896e639e74f5fc389b488206a0",
|
||||
"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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -90,6 +90,7 @@ export default [
|
||||
'disableRemoteControl',
|
||||
'disableRemoteMute',
|
||||
'disableRtx',
|
||||
'disableShortcuts',
|
||||
'disableSimulcast',
|
||||
'disableThirdPartyRequests',
|
||||
'disableTileView',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils/jitsi-local-storage';
|
||||
|
||||
import { browser } from '../lib-jitsi-meet';
|
||||
import { parseURLParams } from '../util/parseURLParams';
|
||||
|
||||
import logger from './logger';
|
||||
@@ -23,7 +24,7 @@ function onFakeLocalStorageChanged() {
|
||||
* @returns {void}
|
||||
*/
|
||||
function setupJitsiLocalStorage() {
|
||||
if (jitsiLocalStorage.isLocalStorageDisabled()) {
|
||||
if (jitsiLocalStorage.isLocalStorageDisabled() || browser.isSafari()) {
|
||||
const urlParams = parseURLParams(window.location);
|
||||
|
||||
try {
|
||||
|
||||
217
react/features/base/media/components/web/AudioTrack.js
Normal file
217
react/features/base/media/components/web/AudioTrack.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -35,6 +35,8 @@ import {
|
||||
setTrackMuted
|
||||
} from './functions';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
declare var APP: Object;
|
||||
|
||||
/**
|
||||
|
||||
24
react/features/base/tracks/subscriber.js
Normal file
24
react/features/base/tracks/subscriber.js
Normal 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);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,8 +1,3 @@
|
||||
/**
|
||||
* Action used to store the billing id.
|
||||
*/
|
||||
export const SET_BILLING_ID = 'SET_BILLING_ID';
|
||||
|
||||
/**
|
||||
* Action used to store the flag signaling the endpoint has been counted.
|
||||
*/
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { SET_BILLING_ID, SET_ENDPOINT_COUNTED } from './actionTypes';
|
||||
import { SET_ENDPOINT_COUNTED } from './actionTypes';
|
||||
import { extractVpaasTenantFromPath, getBillingId, sendCountRequest } from './functions';
|
||||
|
||||
/**
|
||||
* Sends a billing count request when needed.
|
||||
* If there is no billingId, it presists one first and sends the request after.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
@@ -20,12 +17,7 @@ export function countEndpoint() {
|
||||
const shouldSendRequest = Boolean(baseUrl && jwt && tenant);
|
||||
|
||||
if (shouldSendRequest) {
|
||||
let billingId = getBillingId();
|
||||
|
||||
if (!billingId) {
|
||||
billingId = uuid.v4();
|
||||
dispatch(setBillingId(billingId));
|
||||
}
|
||||
const billingId = getBillingId();
|
||||
|
||||
sendCountRequest({
|
||||
baseUrl,
|
||||
@@ -38,19 +30,6 @@ export function countEndpoint() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to set the user billing id.
|
||||
*
|
||||
* @param {string} value - The uid.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function setBillingId(value) {
|
||||
return {
|
||||
type: SET_BILLING_ID,
|
||||
value
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action used to mark the endpoint as counted.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import { jitsiLocalStorage } from '@jitsi/js-utils';
|
||||
import uuid from 'uuid';
|
||||
|
||||
import { BILLING_ID, VPAAS_TENANT_PREFIX } from './constants';
|
||||
import logger from './logger';
|
||||
@@ -72,20 +73,18 @@ export async function sendCountRequest({ baseUrl, billingId, jwt, tenant }: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stored billing id.
|
||||
* Returns the stored billing id (or generates a new one if none is present).
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getBillingId() {
|
||||
return jitsiLocalStorage.getItem(BILLING_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the billing id.
|
||||
*
|
||||
* @param {string} value - The id to be stored.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function setBillingId(value: string) {
|
||||
jitsiLocalStorage.setItem(BILLING_ID, value);
|
||||
let billingId = jitsiLocalStorage.getItem(BILLING_ID);
|
||||
|
||||
if (!billingId) {
|
||||
billingId = uuid.v4();
|
||||
jitsiLocalStorage.setItem(BILLING_ID, billingId);
|
||||
}
|
||||
|
||||
return billingId;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
|
||||
import { PARTICIPANT_JOINED } from '../base/participants/actionTypes';
|
||||
import { MiddlewareRegistry } from '../base/redux';
|
||||
|
||||
import { SET_BILLING_ID } from './actionTypes';
|
||||
import { countEndpoint } from './actions';
|
||||
import { isVpaasMeeting, extractVpaasTenantFromPath, setBillingId } from './functions';
|
||||
import { isVpaasMeeting, extractVpaasTenantFromPath } from './functions';
|
||||
|
||||
/**
|
||||
* The redux middleware for billing counter.
|
||||
@@ -21,11 +20,6 @@ MiddlewareRegistry.register(store => next => async action => {
|
||||
|
||||
break;
|
||||
}
|
||||
case SET_BILLING_ID: {
|
||||
setBillingId(action.value);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
const shouldCount = !store.getState()['features/billing-counter'].endpointCounted
|
||||
|
||||
@@ -247,6 +247,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMe
|
||||
body: message,
|
||||
id,
|
||||
nick: displayName,
|
||||
privateMessage,
|
||||
ts: timestamp
|
||||
});
|
||||
|
||||
|
||||
@@ -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 /> }
|
||||
|
||||
638
react/features/filmstrip/components/web/Thumbnail.js
Normal file
638
react/features/filmstrip/components/web/Thumbnail.js
Normal 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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
|
||||
@@ -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'> </span>
|
||||
<span className = 'info-value'>
|
||||
{ phoneNumber }
|
||||
</span>
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'info-value'>
|
||||
{ phoneNumber }
|
||||
<span className = 'conference-id'>
|
||||
<span className = 'info-label'>
|
||||
{ t('info.dialInConferenceID') }
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'info-value'>
|
||||
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'conference-id'>
|
||||
<span className = 'info-label'>
|
||||
{ t('info.dialInConferenceID') }
|
||||
</span>
|
||||
<span className = 'spacer'> </span>
|
||||
<span className = 'info-value'>
|
||||
{ `${_formatConferenceIDPin(conferenceID)}#` }
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<a
|
||||
className = 'dial-in-copy'
|
||||
onClick = { this._onCopyText }>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
5
react/features/mobile/external-api/logger.js
Normal file
5
react/features/mobile/external-api/logger.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// @flow
|
||||
|
||||
import { getLogger } from '../../base/logging/functions';
|
||||
|
||||
export default getLogger('features/mobile/external-api');
|
||||
@@ -1,5 +1,10 @@
|
||||
// @flow
|
||||
|
||||
import { NativeEventEmitter, NativeModules } from 'react-native';
|
||||
|
||||
import { ENDPOINT_TEXT_MESSAGE_NAME } from '../../../../modules/API/constants';
|
||||
import { appNavigate } from '../../app/actions';
|
||||
import { APP_WILL_MOUNT } from '../../base/app/actionTypes';
|
||||
import {
|
||||
CONFERENCE_FAILED,
|
||||
CONFERENCE_JOINED,
|
||||
@@ -8,6 +13,7 @@ import {
|
||||
JITSI_CONFERENCE_URL_KEY,
|
||||
SET_ROOM,
|
||||
forEachConference,
|
||||
getCurrentConference,
|
||||
isRoomValid
|
||||
} from '../../base/conference';
|
||||
import { LOAD_CONFIG_ERROR } from '../../base/config';
|
||||
@@ -18,10 +24,15 @@ import {
|
||||
JITSI_CONNECTION_URL_KEY,
|
||||
getURLWithoutParams
|
||||
} from '../../base/connection';
|
||||
import { JitsiConferenceEvents } from '../../base/lib-jitsi-meet';
|
||||
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';
|
||||
import logger from './logger';
|
||||
|
||||
/**
|
||||
* Event which will be emitted on the native side to indicate the conference
|
||||
@@ -29,6 +40,15 @@ import { sendEvent } from './functions';
|
||||
*/
|
||||
const CONFERENCE_TERMINATED = 'CONFERENCE_TERMINATED';
|
||||
|
||||
/**
|
||||
* Event which will be emitted on the native side to indicate a message was received
|
||||
* through the channel.
|
||||
*/
|
||||
const ENDPOINT_TEXT_MESSAGE_RECEIVED = 'ENDPOINT_TEXT_MESSAGE_RECEIVED';
|
||||
|
||||
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 +61,9 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
const { type } = action;
|
||||
|
||||
switch (type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_registerForNativeEvents(store);
|
||||
break;
|
||||
case CONFERENCE_FAILED: {
|
||||
const { error, ...data } = action;
|
||||
|
||||
@@ -62,12 +85,16 @@ MiddlewareRegistry.register(store => next => action => {
|
||||
break;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
case CONFERENCE_LEFT:
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
_sendConferenceEvent(store, action);
|
||||
break;
|
||||
|
||||
case CONFERENCE_JOINED:
|
||||
_sendConferenceEvent(store, action);
|
||||
_registerForEndpointTextMessages(store);
|
||||
break;
|
||||
|
||||
case CONNECTION_DISCONNECTED: {
|
||||
// FIXME: This is a hack. See the description in the JITSI_CONNECTION_CONFERENCE_KEY constant definition.
|
||||
// Check if this connection was attached to any conference. If it wasn't, fake a CONFERENCE_TERMINATED event.
|
||||
@@ -111,14 +138,98 @@ 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 {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _registerForNativeEvents({ getState, dispatch }) {
|
||||
eventEmitter.addListener(ExternalAPI.HANG_UP, () => {
|
||||
dispatch(appNavigate(undefined));
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.SET_AUDIO_MUTED, ({ muted }) => {
|
||||
dispatch(muteLocal(muted === 'true'));
|
||||
});
|
||||
|
||||
eventEmitter.addListener(ExternalAPI.SEND_ENDPOINT_TEXT_MESSAGE, ({ to, message }) => {
|
||||
const conference = getCurrentConference(getState());
|
||||
|
||||
try {
|
||||
conference && conference.sendEndpointMessage(to, {
|
||||
name: ENDPOINT_TEXT_MESSAGE_NAME,
|
||||
text: message
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn('Cannot send endpointMessage', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers for endpoint messages sent on conference data channel.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _registerForEndpointTextMessages(store) {
|
||||
const conference = getCurrentConference(store.getState());
|
||||
|
||||
conference && conference.on(
|
||||
JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
|
||||
(...args) => {
|
||||
if (args && args.length >= 2) {
|
||||
const [ sender, eventData ] = args;
|
||||
|
||||
if (eventData.name === ENDPOINT_TEXT_MESSAGE_NAME) {
|
||||
sendEvent(
|
||||
store,
|
||||
ENDPOINT_TEXT_MESSAGE_RECEIVED,
|
||||
/* data */ {
|
||||
message: eventData.text,
|
||||
senderId: sender._id
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@code String} representation of a specific error {@code Object}.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
90
react/features/toolbox/components/web/Drawer.js
Normal file
90
react/features/toolbox/components/web/Drawer.js
Normal 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;
|
||||
47
react/features/toolbox/components/web/DrawerPortal.js
Normal file
47
react/features/toolbox/components/web/DrawerPortal.js
Normal 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;
|
||||
@@ -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));
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user