Compare commits

...

65 Commits

Author SHA1 Message Date
Saúl Ibarra Corretgé
0e51cf15b1 fix(tile-view) avoid covering the logo
Reverts a8b2e6ffb3
2021-01-13 17:42:14 +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
Saúl Ibarra Corretgé
2763c2f5c9 fix(twa) update template
Set version to 1.0.0 with a very large version code so it's automatically kept
around when pushing new versions.

Additionally drop some no longer needed icon assets (bubblewrap did this).
2021-01-12 14:56:04 +01:00
Saúl Ibarra Corretgé
1ec8f70d55 fix(libre-build) skip spurious Firebase and GCM dependencies
Fixes: https://github.com/jitsi/jitsi-meet/issues/8353
2021-01-12 14:55:47 +01:00
Saúl Ibarra Corretgé
916208a5ff fix(libre-build) update react-native-device-info to 8.0.0
Will now work even if installreferrer or GCM are missing.
2021-01-12 14:55:47 +01:00
Mihai-Andrei Uscat
43e655b619 feat(chat): Improve responsiveness.
* Fix toolbox buttons not displaying properly when chat is open.
* Open chat in fullscreen dialog past custom thresholds when mobile/desktop toolbox would become unusable due to chat
* Remove mobile chat check when displaying toolbox
2021-01-12 15:24:55 +02:00
Avram Tudor
1ab0f1993a Merge pull request #8377 from jitsi/tavram/notifications
feat(external_api) allow notifications to be configured
2021-01-12 14:52:31 +02:00
Tudor-Ovidiu Avram
4cb7ebce70 feat(external_api) allow initial gUM requests to be disabled 2021-01-12 06:02:44 -06:00
hmuresan
0a5910f0b3 feat(external_api) set and cancel private chat through external API
- allow managing chat through API when chat button is not present on UI
2021-01-12 06:01:10 -06:00
Tudor-Ovidiu Avram
d91c546a1e feat(external_api) allow notifications to be configured 2021-01-12 11:22:53 +02:00
bgrozev
b6f7f8fba7 Remove the "focus" external component, use client_proxy instead. (#8381)
* feat: Add mod_client_proxy and mod_roster_command.

Taken from prosody-modules 4317:456b9f608fcf with the
mod_roster_command patch applied.

* feat: Use mod_client_proxy to proxy to jicofo.
2021-01-11 15:45:00 -06:00
Pawel Domas
6ebe2c2809 audio output selection in safari blocks the UI
It appears that at the time of this writing, creating audio tracks blocks
the browser's main thread for a long time on safari. Wasn't able to confirm
which part of track creation does the blocking exactly, but not creating
the tracks seems to help and makes the UI much more responsive.
2021-01-11 14:17:29 -05:00
Saygun ICYUZ
067610b3fd feat(recording) - Show recording started notification to the initiator (#8359)
* Show recording started notification to the initiator

* Translate 'recording.on' language key for English and Turkish
Translate 'liveStreaming.on' language key for English and Turkish
2021-01-11 08:21:33 -06:00
Avram Tudor
6f5534fcb6 Merge pull request #8352 from jitsi/tavram/allow-tileview-disabling
feat(jaas) allow tile view to be disabled
2021-01-08 16:03:21 +02:00
Tudor-Ovidiu Avram
dd8b220ff9 feat(jaas) allow tile view to be disabled 2021-01-08 12:00:13 +02:00
Avram Tudor
80d789879c Merge pull request #8356 from jitsi/tavram/top
feat(jaas) add config for displaying participants stats and conferenc…
2021-01-08 11:57:23 +02:00
Tudor-Ovidiu Avram
d49c5a6d8c feat(jaas) add config for displaying participants stats and conference subject 2021-01-08 10:21:07 +02:00
Mejans
9268255ca8 i18n: update Occitan language (#8346)
* Update main-oc.json

* Update languages-oc.json
2021-01-07 14:51:21 -06:00
Pawel Domas
a0806716ae fix(JitsiStreamPresenterEffect): frozen on Safari
Canvas rendering does not work as expected on Safari - the image stays
still on the first frame. Calling play() on the video tags seems to help.
2021-01-07 15:10:27 -05:00
Дамян Минков
3677a2f769 feat: Skip p2p when the participant is jigasi.
* feat: Skip p2p when the participant is jigasi.

1fd7256553...87c6e37475
2021-01-07 09:08:36 -06:00
Avram Tudor
3881da5db9 Merge pull request #8354 from jitsi/tavram/fix-filmstrip
fix(filmstrip) fix button not considering interface config settings
2021-01-07 15:20:22 +02:00
Tudor-Ovidiu Avram
35a586df3c fix(filmstrip) fix button not considering interface config settings 2021-01-07 14:53:11 +02:00
Saúl Ibarra Corretgé
dc5a776123 fix(ios) fix drag handle not rendering with latest react-native-svg
Fill must be properly specified.
2021-01-07 12:02:41 +01:00
Saúl Ibarra Corretgé
a3c6e690dd chore(deps) update react-native-svg to latest
Fixes icons not rendering on iOS 11.
2021-01-07 12:02:41 +01:00
Saúl Ibarra Corretgé
a1c197c73c fix(ios) fix crash on startup on iOS 11 2021-01-07 12:02:41 +01:00
damencho
e8c0c03e49 chore(deps) lib-jitsi-meet@latest
*  feat: Skips using disco-info for features. (#1450)
* sdp: improve sdp matching for simulcast lines (#1452)
* sdp: add missing colon to findLines calls (#1447)

310983c5b0...1fd7256553
2021-01-06 09:32:26 -06:00
Saúl Ibarra Corretgé
4798e0271b chore(android,ios) raise versions 2021-01-06 16:21:44 +01:00
Asif
cd29f10fa8 feat: emit raise hand event to external API (#8312)
* Expose raise hand event to external application

* Fix linting issues

* fix the app non existing issue
2021-01-06 08:49:10 -06:00
Pawel Domas
97dc07810c fix(setAudioOutputDeviceId): check if supported 2021-01-06 08:31:53 -06:00
damencho
28fa1f5dbe fix: Process pre-existing participants properties.
We were not processing properties which are set (fire properties updated) before the conference joined event is fired.
2021-01-05 14:29:34 -06:00
damencho
2ba6100e36 fix: Fixes showing phone icon for jigasi participants. 2021-01-05 14:29:34 -06:00
Shawn
060a8628ce fixed admin check for token verification 2021-01-05 12:56:33 -06:00
Christopher Engelhard
32fb08c56f Consistent formatting/indentation of files in ./doc (#8178)
* unify indentations (debian)

* unify indentations in example-config-files
2021-01-04 08:22:40 -06:00
Jakob Pfeiffer
4a3ff8ce2c fix(jitsi-meet-web-config.postinst) allow cert and key pre-selection (#8319)
* fix(jitsi-meet-web-config.postinst) allow cert and key pre-selection

* fix(jitsi-meet-web-config.postinst) jvb-hostname gets value from db_go instead of db_get

Co-authored-by: Jakob Pfeiffer <pgp-jkp@pfeiffer.ws>
2021-01-04 08:22:27 -06:00
yanglishuan
f32482539a fix(lang) update zhCN translation 2021-01-04 08:22:09 -06:00
53845714nF
f82088fb8f add systemd to letsencrypt installer (#8289)
* add systemd to letsencrypt

* Better readability of systemd change

Co-authored-by: Sebastian Feustel <sebastian.feustel@aei.mpg.de>
2021-01-04 08:21:47 -06:00
Vahid Zafari
76b4899c39 full support persian language (#8300) Fixes #8299 2020-12-23 08:29:07 -06:00
Marc Seitz
9b638a4052 fix(lang) update German translation 2020-12-23 11:07:10 +01:00
Saúl Ibarra Corretgé
a8b2e6ffb3 fix(tile-view) allow watermark to be covered 2020-12-23 11:05:30 +01:00
Saúl Ibarra Corretgé
aefd13ab1b fix(tile-view) reduce margins, take 2
Due to how the filmstrip size if computed I don't think there is a good way to
animate the change in size, so just ignore the toolbar, it will be hidden soon
enough.
2020-12-23 11:05:30 +01:00
163 changed files with 4613 additions and 2399 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

@@ -25,5 +25,5 @@ android.enableDexingArtifactTransform.desugaring=false
android.useAndroidX=true
android.enableJetifier=true
appVersion=20.6.0
sdkVersion=2.12.0
appVersion=21.0.0
sdkVersion=3.0.0

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
@@ -47,7 +48,14 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.squareup.duktape:duktape-android:1.3.0'
if (!rootProject.ext.libreBuild) {
if (rootProject.ext.libreBuild) {
implementation(project(':react-native-device-info')) {
exclude group: 'com.google.firebase'
exclude group: 'com.google.android.gms'
exclude group: 'com.android.installreferrer'
}
} else {
implementation project(':react-native-device-info')
implementation(project(":react-native-google-signin")) {
exclude group: 'com.google.android.gms'
exclude group: 'androidx'
@@ -59,7 +67,6 @@ dependencies {
implementation project(':react-native-calendar-events')
implementation project(':react-native-community_netinfo')
implementation project(':react-native-default-preference')
implementation project(':react-native-device-info')
implementation project(':react-native-immersive')
implementation project(':react-native-keep-awake')
implementation project(':react-native-linear-gradient')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -472,8 +472,8 @@ export default {
*/
createInitialLocalTracks(options = {}) {
const errors = {};
const initialDevices = [ 'audio' ];
const requestedAudio = true;
const initialDevices = config.disableInitialGUM ? [] : [ 'audio' ];
const requestedAudio = !config.disableInitialGUM;
let requestedVideo = false;
// Always get a handle on the audio input device so that we have statistics even if the user joins the
@@ -484,19 +484,22 @@ export default {
this.muteAudio(true, true);
}
if (!options.startWithVideoMuted
if (!config.disableInitialGUM
&& !options.startWithVideoMuted
&& !options.startAudioOnly
&& !options.startScreenSharing) {
initialDevices.push('video');
requestedVideo = true;
}
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
browserName =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browserName))
);
if (!config.disableInitialGUM) {
JitsiMeetJS.mediaDevices.addEventListener(
JitsiMediaDevicesEvents.PERMISSION_PROMPT_IS_SHOWN,
browserName =>
APP.store.dispatch(
mediaPermissionPromptVisibilityChanged(true, browserName))
);
}
let tryCreateLocalTracks;
@@ -2011,7 +2014,6 @@ export default {
formattedDisplayName
|| interfaceConfig.DEFAULT_REMOTE_DISPLAY_NAME)
});
APP.UI.changeDisplayName(id, formattedDisplayName);
}
);
room.on(
@@ -2050,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) => {
@@ -2383,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);
},
/**
@@ -2868,10 +2864,6 @@ export default {
APP.store.dispatch(updateSettings({
displayName: formattedNickname
}));
if (room) {
APP.UI.changeDisplayName(id, formattedNickname);
}
},
/**

View File

@@ -336,6 +336,13 @@ 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,
// Enabling the close page will ignore the welcome page redirection when
// a call is hangup.
// enableClosePage: false,
@@ -623,9 +630,18 @@ var config = {
// otherwise the app doesn't render it.
// moderatedRoomServiceUrl: 'https://moderated.jitsi-meet.example.com',
// If true, tile view will not be enabled automatically when the participants count threshold is reached.
// disableTileView: true,
// Hides the conference subject
// hideConferenceSubject: true
// Hides the conference timer.
// hideConferenceTimer: true,
// Hides the participants stats
// hideParticipantsStats: true
// Sets the conference subject
// subject: 'Conference Subject',
@@ -682,6 +698,65 @@ var config = {
ignoreStartMuted
*/
/**
Use this array to configure which notifications will be shown to the user
The items correspond to the title or description key of that notification
Some of these notifications also depend on some other internal logic to be displayed or not,
so adding them here will not ensure they will always be displayed
A falsy value for this prop will result in having all notifications enabled (e.g null, undefined, false)
*/
// notifications: [
// 'connection.CONNFAIL', // shown when the connection fails,
// 'dialog.cameraNotSendingData', // shown when there's no feed from user's camera
// 'dialog.kickTitle', // shown when user has been kicked
// 'dialog.liveStreaming', // livestreaming notifications (pending, on, off, limits)
// 'dialog.lockTitle', // shown when setting conference password fails
// 'dialog.maxUsersLimitReached', // shown when maximmum users limit has been reached
// 'dialog.micNotSendingData', // shown when user's mic is not sending any audio
// 'dialog.passwordNotSupportedTitle', // shown when setting conference password fails due to password format
// 'dialog.recording', // recording notifications (pending, on, off, limits)
// 'dialog.remoteControlTitle', // remote control notifications (allowed, denied, start, stop, error)
// 'dialog.reservationError',
// 'dialog.serviceUnavailable', // shown when server is not reachable
// 'dialog.sessTerminated', // shown when there is a failed conference session
// 'dialog.tokenAuthFailed', // show when an invalid jwt is used
// 'dialog.transcribing', // transcribing notifications (pending, off)
// 'dialOut.statusMessage', // shown when dial out status is updated.
// 'liveStreaming.busy', // shown when livestreaming service is busy
// 'liveStreaming.failedToStart', // shown when livestreaming fails to start
// 'liveStreaming.unavailableTitle', // shown when livestreaming service is not reachable
// 'lobby.joinRejectedMessage', // shown when while in a lobby, user's request to join is rejected
// 'lobby.notificationTitle', // shown when lobby is toggled and when join requests are allowed / denied
// 'localRecording.localRecording', // shown when a local recording is started
// 'notify.disconnected', // shown when a participant has left
// 'notify.grantedTo', // shown when moderator rights were granted to a participant
// 'notify.invitedOneMember', // shown when 1 participant has been invited
// 'notify.invitedThreePlusMembers', // shown when 3+ participants have been invited
// 'notify.invitedTwoMembers', // shown when 2 participants have been invited
// 'notify.kickParticipant', // shown when a participant is kicked
// 'notify.mutedRemotelyTitle', // shown when user is muted by a remote party
// 'notify.mutedTitle', // shown when user has been muted upon joining,
// 'notify.newDeviceAudioTitle', // prompts the user to use a newly detected audio device
// 'notify.newDeviceCameraTitle', // prompts the user to use a newly detected camera
// 'notify.passwordRemovedRemotely', // shown when a password has been removed remotely
// 'notify.passwordSetRemotely', // shown when a password has been set remotely
// 'notify.raisedHand', // shown when a partcipant used raise hand,
// 'notify.startSilentTitle', // shown when user joined with no audio
// 'prejoin.errorDialOut',
// 'prejoin.errorDialOutDisconnected',
// 'prejoin.errorDialOutFailed',
// 'prejoin.errorDialOutStatus',
// 'prejoin.errorStatusCode',
// 'prejoin.errorValidation',
// 'recording.busy', // shown when recording service is busy
// 'recording.failedToStart', // shown when recording fails to start
// 'recording.unavailableTitle', // shown when recording service is not reachable
// 'toolbar.noAudioSignalTitle', // shown when a broken mic is detected
// 'toolbar.noisyAudioInputTitle', // shown when noise is detected for the current microphone
// 'toolbar.talkWhileMutedPopup', // shown when user tries to speak while muted
// 'transcribing.failedToStart' // shown when transcribing fails to start
// ]
// Allow all above example options to include a trailing comma and
// prevent fear when commenting out the last value.

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

View File

@@ -17,6 +17,7 @@ textarea {
html {
height: 100%;
width: 100%;
overflow: hidden;
}
body {
@@ -201,74 +202,3 @@ form {
background: rgba(0, 0, 0, .5);
border-radius: 4px;
}
.desktop-browser {
@media only screen and (max-width: $smallScreen) {
.watermark {
width: 20%;
height: 20%;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
}
}
}
}
}
@media only screen and (max-width: $verySmallScreen) {
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
.chrome-extension-banner {
display: none;
}
}
}

View File

@@ -379,3 +379,31 @@
}
}
}
.chat-dialog {
display: flex;
flex-direction: column;
height: 100%;
margin-top: -5px; // Margin set by atlaskit.
&-header {
display: flex;
justify-content: space-between;
margin: 16px 16px 24px;
width: calc(100% - 32px);
box-sizing: border-box;
color: #fff;
font-weight: 600;
font-size: 24px;
line-height: 32px;
.jitsi-icon > svg {
cursor: pointer;
fill: #A4B8D1;
}
}
#chatconversation {
width: 100%;
}
}

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

@@ -1,3 +1,73 @@
@mixin small-button-size() {
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.toolbox-button {
.toolbox-icon {
width: 28px;
height: 28px;
svg {
width: 18px;
height: 18px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 30px;
height: 30px;
}
}
}
}
}
}
}
@mixin very-small-button-size() {
.new-toolbox {
.toolbox-content {
.button-group-center, .button-group-left, .button-group-right {
.settings-button-small-icon {
display: none;
}
.toolbox-button {
.toolbox-icon {
width: 18px;
height: 18px;
svg {
width: 12px;
height: 12px;
}
}
&:nth-child(2) {
.toolbox-icon {
width: 20px;
height: 20px;
}
}
}
}
}
}
}
@mixin full-size-modal-positioner() {
height: 100%;
left: 0;
position: fixed;
top: 0;
max-width: 100%;
width: 100%;
}
@mixin full-size-modal-dialog() {
height: 100%;
max-height: 100%;
border-radius: 0;
}
@media only screen and (max-width: $verySmallScreen) {
.welcome {
display: block;
@@ -65,3 +135,65 @@
}
}
}
.desktop-browser {
@media only screen and (max-width: $smallScreen) {
@include small-button-size();
}
@media only screen and (max-width: $verySmallScreen) {
@include very-small-button-size();
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.chrome-extension-banner {
display: none;
}
}
&.shift-right {
@media only screen and (max-width: $smallScreen + $sidebarWidth) {
@include small-button-size()
}
@media only screen and (max-width: $verySmallScreen + $sidebarWidth) {
@include very-small-button-size();
#videoResolutionLabel {
display: none;
}
.vertical-filmstrip .filmstrip {
display: none;
}
.chrome-extension-banner {
display: none;
}
}
}
}
@media (min-width: 480px) and (max-width: 580px) {
.shift-right [class^="Modal__PositionerAbsolute"] {
@include full-size-modal-positioner();
}
.shift-right [class^="Modal__Dialog-"] {
@include full-size-modal-dialog();
}
}
@media (min-width: 580px) and (max-width: 680px) {
.mobile-browser {
&.shift-right [class^="Modal__PositionerAbsolute"] {
@include full-size-modal-positioner();
}
&.shift-right [class^="Modal__Dialog-"] {
@include full-size-modal-dialog();
}
}
}

