Compare commits

...

43 Commits
4635 ... 4658

Author SHA1 Message Date
bgrozev
5f891fd060 debian: Do not read jicofosecret. (#8454) 2021-01-21 13:19:39 -06:00
chipechop
a39905883f Update main-it.json
Added 4 missing lines in italian translation
2021-01-20 12:01:54 -06:00
Saúl Ibarra Corretgé
fe78f104bc feat(android) set compile/target SDK versions to 30 2021-01-20 15:14:09 +01:00
Saúl Ibarra Corretgé
9c13603489 feat(android) update native dependencies 2021-01-20 15:14:09 +01:00
tmoldovan8x8
61037b982b feat(mobile) adds ability to send and receive text messages (#8425) 2021-01-20 14:06:45 +02:00
Jaya Allamsetty
df21ec6f04 chore(deps) lib-jitsi-meet@latest (#8437)
https://github.com/jitsi/lib-jitsi-meet/compare/...94ac35ae818093896e639e74f5fc389b488206a0
2021-01-19 12:45:33 -06:00
Vlad Piersec
23574e9edc fix(vpaas): Store billing id in parent lolcaStorage on Safari 2021-01-18 09:56:03 +02:00
Dennis Scheiba
ec3130af0e make translation gender neutral 2021-01-16 21:56:22 -06:00
Filipe dos Santos Gundim
3b692dc502 fix join label in pt-br 2021-01-16 21:36:00 -06:00
Дамян Минков
6689aa3700 feat: Detects shard changed when using websockets.
* feat: Detects shard changed when using websockets. (#1462)

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

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

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

87c6e37475...7896fc8b92
2021-01-12 08:22:14 -06:00
91 changed files with 2585 additions and 1279 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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;
}
}
}

View File

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

View File

@@ -0,0 +1,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;
}
}
}

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,9 @@ import com.facebook.react.module.annotations.ReactModule;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.HashMap;
import java.util.Map;
/**
* Module implementing an API for sending events from JavaScript to native code.
*/
@@ -35,6 +38,9 @@ class ExternalAPIModule
private static final String TAG = NAME;
private final BroadcastEmitter broadcastEmitter;
private final BroadcastReceiver broadcastReceiver;
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the app.
@@ -44,6 +50,9 @@ class ExternalAPIModule
*/
public ExternalAPIModule(ReactApplicationContext reactContext) {
super(reactContext);
broadcastEmitter = new BroadcastEmitter(reactContext);
broadcastReceiver = new BroadcastReceiver(reactContext);
}
/**
@@ -56,6 +65,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");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,13 +23,13 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import org.jitsi.meet.sdk.log.JitsiMeetLogger;
import java.util.Random;
/**
* Helper class for creating the ongoing notification which is used with
* {@link JitsiMeetOngoingConferenceService}. It allows the user to easily get back to the app
@@ -43,7 +43,6 @@ class OngoingNotification {
static final int NOTIFICATION_ID = new Random().nextInt(99999) + 10000;
static void createOngoingConferenceNotificationChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
@@ -56,7 +55,7 @@ class OngoingNotification {
}
NotificationManager notificationManager
= (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
= (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel
= notificationManager.getNotificationChannel(CHANNEL_ID);
@@ -73,7 +72,7 @@ class OngoingNotification {
notificationManager.createNotificationChannel(channel);
}
static Notification buildOngoingConferenceNotification() {
static Notification buildOngoingConferenceNotification(boolean isMuted) {
Context context = ReactInstanceManagerHolder.getCurrentActivity();
if (context == null) {
JitsiMeetLogger.w(TAG + " Cannot create notification: no current context");
@@ -83,12 +82,7 @@ class OngoingNotification {
Intent notificationIntent = new Intent(context, context.getClass());
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);
NotificationCompat.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new NotificationCompat.Builder(context, CHANNEL_ID);
} else {
builder = new NotificationCompat.Builder(context);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID);
builder
.setCategory(NotificationCompat.CATEGORY_CALL)
@@ -99,21 +93,27 @@ class OngoingNotification {
.setOngoing(true)
.setAutoCancel(false)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setUsesChronometer(true)
.setOnlyAlertOnce(true)
.setSmallIcon(context.getResources().getIdentifier("ic_notification", "drawable", context.getPackageName()));
// Add a "hang-up" action only if we are using ConnectionService.
if (AudioModeModule.useConnectionService()) {
Intent hangupIntent = new Intent(context, JitsiMeetOngoingConferenceService.class);
hangupIntent.setAction(JitsiMeetOngoingConferenceService.Actions.HANGUP);
PendingIntent hangupPendingIntent
= PendingIntent.getService(context, 0, hangupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Action hangupAction = new NotificationCompat.Action(0, "Hang up", hangupPendingIntent);
NotificationCompat.Action hangupAction = createAction(context, JitsiMeetOngoingConferenceService.Action.HANGUP, "Hang up");
builder.addAction(hangupAction);
}
JitsiMeetOngoingConferenceService.Action toggleAudioAction = isMuted
? JitsiMeetOngoingConferenceService.Action.UNMUTE : JitsiMeetOngoingConferenceService.Action.MUTE;
String toggleAudioTitle = isMuted ? "Unmute" : "Mute";
NotificationCompat.Action audioAction = createAction(context, toggleAudioAction, toggleAudioTitle);
builder.addAction(hangupAction);
builder.addAction(audioAction);
return builder.build();
}
private static NotificationCompat.Action createAction(Context context, JitsiMeetOngoingConferenceService.Action action, String title) {
Intent intent = new Intent(context, JitsiMeetOngoingConferenceService.class);
intent.setAction(action.getName());
PendingIntent pendingIntent
= PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
return new NotificationCompat.Action(0, title, pendingIntent);
}
}

View File

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

View File

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

View File

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

124
css/_drawer.scss Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -99,9 +99,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 {

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

25
ios/sdk/src/ExternalAPI.h Normal file
View 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

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,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

View File

@@ -16,11 +16,13 @@
#import <React/RCTBridge.h>
#import "ExternalAPI.h"
#import "JitsiMeet.h"
@interface JitsiMeet ()
- (NSDictionary *)getDefaultProps;
- (RCTBridge *)getReactBridge;
- (ExternalAPI *)getExternalAPI;
@end

View File

@@ -213,4 +213,8 @@
return _bridgeWrapper.bridge;
}
- (ExternalAPI *)getExternalAPI {
return [_bridgeWrapper.bridge moduleForClass:ExternalAPI.class];
}
@end

View File

@@ -37,4 +37,10 @@
*/
- (void)leave;
- (void)hangUp;
- (void)setAudioMuted:(BOOL)muted;
- (void)sendEndpointTextMessage:(NSString*)to :(NSString*)message;
@end

View File

@@ -17,6 +17,7 @@
#include <mach/mach_time.h>
#import "ExternalAPI.h"
#import "JitsiMeet+Private.h"
#import "JitsiMeetConferenceOptions+Private.h"
#import "JitsiMeetView+Private.h"
@@ -49,7 +50,6 @@ static NSString *const PiPEnabledFeatureFlag = @"pip.enabled";
* identifiers within the process).
*/
static NSMapTable<NSString *, JitsiMeetView *> *views;
/**
* This gets called automagically when the program starts.
*/
@@ -115,6 +115,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
/**

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,4 +55,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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

22
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
// @flow
import { getLogger } from '../../base/logging/functions';
export default getLogger('features/mobile/external-api');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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