mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2026-09-28 20:46:58 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fa1b210f5 |
5
Makefile
5
Makefile
@@ -9,7 +9,6 @@ STYLES_BUNDLE = css/all.bundle.css
|
||||
STYLES_DESTINATION = css/all.css
|
||||
STYLES_MAIN = css/main.scss
|
||||
WEBPACK = ./node_modules/.bin/webpack
|
||||
WEBPACK_DEV_SERVER = ./node_modules/.bin/webpack-dev-server
|
||||
|
||||
all: compile deploy clean
|
||||
|
||||
@@ -22,7 +21,6 @@ clean:
|
||||
deploy: deploy-init deploy-appbundle deploy-lib-jitsi-meet deploy-css deploy-local
|
||||
|
||||
deploy-init:
|
||||
rm -fr $(DEPLOY_DIR)
|
||||
mkdir -p $(DEPLOY_DIR)
|
||||
|
||||
deploy-appbundle:
|
||||
@@ -58,9 +56,6 @@ deploy-css:
|
||||
deploy-local:
|
||||
([ ! -x deploy-local.sh ] || ./deploy-local.sh)
|
||||
|
||||
dev: deploy-init deploy-css deploy-lib-jitsi-meet
|
||||
$(WEBPACK_DEV_SERVER)
|
||||
|
||||
source-package:
|
||||
mkdir -p source_package/jitsi-meet/css && \
|
||||
cp -r *.js *.html connection_optimization favicon.ico fonts images libs static sounds LICENSE lang source_package/jitsi-meet && \
|
||||
|
||||
17
README.md
17
README.md
@@ -44,7 +44,7 @@ To build the Jitsi Meet application, just type
|
||||
make
|
||||
```
|
||||
|
||||
### Working with the library sources (lib-jitsi-meet)
|
||||
## Working with the library sources (lib-jitsi-meet)
|
||||
|
||||
By default the library is build from its git repository sources. The default dependency path in package.json is :
|
||||
```json
|
||||
@@ -72,12 +72,12 @@ It allows to link `lib-jitsi-meet` dependency to local source in few steps:
|
||||
```bash
|
||||
cd lib-jitsi-meet
|
||||
|
||||
#### create global symlink for lib-jitsi-meet package
|
||||
# create global symlink for lib-jitsi-meet package
|
||||
npm link
|
||||
|
||||
cd ../jitsi-meet
|
||||
|
||||
#### create symlink from the local node_modules folder to the global lib-jitsi-meet symlink
|
||||
# create symlink from the local node_modules folder to the global lib-jitsi-meet symlink
|
||||
npm link lib-jitsi-meet
|
||||
```
|
||||
|
||||
@@ -90,17 +90,16 @@ cd jitsi-meet
|
||||
npm unlink lib-jitsi-meet
|
||||
npm install
|
||||
```
|
||||
### Running with webpack-dev-server for development
|
||||
## Running with webpack-dev-server for development
|
||||
|
||||
Use it at the CLI, type
|
||||
```
|
||||
make dev
|
||||
node_modules/.bin/webpack-dev-server
|
||||
```
|
||||
|
||||
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable:
|
||||
By default the backend deployment used is `beta.meet.jit.si`, you can point the Jitsi-Meet app at a different backend by using a proxy server. To do this set the WEBPACK_DEV_SERVER_PROXY_TARGET variable, type
|
||||
```
|
||||
export WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com
|
||||
make dev
|
||||
WEBPACK_DEV_SERVER_PROXY_TARGET=https://your-example-server.com node_modules/.bin/webpack-dev-server
|
||||
```
|
||||
|
||||
The app should be running at https://localhost:8080/
|
||||
@@ -123,7 +122,7 @@ network but decrypted on the machine that hosts the bridge.
|
||||
|
||||
The Jitsi Meet architecture allows you to deploy your own version, including
|
||||
all server components, and in that case your security guarantees will be roughly
|
||||
equivalent to these of a direct one-to-one WebRTC call. This is what's unique to
|
||||
equivalent to these of a direct one-to-one WebRTC call. This is what's unique to
|
||||
Jitsi Meet in terms of security.
|
||||
|
||||
The [meet.jit.si](https://meet.jit.si) service is maintained by the Jitsi team
|
||||
|
||||
@@ -22,12 +22,9 @@ import android.util.Log;
|
||||
import org.jitsi.meet.sdk.JitsiMeetActivity;
|
||||
import org.jitsi.meet.sdk.JitsiMeetView;
|
||||
import org.jitsi.meet.sdk.JitsiMeetViewListener;
|
||||
import org.jitsi.meet.sdk.invite.AddPeopleController;
|
||||
import org.jitsi.meet.sdk.invite.InviteControllerListener;
|
||||
|
||||
import com.calendarevents.CalendarEventsPackage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -92,19 +89,6 @@ public class MainActivity extends JitsiMeetActivity {
|
||||
on("LOAD_CONFIG_ERROR", data);
|
||||
}
|
||||
});
|
||||
|
||||
view.getInviteController().setListener(
|
||||
new InviteControllerListener() {
|
||||
public void beginAddPeople(
|
||||
AddPeopleController addPeopleController) {
|
||||
// Log with the tag "ReactNative" in order to have the
|
||||
// log visible in react-native log-android as well.
|
||||
Log.d(
|
||||
"ReactNative",
|
||||
InviteControllerListener.class.getSimpleName()
|
||||
+ ".beginAddPeople");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return view;
|
||||
|
||||
@@ -33,8 +33,6 @@ dependencies {
|
||||
compile project(':react-native-vector-icons')
|
||||
compile project(':react-native-webrtc')
|
||||
compile project(':react-native-calendar-events')
|
||||
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
||||
|
||||
// Build process helpers
|
||||
@@ -71,22 +69,6 @@ gradle.projectsEvaluated {
|
||||
runBefore("processUniversal${buildNameCapitalized}Resources", currentFontTask)
|
||||
runBefore("process${buildNameCapitalized}Resources", currentFontTask)
|
||||
|
||||
def currentSoundsTask = tasks.create(
|
||||
name: "copy${buildNameCapitalized}Sounds",
|
||||
type: Copy) {
|
||||
from("${projectDir}/../../sounds/joined.wav")
|
||||
from("${projectDir}/../../sounds/left.wav")
|
||||
into("${bundlePath}/assets/sounds")
|
||||
}
|
||||
|
||||
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Resources")
|
||||
currentSoundsTask.dependsOn("merge${buildNameCapitalized}Assets")
|
||||
|
||||
runBefore("processArmeabi-v7a${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("processX86${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("processUniversal${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
runBefore("process${buildNameCapitalized}Resources", currentSoundsTask)
|
||||
|
||||
// Bundle JavaScript and React resources.
|
||||
// (adapted from react-native/react.gradle)
|
||||
//
|
||||
|
||||
@@ -111,34 +111,6 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||
return "ExternalAPI";
|
||||
}
|
||||
|
||||
/**
|
||||
* The internal processing for the conference URL set on
|
||||
* a {@link JitsiMeetView} instance.
|
||||
*
|
||||
* @param eventName the name of the external API event to be processed.
|
||||
* @param view the {@link JitsiMeetView} instance.
|
||||
* @param url the "url" attribute value retrieved from the "data" carried by
|
||||
* the event.
|
||||
*/
|
||||
private void maybeSetConferenceUrlOnTheView(
|
||||
String eventName, JitsiMeetView view, String url)
|
||||
{
|
||||
switch(eventName) {
|
||||
case "CONFERENCE_WILL_JOIN":
|
||||
view.setCurrentConferenceUrl(url);
|
||||
break;
|
||||
|
||||
case "CONFERENCE_FAILED":
|
||||
case "CONFERENCE_WILL_LEAVE":
|
||||
case "LOAD_CONFIG_ERROR":
|
||||
// Abandon the conference only if it's for the current URL
|
||||
if (url != null && url.equals(view.getCurrentConferenceUrl())) {
|
||||
view.setCurrentConferenceUrl(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event that occurred on JavaScript to the view's listener.
|
||||
*
|
||||
@@ -158,8 +130,6 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||
return;
|
||||
}
|
||||
|
||||
maybeSetConferenceUrlOnTheView(name, view, data.getString("url"));
|
||||
|
||||
JitsiMeetViewListener listener = view.getListener();
|
||||
|
||||
if (listener == null) {
|
||||
@@ -171,17 +141,7 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||
if (method != null) {
|
||||
try {
|
||||
method.invoke(listener, toHashMap(data));
|
||||
} catch (IllegalAccessException e) {
|
||||
// FIXME There was a multicatch for IllegalAccessException and
|
||||
// InvocationTargetException, but Android Studio complained
|
||||
// with:
|
||||
// "Multi-catch with these reflection exceptions requires
|
||||
// API level 19 (current min is 16) because they get compiled to
|
||||
// the common but new super type ReflectiveOperationException.
|
||||
// As a workaround either create individual catch statements, or
|
||||
// catch Exception."
|
||||
throw new RuntimeException(e);
|
||||
} catch (InvocationTargetException e) {
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -228,7 +229,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
|
||||
if (!super.onKeyUp(keyCode, event)
|
||||
&& BuildConfig.DEBUG
|
||||
&& (reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager())
|
||||
= JitsiMeetView.getReactInstanceManager())
|
||||
!= null
|
||||
&& keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
reactInstanceManager.showDevOptionsDialog();
|
||||
@@ -260,9 +261,7 @@ public class JitsiMeetActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onUserLeaveHint() {
|
||||
if (view != null) {
|
||||
view.onUserLeaveHint();
|
||||
}
|
||||
JitsiMeetView.onUserLeaveHint();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
@@ -28,13 +30,19 @@ import android.widget.FrameLayout;
|
||||
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.ReactRootView;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.rnimmersive.RNImmersiveModule;
|
||||
|
||||
import org.jitsi.meet.sdk.invite.InviteController;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.WeakHashMap;
|
||||
@@ -52,9 +60,28 @@ public class JitsiMeetView extends FrameLayout {
|
||||
*/
|
||||
private final static String TAG = JitsiMeetView.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* React Native bridge. The instance manager allows embedding applications
|
||||
* to create multiple root views off the same JavaScript bundle.
|
||||
*/
|
||||
private static ReactInstanceManager reactInstanceManager;
|
||||
|
||||
private static final Set<JitsiMeetView> views
|
||||
= Collections.newSetFromMap(new WeakHashMap<JitsiMeetView, Boolean>());
|
||||
|
||||
private static List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(
|
||||
new AndroidSettingsModule(reactContext),
|
||||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
public static JitsiMeetView findViewByExternalAPIScope(
|
||||
String externalAPIScope) {
|
||||
synchronized (views) {
|
||||
@@ -68,6 +95,47 @@ public class JitsiMeetView extends FrameLayout {
|
||||
return null;
|
||||
}
|
||||
|
||||
// XXX Strictly internal use only (at the time of this writing)!
|
||||
static ReactInstanceManager getReactInstanceManager() {
|
||||
return reactInstanceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to initialize the React Native instance manager. We
|
||||
* create a single instance in order to load the JavaScript bundle a single
|
||||
* time. All {@code ReactRootView} instances will be tied to the one and
|
||||
* only {@code ReactInstanceManager}.
|
||||
*
|
||||
* @param application {@code Application} instance which is running.
|
||||
*/
|
||||
private static void initReactInstanceManager(Application application) {
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
.setApplication(application)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.addPackage(new com.calendarevents.CalendarEventsPackage())
|
||||
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
|
||||
.addPackage(new com.facebook.react.shell.MainReactPackage())
|
||||
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
|
||||
.addPackage(new com.oblador.vectoricons.VectorIconsPackage())
|
||||
.addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
|
||||
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
|
||||
.addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
|
||||
.addPackage(new com.rnimmersive.RNImmersivePackage())
|
||||
.addPackage(new com.zmxv.RNSound.RNSoundPackage())
|
||||
.addPackage(new ReactPackageAdapter() {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
return JitsiMeetView.createNativeModules(reactContext);
|
||||
}
|
||||
})
|
||||
.setUseDeveloperSupport(BuildConfig.DEBUG)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a specific URL {@code String} in all existing
|
||||
* {@code JitsiMeetView}s.
|
||||
@@ -102,9 +170,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
* implementation.
|
||||
*/
|
||||
public static boolean onBackPressed() {
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager == null) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -121,9 +186,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
* @param activity {@code Activity} being destroyed.
|
||||
*/
|
||||
public static void onHostDestroy(Activity activity) {
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onHostDestroy(activity);
|
||||
}
|
||||
@@ -136,9 +198,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
* @param activity {@code Activity} being paused.
|
||||
*/
|
||||
public static void onHostPause(Activity activity) {
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onHostPause(activity);
|
||||
}
|
||||
@@ -165,9 +224,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
public static void onHostResume(
|
||||
Activity activity,
|
||||
DefaultHardwareBackBtnHandler defaultBackButtonImpl) {
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onHostResume(activity, defaultBackButtonImpl);
|
||||
}
|
||||
@@ -196,22 +252,42 @@ public class JitsiMeetView extends FrameLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
reactInstanceManager.onNewIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the current conference URL. Will have a value when the app is in
|
||||
* a conference.
|
||||
* Activity lifecycle method which should be called from
|
||||
* {@code Activity.onUserLeaveHint} so we can do the required internal
|
||||
* processing.
|
||||
*
|
||||
* Currently one thread writes and one thread reads, so it should be fine to
|
||||
* have this field volatile without additional synchronization.
|
||||
* This is currently not mandatory.
|
||||
*/
|
||||
private volatile String conferenceUrl;
|
||||
public static void onUserLeaveHint() {
|
||||
sendEvent("onUserLeaveHint", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to send an event to JavaScript.
|
||||
*
|
||||
* @param eventName {@code String} containing the event name.
|
||||
* @param params {@code WritableMap} optional ancillary data for the event.
|
||||
*/
|
||||
private static void sendEvent(
|
||||
String eventName,
|
||||
@Nullable WritableMap params) {
|
||||
if (reactInstanceManager != null) {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
reactContext
|
||||
.getJSModule(
|
||||
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The default base {@code URL} used to join a conference when a partial URL
|
||||
@@ -228,12 +304,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
*/
|
||||
private final String externalAPIScope;
|
||||
|
||||
/**
|
||||
* The entry point into the invite feature of Jitsi Meet. The Java
|
||||
* counterpart of the JavaScript {@code InviteButton}.
|
||||
*/
|
||||
private final InviteController inviteController;
|
||||
|
||||
/**
|
||||
* {@link JitsiMeetViewListener} instance for reporting events occurring in
|
||||
* Jitsi Meet.
|
||||
@@ -262,18 +332,15 @@ public class JitsiMeetView extends FrameLayout {
|
||||
|
||||
setBackgroundColor(BACKGROUND_COLOR);
|
||||
|
||||
ReactInstanceManagerHolder.initReactInstanceManager(
|
||||
((Activity) context).getApplication());
|
||||
if (reactInstanceManager == null) {
|
||||
initReactInstanceManager(((Activity) context).getApplication());
|
||||
}
|
||||
|
||||
// Hook this JitsiMeetView into ExternalAPI.
|
||||
externalAPIScope = UUID.randomUUID().toString();
|
||||
synchronized (views) {
|
||||
views.add(this);
|
||||
}
|
||||
|
||||
// The entry point into the invite feature of Jitsi Meet. The Java
|
||||
// counterpart of the JavaScript InviteButton.
|
||||
inviteController = new InviteController(externalAPIScope);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,16 +358,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current conferences URL.
|
||||
*
|
||||
* @return a string with conference URL if the view is currently in
|
||||
* a conference or {@code null} otherwise.
|
||||
*/
|
||||
public String getCurrentConferenceUrl() {
|
||||
return conferenceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default base {@code URL} used to join a conference when a
|
||||
* partial URL (e.g. a room name only) is specified to
|
||||
@@ -314,19 +371,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
return defaultURL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link InviteController} which represents the entry point into
|
||||
* the invite feature of Jitsi Meet and is the Java counterpart of the
|
||||
* JavaScript {@code InviteButton}.
|
||||
*
|
||||
* @return the {@link InviteController} which represents the entry point
|
||||
* into the invite feature of Jitsi Meet and is the Java counterpart of the
|
||||
* JavaScript {@code InviteButton}
|
||||
*/
|
||||
public InviteController getInviteController() {
|
||||
return inviteController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link JitsiMeetViewListener} set on this {@code JitsiMeetView}.
|
||||
*
|
||||
@@ -397,18 +441,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
// externalAPIScope
|
||||
props.putString("externalAPIScope", externalAPIScope);
|
||||
|
||||
// inviteController
|
||||
InviteController inviteController = getInviteController();
|
||||
|
||||
if (inviteController != null) {
|
||||
props.putBoolean(
|
||||
"addPeopleEnabled",
|
||||
inviteController.isAddPeopleEnabled());
|
||||
props.putBoolean(
|
||||
"dialOutEnabled",
|
||||
inviteController.isDialOutEnabled());
|
||||
}
|
||||
|
||||
// pictureInPictureEnabled
|
||||
props.putBoolean(
|
||||
"pictureInPictureEnabled",
|
||||
@@ -436,7 +468,7 @@ public class JitsiMeetView extends FrameLayout {
|
||||
if (reactRootView == null) {
|
||||
reactRootView = new ReactRootView(getContext());
|
||||
reactRootView.startReactApplication(
|
||||
ReactInstanceManagerHolder.getReactInstanceManager(),
|
||||
reactInstanceManager,
|
||||
"App",
|
||||
props);
|
||||
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
|
||||
@@ -466,33 +498,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
loadURLObject(urlObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activity lifecycle method which should be called from
|
||||
* {@code Activity.onUserLeaveHint} so we can do the required internal
|
||||
* processing.
|
||||
*
|
||||
* This is currently not mandatory, but if used will provide automatic
|
||||
* handling of the picture in picture mode when user minimizes the app. It
|
||||
* will be probably the most useful in case the app is using the welcome
|
||||
* page.
|
||||
*/
|
||||
public void onUserLeaveHint() {
|
||||
if (getPictureInPictureEnabled() && conferenceUrl != null) {
|
||||
PictureInPictureModule pipModule
|
||||
= ReactInstanceManagerHolder.getNativeModule(
|
||||
PictureInPictureModule.class);
|
||||
|
||||
if (pipModule != null) {
|
||||
try {
|
||||
pipModule.enterPictureInPicture();
|
||||
} catch (RuntimeException exc) {
|
||||
Log.e(
|
||||
TAG, "onUserLeaveHint: failed to enter PiP mode", exc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the window containing this view gains or loses focus.
|
||||
*
|
||||
@@ -530,17 +535,6 @@ public class JitsiMeetView extends FrameLayout {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current conference URL.
|
||||
*
|
||||
* @param conferenceUrl a string with new conference URL to set if the view
|
||||
* is entering the conference or {@code null} if the view is no longer in
|
||||
* the conference.
|
||||
*/
|
||||
void setCurrentConferenceUrl(String conferenceUrl) {
|
||||
this.conferenceUrl = conferenceUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default base {@code URL} used to join a conference when a
|
||||
* partial URL (e.g. a room name only) is specified to
|
||||
|
||||
@@ -22,46 +22,6 @@ public class PictureInPictureModule extends ReactContextBaseJavaModule {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters Picture-in-Picture (mode) for the current {@link Activity}.
|
||||
* Supported on Android API >= 26 (Oreo) only.
|
||||
*
|
||||
* @throws IllegalStateException if {@link #isPictureInPictureSupported()}
|
||||
* returns {@code false} or if {@link #getCurrentActivity()} returns
|
||||
* {@code null}.
|
||||
* @throws RuntimeException if
|
||||
* {@link Activity#enterPictureInPictureMode(PictureInPictureParams)} fails.
|
||||
* That method can also throw a {@link RuntimeException} in various cases,
|
||||
* including when the activity is not visible (paused or stopped), if the
|
||||
* screen is locked or if the user has an activity pinned.
|
||||
*/
|
||||
public void enterPictureInPicture() {
|
||||
if (!isPictureInPictureSupported()) {
|
||||
throw new IllegalStateException("Picture-in-Picture not supported");
|
||||
}
|
||||
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity == null) {
|
||||
throw new IllegalStateException("No current Activity!");
|
||||
}
|
||||
|
||||
Log.d(TAG, "Entering Picture-in-Picture");
|
||||
|
||||
PictureInPictureParams.Builder builder
|
||||
= new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(1, 1));
|
||||
|
||||
// https://developer.android.com/reference/android/app/Activity.html#enterPictureInPictureMode(android.app.PictureInPictureParams)
|
||||
//
|
||||
// The system may disallow entering picture-in-picture in various cases,
|
||||
// including when the activity is not visible, if the screen is locked
|
||||
// or if the user has an activity pinned.
|
||||
if (!currentActivity.enterPictureInPictureMode(builder.build())) {
|
||||
throw new RuntimeException("Failed to enter Picture-in-Picture");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters Picture-in-Picture (mode) for the current {@link Activity}.
|
||||
* Supported on Android API >= 26 (Oreo) only.
|
||||
@@ -71,12 +31,33 @@ public class PictureInPictureModule extends ReactContextBaseJavaModule {
|
||||
*/
|
||||
@ReactMethod
|
||||
public void enterPictureInPicture(Promise promise) {
|
||||
try {
|
||||
enterPictureInPicture();
|
||||
promise.resolve(null);
|
||||
} catch (RuntimeException re) {
|
||||
promise.reject(re);
|
||||
if (isPictureInPictureSupported()) {
|
||||
Activity currentActivity = getCurrentActivity();
|
||||
|
||||
if (currentActivity == null) {
|
||||
promise.reject(new Exception("No current Activity!"));
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, "Entering Picture-in-Picture");
|
||||
|
||||
PictureInPictureParams.Builder builder
|
||||
= new PictureInPictureParams.Builder()
|
||||
.setAspectRatio(new Rational(1, 1));
|
||||
boolean r
|
||||
= currentActivity.enterPictureInPictureMode(builder.build());
|
||||
|
||||
if (r) {
|
||||
promise.resolve(null);
|
||||
} else {
|
||||
promise.reject(
|
||||
new Exception("Failed to enter Picture-in-Picture"));
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
promise.reject(new Exception("Picture-in-Picture not supported"));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
|
||||
public class ReactContextUtils {
|
||||
public static boolean emitEvent(
|
||||
ReactContext reactContext,
|
||||
String eventName,
|
||||
@Nullable Object data) {
|
||||
if (reactContext == null) {
|
||||
// XXX If no ReactContext is specified, emit through the
|
||||
// ReactContext of ReactInstanceManager. ReactInstanceManager
|
||||
// cooperates with ReactContextUtils i.e. ReactInstanceManager will
|
||||
// not invoke ReactContextUtils without a ReactContext.
|
||||
return ReactInstanceManagerHolder.emitEvent(eventName, data);
|
||||
}
|
||||
|
||||
reactContext
|
||||
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
||||
.emit(eventName, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk;
|
||||
|
||||
import android.app.Application;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.NativeModule;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.common.LifecycleState;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class ReactInstanceManagerHolder {
|
||||
/**
|
||||
* React Native bridge. The instance manager allows embedding applications
|
||||
* to create multiple root views off the same JavaScript bundle.
|
||||
*/
|
||||
private static ReactInstanceManager reactInstanceManager;
|
||||
|
||||
private static List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
return Arrays.<NativeModule>asList(
|
||||
new AndroidSettingsModule(reactContext),
|
||||
new AppInfoModule(reactContext),
|
||||
new AudioModeModule(reactContext),
|
||||
new ExternalAPIModule(reactContext),
|
||||
new PictureInPictureModule(reactContext),
|
||||
new ProximityModule(reactContext),
|
||||
new WiFiStatsModule(reactContext),
|
||||
new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
|
||||
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to send an event to JavaScript.
|
||||
*
|
||||
* @param eventName {@code String} containing the event name.
|
||||
* @param data {@code Object} optional ancillary data for the event.
|
||||
*/
|
||||
public static boolean emitEvent(
|
||||
String eventName,
|
||||
@Nullable Object data) {
|
||||
ReactInstanceManager reactInstanceManager
|
||||
= ReactInstanceManagerHolder.getReactInstanceManager();
|
||||
|
||||
if (reactInstanceManager != null) {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager.getCurrentReactContext();
|
||||
|
||||
return
|
||||
reactContext != null
|
||||
&& ReactContextUtils.emitEvent(
|
||||
reactContext,
|
||||
eventName,
|
||||
data);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a native React module for given class.
|
||||
*
|
||||
* @param nativeModuleClass the native module's class for which an instance
|
||||
* is to be retrieved from the {@link #reactInstanceManager}.
|
||||
* @param <T> the module's type.
|
||||
* @return {@link NativeModule} instance for given interface type or
|
||||
* {@code null} if no instance for this interface is available, or if
|
||||
* {@link #reactInstanceManager} has not been initialized yet.
|
||||
*/
|
||||
static <T extends NativeModule> T getNativeModule(
|
||||
Class<T> nativeModuleClass) {
|
||||
ReactContext reactContext
|
||||
= reactInstanceManager != null
|
||||
? reactInstanceManager.getCurrentReactContext() : null;
|
||||
|
||||
return reactContext != null
|
||||
? reactContext.getNativeModule(nativeModuleClass) : null;
|
||||
}
|
||||
|
||||
static ReactInstanceManager getReactInstanceManager() {
|
||||
return reactInstanceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to initialize the React Native instance manager. We
|
||||
* create a single instance in order to load the JavaScript bundle a single
|
||||
* time. All {@code ReactRootView} instances will be tied to the one and
|
||||
* only {@code ReactInstanceManager}.
|
||||
*
|
||||
* @param application {@code Application} instance which is running.
|
||||
*/
|
||||
static void initReactInstanceManager(Application application) {
|
||||
if (reactInstanceManager != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactInstanceManager
|
||||
= ReactInstanceManager.builder()
|
||||
.setApplication(application)
|
||||
.setBundleAssetName("index.android.bundle")
|
||||
.setJSMainModulePath("index.android")
|
||||
.addPackage(new com.calendarevents.CalendarEventsPackage())
|
||||
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
|
||||
.addPackage(new com.facebook.react.shell.MainReactPackage())
|
||||
.addPackage(new com.i18n.reactnativei18n.ReactNativeI18n())
|
||||
.addPackage(new com.oblador.vectoricons.VectorIconsPackage())
|
||||
.addPackage(new com.ocetnik.timer.BackgroundTimerPackage())
|
||||
.addPackage(new com.oney.WebRTCModule.WebRTCModulePackage())
|
||||
.addPackage(new com.RNFetchBlob.RNFetchBlobPackage())
|
||||
.addPackage(new com.rnimmersive.RNImmersivePackage())
|
||||
.addPackage(new com.zmxv.RNSound.RNSoundPackage())
|
||||
.addPackage(new ReactPackageAdapter() {
|
||||
@Override
|
||||
public List<NativeModule> createNativeModules(
|
||||
ReactApplicationContext reactContext) {
|
||||
return
|
||||
ReactInstanceManagerHolder.createNativeModules(
|
||||
reactContext);
|
||||
}
|
||||
})
|
||||
.setUseDeveloperSupport(BuildConfig.DEBUG)
|
||||
.setInitialLifecycleState(LifecycleState.RESUMED)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk.invite;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.ReadableMap;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableNativeArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Controller object used by native code to query and submit user selections for the user invitation flow.
|
||||
*/
|
||||
public class AddPeopleController {
|
||||
|
||||
/**
|
||||
* The AddPeopleControllerListener for this controller, used to pass query
|
||||
* results back to the native code that initiated the query.
|
||||
*/
|
||||
private AddPeopleControllerListener listener;
|
||||
|
||||
/**
|
||||
* Local cache of search query results. Used to re-hydrate the list
|
||||
* of selected items based on their ids passed to inviteById
|
||||
* in order to pass the full item maps back to the JitsiMeetView during submission.
|
||||
*/
|
||||
private final Map<String, ReadableMap> items = new HashMap<>();
|
||||
|
||||
private final WeakReference<InviteController> owner;
|
||||
|
||||
private final WeakReference<ReactApplicationContext> reactContext;
|
||||
/**
|
||||
* Randomly generated UUID, used for identification in the InviteModule
|
||||
*/
|
||||
private final String uuid = UUID.randomUUID().toString();
|
||||
|
||||
public AddPeopleController(
|
||||
InviteController owner,
|
||||
ReactApplicationContext reactContext) {
|
||||
this.owner = new WeakReference<>(owner);
|
||||
this.reactContext = new WeakReference<>(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the invitation flow and free memory allocated to the
|
||||
* AddPeopleController. After calling this method, this object is invalid -
|
||||
* a new AddPeopleController will be passed to the caller through
|
||||
* beginAddPeople.
|
||||
*/
|
||||
public void endAddPeople() {
|
||||
InviteController owner = this.owner.get();
|
||||
|
||||
if (owner != null) {
|
||||
owner.endAddPeople(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the AddPeopleControllerListener for this controller, used to pass
|
||||
* query results back to the native code that initiated the query.
|
||||
*/
|
||||
public AddPeopleControllerListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
final ReactApplicationContext getReactApplicationContext() {
|
||||
return reactContext.get();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the unique identifier for this AddPeopleController
|
||||
*/
|
||||
public String getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send invites to selected users based on their item ids
|
||||
*
|
||||
* @param ids
|
||||
*/
|
||||
public void inviteById(List<String> ids) {
|
||||
InviteController owner = this.owner.get();
|
||||
|
||||
if (owner != null) {
|
||||
WritableArray invitees = new WritableNativeArray();
|
||||
|
||||
for(int i = 0, size = ids.size(); i < size; i++) {
|
||||
String id = ids.get(i);
|
||||
|
||||
if(items.containsKey(id)) {
|
||||
WritableNativeMap map = new WritableNativeMap();
|
||||
map.merge(items.get(ids));
|
||||
invitees.pushMap(map);
|
||||
} else {
|
||||
// If the id doesn't exist in the map, we can't do anything,
|
||||
// so just skip it.
|
||||
}
|
||||
}
|
||||
|
||||
owner.invite(this, invitees);
|
||||
}
|
||||
}
|
||||
|
||||
void inviteSettled(ReadableArray failedInvitees) {
|
||||
AddPeopleControllerListener listener = getListener();
|
||||
|
||||
if (listener != null) {
|
||||
ArrayList<Map<String, Object>> jFailedInvitees = new ArrayList<>();
|
||||
|
||||
for (int i = 0, size = failedInvitees.size(); i < size; ++i) {
|
||||
jFailedInvitees.add(failedInvitees.getMap(i).toHashMap());
|
||||
}
|
||||
|
||||
listener.inviteSettled(this, jFailedInvitees);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a search for entities to invite with the given query. Results will
|
||||
* be returned through the associated AddPeopleControllerListener's
|
||||
* onReceiveResults method.
|
||||
*
|
||||
* @param query
|
||||
*/
|
||||
public void performQuery(String query) {
|
||||
InviteController owner = this.owner.get();
|
||||
|
||||
if (owner != null) {
|
||||
owner.performQuery(this, query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches results received by the search into a local map for use
|
||||
* later when the items are submitted. Submission requires the full
|
||||
* map of information, but only the IDs are returned back to the delegate.
|
||||
* Using this map means we don't have to send the whole map back to the delegate.
|
||||
*
|
||||
* @param results
|
||||
* @param query
|
||||
*/
|
||||
void receivedResultsForQuery(ReadableArray results, String query) {
|
||||
AddPeopleControllerListener listener = getListener();
|
||||
|
||||
if (listener != null) {
|
||||
List<Map<String, Object>> jvmResults = new ArrayList<>();
|
||||
|
||||
// cache results for use in submission later
|
||||
// convert to jvm array
|
||||
for(int i = 0; i < results.size(); i++) {
|
||||
ReadableMap map = results.getMap(i);
|
||||
|
||||
if(map.hasKey("id")) {
|
||||
items.put(map.getString("id"), map);
|
||||
} else if(map.hasKey("type") && map.getString("type").equals("phone") && map.hasKey("number")) {
|
||||
items.put(map.getString("number"), map);
|
||||
} else {
|
||||
Log.w("AddPeopleController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
|
||||
}
|
||||
|
||||
jvmResults.add(map.toHashMap());
|
||||
}
|
||||
|
||||
listener.onReceiveResults(this, jvmResults, query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the AddPeopleControllerListener for this controller, used to pass
|
||||
* query results back to the native code that initiated the query.
|
||||
*
|
||||
* @param listener
|
||||
*/
|
||||
public void setListener(AddPeopleControllerListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk.invite;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface AddPeopleControllerListener {
|
||||
/**
|
||||
* Called when results are received for a query called through AddPeopleController.query()
|
||||
*
|
||||
* @param addPeopleController
|
||||
* @param results a List of Map<String, Object> objects that represent items returned by the query.
|
||||
* The object at key "type" describes the type of item: "user", "videosipgw" (conference room), or "phone".
|
||||
* "user" types have properties at "id", "name", and "avatar"
|
||||
* "videosipgw" types have properties at "id" and "name"
|
||||
* "phone" types have properties at "number", "title", "and "subtitle"
|
||||
* @param query the query that generated the given results
|
||||
*/
|
||||
void onReceiveResults(AddPeopleController addPeopleController, List<Map<String, Object>> results, String query);
|
||||
|
||||
/**
|
||||
* Called when the call to {@link AddPeopleController#inviteById(List)} completes, but the
|
||||
* invitation fails for one or more of the selected items.
|
||||
*
|
||||
* @param addPeopleController the active {@link AddPeopleController} for this invite flow. This object
|
||||
* should be cleaned up by calling {@link AddPeopleController#endAddPeople()} if
|
||||
* the user exits the invite flow. Otherwise, it can stay active if the user
|
||||
* will attempt to invite
|
||||
* @param failedInvitees a {@code List} of {@code Map<String, Object>} dictionaries that represent the
|
||||
* invitations that failed. The data type of the objects is identical to the results
|
||||
* returned in onReceiveResuls.
|
||||
*/
|
||||
void inviteSettled(AddPeopleController addPeopleController, List<Map<String, Object>> failedInvitees);
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk.invite;
|
||||
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableNativeMap;
|
||||
|
||||
import org.jitsi.meet.sdk.ReactContextUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.FutureTask;
|
||||
|
||||
/**
|
||||
* Represents the entry point into the invite feature of Jitsi Meet and is the
|
||||
* Java counterpart of the JavaScript {@code InviteButton}.
|
||||
*/
|
||||
public class InviteController {
|
||||
private AddPeopleController addPeopleController;
|
||||
|
||||
/**
|
||||
* Whether adding/inviting people by name (as opposed to phone number) is
|
||||
* enabled.
|
||||
*/
|
||||
private Boolean addPeopleEnabled;
|
||||
|
||||
/**
|
||||
* Whether adding/inviting people by phone number (as opposed to name) is
|
||||
* enabled.
|
||||
*/
|
||||
private Boolean dialOutEnabled;
|
||||
|
||||
private final String externalAPIScope;
|
||||
|
||||
private InviteControllerListener listener;
|
||||
|
||||
public InviteController(String externalAPIScope) {
|
||||
this.externalAPIScope = externalAPIScope;
|
||||
}
|
||||
|
||||
public InviteControllerListener getListener() {
|
||||
return listener;
|
||||
}
|
||||
|
||||
void beginAddPeople(ReactApplicationContext reactContext) {
|
||||
InviteControllerListener listener = getListener();
|
||||
|
||||
if (listener != null) {
|
||||
// XXX For the sake of simplicity and in order to reduce the risk of
|
||||
// memory leaks, allow a single AddPeopleController at a time.
|
||||
AddPeopleController addPeopleController = this.addPeopleController;
|
||||
|
||||
if (addPeopleController != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize a new AddPeopleController to represent the click/tap
|
||||
// on the InviteButton and notify the InviteControllerListener
|
||||
// about the event.
|
||||
addPeopleController = new AddPeopleController(this, reactContext);
|
||||
|
||||
boolean success = false;
|
||||
|
||||
this.addPeopleController = addPeopleController;
|
||||
try {
|
||||
listener.beginAddPeople(addPeopleController);
|
||||
success = true;
|
||||
} finally {
|
||||
if (!success) {
|
||||
endAddPeople(addPeopleController);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void endAddPeople(AddPeopleController addPeopleController) {
|
||||
if (this.addPeopleController == addPeopleController) {
|
||||
this.addPeopleController = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends JavaScript event to submit invitations to the given item ids
|
||||
*
|
||||
* @param invitees a WritableArray of WritableNativeMaps representing
|
||||
* selected items. Each map representing a selected item should match the
|
||||
* data passed back in the return from a query.
|
||||
*/
|
||||
boolean invite(
|
||||
AddPeopleController addPeopleController,
|
||||
WritableArray invitees) {
|
||||
return
|
||||
invite(
|
||||
addPeopleController.getUuid(),
|
||||
addPeopleController.getReactApplicationContext(),
|
||||
invitees);
|
||||
}
|
||||
|
||||
public Future<List<Map<String, Object>>> invite(
|
||||
final List<Map<String, Object>> invitees) {
|
||||
final boolean inviteBegan
|
||||
= invite(
|
||||
UUID.randomUUID().toString(),
|
||||
/* reactContext */ null,
|
||||
Arguments.makeNativeArray(invitees));
|
||||
FutureTask futureTask
|
||||
= new FutureTask(new Callable() {
|
||||
@Override
|
||||
public List<Map<String, Object>> call() {
|
||||
if (inviteBegan) {
|
||||
// TODO Complete the returned Future when the invite
|
||||
// settles.
|
||||
return Collections.emptyList();
|
||||
} else {
|
||||
// The invite failed to even begin so report that all
|
||||
// invitees failed.
|
||||
return invitees;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If the invite failed to even begin, complete the returned Future
|
||||
// already and the Future implementation will report that all invitees
|
||||
// failed.
|
||||
if (!inviteBegan) {
|
||||
futureTask.run();
|
||||
}
|
||||
|
||||
return futureTask;
|
||||
}
|
||||
|
||||
private boolean invite(
|
||||
String addPeopleControllerScope,
|
||||
ReactContext reactContext,
|
||||
WritableArray invitees) {
|
||||
WritableNativeMap data = new WritableNativeMap();
|
||||
|
||||
data.putString("addPeopleControllerScope", addPeopleControllerScope);
|
||||
data.putString("externalAPIScope", externalAPIScope);
|
||||
data.putArray("invitees", invitees);
|
||||
|
||||
return
|
||||
ReactContextUtils.emitEvent(
|
||||
reactContext,
|
||||
"org.jitsi.meet:features/invite#invite",
|
||||
data);
|
||||
}
|
||||
|
||||
void inviteSettled(
|
||||
String addPeopleControllerScope,
|
||||
ReadableArray failedInvitees) {
|
||||
AddPeopleController addPeopleController = this.addPeopleController;
|
||||
|
||||
if (addPeopleController != null
|
||||
&& addPeopleController.getUuid().equals(
|
||||
addPeopleControllerScope)) {
|
||||
try {
|
||||
addPeopleController.inviteSettled(failedInvitees);
|
||||
} finally {
|
||||
if (failedInvitees.size() == 0) {
|
||||
endAddPeople(addPeopleController);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAddPeopleEnabled() {
|
||||
Boolean b = this.addPeopleEnabled;
|
||||
|
||||
return
|
||||
(b == null || b.booleanValue()) ? (getListener() != null) : false;
|
||||
}
|
||||
|
||||
public boolean isDialOutEnabled() {
|
||||
Boolean b = this.dialOutEnabled;
|
||||
|
||||
return
|
||||
(b == null || b.booleanValue()) ? (getListener() != null) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a query for users to invite to the conference. Results will be
|
||||
* returned through the {@link AddPeopleControllerListener#onReceiveResults(AddPeopleController, List, String)}
|
||||
* method.
|
||||
*
|
||||
* @param query {@code String} to use for the query
|
||||
*/
|
||||
void performQuery(AddPeopleController addPeopleController, String query) {
|
||||
WritableNativeMap params = new WritableNativeMap();
|
||||
|
||||
params.putString("externalAPIScope", externalAPIScope);
|
||||
params.putString("addPeopleControllerScope", addPeopleController.getUuid());
|
||||
params.putString("query", query);
|
||||
ReactContextUtils.emitEvent(
|
||||
addPeopleController.getReactApplicationContext(),
|
||||
"org.jitsi.meet:features/invite#performQuery",
|
||||
params);
|
||||
}
|
||||
|
||||
void receivedResultsForQuery(
|
||||
String addPeopleControllerScope,
|
||||
String query,
|
||||
ReadableArray results) {
|
||||
AddPeopleController addPeopleController = this.addPeopleController;
|
||||
|
||||
if (addPeopleController != null
|
||||
&& addPeopleController.getUuid().equals(
|
||||
addPeopleControllerScope)) {
|
||||
addPeopleController.receivedResultsForQuery(results, query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the ability to add users to the call is enabled. If this is
|
||||
* enabled, an add user button will appear on the {@link JitsiMeetView}. If
|
||||
* enabled, and the user taps the add user button,
|
||||
* {@link InviteControllerListener#beginAddPeople(AddPeopleController)}
|
||||
* will be called.
|
||||
*
|
||||
* @param addPeopleEnabled {@code true} to enable the add people button;
|
||||
* otherwise, {@code false}
|
||||
*/
|
||||
public void setAddPeopleEnabled(boolean addPeopleEnabled) {
|
||||
this.addPeopleEnabled = Boolean.valueOf(addPeopleEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the ability to add phone numbers to the call is enabled.
|
||||
* Must be enabled along with {@link #setAddPeopleEnabled(boolean)} to be
|
||||
* effective.
|
||||
*
|
||||
* @param dialOutEnabled {@code true} to enable the ability to add phone
|
||||
* numbers to the call; otherwise, {@code false}
|
||||
*/
|
||||
public void setDialOutEnabled(boolean dialOutEnabled) {
|
||||
this.dialOutEnabled = Boolean.valueOf(dialOutEnabled);
|
||||
}
|
||||
|
||||
public void setListener(InviteControllerListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk.invite;
|
||||
|
||||
public interface InviteControllerListener {
|
||||
/**
|
||||
* Called when the add user button is tapped.
|
||||
*
|
||||
* @param addPeopleController {@code AddPeopleController} scoped
|
||||
* for this user invite flow. The {@code AddPeopleController} is used
|
||||
* to start user queries and accepts an {@code AddPeopleControllerListener}
|
||||
* for receiving user query responses.
|
||||
*/
|
||||
void beginAddPeople(AddPeopleController addPeopleController);
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.jitsi.meet.sdk.invite;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
import com.facebook.react.bridge.ReadableArray;
|
||||
|
||||
import org.jitsi.meet.sdk.JitsiMeetView;
|
||||
|
||||
/**
|
||||
* Implements the react-native module of the feature invite.
|
||||
*/
|
||||
public class InviteModule extends ReactContextBaseJavaModule {
|
||||
public InviteModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that a click/tap has been performed on {@code InviteButton} and
|
||||
* that the execution flow for adding/inviting people to the current
|
||||
* conference/meeting is to begin
|
||||
*
|
||||
* @param externalAPIScope the unique identifier of the
|
||||
* {@code JitsiMeetView} whose {@code InviteButton} was clicked/tapped.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void beginAddPeople(String externalAPIScope) {
|
||||
InviteController inviteController
|
||||
= findInviteControllerByExternalAPIScope(externalAPIScope);
|
||||
|
||||
if (inviteController != null) {
|
||||
inviteController.beginAddPeople(getReactApplicationContext());
|
||||
}
|
||||
}
|
||||
|
||||
private InviteController findInviteControllerByExternalAPIScope(
|
||||
String externalAPIScope) {
|
||||
JitsiMeetView view
|
||||
= JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
|
||||
|
||||
return view == null ? null : view.getInviteController();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Invite";
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for invitation failures
|
||||
*
|
||||
* @param failedInvitees the items for which the invitation failed
|
||||
* @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController
|
||||
*/
|
||||
@ReactMethod
|
||||
public void inviteSettled(
|
||||
String externalAPIScope,
|
||||
String addPeopleControllerScope,
|
||||
ReadableArray failedInvitees) {
|
||||
InviteController inviteController
|
||||
= findInviteControllerByExternalAPIScope(externalAPIScope);
|
||||
|
||||
if (inviteController == null) {
|
||||
Log.w(
|
||||
"InviteModule",
|
||||
"Invite settled, but failed to find active controller to notify");
|
||||
} else {
|
||||
inviteController.inviteSettled(
|
||||
addPeopleControllerScope,
|
||||
failedInvitees);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for results received from the JavaScript invite search call
|
||||
*
|
||||
* @param results the results in a ReadableArray of ReadableMap objects
|
||||
* @param query the query associated with the search
|
||||
* @param addPeopleControllerScope a string that represents a connection to a specific AddPeopleController
|
||||
*/
|
||||
@ReactMethod
|
||||
public void receivedResults(
|
||||
String externalAPIScope,
|
||||
String addPeopleControllerScope,
|
||||
String query,
|
||||
ReadableArray results) {
|
||||
InviteController inviteController
|
||||
= findInviteControllerByExternalAPIScope(externalAPIScope);
|
||||
|
||||
if (inviteController == null) {
|
||||
Log.w(
|
||||
"InviteModule",
|
||||
"Received results, but failed to find active controller to send results back");
|
||||
} else {
|
||||
inviteController.receivedResultsForQuery(
|
||||
addPeopleControllerScope,
|
||||
query,
|
||||
results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,238 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Constructs IPv6 addresses for IPv4 addresses in the NAT64 environment.
|
||||
*
|
||||
* NAT64 translates IPv4 to IPv6 addresses by adding "well known" prefix and
|
||||
* suffix configured by the administrator. Those are figured out by discovering
|
||||
* both IPv6 and IPv4 addresses of a host and then trying to find a place where
|
||||
* the IPv4 address fits into the format described here:
|
||||
* https://tools.ietf.org/html/rfc6052#section-2.2
|
||||
*/
|
||||
public class NAT64AddrInfo {
|
||||
/**
|
||||
* Coverts bytes array to upper case HEX string.
|
||||
*
|
||||
* @param bytes an array of bytes to be converted
|
||||
* @return ex. "010AFF" for an array of {1, 10, 255}.
|
||||
*/
|
||||
static String bytesToHexString(byte[] bytes) {
|
||||
StringBuilder hexStr = new StringBuilder();
|
||||
|
||||
for (byte b : bytes) {
|
||||
hexStr.append(String.format("%02X", b));
|
||||
}
|
||||
|
||||
return hexStr.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to discover the NAT64 prefix/suffix based on the IPv4 and IPv6
|
||||
* addresses resolved for given {@code host}.
|
||||
*
|
||||
* @param host the host for which the code will try to discover IPv4 and
|
||||
* IPv6 addresses which then will be used to figure out the NAT64 prefix.
|
||||
* @return {@link NAT64AddrInfo} instance if the NAT64 prefix/suffix was
|
||||
* successfully discovered or {@code null} if it failed for any reason.
|
||||
* @throws UnknownHostException thrown by {@link InetAddress#getAllByName}.
|
||||
*/
|
||||
public static NAT64AddrInfo discover(String host)
|
||||
throws UnknownHostException {
|
||||
InetAddress ipv4 = null;
|
||||
InetAddress ipv6 = null;
|
||||
|
||||
for(InetAddress addr : InetAddress.getAllByName(host)) {
|
||||
byte[] bytes = addr.getAddress();
|
||||
|
||||
if (bytes.length == 4) {
|
||||
ipv4 = addr;
|
||||
} else if (bytes.length == 16) {
|
||||
ipv6 = addr;
|
||||
}
|
||||
}
|
||||
|
||||
if (ipv4 != null && ipv6 != null) {
|
||||
return figureOutNAT64AddrInfo(ipv4.getAddress(), ipv6.getAddress());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on IPv4 and IPv6 addresses of the same host, the method will make
|
||||
* an attempt to figure out what are the NAT64 prefix and suffix.
|
||||
*
|
||||
* @param ipv4AddrBytes the IPv4 address of the same host in NAT64 network,
|
||||
* as returned by {@link InetAddress#getAddress()}.
|
||||
* @param ipv6AddrBytes the IPv6 address of the same host in NAT64 network,
|
||||
* as returned by {@link InetAddress#getAddress()}.
|
||||
* @return {@link NAT64AddrInfo} instance which contains the prefix/suffix
|
||||
* of the current NAT64 network or {@code null} if the prefix could not be
|
||||
* found.
|
||||
*/
|
||||
static NAT64AddrInfo figureOutNAT64AddrInfo(
|
||||
byte[] ipv4AddrBytes,
|
||||
byte[] ipv6AddrBytes) {
|
||||
String ipv6Str = bytesToHexString(ipv6AddrBytes);
|
||||
String ipv4Str = bytesToHexString(ipv4AddrBytes);
|
||||
|
||||
// NAT64 address format:
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |PL| 0-------------32--40--48--56--64--72--80--88--96--104---------|
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |32| prefix |v4(32) | u | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |40| prefix |v4(24) | u |(8)| suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |48| prefix |v4(16) | u | (16) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |56| prefix |(8)| u | v4(24) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |64| prefix | u | v4(32) | suffix |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
// |96| prefix | v4(32) |
|
||||
// +--+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
|
||||
int prefixLength = 96;
|
||||
int suffixLength = 0;
|
||||
String prefix = null;
|
||||
String suffix = null;
|
||||
|
||||
if (ipv4Str.equalsIgnoreCase(ipv6Str.substring(prefixLength / 4))) {
|
||||
prefix = ipv6Str.substring(0, prefixLength / 4);
|
||||
} else {
|
||||
// Cut out the 'u' octet
|
||||
ipv6Str = ipv6Str.substring(0, 16) + ipv6Str.substring(18);
|
||||
|
||||
for (prefixLength = 64, suffixLength = 6; prefixLength >= 32; ) {
|
||||
if (ipv4Str.equalsIgnoreCase(
|
||||
ipv6Str.substring(
|
||||
prefixLength / 4, prefixLength / 4 + 8))) {
|
||||
prefix = ipv6Str.substring(0, prefixLength / 4);
|
||||
suffix = ipv6Str.substring(ipv6Str.length() - suffixLength);
|
||||
break;
|
||||
}
|
||||
|
||||
prefixLength -= 8;
|
||||
suffixLength += 2;
|
||||
}
|
||||
}
|
||||
|
||||
return prefix != null ? new NAT64AddrInfo(prefix, suffix) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* An overload for {@link #hexStringToIPv6String(StringBuilder)}.
|
||||
*
|
||||
* @param hexStr a hex representation of IPv6 address bytes.
|
||||
* @return an IPv6 address string.
|
||||
*/
|
||||
static String hexStringToIPv6String(String hexStr) {
|
||||
return hexStringToIPv6String(new StringBuilder(hexStr));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts from HEX representation of IPv6 address bytes into IPv6 address
|
||||
* string which includes the ':' signs.
|
||||
*
|
||||
* @param str a hex representation of IPv6 address bytes.
|
||||
* @return eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
|
||||
*/
|
||||
static String hexStringToIPv6String(StringBuilder str) {
|
||||
for (int i = 32 - 4; i > 0; i -= 4) {
|
||||
str.insert(i, ":");
|
||||
}
|
||||
|
||||
return str.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an IPv4 address string and returns it's byte array representation.
|
||||
*
|
||||
* @param ipv4Address eg. '192.168.3.23'
|
||||
* @return byte representation of given IPv4 address string.
|
||||
* @throws IllegalArgumentException if the address is not in valid format.
|
||||
*/
|
||||
static byte[] ipv4AddressStringToBytes(String ipv4Address) {
|
||||
InetAddress address;
|
||||
|
||||
try {
|
||||
address = InetAddress.getByName(ipv4Address);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid IP address: " + ipv4Address, e);
|
||||
}
|
||||
|
||||
byte[] bytes = address.getAddress();
|
||||
|
||||
if (bytes.length != 4) {
|
||||
throw new IllegalArgumentException(
|
||||
"Not an IPv4 address: " + ipv4Address);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The NAT64 prefix added to construct IPv6 from an IPv4 address.
|
||||
*/
|
||||
private final String prefix;
|
||||
|
||||
/**
|
||||
* The NAT64 suffix (if any) used to construct IPv6 from an IPv4 address.
|
||||
*/
|
||||
private final String suffix;
|
||||
|
||||
/**
|
||||
* Creates new instance of {@link NAT64AddrInfo}.
|
||||
*
|
||||
* @param prefix the NAT64 prefix.
|
||||
* @param suffix the NAT64 suffix.
|
||||
*/
|
||||
private NAT64AddrInfo(String prefix, String suffix) {
|
||||
this.prefix = prefix;
|
||||
this.suffix = suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the NAT64 prefix and suffix will create an IPv6 representation
|
||||
* of the given IPv4 address.
|
||||
*
|
||||
* @param ipv4Address eg. '192.34.2.3'
|
||||
* @return IPv6 address string eg. FE80:CD00:0000:0CDA:1357:0000:212F:749C
|
||||
* @throws IllegalArgumentException if given string is not a valid IPv4
|
||||
* address.
|
||||
*/
|
||||
public String getIPv6Address(String ipv4Address) {
|
||||
byte[] ipv4AddressBytes = ipv4AddressStringToBytes(ipv4Address);
|
||||
StringBuilder newIPv6Str = new StringBuilder();
|
||||
|
||||
newIPv6Str.append(prefix);
|
||||
newIPv6Str.append(bytesToHexString(ipv4AddressBytes));
|
||||
|
||||
if (suffix != null) {
|
||||
// Insert the 'u' octet.
|
||||
newIPv6Str.insert(16, "00");
|
||||
newIPv6Str.append(suffix);
|
||||
}
|
||||
|
||||
return hexStringToIPv6String(newIPv6Str);
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.react.bridge.Promise;
|
||||
import com.facebook.react.bridge.ReactApplicationContext;
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||
import com.facebook.react.bridge.ReactMethod;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* This module exposes the functionality of creating an IPv6 representation
|
||||
* of IPv4 addresses in NAT64 environment.
|
||||
*
|
||||
* See[1] and [2] for more info on what NAT64 is.
|
||||
* [1]: https://tools.ietf.org/html/rfc6146
|
||||
* [2]: https://tools.ietf.org/html/rfc6052
|
||||
*/
|
||||
public class NAT64AddrInfoModule extends ReactContextBaseJavaModule {
|
||||
/**
|
||||
* The host for which the module wil try to resolve both IPv4 and IPv6
|
||||
* addresses in order to figure out the NAT64 prefix.
|
||||
*/
|
||||
private final static String HOST = "nat64.jitsi.net";
|
||||
|
||||
/**
|
||||
* How long is the {@link NAT64AddrInfo} instance valid.
|
||||
*/
|
||||
private final static long INFO_LIFETIME = 60 * 1000;
|
||||
|
||||
/**
|
||||
* The name of this module.
|
||||
*/
|
||||
private final static String MODULE_NAME = "NAT64AddrInfo";
|
||||
|
||||
/**
|
||||
* The {@code Log} tag {@code NAT64AddrInfoModule} is to log messages with.
|
||||
*/
|
||||
private final static String TAG = MODULE_NAME;
|
||||
|
||||
/**
|
||||
* The {@link NAT64AddrInfo} instance which holds NAT64 prefix/suffix.
|
||||
*/
|
||||
private NAT64AddrInfo info;
|
||||
|
||||
/**
|
||||
* When {@link #info} was created.
|
||||
*/
|
||||
private long infoTimestamp;
|
||||
|
||||
/**
|
||||
* Creates new {@link NAT64AddrInfoModule}.
|
||||
*
|
||||
* @param reactContext the react context to be used by the new module
|
||||
* instance.
|
||||
*/
|
||||
public NAT64AddrInfoModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to obtain IPv6 address for given IPv4 address in NAT64 environment.
|
||||
*
|
||||
* @param ipv4Address IPv4 address string.
|
||||
* @param promise a {@link Promise} which will be resolved either with IPv6
|
||||
* address for given IPv4 address or with {@code null} if no
|
||||
* {@link NAT64AddrInfo} was resolved for the current network. Will be
|
||||
* rejected if given {@code ipv4Address} is not a valid IPv4 address.
|
||||
*/
|
||||
@ReactMethod
|
||||
public void getIPv6Address(String ipv4Address, final Promise promise) {
|
||||
// Reset if cached for too long.
|
||||
if (System.currentTimeMillis() - infoTimestamp > INFO_LIFETIME) {
|
||||
info = null;
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
String host = HOST;
|
||||
|
||||
try {
|
||||
info = NAT64AddrInfo.discover(host);
|
||||
} catch (UnknownHostException e) {
|
||||
Log.e(TAG, "NAT64AddrInfo.discover: " + host, e);
|
||||
}
|
||||
infoTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
String result;
|
||||
|
||||
try {
|
||||
result = info == null ? null : info.getIPv6Address(ipv4Address);
|
||||
} catch (IllegalArgumentException exc) {
|
||||
Log.e(TAG, "Failed to get IPv6 address for: " + ipv4Address, exc);
|
||||
|
||||
// We don't want to reject. It's not a big deal if there's no IPv6
|
||||
// address resolved.
|
||||
result = null;
|
||||
}
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MODULE_NAME;
|
||||
}
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.jitsi.meet.sdk.net;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Tests for {@link NAT64AddrInfo} class.
|
||||
*/
|
||||
public class NAT64AddrInfoTest {
|
||||
/**
|
||||
* Test case for the 96 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test96Prefix() {
|
||||
testPrefixSuffix(
|
||||
"260777000000000400000000", "", "203.0.113.1", "23.17.23.3");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 64 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test64Prefix() {
|
||||
String prefix = "1FF2A227B3AAF3D2";
|
||||
String suffix = "BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "48.46.87.34", "23.87.145.4");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 56 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test56Prefix() {
|
||||
String prefix = "1FF2A227B3AAF3";
|
||||
String suffix = "A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "34.72.234.255", "1.235.3.65");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 48 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test48Prefix() {
|
||||
String prefix = "1FF2A227B3AA";
|
||||
String suffix = "72A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "97.54.3.23", "77.49.0.33");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 40 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test40Prefix() {
|
||||
String prefix = "1FF2A227B3";
|
||||
String suffix = "D972A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "10.23.56.121", "97.65.32.21");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test case for the 32 prefix length.
|
||||
*/
|
||||
@Test
|
||||
public void test32Prefix()
|
||||
throws UnknownHostException {
|
||||
String prefix = "1FF2A227";
|
||||
String suffix = "20D972A2BB87C8";
|
||||
|
||||
testPrefixSuffix(prefix, suffix, "162.63.65.189", "135.222.84.206");
|
||||
}
|
||||
|
||||
private static String buildIPv6Addr(
|
||||
String prefix, String suffix, String ipv4Hex) {
|
||||
String ipv6Str = prefix + ipv4Hex + suffix;
|
||||
|
||||
if (suffix.length() > 0) {
|
||||
ipv6Str = new StringBuilder(ipv6Str).insert(16, "00").toString();
|
||||
}
|
||||
|
||||
return ipv6Str;
|
||||
}
|
||||
|
||||
private void testPrefixSuffix(
|
||||
String prefix, String suffix, String ipv4, String otherIPv4) {
|
||||
byte[] ipv4Bytes = NAT64AddrInfo.ipv4AddressStringToBytes(ipv4);
|
||||
String ipv4String = NAT64AddrInfo.bytesToHexString(ipv4Bytes);
|
||||
String ipv6Str = buildIPv6Addr(prefix, suffix, ipv4String);
|
||||
|
||||
BigInteger ipv6Address = new BigInteger(ipv6Str, 16);
|
||||
|
||||
NAT64AddrInfo nat64AddrInfo
|
||||
= NAT64AddrInfo.figureOutNAT64AddrInfo(
|
||||
ipv4Bytes, ipv6Address.toByteArray());
|
||||
|
||||
assertNotNull("Failed to figure out NAT64 info", nat64AddrInfo);
|
||||
|
||||
String newIPv6 = nat64AddrInfo.getIPv6Address(ipv4);
|
||||
|
||||
assertEquals(
|
||||
NAT64AddrInfo.hexStringToIPv6String(ipv6Address.toString(16)),
|
||||
newIPv6);
|
||||
|
||||
byte[] ipv4Addr2 = NAT64AddrInfo.ipv4AddressStringToBytes(otherIPv4);
|
||||
String ipv4Addr2Hex = NAT64AddrInfo.bytesToHexString(ipv4Addr2);
|
||||
|
||||
newIPv6 = nat64AddrInfo.getIPv6Address(otherIPv4);
|
||||
|
||||
assertEquals(
|
||||
NAT64AddrInfo.hexStringToIPv6String(
|
||||
buildIPv6Addr(prefix, suffix, ipv4Addr2Hex)),
|
||||
newIPv6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidIPv4Format() {
|
||||
testInvalidIPv4Format("256.1.2.3");
|
||||
testInvalidIPv4Format("FE80:CD00:0000:0CDA:1357:0000:212F:749C");
|
||||
}
|
||||
|
||||
private void testInvalidIPv4Format(String ipv4Str) {
|
||||
try {
|
||||
NAT64AddrInfo.ipv4AddressStringToBytes(ipv4Str);
|
||||
fail("Did not throw IllegalArgumentException");
|
||||
} catch (IllegalArgumentException exc) {
|
||||
/* OK */
|
||||
}
|
||||
}
|
||||
}
|
||||
198
conference.js
198
conference.js
@@ -21,13 +21,13 @@ import {
|
||||
createSelectParticipantFailedEvent,
|
||||
createStreamSwitchDelayEvent,
|
||||
createTrackMutedEvent,
|
||||
initAnalytics,
|
||||
sendAnalytics
|
||||
} from './react/features/analytics';
|
||||
import {
|
||||
redirectWithStoredParams,
|
||||
reloadWithStoredParams
|
||||
} from './react/features/app';
|
||||
import { updateRecordingState } from './react/features/recording';
|
||||
|
||||
import EventEmitter from 'events';
|
||||
|
||||
@@ -43,11 +43,11 @@ import {
|
||||
lockStateChanged,
|
||||
onStartMutedPolicyChanged,
|
||||
p2pStatusChanged,
|
||||
sendLocalParticipant,
|
||||
setDesktopSharingEnabled
|
||||
sendLocalParticipant
|
||||
} from './react/features/base/conference';
|
||||
import { updateDeviceList } from './react/features/base/devices';
|
||||
import {
|
||||
isAnalyticsEnabled,
|
||||
isFatalJitsiConnectionError,
|
||||
JitsiConferenceErrors,
|
||||
JitsiConferenceEvents,
|
||||
@@ -93,6 +93,7 @@ import {
|
||||
getLocationContextRoot,
|
||||
getJitsiMeetGlobalNS
|
||||
} from './react/features/base/util';
|
||||
import { statsEmitter } from './react/features/connection-indicator';
|
||||
import { showDesktopPicker } from './react/features/desktop-picker';
|
||||
import { appendSuffix } from './react/features/display-name';
|
||||
import {
|
||||
@@ -103,8 +104,10 @@ import {
|
||||
mediaPermissionPromptVisibilityChanged,
|
||||
suspendDetected
|
||||
} from './react/features/overlay';
|
||||
import { setSharedVideoStatus } from './react/features/shared-video';
|
||||
import { isButtonEnabled } from './react/features/toolbox';
|
||||
import {
|
||||
isButtonEnabled,
|
||||
showDesktopSharingButton
|
||||
} from './react/features/toolbox';
|
||||
|
||||
const logger = require('jitsi-meet-logger').getLogger(__filename);
|
||||
|
||||
@@ -231,14 +234,13 @@ function maybeRedirectToWelcomePage(options) {
|
||||
}));
|
||||
}
|
||||
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec, if
|
||||
// there is a thank you message to be shown, 0.5s otherwise.
|
||||
// if Welcome page is enabled redirect to welcome page after 3 sec.
|
||||
if (config.enableWelcomePage) {
|
||||
setTimeout(
|
||||
() => {
|
||||
APP.store.dispatch(redirectWithStoredParams('/'));
|
||||
},
|
||||
options.showThankYou ? 3000 : 500);
|
||||
3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -503,6 +505,16 @@ export default {
|
||||
*/
|
||||
desktopSharingDisabledTooltip: null,
|
||||
|
||||
/*
|
||||
* Whether the local "raisedHand" flag is on.
|
||||
*/
|
||||
isHandRaised: false,
|
||||
|
||||
/*
|
||||
* Whether the local participant is the dominant speaker in the conference.
|
||||
*/
|
||||
isDominantSpeaker: false,
|
||||
|
||||
/**
|
||||
* The local audio track (if any).
|
||||
* FIXME tracks from redux store should be the single source of truth
|
||||
@@ -683,20 +695,53 @@ export default {
|
||||
/**
|
||||
* Open new connection and join to the conference.
|
||||
* @param {object} options
|
||||
* @param {string} roomName - The name of the conference.
|
||||
* @param {string} roomName name of the conference
|
||||
* @returns {Promise}
|
||||
*/
|
||||
init(options) {
|
||||
this.roomName = options.roomName;
|
||||
|
||||
// attaches global error handler, if there is already one, respect it
|
||||
if (JitsiMeetJS.getGlobalOnErrorHandler) {
|
||||
const oldOnErrorHandler = window.onerror;
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
window.onerror = (message, source, lineno, colno, error) => {
|
||||
JitsiMeetJS.getGlobalOnErrorHandler(
|
||||
message, source, lineno, colno, error);
|
||||
|
||||
if (oldOnErrorHandler) {
|
||||
oldOnErrorHandler(message, source, lineno, colno, error);
|
||||
}
|
||||
};
|
||||
|
||||
const oldOnUnhandledRejection = window.onunhandledrejection;
|
||||
|
||||
window.onunhandledrejection = function(event) {
|
||||
JitsiMeetJS.getGlobalOnErrorHandler(
|
||||
null, null, null, null, event.reason);
|
||||
|
||||
if (oldOnUnhandledRejection) {
|
||||
oldOnUnhandledRejection(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
this.createInitialLocalTracksAndConnect(
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted,
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
})
|
||||
JitsiMeetJS.init({
|
||||
enableAnalyticsLogging: isAnalyticsEnabled(APP.store),
|
||||
...config
|
||||
}).then(() => {
|
||||
initAnalytics(APP.store);
|
||||
|
||||
return this.createInitialLocalTracksAndConnect(
|
||||
options.roomName, {
|
||||
startAudioOnly: config.startAudioOnly,
|
||||
startScreenSharing: config.startScreenSharing,
|
||||
startWithAudioMuted: config.startWithAudioMuted,
|
||||
startWithVideoMuted: config.startWithVideoMuted
|
||||
});
|
||||
})
|
||||
.then(([ tracks, con ]) => {
|
||||
tracks.forEach(track => {
|
||||
if ((track.isAudioTrack() && this.isLocalAudioMuted())
|
||||
@@ -728,8 +773,7 @@ export default {
|
||||
JitsiMeetConferenceEvents.DESKTOP_SHARING_ENABLED_CHANGED,
|
||||
this.isDesktopSharingEnabled);
|
||||
|
||||
APP.store.dispatch(
|
||||
setDesktopSharingEnabled(this.isDesktopSharingEnabled));
|
||||
APP.store.dispatch(showDesktopSharingButton());
|
||||
|
||||
this._createRoom(tracks);
|
||||
APP.remoteControl.init();
|
||||
@@ -1320,6 +1364,7 @@ export default {
|
||||
this.isSharingScreen = newStream && newStream.videoType === 'desktop';
|
||||
|
||||
if (wasSharingScreen !== this.isSharingScreen) {
|
||||
APP.UI.updateDesktopSharingButtons();
|
||||
APP.API.notifyScreenSharingStatusChanged(this.isSharingScreen);
|
||||
}
|
||||
},
|
||||
@@ -1342,6 +1387,37 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Triggers a tooltip to display when a feature was attempted to be used
|
||||
* while in audio only mode.
|
||||
*
|
||||
* @param {string} featureName - The name of the feature that attempted to
|
||||
* toggle.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_displayAudioOnlyTooltip(featureName) {
|
||||
let buttonName = null;
|
||||
let tooltipElementId = null;
|
||||
|
||||
switch (featureName) {
|
||||
case 'screenShare':
|
||||
buttonName = 'desktop';
|
||||
tooltipElementId = 'screenshareWhileAudioOnly';
|
||||
break;
|
||||
case 'videoMute':
|
||||
buttonName = 'camera';
|
||||
tooltipElementId = 'unmuteWhileAudioOnly';
|
||||
break;
|
||||
}
|
||||
|
||||
if (tooltipElementId) {
|
||||
APP.UI.showToolbar(6000);
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
buttonName, tooltipElementId, true, 5000);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether or not the conference is currently in audio only mode.
|
||||
*
|
||||
@@ -1452,6 +1528,8 @@ export default {
|
||||
}
|
||||
|
||||
if (this.isAudioOnly()) {
|
||||
this._displayAudioOnlyTooltip('screenShare');
|
||||
|
||||
return Promise.reject('No screensharing in audio only mode');
|
||||
}
|
||||
|
||||
@@ -1793,6 +1871,9 @@ export default {
|
||||
|
||||
room.on(JitsiConferenceEvents.TALK_WHILE_MUTED, () => {
|
||||
APP.UI.showToolbar(6000);
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'microphone', 'talkWhileMutedPopup', true, 5000);
|
||||
});
|
||||
|
||||
room.on(
|
||||
@@ -1815,13 +1896,20 @@ export default {
|
||||
});
|
||||
room.on(JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, id => {
|
||||
APP.store.dispatch(dominantSpeakerChanged(id));
|
||||
});
|
||||
|
||||
room.on(JitsiConferenceEvents.LIVE_STREAM_URL_CHANGED,
|
||||
(from, liveStreamViewURL) =>
|
||||
APP.store.dispatch(updateRecordingState({
|
||||
liveStreamViewURL
|
||||
})));
|
||||
if (this.isLocalId(id)) {
|
||||
this.isDominantSpeaker = true;
|
||||
this.setRaisedHand(false);
|
||||
} else {
|
||||
this.isDominantSpeaker = false;
|
||||
const participant = room.getParticipantById(id);
|
||||
|
||||
if (participant) {
|
||||
APP.UI.setRaisedHandStatus(participant, false);
|
||||
}
|
||||
}
|
||||
APP.UI.markDominantSpeaker(id);
|
||||
});
|
||||
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||
@@ -1875,6 +1963,10 @@ export default {
|
||||
reportError(e);
|
||||
}
|
||||
});
|
||||
|
||||
APP.UI.addListener(
|
||||
UIEvents.VIDEO_UNMUTING_WHILE_AUDIO_ONLY,
|
||||
() => this._displayAudioOnlyTooltip('videoMute'));
|
||||
}
|
||||
|
||||
room.on(JitsiConferenceEvents.CONNECTION_INTERRUPTED, () => {
|
||||
@@ -1930,10 +2022,7 @@ export default {
|
||||
(participant, name, oldValue, newValue) => {
|
||||
switch (name) {
|
||||
case 'raisedHand':
|
||||
APP.store.dispatch(participantUpdated({
|
||||
id: participant.getId(),
|
||||
raisedHand: newValue === 'true'
|
||||
}));
|
||||
APP.UI.setRaisedHandStatus(participant, newValue);
|
||||
break;
|
||||
case 'remoteControlSessionStatus':
|
||||
APP.UI.setRemoteControlActiveStatus(
|
||||
@@ -1992,13 +2081,26 @@ export default {
|
||||
}
|
||||
});
|
||||
|
||||
room.on(
|
||||
JitsiConferenceEvents.DTMF_SUPPORT_CHANGED,
|
||||
isDTMFSupported => {
|
||||
APP.UI.updateDTMFSupport(isDTMFSupported);
|
||||
}
|
||||
);
|
||||
|
||||
APP.UI.addListener(UIEvents.AUDIO_MUTED, muted => {
|
||||
this.muteAudio(muted);
|
||||
});
|
||||
APP.UI.addListener(UIEvents.VIDEO_MUTED, muted => {
|
||||
this.muteVideo(muted);
|
||||
if (this.isAudioOnly() && !muted) {
|
||||
this._displayAudioOnlyTooltip('videoMute');
|
||||
} else {
|
||||
this.muteVideo(muted);
|
||||
}
|
||||
});
|
||||
|
||||
statsEmitter.startListeningForStats(room);
|
||||
|
||||
room.addCommandListener(this.commands.defaults.ETHERPAD,
|
||||
({ value }) => {
|
||||
APP.UI.initEtherpad(value);
|
||||
@@ -2096,6 +2198,13 @@ export default {
|
||||
room.toggleRecording(options);
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.SUBJECT_CHANGED, topic => {
|
||||
room.setSubject(topic);
|
||||
});
|
||||
room.on(JitsiConferenceEvents.SUBJECT_CHANGED, subject => {
|
||||
APP.UI.setSubject(subject);
|
||||
});
|
||||
|
||||
APP.UI.addListener(UIEvents.AUTH_CLICKED, () => {
|
||||
AuthHandler.authenticate(room);
|
||||
});
|
||||
@@ -2252,8 +2361,6 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
APP.store.dispatch(setSharedVideoStatus(state));
|
||||
});
|
||||
room.addCommandListener(
|
||||
this.commands.defaults.SHARED_VIDEO,
|
||||
@@ -2516,6 +2623,30 @@ export default {
|
||||
APP.API.notifyVideoAvailabilityChanged(available);
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles the local "raised hand" status.
|
||||
*/
|
||||
maybeToggleRaisedHand() {
|
||||
this.setRaisedHand(!this.isHandRaised);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the local "raised hand" status to a particular value.
|
||||
*/
|
||||
setRaisedHand(raisedHand) {
|
||||
if (raisedHand !== this.isHandRaised) {
|
||||
APP.UI.onLocalRaiseHandChanged(raisedHand);
|
||||
|
||||
this.isHandRaised = raisedHand;
|
||||
|
||||
// Advertise the updated status
|
||||
room.setLocalParticipantProperty('raisedHand', raisedHand);
|
||||
|
||||
// Update the view
|
||||
APP.UI.setLocalRaisedHandStatus(raisedHand);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disconnect from the conference and optionally request user feedback.
|
||||
* @param {boolean} [requestFeedback=false] if user feedback should be
|
||||
@@ -2525,11 +2656,6 @@ export default {
|
||||
eventEmitter.emit(JitsiMeetConferenceEvents.BEFORE_HANGUP);
|
||||
APP.UI.removeLocalMedia();
|
||||
|
||||
// Remove unnecessary event listeners from firing callbacks.
|
||||
JitsiMeetJS.mediaDevices.removeEventListener(
|
||||
JitsiMediaDevicesEvents.DEVICE_LIST_CHANGED,
|
||||
this.deviceChangeListener);
|
||||
|
||||
let requestFeedbackPromise;
|
||||
|
||||
if (requestFeedback) {
|
||||
|
||||
21
config.js
21
config.js
@@ -57,9 +57,6 @@ var config = {
|
||||
// P2P test mode disables automatic switching to P2P when there are 2
|
||||
// participants in the conference.
|
||||
p2pTestMode: false
|
||||
|
||||
// Enables the test specific features consumed by jitsi-meet-torture
|
||||
// testMode: false
|
||||
},
|
||||
|
||||
// Disables ICE/UDP by filtering out local and remote UDP candidates in
|
||||
@@ -181,23 +178,6 @@ var config = {
|
||||
// Disables or enables RTX (RFC 4588) (defaults to false).
|
||||
// disableRtx: false,
|
||||
|
||||
// Disables or enables TCC (the default is in Jicofo and set to true)
|
||||
// (draft-holmer-rmcat-transport-wide-cc-extensions-01). This setting
|
||||
// affects congestion control, it practically enables send-side bandwidth
|
||||
// estimations.
|
||||
// enableTcc: true,
|
||||
|
||||
// Disables or enables REMB (the default is in Jicofo and set to false)
|
||||
// (draft-alvestrand-rmcat-remb-03). This setting affects congestion
|
||||
// control, it practically enables recv-side bandwidth estimations. When
|
||||
// both TCC and REMB are enabled, TCC takes precedence. When both are
|
||||
// disabled, then bandwidth estimations are disabled.
|
||||
// enableRemb: false,
|
||||
|
||||
// Defines the minimum number of participants to start a call (the default
|
||||
// is set in Jicofo and set to 2).
|
||||
// minParticipants: 2,
|
||||
|
||||
// Use XEP-0215 to fetch STUN and TURN servers.
|
||||
// useStunTurn: true,
|
||||
|
||||
@@ -342,6 +322,7 @@ var config = {
|
||||
// List of undocumented settings used in jitsi-meet
|
||||
/**
|
||||
alwaysVisibleToolbar
|
||||
autoEnableDesktopSharing
|
||||
autoRecord
|
||||
autoRecordToken
|
||||
debug
|
||||
|
||||
@@ -2,6 +2,42 @@
|
||||
* Project animations
|
||||
**/
|
||||
|
||||
/**
|
||||
* Slide in animation for extended toolbar.
|
||||
*/
|
||||
@include keyframes(slideInX) {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(0%); }
|
||||
}
|
||||
|
||||
@include keyframes(slideOutX) {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
@include keyframes(slideInExtX) {
|
||||
0% { transform: translateX(-500%); }
|
||||
100% { transform: translateX(0%); }
|
||||
}
|
||||
|
||||
@include keyframes(slideOutExtX) {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-500%); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide in / out animation for main toolbar.
|
||||
*/
|
||||
|
||||
@include keyframes(slideInY) {
|
||||
100% { transform: translateY(0%); }
|
||||
}
|
||||
|
||||
@include keyframes(slideOutY) {
|
||||
0% { transform: translateY(0%); }
|
||||
100% { transform: translateY(-100%); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide in animation for extended toolbar (inner) panel.
|
||||
*/
|
||||
@@ -17,3 +53,17 @@
|
||||
from { left: 0; }
|
||||
to { left: -$sidebarWidth; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide in animation for extended toolbar container
|
||||
**/
|
||||
|
||||
@include keyframes(slideOutExtContainer) {
|
||||
from { width: $sidebarWidth; }
|
||||
to { width: 0; }
|
||||
}
|
||||
|
||||
@include keyframes(slideInExtContainer) {
|
||||
from { width: 0; }
|
||||
to { width: $sidebarWidth; }
|
||||
}
|
||||
@@ -3,17 +3,11 @@
|
||||
* none is applied. Other browsers already support selecting within inputs while
|
||||
* user-select is none. As such, disallow user-select except on inputs.
|
||||
*/
|
||||
* {
|
||||
*:not(input) {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0px;
|
||||
width: 100%;
|
||||
@@ -28,16 +22,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AtlasKitThemeProvider sets a background color on an app-wrapping div, thereby
|
||||
* preventing transparency in filmstrip-only mode. The selector chosen to
|
||||
* override this behavior is specific to where the AtlasKitThemeProvider might
|
||||
* be placed within the app hierarchy.
|
||||
*/
|
||||
.filmstrip-only #react > .ckAJgx {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -66,6 +50,7 @@ input[type="reset"], input[type="submit"] {
|
||||
}
|
||||
|
||||
textarea {
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
resize: none;
|
||||
line-height: 1.5em;
|
||||
|
||||
@@ -62,36 +62,6 @@
|
||||
.localuser {
|
||||
color: #087dba;
|
||||
}
|
||||
.use-new-toolbox {
|
||||
.chatmessage {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.localuser {
|
||||
color: #4C9AFF;
|
||||
}
|
||||
|
||||
.remoteuser {
|
||||
color: #B8C7E0;
|
||||
}
|
||||
|
||||
#usermsg {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chatmessage,
|
||||
#smileysarea,
|
||||
#smileysContainer,
|
||||
#usermsg {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
}
|
||||
|
||||
.smileyContainer:hover {
|
||||
background-color: $newToolbarButtonToggleColor;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: red;
|
||||
|
||||
59
css/_contact_list.scss
Normal file
59
css/_contact_list.scss
Normal file
@@ -0,0 +1,59 @@
|
||||
#contacts_container {
|
||||
cursor: default;
|
||||
|
||||
#contacts {
|
||||
font-size: 12px;
|
||||
bottom: 0px;
|
||||
margin: 0;
|
||||
margin-top: 12px;
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#contacts {
|
||||
.contact-list-item {
|
||||
align-items: center;
|
||||
border-radius: 3px;
|
||||
color: $baseLight;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 36px;
|
||||
list-style-type: none;
|
||||
padding: 0 10%;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background: $toolbarSelectBackground;
|
||||
}
|
||||
|
||||
.contact-list-item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
margin: 0 8px 0 4px;
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.avatar {
|
||||
padding: 0px;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
font-size: 22pt;
|
||||
border-radius: 20px;
|
||||
max-height: 30px;
|
||||
max-width: 30px;
|
||||
}
|
||||
@@ -5,20 +5,6 @@
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.use-new-toolbox {
|
||||
.filmstrip.reduce-height {
|
||||
bottom: $newToolbarSizeWithPadding;
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
transition: bottom .3s;
|
||||
}
|
||||
|
||||
.filmstrip__videos.hidden {
|
||||
bottom: calc(-196px - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -80,6 +66,37 @@
|
||||
&#filmstripLocalVideo {
|
||||
align-self: flex-end;
|
||||
display: block;
|
||||
|
||||
/**
|
||||
* The invite button style.
|
||||
*/
|
||||
.filmstrip__invite {
|
||||
padding-bottom: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/**
|
||||
* The invite button group style.
|
||||
* TOFIX: use AtlasKit.ButtonGroup if it starts supporting different
|
||||
* flex grow options for the buttons.
|
||||
*/
|
||||
.invite-button-group {
|
||||
display: inline-flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
& button {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
& > * {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
|
||||
@@ -180,27 +180,3 @@
|
||||
.icon-gsm-bars:before {
|
||||
content: "\e926";
|
||||
}
|
||||
.icon-open_in_new:before {
|
||||
content: "\e89e";
|
||||
}
|
||||
.icon-AUD:before {
|
||||
content: "\e900";
|
||||
}
|
||||
.icon-HD:before {
|
||||
content: "\e927";
|
||||
}
|
||||
.icon-LD:before {
|
||||
content: "\e928";
|
||||
}
|
||||
.icon-SD:before {
|
||||
content: "\e929";
|
||||
}
|
||||
.icon-camera-take-picture:before {
|
||||
content: "\e92a";
|
||||
}
|
||||
.icon-rec:before {
|
||||
content: "\e92b";
|
||||
}
|
||||
.icon-live:before {
|
||||
content: "\e92c";
|
||||
}
|
||||
|
||||
@@ -1,37 +1,6 @@
|
||||
/**
|
||||
* Toolbar side panel main container element.
|
||||
*/
|
||||
.use-new-toolbox #sideToolbarContainer {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
|
||||
/**
|
||||
* Make the sidebar flush with the top of the toolbar. Take the size of
|
||||
* the toolbar and subtract from 100%.
|
||||
*/
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
left: 0;
|
||||
|
||||
.side-toolbar-close {
|
||||
background: gray;
|
||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 100%;
|
||||
color: white;
|
||||
cursor:pointer;
|
||||
height: 10px;
|
||||
line-height: 10px;
|
||||
padding: 4px;
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#chatconversation {
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
#sideToolbarContainer {
|
||||
background-color: $sideToolbarContainerBg;
|
||||
height: 100%;
|
||||
@@ -75,7 +44,7 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner container, for example settings or profile.
|
||||
* Inner container, for example contact list, settings or profile.
|
||||
*/
|
||||
.sideToolbarContainer__inner {
|
||||
display: none;
|
||||
|
||||
@@ -19,266 +19,361 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.use-new-toolbox {
|
||||
.cxGWJB{
|
||||
bottom: calc(#{$newToolbarSizeWithPadding});
|
||||
}
|
||||
.gXSEsl:nth-child(n+2) {
|
||||
transform: translateX(0) translateY(100%) translateY(16px);
|
||||
-ms-transform: translateX(0) translateY(100%) translateY(16px);
|
||||
-webkit-transform: translateX(0) translateY(100%) translateY(16px);
|
||||
}
|
||||
.toolbar-container {
|
||||
display: block;
|
||||
left:0;
|
||||
min-height: 100px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right:0;
|
||||
text-align: center;
|
||||
top:0;
|
||||
z-index: $toolbarZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: when the old filmstrip has been removed, remove the "new-" prefix.
|
||||
* Common toolbar styles.
|
||||
*/
|
||||
.new-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
bottom: calc((#{$newToolbarSize} * 2) * -1);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 16px 8px;
|
||||
position: absolute;
|
||||
transition: bottom .3s ease-in;
|
||||
width: 100%;
|
||||
.toolbar {
|
||||
height: 100%;
|
||||
pointer-events: auto;
|
||||
position: relative;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
&.visible {
|
||||
bottom: 0;
|
||||
/**
|
||||
* Ensure nested elements that don't have a button class, maybe because they
|
||||
* are wrapped in a React Element, still display in a line.
|
||||
*/
|
||||
> div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&.no-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.button-group-center,
|
||||
.button-group-left,
|
||||
.button-group-right {
|
||||
display: flex;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.button-group-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.button-group-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
i {
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.disabled {
|
||||
cursor: initial
|
||||
}
|
||||
|
||||
i.disabled:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.overflow-menu {
|
||||
font-size: 1.2em;
|
||||
list-style-type: none;
|
||||
/**
|
||||
* Always on top overrides.
|
||||
*/
|
||||
&.always-on-top {
|
||||
/**
|
||||
* Undo atlaskit padding by reducing margins.
|
||||
* Toolbar button styles for always on top.
|
||||
*/
|
||||
margin: -15px -24px;
|
||||
padding: 4px 0;
|
||||
|
||||
.overflow-menu-item {
|
||||
align-items: center;
|
||||
color: #B8C7E0;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
height: 22px;
|
||||
padding: 5px 12px;
|
||||
|
||||
&:hover {
|
||||
background: #313D52;
|
||||
.button {
|
||||
font-size: $alwaysOnTopToolbarFontSize;
|
||||
height: $alwaysOnTopToolbarSize;
|
||||
line-height: $alwaysOnTopToolbarSize;
|
||||
width: $alwaysOnTopToolbarSize;
|
||||
&_hangup, &_hangup:hover {
|
||||
font-size: $alwaysOnTopToolbarFontSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar button styles.
|
||||
*/
|
||||
.button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
z-index: $zindex1;
|
||||
display: inline-block;
|
||||
font-size: $toolbarFontSize;
|
||||
height: $defaultToolbarSize;
|
||||
line-height: $defaultToolbarSize;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
top:0px;
|
||||
vertical-align: middle;
|
||||
width: $defaultToolbarSize;
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover, &:active {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&_hangup, &_hangup:hover {
|
||||
color: $hangupColor;
|
||||
font-size: $hangupFontSize;
|
||||
}
|
||||
|
||||
&:not(.toggled) {
|
||||
&:hover, &:active {
|
||||
// sum opacity with background layer should give us 0.8
|
||||
background: $toolbarSelectBackground;
|
||||
}
|
||||
}
|
||||
|
||||
&.toggled {
|
||||
background: $toolbarToggleBackground;
|
||||
|
||||
&.icon-camera {
|
||||
@extend .icon-camera-disabled;
|
||||
}
|
||||
|
||||
&.icon-full-screen {
|
||||
@extend .icon-exit-full-screen;
|
||||
}
|
||||
|
||||
&.icon-microphone {
|
||||
@extend .icon-mic-disabled;
|
||||
}
|
||||
}
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
|
||||
&:hover, &:active, &.selected {
|
||||
background: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary toolbar styles.
|
||||
*/
|
||||
&_primary {
|
||||
background-color: $toolbarBackground;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 30px;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: $defaultToolbarSize;
|
||||
border-radius: 3px;
|
||||
opacity: 0;
|
||||
|
||||
&.always-on-top {
|
||||
height: $alwaysOnTopToolbarSize;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
@include transform(translateX(-50%));
|
||||
|
||||
> a:first-child.button,
|
||||
> div:first-child .button {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-top-left-radius: 3px;
|
||||
}
|
||||
|
||||
> a:last-child.button,
|
||||
> div:last-child .button {
|
||||
border-bottom-right-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&_primary a.button:last-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondary toolbar styles.
|
||||
*/
|
||||
&_secondary {
|
||||
background-color: $secondaryToolbarBg;
|
||||
position: absolute;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
height: 100%;
|
||||
justify-content: flex-start;
|
||||
left: 0;
|
||||
padding-top: 24px;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
transform: translateX(-100%);
|
||||
width: $defaultToolbarSize;
|
||||
-webkit-transform: translateX(-100%);
|
||||
|
||||
.button {
|
||||
font-size: $secToolbarFontSize;
|
||||
height: $secToolbarLineHeight;
|
||||
line-height: $secToolbarLineHeight;
|
||||
}
|
||||
|
||||
> * {
|
||||
pointer-events: auto
|
||||
}
|
||||
|
||||
.button.toggled:not(.icon-raised-hand):not(.button-active) {
|
||||
background: $secondaryToolbarBg;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
|
||||
&.unclickable {
|
||||
cursor: default;
|
||||
}
|
||||
&.unclickable:hover {
|
||||
background: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.overflow-menu-item-icon {
|
||||
margin-right: 10px;
|
||||
|
||||
i {
|
||||
display: inline;
|
||||
font-size: 24px;
|
||||
&:hover, &:active, &.selected {
|
||||
background: none;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: initial;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-text {
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
/**
|
||||
* Styles the toolbar in filmstrip-only mode.
|
||||
*/
|
||||
&_filmstrip-only {
|
||||
background-color: $toolbarBackground;
|
||||
border-radius: 3px;
|
||||
display: inline-block;
|
||||
line-height: $newToolbarSize;
|
||||
margin: 0 4px;
|
||||
text-align: center;
|
||||
}
|
||||
height: auto;
|
||||
width: $defaultFilmStripOnlyToolbarSize;
|
||||
|
||||
.toolbar-button-with-badge {
|
||||
position: relative;
|
||||
.button {
|
||||
height: 37px;
|
||||
line-height: 37px !important;
|
||||
width: 37px;
|
||||
}
|
||||
|
||||
.badge-round {
|
||||
bottom: -5px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
min-width: 20px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
.button-popover-message {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.toolbar-button-wrapper:first-child .button {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
.toolbar-button-wrapper:last-child .button {
|
||||
border-bottom-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbox-button-wth-dialog {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.toolbox-icon {
|
||||
height: $newToolbarSize;
|
||||
font-size: 24px;
|
||||
width: $newToolbarSize;
|
||||
/**
|
||||
* Toolbar specific round badge.
|
||||
*/
|
||||
.badge-round {
|
||||
bottom: 9px;
|
||||
position: absolute;
|
||||
right: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
.always-on-top-toolbox,
|
||||
.filmstrip-toolbox {
|
||||
background-color: $newToolbarBackgroundColor;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
i.toggled {
|
||||
background: $newToolbarButtonToggleColor;
|
||||
}
|
||||
|
||||
i.toggled:hover:not(.disabled) {
|
||||
background-color: $newToolbarButtonHoverColor;
|
||||
}
|
||||
|
||||
.icon-hangup {
|
||||
color: $hangupColor;
|
||||
}
|
||||
|
||||
.toolbox-button {
|
||||
color: $toolbarButtonColor;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.always-on-top-toolbox {
|
||||
flex-direction: row;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translateX(-50%);
|
||||
z-index: $toolbarZ;
|
||||
|
||||
i {
|
||||
font-size: $alwaysOnTopToolbarFontSize;
|
||||
height: $alwaysOnTopToolbarSize;
|
||||
line-height: $alwaysOnTopToolbarSize;
|
||||
width: $alwaysOnTopToolbarSize;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
.filmstrip-only {
|
||||
.toolbox,
|
||||
.toolbox-toolbars {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip-toolbox {
|
||||
i {
|
||||
font-size: 1.9em;
|
||||
height: 37px;
|
||||
line-height: 37px;
|
||||
width: 37px;
|
||||
.subject {
|
||||
background: linear-gradient(to bottom, rgba(255,255,255,.85) , rgba(255,255,255,.35));
|
||||
border-bottom-left-radius: 12px;
|
||||
border-bottom-right-radius: 12px;
|
||||
box-shadow: 0 0 2px #000000, 0 0 10px #000000;
|
||||
margin-left: 40%;
|
||||
margin-right: 40%;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
width: auto;
|
||||
z-index: $zindex3;
|
||||
|
||||
&.subject_slide-in {
|
||||
top: 80px;
|
||||
@include transition(top .3s ease-in);
|
||||
}
|
||||
|
||||
.toolbox-button:first-child i {
|
||||
border-top-left-radius: 3px;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
.toolbox-button:last-child i {
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
&.subject_slide-out {
|
||||
top: 0;
|
||||
@include transition(top .3s ease-out);
|
||||
}
|
||||
}
|
||||
|
||||
#toolbar_button_profile {
|
||||
height: $toolbarAvatarSize + 2*$toolbarAvatarPadding;
|
||||
}
|
||||
|
||||
a.button>#avatar {
|
||||
border-radius: 50%;
|
||||
padding-bottom: $toolbarAvatarPadding;
|
||||
padding-top: $toolbarAvatarPadding;
|
||||
width: $toolbarAvatarSize;
|
||||
}
|
||||
|
||||
#feedbackButton {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* START of slide in animation for extended toolbar.
|
||||
*/
|
||||
@include keyframes(slideInX) {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(0%); }
|
||||
}
|
||||
|
||||
.slideInX {
|
||||
@include animation('slideInX .5s forwards');
|
||||
}
|
||||
|
||||
@include keyframes(slideOutX) {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
||||
|
||||
.slideOutX {
|
||||
@include animation('slideOutX .5s forwards');
|
||||
}
|
||||
|
||||
@include keyframes(slideInExtX) {
|
||||
0% { transform: translateX(-500%); }
|
||||
100% { transform: translateX(0%); }
|
||||
}
|
||||
|
||||
.slideInExtX {
|
||||
@include animation('slideInExtX .5s forwards');
|
||||
}
|
||||
|
||||
@include keyframes(slideOutExtX) {
|
||||
0% { transform: translateX(0%); }
|
||||
100% { transform: translateX(-500%); }
|
||||
}
|
||||
|
||||
.slideOutExtX {
|
||||
@include animation('slideOutExtX .5s forwards');
|
||||
}
|
||||
|
||||
/**
|
||||
* END of slide out animation for extended toolbar.
|
||||
*/
|
||||
|
||||
/**
|
||||
* START of slide in / out animation for main toolbar.
|
||||
*/
|
||||
@include keyframes(slideInY) {
|
||||
100% { transform: translateY(0%); }
|
||||
}
|
||||
|
||||
.slideInY {
|
||||
@include animation('slideInY .5s forwards');
|
||||
}
|
||||
|
||||
@include keyframes(slideOutY) {
|
||||
0% { transform: translateY(0%); }
|
||||
100% { transform: translateY(-100%); }
|
||||
}
|
||||
|
||||
.slideOutY {
|
||||
@include animation('slideOutY .5s forwards');
|
||||
}
|
||||
/**
|
||||
* END of slide in / out animation for main toolbar.
|
||||
*/
|
||||
|
||||
/**
|
||||
* START of slide in animation for extended toolbar panel.
|
||||
*/
|
||||
|
||||
@@ -35,14 +35,21 @@ $defaultDarkColor: #2b3d5c;
|
||||
$alwaysOnTopToolbarFontSize: 1em;
|
||||
$alwaysOnTopToolbarSize: 30px;
|
||||
$defaultToolbarSize: 50px;
|
||||
$newToolbarBackgroundColor: rgba(22, 38, 55, 0.8);
|
||||
$newToolbarButtonHoverColor: rgba(14, 20, 35, 0.6);
|
||||
$newToolbarButtonToggleColor: rgba(14, 20, 35, 1);
|
||||
$newToolbarFontSize: 1.9em;
|
||||
$newToolbarSize: 32px;
|
||||
$newToolbarSizeWithPadding: calc(32px + 32px);
|
||||
$defaultFilmStripOnlyToolbarSize: 37px;
|
||||
$secToolbarFontSize: 1.9em;
|
||||
$secToolbarLineHeight: 45px;
|
||||
$toolbarAvatarPadding: 10px;
|
||||
$toolbarAvatarSize: 40px;
|
||||
$toolbarFontSize: 1.9em;
|
||||
$toolbarTitleFontSize: 19px;
|
||||
|
||||
/**
|
||||
* Main controls
|
||||
* TODO: looks like we don't use it
|
||||
*/
|
||||
$inputSemiBackground: rgba(132, 132, 132, .8);
|
||||
$inputLightBackground: #EBEBEB;
|
||||
|
||||
/**
|
||||
* Video layout
|
||||
*/
|
||||
@@ -67,6 +74,10 @@ $feedbackInputBg: #fff;
|
||||
$feedbackTextColor: #000;
|
||||
$feedbackInputTextColor: #333;
|
||||
$feedbackInputPlaceholderColor: #777;
|
||||
$rateStarLabelColor: #333;
|
||||
$rateStarDefault: #ccc;
|
||||
$rateStarActivity: #165ecc;
|
||||
$rateStarSize: 34px;
|
||||
|
||||
/**
|
||||
* Modals
|
||||
|
||||
@@ -19,20 +19,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
&.use-new-toolbox {
|
||||
/**
|
||||
* Adjust the height of the filmstrip as the toolbar is displayed.
|
||||
*/
|
||||
.filmstrip {
|
||||
top: 0;
|
||||
transition: height .3s ease-in;
|
||||
|
||||
&.reduce-height {
|
||||
height: calc(100% - #{$newToolbarSizeWithPadding});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filmstrip {
|
||||
align-items: flex-end;
|
||||
box-sizing: border-box;
|
||||
@@ -46,7 +32,14 @@
|
||||
* any parent is also fixed.
|
||||
*/
|
||||
position: fixed;
|
||||
z-index: $filmstripVideosZ;
|
||||
|
||||
/**
|
||||
* z-index adjusting is needed because the video state indicator has to
|
||||
* display over the filmstrip when no videos are displayed but still be
|
||||
* clickable but its inline dialogs must display over the video state
|
||||
* indicator when videos are displayed.
|
||||
*/
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
|
||||
/**
|
||||
* Hide videos by making them slight to the right.
|
||||
@@ -66,9 +59,8 @@
|
||||
&#remoteVideos {
|
||||
/**
|
||||
* Remove horizontal filmstrip padding used to prevent videos
|
||||
* from overlapping getting near the left-side of the screen.
|
||||
* An id selector is used to match id specificity with filmstrip
|
||||
* styles.
|
||||
* from overlapping the left-side toolbar. An id selector is
|
||||
* used to match id specificity with filmstrip styles.
|
||||
*/
|
||||
padding-left: 0;
|
||||
transition: right 2s;
|
||||
@@ -76,7 +68,8 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-styles the local Video to better fit vertical filmstrip layout.
|
||||
* Re-styles the local Video and invite button to better fit the
|
||||
* vertical filmstrip layout.
|
||||
*/
|
||||
#filmstripLocalVideo {
|
||||
align-self: initial;
|
||||
@@ -85,6 +78,12 @@
|
||||
flex-direction: column-reverse;
|
||||
height: auto;
|
||||
justify-content: flex-start;
|
||||
|
||||
.filmstrip__invite {
|
||||
padding-bottom: 0px;
|
||||
padding-top: 5px;
|
||||
z-index: $dropdownZ;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,6 +168,20 @@
|
||||
.filmstrip__videos-filmstripOnly {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* In filmstrip only mode, the toolbar is normally displayed in the
|
||||
* vertical center of the filmstrip strip. In vertical filmstrip mode,
|
||||
* that alignment makes the toolbar appear floating and detached from
|
||||
* the filmstrip. So, instead anchor the toolbar next to the local
|
||||
* video.
|
||||
*/
|
||||
.toolbar_filmstrip-only {
|
||||
bottom: 0;
|
||||
top: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#largeVideoBackgroundContainer,
|
||||
.large-video-background {
|
||||
height: 100%;
|
||||
filter: blur(40px);
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
@@ -31,9 +32,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
#largeVideoBackgroundContainer {
|
||||
filter: blur(40px);
|
||||
}
|
||||
|
||||
.videocontainer {
|
||||
position: relative;
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
body.welcome-page {
|
||||
background: inherit;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.welcome {
|
||||
font-family: $welcomePageFontFamily;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.header {
|
||||
@@ -23,7 +19,6 @@ body.welcome-page {
|
||||
justify-content: space-around;
|
||||
margin-top: 120px;
|
||||
margin-bottom: 20px;
|
||||
max-width: calc(100% - 40px);
|
||||
min-height: 286px;
|
||||
width: 645px;
|
||||
}
|
||||
@@ -55,7 +50,6 @@ body.welcome-page {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
max-width: calc(100% - 40px);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
.deep-linking-desktop {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 55px;
|
||||
background-color: #f1f2f5;
|
||||
padding-top: 15px;
|
||||
padding-left: 50px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
flex: 0 0 55px;
|
||||
.logo {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-flow: row;
|
||||
.leftColumn {
|
||||
left: 0px;
|
||||
width: 50%;
|
||||
min-height: 156px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
.leftColumnContent{
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
.image {
|
||||
background-image: url('../images/deep-linking-image.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.rightColumn {
|
||||
top: 0px;
|
||||
width: 50%;
|
||||
min-height: 156px;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
.rightColumnContent {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
padding: 20px 20px 20px 60px;
|
||||
.title {
|
||||
color: #1c2946;
|
||||
}
|
||||
.description {
|
||||
color: #606a80;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.buttons {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@import 'desktop';
|
||||
@import 'mobile';
|
||||
@import 'no-mobile-app';
|
||||
@@ -48,6 +48,7 @@
|
||||
@import 'popup_menu';
|
||||
@import 'recording';
|
||||
@import 'login_menu';
|
||||
@import 'contact_list';
|
||||
@import 'chat';
|
||||
@import 'ringing/ringing';
|
||||
@import 'welcome_page';
|
||||
@@ -71,6 +72,5 @@
|
||||
@import 'unsupported-browser/main';
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'vertical_filmstrip_overrides';
|
||||
@import 'deep-linking/main';
|
||||
|
||||
/* Modules END */
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
> div {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.device-selector-icon {
|
||||
@@ -14,10 +18,6 @@
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.device-selector-label {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* device-selector-trigger stylings attempt to mimic AtlasKit button */
|
||||
.device-selector-trigger {
|
||||
background-color: #0E1624;
|
||||
@@ -101,6 +101,7 @@
|
||||
|
||||
.audio-output-preview {
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
|
||||
a {
|
||||
color: 4C9AFF;
|
||||
@@ -110,9 +111,9 @@
|
||||
}
|
||||
|
||||
.audio-input-preview {
|
||||
background: #1B2638;
|
||||
background: #7b7b7b;
|
||||
border-radius: 5px;
|
||||
height: 8px;
|
||||
height: 6px;
|
||||
|
||||
.audio-input-preview-level {
|
||||
background: #4C9AFF;
|
||||
|
||||
@@ -47,6 +47,10 @@
|
||||
|
||||
.feedback-dialog {
|
||||
.details {
|
||||
margin-top: 20px;
|
||||
padding-left: 60px;
|
||||
padding-right: 60px;
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
}
|
||||
@@ -73,15 +77,16 @@
|
||||
text-align: center;
|
||||
|
||||
.star-label {
|
||||
color: $rateStarLabelColor;
|
||||
font-size: 14px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.star-btn {
|
||||
color: inherit;
|
||||
color: $rateStarDefault;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 34px;
|
||||
font-size: $rateStarSize;
|
||||
outline: none;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
@@ -90,7 +95,7 @@
|
||||
&.active,
|
||||
&:hover,
|
||||
&.starHover {
|
||||
color: #36B37E;
|
||||
color: $rateStarActivity;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -13,15 +13,7 @@
|
||||
}
|
||||
|
||||
.add-telephone-icon {
|
||||
display: flex;
|
||||
height: 28px;
|
||||
transform: scaleX(-1);
|
||||
width: 28px;
|
||||
|
||||
i {
|
||||
line-height: 28px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.use-new-toolbox {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-dialog {
|
||||
cursor: default;
|
||||
display: flex;
|
||||
@@ -42,7 +38,7 @@
|
||||
.info-dialog-copy-element {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
position: fixed;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
@@ -59,13 +55,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog-conference-url,
|
||||
.info-dialog-live-stream-url {
|
||||
width: max-content;
|
||||
width: -moz-max-content;
|
||||
width: -webkit-max-content;
|
||||
word-break: break-all;
|
||||
max-width: 400px;
|
||||
.info-dialog-conference-url {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-dialog-dial-in {
|
||||
@@ -82,29 +77,19 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.info-dialog-url-text,
|
||||
.info-dialog-url-text:hover {
|
||||
color: inherit;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.info-dialog-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-dialog-password,
|
||||
.info-password,
|
||||
.info-dialog-password,
|
||||
.info-password-form {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info-password-field {
|
||||
margin-left: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -125,6 +110,10 @@
|
||||
.info-password-local {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.conference-id {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.dial-in-page {
|
||||
@@ -136,6 +125,10 @@
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
* {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.dial-in-numbers-list {
|
||||
font-size: 24px;
|
||||
margin-top: 20px;
|
||||
@@ -146,12 +139,3 @@
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
.info-dialog,
|
||||
.dial-in-page {
|
||||
* {
|
||||
user-select: text;
|
||||
-moz-user-select: text;
|
||||
-webkit-user-select: text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,12 +135,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal-dialog-form {
|
||||
.video-quality-dialog-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.video-state-indicator {
|
||||
background: $videoStateIndicatorBackground;
|
||||
cursor: default;
|
||||
@@ -153,12 +147,6 @@
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.is-recording {
|
||||
background: none;
|
||||
opacity: 0.9;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i {
|
||||
line-height: $videoStateIndicatorSize;
|
||||
}
|
||||
@@ -171,46 +159,14 @@
|
||||
min-width: $videoStateIndicatorSize;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.recording-icon,
|
||||
.recording-icon i {
|
||||
line-height: $videoStateIndicatorSize;
|
||||
font-size: $videoStateIndicatorSize;
|
||||
opacity: 0.9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-rec {
|
||||
color: #FF5630;
|
||||
}
|
||||
|
||||
.icon-live {
|
||||
color: #0065FF;
|
||||
}
|
||||
|
||||
.recording-icon-background {
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
height: calc(#{$videoStateIndicatorSize} - 1px);
|
||||
left: 50%;
|
||||
opacity: 0.9;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: calc(#{$videoStateIndicatorSize} - 1px);
|
||||
}
|
||||
|
||||
#recordingLabelText {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.centeredVideoLabel.moveToCorner {
|
||||
z-index: $zindex3;
|
||||
z-index: $tooltipsZ;
|
||||
}
|
||||
|
||||
#videoResolutionLabel {
|
||||
z-index: $zindex3 + 1;
|
||||
z-index: #{$tooltipsZ + 1};
|
||||
}
|
||||
|
||||
.centeredVideoLabel {
|
||||
@@ -224,6 +180,8 @@
|
||||
&.moveToCorner {
|
||||
bottom: auto;
|
||||
transform: none;
|
||||
-webkit-transition: all 2s 2s linear;
|
||||
transition: all 2s 2s linear;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
@import 'no-mobile-app';
|
||||
@import 'unsupported-desktop-browser';
|
||||
@import 'unsupported-mobile-browser';
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
.deep-linking-mobile {
|
||||
.unsupported-mobile-browser {
|
||||
background-color: #fff;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
height: 70px;
|
||||
background-color: #f1f2f5;
|
||||
text-align: center;
|
||||
.logo {
|
||||
margin-top: 15px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
@@ -33,19 +20,10 @@
|
||||
a:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.image {
|
||||
max-width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-weight: bolder;
|
||||
padding: 10px 10px 0px 10px;
|
||||
}
|
||||
|
||||
&__text,
|
||||
.deep-linking-dial-in {
|
||||
.unsupported-dial-in {
|
||||
font-size: 1.2em;
|
||||
line-height: em(29px, 21px);
|
||||
margin-bottom: 0.65em;
|
||||
@@ -61,27 +39,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__href {
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
margin: 18px auto 20px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
font-weight: bolder;
|
||||
&__logo {
|
||||
height: 108px;
|
||||
width: 77px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
border: 0;
|
||||
height: 2.2857142857142856em;
|
||||
line-height: 2.2857142857142856em;
|
||||
margin: 18px auto 10px;
|
||||
padding: 0px 10px 0px 10px;
|
||||
margin: 18px auto 20px;
|
||||
max-width: 300px;
|
||||
width: auto;
|
||||
@include border-radius(3px);
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
color: #505F79;
|
||||
font-weight: bold;
|
||||
|
||||
&:active {
|
||||
background-color: $unsupportedBrowserButtonBgColor;
|
||||
@@ -97,7 +69,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.deep-linking-dial-in {
|
||||
.unsupported-dial-in {
|
||||
display: none;
|
||||
|
||||
&.has-numbers {
|
||||
21
debian/jitsi-meet-prosody.postinst
vendored
21
debian/jitsi-meet-prosody.postinst
vendored
@@ -50,21 +50,17 @@ case "$1" in
|
||||
|
||||
db_get jicofo/jicofo-authpassword
|
||||
if [ -z "$RET" ] ; then
|
||||
# if password is missing generate it, and store it
|
||||
JICOFO_AUTH_PASSWORD=`head -c 8 /dev/urandom | tr '\0-\377' 'a-zA-Z0-9a-zA-Z0-9a-zA-Z0-9a-zA-Z0-9@@@@####'`
|
||||
db_set jicofo/jicofo-authpassword "$JICOFO_AUTH_PASSWORD"
|
||||
else
|
||||
JICOFO_AUTH_PASSWORD="$RET"
|
||||
db_input critical jicofo/jicofo-authpassword || true
|
||||
db_go
|
||||
fi
|
||||
JICOFO_AUTH_PASSWORD="$RET"
|
||||
|
||||
db_get jicofo/jicofosecret
|
||||
if [ -z "$RET" ] ; then
|
||||
# if secret is missing generate it, and store it
|
||||
JICOFO_SECRET=`head -c 8 /dev/urandom | tr '\0-\377' 'a-zA-Z0-9a-zA-Z0-9a-zA-Z0-9a-zA-Z0-9@@@@####'`
|
||||
db_set jicofo/jicofosecret "$JICOFO_SECRET"
|
||||
else
|
||||
JICOFO_SECRET="$RET"
|
||||
db_input critical jicofo/jicofosecret || true
|
||||
db_go
|
||||
fi
|
||||
JICOFO_SECRET="$RET"
|
||||
|
||||
JICOFO_AUTH_DOMAIN="auth.$JVB_HOSTNAME"
|
||||
|
||||
@@ -92,7 +88,6 @@ case "$1" in
|
||||
if [ ! -f $PROSODY_HOST_CONFIG ] && ! grep -q "VirtualHost \"$JVB_HOSTNAME\"" $PROSODY_CONFIG_OLD; then
|
||||
PROSODY_CONFIG_PRESENT="false"
|
||||
mkdir -p /etc/prosody/conf.avail/
|
||||
mkdir -p /etc/prosody/conf.d/
|
||||
cp /usr/share/doc/jitsi-meet-prosody/prosody.cfg.lua-jvb.example $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeet.example.com/$JVB_HOSTNAME/g" $PROSODY_HOST_CONFIG
|
||||
sed -i "s/jitmeetSecret/$JVB_SECRET/g" $PROSODY_HOST_CONFIG
|
||||
@@ -137,9 +132,7 @@ case "$1" in
|
||||
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt $AUTH_CRT_FILE
|
||||
ln -sf /var/lib/prosody/$JICOFO_AUTH_DOMAIN.crt /usr/local/share/ca-certificates/$JICOFO_AUTH_DOMAIN.crt
|
||||
|
||||
# we need to force updating certificates, in some cases java trust
|
||||
# store not get re-generated with latest changes
|
||||
update-ca-certificates -f
|
||||
update-ca-certificates
|
||||
|
||||
# don't fail on systems with custom config ($PROSODY_HOST_CONFIG is missing)
|
||||
if [ -f $PROSODY_HOST_CONFIG ]; then
|
||||
|
||||
8
debian/jitsi-meet-prosody.postrm
vendored
8
debian/jitsi-meet-prosody.postrm
vendored
@@ -35,14 +35,6 @@ case "$1" in
|
||||
if [ -n "$RET" ]; then
|
||||
rm -f /etc/prosody/conf.avail/$JVB_HOSTNAME.cfg.lua
|
||||
rm -f /etc/prosody/conf.d/$JVB_HOSTNAME.cfg.lua
|
||||
|
||||
# clean up generated certificates
|
||||
rm -f /etc/prosody/certs/$JVB_HOSTNAME.crt
|
||||
rm -f /etc/prosody/certs/$JVB_HOSTNAME.key
|
||||
rm -f /etc/prosody/certs/auth.$JVB_HOSTNAME.crt
|
||||
rm -f /etc/prosody/certs/auth.$JVB_HOSTNAME.key
|
||||
rm -rf /var/lib/prosody/auth.$JVB_HOSTNAME.*
|
||||
rm -rf /var/lib/prosody/$JVB_HOSTNAME.*
|
||||
fi
|
||||
|
||||
# Clear the debconf variable
|
||||
|
||||
26
doc/api.md
26
doc/api.md
@@ -28,7 +28,6 @@ Its constructor gets a number of options:
|
||||
* **noSSL**: (optional, defaults to true) Boolean indicating if the server should be contacted using HTTP or HTTPS.
|
||||
* **jwt**: (optional) [JWT](https://jwt.io/) token.
|
||||
* **onload**: (optional) handler for the iframe onload event.
|
||||
* **invitees**: (optional) Array of objects containing information about new participants that will be invited in the call.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -98,6 +97,11 @@ api.executeCommand('toggleFilmStrip')
|
||||
api.executeCommand('toggleChat')
|
||||
```
|
||||
|
||||
* **toggleContactList** - Hides / shows the contact list. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('toggleContactList')
|
||||
```
|
||||
|
||||
* **toggleShareScreen** - Starts / stops screen sharing. No arguments are required.
|
||||
```javascript
|
||||
api.executeCommand('toggleShareScreen')
|
||||
@@ -185,7 +189,7 @@ messages. The listener will receive an object with the following structure:
|
||||
}
|
||||
```
|
||||
|
||||
* **displayNameChange** - event notifications about display name
|
||||
* **displayNameChanged** - event notifications about display name
|
||||
changes. The listener will receive an object with the following structure:
|
||||
```javascript
|
||||
{
|
||||
@@ -297,42 +301,32 @@ var iframe = api.getIFrame();
|
||||
|
||||
You can check whether the audio is muted with the following API function:
|
||||
```javascript
|
||||
api.isAudioMuted().then(function(muted) {
|
||||
isAudioMuted().then(function(muted) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
You can check whether the video is muted with the following API function:
|
||||
```javascript
|
||||
api.isVideoMuted().then(function(muted) {
|
||||
isVideoMuted().then(function(muted) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
You can check whether the audio is available with the following API function:
|
||||
```javascript
|
||||
api.isAudioAvailable().then(function(available) {
|
||||
isAudioAvailable().then(function(available) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
You can check whether the video is available with the following API function:
|
||||
```javascript
|
||||
api.isVideoAvailable().then(function(available) {
|
||||
isVideoAvailable().then(function(available) {
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
You can invite new participants to the call with the following API function:
|
||||
```javascript
|
||||
api.invite([{...}, {...}, {...}]).then(function() {
|
||||
// success
|
||||
}).catch(function() {
|
||||
// failure
|
||||
});
|
||||
```
|
||||
**NOTE: The format of the invitees in the array depends on the invite service used for the deployment.**
|
||||
|
||||
You can remove the embedded Jitsi Meet Conference with the following API function:
|
||||
```javascript
|
||||
api.dispose()
|
||||
|
||||
@@ -20,14 +20,6 @@ wget -qO - https://download.jitsi.org/jitsi-key.gpg.key | apt-key add -
|
||||
apt-get update
|
||||
```
|
||||
|
||||
(If you get an error:
|
||||
E: The method driver /usr/lib/apt/methods/https could not be found.
|
||||
run:
|
||||
```sh
|
||||
apt-get install apt-transport-https
|
||||
```
|
||||
)
|
||||
|
||||
### Install Jitsi Meet
|
||||
|
||||
Note : Something to consider before installation is how you're planning to serve Jitsi Meet. The installer will check if Nginx or Apache is present (with this order) and configure a virtualhost within the web server it finds to serve Jitsi Meet. If none of the above is found it then configures itself to be served via jetty. So if for example you are planning on deploying Jitsi Meet with a web server, you have to make sure to install the server **before** installing jitsi-meet.
|
||||
|
||||
BIN
fonts/jitsi.eot
BIN
fonts/jitsi.eot
Binary file not shown.
@@ -23,9 +23,7 @@
|
||||
<glyph unicode="" glyph-name="event_note" d="M598 426v-84h-300v84h300zM810 214v468h-596v-468h596zM810 896c46 0 86-40 86-86v-596c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h42v86h86v-86h340v86h86v-86h42zM726 598v-86h-428v86h428z" />
|
||||
<glyph unicode="" glyph-name="phone-talk" d="M640 512c0 70-58 128-128 128v86c118 0 214-96 214-214h-86zM810 512c0 166-132 298-298 298v86c212 0 384-172 384-384h-86zM854 362c24 0 42-18 42-42v-150c0-24-18-42-42-42-400 0-726 326-726 726 0 24 18 42 42 42h150c24 0 42-18 42-42 0-54 8-104 24-152 4-14 2-32-10-44l-94-94c62-122 162-220 282-282l94 94c12 12 30 14 44 10 48-16 98-24 152-24z" />
|
||||
<glyph unicode="" glyph-name="public" d="M764 282c56 60 90 142 90 230 0 142-88 266-214 316v-18c0-46-40-84-86-84h-84v-86c0-24-20-42-44-42h-84v-86h256c24 0 42-18 42-42v-128h42c38 0 70-26 82-60zM470 174v82c-46 0-86 40-86 86v42l-204 204c-6-24-10-50-10-76 0-174 132-318 300-338zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="open_in_new" d="M598 896h298v-298h-86v152l-418-418-60 60 418 418h-152v86zM810 214v298h86v-298c0-46-40-86-86-86h-596c-48 0-86 40-86 86v596c0 46 38 86 86 86h298v-86h-298v-596h596z" />
|
||||
<glyph unicode="" glyph-name="restore" d="M512 682h64v-180l150-90-32-52-182 110v212zM554 896c212 0 384-172 384-384s-172-384-384-384c-106 0-200 42-270 112l60 62c54-54 128-88 210-88 166 0 300 132 300 298s-134 298-300 298-298-132-298-298h128l-172-172-4 6-166 166h128c0 212 172 384 384 384z" />
|
||||
<glyph unicode="" glyph-name="AUD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM308.25 387.3h57.225l-87.675 252.525h-62.125l-87.675-252.525h53.025l19.425 60.2h88.725l19.075-60.2zM461.9 639.825h-52.85v-165.375c0-56 41.125-93.625 105.7-93.625 64.75 0 105.875 37.625 105.875 93.625v165.375h-52.85v-159.95c0-31.85-19.075-52.15-53.025-52.15-33.775 0-52.85 20.3-52.85 52.15v159.95zM682.225 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM735.075 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15zM243.5 587.325l-31.675-99.050h66.15l-31.325 99.050h-3.15z" />
|
||||
<glyph unicode="" glyph-name="avatar" d="M512 204c106 0 200 56 256 138-2 84-172 132-256 132-86 0-254-48-256-132 56-82 150-138 256-138zM512 810c-70 0-128-58-128-128s58-128 128-128 128 58 128 128-58 128-128 128zM512 938c236 0 426-190 426-426s-190-426-426-426-426 190-426 426 190 426 426 426z" />
|
||||
<glyph unicode="" glyph-name="download" d="M726 470h-128v170h-172v-170h-128l214-214zM826 596c110-8 198-100 198-212 0-118-96-214-214-214h-554c-142 0-256 114-256 256 0 132 100 240 228 254 54 102 160 174 284 174 156 0 284-110 314-258z" />
|
||||
<glyph unicode="" glyph-name="mic-camera-combined" d="M756.704 628.138l267.296 202.213v-635.075l-267.296 202.213v-191.923c0-12.085-11.296-21.863-25.216-21.863h-706.272c-13.92 0-25.216 9.777-25.216 21.863v612.25c0 12.085 11.296 21.863 25.216 21.863h706.272c13.92 0 25.216-9.777 25.216-21.863v-189.679zM371.338 376.228c47.817 0 86.529 40.232 86.529 89.811v184.835c0 49.651-38.713 89.883-86.529 89.883-47.788 0-86.515-40.232-86.515-89.883v-184.835c0-49.579 38.756-89.811 86.515-89.811v0zM356.754 314.070v-32.78h33.718v33.412c73.858 9.606 131.235 73.73 131.235 151.351v88.232h-30.636v-88.232c0-67.57-53.696-122.534-119.734-122.534-66.024 0-119.691 54.964-119.691 122.534v88.232h-30.636v-88.232c0-79.215 59.674-144.502 135.744-151.969v-0.014z" />
|
||||
@@ -64,10 +62,4 @@
|
||||
<glyph unicode="" glyph-name="visibility-off" d="M506 640h6c70 0 128-58 128-128v-8zM322 606c-14-28-24-60-24-94 0-118 96-214 214-214 34 0 66 10 94 24l-66 66c-8-2-18-4-28-4-70 0-128 58-128 128 0 10 2 20 4 28zM86 842l54 54 756-756-54-54c-47.968 47.365-96.266 94.401-144 142-58-24-120-36-186-36-214 0-396 132-470 320 34 84 90 156 160 212-39.017 38.983-77.307 78.693-116 118zM512 726c-28 0-54-6-78-16l-92 92c52 20 110 30 170 30 214 0 394-132 468-320-32-80-82-148-146-202l-124 124c10 24 16 50 16 78 0 118-96 214-214 214z" />
|
||||
<glyph unicode="" glyph-name="dialpad" d="M512 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM768 810c-46 0-86 40-86 86s40 86 86 86 86-40 86-86-40-86-86-86zM256 470c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 726c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM256 982c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86zM512 214c46 0 86-40 86-86s-40-86-86-86-86 40-86 86 40 86 86 86z" />
|
||||
<glyph unicode="" glyph-name="gsm-bars-black" d="M896 1024c70.692 0 128-57.308 128-128v-768c0-70.692-57.308-128-128-128s-128 57.308-128 128v768c0 70.692 57.308 128 128 128zM512 768c70.692 0 128-57.308 128-128v-512c0-70.692-57.308-128-128-128s-128 57.308-128 128v512c0 70.692 57.308 128 128 128zM128 384v0c70.692 0 128-57.308 128-128v-128c0-70.692-57.308-128-128-128s-128 57.308-128 128v128c0 70.692 57.308 128 128 128v0z" />
|
||||
<glyph unicode="" glyph-name="HD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM481.359 384v255.823h-54.273v-103.18h-116.813v103.18h-54.273v-255.823h54.273v106.903h116.813v-106.903h54.273zM544.258 640v-256h102.077c77.636 0 121.665 46.626 121.665 129.773 0 80.133-44.569 126.227-121.665 126.227h-102.077zM598.531 594.26v-164.521h39.177c47.983 0 74.94 29.075 74.94 83.147 0 51.767-27.855 81.374-74.94 81.374h-39.177z" />
|
||||
<glyph unicode="" glyph-name="LD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM472.4 433.325h-112.35v206.5h-52.85v-252.525h165.2v46.025zM520.35 640v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM573.2 594.85v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15z" />
|
||||
<glyph unicode="" glyph-name="SD" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM281.6 451.175c1.925-47.075 40.95-76.65 101.15-76.65 63.35 0 102.375 31.15 102.375 82.075 0 39.2-21.875 61.075-72.625 71.75l-30.45 6.475c-29.575 6.3-41.65 15.225-41.65 30.8 0 19.25 17.5 31.5 43.925 31.5 25.55 0 44.1-13.3 46.55-33.25h49.7c-1.575 44.975-40.95 76.125-96.6 76.125-58.275 0-96.6-31.325-96.6-78.925 0-38.5 22.575-62.475 68.6-72.1l32.9-7c30.975-6.65 43.575-15.925 43.575-32.025 0-19.075-19.425-32.375-46.9-32.375-29.75 0-50.4 13.125-52.85 33.6h-51.1zM535 633.7v-252.7h99.4c75.6 0 118.475 46.025 118.475 128.1 0 79.1-43.4 124.6-118.475 124.6h-99.4zM587.85 588.55v-162.4h38.15c46.725 0 72.975 28.7 72.975 82.075 0 51.1-27.125 80.325-72.975 80.325h-38.15z" />
|
||||
<glyph unicode="" glyph-name="camera-take-picture" d="M725.333 512c0-117.821-95.513-213.333-213.333-213.333s-213.333 95.513-213.333 213.333c0 117.821 95.513 213.333 213.333 213.333s213.333-95.513 213.333-213.333zM512 256c141.385 0 256 114.615 256 256s-114.615 256-256 256v0c-141.385 0-256-114.615-256-256s114.615-256 256-256v0zM512 213.333c-164.949 0-298.667 133.718-298.667 298.667s133.718 298.667 298.667 298.667v0c164.949 0 298.667-133.718 298.667-298.667s-133.718-298.667-298.667-298.667v0z" />
|
||||
<glyph unicode="" glyph-name="rec" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM581.333 433.782h-110.595v59.233h104.338v40.332h-104.338v56.87h110.595v43.539h-161.665v-243.512h161.665v43.539zM738.771 384c58.849 0 101.802 36.282 106.029 88.933h-49.717c-4.904-26.832-26.888-44.045-56.143-44.045-38.556 0-62.4 31.895-62.4 83.196s23.844 83.027 62.231 83.027c29.086 0 51.239-18.394 56.143-46.407h49.717c-3.72 52.989-48.026 91.296-105.86 91.296-70.855 0-114.485-48.77-114.485-127.916 0-79.314 43.798-128.084 114.485-128.084zM230.27 478.502h41.769l45.489-88.258h57.834l-51.408 96.19c28.072 11.138 44.306 38.138 44.306 69.189 0 48.432-32.976 78.133-86.582 78.133h-102.478v-243.512h51.070v88.258zM230.27 592.58v-74.927h44.813c25.704 0 40.754 13.838 40.754 37.295 0 23.119-15.896 37.632-41.262 37.632h-44.306z" />
|
||||
<glyph unicode="" glyph-name="live" d="M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512c282.77 0 512-229.23 512-512s-229.23-512-512-512zM298.225 430.025h-112.35v206.5h-52.85v-252.525h165.2v46.025zM399.025 384v252.525h-52.85v-252.525h52.85zM591.525 384l84.175 252.525h-56.875l-56.35-193.025h-3.15l-57.4 193.025h-59.675l85.4-252.525h63.875zM886.050 429.15h-114.45v61.425h107.975v41.825h-107.975v58.975h114.45v45.15h-167.3v-252.525h167.3v45.15z" />
|
||||
</font></defs></svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 20 KiB |
BIN
fonts/jitsi.ttf
BIN
fonts/jitsi.ttf
Binary file not shown.
BIN
fonts/jitsi.woff
BIN
fonts/jitsi.woff
Binary file not shown.
2485
fonts/selection.json
2485
fonts/selection.json
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 612 B |
Binary file not shown.
|
Before Width: | Height: | Size: 889 B |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
@@ -23,11 +23,9 @@ var interfaceConfig = {
|
||||
SHOW_BRAND_WATERMARK: false,
|
||||
BRAND_WATERMARK_LINK: '',
|
||||
SHOW_POWERED_BY: false,
|
||||
SHOW_DEEP_LINKING_IMAGE: false,
|
||||
GENERATE_ROOMNAMES_ON_WELCOME_PAGE: true,
|
||||
DISPLAY_WELCOME_PAGE_CONTENT: true,
|
||||
APP_NAME: 'Jitsi Meet',
|
||||
NATIVE_APP_NAME: 'Jitsi Meet',
|
||||
LANG_DETECTION: false, // Allow i18n to detect the system language
|
||||
INVITATION_POWERED_BY: true,
|
||||
|
||||
@@ -46,18 +44,22 @@ var interfaceConfig = {
|
||||
'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup',
|
||||
|
||||
// extended toolbar
|
||||
'profile', 'info', 'chat', 'recording', 'etherpad',
|
||||
'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip',
|
||||
'invite', 'feedback', 'stats', 'shortcuts'
|
||||
],
|
||||
'profile', 'contacts', 'info', 'chat', 'recording', 'etherpad', 'sharedvideo', 'settings', 'raisehand', 'videoquality', 'filmstrip' ],
|
||||
|
||||
/**
|
||||
* Main Toolbar Buttons
|
||||
* All of them should be in TOOLBAR_BUTTONS
|
||||
*/
|
||||
MAIN_TOOLBAR_BUTTONS: [ 'microphone', 'camera', 'desktop', 'fullscreen', 'fodeviceselection', 'hangup' ],
|
||||
SETTINGS_SECTIONS: [ 'language', 'devices', 'moderator' ],
|
||||
INVITE_OPTIONS: [ 'invite', 'dialout', 'addtocall' ],
|
||||
|
||||
// Determines how the video would fit the screen. 'both' would fit the whole
|
||||
// screen, 'height' would fit the original video height to the height of the
|
||||
// screen, 'width' would fit the original video width to the width of the
|
||||
// screen respecting ratio.
|
||||
VIDEO_LAYOUT_FIT: 'both',
|
||||
SHOW_CONTACTLIST_AVATARS: true,
|
||||
|
||||
/**
|
||||
* Whether to only show the filmstrip (and hide the toolbar).
|
||||
@@ -148,17 +150,7 @@ var interfaceConfig = {
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false,
|
||||
|
||||
/**
|
||||
* Temporary feature flag to debug performance with the large video
|
||||
* background blur. On initial implementation, blur was always enabled so a
|
||||
* falsy value here will be used to keep blur enabled, as will the value
|
||||
* "video", and will render the blur over a video element. The value
|
||||
* "canvas" will display the blur over a canvas element, while the value
|
||||
* "off" will prevent the background from rendering.
|
||||
*/
|
||||
_BACKGROUND_BLUR: 'canvas'
|
||||
VIDEO_QUALITY_LABEL_DISABLED: false
|
||||
|
||||
/**
|
||||
* Specify custom URL for downloading android mobile app.
|
||||
@@ -173,7 +165,7 @@ var interfaceConfig = {
|
||||
/**
|
||||
* Specify mobile app scheme for opening the app from the mobile browser.
|
||||
*/
|
||||
// APP_SCHEME: 'org.jitsi.meet'
|
||||
// MOBILE_APP_SCHEME: 'org.jitsi.meet'
|
||||
};
|
||||
|
||||
/* eslint-enable no-unused-vars, no-var, max-len */
|
||||
|
||||
@@ -3,7 +3,7 @@ PODS:
|
||||
- React/Core (= 0.51.0)
|
||||
- react-native-background-timer (2.0.0):
|
||||
- React
|
||||
- react-native-calendar-events (1.5.0):
|
||||
- react-native-calendar-events (1.4.3):
|
||||
- React
|
||||
- react-native-fetch-blob (0.10.6):
|
||||
- React/Core
|
||||
@@ -11,8 +11,7 @@ PODS:
|
||||
- React
|
||||
- react-native-locale-detector (1.0.0):
|
||||
- React
|
||||
- react-native-webrtc (1.58.2):
|
||||
- React
|
||||
- react-native-webrtc (1.58.2)
|
||||
- React/BatchedBridge (0.51.0):
|
||||
- React/Core
|
||||
- React/cxxreact_legacy
|
||||
@@ -44,10 +43,10 @@ PODS:
|
||||
- React/Core
|
||||
- React/fishhook
|
||||
- React/RCTBlob
|
||||
- RNSound (0.10.9):
|
||||
- RNSound (0.10.4):
|
||||
- React/Core
|
||||
- RNSound/Core (= 0.10.9)
|
||||
- RNSound/Core (0.10.9):
|
||||
- RNSound/Core (= 0.10.4)
|
||||
- RNSound/Core (0.10.4):
|
||||
- React/Core
|
||||
- RNVectorIcons (4.4.2):
|
||||
- React
|
||||
@@ -103,8 +102,8 @@ SPEC CHECKSUMS:
|
||||
react-native-fetch-blob: 63394b1d7b0781547b3e4463b3195790177b1222
|
||||
react-native-keep-awake: 0de4bd66de0c23178107dce0c2fcc3354b2a8e94
|
||||
react-native-locale-detector: d1b2c6fe5abb56e3a1efb6c2d6f308c05c4251f1
|
||||
react-native-webrtc: 31b6d3f1e3e2ce373aa43fd682b04367250f807d
|
||||
RNSound: b360b3862d3118ed1c74bb9825696b5957686ac4
|
||||
react-native-webrtc: bc044ca9530fc802e7533f247aa08fe1b6bf8dc5
|
||||
RNSound: d0818fe2435254fe30540fae48a429c5ffb72e09
|
||||
RNVectorIcons: c0dbfbf6068fefa240c37b0f71bd03b45dddac44
|
||||
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
|
||||
|
||||
|
||||
@@ -255,7 +255,6 @@
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDebug;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -287,7 +286,6 @@
|
||||
13B07F951A680F5B00A75B9A /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIconRelease;
|
||||
CODE_SIGN_ENTITLEMENTS = app.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
||||
@@ -55,8 +55,8 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>See your scheduled conferences in the app.</string>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
<string>See your scheduled conferences in the app.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Participate in conferences with video.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
|
||||
@@ -18,6 +18,6 @@
|
||||
|
||||
#import <JitsiMeet/JitsiMeet.h>
|
||||
|
||||
@interface ViewController : UIViewController<JitsiMeetViewDelegate, InviteControllerDelegate>
|
||||
@interface ViewController : UIViewController<JitsiMeetViewDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@@ -27,22 +27,12 @@
|
||||
|
||||
JitsiMeetView *view = (JitsiMeetView *) self.view;
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
view.delegate = self;
|
||||
|
||||
// inviteController
|
||||
InviteController *inviteController = view.inviteController;
|
||||
inviteController.delegate = self;
|
||||
|
||||
#endif // #ifdef DEBUG
|
||||
|
||||
// As this is the Jitsi Meet app (i.e. not the Jitsi Meet SDK), we do want
|
||||
// the Welcome page to be enabled. It defaults to disabled in the SDK at the
|
||||
// time of this writing but it is clearer to be explicit about what we want
|
||||
// anyway.
|
||||
view.welcomePageEnabled = YES;
|
||||
|
||||
[view loadURL:nil];
|
||||
}
|
||||
|
||||
@@ -78,17 +68,6 @@ void _onJitsiMeetViewDelegateEvent(NSString *name, NSDictionary *data) {
|
||||
_onJitsiMeetViewDelegateEvent(@"LOAD_CONFIG_ERROR", data);
|
||||
}
|
||||
|
||||
- (void)beginAddPeople:(AddPeopleController *)addPeopleController {
|
||||
NSLog(
|
||||
@"[%s:%d] InviteControllerDelegate %s",
|
||||
__FILE__, __LINE__, __FUNCTION__);
|
||||
|
||||
// XXX Explicitly invoke endAddPeople on addPeopleController; otherwise, it
|
||||
// is going to be memory-leaked in the associated InviteController and no
|
||||
// subsequent InviteButton clicks/taps will be delivered.
|
||||
[addPeopleController endAddPeople];
|
||||
}
|
||||
|
||||
#endif // #ifdef DEBUG
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -51,8 +51,6 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
|
||||
@@ -21,73 +21,19 @@ class ViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var videoButton: UIButton?
|
||||
|
||||
fileprivate var pipViewCoordinator: PiPViewCoordinator?
|
||||
fileprivate var jitsiMeetView: JitsiMeetView?
|
||||
private var jitsiMeetCoordinator: JitsiMeetPresentationCoordinator?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize,
|
||||
with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
let rect = CGRect(origin: CGPoint.zero, size: size)
|
||||
pipViewCoordinator?.resetBounds(bounds: rect)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func openJitsiMeet(sender: Any?) {
|
||||
cleanUp()
|
||||
|
||||
// create and configure jitsimeet view
|
||||
let jitsiMeetView = JitsiMeetView()
|
||||
jitsiMeetView.welcomePageEnabled = true
|
||||
jitsiMeetView.pictureInPictureEnabled = true
|
||||
jitsiMeetView.load(nil)
|
||||
jitsiMeetView.delegate = self
|
||||
self.jitsiMeetView = jitsiMeetView
|
||||
|
||||
// Enable jitsimeet view to be a view that can be displayed
|
||||
// on top of all the things, and let the coordinator to manage
|
||||
// the view state and interactions
|
||||
pipViewCoordinator = PiPViewCoordinator(withView: jitsiMeetView)
|
||||
pipViewCoordinator?.configureAsStickyView(withParentView: view)
|
||||
|
||||
// animate in
|
||||
jitsiMeetView.alpha = 0
|
||||
pipViewCoordinator?.show()
|
||||
}
|
||||
|
||||
fileprivate func cleanUp() {
|
||||
jitsiMeetView?.removeFromSuperview()
|
||||
jitsiMeetView = nil
|
||||
pipViewCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension ViewController: JitsiMeetViewDelegate {
|
||||
|
||||
func conferenceFailed(_ data: [AnyHashable : Any]!) {
|
||||
hideJitsiMeetViewAndCleanUp()
|
||||
}
|
||||
|
||||
func conferenceLeft(_ data: [AnyHashable : Any]!) {
|
||||
hideJitsiMeetViewAndCleanUp()
|
||||
}
|
||||
|
||||
func enterPicture(inPicture data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.pipViewCoordinator?.enterPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
private func hideJitsiMeetViewAndCleanUp() {
|
||||
DispatchQueue.main.async {
|
||||
self.pipViewCoordinator?.hide() { _ in
|
||||
self.cleanUp()
|
||||
}
|
||||
}
|
||||
let jitsiMeetCoordinator = JitsiMeetPresentationCoordinator()
|
||||
self.jitsiMeetCoordinator = jitsiMeetCoordinator
|
||||
jitsiMeetCoordinator.jitsiMeetView().welcomePageEnabled = true
|
||||
jitsiMeetCoordinator.jitsiMeetView().load(nil)
|
||||
jitsiMeetCoordinator.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -27,19 +27,12 @@
|
||||
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0BCA496B1EC4BBF900B793EE /* jitsi.ttf */; };
|
||||
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */ = {isa = PBXBuildFile; fileRef = 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
0F65EECE1D95DA94561BB47E /* libPods-JitsiMeet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 03F2ADC957FF109849B7FCA1 /* libPods-JitsiMeet.a */; };
|
||||
75635B0A20751D6D00F29C9F /* joined.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0820751D6D00F29C9F /* joined.wav */; };
|
||||
75635B0B20751D6D00F29C9F /* left.wav in Resources */ = {isa = PBXBuildFile; fileRef = 75635B0920751D6D00F29C9F /* left.wav */; };
|
||||
B386B85720981A75000DEF7A /* InviteController.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85020981A74000DEF7A /* InviteController.m */; };
|
||||
B386B85820981A75000DEF7A /* AddPeopleController.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85120981A74000DEF7A /* AddPeopleController.m */; };
|
||||
B386B85920981A75000DEF7A /* AddPeopleControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B386B85A20981A75000DEF7A /* AddPeopleController.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85320981A74000DEF7A /* AddPeopleController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B386B85B20981A75000DEF7A /* InviteControllerDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85420981A74000DEF7A /* InviteControllerDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B386B85C20981A75000DEF7A /* InviteController.h in Headers */ = {isa = PBXBuildFile; fileRef = B386B85520981A75000DEF7A /* InviteController.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B386B85D20981A75000DEF7A /* Invite.m in Sources */ = {isa = PBXBuildFile; fileRef = B386B85620981A75000DEF7A /* Invite.m */; };
|
||||
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5B2053091D0040BE68 /* image-resize@2x.png */; };
|
||||
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = C6245F5C2053091D0040BE68 /* image-resize@3x.png */; };
|
||||
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425C204EF76800E062DD /* PiPWindow.swift */; };
|
||||
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */; };
|
||||
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3425E204EF76800E062DD /* DragGestureController.swift */; };
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; };
|
||||
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */; };
|
||||
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -49,9 +42,6 @@
|
||||
0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = "<group>"; };
|
||||
0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = "<group>"; };
|
||||
0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = "<group>"; };
|
||||
0B6F414F20987DE600FF6789 /* Invite+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Invite+Private.h"; sourceTree = "<group>"; };
|
||||
0B6F41502098840600FF6789 /* InviteController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "InviteController+Private.h"; sourceTree = "<group>"; };
|
||||
0B6F4151209884E500FF6789 /* AddPeopleController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "AddPeopleController+Private.h"; sourceTree = "<group>"; };
|
||||
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchOptions.m; sourceTree = "<group>"; };
|
||||
0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
|
||||
0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = "<group>"; };
|
||||
@@ -70,21 +60,14 @@
|
||||
0BD906E51EC0C00300C8C18E /* JitsiMeet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JitsiMeet.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0BD906E81EC0C00300C8C18E /* JitsiMeet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeet.h; sourceTree = "<group>"; };
|
||||
0BD906E91EC0C00300C8C18E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
75635B0820751D6D00F29C9F /* joined.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = joined.wav; path = ../../sounds/joined.wav; sourceTree = "<group>"; };
|
||||
75635B0920751D6D00F29C9F /* left.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = left.wav; path = ../../sounds/left.wav; sourceTree = "<group>"; };
|
||||
98E09B5C73D9036B4ED252FC /* Pods-JitsiMeet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
9C77CA3CC919B081F1A52982 /* Pods-JitsiMeet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-JitsiMeet.release.xcconfig"; path = "../Pods/Target Support Files/Pods-JitsiMeet/Pods-JitsiMeet.release.xcconfig"; sourceTree = "<group>"; };
|
||||
B386B85020981A74000DEF7A /* InviteController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InviteController.m; sourceTree = "<group>"; };
|
||||
B386B85120981A74000DEF7A /* AddPeopleController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AddPeopleController.m; sourceTree = "<group>"; };
|
||||
B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleControllerDelegate.h; sourceTree = "<group>"; };
|
||||
B386B85320981A74000DEF7A /* AddPeopleController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AddPeopleController.h; sourceTree = "<group>"; };
|
||||
B386B85420981A74000DEF7A /* InviteControllerDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteControllerDelegate.h; sourceTree = "<group>"; };
|
||||
B386B85520981A75000DEF7A /* InviteController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InviteController.h; sourceTree = "<group>"; };
|
||||
B386B85620981A75000DEF7A /* Invite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Invite.m; sourceTree = "<group>"; };
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@2x.png"; path = "src/picture-in-picture/image-resize@2x.png"; sourceTree = "<group>"; };
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "image-resize@3x.png"; path = "src/picture-in-picture/image-resize@3x.png"; sourceTree = "<group>"; };
|
||||
C6A3425C204EF76800E062DD /* PiPWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiPWindow.swift; sourceTree = "<group>"; };
|
||||
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JitsiMeetPresentationCoordinator.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JitsiMeetViewController.swift; sourceTree = "<group>"; };
|
||||
C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@@ -106,8 +89,6 @@
|
||||
0BCA49681EC4BBE500B793EE /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
75635B0820751D6D00F29C9F /* joined.wav */,
|
||||
75635B0920751D6D00F29C9F /* left.wav */,
|
||||
0BC4B8681F8C01E100CE8B21 /* CallKitIcon.png */,
|
||||
C6245F5B2053091D0040BE68 /* image-resize@2x.png */,
|
||||
C6245F5C2053091D0040BE68 /* image-resize@3x.png */,
|
||||
@@ -138,7 +119,6 @@
|
||||
0BD906E71EC0C00300C8C18E /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B386B84F20981A11000DEF7A /* invite */,
|
||||
C6A3426B204F127900E062DD /* picture-in-picture */,
|
||||
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
|
||||
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
|
||||
@@ -172,23 +152,6 @@
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B386B84F20981A11000DEF7A /* invite */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B386B85320981A74000DEF7A /* AddPeopleController.h */,
|
||||
B386B85120981A74000DEF7A /* AddPeopleController.m */,
|
||||
0B6F4151209884E500FF6789 /* AddPeopleController+Private.h */,
|
||||
B386B85220981A74000DEF7A /* AddPeopleControllerDelegate.h */,
|
||||
B386B85620981A75000DEF7A /* Invite.m */,
|
||||
0B6F414F20987DE600FF6789 /* Invite+Private.h */,
|
||||
B386B85520981A75000DEF7A /* InviteController.h */,
|
||||
B386B85020981A74000DEF7A /* InviteController.m */,
|
||||
0B6F41502098840600FF6789 /* InviteController+Private.h */,
|
||||
B386B85420981A74000DEF7A /* InviteControllerDelegate.h */,
|
||||
);
|
||||
path = invite;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C5E72ADFC30ED96F9B35F076 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -202,7 +165,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C6A3425E204EF76800E062DD /* DragGestureController.swift */,
|
||||
C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */,
|
||||
C6A3425D204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift */,
|
||||
C6A3426C204F1C3300E062DD /* JitsiMeetViewController.swift */,
|
||||
C6A3425C204EF76800E062DD /* PiPWindow.swift */,
|
||||
);
|
||||
path = "picture-in-picture";
|
||||
sourceTree = "<group>";
|
||||
@@ -214,14 +179,10 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B386B85C20981A75000DEF7A /* InviteController.h in Headers */,
|
||||
B386B85B20981A75000DEF7A /* InviteControllerDelegate.h in Headers */,
|
||||
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
|
||||
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
|
||||
B386B85920981A75000DEF7A /* AddPeopleControllerDelegate.h in Headers */,
|
||||
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
|
||||
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
|
||||
B386B85A20981A75000DEF7A /* AddPeopleController.h in Headers */,
|
||||
0BD906EA1EC0C00300C8C18E /* JitsiMeet.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -291,8 +252,6 @@
|
||||
0BCA496C1EC4BBF900B793EE /* jitsi.ttf in Resources */,
|
||||
C6245F5D2053091D0040BE68 /* image-resize@2x.png in Resources */,
|
||||
0BC4B8691F8C03A700CE8B21 /* CallKitIcon.png in Resources */,
|
||||
75635B0B20751D6D00F29C9F /* left.wav in Resources */,
|
||||
75635B0A20751D6D00F29C9F /* joined.wav in Resources */,
|
||||
C6245F5E2053091D0040BE68 /* image-resize@3x.png in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -378,15 +337,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0BB9AD7B1F5EC8F4001C08DB /* CallKit.m in Sources */,
|
||||
C6A34260204EF76800E062DD /* JitsiMeetPresentationCoordinator.swift in Sources */,
|
||||
0BB9AD7D1F60356D001C08DB /* AppInfo.m in Sources */,
|
||||
0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
|
||||
0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
|
||||
0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
|
||||
B386B85D20981A75000DEF7A /* Invite.m in Sources */,
|
||||
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
|
||||
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
|
||||
B386B85720981A75000DEF7A /* InviteController.m in Sources */,
|
||||
B386B85820981A75000DEF7A /* AddPeopleController.m in Sources */,
|
||||
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
|
||||
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
|
||||
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
|
||||
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
|
||||
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
|
||||
@@ -447,7 +405,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -500,7 +458,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 10.3;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
@@ -29,13 +29,8 @@ RCT_EXPORT_MODULE();
|
||||
- (NSDictionary *)constantsToExport {
|
||||
NSDictionary<NSString *, id> *infoDictionary
|
||||
= [[NSBundle mainBundle] infoDictionary];
|
||||
|
||||
// calendarEnabled
|
||||
BOOL calendarEnabled
|
||||
= infoDictionary[@"NSCalendarsUsageDescription"] != nil;
|
||||
|
||||
// name
|
||||
NSString *name = infoDictionary[@"CFBundleDisplayName"];
|
||||
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
|
||||
|
||||
if (name == nil) {
|
||||
name = infoDictionary[@"CFBundleName"];
|
||||
@@ -43,13 +38,6 @@ RCT_EXPORT_MODULE();
|
||||
name = @"";
|
||||
}
|
||||
}
|
||||
|
||||
// sdkBundlePath
|
||||
NSString *sdkBundlePath = [[NSBundle bundleForClass:self.class] bundlePath];
|
||||
|
||||
// version
|
||||
NSString *version = infoDictionary[@"CFBundleShortVersionString"];
|
||||
|
||||
if (version == nil) {
|
||||
version = infoDictionary[@"CFBundleVersion"];
|
||||
if (version == nil) {
|
||||
@@ -58,9 +46,7 @@ RCT_EXPORT_MODULE();
|
||||
}
|
||||
|
||||
return @{
|
||||
@"calendarEnabled": [NSNumber numberWithBool:calendarEnabled],
|
||||
@"name": name,
|
||||
@"sdkBundlePath": sdkBundlePath,
|
||||
@"version": version
|
||||
};
|
||||
};
|
||||
|
||||
@@ -14,12 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// JitsiMeetView
|
||||
#import <JitsiMeet/JitsiMeetView.h>
|
||||
#import <JitsiMeet/JitsiMeetViewDelegate.h>
|
||||
|
||||
// invite/
|
||||
#import <JitsiMeet/AddPeopleController.h>
|
||||
#import <JitsiMeet/AddPeopleControllerDelegate.h>
|
||||
#import <JitsiMeet/InviteController.h>
|
||||
#import <JitsiMeet/InviteControllerDelegate.h>
|
||||
|
||||
@@ -17,16 +17,13 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "InviteController.h"
|
||||
#import "JitsiMeetViewDelegate.h"
|
||||
|
||||
@interface JitsiMeetView : UIView
|
||||
|
||||
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
||||
|
||||
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) InviteController *inviteController;
|
||||
@property (copy, nonatomic, nullable) NSURL *defaultURL;
|
||||
|
||||
@property (nonatomic) BOOL pictureInPictureEnabled;
|
||||
|
||||
|
||||
@@ -23,8 +23,6 @@
|
||||
#import <React/RCTLinkingManager.h>
|
||||
#import <React/RCTRootView.h>
|
||||
|
||||
#import "Invite+Private.h"
|
||||
#import "InviteController+Private.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
#import "RCTBridgeWrapper.h"
|
||||
|
||||
@@ -274,9 +272,6 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
||||
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
|
||||
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);
|
||||
|
||||
props[@"addPeopleEnabled"] = @(_inviteController.addPeopleEnabled);
|
||||
props[@"dialOutEnabled"] = @(_inviteController.dialOutEnabled);
|
||||
|
||||
// XXX If urlObject is nil, then it must appear as undefined in the
|
||||
// JavaScript source code so that we check the launchOptions there.
|
||||
if (urlObject) {
|
||||
@@ -408,13 +403,10 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
|
||||
});
|
||||
|
||||
// Hook this JitsiMeetView into ExternalAPI.
|
||||
externalAPIScope = [NSUUID UUID].UUIDString;
|
||||
[views setObject:self forKey:externalAPIScope];
|
||||
|
||||
Invite *inviteModule = [bridgeWrapper.bridge moduleForName:@"Invite"];
|
||||
_inviteController
|
||||
= [[InviteController alloc] initWithExternalAPIScope:externalAPIScope
|
||||
andInviteModule:inviteModule];
|
||||
if (!externalAPIScope) {
|
||||
externalAPIScope = [NSUUID UUID].UUIDString;
|
||||
[views setObject:self forKey:externalAPIScope];
|
||||
}
|
||||
|
||||
// Set a background color which is in accord with the JavaScript and Android
|
||||
// parts of the application and causes less perceived visual flicker than
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "AddPeopleController.h"
|
||||
#import "InviteController.h"
|
||||
|
||||
@interface AddPeopleController ()
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary* _Nonnull items;
|
||||
@property (nonatomic, weak) InviteController *owner;
|
||||
@property (nonatomic, readonly) NSString* _Nonnull uuid;
|
||||
|
||||
- (instancetype)initWithOwner:(InviteController *)owner;
|
||||
|
||||
- (void)inviteSettled:(NSArray<NSDictionary *> *)failedInvitees;
|
||||
|
||||
- (void)receivedResults:(NSArray<NSDictionary*> * _Nonnull)results
|
||||
forQuery:(NSString * _Nonnull)query;
|
||||
|
||||
@end
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "AddPeopleControllerDelegate.h"
|
||||
|
||||
@interface AddPeopleController: NSObject
|
||||
|
||||
@property (nonatomic, nullable, weak) id<AddPeopleControllerDelegate> delegate;
|
||||
|
||||
- (void)endAddPeople;
|
||||
|
||||
- (void)inviteById:(NSArray<NSString *> * _Nonnull)ids;
|
||||
|
||||
- (void)performQuery:(NSString * _Nonnull)query;
|
||||
|
||||
@end
|
||||
@@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "AddPeopleController+Private.h"
|
||||
#import "InviteController+Private.h"
|
||||
|
||||
@implementation AddPeopleController
|
||||
|
||||
- (instancetype)initWithOwner:(InviteController *)owner {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_uuid = [[NSUUID UUID] UUIDString];
|
||||
_items = [[NSMutableDictionary alloc] init];
|
||||
_owner = owner;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark API
|
||||
|
||||
- (void)endAddPeople {
|
||||
[self.owner endAddPeopleForController:self];
|
||||
}
|
||||
|
||||
- (void)inviteById:(NSArray<NSString *> * _Nonnull)ids {
|
||||
NSMutableArray* invitees = [[NSMutableArray alloc] init];
|
||||
|
||||
for (NSString* itemId in ids) {
|
||||
id invitee = [self.items objectForKey:itemId];
|
||||
|
||||
if (invitee) {
|
||||
[invitees addObject:invitee];
|
||||
}
|
||||
}
|
||||
|
||||
[self.owner invite:invitees forController:self];
|
||||
}
|
||||
|
||||
- (void)performQuery:(NSString *)query {
|
||||
[self.owner performQuery:query forController:self];
|
||||
}
|
||||
|
||||
#pragma mark Internal API, used to call the delegate and report to the user
|
||||
|
||||
- (void)receivedResults:(NSArray<NSDictionary *> *)results
|
||||
forQuery:(NSString *)query {
|
||||
for (NSDictionary* item in results) {
|
||||
NSString* itemId = item[@"id"];
|
||||
NSString* itemType = item[@"type"];
|
||||
if (itemId) {
|
||||
[self.items setObject:item forKey:itemId];
|
||||
} else if (itemType != nil && [itemType isEqualToString: @"phone"]) {
|
||||
NSString* number = item[@"number"];
|
||||
if (number) {
|
||||
[self.items setObject:item forKey:number];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[self.delegate addPeopleController:self
|
||||
didReceiveResults:results
|
||||
forQuery:query];
|
||||
}
|
||||
|
||||
- (void)inviteSettled:(NSArray<NSDictionary *> *)failedInvitees {
|
||||
[self.delegate inviteSettled:failedInvitees fromSearchController:self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "AddPeopleController.h"
|
||||
|
||||
@class AddPeopleController;
|
||||
|
||||
@protocol AddPeopleControllerDelegate
|
||||
|
||||
/**
|
||||
* Called when an AddPeopleController has results for a query that was
|
||||
* previously provided.
|
||||
*/
|
||||
- (void)addPeopleController:(AddPeopleController * _Nonnull)controller
|
||||
didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
|
||||
forQuery:(NSString * _Nonnull)query;
|
||||
|
||||
/**
|
||||
* Called when an AddPeopleController has finished the inviting process, either
|
||||
* succesfully or not. In case of failure the failedInvitees array will contain
|
||||
* the items for which invitations failed.
|
||||
*/
|
||||
- (void)inviteSettled:(NSArray<NSDictionary *> * _Nonnull)failedInvitees
|
||||
fromSearchController:(AddPeopleController * _Nonnull)addPeopleController;
|
||||
|
||||
@end
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <React/RCTBridge.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface Invite : RCTEventEmitter <RCTBridgeModule>
|
||||
|
||||
- (void) invite:(NSArray<NSDictionary *> * _Nonnull)invitees
|
||||
externalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope;
|
||||
|
||||
- (void) performQuery:(NSString * _Nonnull)query
|
||||
externalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
addPeopleControllerScope:(NSString * _Nonnull)addPeopleControllerScope;
|
||||
|
||||
@end
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "Invite+Private.h"
|
||||
#import "InviteController+Private.h"
|
||||
#import "JitsiMeetView+Private.h"
|
||||
|
||||
// The events emitted/supported by the Invite react-native module:
|
||||
//
|
||||
// XXX The event names are ridiculous on purpose. Even though iOS makes it look
|
||||
// like it emits within the bounderies of a react-native module ony, it actually
|
||||
// also emits through DeviceEventEmitter. (Of course, Android emits only through
|
||||
// DeviceEventEmitter.)
|
||||
static NSString * const InviteEmitterEvent
|
||||
= @"org.jitsi.meet:features/invite#invite";
|
||||
static NSString * const PerformQueryEmitterEvent
|
||||
= @"org.jitsi.meet:features/invite#performQuery";
|
||||
|
||||
@implementation Invite
|
||||
|
||||
RCT_EXPORT_MODULE();
|
||||
|
||||
- (NSArray<NSString *> *)supportedEvents {
|
||||
return @[
|
||||
InviteEmitterEvent,
|
||||
PerformQueryEmitterEvent
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates the process to add people. This involves calling a delegate method
|
||||
* in the InviteControllerDelegate so the native host application can start
|
||||
* the query process.
|
||||
*
|
||||
* @param externalAPIScope - Scope identifying the JitsiMeetView where the
|
||||
* calling JS code is being executed.
|
||||
*/
|
||||
RCT_EXPORT_METHOD(beginAddPeople:(NSString *)externalAPIScope) {
|
||||
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
|
||||
InviteController *controller = view.inviteController;
|
||||
[controller beginAddPeople];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the the invite process has settled / finished.
|
||||
*
|
||||
* @param externalAPIScope - Scope identifying the JitsiMeetView where the
|
||||
* calling JS code is being executed.
|
||||
* @param addPeopleControllerScope - Scope identifying the AddPeopleController
|
||||
* wich was settled.
|
||||
* @param failedInvitees - Array with the invitees which were not invited due
|
||||
* to a failure.
|
||||
*/
|
||||
RCT_EXPORT_METHOD(inviteSettled:(NSString *)externalAPIScope
|
||||
addPeopleControllerScope:(NSString *)addPeopleControllerScope
|
||||
failedInvitees:(NSArray *)failedInvitees) {
|
||||
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
|
||||
InviteController *controller = view.inviteController;
|
||||
[controller inviteSettled:addPeopleControllerScope failedInvitees:failedInvitees];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process results received for the given query. This involves calling a
|
||||
* delegate method in AddPeopleControllerDelegate so the native host application
|
||||
* is made aware of the query results.
|
||||
*
|
||||
* @param externalAPIScope - Scope identifying the JitsiMeetView where the
|
||||
* calling JS code is being executed.
|
||||
* @param addPeopleControllerScope - Scope identifying the AddPeopleController
|
||||
* for which the results were received.
|
||||
* @param query - The actual query for which the results were received.
|
||||
* @param results - The query results.
|
||||
*/
|
||||
RCT_EXPORT_METHOD(receivedResults:(NSString *)externalAPIScope
|
||||
addPeopleControllerScope:(NSString *)addPeopleControllerScope
|
||||
query:(NSString *)query
|
||||
results:(NSArray *)results) {
|
||||
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:externalAPIScope];
|
||||
InviteController *controller = view.inviteController;
|
||||
[controller receivedResults:addPeopleControllerScope query:query results:results];
|
||||
}
|
||||
|
||||
- (void) invite:(NSArray<NSDictionary *> * _Nonnull)invitees
|
||||
externalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
addPeopleControllerScope:(NSString * _Nonnull) addPeopleControllerScope {
|
||||
[self sendEventWithName:InviteEmitterEvent
|
||||
body:@{ @"addPeopleControllerScope": addPeopleControllerScope,
|
||||
@"externalAPIScope": externalAPIScope,
|
||||
@"invitees": invitees }];
|
||||
}
|
||||
|
||||
- (void) performQuery:(NSString * _Nonnull)query
|
||||
externalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
addPeopleControllerScope:(NSString * _Nonnull) addPeopleControllerScope {
|
||||
[self sendEventWithName:PerformQueryEmitterEvent
|
||||
body:@{ @"addPeopleControllerScope": addPeopleControllerScope,
|
||||
@"externalAPIScope": externalAPIScope,
|
||||
@"query": query }];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,50 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2018-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "InviteController.h"
|
||||
|
||||
#import "AddPeopleController.h"
|
||||
#import "Invite+Private.h"
|
||||
|
||||
@interface InviteController ()
|
||||
|
||||
@property (nonatomic, nullable) AddPeopleController *addPeopleController;
|
||||
|
||||
@property (nonatomic) NSString *externalAPIScope;
|
||||
|
||||
@property (nonatomic, nullable, weak) Invite *inviteModule;
|
||||
|
||||
- (instancetype)initWithExternalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
andInviteModule:(Invite * _Nonnull)inviteModule;
|
||||
|
||||
- (void)beginAddPeople;
|
||||
|
||||
- (void)endAddPeopleForController:(AddPeopleController *)controller;
|
||||
|
||||
- (void) invite:(NSArray *)invitees
|
||||
forController:(AddPeopleController * _Nonnull)controller;
|
||||
|
||||
- (void)inviteSettled:(NSString * _Nonnull)addPeopleControllerScope
|
||||
failedInvitees:(NSArray *)failedInvitees;
|
||||
|
||||
- (void)performQuery:(NSString * _Nonnull)query
|
||||
forController:(AddPeopleController * _Nonnull)controller;
|
||||
|
||||
- (void)receivedResults:(NSString * _Nonnull)addPeopleControllerScope
|
||||
query:(NSString * _Nonnull)query
|
||||
results:(NSArray *)results;
|
||||
|
||||
@end
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "InviteControllerDelegate.h"
|
||||
|
||||
@interface InviteController : NSObject
|
||||
|
||||
@property (nonatomic) BOOL addPeopleEnabled;
|
||||
|
||||
@property (nonatomic) BOOL dialOutEnabled;
|
||||
|
||||
@property (nonatomic, nullable, weak) id<InviteControllerDelegate> delegate;
|
||||
|
||||
- (void) invite:(NSArray *)invitees
|
||||
withCompletion:(void (^)(NSArray<NSDictionary *> *failedInvitees))completion;
|
||||
|
||||
@end
|
||||
@@ -1,156 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "InviteController+Private.h"
|
||||
#import "AddPeopleController+Private.h"
|
||||
|
||||
@implementation InviteController {
|
||||
NSNumber *_addPeopleEnabled;
|
||||
NSNumber *_dialOutEnabled;
|
||||
}
|
||||
|
||||
@dynamic addPeopleEnabled;
|
||||
@dynamic dialOutEnabled;
|
||||
|
||||
#pragma mark Constructor
|
||||
|
||||
-(instancetype)initWithExternalAPIScope:(NSString * _Nonnull)externalAPIScope
|
||||
andInviteModule:(Invite * _Nonnull)inviteModule {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.externalAPIScope = externalAPIScope;
|
||||
self.inviteModule = inviteModule;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Public API
|
||||
|
||||
-(void)beginAddPeople {
|
||||
if (_delegate == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_addPeopleController != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
_addPeopleController = [[AddPeopleController alloc] initWithOwner:self];
|
||||
|
||||
@try {
|
||||
if (self.delegate
|
||||
&& [self.delegate respondsToSelector:@selector(beginAddPeople:)]) {
|
||||
[self.delegate beginAddPeople:_addPeopleController];
|
||||
}
|
||||
} @catch (NSException *e) {
|
||||
[self endAddPeopleForController:_addPeopleController];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)endAddPeopleForController:(AddPeopleController *)controller {
|
||||
if (self.addPeopleController == controller) {
|
||||
self.addPeopleController = nil;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Property getters / setters
|
||||
|
||||
- (void) setAddPeopleEnabled:(BOOL)addPeopleEnabled {
|
||||
_addPeopleEnabled = [NSNumber numberWithBool:addPeopleEnabled];
|
||||
}
|
||||
|
||||
- (BOOL) addPeopleEnabled {
|
||||
if (_addPeopleEnabled == nil || [_addPeopleEnabled boolValue]) {
|
||||
return self.delegate
|
||||
&& [self.delegate respondsToSelector:@selector(beginAddPeople:)];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void) setDialOutEnabled:(BOOL)dialOutEnabled {
|
||||
_dialOutEnabled = [NSNumber numberWithBool:dialOutEnabled];
|
||||
}
|
||||
|
||||
- (BOOL) dialOutEnabled {
|
||||
if (_dialOutEnabled == nil || [_dialOutEnabled boolValue]) {
|
||||
return self.delegate
|
||||
&& [self.delegate respondsToSelector:@selector(beginAddPeople:)];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark Result handling
|
||||
|
||||
- (void)inviteSettled:(NSString *)addPeopleControllerScope
|
||||
failedInvitees:(NSArray *)failedInvitees {
|
||||
AddPeopleController *controller = self.addPeopleController;
|
||||
|
||||
if (controller != nil
|
||||
&& [controller.uuid isEqualToString:addPeopleControllerScope]) {
|
||||
@try {
|
||||
[controller inviteSettled:failedInvitees];
|
||||
} @finally {
|
||||
if ([failedInvitees count] == 0) {
|
||||
[self endAddPeopleForController:controller];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)receivedResults:(NSString *)addPeopleControllerScope
|
||||
query:(NSString *)query
|
||||
results:(NSArray *)results {
|
||||
AddPeopleController *controller = self.addPeopleController;
|
||||
|
||||
if (controller != nil
|
||||
&& [controller.uuid isEqualToString:addPeopleControllerScope]) {
|
||||
[controller receivedResults:results forQuery:query];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Use the Invite react-native module to emit the search / submission events
|
||||
|
||||
- (void) invite:(NSArray *)invitees
|
||||
forController:(AddPeopleController * _Nonnull)controller {
|
||||
[self invite:invitees
|
||||
forControllerScope:controller.uuid];
|
||||
}
|
||||
|
||||
- (void) invite:(NSArray *)invitees
|
||||
forControllerScope:(NSString * _Nonnull)controllerScope {
|
||||
[self.inviteModule invite:invitees
|
||||
externalAPIScope:self.externalAPIScope
|
||||
addPeopleControllerScope:controllerScope];
|
||||
}
|
||||
|
||||
- (void) invite:(NSArray *)invitees
|
||||
withCompletion:(void (^)(NSArray<NSDictionary *> *failedInvitees))completion {
|
||||
// TODO Execute the specified completion block when the invite settles.
|
||||
[self invite:invitees
|
||||
forControllerScope:[[NSUUID UUID] UUIDString]];
|
||||
}
|
||||
|
||||
- (void)performQuery:(NSString * _Nonnull)query
|
||||
forController:(AddPeopleController * _Nonnull)controller {
|
||||
[self.inviteModule performQuery:query
|
||||
externalAPIScope:self.externalAPIScope
|
||||
addPeopleControllerScope:controller.uuid];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#import "AddPeopleController.h"
|
||||
|
||||
@protocol InviteControllerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Called when the invite button in the conference is tapped.
|
||||
*
|
||||
* The search controller provided can be used to query user search within the
|
||||
* conference.
|
||||
*/
|
||||
- (void)beginAddPeople:(AddPeopleController *)addPeopleController;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Coordinates the presentation of JitsiMeetViewController inside of
|
||||
/// an external window that can be resized and dragged with custom PiP mode
|
||||
open class JitsiMeetPresentationCoordinator: NSObject {
|
||||
|
||||
fileprivate let meetViewController: JitsiMeetViewController
|
||||
fileprivate let meetWindow: PiPWindow
|
||||
|
||||
public init(meetViewController: JitsiMeetViewController? = nil,
|
||||
meetWindow: PiPWindow? = nil) {
|
||||
self.meetViewController = meetViewController ?? JitsiMeetViewController()
|
||||
self.meetWindow = meetWindow ?? PiPWindow(frame: UIScreen.main.bounds)
|
||||
|
||||
super.init()
|
||||
|
||||
configureMeetWindow()
|
||||
configureMeetViewController()
|
||||
}
|
||||
|
||||
public func jitsiMeetView() -> JitsiMeetView {
|
||||
return meetViewController.jitsiMeetView
|
||||
}
|
||||
|
||||
open func show(completion: CompletionAction? = nil) {
|
||||
meetWindow.show(completion: completion)
|
||||
}
|
||||
|
||||
open func hide(completion: CompletionAction? = nil) {
|
||||
meetWindow.hide(completion: completion)
|
||||
}
|
||||
|
||||
open func cleanUp() {
|
||||
// TODO: more clean up work on this
|
||||
|
||||
meetWindow.isHidden = true
|
||||
meetWindow.stopDragGesture()
|
||||
}
|
||||
|
||||
deinit {
|
||||
cleanUp()
|
||||
}
|
||||
|
||||
// MARK: - helpers
|
||||
|
||||
private func configureMeetViewController() {
|
||||
meetViewController.jitsiMeetView.pictureInPictureEnabled = true
|
||||
meetViewController.delegate = self
|
||||
}
|
||||
|
||||
private func configureMeetWindow() {
|
||||
meetWindow.backgroundColor = .clear
|
||||
meetWindow.windowLevel = UIWindowLevelStatusBar + 100
|
||||
meetWindow.rootViewController = self.meetViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension JitsiMeetPresentationCoordinator: JitsiMeetViewControllerDelegate {
|
||||
|
||||
open func performPresentationUpdate(to: JitsiMeetPresentationUpdate) {
|
||||
switch to {
|
||||
case .enterPictureInPicture:
|
||||
meetWindow.enterPictureInPicture()
|
||||
case .traitChange:
|
||||
// resize to full screen if rotation happens
|
||||
if meetWindow.isInPiP {
|
||||
meetWindow.exitPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceStarted() {
|
||||
if meetWindow.isHidden {
|
||||
meetWindow.show()
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceEnded(didFail: Bool) {
|
||||
cleanUp()
|
||||
}
|
||||
}
|
||||
107
ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift
Normal file
107
ios/sdk/src/picture-in-picture/JitsiMeetViewController.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public enum JitsiMeetPresentationUpdate {
|
||||
|
||||
/// The conference wants to enter Picture-in-Picture
|
||||
case enterPictureInPicture
|
||||
|
||||
/// A system traitCollectionChange (usually screen rotation)
|
||||
case traitChange
|
||||
}
|
||||
|
||||
public protocol JitsiMeetViewControllerDelegate: class {
|
||||
|
||||
/// Notifies a change of the conference presentation style.
|
||||
///
|
||||
/// - Parameter to: The presentation state that will be changed to
|
||||
func performPresentationUpdate(to: JitsiMeetPresentationUpdate)
|
||||
|
||||
/// The conference started
|
||||
func conferenceStarted()
|
||||
|
||||
/// The conference ended
|
||||
///
|
||||
/// - Parameter didFail: The reason of ending the conference
|
||||
func conferenceEnded(didFail: Bool)
|
||||
}
|
||||
|
||||
/// Wrapper ViewController of a JitsiMeetView
|
||||
///
|
||||
/// Is suggested to override this class and implement some customization
|
||||
/// on how to handle the JitsiMeetView delegate events
|
||||
open class JitsiMeetViewController: UIViewController {
|
||||
|
||||
open weak var delegate: JitsiMeetViewControllerDelegate?
|
||||
|
||||
private(set) var jitsiMeetView: JitsiMeetView = JitsiMeetView()
|
||||
|
||||
override open func loadView() {
|
||||
super.loadView()
|
||||
self.view = jitsiMeetView
|
||||
}
|
||||
|
||||
open override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
jitsiMeetView.delegate = self
|
||||
}
|
||||
|
||||
open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
delegate?.performPresentationUpdate(to: .traitChange)
|
||||
}
|
||||
}
|
||||
|
||||
extension JitsiMeetViewController: JitsiMeetViewDelegate {
|
||||
|
||||
open func conferenceWillJoin(_ data: [AnyHashable : Any]!) {
|
||||
// do something
|
||||
}
|
||||
|
||||
open func conferenceJoined(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceStarted()
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceWillLeave(_ data: [AnyHashable : Any]!) {
|
||||
// do something
|
||||
}
|
||||
|
||||
open func conferenceLeft(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: false)
|
||||
}
|
||||
}
|
||||
|
||||
open func conferenceFailed(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: true)
|
||||
}
|
||||
}
|
||||
|
||||
open func loadConfigError(_ data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.conferenceEnded(didFail: true)
|
||||
}
|
||||
}
|
||||
|
||||
open func enterPicture(inPicture data: [AnyHashable : Any]!) {
|
||||
DispatchQueue.main.async {
|
||||
self.delegate?.performPresentationUpdate(to: .enterPictureInPicture)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,15 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
public typealias AnimationCompletion = (Bool) -> Void
|
||||
/// Alias defining a completion closure that returns a Bool
|
||||
public typealias CompletionAction = (Bool) -> Void
|
||||
|
||||
/// Coordinates the view state of a specified view to allow
|
||||
/// to be presented in full screen or in a custom Picture in Picture mode.
|
||||
/// This object will also provide the drag and tap interactions of the view
|
||||
/// when is presented in Picure in Picture mode.
|
||||
public class PiPViewCoordinator {
|
||||
/// A window that allows its root view controller to be presented
|
||||
/// in full screen or in a custom Picture in Picture mode
|
||||
open class PiPWindow: UIWindow {
|
||||
|
||||
/// Limits the boundries of view position on screen when minimized
|
||||
/// Limits the boundries of root view position on screen when minimized
|
||||
public var dragBoundInsets: UIEdgeInsets = UIEdgeInsets(top: 25,
|
||||
left: 5,
|
||||
bottom: 5,
|
||||
@@ -32,75 +31,53 @@ public class PiPViewCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// The size ratio of the view when in PiP mode
|
||||
public var pipSizeRatio: CGFloat = {
|
||||
let deviceIdiom = UIScreen.main.traitCollection.userInterfaceIdiom
|
||||
switch deviceIdiom {
|
||||
case .pad:
|
||||
return 0.25
|
||||
case .phone:
|
||||
return 0.33
|
||||
default:
|
||||
return 0.25
|
||||
}
|
||||
}()
|
||||
/// The size ratio for root view controller view when in PiP mode
|
||||
public var pipSizeRatio: CGFloat = 0.333
|
||||
|
||||
private(set) var isInPiP: Bool = false // true if view is in PiP mode
|
||||
|
||||
private(set) var view: UIView
|
||||
private var currentBounds: CGRect = CGRect.zero
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
private var exitPiPButton: UIButton?
|
||||
/// The PiP state of this contents of the window
|
||||
private(set) var isInPiP: Bool = false
|
||||
|
||||
private let dragController: DragGestureController = DragGestureController()
|
||||
|
||||
public init(withView view: UIView) {
|
||||
self.view = view
|
||||
/// Used when in PiP mode to enable/disable exit PiP UI
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer?
|
||||
private var exitPiPButton: UIButton?
|
||||
|
||||
/// Help out to bubble up the gesture detection outside of the rootVC frame
|
||||
open override func point(inside point: CGPoint,
|
||||
with event: UIEvent?) -> Bool {
|
||||
guard let vc = rootViewController else {
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
return vc.view.frame.contains(point)
|
||||
}
|
||||
|
||||
/// Configure the view to be always on top of all the contents
|
||||
/// of the provided parent view.
|
||||
/// If a parentView is not provided it will try to use the main window
|
||||
public func configureAsStickyView(withParentView parentView: UIView? = nil) {
|
||||
guard
|
||||
let parentView = parentView ?? UIApplication.shared.keyWindow
|
||||
else { return }
|
||||
|
||||
parentView.addSubview(view)
|
||||
currentBounds = parentView.bounds
|
||||
view.frame = currentBounds
|
||||
view.layer.zPosition = CGFloat(Float.greatestFiniteMagnitude)
|
||||
}
|
||||
|
||||
/// Show view with fade in animation
|
||||
public func show(completion: AnimationCompletion? = nil) {
|
||||
if view.isHidden || view.alpha < 1 {
|
||||
view.isHidden = false
|
||||
view.alpha = 0
|
||||
/// animate in the window
|
||||
open func show(completion: CompletionAction? = nil) {
|
||||
if self.isHidden || self.alpha < 1 {
|
||||
self.isHidden = false
|
||||
self.alpha = 0
|
||||
|
||||
animateTransition(animations: { [weak self] in
|
||||
self?.view.alpha = 1
|
||||
animateTransition(animations: {
|
||||
self.alpha = 1
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hide view with fade out animation
|
||||
public func hide(completion: AnimationCompletion? = nil) {
|
||||
if view.isHidden || view.alpha > 0 {
|
||||
animateTransition(animations: { [weak self] in
|
||||
self?.view.alpha = 0
|
||||
self?.view.isHidden = true
|
||||
/// animate out the window
|
||||
open func hide(completion: CompletionAction? = nil) {
|
||||
if !self.isHidden || self.alpha > 0 {
|
||||
animateTransition(animations: {
|
||||
self.alpha = 1
|
||||
}, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize view to and change state to custom PictureInPicture mode
|
||||
/// This will resize view, add a gesture to enable user to "drag" view
|
||||
/// around screen, and add a button of top of the view to be able to exit mode
|
||||
public func enterPictureInPicture() {
|
||||
/// Resize the root view to PiP mode
|
||||
open func enterPictureInPicture() {
|
||||
guard let view = rootViewController?.view else { return }
|
||||
isInPiP = true
|
||||
animateViewChange()
|
||||
animateRootViewChange()
|
||||
dragController.startDragListener(inView: view)
|
||||
dragController.insets = dragBoundInsets
|
||||
|
||||
@@ -112,11 +89,10 @@ public class PiPViewCoordinator {
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
}
|
||||
|
||||
/// Exit Picture in picture mode, this will resize view, remove
|
||||
/// exit pip button, and disable the drag gesture
|
||||
@objc public func exitPictureInPicture() {
|
||||
/// Resize the root view to full screen
|
||||
open func exitPictureInPicture() {
|
||||
isInPiP = false
|
||||
animateViewChange()
|
||||
animateRootViewChange()
|
||||
dragController.stopDragListener()
|
||||
|
||||
// hide PiP UI
|
||||
@@ -129,13 +105,6 @@ public class PiPViewCoordinator {
|
||||
tapGestureRecognizer = nil
|
||||
}
|
||||
|
||||
/// Reset view to provide bounds, use this method on rotation or
|
||||
/// screen size changes
|
||||
public func resetBounds(bounds: CGRect) {
|
||||
currentBounds = bounds
|
||||
exitPictureInPicture()
|
||||
}
|
||||
|
||||
/// Stop the dragging gesture of the root view
|
||||
public func stopDragGesture() {
|
||||
dragController.stopDragListener()
|
||||
@@ -153,14 +122,41 @@ public class PiPViewCoordinator {
|
||||
button.backgroundColor = .gray
|
||||
button.layer.cornerRadius = size.width / 2
|
||||
button.frame = CGRect(origin: CGPoint.zero, size: size)
|
||||
button.center = view.convert(view.center, from: view.superview)
|
||||
if let view = rootViewController?.view {
|
||||
button.center = view.convert(view.center, from:view.superview)
|
||||
}
|
||||
button.addTarget(target, action: action, for: .touchUpInside)
|
||||
return button
|
||||
}
|
||||
|
||||
// MARK: - Interactions
|
||||
// MARK: - Manage presentation switching
|
||||
|
||||
private func animateRootViewChange() {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.rootViewController?.view.frame = self.changeRootViewRect()
|
||||
self.rootViewController?.view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
private func changeRootViewRect() -> CGRect {
|
||||
guard isInPiP else {
|
||||
return self.bounds
|
||||
}
|
||||
|
||||
// resize to suggested ratio and position to the bottom right
|
||||
let adjustedBounds = UIEdgeInsetsInsetRect(self.bounds, dragBoundInsets)
|
||||
let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
||||
height: bounds.size.height * pipSizeRatio)
|
||||
let x: CGFloat = adjustedBounds.maxX - size.width
|
||||
let y: CGFloat = adjustedBounds.maxY - size.height
|
||||
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Exit PiP
|
||||
|
||||
@objc private func toggleExitPiP() {
|
||||
guard let view = rootViewController?.view else { return }
|
||||
|
||||
if exitPiPButton == nil {
|
||||
// show button
|
||||
let exitSelector = #selector(exitPictureInPicture)
|
||||
@@ -176,40 +172,18 @@ public class PiPViewCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Size calculation
|
||||
|
||||
private func animateViewChange() {
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
self.view.frame = self.changeViewRect()
|
||||
self.view.setNeedsLayout()
|
||||
}
|
||||
@objc private func exitPiP() {
|
||||
exitPictureInPicture()
|
||||
}
|
||||
|
||||
private func changeViewRect() -> CGRect {
|
||||
let bounds = currentBounds
|
||||
|
||||
guard isInPiP else {
|
||||
return bounds
|
||||
}
|
||||
|
||||
// resize to suggested ratio and position to the bottom right
|
||||
let adjustedBounds = UIEdgeInsetsInsetRect(bounds, dragBoundInsets)
|
||||
let size = CGSize(width: bounds.size.width * pipSizeRatio,
|
||||
height: bounds.size.height * pipSizeRatio)
|
||||
let x: CGFloat = adjustedBounds.maxX - size.width
|
||||
let y: CGFloat = adjustedBounds.maxY - size.height
|
||||
return CGRect(x: x, y: y, width: size.width, height: size.height)
|
||||
}
|
||||
|
||||
// MARK: - Animation helpers
|
||||
// MARK: - Animation transition
|
||||
|
||||
private func animateTransition(animations: @escaping () -> Void,
|
||||
completion: AnimationCompletion?) {
|
||||
completion: CompletionAction?) {
|
||||
UIView.animate(withDuration: 0.1,
|
||||
delay: 0,
|
||||
options: .beginFromCurrentState,
|
||||
animations: animations,
|
||||
completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"en": "",
|
||||
"bg": "",
|
||||
"de": "",
|
||||
"es": "",
|
||||
"fr": "",
|
||||
"hy": "",
|
||||
"it": "",
|
||||
"oc": "",
|
||||
"pl": "",
|
||||
"ptBR": "",
|
||||
"ru": "",
|
||||
"sk": "",
|
||||
"sl": "",
|
||||
"sv": "",
|
||||
"tr": "",
|
||||
"zhCN": "",
|
||||
"nb": "",
|
||||
"eo": ""
|
||||
}
|
||||
@@ -1,17 +1,11 @@
|
||||
{
|
||||
"en": "English",
|
||||
"az": "Azerbaijani",
|
||||
"bg": "Bulgarian",
|
||||
"cs": "Czech",
|
||||
"de": "German",
|
||||
"eo": "Esperanto",
|
||||
"es": "Spanish",
|
||||
"fr": "French",
|
||||
"hy": "Armenian",
|
||||
"it": "Italian",
|
||||
"ja": "Japanese",
|
||||
"ko": "Korean",
|
||||
"nb": "Norwegian Bokmal",
|
||||
"oc": "Occitan",
|
||||
"pl": "Polish",
|
||||
"ptBR": "Portuguese (Brazil)",
|
||||
@@ -20,6 +14,7 @@
|
||||
"sl": "Slovenian",
|
||||
"sv": "Swedish",
|
||||
"tr": "Turkish",
|
||||
"vi": "Vietnamese",
|
||||
"zhCN": "Chinese (China)"
|
||||
"zhCN": "Chinese (China)",
|
||||
"nb": "Norwegian Bokmal",
|
||||
"eo": "Esperanto"
|
||||
}
|
||||
|
||||
@@ -1,512 +0,0 @@
|
||||
{
|
||||
"contactlist": "",
|
||||
"contactlist_plural_undefined": "",
|
||||
"passwordSetRemotely": "",
|
||||
"poweredby": "",
|
||||
"inviteUrlDefaultMsg": "",
|
||||
"me": "",
|
||||
"speaker": "",
|
||||
"raisedHand": "",
|
||||
"defaultNickname": "",
|
||||
"defaultLink": "",
|
||||
"audioDevices": {
|
||||
"bluetooth": "",
|
||||
"headphones": "",
|
||||
"phone": "",
|
||||
"speaker": ""
|
||||
},
|
||||
"audioOnly": {
|
||||
"audioOnly": "",
|
||||
"featureToggleDisabled": ""
|
||||
},
|
||||
"userMedia": {
|
||||
"react-nativeGrantPermissions": "",
|
||||
"chromeGrantPermissions": "",
|
||||
"androidGrantPermissions": "",
|
||||
"firefoxGrantPermissions": "",
|
||||
"operaGrantPermissions": "",
|
||||
"iexplorerGrantPermissions": "",
|
||||
"safariGrantPermissions": "",
|
||||
"nwjsGrantPermissions": "",
|
||||
"edgeGrantPermissions": ""
|
||||
},
|
||||
"keyboardShortcuts": {
|
||||
"keyboardShortcuts": "",
|
||||
"raiseHand": "",
|
||||
"pushToTalk": "",
|
||||
"toggleScreensharing": "",
|
||||
"toggleFilmstrip": "",
|
||||
"toggleShortcuts": "",
|
||||
"focusLocal": "",
|
||||
"focusRemote": "",
|
||||
"toggleChat": "",
|
||||
"mute": "",
|
||||
"fullScreen": "",
|
||||
"videoMute": "",
|
||||
"showSpeakerStats": ""
|
||||
},
|
||||
"welcomepage": {
|
||||
"appDescription": "",
|
||||
"audioVideoSwitch": {
|
||||
"audio": "",
|
||||
"video": ""
|
||||
},
|
||||
"calendar": "",
|
||||
"go": "",
|
||||
"join": "",
|
||||
"privacy": "",
|
||||
"roomname": "",
|
||||
"roomnameHint": "",
|
||||
"sendFeedback": "",
|
||||
"terms": "",
|
||||
"title": ""
|
||||
},
|
||||
"startupoverlay": {
|
||||
"policyText": "",
|
||||
"title": ""
|
||||
},
|
||||
"suspendedoverlay": {
|
||||
"title": "",
|
||||
"text": "",
|
||||
"rejoinKeyTitle": ""
|
||||
},
|
||||
"toolbar": {
|
||||
"addPeople": "",
|
||||
"audioonly": "",
|
||||
"mute": "",
|
||||
"videomute": "",
|
||||
"authenticate": "",
|
||||
"lock": "",
|
||||
"chat": "",
|
||||
"etherpad": "",
|
||||
"sharedvideo": "",
|
||||
"sharescreen": "",
|
||||
"fullscreen": "",
|
||||
"sip": "",
|
||||
"Settings": "",
|
||||
"hangup": "",
|
||||
"login": "",
|
||||
"logout": "",
|
||||
"dialpad": "",
|
||||
"sharedVideoMutedPopup": "",
|
||||
"micMutedPopup": "",
|
||||
"talkWhileMutedPopup": "",
|
||||
"unableToUnmutePopup": "",
|
||||
"cameraDisabled": "",
|
||||
"micDisabled": "",
|
||||
"filmstrip": "",
|
||||
"profile": "",
|
||||
"raiseHand": ""
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appNotInstalled": "",
|
||||
"downloadApp": "",
|
||||
"openApp": ""
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "",
|
||||
"filmstrip": "",
|
||||
"contactlist": ""
|
||||
},
|
||||
"chat": {
|
||||
"nickname": {
|
||||
"title": "",
|
||||
"popover": ""
|
||||
},
|
||||
"messagebox": ""
|
||||
},
|
||||
"settings": {
|
||||
"title": "",
|
||||
"update": "",
|
||||
"name": "",
|
||||
"startAudioMuted": "",
|
||||
"startVideoMuted": "",
|
||||
"selectCamera": "",
|
||||
"selectMic": "",
|
||||
"selectAudioOutput": "",
|
||||
"followMe": "",
|
||||
"noDevice": "",
|
||||
"cameraAndMic": "",
|
||||
"moderator": "",
|
||||
"password": "",
|
||||
"audioVideo": ""
|
||||
},
|
||||
"profile": {
|
||||
"title": "",
|
||||
"setDisplayNameLabel": "",
|
||||
"setEmailLabel": "",
|
||||
"setEmailInput": ""
|
||||
},
|
||||
"videothumbnail": {
|
||||
"moderator": "",
|
||||
"videomute": "",
|
||||
"mute": "",
|
||||
"kick": "",
|
||||
"muted": "",
|
||||
"domute": "",
|
||||
"flip": "",
|
||||
"remoteControl": ""
|
||||
},
|
||||
"connectionindicator": {
|
||||
"header": "",
|
||||
"bitrate": "",
|
||||
"packetloss": "",
|
||||
"resolution": "",
|
||||
"framerate": "",
|
||||
"less": "",
|
||||
"more": "",
|
||||
"address": "",
|
||||
"remoteport": "",
|
||||
"remoteport_plural_undefined": "",
|
||||
"localport": "",
|
||||
"localport_plural_undefined": "",
|
||||
"localaddress": "",
|
||||
"localaddress_plural_undefined": "",
|
||||
"remoteaddress": "",
|
||||
"remoteaddress_plural_undefined": "",
|
||||
"transport": "",
|
||||
"transport_plural_undefined": "",
|
||||
"bandwidth": "",
|
||||
"na": "",
|
||||
"turn": "",
|
||||
"quality": {
|
||||
"good": "",
|
||||
"inactive": "",
|
||||
"lost": "",
|
||||
"nonoptimal": "",
|
||||
"poor": ""
|
||||
},
|
||||
"status": ""
|
||||
},
|
||||
"notify": {
|
||||
"disconnected": "",
|
||||
"moderator": "",
|
||||
"connectedOneMember": "",
|
||||
"connectedTwoMembers": "",
|
||||
"connectedThreePlusMembers": "",
|
||||
"somebody": "",
|
||||
"me": "",
|
||||
"focus": "",
|
||||
"focusFail": "",
|
||||
"grantedTo": "",
|
||||
"grantedToUnknown": "",
|
||||
"muted": "",
|
||||
"mutedTitle": "",
|
||||
"raisedHand": "",
|
||||
"suboptimalExperienceTitle": "",
|
||||
"suboptimalExperienceDescription": ""
|
||||
},
|
||||
"dialog": {
|
||||
"allow": "",
|
||||
"kickMessage": "",
|
||||
"popupErrorTitle": "",
|
||||
"popupError": "",
|
||||
"passwordErrorTitle": "",
|
||||
"passwordError": "",
|
||||
"passwordError2": "",
|
||||
"connectError": "",
|
||||
"connectErrorWithMsg": "",
|
||||
"incorrectPassword": "",
|
||||
"connecting": "",
|
||||
"copy": "",
|
||||
"contactSupport": "",
|
||||
"error": "",
|
||||
"detectext": "",
|
||||
"failedpermissions": "",
|
||||
"conferenceReloadTitle": "",
|
||||
"conferenceReloadMsg": "",
|
||||
"conferenceDisconnectTitle": "",
|
||||
"conferenceDisconnectMsg": "",
|
||||
"dismiss": "",
|
||||
"rejoinNow": "",
|
||||
"maxUsersLimitReachedTitle": "",
|
||||
"maxUsersLimitReached": "",
|
||||
"lockTitle": "",
|
||||
"lockMessage": "",
|
||||
"warning": "",
|
||||
"passwordNotSupportedTitle": "",
|
||||
"passwordNotSupported": "",
|
||||
"internalErrorTitle": "",
|
||||
"internalError": "",
|
||||
"unableToSwitch": "",
|
||||
"SLDFailure": "",
|
||||
"SRDFailure": "",
|
||||
"oops": "",
|
||||
"currentPassword": "",
|
||||
"passwordLabel": "",
|
||||
"defaultError": "",
|
||||
"passwordRequired": "",
|
||||
"Ok": "",
|
||||
"done": "",
|
||||
"Remove": "",
|
||||
"removePassword": "",
|
||||
"shareVideoTitle": "",
|
||||
"shareVideoLinkError": "",
|
||||
"removeSharedVideoTitle": "",
|
||||
"removeSharedVideoMsg": "",
|
||||
"alreadySharedVideoMsg": "",
|
||||
"alreadySharedVideoTitle": "",
|
||||
"WaitingForHost": "",
|
||||
"WaitForHostMsg": "",
|
||||
"IamHost": "",
|
||||
"Cancel": "",
|
||||
"Submit": "",
|
||||
"retry": "",
|
||||
"logoutTitle": "",
|
||||
"logoutQuestion": "",
|
||||
"sessTerminated": "",
|
||||
"hungUp": "",
|
||||
"joinAgain": "",
|
||||
"Share": "",
|
||||
"Save": "",
|
||||
"recording": "",
|
||||
"recordingToken": "",
|
||||
"Back": "",
|
||||
"serviceUnavailable": "",
|
||||
"gracefulShutdown": "",
|
||||
"Yes": "",
|
||||
"reservationError": "",
|
||||
"reservationErrorMsg": "",
|
||||
"password": "",
|
||||
"userPassword": "",
|
||||
"token": "",
|
||||
"tokenAuthFailedTitle": "",
|
||||
"tokenAuthFailed": "",
|
||||
"displayNameRequired": "",
|
||||
"enterDisplayName": "",
|
||||
"feedbackHelp": "",
|
||||
"feedbackQuestion": "",
|
||||
"thankYou": "",
|
||||
"sorryFeedback": "",
|
||||
"liveStreaming": "",
|
||||
"streamKey": "",
|
||||
"startLiveStreaming": "",
|
||||
"stopStreamingWarning": "",
|
||||
"stopRecordingWarning": "",
|
||||
"stopLiveStreaming": "",
|
||||
"stopRecording": "",
|
||||
"doNotShowMessageAgain": "",
|
||||
"permissionDenied": "",
|
||||
"screenSharingFailedToInstall": "",
|
||||
"screenSharingFailedToInstallTitle": "",
|
||||
"screenSharingFirefoxPermissionDeniedError": "",
|
||||
"screenSharingFirefoxPermissionDeniedTitle": "",
|
||||
"screenSharingPermissionDeniedError": "",
|
||||
"cameraUnsupportedResolutionError": "",
|
||||
"cameraUnknownError": "",
|
||||
"cameraPermissionDeniedError": "",
|
||||
"cameraNotFoundError": "",
|
||||
"cameraConstraintFailedError": "",
|
||||
"micUnknownError": "",
|
||||
"micPermissionDeniedError": "",
|
||||
"micNotFoundError": "",
|
||||
"micConstraintFailedError": "",
|
||||
"micNotSendingDataTitle": "",
|
||||
"micNotSendingData": "",
|
||||
"cameraNotSendingDataTitle": "",
|
||||
"cameraNotSendingData": "",
|
||||
"goToStore": "",
|
||||
"externalInstallationTitle": "",
|
||||
"externalInstallationMsg": "",
|
||||
"inlineInstallationMsg": "",
|
||||
"inlineInstallExtension": "",
|
||||
"muteParticipantTitle": "",
|
||||
"muteParticipantBody": "",
|
||||
"muteParticipantButton": "",
|
||||
"remoteControlTitle": "",
|
||||
"remoteControlRequestMessage": "",
|
||||
"remoteControlShareScreenWarning": "",
|
||||
"remoteControlDeniedMessage": "",
|
||||
"remoteControlAllowedMessage": "",
|
||||
"remoteControlErrorMessage": "",
|
||||
"startRemoteControlErrorMessage": "",
|
||||
"remoteControlStopMessage": "",
|
||||
"close": "",
|
||||
"shareYourScreen": "",
|
||||
"yourEntireScreen": "",
|
||||
"applicationWindow": ""
|
||||
},
|
||||
"email": {
|
||||
"sharedKey": "",
|
||||
"subject": "",
|
||||
"body": "",
|
||||
"and": ""
|
||||
},
|
||||
"connection": {
|
||||
"ERROR": "",
|
||||
"CONNECTING": "",
|
||||
"RECONNECTING": "",
|
||||
"CONNFAIL": "",
|
||||
"AUTHENTICATING": "",
|
||||
"AUTHFAIL": "",
|
||||
"CONNECTED": "",
|
||||
"DISCONNECTED": "",
|
||||
"DISCONNECTING": "",
|
||||
"ATTACHED": ""
|
||||
},
|
||||
"recording": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"error": "",
|
||||
"failedToStart": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"unavailable": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"liveStreaming": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"buttonTooltip": "",
|
||||
"changeSignIn": "",
|
||||
"choose": "",
|
||||
"chooseCTA": "",
|
||||
"enterStreamKey": "",
|
||||
"error": "",
|
||||
"errorAPI": "",
|
||||
"failedToStart": "",
|
||||
"off": "",
|
||||
"on": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"signIn": "",
|
||||
"signInCTA": "",
|
||||
"start": "",
|
||||
"streamIdHelp": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"videoSIPGW": {
|
||||
"busy": "",
|
||||
"busyTitle": "",
|
||||
"errorInvite": "",
|
||||
"errorInviteTitle": "",
|
||||
"errorAlreadyInvited": "",
|
||||
"errorInviteFailedTitle": "",
|
||||
"errorInviteFailed": "",
|
||||
"pending": "",
|
||||
"serviceName": "",
|
||||
"unavailableTitle": ""
|
||||
},
|
||||
"speakerStats": {
|
||||
"hours": "",
|
||||
"minutes": "",
|
||||
"name": "",
|
||||
"seconds": "",
|
||||
"speakerStats": "",
|
||||
"speakerTime": ""
|
||||
},
|
||||
"deviceSelection": {
|
||||
"deviceSettings": "",
|
||||
"noPermission": "",
|
||||
"previewUnavailable": "",
|
||||
"selectADevice": "",
|
||||
"testAudio": ""
|
||||
},
|
||||
"videoStatus": {
|
||||
"callQuality": "",
|
||||
"hd": "",
|
||||
"highDefinition": "",
|
||||
"labelTooltipVideo": "",
|
||||
"labelTooltipAudioOnly": "",
|
||||
"ld": "",
|
||||
"lowDefinition": "",
|
||||
"onlyAudioAvailable": "",
|
||||
"onlyAudioSupported": "",
|
||||
"p2pEnabled": "",
|
||||
"p2pVideoQualityDescription": "",
|
||||
"recHighDefinitionOnly": "",
|
||||
"sd": "",
|
||||
"standardDefinition": "",
|
||||
"qualityButtonTip": ""
|
||||
},
|
||||
"dialOut": {
|
||||
"statusMessage": ""
|
||||
},
|
||||
"addPeople": {
|
||||
"add": "",
|
||||
"countryNotSupported": "",
|
||||
"countryReminder": "",
|
||||
"disabled": "",
|
||||
"invite": "",
|
||||
"loading": "",
|
||||
"loadingNumber": "",
|
||||
"loadingPeople": "",
|
||||
"noResults": "",
|
||||
"noValidNumbers": "",
|
||||
"searchNumbers": "",
|
||||
"searchPeople": "",
|
||||
"searchPeopleAndNumbers": "",
|
||||
"telephone": "",
|
||||
"title": "",
|
||||
"failedToAdd": ""
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
"msg": "",
|
||||
"retry": "",
|
||||
"support": "",
|
||||
"supportMsg": ""
|
||||
},
|
||||
"deviceError": {
|
||||
"cameraError": "",
|
||||
"microphoneError": "",
|
||||
"cameraPermission": "",
|
||||
"microphonePermission": ""
|
||||
},
|
||||
"feedback": {
|
||||
"average": "",
|
||||
"bad": "",
|
||||
"good": "",
|
||||
"rateExperience": "",
|
||||
"veryBad": "",
|
||||
"veryGood": ""
|
||||
},
|
||||
"info": {
|
||||
"addPassword": "",
|
||||
"cancelPassword": "",
|
||||
"conferenceURL": "",
|
||||
"country": "",
|
||||
"dialANumber": "",
|
||||
"dialInNumber": "",
|
||||
"dialInConferenceID": "",
|
||||
"dialInNotSupported": "",
|
||||
"genericError": "",
|
||||
"invitePhone": "",
|
||||
"invitePhoneAlternatives": "",
|
||||
"inviteURL": "",
|
||||
"moreNumbers": "",
|
||||
"noNumbers": "",
|
||||
"noPassword": "",
|
||||
"noRoom": "",
|
||||
"numbers": "",
|
||||
"password": "",
|
||||
"title": "",
|
||||
"tooltip": ""
|
||||
},
|
||||
"settingsView": {
|
||||
"alertOk": "",
|
||||
"alertTitle": "",
|
||||
"alertURLText": "",
|
||||
"conferenceSection": "",
|
||||
"displayName": "",
|
||||
"email": "",
|
||||
"header": "",
|
||||
"profileSection": "",
|
||||
"serverURL": "",
|
||||
"startWithAudioMuted": "",
|
||||
"startWithVideoMuted": ""
|
||||
},
|
||||
"calendarSync": {
|
||||
"later": "",
|
||||
"next": "",
|
||||
"nextMeeting": "",
|
||||
"now": ""
|
||||
},
|
||||
"recentList": {
|
||||
"today": "",
|
||||
"yesterday": "",
|
||||
"earlier": ""
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"contactlist": "__count__ Member",
|
||||
"contactlist_plural": "__count__ Members",
|
||||
"passwordSetRemotely": "set by another member",
|
||||
"poweredby": "powered by",
|
||||
@@ -22,7 +23,6 @@
|
||||
"react-nativeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"chromeGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"androidGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"electronGrantPermissions": "Please grant permissions to use your camera and microphone",
|
||||
"firefoxGrantPermissions": "Select <b><i>Share Selected Device</i></b> when your browser asks for permissions.",
|
||||
"operaGrantPermissions": "Select <b><i>Allow</i></b> when your browser asks for permissions.",
|
||||
"iexplorerGrantPermissions": "Select <b><i>OK</i></b> when your browser asks for permissions.",
|
||||
@@ -55,7 +55,6 @@
|
||||
"go": "GO",
|
||||
"join": "JOIN",
|
||||
"privacy": "Privacy",
|
||||
"recentList": "History",
|
||||
"roomname": "Enter room name",
|
||||
"roomnameHint": "Enter the name or URL of the room you want to join. You may make a name up, just let the people you are meeting know it so that they enter the same name.",
|
||||
"sendFeedback": "Send feedback",
|
||||
@@ -74,28 +73,21 @@
|
||||
"toolbar": {
|
||||
"addPeople": "Add people to your call",
|
||||
"audioonly": "Enable / Disable audio only mode (saves bandwidth)",
|
||||
"callQuality": "Manage call quality",
|
||||
"enterFullScreen": "View full screen",
|
||||
"exitFullScreen": "Exit full screen",
|
||||
"feedback": "Leave feedback",
|
||||
"moreActions": "More actions",
|
||||
"mute": "Mute / Unmute",
|
||||
"videomute": "Start / Stop camera",
|
||||
"authenticate": "Authenticate",
|
||||
"lock": "Lock / Unlock room",
|
||||
"chat": "Open / Close chat",
|
||||
"etherpad": "Open / Close shared document",
|
||||
"documentOpen": "Open shared document",
|
||||
"documentClose": "Close shared document",
|
||||
"sharedvideo": "Share a YouTube video",
|
||||
"sharescreen": "Screen share",
|
||||
"stopSharedVideo": "Stop YouTube video",
|
||||
"sharescreen": "Start / Stop screen sharing",
|
||||
"fullscreen": "View / Exit full screen",
|
||||
"sip": "Call SIP number",
|
||||
"Settings": "Settings",
|
||||
"hangup": "Leave",
|
||||
"login": "Login",
|
||||
"logout": "Logout",
|
||||
"dialpad": "Open / Close dialpad",
|
||||
"sharedVideoMutedPopup": "Your shared video has been muted so that you can talk to the other members.",
|
||||
"micMutedPopup": "Your microphone has been muted so that you would fully enjoy your shared video.",
|
||||
"talkWhileMutedPopup": "Trying to speak? You are muted.",
|
||||
@@ -104,9 +96,17 @@
|
||||
"micDisabled": "Microphone is not available",
|
||||
"filmstrip": "Show / Hide videos",
|
||||
"profile": "Edit your profile",
|
||||
"raiseHand": "Raise / Lower your hand",
|
||||
"shortcuts": "View shortcuts",
|
||||
"speakerStats": "Speaker stats"
|
||||
"raiseHand": "Raise / Lower your hand"
|
||||
},
|
||||
"unsupportedBrowser": {
|
||||
"appNotInstalled": "Join this meeting with __app__ on your phone.",
|
||||
"downloadApp": "Download the app",
|
||||
"openApp": "Continue to __app__"
|
||||
},
|
||||
"bottomtoolbar": {
|
||||
"chat": "Open / close chat",
|
||||
"filmstrip": "Show / hide videos",
|
||||
"contactlist": "View and invite members"
|
||||
},
|
||||
"chat":{
|
||||
"nickname": {
|
||||
@@ -285,7 +285,6 @@
|
||||
"liveStreaming": "Live Streaming",
|
||||
"streamKey": "Live stream key",
|
||||
"startLiveStreaming": "Go live now",
|
||||
"startRecording": "Start recording",
|
||||
"stopStreamingWarning": "Are you sure you would like to stop the live streaming?",
|
||||
"stopRecordingWarning": "Are you sure you would like to stop the recording?",
|
||||
"stopLiveStreaming": "Stop live streaming",
|
||||
@@ -441,18 +440,15 @@
|
||||
"noPermission": "Permission not granted",
|
||||
"previewUnavailable": "Preview unavailable",
|
||||
"selectADevice": "Select a device",
|
||||
"testAudio": "Play a test sound"
|
||||
"testAudio": "Test sound"
|
||||
},
|
||||
"videoStatus": {
|
||||
"callQuality": "Call Quality",
|
||||
"hd": "HD",
|
||||
"hdTooltip": "Viewing high definition video",
|
||||
"highDefinition": "High definition",
|
||||
"labelTooltipAudioOnly": "Audio-only mode enabled",
|
||||
"labelTooiltipNoVideo": "No video",
|
||||
"labelTooltipVideo": "Current video quality",
|
||||
"labelTooltipAudioOnly": "Audio-only mode enabled",
|
||||
"ld": "LD",
|
||||
"ldTooltip": "Viewing low definition video",
|
||||
"lowDefinition": "Low definition",
|
||||
"onlyAudioAvailable": "Only audio is available",
|
||||
"onlyAudioSupported": "We only support audio in this browser.",
|
||||
@@ -460,7 +456,6 @@
|
||||
"p2pVideoQualityDescription": "In peer to peer mode, received call quality can only be toggled between high and audio only. Other settings will not be honored until peer to peer is exited.",
|
||||
"recHighDefinitionOnly": "Will prefer high definition.",
|
||||
"sd": "SD",
|
||||
"sdTooltip": "Viewing standard definition video",
|
||||
"standardDefinition": "Standard definition",
|
||||
"qualityButtonTip": "Change received video quality"
|
||||
},
|
||||
@@ -468,7 +463,7 @@
|
||||
"statusMessage": "is now __status__"
|
||||
},
|
||||
"addPeople": {
|
||||
"add": "Invite",
|
||||
"add": "Add",
|
||||
"countryNotSupported": "We do not support this destination yet.",
|
||||
"countryReminder": "Calling outside the US? Please make sure you start with the country code!",
|
||||
"disabled": "You can't invite people.",
|
||||
@@ -478,12 +473,11 @@
|
||||
"loadingPeople": "Searching for people to invite",
|
||||
"noResults": "No matching search results",
|
||||
"noValidNumbers": "Please enter a phone number",
|
||||
"notAvailable": "You can't invite people.",
|
||||
"searchNumbers": "Add phone numbers",
|
||||
"searchPeople": "Search for people",
|
||||
"searchPeopleAndNumbers": "Search for people or add their phone numbers",
|
||||
"searchNumbers": "Enter a phone number to invite",
|
||||
"searchPeople": "Enter a name to invite",
|
||||
"searchPeopleAndNumbers": "Enter a name or phone number to invite",
|
||||
"telephone": "Telephone: __number__",
|
||||
"title": "Invite people to this meeting",
|
||||
"title": "Invite people to your meeting",
|
||||
"failedToAdd": "Failed to add members"
|
||||
},
|
||||
"inlineDialogFailure": {
|
||||
@@ -502,33 +496,30 @@
|
||||
"average": "Average",
|
||||
"bad": "Bad",
|
||||
"good": "Good",
|
||||
"detailsLabel": "Tell us more about it.",
|
||||
"rateExperience": "Rate your meeting experience",
|
||||
"rateExperience": "Please rate your meeting experience.",
|
||||
"veryBad": "Very Bad",
|
||||
"veryGood": "Very Good"
|
||||
},
|
||||
"info": {
|
||||
"addPassword": "Add password",
|
||||
"cancelPassword": "Cancel password",
|
||||
"conferenceURL": "Link:",
|
||||
"conferenceURL": "Link: __url__",
|
||||
"country": "Country",
|
||||
"dialANumber": "To join your meeting, dial one of these numbers and then enter this PIN: __conferenceID__#",
|
||||
"dialInNumber": "Dial-in:",
|
||||
"dialInConferenceID": "PIN:",
|
||||
"dialInNumber": "Dial-in: __phoneNumber__",
|
||||
"dialInConferenceID": "PIN: __conferenceID__#",
|
||||
"dialInNotSupported": "Sorry, dialing in is currently not suppported.",
|
||||
"genericError": "Whoops, something went wrong.",
|
||||
"inviteLiveStream": "To view the live stream of this meeting, click this link: __url__",
|
||||
"invitePhone": "To join by phone, dial __number__ and enter this PIN: __conferenceID__#",
|
||||
"invitePhoneAlternatives": "To view more phone numbers, click this link: __url__",
|
||||
"inviteURL": "To join the video meeting, click this link: __url__",
|
||||
"liveStreamURL": "Live stream:",
|
||||
"moreNumbers": "More numbers",
|
||||
"noNumbers": "No dial-in numbers.",
|
||||
"noPassword": "None",
|
||||
"noRoom": "No room was specified to dial-in into.",
|
||||
"numbers": "Dial-in Numbers",
|
||||
"password": "Password:",
|
||||
"title": "Share",
|
||||
"title": "Call info",
|
||||
"tooltip": "Get access info about the meeting"
|
||||
},
|
||||
"settingsView": {
|
||||
@@ -548,25 +539,11 @@
|
||||
"later": "Later",
|
||||
"next": "Upcoming",
|
||||
"nextMeeting": "next meeting",
|
||||
"now": "Now",
|
||||
"permissionButton": "Open settings",
|
||||
"permissionMessage": "Calendar permission is required to list your meetings in the app."
|
||||
"now": "Now"
|
||||
},
|
||||
"recentList": {
|
||||
"today": "Today",
|
||||
"yesterday": "Yesterday",
|
||||
"earlier": "Earlier"
|
||||
},
|
||||
"sectionList": {
|
||||
"pullToRefresh": "Pull to refresh"
|
||||
},
|
||||
"deepLinking": {
|
||||
"title": "Launching your meeting in __app__...",
|
||||
"description": "Nothing happened? We tried launching your meeting in the __app__ desktop app. Try again or launch it in the __app__ web app.",
|
||||
"tryAgainButton": "Try again in desktop",
|
||||
"launchWebButton": "Launch in web",
|
||||
"appNotInstalled": "You need the __app__ mobile app to join this meeting on your phone.",
|
||||
"downloadApp": "Download the app",
|
||||
"openApp": "Continue to the app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
// @flow
|
||||
|
||||
import * as JitsiMeetConferenceEvents from '../../ConferenceEvents';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import {
|
||||
createApiEvent,
|
||||
sendAnalytics
|
||||
} from '../../react/features/analytics';
|
||||
import { parseJWTFromURLParams } from '../../react/features/base/jwt';
|
||||
import { invite } from '../../react/features/invite';
|
||||
import { getJitsiMeetTransport } from '../transport';
|
||||
|
||||
import { API_ID } from './constants';
|
||||
@@ -82,6 +81,10 @@ function initCommands() {
|
||||
sendAnalytics(createApiEvent('chat.toggled'));
|
||||
APP.UI.toggleChat();
|
||||
},
|
||||
'toggle-contact-list': () => {
|
||||
sendAnalytics(createApiEvent('contact.list.toggled'));
|
||||
APP.UI.toggleContactList();
|
||||
},
|
||||
'toggle-share-screen': () => {
|
||||
sendAnalytics(createApiEvent('screen.sharing.toggled'));
|
||||
toggleScreenSharing();
|
||||
@@ -108,29 +111,8 @@ function initCommands() {
|
||||
|
||||
return false;
|
||||
});
|
||||
transport.on('request', (request, callback) => {
|
||||
const { name } = request;
|
||||
|
||||
transport.on('request', ({ name }, callback) => {
|
||||
switch (name) {
|
||||
case 'invite':
|
||||
APP.store.dispatch(
|
||||
invite(request.invitees))
|
||||
.then(failedInvitees => {
|
||||
let error;
|
||||
let result;
|
||||
|
||||
if (failedInvitees.length) {
|
||||
error = new Error('One or more invites failed!');
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
|
||||
callback({
|
||||
error,
|
||||
result
|
||||
});
|
||||
});
|
||||
break;
|
||||
case 'is-audio-muted':
|
||||
callback(APP.conference.isLocalAudioMuted());
|
||||
break;
|
||||
|
||||
24
modules/API/external/external_api.js
vendored
24
modules/API/external/external_api.js
vendored
@@ -24,6 +24,7 @@ const commands = {
|
||||
submitFeedback: 'submit-feedback',
|
||||
toggleAudio: 'toggle-audio',
|
||||
toggleChat: 'toggle-chat',
|
||||
toggleContactList: 'toggle-contact-list',
|
||||
toggleFilmStrip: 'toggle-film-strip',
|
||||
toggleShareScreen: 'toggle-share-screen',
|
||||
toggleVideo: 'toggle-video'
|
||||
@@ -200,8 +201,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* authentication.
|
||||
* @param {string} [options.onload] - The onload function that will listen
|
||||
* for iframe onload event.
|
||||
* @param {Array<Object>} [options.invitees] - Array of objects containing
|
||||
* information about new participants that will be invited in the call.
|
||||
*/
|
||||
constructor(domain, ...args) {
|
||||
super();
|
||||
@@ -214,8 +213,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
interfaceConfigOverwrite = {},
|
||||
noSSL = false,
|
||||
jwt = undefined,
|
||||
onload = undefined,
|
||||
invitees
|
||||
onload = undefined
|
||||
} = parseArguments(args);
|
||||
|
||||
this._parentNode = parentNode;
|
||||
@@ -235,7 +233,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
}
|
||||
})
|
||||
});
|
||||
this._invitees = invitees;
|
||||
this._isLargeVideoVisible = true;
|
||||
this._numberOfParticipants = 0;
|
||||
this._participants = {};
|
||||
@@ -366,9 +363,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
|
||||
switch (name) {
|
||||
case 'video-conference-joined':
|
||||
if (this._invitees) {
|
||||
this.invite(this._invitees);
|
||||
}
|
||||
this._myUserID = userID;
|
||||
this._participants[userID] = {
|
||||
avatarURL: data.avatarURL
|
||||
@@ -557,6 +551,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
* toggleVideo - mutes / unmutes video. no arguments
|
||||
* toggleFilmStrip - hides / shows the filmstrip. no arguments
|
||||
* toggleChat - hides / shows chat. no arguments.
|
||||
* toggleContactList - hides / shows contact list. no arguments.
|
||||
* toggleShareScreen - starts / stops screen sharing. no arguments.
|
||||
*
|
||||
* @param {Object} commandList - The object with commands to be executed.
|
||||
@@ -582,19 +577,6 @@ export default class JitsiMeetExternalAPI extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite people to the call.
|
||||
*
|
||||
* @param {Array<Object>} invitees - The invitees.
|
||||
* @returns {Promise} - Resolves on success and rejects on failure.
|
||||
*/
|
||||
invite(invitees) {
|
||||
return this._transport.sendRequest({
|
||||
name: 'invite',
|
||||
invitees
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the audio mute status.
|
||||
*
|
||||
|
||||
132
modules/UI/UI.js
132
modules/UI/UI.js
@@ -30,14 +30,19 @@ import {
|
||||
} from '../../react/features/base/participants';
|
||||
import { destroyLocalTracks } from '../../react/features/base/tracks';
|
||||
import { openDisplayNamePrompt } from '../../react/features/display-name';
|
||||
import { setEtherpadHasInitialzied } from '../../react/features/etherpad';
|
||||
import {
|
||||
setNotificationsEnabled,
|
||||
showWarningNotification
|
||||
} from '../../react/features/notifications';
|
||||
import { shouldShowOnlyDeviceSelection } from '../../react/features/settings';
|
||||
import {
|
||||
checkAutoEnableDesktopSharing,
|
||||
clearButtonPopup,
|
||||
dockToolbox,
|
||||
setButtonPopupTimeout,
|
||||
setToolbarButton,
|
||||
showDialPadButton,
|
||||
showEtherpadButton,
|
||||
showSharedVideoButton,
|
||||
showToolbox
|
||||
} from '../../react/features/toolbox';
|
||||
|
||||
@@ -95,6 +100,9 @@ const UIListeners = new Map([
|
||||
], [
|
||||
UIEvents.SHARED_VIDEO_CLICKED,
|
||||
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
|
||||
], [
|
||||
UIEvents.TOGGLE_FULLSCREEN,
|
||||
() => UI.toggleFullScreen()
|
||||
], [
|
||||
UIEvents.TOGGLE_CHAT,
|
||||
() => UI.toggleChat()
|
||||
@@ -105,12 +113,16 @@ const UIListeners = new Map([
|
||||
// opened through a button in settings and not directly displayed in
|
||||
// settings itself. As it is not useful to only have a settings menu
|
||||
// with a button to open a dialog, open the dialog directly instead.
|
||||
if (shouldShowOnlyDeviceSelection()) {
|
||||
if (interfaceConfig.SETTINGS_SECTIONS.length === 1
|
||||
&& UIUtil.isSettingEnabled('devices')) {
|
||||
APP.store.dispatch(openDeviceSelectionDialog());
|
||||
} else {
|
||||
UI.toggleSidePanel('settings_container');
|
||||
}
|
||||
}
|
||||
], [
|
||||
UIEvents.TOGGLE_CONTACT_LIST,
|
||||
() => UI.toggleContactList()
|
||||
], [
|
||||
UIEvents.TOGGLE_PROFILE,
|
||||
() => UI.toggleSidePanel('profile_container')
|
||||
@@ -123,6 +135,14 @@ const UIListeners = new Map([
|
||||
]
|
||||
]);
|
||||
|
||||
/**
|
||||
* Toggles the application in and out of full screen mode
|
||||
* (a.k.a. presentation mode in Chrome).
|
||||
*/
|
||||
UI.toggleFullScreen = function() {
|
||||
UIUtil.isFullScreen() ? UIUtil.exitFullScreen() : UIUtil.enterFullScreen();
|
||||
};
|
||||
|
||||
/**
|
||||
* Indicates if we're currently in full screen mode.
|
||||
*
|
||||
@@ -235,20 +255,12 @@ UI.showLocalConnectionInterrupted = function(isInterrupted) {
|
||||
|
||||
/**
|
||||
* Sets the "raised hand" status for a participant.
|
||||
*
|
||||
* @param {string} id - The id of the participant whose raised hand UI should
|
||||
* be updated.
|
||||
* @param {string} name - The name of the participant with the raised hand
|
||||
* update.
|
||||
* @param {boolean} raisedHandStatus - Whether the participant's hand is raised
|
||||
* or not.
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.setRaisedHandStatus = (id, name, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(id, raisedHandStatus);
|
||||
UI.setRaisedHandStatus = (participant, raisedHandStatus) => {
|
||||
VideoLayout.setRaisedHandStatus(participant.getId(), raisedHandStatus);
|
||||
if (raisedHandStatus) {
|
||||
messageHandler.participantNotification(
|
||||
name,
|
||||
participant.getDisplayName(),
|
||||
'notify.somebody',
|
||||
'connected',
|
||||
'notify.raisedHand');
|
||||
@@ -268,7 +280,7 @@ UI.setLocalRaisedHandStatus
|
||||
* Initialize conference UI.
|
||||
*/
|
||||
UI.initConference = function() {
|
||||
const { getState } = APP.store;
|
||||
const { dispatch, getState } = APP.store;
|
||||
const { email, id, name } = getLocalParticipant(getState);
|
||||
|
||||
// Update default button states before showing the toolbar
|
||||
@@ -288,6 +300,8 @@ UI.initConference = function() {
|
||||
UI.setUserEmail(id, email);
|
||||
}
|
||||
|
||||
dispatch(checkAutoEnableDesktopSharing());
|
||||
|
||||
// FollowMe attempts to copy certain aspects of the moderator's UI into the
|
||||
// other participants' UI. Consequently, it needs (1) read and write access
|
||||
// to the UI (depending on the moderator role of the local participant) and
|
||||
@@ -340,28 +354,25 @@ UI.start = function() {
|
||||
VideoLayout.resizeVideoArea(true, true);
|
||||
|
||||
sharedVideoManager = new SharedVideoManager(eventEmitter);
|
||||
|
||||
if (interfaceConfig.filmStripOnly) {
|
||||
$('body').addClass('filmstrip-only');
|
||||
Filmstrip.setFilmstripOnly();
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
} else {
|
||||
// eslint-disable-next-line no-negated-condition
|
||||
if (!interfaceConfig.filmStripOnly) {
|
||||
// Initialise the recording module.
|
||||
config.enableRecording
|
||||
&& Recording.init(eventEmitter, config.recordingType);
|
||||
if (config.enableRecording) {
|
||||
Recording.init(eventEmitter, config.recordingType);
|
||||
}
|
||||
|
||||
// Initialize side panels
|
||||
SidePanels.init(eventEmitter);
|
||||
|
||||
// TODO: remove this class once the old toolbar has been removed. This
|
||||
// class is set so that any CSS changes needed to adjust elements
|
||||
// outside of the new toolbar can be scoped to just the app with the new
|
||||
// toolbar enabled.
|
||||
$('body').addClass('use-new-toolbox');
|
||||
} else {
|
||||
$('body').addClass('filmstrip-only');
|
||||
UI.showToolbar();
|
||||
Filmstrip.setFilmstripOnly();
|
||||
APP.store.dispatch(setNotificationsEnabled(false));
|
||||
}
|
||||
|
||||
interfaceConfig.VERTICAL_FILMSTRIP
|
||||
&& $('body').addClass('vertical-filmstrip');
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('body').addClass('vertical-filmstrip');
|
||||
}
|
||||
|
||||
document.title = interfaceConfig.APP_NAME;
|
||||
};
|
||||
@@ -393,7 +404,12 @@ UI.bindEvents = () => {
|
||||
// Resize and reposition videos in full screen mode.
|
||||
$(document).on(
|
||||
'webkitfullscreenchange mozfullscreenchange fullscreenchange',
|
||||
onResize);
|
||||
() => {
|
||||
eventEmitter.emit(
|
||||
UIEvents.FULLSCREEN_TOGGLED,
|
||||
UIUtil.isFullScreen());
|
||||
onResize();
|
||||
});
|
||||
|
||||
$(window).resize(onResize);
|
||||
};
|
||||
@@ -440,6 +456,12 @@ UI.addRemoteStream = track => VideoLayout.onRemoteStreamAdded(track);
|
||||
*/
|
||||
UI.removeRemoteStream = track => VideoLayout.onRemoteStreamRemoved(track);
|
||||
|
||||
/**
|
||||
* Update chat subject.
|
||||
* @param {string} subject new chat subject
|
||||
*/
|
||||
UI.setSubject = subject => Chat.setSubject(subject);
|
||||
|
||||
/**
|
||||
* Setup and show Etherpad.
|
||||
* @param {string} name etherpad id
|
||||
@@ -452,7 +474,7 @@ UI.initEtherpad = name => {
|
||||
etherpadManager
|
||||
= new EtherpadManager(config.etherpad_base, name, eventEmitter);
|
||||
|
||||
APP.store.dispatch(setEtherpadHasInitialzied());
|
||||
APP.store.dispatch(showEtherpadButton());
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -520,6 +542,10 @@ UI.onPeerVideoTypeChanged
|
||||
UI.updateLocalRole = isModerator => {
|
||||
VideoLayout.showModeratorIndicator();
|
||||
|
||||
APP.store.dispatch(showSharedVideoButton());
|
||||
|
||||
Recording.showRecordingButton(isModerator);
|
||||
|
||||
if (isModerator) {
|
||||
if (!interfaceConfig.DISABLE_FOCUS_INDICATOR) {
|
||||
messageHandler.participantNotification(
|
||||
@@ -613,6 +639,11 @@ UI.isChatVisible = () => Chat.isVisible();
|
||||
*/
|
||||
UI.toggleChat = () => UI.toggleSidePanel('chat_container');
|
||||
|
||||
/**
|
||||
* Toggles contact list panel.
|
||||
*/
|
||||
UI.toggleContactList = () => UI.toggleSidePanel('contacts_container');
|
||||
|
||||
/**
|
||||
* Toggles the given side panel.
|
||||
*
|
||||
@@ -628,6 +659,27 @@ UI.inputDisplayNameHandler = function(newDisplayName) {
|
||||
eventEmitter.emit(UIEvents.NICKNAME_CHANGED, newDisplayName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show custom popup/tooltip for a specified button.
|
||||
*
|
||||
* @param {string} buttonName - The name of the button as specified in the
|
||||
* button configurations for the toolbar.
|
||||
* @param {string} popupSelectorID - The id of the popup to show as specified in
|
||||
* the button configurations for the toolbar.
|
||||
* @param {boolean} show - True or false/show or hide the popup
|
||||
* @param {number} timeout - The time to show the popup
|
||||
* @returns {void}
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
UI.showCustomToolbarPopup = function(buttonName, popupID, show, timeout) {
|
||||
const action
|
||||
= show
|
||||
? setButtonPopupTimeout(buttonName, popupID, timeout)
|
||||
: clearButtonPopup(buttonName);
|
||||
|
||||
APP.store.dispatch(action);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the type of the remote video.
|
||||
* @param jid the jid for the remote video
|
||||
@@ -850,6 +902,17 @@ UI.promptDisplayName = () => {
|
||||
*/
|
||||
UI.setAudioLevel = (id, lvl) => VideoLayout.setAudioLevel(id, lvl);
|
||||
|
||||
/**
|
||||
* Update state of desktop sharing buttons.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
UI.updateDesktopSharingButtons
|
||||
= () =>
|
||||
APP.store.dispatch(setToolbarButton('desktop', {
|
||||
toggled: APP.conference.isSharingScreen
|
||||
}));
|
||||
|
||||
/**
|
||||
* Hide connection quality statistics from UI.
|
||||
*/
|
||||
@@ -881,6 +944,9 @@ UI.addMessage = function(from, displayName, message, stamp) {
|
||||
Chat.updateChatConversation(from, displayName, message, stamp);
|
||||
};
|
||||
|
||||
UI.updateDTMFSupport
|
||||
= isDTMFSupported => APP.store.dispatch(showDialPadButton(isDTMFSupported));
|
||||
|
||||
UI.updateRecordingState = function(state) {
|
||||
Recording.updateRecordingState(state);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/* global $, APP, interfaceConfig */
|
||||
|
||||
import { setDocumentEditingState } from '../../../react/features/etherpad';
|
||||
import { getToolboxHeight } from '../../../react/features/toolbox';
|
||||
/* global $, interfaceConfig */
|
||||
|
||||
import VideoLayout from '../videolayout/VideoLayout';
|
||||
import LargeContainer from '../videolayout/LargeContainer';
|
||||
@@ -129,7 +126,7 @@ class Etherpad extends LargeContainer {
|
||||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight - getToolboxHeight();
|
||||
height = containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
@@ -155,9 +152,6 @@ class Etherpad extends LargeContainer {
|
||||
document.body.style.background = '#eeeeee';
|
||||
$iframe.css({ visibility: 'visible' });
|
||||
$container.css({ zIndex: 2 });
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(true));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -176,9 +170,6 @@ class Etherpad extends LargeContainer {
|
||||
$iframe.fadeOut(300, () => {
|
||||
$iframe.css({ visibility: 'hidden' });
|
||||
$container.css({ zIndex: 0 });
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(false));
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
@@ -251,7 +242,5 @@ export default class EtherpadManager {
|
||||
|
||||
this.eventEmitter
|
||||
.emit(UIEvents.TOGGLED_SHARED_DOCUMENT, !isVisible);
|
||||
|
||||
APP.store.dispatch(setDocumentEditingState(!isVisible));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* global APP, config, interfaceConfig */
|
||||
/* global APP, $, config, interfaceConfig */
|
||||
/*
|
||||
* Copyright @ 2015 Atlassian Pty Ltd
|
||||
*
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
StartLiveStreamDialog,
|
||||
StopLiveStreamDialog,
|
||||
hideRecordingLabel,
|
||||
setRecordingType,
|
||||
updateRecordingState
|
||||
} from '../../../react/features/recording';
|
||||
|
||||
@@ -109,10 +108,7 @@ function _requestLiveStreamId() {
|
||||
return new Promise((resolve, reject) =>
|
||||
APP.store.dispatch(openDialog(StartLiveStreamDialog, {
|
||||
onCancel: reject,
|
||||
onSubmit: (streamId, broadcastId) => resolve({
|
||||
broadcastId,
|
||||
streamId
|
||||
})
|
||||
onSubmit: resolve
|
||||
})));
|
||||
}
|
||||
|
||||
@@ -195,7 +191,8 @@ function isStartingStatus(status) {
|
||||
|
||||
/**
|
||||
* Manages the recording user interface and user experience.
|
||||
* @type {{init, updateRecordingState, updateRecordingUI, checkAutoRecord}}
|
||||
* @type {{init, initRecordingButton, showRecordingButton, updateRecordingState,
|
||||
* updateRecordingUI, checkAutoRecord}}
|
||||
*/
|
||||
const Recording = {
|
||||
/**
|
||||
@@ -205,8 +202,6 @@ const Recording = {
|
||||
this.eventEmitter = eventEmitter;
|
||||
this.recordingType = recordingType;
|
||||
|
||||
APP.store.dispatch(setRecordingType(recordingType));
|
||||
|
||||
this.updateRecordingState(APP.conference.getRecordingState());
|
||||
|
||||
if (recordingType === 'jibri') {
|
||||
@@ -217,8 +212,12 @@ const Recording = {
|
||||
Object.assign(this, RECORDING_TRANSLATION_KEYS);
|
||||
}
|
||||
|
||||
this.eventEmitter.on(UIEvents.TOGGLE_RECORDING,
|
||||
() => this._onToolbarButtonClick());
|
||||
// XXX Due to the React-ification of Toolbox, the HTMLElement with id
|
||||
// toolbar_button_record may not exist yet.
|
||||
$(document).on(
|
||||
'click',
|
||||
'#toolbar_button_record',
|
||||
ev => this._onToolbarButtonClick(ev));
|
||||
|
||||
// If I am a recorder then I publish my recorder custom role to notify
|
||||
// everyone.
|
||||
@@ -237,6 +236,28 @@ const Recording = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialise the recording button.
|
||||
*/
|
||||
initRecordingButton() {
|
||||
const selector = $('#toolbar_button_record');
|
||||
|
||||
selector.addClass(this.baseClass);
|
||||
selector.attr('data-i18n', `[content]${this.recordingButtonTooltip}`);
|
||||
APP.translation.translateElement(selector);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows or hides the 'recording' button.
|
||||
* @param show {true} to show the recording button, {false} to hide it
|
||||
*/
|
||||
showRecordingButton(show) {
|
||||
const shouldShow = show && _isRecordingButtonEnabled();
|
||||
const id = 'toolbar_button_record';
|
||||
|
||||
UIUtil.setVisible(id, shouldShow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the recording state UI.
|
||||
* @param recordingState gives us the current recording state
|
||||
@@ -260,12 +281,12 @@ const Recording = {
|
||||
* @param recordingState gives us the current recording state
|
||||
*/
|
||||
updateRecordingUI(recordingState) {
|
||||
|
||||
const oldState = this.currentState;
|
||||
|
||||
this.currentState = recordingState;
|
||||
|
||||
let labelDisplayConfiguration;
|
||||
let isRecording = false;
|
||||
|
||||
switch (recordingState) {
|
||||
case JitsiRecordingStatus.ON:
|
||||
@@ -276,7 +297,7 @@ const Recording = {
|
||||
showSpinner: recordingState === JitsiRecordingStatus.RETRYING
|
||||
};
|
||||
|
||||
isRecording = true;
|
||||
this._setToolbarButtonToggled(true);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -301,6 +322,8 @@ const Recording = {
|
||||
: this.recordingOffKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
setTimeout(() => {
|
||||
APP.store.dispatch(hideRecordingLabel());
|
||||
}, 5000);
|
||||
@@ -314,6 +337,8 @@ const Recording = {
|
||||
key: this.recordingPendingKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -323,6 +348,8 @@ const Recording = {
|
||||
key: this.recordingErrorKey
|
||||
};
|
||||
|
||||
this._setToolbarButtonToggled(false);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -335,7 +362,6 @@ const Recording = {
|
||||
}
|
||||
|
||||
APP.store.dispatch(updateRecordingState({
|
||||
isRecording,
|
||||
labelDisplayConfiguration,
|
||||
recordingState
|
||||
}));
|
||||
@@ -390,13 +416,10 @@ const Recording = {
|
||||
case JitsiRecordingStatus.OFF: {
|
||||
if (this.recordingType === 'jibri') {
|
||||
_requestLiveStreamId()
|
||||
.then(({ broadcastId, streamId }) => {
|
||||
.then(streamId => {
|
||||
this.eventEmitter.emit(
|
||||
UIEvents.RECORDING_TOGGLED,
|
||||
{
|
||||
broadcastId,
|
||||
streamId
|
||||
});
|
||||
{ streamId });
|
||||
|
||||
// The confirm button on the start recording dialog was
|
||||
// clicked
|
||||
@@ -456,6 +479,16 @@ const Recording = {
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the toggled state of the recording toolbar button.
|
||||
*
|
||||
* @param {boolean} isToggled indicates if the button should be toggled
|
||||
* or not
|
||||
*/
|
||||
_setToolbarButtonToggled(isToggled) {
|
||||
$('#toolbar_button_record').toggleClass('toggled', isToggled);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,11 +18,7 @@ import {
|
||||
participantJoined,
|
||||
participantLeft
|
||||
} from '../../../react/features/base/participants';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight,
|
||||
showToolbox
|
||||
} from '../../../react/features/toolbox';
|
||||
import { dockToolbox, showToolbox } from '../../../react/features/toolbox';
|
||||
|
||||
import SharedVideoThumb from './SharedVideoThumb';
|
||||
|
||||
@@ -286,7 +282,7 @@ export default class SharedVideoManager {
|
||||
|
||||
thumb.setDisplayName('YouTube');
|
||||
VideoLayout.addRemoteVideoContainer(self.url, thumb);
|
||||
VideoLayout.resizeThumbnails(true);
|
||||
VideoLayout.resizeThumbnails(false, true);
|
||||
|
||||
const iframe = player.getIframe();
|
||||
|
||||
@@ -364,6 +360,7 @@ export default class SharedVideoManager {
|
||||
|
||||
player.setVolume(attributes.volume);
|
||||
logger.info(`Player change of volume:${attributes.volume}`);
|
||||
this.showSharedVideoMutedPopup(false);
|
||||
}
|
||||
|
||||
if (isPlayerPaused) {
|
||||
@@ -563,6 +560,8 @@ export default class SharedVideoManager {
|
||||
this.smartAudioMute();
|
||||
}
|
||||
}
|
||||
|
||||
this.showSharedVideoMutedPopup(mute);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -577,6 +576,7 @@ export default class SharedVideoManager {
|
||||
sendAnalytics(createEvent('audio.unmuted'));
|
||||
logger.log('Shared video: audio unmuted');
|
||||
this.emitter.emit(UIEvents.AUDIO_MUTED, false, false);
|
||||
this.showMicMutedPopup(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,8 +590,38 @@ export default class SharedVideoManager {
|
||||
sendAnalytics(createEvent('audio.muted'));
|
||||
logger.log('Shared video: audio muted');
|
||||
this.emitter.emit(UIEvents.AUDIO_MUTED, true, false);
|
||||
this.showMicMutedPopup(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup under the microphone toolbar icon that notifies the user
|
||||
* of automatic mute after a shared video has started.
|
||||
* @param show boolean, show or hide the notification
|
||||
*/
|
||||
showMicMutedPopup(show) {
|
||||
if (show) {
|
||||
this.showSharedVideoMutedPopup(false);
|
||||
}
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'microphone', 'micMutedPopup', show, 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a popup under the shared video toolbar icon that notifies the user
|
||||
* of automatic mute of the shared video after the user has unmuted their
|
||||
* mic.
|
||||
* @param show boolean, show or hide the notification
|
||||
*/
|
||||
showSharedVideoMutedPopup(show) {
|
||||
if (show) {
|
||||
this.showMicMutedPopup(false);
|
||||
}
|
||||
|
||||
APP.UI.showCustomToolbarPopup(
|
||||
'sharedvideo', 'sharedVideoMutedPopup', show, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -665,7 +695,7 @@ class SharedVideoContainer extends LargeContainer {
|
||||
let height, width;
|
||||
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
height = containerHeight - getToolboxHeight();
|
||||
height = containerHeight;
|
||||
width = containerWidth - Filmstrip.getFilmstripWidth();
|
||||
} else {
|
||||
height = containerHeight - Filmstrip.getFilmstripHeight();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
/* global $, APP */
|
||||
/* global $ */
|
||||
import UIEvents from '../../../service/UI/UIEvents';
|
||||
import { setVisiblePanel } from '../../../react/features/side-panel';
|
||||
|
||||
/**
|
||||
* Handles open and close of the extended toolbar side panel
|
||||
@@ -58,7 +57,6 @@ const SideContainerToggler = {
|
||||
|
||||
if (isSelectorVisible) {
|
||||
this.hide();
|
||||
APP.store.dispatch(setVisiblePanel(null));
|
||||
} else {
|
||||
if (this.isVisible()) {
|
||||
$('#sideToolbarContainer').children()
|
||||
@@ -76,7 +74,6 @@ const SideContainerToggler = {
|
||||
}
|
||||
|
||||
this.showInnerContainer(elementSelector);
|
||||
APP.store.dispatch(setVisiblePanel(elementId));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Chat from './chat/Chat';
|
||||
import SettingsMenu from './settings/SettingsMenu';
|
||||
import Profile from './profile/Profile';
|
||||
import ContactListView from './contactlist/ContactListView';
|
||||
import { isButtonEnabled } from '../../../react/features/toolbox';
|
||||
|
||||
const SidePanels = {
|
||||
@@ -19,6 +20,11 @@ const SidePanels = {
|
||||
if (isButtonEnabled('profile')) {
|
||||
Profile.init(eventEmitter);
|
||||
}
|
||||
|
||||
// Initialize contact list view
|
||||
if (isButtonEnabled('contacts')) {
|
||||
ContactListView.init();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* global APP, $ */
|
||||
|
||||
import { processReplacements } from './Replacement';
|
||||
import { processReplacements, linkify } from './Replacement';
|
||||
import CommandsProcessor from './Commands';
|
||||
import VideoLayout from '../../videolayout/VideoLayout';
|
||||
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
@@ -8,11 +9,7 @@ import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
import { smileys } from './smileys';
|
||||
|
||||
import { addMessage, markAllRead } from '../../../../react/features/chat';
|
||||
import {
|
||||
dockToolbox,
|
||||
getToolboxHeight
|
||||
} from '../../../../react/features/toolbox';
|
||||
import { dockToolbox, setSubject } from '../../../../react/features/toolbox';
|
||||
|
||||
let unreadMessages = 0;
|
||||
const sidePanelsContainerId = 'sideToolbarContainer';
|
||||
@@ -166,8 +163,6 @@ function addSmileys() {
|
||||
* Resizes the chat conversation.
|
||||
*/
|
||||
function resizeChatConversation() {
|
||||
// FIXME: this function can all be done with CSS. If Chat is ever rewritten,
|
||||
// do not copy over this logic.
|
||||
const msgareaHeight = $('#usermsg').outerHeight();
|
||||
const chatspace = $(`#${CHAT_CONTAINER_ID}`);
|
||||
const width = chatspace.width();
|
||||
@@ -178,12 +173,7 @@ function resizeChatConversation() {
|
||||
$('#smileys').css('bottom', (msgareaHeight - 26) / 2);
|
||||
$('#smileysContainer').css('bottom', msgareaHeight);
|
||||
chat.width(width - 10);
|
||||
|
||||
const maybeAMagicNumberForPaddingAndMargin = 100;
|
||||
const offset = maybeAMagicNumberForPaddingAndMargin
|
||||
+ msgareaHeight + getToolboxHeight();
|
||||
|
||||
chat.height(window.innerHeight - offset);
|
||||
chat.height(window.innerHeight - 15 - msgareaHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,10 +223,15 @@ const Chat = {
|
||||
|
||||
usermsg.val('').trigger('autosize.resize');
|
||||
this.focus();// eslint-disable-line no-invalid-this
|
||||
const command = new CommandsProcessor(value, eventEmitter);
|
||||
|
||||
const message = UIUtil.escapeHtml(value);
|
||||
if (command.isCommand()) {
|
||||
command.processCommand();
|
||||
} else {
|
||||
const message = UIUtil.escapeHtml(value);
|
||||
|
||||
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
|
||||
eventEmitter.emit(UIEvents.MESSAGE_CREATED, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -254,7 +249,6 @@ const Chat = {
|
||||
}
|
||||
|
||||
unreadMessages = 0;
|
||||
APP.store.dispatch(markAllRead());
|
||||
updateVisualNotification();
|
||||
|
||||
// Undock the toolbar when the chat is shown and if we're in a
|
||||
@@ -280,10 +274,9 @@ const Chat = {
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
updateChatConversation(id, displayName, message, stamp) {
|
||||
const isFromLocalParticipant = APP.conference.isLocalId(id);
|
||||
let divClassName = '';
|
||||
|
||||
if (isFromLocalParticipant) {
|
||||
if (APP.conference.isLocalId(id)) {
|
||||
divClassName = 'localuser';
|
||||
} else {
|
||||
divClassName = 'remoteuser';
|
||||
@@ -301,7 +294,6 @@ const Chat = {
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br/>');
|
||||
const escDisplayName = UIUtil.escapeHtml(displayName);
|
||||
const timestamp = getCurrentTime(stamp);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
message = processReplacements(escMessage);
|
||||
@@ -310,18 +302,13 @@ const Chat = {
|
||||
= `${'<div class="chatmessage">'
|
||||
+ '<img src="images/chatArrow.svg" class="chatArrow">'
|
||||
+ '<div class="username '}${divClassName}">${escDisplayName
|
||||
}</div><div class="timestamp">${timestamp
|
||||
}</div><div class="timestamp">${getCurrentTime(stamp)
|
||||
}</div><div class="usermessage">${message}</div>`
|
||||
+ '</div>';
|
||||
|
||||
$('#chatconversation').append(messageContainer);
|
||||
$('#chatconversation').animate(
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
|
||||
const markAsRead = Chat.isVisible() || isFromLocalParticipant;
|
||||
|
||||
APP.store.dispatch(addMessage(
|
||||
escDisplayName, message, timestamp, markAsRead));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -344,6 +331,21 @@ const Chat = {
|
||||
{ scrollTop: $('#chatconversation')[0].scrollHeight }, 1000);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the subject to the UI
|
||||
* @param subject the subject
|
||||
*/
|
||||
setSubject(subject) {
|
||||
if (subject) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
subject = subject.trim();
|
||||
}
|
||||
|
||||
const html = linkify(UIUtil.escapeHtml(subject));
|
||||
|
||||
APP.store.dispatch(setSubject(html));
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the chat conversation mode.
|
||||
* Conversation mode is the normal chat mode, non conversation mode is
|
||||
|
||||
94
modules/UI/side_pannels/chat/Commands.js
Normal file
94
modules/UI/side_pannels/chat/Commands.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
import UIEvents from '../../../../service/UI/UIEvents';
|
||||
|
||||
/**
|
||||
* List with supported commands. The keys are the names of the commands and
|
||||
* the value is the function that processes the message.
|
||||
* @type {{String: function}}
|
||||
*/
|
||||
const commands = {
|
||||
'topic': processTopic
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts the command from the message.
|
||||
* @param message the received message
|
||||
* @returns {string} the command
|
||||
*/
|
||||
function getCommand(message) {
|
||||
if (message) {
|
||||
for (const command in commands) {
|
||||
if (message.indexOf(`/${command}`) === 0) {
|
||||
return command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the data for topic command.
|
||||
* @param commandArguments the arguments of the topic command.
|
||||
*/
|
||||
function processTopic(commandArguments, emitter) {
|
||||
const topic = UIUtil.escapeHtml(commandArguments);
|
||||
|
||||
emitter.emit(UIEvents.SUBJECT_CHANGED, topic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new CommandProccessor instance from a message that
|
||||
* handles commands received via chat messages.
|
||||
* @param message the message
|
||||
* @constructor
|
||||
*/
|
||||
function CommandsProcessor(message, emitter) {
|
||||
const command = getCommand(message);
|
||||
|
||||
this.emitter = emitter;
|
||||
|
||||
/**
|
||||
* Returns the name of the command.
|
||||
* @returns {String} the command
|
||||
*/
|
||||
this.getCommand = function() {
|
||||
return command;
|
||||
};
|
||||
|
||||
|
||||
const messageArgument = message.substr(command.length + 2);
|
||||
|
||||
/**
|
||||
* Returns the arguments of the command.
|
||||
* @returns {string}
|
||||
*/
|
||||
this.getArgument = function() {
|
||||
return messageArgument;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this instance is valid command or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
CommandsProcessor.prototype.isCommand = function() {
|
||||
if (this.getCommand()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Processes the command.
|
||||
*/
|
||||
CommandsProcessor.prototype.processCommand = function() {
|
||||
if (!this.isCommand()) {
|
||||
return;
|
||||
}
|
||||
|
||||
commands[this.getCommand()](this.getArgument(), this.emitter);
|
||||
};
|
||||
|
||||
export default CommandsProcessor;
|
||||
56
modules/UI/side_pannels/contactlist/ContactListView.js
Normal file
56
modules/UI/side_pannels/contactlist/ContactListView.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/* global $, APP */
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React 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 { ContactListPanel } from '../../../../react/features/contact-list';
|
||||
/* eslint-enable no-unused-vars */
|
||||
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
|
||||
/**
|
||||
* Contact list.
|
||||
*
|
||||
* FIXME: One day this view should no longer be called "contact list" because
|
||||
* the term "contact" is not used elsewhere. Normally people in the conference
|
||||
* are internally refered to as "participants" or externally as "members".
|
||||
*/
|
||||
const ContactListView = {
|
||||
/**
|
||||
* Creates and appends the contact list to the side panel.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
init() {
|
||||
const contactListPanelContainer = document.createElement('div');
|
||||
|
||||
contactListPanelContainer.id = 'contacts_container';
|
||||
contactListPanelContainer.className = 'sideToolbarContainer__inner';
|
||||
|
||||
$('#sideToolbarContainer').append(contactListPanelContainer);
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store = { APP.store }>
|
||||
<I18nextProvider i18n = { i18next }>
|
||||
<ContactListPanel />
|
||||
</I18nextProvider>
|
||||
</Provider>,
|
||||
contactListPanelContainer
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the contact list is currently visible.
|
||||
*
|
||||
* @return {boolean) true if the contact list is currently visible.
|
||||
*/
|
||||
isVisible() {
|
||||
return UIUtil.isVisible(document.getElementById('contactlist'));
|
||||
}
|
||||
};
|
||||
|
||||
export default ContactListView;
|
||||
@@ -8,10 +8,7 @@ import { I18nextProvider } from 'react-i18next';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { i18next } from '../../../../react/features/base/i18n';
|
||||
import {
|
||||
SettingsMenu,
|
||||
isSettingEnabled
|
||||
} from '../../../../react/features/settings';
|
||||
import { SettingsMenu } from '../../../../react/features/settings';
|
||||
import UIUtil from '../../util/UIUtil';
|
||||
|
||||
/* eslint-enable no-unused-vars */
|
||||
@@ -26,9 +23,9 @@ export default {
|
||||
$('#sideToolbarContainer').append(settingsMenuContainer);
|
||||
|
||||
const props = {
|
||||
showDeviceSettings: isSettingEnabled('devices'),
|
||||
showLanguageSettings: isSettingEnabled('language'),
|
||||
showModeratorSettings: isSettingEnabled('moderator'),
|
||||
showDeviceSettings: UIUtil.isSettingEnabled('devices'),
|
||||
showLanguageSettings: UIUtil.isSettingEnabled('language'),
|
||||
showModeratorSettings: UIUtil.isSettingEnabled('moderator'),
|
||||
showTitles: interfaceConfig.SETTINGS_SECTIONS.length > 1
|
||||
};
|
||||
|
||||
|
||||
@@ -121,6 +121,18 @@ const UIUtil = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the setting section is enabled.
|
||||
*
|
||||
* @param name the name of the setting section as defined in
|
||||
* interface_config.js and SettingsMenu.js
|
||||
* @returns {boolean} {true} to indicate that the given setting section
|
||||
* is enabled, {false} - otherwise
|
||||
*/
|
||||
isSettingEnabled(name) {
|
||||
return interfaceConfig.SETTINGS_SECTIONS.indexOf(name) !== -1;
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if Authentication Section should be shown
|
||||
*
|
||||
@@ -215,10 +227,43 @@ const UIUtil = {
|
||||
* mode, {false} otherwise
|
||||
*/
|
||||
isFullScreen() {
|
||||
return Boolean(document.fullscreenElement
|
||||
return document.fullscreenElement
|
||||
|| document.mozFullScreenElement
|
||||
|| document.webkitFullscreenElement
|
||||
|| document.msFullscreenElement);
|
||||
|| document.msFullscreenElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* Exits full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
exitFullScreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Enter full screen mode.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API
|
||||
*/
|
||||
enterFullScreen() {
|
||||
if (document.documentElement.requestFullscreen) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else if (document.documentElement.msRequestFullscreen) {
|
||||
document.documentElement.msRequestFullscreen();
|
||||
} else if (document.documentElement.mozRequestFullScreen) {
|
||||
document.documentElement.mozRequestFullScreen();
|
||||
} else if (document.documentElement.webkitRequestFullscreen) {
|
||||
document.documentElement
|
||||
.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -438,56 +438,87 @@ const Filmstrip = {
|
||||
* Resizes thumbnails
|
||||
* @param local
|
||||
* @param remote
|
||||
* @param animate
|
||||
* @param forceUpdate
|
||||
* @returns {Promise}
|
||||
*/
|
||||
// eslint-disable-next-line max-params
|
||||
resizeThumbnails(local, remote, forceUpdate = false) {
|
||||
const thumbs = this.getThumbs(!forceUpdate);
|
||||
resizeThumbnails(local, remote, animate = false, forceUpdate = false) {
|
||||
return new Promise(resolve => {
|
||||
const thumbs = this.getThumbs(!forceUpdate);
|
||||
const promises = [];
|
||||
|
||||
if (thumbs.localThumb) {
|
||||
if (thumbs.localThumb) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
promises.push(new Promise(resolve => {
|
||||
thumbs.localThumb.animate({
|
||||
height: local.thumbHeight,
|
||||
'min-height': local.thumbHeight,
|
||||
'min-width': local.thumbWidth,
|
||||
width: local.thumbWidth
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
}
|
||||
if (thumbs.remoteThumbs) {
|
||||
// eslint-disable-next-line no-shadow
|
||||
promises.push(new Promise(resolve => {
|
||||
thumbs.remoteThumbs.animate({
|
||||
height: remote.thumbHeight,
|
||||
'min-height': remote.thumbHeight,
|
||||
'min-width': remote.thumbWidth,
|
||||
width: remote.thumbWidth
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
}
|
||||
// eslint-disable-next-line no-shadow
|
||||
thumbs.localThumb.css({
|
||||
display: 'inline-block',
|
||||
height: `${local.thumbHeight}px`,
|
||||
'min-height': `${local.thumbHeight}px`,
|
||||
'min-width': `${local.thumbWidth}px`,
|
||||
width: `${local.thumbWidth}px`
|
||||
});
|
||||
}
|
||||
promises.push(new Promise(resolve => {
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('#filmstripLocalVideo').animate({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: local.thumbWidth + 4
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
} else {
|
||||
this.filmstrip.animate({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: remote.thumbHeight + 4
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}
|
||||
}));
|
||||
|
||||
if (thumbs.remoteThumbs) {
|
||||
thumbs.remoteThumbs.css({
|
||||
display: 'inline-block',
|
||||
height: `${remote.thumbHeight}px`,
|
||||
'min-height': `${remote.thumbHeight}px`,
|
||||
'min-width': `${remote.thumbWidth}px`,
|
||||
width: `${remote.thumbWidth}px`
|
||||
});
|
||||
}
|
||||
promises.push(new Promise(() => {
|
||||
const { localThumb } = this.getThumbs();
|
||||
const height = localThumb ? localThumb.height() : 0;
|
||||
const fontSize = UIUtil.getIndicatorFontSize(height);
|
||||
|
||||
// Let CSS take care of height in vertical filmstrip mode.
|
||||
if (interfaceConfig.VERTICAL_FILMSTRIP) {
|
||||
$('#filmstripLocalVideo').css({
|
||||
// adds 4 px because of small video 2px border
|
||||
width: `${local.thumbWidth + 4}px`
|
||||
});
|
||||
} else {
|
||||
this.filmstrip.css({
|
||||
// adds 4 px because of small video 2px border
|
||||
height: `${remote.thumbHeight + 4}px`
|
||||
});
|
||||
}
|
||||
this.filmstrip.find('.indicator').animate({
|
||||
fontSize
|
||||
}, this._getAnimateOptions(animate, resolve));
|
||||
}));
|
||||
|
||||
const { localThumb } = this.getThumbs();
|
||||
const height = localThumb ? localThumb.height() : 0;
|
||||
const fontSize = UIUtil.getIndicatorFontSize(height);
|
||||
if (!animate) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
this.filmstrip.find('.indicator').css({
|
||||
'font-size': `${fontSize}px`
|
||||
Promise.all(promises).then(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper method. Returns options for jQuery animation
|
||||
* @param animate {Boolean} - animation flag
|
||||
* @param cb {Function} - complete callback
|
||||
* @returns {Object} - animation options object
|
||||
* @private
|
||||
*/
|
||||
_getAnimateOptions(animate, cb = $.noop) {
|
||||
return {
|
||||
queue: false,
|
||||
duration: animate ? 500 : 0,
|
||||
complete: cb
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns thumbnails of the filmstrip
|
||||
* @param onlyVisible
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user