View File

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

View File

@@ -109,14 +109,4 @@
.has-overflow .videocontainer {
align-self: baseline;
}
/**
* Firefox flex acts a little differently. To make sure the bottom row of
* thumbnails is not overlapped by the horizontal toolbar, margin is added
* to the local thumbnail to keep it from the bottom of the screen. It is
* assumed the local thumbnail will always be on the bottom row.
*/
.has-overflow #localVideoContainer {
margin-bottom: 100px !important;
}
}

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

@@ -142,6 +142,28 @@ case "$1" in
echo -e " admins = { \"$JICOFO_AUTH_USER@auth.$JVB_HOSTNAME\", \"jvb@auth.$JVB_HOSTNAME\" }" >> $PROSODY_HOST_CONFIG
fi
# Convert the old focus component config to the new one.
# Old:
# Component "focus.jitmeet.example.com"
# component_secret = "focusSecret"
# New:
# 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 "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/" }' ;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
# Make sure the focus@auth user's roster includes the proxy component (this is idempotent)
prosodyctl mod_roster_command subscribe focus.$JVB_HOSTNAME $JICOFO_AUTH_USER@auth.$JVB_HOSTNAME
if [ ! -f /var/lib/prosody/$JVB_HOSTNAME.crt ]; then
# prosodyctl takes care for the permissions
# echo for using all default values

View File

@@ -30,6 +30,7 @@ case "$1" in
db_set jitsi-videobridge/jvb-hostname "localhost"
db_input critical jitsi-videobridge/jvb-hostname || true
db_go
db_get jitsi-videobridge/jvb-hostname
fi
JVB_HOSTNAME="$RET"
@@ -75,15 +76,21 @@ case "$1" in
CERT_CHOICE="$RET"
if [ "$CERT_CHOICE" = "$UPLOADED_CERT_CHOICE" ] ; then
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
db_input critical jitsi-meet/cert-path-key || true
db_go
db_get jitsi-meet/cert-path-key
if [ -z "$RET" ] ; then
db_set jitsi-meet/cert-path-key "/etc/ssl/$JVB_HOSTNAME.key"
db_input critical jitsi-meet/cert-path-key || true
db_go
db_get jitsi-meet/cert-path-key
fi
CERT_KEY="$RET"
db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
db_input critical jitsi-meet/cert-path-crt || true
db_go
db_get jitsi-meet/cert-path-crt
if [ -z "$RET" ] ; then
db_set jitsi-meet/cert-path-crt "/etc/ssl/$JVB_HOSTNAME.crt"
db_input critical jitsi-meet/cert-path-crt || true
db_go
db_get jitsi-meet/cert-path-crt
fi
CERT_CRT="$RET"
else
# create self-signed certs

View File

@@ -6,9 +6,9 @@ muc_mapper_domain_base = "jitmeet.example.com";
turncredentials_secret = "__turnSecret__";
turncredentials = {
{ type = "stun", host = "jitmeet.example.com", port = "3478" },
{ type = "turn", host = "jitmeet.example.com", port = "3478", transport = "udp" },
{ type = "turns", host = "jitmeet.example.com", port = "5349", transport = "tcp" }
{ type = "stun", host = "jitmeet.example.com", port = "3478" },
{ type = "turn", host = "jitmeet.example.com", port = "3478", transport = "udp" },
{ type = "turns", host = "jitmeet.example.com", port = "5349", transport = "tcp" }
};
cross_domain_bosh = false;
@@ -17,48 +17,48 @@ consider_bosh_secure = true;
-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
ssl = {
protocol = "tlsv1_2+";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
protocol = "tlsv1_2+";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
}
VirtualHost "jitmeet.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/jitmeet.example.com.key";
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conferenceduration.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"conference_duration";
"muc_lobby_rooms";
}
c2s_require_encryption = false
lobby_muc = "lobby.jitmeet.example.com"
main_muc = "conference.jitmeet.example.com"
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/jitmeet.example.com.key";
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conferenceduration.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"conference_duration";
"muc_lobby_rooms";
}
c2s_require_encryption = false
lobby_muc = "lobby.jitmeet.example.com"
main_muc = "conference.jitmeet.example.com"
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
Component "conference.jitmeet.example.com" "muc"
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
-- "token_verification";
--"token_verification";
}
admins = { "focusUser@auth.jitmeet.example.com" }
muc_room_locking = false
@@ -68,7 +68,7 @@ Component "conference.jitmeet.example.com" "muc"
Component "internal.auth.jitmeet.example.com" "muc"
storage = "memory"
modules_enabled = {
"ping";
"ping";
}
admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
muc_room_locking = false
@@ -77,8 +77,9 @@ Component "internal.auth.jitmeet.example.com" "muc"
VirtualHost "auth.jitmeet.example.com"
authentication = "internal_plain"
Component "focus.jitmeet.example.com"
component_secret = "focusSecret"
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
Component "focus.jitmeet.example.com" "client_proxy"
target_address = "focusUser@auth.jitmeet.example.com"
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
muc_component = "conference.jitmeet.example.com"

View File

@@ -6,14 +6,14 @@ server {
server_name jitsi-meet.example.com;
location ^~ /.well-known/acme-challenge/ {
default_type "text/plain";
root /usr/share/jitsi-meet;
default_type "text/plain";
root /usr/share/jitsi-meet;
}
location = /.well-known/acme-challenge/ {
return 404;
return 404;
}
location / {
return 301 https://$host$request_uri;
return 301 https://$host$request_uri;
}
}
server {
@@ -21,7 +21,7 @@ server {
listen [::]:443 ssl;
server_name jitsi-meet.example.com;
# Mozilla Guideline v5.4, nginx 1.17.7, OpenSSL 1.1.1d, intermediate configuration
# Mozilla Guideline v5.4, nginx 1.17.7, OpenSSL 1.1.1d, intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
@@ -58,7 +58,7 @@ server {
alias /usr/share/jitsi-meet/libs/external_api.min.js;
}
#ensure all static content can always be found first
# ensure all static content can always be found first
location ~ ^/(libs|css|static|images|fonts|lang|sounds|connection_optimization|.well-known)/(.*)$
{
add_header 'Access-Control-Allow-Origin' '*';
@@ -66,13 +66,13 @@ server {
# cache all versioned files
if ($arg_v) {
expires 1y;
expires 1y;
}
}
# BOSH
location = /http-bind {
proxy_pass http://localhost:5280/http-bind;
proxy_pass http://localhost:5280/http-bind;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
}
@@ -89,11 +89,11 @@ server {
# colibri (JVB) websockets for jvb1
location ~ ^/colibri-ws/default-id/(.*) {
proxy_pass http://127.0.0.1:9090/colibri-ws/default-id/$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
tcp_nodelay on;
proxy_pass http://127.0.0.1:9090/colibri-ws/default-id/$1$is_args$args;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
tcp_nodelay on;
}
location ~ ^/([^/?&:'"]+)$ {
@@ -106,13 +106,13 @@ server {
location ~ ^/([^/?&:'"]+)/config.js$
{
set $subdomain "$1.";
set $subdir "$1/";
set $subdomain "$1.";
set $subdir "$1/";
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
alias /etc/jitsi/meet/jitsi-meet.example.com-config.js;
}
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
# Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
location ~ ^/([^/?&:'"]+)/(.*)$ {
set $subdomain "$1.";
set $subdir "$1/";

View File

@@ -8,50 +8,49 @@
</VirtualHost>
<VirtualHost *:443>
ServerName jitsi-meet.example.com
ServerName jitsi-meet.example.com
# enable HTTP/2, if available
Protocols h2 http/1.1
# enable HTTP/2, if available
Protocols h2 http/1.1
SSLEngine on
SSLProxyEngine on
SSLCertificateFile /etc/jitsi/meet/jitsi-meet.example.com.crt
SSLCertificateKeyFile /etc/jitsi/meet/jitsi-meet.example.com.key
SSLEngine on
SSLProxyEngine on
SSLCertificateFile /etc/jitsi/meet/jitsi-meet.example.com.crt
SSLCertificateKeyFile /etc/jitsi/meet/jitsi-meet.example.com.key
Header always set Strict-Transport-Security "max-age=63072000"
Header always set Strict-Transport-Security "max-age=63072000"
DocumentRoot "/usr/share/jitsi-meet"
<Directory "/usr/share/jitsi-meet">
Options Indexes MultiViews Includes FollowSymLinks
AddOutputFilter Includes html
AllowOverride All
Order allow,deny
Allow from all
</Directory>
DocumentRoot "/usr/share/jitsi-meet"
<Directory "/usr/share/jitsi-meet">
Options Indexes MultiViews Includes FollowSymLinks
AddOutputFilter Includes html
AllowOverride All
Order allow,deny
Allow from all
</Directory>
ErrorDocument 404 /static/404.html
ErrorDocument 404 /static/404.html
Alias "/config.js" "/etc/jitsi/meet/jitsi-meet.example.com-config.js"
<Location /config.js>
Require all granted
</Location>
Alias "/config.js" "/etc/jitsi/meet/jitsi-meet.example.com-config.js"
<Location /config.js>
Require all granted
</Location>
Alias "/external_api.js" "/usr/share/jitsi-meet/libs/external_api.min.js"
<Location /external_api.js>
Require all granted
</Location>
Alias "/external_api.js" "/usr/share/jitsi-meet/libs/external_api.min.js"
<Location /external_api.js>
Require all granted
</Location>
ProxyPreserveHost on
ProxyPass /http-bind http://localhost:5280/http-bind/
ProxyPassReverse /http-bind http://localhost:5280/http-bind/
ProxyPreserveHost on
ProxyPass /http-bind http://localhost:5280/http-bind/
ProxyPassReverse /http-bind http://localhost:5280/http-bind/
RewriteEngine on
RewriteRule ^/([a-zA-Z0-9]+)$ /index.html
RewriteEngine on
RewriteRule ^/([a-zA-Z0-9]+)$ /index.html
</VirtualHost>
# Mozilla Guideline v5.4, Apache 2.4.41, OpenSSL 1.1.1d, intermediate configuration, no OCSP
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLSessionTickets off

View File

@@ -10,7 +10,6 @@ server {
index index.html;
set $prefix "";
# BOSH
location /http-bind {
proxy_pass http://localhost:5280/http-bind;
@@ -44,13 +43,13 @@ server {
location ~ ^/([^/?&:'"]+)/config.js$
{
set $subdomain "$1.";
set $subdir "$1/";
set $subdomain "$1.";
set $subdir "$1/";
alias /etc/jitsi/meet/{{jitsi_meet_domain_name}}-config.js;
alias /etc/jitsi/meet/{{jitsi_meet_domain_name}}-config.js;
}
#Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
# Anything that didn't match above, and isn't a real file, assume it's a room name and redirect to /
location ~ ^/([^/?&:'"]+)/(.*)$ {
set $subdomain "$1.";
set $subdir "$1/";

View File

@@ -34,55 +34,56 @@ component_ports = { 5347 }
-- Documentation on modules can be found at: http://prosody.im/doc/modules
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
-- These are commented by default as they have a performance impact
--"privacy"; -- Support privacy lists
"compression"; -- Stream compression (requires the lua-zlib package installed)
-- These are commented by default as they have a performance impact
--"privacy"; -- Support privacy lists
"compression"; -- Stream compression (requires the lua-zlib package installed)
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"http_files"; -- Serve static files from a directory over HTTP
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-- jitsi
"smacks";
"carbons";
"mam";
"lastactivity";
"offline";
"pubsub";
"adhoc";
"websocket";
"http_altconnect";
-- Other specific functionality
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-- jitsi
"smacks";
"carbons";
"mam";
"lastactivity";
"offline";
"pubsub";
"adhoc";
"websocket";
"http_altconnect";
}
-- domain mapper options, must at least have domain base set to use the mapper
@@ -91,9 +92,9 @@ muc_mapper_domain_base = "jitsi.example.com";
-- These modules are auto-loaded, but should you want
-- to disable them then uncomment them here:
modules_disabled = {
-- "offline"; -- Store offline messages
-- "c2s"; -- Handle client connections
-- "s2s"; -- Handle server-to-server connections
--"offline"; -- Store offline messages
--"c2s"; -- Handle client connections
--"s2s"; -- Handle server-to-server connections
}
-- Disable account creation by default, for security
@@ -110,7 +111,7 @@ ssl = {
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
-- c2s_require_encryption = true
--c2s_require_encryption = true
-- Force certificate authentication for server-to-server connections?
-- This provides ideal security, but requires servers you communicate
@@ -118,7 +119,7 @@ ssl = {
-- NOTE: Your version of LuaSec must support certificate verification!
-- For more information see http://prosody.im/doc/s2s#security
-- s2s_secure_auth = false
--s2s_secure_auth = false
-- Many servers don't support encryption or have invalid or self-signed
-- certificates. You can list domains here that will not be required to
@@ -159,9 +160,9 @@ authentication = "internal_hashed"
-- Logging configuration
-- For advanced logging see http://prosody.im/doc/logging
log = {
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
"*syslog";
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
"*syslog";
}
----------- Virtual hosts -----------
@@ -171,25 +172,25 @@ log = {
--VirtualHost "localhost"
VirtualHost "jitsi.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
c2s_require_encryption = false
c2s_require_encryption = false
VirtualHost "auth.jitsi.example.com"
ssl = {
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
ssl = {
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
------ Components ------
-- You can specify components to add hosts that provide special services,
@@ -209,7 +210,7 @@ VirtualHost "auth.jitsi.example.com"
-- see: http://prosody.im/doc/components#adding_an_external_component
--
--Component "gateway.example.com"
-- component_secret = "password"
-- component_secret = "password"
Component "conference.jitsi.example.com" "muc"
modules_enabled = { "muc_domain_mapper" }

View File

@@ -34,63 +34,64 @@ component_ports = { 5347 }
-- Documentation on modules can be found at: http://prosody.im/doc/modules
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
-- Not essential, but recommended
"private"; -- Private XML storage (for room bookmarks, etc.)
"vcard"; -- Allow users to set vCards
-- These are commented by default as they have a performance impact
--"privacy"; -- Support privacy lists
"compression"; -- Stream compression (requires the lua-zlib package installed)
-- These are commented by default as they have a performance impact
--"privacy"; -- Support privacy lists
"compression"; -- Stream compression (requires the lua-zlib package installed)
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"pep"; -- Enables users to publish their mood, activity, playing music and more
"register"; -- Allow users to register on this server using a client and change passwords
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"http_files"; -- Serve static files from a directory over HTTP
-- HTTP modules
"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-- jitsi
"smacks";
"carbons";
"mam";
"lastactivity";
"offline";
"pubsub";
"adhoc";
"websocket";
"http_altconnect";
-- Other specific functionality
--"groups"; -- Shared roster support
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
-- jitsi
"smacks";
"carbons";
"mam";
"lastactivity";
"offline";
"pubsub";
"adhoc";
"websocket";
"http_altconnect";
}
-- These modules are auto-loaded, but should you want
-- to disable them then uncomment them here:
modules_disabled = {
-- "offline"; -- Store offline messages
-- "c2s"; -- Handle client connections
-- "s2s"; -- Handle server-to-server connections
--"offline"; -- Store offline messages
--"c2s"; -- Handle client connections
--"s2s"; -- Handle server-to-server connections
}
-- Disable account creation by default, for security
@@ -107,7 +108,7 @@ ssl = {
-- Force clients to use encrypted connections? This option will
-- prevent clients from authenticating unless they are using encryption.
-- c2s_require_encryption = true
--c2s_require_encryption = true
-- Force certificate authentication for server-to-server connections?
-- This provides ideal security, but requires servers you communicate
@@ -115,7 +116,7 @@ ssl = {
-- NOTE: Your version of LuaSec must support certificate verification!
-- For more information see http://prosody.im/doc/s2s#security
-- s2s_secure_auth = false
--s2s_secure_auth = false
-- Many servers don't support encryption or have invalid or self-signed
-- certificates. You can list domains here that will not be required to
@@ -156,9 +157,9 @@ authentication = "internal_hashed"
-- Logging configuration
-- For advanced logging see http://prosody.im/doc/logging
log = {
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
"*syslog";
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
"*syslog";
}
----------- Virtual hosts -----------
@@ -168,25 +169,25 @@ log = {
--VirtualHost "localhost"
VirtualHost "jitsi.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/var/lib/prosody/jitsi.example.com.key";
certificate = "/var/lib/prosody/jitsi.example.com.crt";
}
c2s_require_encryption = false
c2s_require_encryption = false
VirtualHost "auth.jitsi.example.com"
ssl = {
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
ssl = {
key = "/var/lib/prosody/auth.jitsi.example.com.key";
certificate = "/var/lib/prosody/auth.jitsi.example.com.crt";
}
authentication = "internal_plain"
------ Components ------
-- You can specify components to add hosts that provide special services,
@@ -206,7 +207,7 @@ VirtualHost "auth.jitsi.example.com"
-- see: http://prosody.im/doc/components#adding_an_external_component
--
--Component "gateway.example.com"
-- component_secret = "password"
-- component_secret = "password"
Component "conference.jitsi.example.com" "muc"

View File

@@ -24,4 +24,9 @@ JICOFO_AUTH_PASSWORD=$JICOFO_PASSWORD
JICOFO_OPTS=""
# adds java system props that are passed to jicofo (default are for home and logging config file)
JAVA_SYS_PROPS="-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi -Dnet.java.sip.communicator.SC_HOME_DIR_NAME=jicofo -Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi -Djava.util.logging.config.file=/etc/jitsi/jicofo/logging.properties"
JAVA_SYS_PROPS=" \
-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi \
-Dnet.java.sip.communicator.SC_HOME_DIR_NAME=jicofo \
-Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi \
-Djava.util.logging.config.file=/etc/jitsi/jicofo/logging.properties \
"

View File

@@ -6,48 +6,48 @@ muc_mapper_domain_base = "meet.example.com";
turncredentials_secret = "turncredentials_secret_test";
turncredentials = {
{ type = "stun", host = "meet.example.com", port = "443" },
{ type = "turn", host = "meet.example.com", port = "443", transport = "udp" },
{ type = "turns", host = "meet.example.com", port = "443", transport = "tcp" }
{ type = "stun", host = "meet.example.com", port = "443" },
{ type = "turn", host = "meet.example.com", port = "443", transport = "udp" },
{ type = "turns", host = "meet.example.com", port = "443", transport = "tcp" }
};
cross_domain_bosh = false;
consider_bosh_secure = true;
VirtualHost "meet.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/meet.example.com.key";
certificate = "/etc/prosody/certs/meet.example.com.crt";
}
speakerstats_component = "speakerstats.meet.example.com"
conference_duration_component = "conferenceduration.meet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"conference_duration";
}
c2s_require_encryption = false
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/meet.example.com.key";
certificate = "/etc/prosody/certs/meet.example.com.crt";
}
speakerstats_component = "speakerstats.meet.example.com"
conference_duration_component = "conferenceduration.meet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"turncredentials";
"conference_duration";
}
c2s_require_encryption = false
Component "conference.meet.example.com" "muc"
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
-- "token_verification";
--"token_verification";
}
admins = { "focus@auth.meet.example.com" }
muc_room_locking = false
@@ -58,7 +58,7 @@ Component "conference.meet.example.com" "muc"
Component "internal.auth.meet.example.com" "muc"
storage = "memory"
modules_enabled = {
"ping";
"ping";
}
admins = { "focus@auth.meet.example.com", "jvb@auth.meet.example.com" }
@@ -75,7 +75,6 @@ Component "focus.meet.example.com"
Component "speakerstats.meet.example.com" "speakerstats_component"
muc_component = "conference.meet.example.com"
Component "conferenceduration.meet.example.com" "conference_duration_component"
muc_component = "conference.meet.example.com"

View File

@@ -13,58 +13,58 @@ network_backend = "epoll"
-- Documentation for bundled modules can be found at: https://prosody.im/doc/modules
modules_enabled = {
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
-- Generally required
"roster"; -- Allow users to have a roster. Recommended ;)
"saslauth"; -- Authentication for clients and servers. Recommended if you want to log in.
"tls"; -- Add support for secure TLS on c2s/s2s connections
"dialback"; -- s2s dialback support
"disco"; -- Service discovery
-- Not essential, but recommended
"carbons"; -- Keep multiple clients in sync
"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
"blocklist"; -- Allow users to block communications with other users
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Not essential, but recommended
"carbons"; -- Keep multiple clients in sync
"pep"; -- Enables users to publish their avatar, mood, activity, playing music and more
"private"; -- Private XML storage (for room bookmarks, etc.)
"blocklist"; -- Allow users to block communications with other users
"vcard4"; -- User profiles (stored in PEP)
"vcard_legacy"; -- Conversion between legacy vCard and PEP Avatar, vcard
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
--"mam"; -- Store messages in an archive and allow users to access it
--"csi_simple"; -- Simple Mobile optimizations
-- Nice to have
"version"; -- Replies to server version requests
"uptime"; -- Report how long server has been running
"time"; -- Let others know the time here on this server
"ping"; -- Replies to XMPP pings with pongs
"register"; -- Allow users to register on this server using a client and change passwords
--"mam"; -- Store messages in an archive and allow users to access it
--"csi_simple"; -- Simple Mobile optimizations
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- Admin interfaces
"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
--"admin_telnet"; -- Opens telnet console interface on localhost port 5582
-- HTTP modules
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"websocket"; -- XMPP over WebSockets
--"http_files"; -- Serve static files from a directory over HTTP
-- HTTP modules
--"bosh"; -- Enable BOSH clients, aka "Jabber over HTTP"
--"websocket"; -- XMPP over WebSockets
--"http_files"; -- Serve static files from a directory over HTTP
-- Other specific functionality
--"limits"; -- Enable bandwidth limiting for XMPP connections
--"groups"; -- Shared roster support
--"server_contact_info"; -- Publish contact information for this service
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
-- Other specific functionality
--"limits"; -- Enable bandwidth limiting for XMPP connections
--"groups"; -- Shared roster support
--"server_contact_info"; -- Publish contact information for this service
--"announce"; -- Send announcement to all online users
--"welcome"; -- Welcome users who register accounts
--"watchregistrations"; -- Alert admins of registrations
--"motd"; -- Send a message to users when they log in
--"legacyauth"; -- Legacy authentication. Only used by some old clients and bots.
--"proxy65"; -- Enables a file transfer proxy service which clients behind NAT can use
}
-- These modules are auto-loaded, but should you want
-- to disable them then uncomment them here:
modules_disabled = {
-- "offline"; -- Store offline messages
-- "c2s"; -- Handle client connections
-- "s2s"; -- Handle server-to-server connections
-- "posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
--"offline"; -- Store offline messages
--"c2s"; -- Handle client connections
--"s2s"; -- Handle server-to-server connections
--"posix"; -- POSIX functionality, sends server to background, enables syslog, etc.
}
-- Disable account creation by default, for security
@@ -85,7 +85,6 @@ s2s_require_encryption = true
s2s_secure_auth = false
-- Required for init scripts and prosodyctl
pidfile = "/var/run/prosody/prosody.pid"
@@ -99,13 +98,12 @@ archive_expires_after = "1w" -- Remove archived messages after 1 week
-- Logging configuration
-- For advanced logging see https://prosody.im/doc/logging
log = {
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
-- "*syslog"; -- Uncomment this for logging to syslog
-- "*console"; -- Log to the console, useful for debugging with daemonize=false
info = "/var/log/prosody/prosody.log"; -- Change 'info' to 'debug' for verbose logging
error = "/var/log/prosody/prosody.err";
--"*syslog"; -- Uncomment this for logging to syslog
--"*console"; -- Log to the console, useful for debugging with daemonize=false
}
-- Location of directory to find certificates in (relative to main config file):
certificates = "certs"

View File

@@ -15,6 +15,10 @@ JVB_SECRET=$VP_SECRET
# extra options to pass to the JVB daemon
JVB_OPTS="--apis=rest,"
# adds java system props that are passed to jvb (default are for home and logging config file)
JAVA_SYS_PROPS="-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi -Dnet.java.sip.communicator.SC_HOME_DIR_NAME=videobridge -Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi -Djava.util.logging.config.file=/etc/jitsi/videobridge/logging.properties"
JAVA_SYS_PROPS=" \
-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION=/etc/jitsi \
-Dnet.java.sip.communicator.SC_HOME_DIR_NAME=videobridge \
-Dnet.java.sip.communicator.SC_LOG_DIR_LOCATION=/var/log/jitsi \
-Djava.util.logging.config.file=/etc/jitsi/videobridge/logging.properties \
"

View File

@@ -354,7 +354,7 @@ PODS:
- React
- RNDefaultPreference (1.4.2):
- React
- RNDeviceInfo (7.3.1):
- RNDeviceInfo (8.0.0):
- React-Core
- RNGoogleSignin (3.0.1):
- GoogleSignIn (~> 5.0.0)
@@ -364,7 +364,7 @@ PODS:
- RNSound/Core (= 0.11.0)
- RNSound/Core (0.11.0):
- React
- RNSVG (10.1.0):
- RNSVG (12.1.0):
- React
- RNWatch (0.4.3):
- React
@@ -577,10 +577,10 @@ SPEC CHECKSUMS:
ReactCommon: 18014e1d98dbeb9141e935cfe35fc93bd511ffb6
RNCAsyncStorage: bc2f81cc1df90c267ce9ed30bb2dbc93b945a8ee
RNDefaultPreference: 56a405ce61033ac77b95004dccd7ac54c2eb50d1
RNDeviceInfo: 57bb2806fb7bd982a1434e9f0b4e6a6ab1f6702e
RNDeviceInfo: 72ded653ce636b3f03571e90bed99309a714944e
RNGoogleSignin: 39336070b35fc4cea6a98cf111e00480317be0ae
RNSound: c980916b596cc15c8dcd2f6ecd3b13c4881dbe20
RNSVG: 069864be08c9fe065a2cf7e63656a34c78653c99
RNSVG: ce9d996113475209013317e48b05c21ee988d42e
RNWatch: a5320c959c75e72845c07985f3e935e58998f1d3
Yoga: 96b469c5e81ff51b917b92e8c3390642d4ded30c

View File

@@ -715,6 +715,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 609CB2080B75F75A89923F3D /* Pods-JitsiMeet.debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
@@ -744,6 +745,7 @@
isa = XCBuildConfiguration;
baseConfigurationReference = FC040BBED70876444D89E91C /* Pods-JitsiMeet.release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
CODE_SIGN_ENTITLEMENTS = app.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.6.0</string>
<string>21.0.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>

View File

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

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>20.6.0</string>
<string>21.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UISupportedInterfaceOrientations</key>

View File

@@ -17,7 +17,7 @@
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>20.6.0</string>
<string>21.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CLKComplicationPrincipalClass</key>

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
/*
* Copyright @ 2017-present Atlassian Pty Ltd
* Copyright @ 2017-present 8x8, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,4 +55,24 @@
*/
- (void)enterPictureInPicture:(NSDictionary *)data;
/**
* Called when a participant has joined the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has joined.
*/
- (void)participantJoined:(NSDictionary *)data;
/**
* Called when a participant has left the conference.
*
* The `data` dictionary contains a `participantId` key with the id of the participant that has left.
*/
- (void)participantLeft:(NSDictionary *)data;
/**
* Called when audioMuted state changed.
*
* The `data` dictionary contains a `muted` key with state of the audioMuted for the localParticipant.
*/
- (void)audioMutedChanged:(NSDictionary *)data;
@end

View File

@@ -1,32 +1,50 @@
{
"en": "انگلیسی",
"af": "آفریقایی",
"ar": "عربی",
"bg": "بلغاری",
"ca": "کاتالانی",
"cs": "چک",
"da": "دانمارکی",
"de": "آلمانی",
"el": "یونانی",
"enGB": "انگلیسی (انگلستان)",
"eo": "اسپرانتو",
"es": "اسپانیایی",
"esUS": "اسپانیایی (آمریکا لاتین)",
"et": "استونیایی",
"eu": "باسکایی",
"fi": "فنلاندی",
"fr": "فرانسوی",
"frCA": "فرانسوی (کانادا)",
"hr": "",
"he": "عبری",
"mr":"مراتی",
"hr": "کرواتی",
"hu": "بلغاری",
"hy": "ارمنی",
"id": "اندونزیایی",
"it": "ایتالیایی",
"ja": "ژاپنی",
"kab": "کابیلی",
"ko": "کره ای",
"nl": "",
"lt": "لیتوانیایی",
"ml": "مالایایی",
"lv": "لتونیایی",
"nl": "هلندی",
"oc": "اکسیتان(قدیمی)",
"pl": "لهستانی",
"ptBR": "پرتغالی (برزیل)",
"ru": "روسی",
"ro": "رومانیایی",
"sc": "ساردینی",
"sk": "اسلواکیایی",
"sl": "اسلوونیایی",
"sr": "صربی",
"sv": "سوئدی",
"th": "تایلندی",
"tr": "ترکی",
"uk": "اوکراینی",
"vi": "ویتنامی",
"zhCN": "چینی",
"zhTW": ""
"zhTW": "چینی (تایوان)"
}

View File

@@ -28,6 +28,8 @@
"kab": "Cabil",
"ko": "Corean",
"lt": "Lituanian",
"ml": "Malaialam",
"lv": "Leton",
"nl": "Neerlandés",
"oc": "Occitan",
"pl": "Polonés",

View File

@@ -903,7 +903,7 @@
"allow": "Annehmen",
"backToKnockModeButton": "Kein Passwort, stattdessen Beitritt anfragen",
"dialogTitle": "Lobbymodus",
"disableDialogContent": "Lobbymodus derzeit deaktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion aktivieren?",
"disableDialogContent": "Lobbymodus derzeit aktiviert. Diese Funktion stellt sicher, dass unerwünschte Personen Ihrer Konferenz nicht beitreten können. Funktion deaktivieren?",
"disableDialogSubmit": "Deaktivieren",
"emailField": "E-Mail-Adresse eingeben",
"enableDialogPasswordField": "Passwort setzen (optional)",

View File

@@ -380,7 +380,7 @@
"getStreamKeyManually": "We werent able to fetch any live streams. Try getting your live stream key from YouTube.",
"invalidStreamKey": "Live stream key may be incorrect.",
"off": "Live Streaming stopped",
"on": "Live Streaming",
"on": "Live Streaming started",
"pending": "Starting Live Stream…",
"serviceName": "Live Streaming service",
"signedInAs": "You are currently signed in as:",
@@ -497,7 +497,7 @@
"live": "LIVE",
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
"on": "Recording",
"on": "Recording started",
"pending": "Preparing to record the meeting…",
"rec": "REC",
"serviceDescription": "Your recording will be saved by the recording service",

File diff suppressed because it is too large Load Diff

View File

@@ -314,7 +314,8 @@
"e2eeDescription": "Lo chiframent del cap a la fin es actualament EXPERIMENTALA. Mercés de gardar a l'esperit qu'activar lo chiframent del cap a la fin desactivarà en efièch los servicis costat servidor coma: l'enregistrament, la difusion en dirècte e las participacions telefonicas. Remembratz tanben que la conferéncia foncionarà pas que per lo monde que participan amb un navigador compatible amb los fluxes inseribles.",
"screenSharingFailed": "Ops! Quicòm a trucat, avèm pas pogut començar lo partiment d'ecran!",
"e2eeWarning": "AVERTIMENT: pas totes los participants d'aquesta conferéncia semblan poder suportar lo chiframent del cap a la fin. Se l'activatz poiràn pas vos veire nimai vos entendre.",
"muteEveryoneElseDialog": "Un còp mut, poiretz pas mai lo tornar la paraula, mas la se pòdon tornar quora vòlgan."
"muteEveryoneElseDialog": "Un còp mut, poiretz pas mai lo tornar la paraula, mas la se pòdon tornar quora vòlgan.",
"user": "utilizaire"
},
"dialOut": {
"statusMessage": "ara es {{status}}"
@@ -549,7 +550,8 @@
"signIn": "Connexion",
"signOut": "Se desconnectar",
"unavailable": "Ops! Lo {{serviceName}} es pas disponible pel moment. Sèm a reglar aqueste problèma. Mercés de tornar ensajar mai tard.",
"unavailableTitle": "Enregistrament indisponible"
"unavailableTitle": "Enregistrament indisponible",
"serviceDescriptionCloud": "Enregistrament distant"
},
"sectionList": {
"pullToRefresh": "Tirar per actualizar"
@@ -616,7 +618,8 @@
},
"startupoverlay": {
"policyText": " ",
"title": "{{app}} a besonh d'utilizar vòstre microfòn e camèra."
"title": "{{app}} a besonh d'utilizar vòstre microfòn e camèra.",
"genericTitle": "Aquesta reünion requerís l'utilizacion del microfòn e de la camèra."
},
"suspendedoverlay": {
"rejoinKeyTitle": "Tornar participar",
@@ -666,7 +669,8 @@
"security": "Opcions de seguretat",
"embedMeeting": "Conferéncia integrada",
"grantModerator": "Passar moderator",
"lobbyButton": "Activar/Desactivar mòde sala d'espèra"
"lobbyButton": "Activar/Desactivar mòde sala d'espèra",
"toggleFilmstrip": "Bascular mòde benda film"
},
"addPeople": "Ajustar de monde a vòstra sonada",
"audioOnlyOff": "Desactivar lo mòde connexion febla",
@@ -791,7 +795,9 @@
"remoteControl": "Contraròtle alonhat",
"show": "Mostrar davant",
"videomute": "Lo participant a arrestat la camèra",
"domuteOthers": "Rendre mut totes los autres"
"domuteOthers": "Rendre mut totes los autres",
"connectionInfo": "Info connexion",
"grantModerator": "Nomenar moderator"
},
"welcomepage": {
"accessibilityLabel": {
@@ -827,7 +833,8 @@
"jitsiOnMobile": "Jitsi sus mobil telecargatz nòstra aplicacion e començatz de conferéncias de pertot",
"moderatedMessage": "O <a href=\"{{url}}\" rel=\"noopener noreferrer\" target=\"_blank\">reservatz una URL de conferéncia</a> a l'avança ont sètz l'unic moderator.",
"jitsiMeet": "Jitsi Meet",
"secureMeetings": "Conferéncias seguras e de nauta qualitat"
"secureMeetings": "Conferéncias seguras e de nauta qualitat",
"headerTitle": "Jitsi Meet"
},
"helpView": {
"header": "Centre dajuda"
@@ -884,7 +891,14 @@
"videoFreezing": "Nos esperam a veire vòstra vidèo se gelar, venir negra e se pixelizar."
},
"premeeting": "Preconferéncia",
"errorMissingName": "Mercés de picar vòstre nom per rejónher la conferéncia"
"errorMissingName": "Mercés de picar vòstre nom per rejónher la conferéncia",
"errorDialOutDisconnected": "Impossible de sonar aqueste numèro. Desconnectat",
"errorDialOutStatus": "Error en recuperant lestat de la sonada sortissenta",
"errorDialOut": "Impossible de sonar aqueste numèro",
"errorDialOutFailed": "Impossible de sonar aqueste numèro. La sonada a fracassat",
"dialInMeeting": "Participatz a la reünion",
"dialInPin": "Participar a la reünion e picar lo còdi PIN :",
"iWantToDialIn": "Vòli me connectar"
},
"lobby": {
"reject": "Regetar",

View File

@@ -404,7 +404,7 @@
"invalidStreamKey": "Canlı akış anahtarı yanlış olabilir.",
"off": "Canlı Akış durduruldu",
"offBy": "{{name}} canlı akışı durdurdu",
"on": "Canlı Akış",
"on": "Canlı Akış başlatıldı",
"onBy": "{{name}} canlı akışı başlattı",
"pending": "Canlı Akış başlatılıyor...",
"serviceName": "Canlı Akış hizmeti",
@@ -564,7 +564,7 @@
"live": "CANLI",
"loggedIn": "{{userName}} olarak giriş yapıldı",
"off": "Kayıt durdu",
"on": "Kaydediliyor",
"on": "Kayıt başladı",
"pending": "Toplantıyı kaydetmeye hazırlanıyor ...",
"rec": "KAYIT",
"serviceDescription": "Kaydınız kayıt hizmeti tarafından kaydedilecektir",
@@ -844,7 +844,7 @@
"sendFeedback": "Geri bildirim gönder",
"terms": "Kurallar",
"title": "Güvenli, tüm özelliklere erişimli ve tamamen ücretsiz görüntülü arama"
},
"lonelyMeetingExperience": {

View File

@@ -298,7 +298,7 @@
"tokenAuthFailed": "对不起,您未被允许参加此会议。",
"tokenAuthFailedTitle": "认证失败",
"transcribing": "转录中",
"unlockRoom": "",
"unlockRoom": "移除会议 $t(lockRoomPassword)",
"userPassword": "用户密码",
"WaitForHostMsg": "会议<b>{{room}}</b>尚未开始。如果您是主持人,请进行身份验证。否则,请等待主持人的到来。",
"WaitForHostMsgWOk": "会议<b>{{room}}</b>尚未开始。如果您是主持人,请进行身份验证。否则,请等待主持人的到来。",

View File

@@ -416,7 +416,7 @@
"invalidStreamKey": "Live stream key may be incorrect.",
"off": "Live Streaming stopped",
"offBy": "{{name}} stopped the live streaming",
"on": "Live Streaming",
"on": "Live Streaming started",
"onBy": "{{name}} started the live streaming",
"pending": "Starting Live Stream...",
"serviceName": "Live Streaming service",
@@ -596,7 +596,7 @@
"loggedIn": "Logged in as {{userName}}",
"off": "Recording stopped",
"offBy": "{{name}} stopped the recording",
"on": "Recording",
"on": "Recording started",
"onBy": "{{name}} started the recording",
"pending": "Preparing to record the meeting...",
"rec": "REC",

View File

@@ -14,7 +14,8 @@ import {
} from '../../react/features/base/conference';
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
import JitsiMeetJS, { JitsiRecordingConstants } from '../../react/features/base/lib-jitsi-meet';
import { pinParticipant } from '../../react/features/base/participants';
import { pinParticipant, getParticipantById } from '../../react/features/base/participants';
import { setPrivateMessageRecipient } from '../../react/features/chat/actions';
import {
processExternalDeviceRequest
} from '../../react/features/device-selection/functions';
@@ -330,6 +331,24 @@ function initCommands() {
} else {
logger.error('No recording or streaming session found');
}
},
'initiate-private-chat': participantId => {
const state = APP.store.getState();
const participant = getParticipantById(state, participantId);
if (participant) {
const { isOpen: isChatOpen } = state['features/chat'];
if (!isChatOpen) {
APP.UI.toggleChat();
}
APP.store.dispatch(setPrivateMessageRecipient(participant));
} else {
logger.error('No participant found for the given participantId');
}
},
'cancel-private-chat': () => {
APP.store.dispatch(setPrivateMessageRecipient());
}
};
transport.on('event', ({ data, name }) => {
@@ -413,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;
}
@@ -564,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;
@@ -576,6 +604,7 @@ class API {
from: id,
message: body,
nick,
privateMessage,
stamp: ts
});
}
@@ -656,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.
@@ -1022,6 +1064,21 @@ class API {
});
}
/**
* Notify external application (if API is enabled) that user updated their hand raised.
*
* @param {string} id - User id.
* @param {boolean} handRaised - Whether user has raised hand.
* @returns {void}
*/
notifyRaiseHandUpdated(id: string, handRaised: boolean) {
this._sendEvent({
name: 'raise-hand-updated',
handRaised,
id
});
}
/**
* Disposes the allocated resources.
*

View File

@@ -28,11 +28,13 @@ const ALWAYS_ON_TOP_FILENAMES = [
*/
const commands = {
avatarUrl: 'avatar-url',
cancelPrivateChat: 'cancel-private-chat',
displayName: 'display-name',
e2eeKey: 'e2ee-key',
email: 'email',
toggleLobby: 'toggle-lobby',
hangup: 'video-hangup',
intiatePrivateChat: 'initiate-private-chat',
muteEveryone: 'mute-everyone',
password: 'password',
pinParticipant: 'pin-participant',
@@ -62,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',
@@ -79,6 +82,7 @@ const events = {
'participant-role-changed': 'participantRoleChanged',
'password-required': 'passwordRequired',
'proxy-connection-event': 'proxyConnectionEvent',
'raise-hand-updated': 'raiseHandUpdated',
'video-ready-to-close': 'readyToClose',
'video-conference-joined': 'videoConferenceJoined',
'video-conference-left': 'videoConferenceLeft',
@@ -722,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);
}

30
package-lock.json generated
View File

@@ -7198,18 +7198,18 @@
}
},
"css-tree": {
"version": "1.0.0-alpha.39",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.39.tgz",
"integrity": "sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz",
"integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==",
"requires": {
"mdn-data": "2.0.6",
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"dependencies": {
"mdn-data": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.6.tgz",
"integrity": "sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA=="
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
},
"source-map": {
"version": "0.6.1",
@@ -10816,8 +10816,8 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"from": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"version": "github:jitsi/lib-jitsi-meet#7896fc8b92f1416f800b56c87e51fb69700d620d",
"from": "github:jitsi/lib-jitsi-meet#7896fc8b92f1416f800b56c87e51fb69700d620d",
"requires": {
"@jitsi/js-utils": "1.0.2",
"@jitsi/sdp-interop": "1.0.3",
@@ -14199,9 +14199,9 @@
"integrity": "sha512-kNhBLv8s6kO2gJJFEKM7qew7oRvJnygjgG1CU2ZEY6SlG5qsRX8z1Ms7z1Oo/XB7fVfyXrAoZDGhIvy+uiByrg=="
},
"react-native-device-info": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-7.3.1.tgz",
"integrity": "sha512-RQP3etbmXsOlcaxHeHNug68nRli02S9iGC7TbaXpkvyyevIuRogfnrI71sWtqmlT91kdpYAOYKmNfRL9LOSKVw=="
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/react-native-device-info/-/react-native-device-info-8.0.0.tgz",
"integrity": "sha512-7/DOEhg8GtyW1hpVtWf8F6RvGLaFaOGmex+IkmiBWQC2uW4NFDcfXm+lMMZnduFavTyUTX7AF6lAM3y286cEfA=="
},
"react-native-immersive": {
"version": "2.0.0",
@@ -14228,9 +14228,9 @@
"integrity": "sha512-Ls9qiNZzW/OLFoI25wfjjAcrf2DZ975hn2vr6U9gyuxi2nooVbzQeFoQS5vQcbCt9QX5NY8ASEEAtlLdIa6KVg=="
},
"react-native-svg": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-10.1.0.tgz",
"integrity": "sha512-mgo6CshQIQrDDBVUPqJK/iOsJEdlagk7N4q8fyo1sqCiSUP2efpt+AQ1IRXZtHXut210/7TliAamvM59NV0Bzg==",
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-12.1.0.tgz",
"integrity": "sha512-1g9qBRci7man8QsHoXn6tP3DhCDiypGgc6+AOWq+Sy+PmP6yiyf8VmvKuoqrPam/tf5x+ZaBT2KI0gl7bptZ7w==",
"requires": {
"css-select": "^2.1.0",
"css-tree": "^1.0.0-alpha.39"

View File

@@ -56,7 +56,7 @@
"jquery-i18next": "1.2.1",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#310983c5b0398d213a2ca054c2eb0e9797fa1ae0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#7896fc8b92f1416f800b56c87e51fb69700d620d",
"libflacjs": "github:mmig/libflac.js#93d37e7f811f01cf7d8b6a603e38bd3c3810907d",
"lodash": "4.17.19",
"moment": "2.19.4",
@@ -75,13 +75,13 @@
"react-native-callstats": "3.61.0",
"react-native-collapsible": "1.5.1",
"react-native-default-preference": "1.4.2",
"react-native-device-info": "7.3.1",
"react-native-device-info": "8.0.0",
"react-native-immersive": "2.0.0",
"react-native-keep-awake": "4.0.0",
"react-native-linear-gradient": "2.5.6",
"react-native-sound": "github:jitsi/react-native-sound#3fe5480fce935e888d5089d94a191c7c7e3aa190",
"react-native-splash-screen": "3.2.0",
"react-native-svg": "10.1.0",
"react-native-svg": "12.1.0",
"react-native-svg-transformer": "0.14.3",
"react-native-url-polyfill": "1.2.0",
"react-native-watch-connectivity": "0.4.3",

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

@@ -80,6 +80,7 @@ export default [
'disableAP',
'disableAudioLevels',
'disableDeepLinking',
'disableInitialGUM',
'disableH264',
'disableHPF',
'disableInviteFunctions',
@@ -89,8 +90,10 @@ export default [
'disableRemoteControl',
'disableRemoteMute',
'disableRtx',
'disableShortcuts',
'disableSimulcast',
'disableThirdPartyRequests',
'disableTileView',
'displayJids',
'doNotStoreRoom',
'e2eping',
@@ -117,6 +120,8 @@ export default [
'forceTurnRelay',
'gatherStats',
'googleApiApplicationClientID',
'hideConferenceSubject',
'hideParticipantsStats',
'hideConferenceTimer',
'hiddenDomain',
'hideLobbyButton',
@@ -128,6 +133,7 @@ export default [
'liveStreamingEnabled',
'localRecording',
'maxFullResolutionParticipants',
'notifications',
'openBridgeChannel',
'openSharedDocumentOnJoin',
'opusMaxAverageBitrate',

View File

@@ -267,6 +267,12 @@ export function setAudioOutputDeviceId(
logger.debug(`setAudioOutputDevice: ${String(newLabel)}[${newId}]`);
if (!JitsiMeetJS.mediaDevices.isDeviceChangeAvailable('output')) {
logger.warn('Adjusting audio output is not supported');
return Promise.resolve();
}
return JitsiMeetJS.mediaDevices.setAudioOutputDevice(newId)
.then(() => {
const newSettings = {

View File

@@ -171,10 +171,6 @@ ColorSchemeRegistry.register('BottomSheet', {
underlayColor: ColorPalette.overflowMenuItemUnderlay
},
expandIcon: {
color: schemeColor('icon')
},
/**
* Bottom sheet's base style.
*/

View File

@@ -67,6 +67,12 @@ const _LANGUAGES = {
main: require('../../../../lang/main-et')
},
// Persian
'fa': {
languages: require('../../../../lang/languages-fa'),
main: require('../../../../lang/main-fa')
},
// Finnish
'fi': {
languages: require('../../../../lang/languages-fi'),

View File

@@ -14,6 +14,7 @@ require('moment/locale/bg');
require('moment/locale/de');
require('moment/locale/eo');
require('moment/locale/es');
require('moment/locale/fa');
require('moment/locale/fr');
require('moment/locale/hy-am');
require('moment/locale/it');

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

@@ -16,7 +16,6 @@ import {
} from './constants';
import { preloadImage } from './preloadImage';
declare var config: Object;
declare var interfaceConfig: Object;
/**
@@ -299,7 +298,7 @@ export function isEveryoneModerator(stateful: Object | Function) {
* @returns {boolean}
*/
export function isIconUrl(icon: ?string | ?Object) {
return Boolean(icon) && typeof icon === 'object';
return Boolean(icon) && (typeof icon === 'object' || typeof icon === 'function');
}
/**

View File

@@ -206,51 +206,53 @@ StateListenerRegistry.register(
state => state['features/base/conference'].conference,
(conference, store) => {
if (conference) {
const propertyHandlers = {
'e2eeEnabled': (participant, value) => _e2eeUpdated(store, conference, participant.getId(), value),
'features_e2ee': (participant, value) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: value
})),
'features_jigasi': (participant, value) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
isJigasi: value
})),
'features_screen-sharing': (participant, value) => // eslint-disable-line no-unused-vars
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
features: { 'screen-sharing': true }
})),
'raisedHand': (participant, value) => _raiseHandUpdated(store, conference, participant.getId(), value),
'remoteControlSessionStatus': (participant, value) =>
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
remoteControlSessionStatus: value
}))
};
// update properties for the participants that are already in the conference
conference.getParticipants().forEach(participant => {
Object.keys(propertyHandlers).forEach(propertyName => {
const value = participant.getProperty(propertyName);
if (value !== undefined) {
propertyHandlers[propertyName](participant, value);
}
});
});
// We joined a conference
conference.on(
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
(participant, propertyName, oldValue, newValue) => {
switch (propertyName) {
case 'e2eeEnabled':
_e2eeUpdated(store, conference, participant.getId(), newValue);
break;
case 'features_e2ee':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
e2eeSupported: newValue
}));
break;
case 'features_jigasi':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
isJigasi: newValue
}));
break;
case 'features_screen-sharing':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
features: { 'screen-sharing': true }
}));
break;
case 'raisedHand': {
_raiseHandUpdated(store, conference, participant.getId(), newValue);
break;
if (propertyHandlers.hasOwnProperty(propertyName)) {
propertyHandlers[propertyName](participant, newValue);
}
case 'remoteControlSessionStatus':
store.dispatch(participantUpdated({
conference,
id: participant.getId(),
remoteControlSessionStatus: newValue
}));
break;
default:
// Ignore for now.
}
});
} else {
const localParticipantId = getLocalParticipant(store.getState).id;
@@ -446,6 +448,10 @@ function _raiseHandUpdated({ dispatch, getState }, conference, participantId, ne
raisedHand
}));
if (typeof APP !== 'undefined') {
APP.API.notifyRaiseHandUpdated(participantId, raisedHand);
}
if (raisedHand) {
dispatch(showNotification({
titleArguments: {

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

@@ -3,14 +3,21 @@
import { Component } from 'react';
import type { Dispatch } from 'redux';
import { isMobileBrowser } from '../../base/environment/utils';
import { getLocalParticipant } from '../../base/participants';
import { sendMessage, toggleChat } from '../actions';
import { DESKTOP_SMALL_WIDTH_THRESHOLD, MOBILE_SMALL_WIDTH_THRESHOLD } from '../constants';
/**
* The type of the React {@code Component} props of {@code AbstractChat}.
*/
export type Props = {
/**
* Whether the chat is opened in a modal or not (computed based on window width).
*/
_isModal: boolean,
/**
* True if the chat window should be rendered.
*/
@@ -106,6 +113,9 @@ export function _mapStateToProps(state: Object) {
const _localParticipant = getLocalParticipant(state);
return {
_isModal: isMobileBrowser()
? window.innerWidth <= MOBILE_SMALL_WIDTH_THRESHOLD
: window.innerWidth <= DESKTOP_SMALL_WIDTH_THRESHOLD,
_isOpen: isOpen,
_messages: messages,
_showNamePrompt: !_localParticipant.name

View File

@@ -11,6 +11,7 @@ import AbstractChat, {
type Props
} from '../AbstractChat';
import ChatDialog from './ChatDialog';
import ChatInput from './ChatInput';
import DisplayNameForm from './DisplayNameForm';
import MessageContainer from './MessageContainer';
@@ -151,16 +152,25 @@ class Chat extends AbstractChat<Props> {
* @returns {ReactElement | null}
*/
_renderPanelContent() {
const { _isOpen, _showNamePrompt } = this.props;
const ComponentToRender = _isOpen
? (
<>
{ this._renderChatHeader() }
{ _showNamePrompt
? <DisplayNameForm /> : this._renderChat() }
</>
)
: null;
const { _isModal, _isOpen, _showNamePrompt } = this.props;
let ComponentToRender = null;
if (_isOpen) {
if (_isModal) {
ComponentToRender = (
<ChatDialog>
{ _showNamePrompt ? <DisplayNameForm /> : this._renderChat() }
</ChatDialog>
);
} else {
ComponentToRender = (
<>
{ this._renderChatHeader() }
{ _showNamePrompt ? <DisplayNameForm /> : this._renderChat() }
</>
);
}
}
let className = '';
if (_isOpen) {

View File

@@ -0,0 +1,37 @@
// @flow
import React from 'react';
import { Dialog } from '../../../base/dialog';
import Header from './ChatDialogHeader';
type Props = {
/**
* Children of the component.
*/
children: React$Node
}
/**
* Component that renders the content of the chat in a modal.
*
* @returns {React$Element<any>}
*/
function ChatDialog({ children }: Props) {
return (
<Dialog
customHeader = { Header }
disableEnter = { true }
hideCancelButton = { true }
submitDisabled = { true }
titleKey = 'chat.title'>
<div className = 'chat-dialog'>
{children}
</div>
</Dialog>
);
}
export default ChatDialog;

View File

@@ -0,0 +1,42 @@
// @flow
import React from 'react';
import { translate } from '../../../base/i18n';
import { Icon, IconClose } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { toggleChat } from '../../../chat';
type Props = {
/**
* Function to be called when pressing the close button.
*/
onCancel: Function,
/**
* Invoked to obtain translated strings.
*/
t: Function
};
/**
* Custom header of the {@code ChatDialog}.
*
* @returns {React$Element<any>}
*/
function Header({ onCancel, t }: Props) {
return (
<div
className = 'chat-dialog-header'>
{ t('chat.title') }
<Icon
onClick = { onCancel }
src = { IconClose } />
</div>
);
}
const mapDispatchToProps = { onCancel: toggleChat };
export default translate(connect(null, mapDispatchToProps)(Header));

View File

@@ -29,3 +29,7 @@ export const MESSAGE_TYPE_LOCAL = 'local';
* The {@code messageType} of remote messages.
*/
export const MESSAGE_TYPE_REMOTE = 'remote';
export const DESKTOP_SMALL_WIDTH_THRESHOLD = 580;
export const MOBILE_SMALL_WIDTH_THRESHOLD = 680;

View File

@@ -19,7 +19,6 @@ import {
import { MiddlewareRegistry, StateListenerRegistry } from '../base/redux';
import { playSound, registerSound, unregisterSound } from '../base/sounds';
import { showToolbox } from '../toolbox/actions';
import { isButtonEnabled } from '../toolbox/functions';
import { SEND_MESSAGE, SET_PRIVATE_MESSAGE_RECIPIENT } from './actionTypes';
import { addMessage, clearMessages, toggleChat } from './actions';
@@ -152,10 +151,9 @@ StateListenerRegistry.register(
* @returns {void}
*/
function _addChatMsgListener(conference, store) {
if ((typeof APP !== 'undefined' && !isButtonEnabled('chat'))
|| store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if the chat button is not enabled in interfaceConfig
// or we are in iAmRecorder mode
if (store.getState()['features/base/config'].iAmRecorder) {
// We don't register anything on web if we are in iAmRecorder mode
return;
}
@@ -249,6 +247,7 @@ function _handleReceivedMessage({ dispatch, getState }, { id, message, privateMe
body: message,
id,
nick: displayName,
privateMessage,
ts: timestamp
});

View File

@@ -95,11 +95,14 @@ class NavigationBar extends Component<Props> {
* @returns {Props}
*/
function _mapStateToProps(state) {
const { hideConferenceTimer, hideConferenceSubject } = state['features/base/config'];
return {
_conferenceTimerEnabled:
getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !state['features/base/config'].hideConferenceTimer,
getFeatureFlag(state, CONFERENCE_TIMER_ENABLED, true) && !hideConferenceTimer,
_meetingName: getConferenceName(state),
_meetingNameEnabled: getFeatureFlag(state, MEETING_NAME_ENABLED, true),
_meetingNameEnabled:
getFeatureFlag(state, MEETING_NAME_ENABLED, true) && !hideConferenceSubject,
_visible: isToolboxVisible(state)
};
}

View File

@@ -25,6 +25,11 @@ type Props = {
*/
_showParticipantCount: boolean,
/**
* Whether the conference subject should be shown or not.
*/
_showSubject: boolean,
/**
* The subject or the of the conference.
* Falls back to conference name.
@@ -51,11 +56,11 @@ class Subject extends Component<Props> {
* @returns {ReactElement}
*/
render() {
const { _hideConferenceTimer, _showParticipantCount, _subject, _visible } = this.props;
const { _hideConferenceTimer, _showParticipantCount, _showSubject, _subject, _visible } = this.props;
return (
<div className = { `subject ${_visible ? 'visible' : ''}` }>
<span className = 'subject-text'>{ _subject }</span>
{ _showSubject && <span className = 'subject-text'>{ _subject }</span>}
{ _showParticipantCount && <ParticipantsCount /> }
{ !_hideConferenceTimer && <ConferenceTimer /> }
</div>
@@ -72,16 +77,19 @@ class Subject extends Component<Props> {
* @returns {{
* _hideConferenceTimer: boolean,
* _showParticipantCount: boolean,
* _showSubject: boolean,
* _subject: string,
* _visible: boolean
* }}
*/
function _mapStateToProps(state) {
const participantCount = getParticipantCount(state);
const { hideConferenceTimer, hideConferenceSubject, hideParticipantsStats } = state['features/base/config'];
return {
_hideConferenceTimer: Boolean(state['features/base/config'].hideConferenceTimer),
_showParticipantCount: participantCount > 2,
_hideConferenceTimer: Boolean(hideConferenceTimer),
_showParticipantCount: participantCount > 2 && !hideParticipantsStats,
_showSubject: !hideConferenceSubject,
_subject: getConferenceName(state),
_visible: isToolboxVisible(state) && participantCount > 1
};

View File

@@ -6,9 +6,9 @@ import { SET_HORIZONTAL_VIEW_DIMENSIONS, SET_TILE_VIEW_DIMENSIONS } from './acti
import { calculateThumbnailSizeForHorizontalView, calculateThumbnailSizeForTileView } from './functions';
/**
* The size of the side margins for each tile as set in CSS.
* The size of the side margins for the entire tile view area.
*/
const TILE_VIEW_SIDE_MARGINS = 10 * 2;
const TILE_VIEW_SIDE_MARGINS = 20;
/**
* Sets the dimensions of the tile view grid.
@@ -24,21 +24,15 @@ const TILE_VIEW_SIDE_MARGINS = 10 * 2;
* dimensions: Object
* }}
*/
export function setTileViewDimensions(
dimensions: Object, windowSize: Object, isChatOpen: boolean, isToolboxVisible: boolean) {
export function setTileViewDimensions(dimensions: Object, windowSize: Object, isChatOpen: boolean) {
const { clientWidth, clientHeight } = windowSize;
let heightToUse = clientHeight;
const heightToUse = clientHeight;
let widthToUse = clientWidth;
if (isChatOpen) {
widthToUse -= CHAT_SIZE;
}
if (isToolboxVisible) {
// The distance from the top and bottom of the screen, to avoid overlapping UI elements.
heightToUse -= 150;
}
const thumbnailSize = calculateThumbnailSizeForTileView({
...dimensions,
clientWidth: widthToUse,

View File

@@ -13,6 +13,7 @@ import { translate } from '../../../base/i18n';
import { Icon, IconMenuDown, IconMenuUp } from '../../../base/icons';
import { connect } from '../../../base/redux';
import { dockToolbox } from '../../../toolbox/actions.web';
import { isButtonEnabled } from '../../../toolbox/functions.web';
import { getCurrentLayout, LAYOUTS } from '../../../video-layout';
import { setFilmstripHovered, setFilmstripVisible } from '../../actions';
import { shouldRemoteVideosBeVisible } from '../../functions';
@@ -197,7 +198,7 @@ class Filmstrip extends Component <Props> {
let toolbar = null;
if (!this.props._hideToolbar) {
if (!this.props._hideToolbar && isButtonEnabled('filmstrip')) {
toolbar = this._renderToggleButton();
}

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

Some files were not shown because too many files have changed in this diff Show More