Compare commits

..

1 Commits

Author SHA1 Message Date
hristoterezov
9fa1b210f5 feat(loadScript): Use the function from js-utils 2018-03-22 13:15:14 -05:00
342 changed files with 9337 additions and 15924 deletions

View File

@@ -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
@@ -57,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 && \

View File

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

View File

@@ -19,14 +19,12 @@ package org.jitsi.meet;
import android.os.Bundle;
import android.util.Log;
import org.jitsi.meet.sdk.InviteSearchController;
import org.jitsi.meet.sdk.JitsiMeetActivity;
import org.jitsi.meet.sdk.JitsiMeetView;
import org.jitsi.meet.sdk.JitsiMeetViewListener;
import com.calendarevents.CalendarEventsPackage;
import java.util.HashMap;
import java.util.Map;
/**
@@ -86,11 +84,6 @@ public class MainActivity extends JitsiMeetActivity {
on("CONFERENCE_WILL_LEAVE", data);
}
@Override
public void launchNativeInvite(InviteSearchController inviteSearchController) {
on("LAUNCH_NATIVE_INVITE", new HashMap<String, Object>());
}
@Override
public void onLoadConfigError(Map<String, Object> data) {
on("LOAD_CONFIG_ERROR", data);

View File

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

View File

@@ -1,184 +0,0 @@
package org.jitsi.meet.sdk;
import android.util.Log;
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 InviteSearchController {
/**
* The InviteSearchControllerDelegate for this controller, used to pass query
* results back to the native code that initiated the query.
*/
private InviteSearchControllerDelegate searchControllerDelegate;
/**
* Local cache of search query results. Used to re-hydrate the list
* of selected items based on their ids passed to submitSelectedItemIds
* in order to pass the full item maps back to the JitsiMeetView during submission.
*/
private Map<String, ReadableMap> items = new HashMap<>();
/**
* Randomly generated UUID, used for identification in the InviteSearchModule
*/
private String uuid = UUID.randomUUID().toString();
private WeakReference<InviteSearchModule> parentModuleRef;
public InviteSearchController(InviteSearchModule module) {
parentModuleRef = new WeakReference<>(module);
}
/**
* Start a search for entities to invite with the given query.
* Results will be returned through the associated InviteSearchControllerDelegate's
* onReceiveResults method.
*
* @param query
*/
public void performQuery(String query) {
JitsiMeetView.onInviteQuery(query, uuid);
}
/**
* Send invites to selected users based on their item ids
*
* @param ids
*/
public void submitSelectedItemIds(List<String> ids) {
WritableArray selectedItems = new WritableNativeArray();
for(int i=0; i<ids.size(); i++) {
if(items.containsKey(ids.get(i))) {
WritableNativeMap map = new WritableNativeMap();
map.merge(items.get(ids.get(i)));
selectedItems.pushMap(map);
} else {
// if the id doesn't exist in the map, we can't do anything, so just skip it
}
}
JitsiMeetView.submitSelectedItems(selectedItems, uuid);
}
/**
* 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) {
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("InviteSearchController", "Received result without id and that was not a phone number, so not adding it to suggestions: " + map);
}
jvmResults.add(map.toHashMap());
}
searchControllerDelegate.onReceiveResults(this, jvmResults, query);
}
/**
*
* @return the InviteSearchControllerDelegate for this controller, used to pass query
* results back to the native code that initiated the query.
*/
public InviteSearchControllerDelegate getSearchControllerDelegate() {
return searchControllerDelegate;
}
/**
* Sets the InviteSearchControllerDelegate for this controller, used to pass query results
* back to the native code that initiated the query.
*
* @param searchControllerDelegate
*/
public void setSearchControllerDelegate(InviteSearchControllerDelegate searchControllerDelegate) {
this.searchControllerDelegate = searchControllerDelegate;
}
/**
* Cancel the invitation flow and free memory allocated to the InviteSearchController. After
* calling this method, this object is invalid - a new InviteSearchController will be passed
* to the caller through launchNativeInvite.
*/
public void cancelSearch() {
InviteSearchModule parentModule = parentModuleRef.get();
if(parentModule != null) {
parentModule.removeSearchController(uuid);
}
}
/**
* @return the unique identifier for this InviteSearchController
*/
public String getUuid() {
return uuid;
}
public interface InviteSearchControllerDelegate {
/**
* Called when results are received for a query called through InviteSearchController.query()
*
* @param searchController
* @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(InviteSearchController searchController, List<Map<String, Object>> results, String query);
/**
* Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes successfully
* and invitations are sent to all given IDs.
*
* @param searchController the active {@link InviteSearchController} for this invite flow. This object will be
* cleaned up after the call to inviteSucceeded completes.
*/
void inviteSucceeded(InviteSearchController searchController);
/**
* Called when the call to {@link InviteSearchController#submitSelectedItemIds(List)} completes, but the
* invitation fails for one or more of the selected items.
*
* @param searchController the active {@link InviteSearchController} for this invite flow. This object
* should be cleaned up by calling {@link InviteSearchController#cancelSearch()} if
* the user exits the invite flow. Otherwise, it can stay active if the user
* will attempt to invite
* @param failedInviteItems 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 inviteFailed(InviteSearchController searchController, List<Map<String, Object>> failedInviteItems);
}
}

View File

@@ -1,126 +0,0 @@
package org.jitsi.meet.sdk;
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 com.facebook.react.bridge.ReadableMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Native module for Invite Search
*/
class InviteSearchModule extends ReactContextBaseJavaModule {
/**
* Map of InviteSearchController objects passed to connected JitsiMeetView.
* A call to launchNativeInvite will create a new InviteSearchController and pass
* it back to the caller. On a successful invitation, the controller will be removed automatically.
* On a failed invitation, the caller has the option of calling InviteSearchController#cancelSearch()
* to remove the controller from this map. The controller should also be removed if the user cancels
* the invitation flow.
*/
private Map<String, InviteSearchController> searchControllers = new HashMap<>();
public InviteSearchModule(ReactApplicationContext reactContext) {
super(reactContext);
}
/**
* Launch the native user invite flow
*
* @param externalAPIScope a string that represents a connection to a specific JitsiMeetView
*/
@ReactMethod
public void launchNativeInvite(String externalAPIScope) {
JitsiMeetView viewToLaunchInvite = JitsiMeetView.findViewByExternalAPIScope(externalAPIScope);
if(viewToLaunchInvite == null) {
return;
}
if(viewToLaunchInvite.getListener() == null) {
return;
}
InviteSearchController controller = createSearchController();
viewToLaunchInvite.getListener().launchNativeInvite(controller);
}
/**
* 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 inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
*/
@ReactMethod
public void receivedResults(ReadableArray results, String query, String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Received results, but unable to find active controller to send results back");
return;
}
controller.receivedResultsForQuery(results, query);
}
/**
* Callback for invitation failures
*
* @param items the items for which the invitation failed
* @param inviteSearchControllerScope a string that represents a connection to a specific InviteSearchController
*/
@ReactMethod
public void inviteFailedForItems(ReadableArray items, String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Invite failed, but unable to find active controller to notify");
return;
}
ArrayList<Map<String, Object>> jvmItems = new ArrayList<>();
for(int i=0; i<items.size(); i++) {
ReadableMap item = items.getMap(i);
jvmItems.add(item.toHashMap());
}
controller.getSearchControllerDelegate().inviteFailed(controller, jvmItems);
}
@ReactMethod
public void inviteSucceeded(String inviteSearchControllerScope) {
InviteSearchController controller = searchControllers.get(inviteSearchControllerScope);
if(controller == null) {
Log.w("InviteSearchModule", "Invite succeeded, but unable to find active controller to notify");
return;
}
controller.getSearchControllerDelegate().inviteSucceeded(controller);
searchControllers.remove(inviteSearchControllerScope);
}
void removeSearchController(String inviteSearchControllerUuid) {
searchControllers.remove(inviteSearchControllerUuid);
}
@Override
public String getName() {
return "InviteSearch";
}
private InviteSearchController createSearchController() {
InviteSearchController searchController = new InviteSearchController(this);
searchControllers.put(searchController.getUuid(), searchController);
return searchController;
}
}

View File

@@ -21,6 +21,7 @@ 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;
@@ -32,9 +33,7 @@ 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.WritableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.common.LifecycleState;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
import com.facebook.react.modules.core.DeviceEventManagerModule;
@@ -44,7 +43,6 @@ import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
@@ -78,11 +76,9 @@ public class JitsiMeetView extends FrameLayout {
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new ExternalAPIModule(reactContext),
new InviteSearchModule(reactContext),
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new WiFiStatsModule(reactContext),
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
new WiFiStatsModule(reactContext)
);
}
@@ -272,43 +268,15 @@ public class JitsiMeetView extends FrameLayout {
sendEvent("onUserLeaveHint", null);
}
/**
* Starts a query for users to invite to the conference. Results will be
* returned through the {@link InviteSearchController.InviteSearchControllerDelegate#onReceiveResults(InviteSearchController, List, String)}
* method.
*
* @param query {@code String} to use for the query
*/
public static void onInviteQuery(String query, String inviteSearchControllerScope) {
WritableNativeMap params = new WritableNativeMap();
params.putString("query", query);
params.putString("inviteScope", inviteSearchControllerScope);
sendEvent("performQueryAction", params);
}
/**
* Sends JavaScript event to submit invitations to the given item ids
*
* @param selectedItems 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.
*/
public static void submitSelectedItems(WritableArray selectedItems, String inviteSearchControllerScope) {
WritableNativeMap params = new WritableNativeMap();
params.putArray("selectedItems", selectedItems);
params.putString("inviteScope", inviteSearchControllerScope);
sendEvent("performSubmitInviteAction", params);
}
/**
* 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.
* @param params {@code WritableMap} optional ancillary data for the event.
*/
private static void sendEvent(
String eventName,
@Nullable Object data) {
@Nullable WritableMap params) {
if (reactInstanceManager != null) {
ReactContext reactContext
= reactInstanceManager.getCurrentReactContext();
@@ -316,16 +284,11 @@ public class JitsiMeetView extends FrameLayout {
reactContext
.getJSModule(
DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, data);
.emit(eventName, params);
}
}
}
/**
* Whether user invitation is enabled.
*/
private boolean addPeopleEnabled;
/**
* The default base {@code URL} used to join a conference when a partial URL
* (e.g. a room name only) is specified to {@link #loadURLString(String)} or
@@ -333,11 +296,6 @@ public class JitsiMeetView extends FrameLayout {
*/
private URL defaultURL;
/**
* Whether the ability to add users by phone number is enabled.
*/
private boolean dialOutEnabled;
/**
* The unique identifier of this {@code JitsiMeetView} within the process
* for the purposes of {@link ExternalAPI}. The name scope was inspired by
@@ -496,9 +454,6 @@ public class JitsiMeetView extends FrameLayout {
// welcomePageEnabled
props.putBoolean("welcomePageEnabled", welcomePageEnabled);
props.putBoolean("addPeopleEnabled", addPeopleEnabled);
props.putBoolean("dialOutEnabled", dialOutEnabled);
// XXX The method loadURLObject: is supposed to be imperative i.e.
// a second invocation with one and the same URL is expected to join
// the respective conference again if the first invocation was followed
@@ -580,18 +535,6 @@ public class JitsiMeetView extends FrameLayout {
}
}
/**
* 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 JitsiMeetViewListener#launchNativeInvite(Map)} will be called.
*
* @param addPeopleEnabled {@code true} to enable the add people button; otherwise, {@code false}
*/
public void setAddPeopleEnabled(boolean addPeopleEnabled) {
this.addPeopleEnabled = addPeopleEnabled;
}
/**
* Sets the default base {@code URL} used to join a conference when a
* partial URL (e.g. a room name only) is specified to
@@ -605,18 +548,6 @@ public class JitsiMeetView extends FrameLayout {
this.defaultURL = defaultURL;
}
/**
* 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 = dialOutEnabled;
}
/**
* Sets a specific {@link JitsiMeetViewListener} on this
* {@code JitsiMeetView}.

View File

@@ -46,8 +46,4 @@ public abstract class JitsiMeetViewAdapter implements JitsiMeetViewListener {
@Override
public void onLoadConfigError(Map<String, Object> data) {
}
@Override
public void launchNativeInvite(InviteSearchController inviteSearchController) {
}
}

View File

@@ -59,16 +59,6 @@ public interface JitsiMeetViewListener {
*/
void onConferenceWillLeave(Map<String, Object> data);
/**
* Called when the add user button is tapped.
*
* @param inviteSearchController {@code InviteSearchController} scoped
* for this user invite flow. The {@code InviteSearchController} is used
* to start user queries and accepts an {@code InviteSearchControllerDelegate}
* for receiving user query responses.
*/
void launchNativeInvite(InviteSearchController inviteSearchController);
/**
* Called when loading the main configuration file from the Jitsi Meet
* deployment fails.

View File

@@ -44,26 +44,14 @@ public class PictureInPictureModule extends ReactContextBaseJavaModule {
PictureInPictureParams.Builder builder
= new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(1, 1));
Throwable error;
boolean r
= currentActivity.enterPictureInPictureMode(builder.build());
// 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.
try {
error
= currentActivity.enterPictureInPictureMode(builder.build())
? null
: new Exception("Failed to enter Picture-in-Picture");
} catch (RuntimeException re) {
error = re;
}
if (error == null) {
if (r) {
promise.resolve(null);
} else {
promise.reject(error);
promise.reject(
new Exception("Failed to enter Picture-in-Picture"));
}
return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -180,24 +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";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +0,0 @@
@import 'desktop';
@import 'mobile';
@import 'no-mobile-app';

View File

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

View File

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

View File

@@ -13,15 +13,7 @@
}
.add-telephone-icon {
display: flex;
height: 28px;
transform: scaleX(-1);
width: 28px;
i {
line-height: 28px;
margin: auto;
}
}
}
}

View File

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

View File

@@ -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,38 +159,14 @@
min-width: $videoStateIndicatorSize;
text-align: center;
}
.recording-icon,
.recording-icon i {
color: rgb(255, 86, 48);
line-height: $videoStateIndicatorSize;
font-size: $videoStateIndicatorSize;
opacity: 0.9;
position: relative;
}
.recording-icon-background {
background: white;
height: 25%;
left: 50%;
opacity: 0.9;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 80%;
}
#recordingLabelText {
display: inline-block;
}
}
.centeredVideoLabel.moveToCorner {
z-index: $zindex3;
z-index: $tooltipsZ;
}
#videoResolutionLabel {
z-index: $zindex3 + 1;
z-index: #{$tooltipsZ + 1};
}
.centeredVideoLabel {
@@ -216,6 +180,8 @@
&.moveToCorner {
bottom: auto;
transform: none;
-webkit-transition: all 2s 2s linear;
transition: all 2s 2s linear;
}
}

View File

@@ -1 +1,3 @@
@import 'no-mobile-app';
@import 'unsupported-desktop-browser';
@import 'unsupported-mobile-browser';

View File

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

View File

@@ -97,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')

View File

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

Binary file not shown.

View File

@@ -23,9 +23,7 @@
<glyph unicode="&#xe616;" 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="&#xe61d;" 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="&#xe80b;" 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="&#xe89e;" 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="&#xe8b3;" 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="&#xe900;" 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="&#xe901;" 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="&#xe902;" 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="&#xe903;" 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,9 +62,4 @@
<glyph unicode="&#xe924;" 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="&#xe925;" 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="&#xe926;" 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="&#xe927;" 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="&#xe928;" 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="&#xe929;" 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="&#xe92a;" 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="&#xe92b;" 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" />
</font></defs></svg>

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,8 +51,6 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>

View File

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

View File

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

View File

@@ -27,14 +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 */; };
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */ = {isa = PBXBuildFile; fileRef = 412BF89C206AA66F0053B9E5 /* InviteSearch.m */; };
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */ = {isa = PBXBuildFile; fileRef = 412BF89E206AA82F0053B9E5 /* InviteSearch.h */; settings = {ATTRIBUTES = (Public, ); }; };
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 */
@@ -62,16 +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>"; };
412BF89C206AA66F0053B9E5 /* InviteSearch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InviteSearch.m; sourceTree = "<group>"; };
412BF89E206AA82F0053B9E5 /* InviteSearch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InviteSearch.h; 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>"; };
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 */
@@ -93,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 */,
@@ -129,8 +123,6 @@
0BCA495C1EC4B6C600B793EE /* AudioMode.m */,
0BB9AD7C1F60356D001C08DB /* AppInfo.m */,
0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
412BF89E206AA82F0053B9E5 /* InviteSearch.h */,
412BF89C206AA66F0053B9E5 /* InviteSearch.m */,
0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
0BD906E91EC0C00300C8C18E /* Info.plist */,
0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
@@ -173,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>";
@@ -186,7 +180,6 @@
buildActionMask = 2147483647;
files = (
C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */,
412BF89F206ABAE40053B9E5 /* InviteSearch.h in Headers */,
0B412F181EDEC65D00B1A0A6 /* JitsiMeetView.h in Headers */,
0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */,
0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */,
@@ -259,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;
@@ -346,15 +337,16 @@
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 */,
0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */,
C6A3426D204F1C3300E062DD /* JitsiMeetViewController.swift in Sources */,
C6A3425F204EF76800E062DD /* PiPWindow.swift in Sources */,
0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
412BF89D206AA66F0053B9E5 /* InviteSearch.m in Sources */,
0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,
C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */,
0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */,
@@ -413,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;
@@ -466,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";

View File

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

View File

@@ -1,49 +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.
*/
@class InviteSearchController;
@protocol InviteSearchControllerDelegate
/**
* Called when an InviteSearchController has results for a query that was previously provided.
*/
- (void)inviteSearchController:(InviteSearchController * _Nonnull)controller
didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
/**
* Called when all invitations were sent successfully.
*/
- (void)inviteDidSucceedForSearchController:(InviteSearchController * _Nonnull)searchController;
/**
* Called when one or more invitations fails to send successfully.
*/
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> * _Nonnull)items
fromSearchController:(InviteSearchController * _Nonnull)searchController;
@end
@interface InviteSearchController: NSObject
@property (nonatomic, nullable, weak) id<InviteSearchControllerDelegate> delegate;
- (void)performQuery:(NSString * _Nonnull)query;
- (void)cancelSearch;
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids;
@end

View File

@@ -1,215 +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>
#import <React/RCTUtils.h>
#import "JitsiMeetView+Private.h"
#import "InviteSearch.h"
// The events emitted/supported by InviteSearch:
static NSString * const InviteSearchPerformQueryAction = @"performQueryAction";
static NSString * const InviteSearchPerformSubmitInviteAction = @"performSubmitInviteAction";
@interface InviteSearch : RCTEventEmitter
@end
@interface InviteSearchController ()
@property (nonatomic, readonly) NSString* _Nonnull identifier;
@property (nonatomic, strong) NSMutableDictionary* _Nonnull items;
@property (nonatomic, nullable, weak) InviteSearch* module;
- (instancetype)initWithSearchModule:(InviteSearch *)module;
- (void)didReceiveResults:(NSArray<NSDictionary*> * _Nonnull)results
forQuery:(NSString * _Nonnull)query;
- (void)inviteDidSucceed;
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items;
@end
@implementation InviteSearch
static NSMutableDictionary* searchControllers;
RCT_EXTERN void RCTRegisterModule(Class);
+ (void)load {
RCTRegisterModule(self);
searchControllers = [[NSMutableDictionary alloc] init];
}
+ (NSString *)moduleName {
return @"InviteSearch";
}
- (NSArray<NSString *> *)supportedEvents {
return @[
InviteSearchPerformQueryAction,
InviteSearchPerformSubmitInviteAction
];
}
/**
* Calls the corresponding JitsiMeetView's delegate to request that the native
* invite search be presented.
*
* @param scope
*/
RCT_EXPORT_METHOD(launchNativeInvite:(NSString *)scope) {
// The JavaScript App needs to provide uniquely identifying information to
// the native module so that the latter may match the former to the native
// JitsiMeetView which hosts it.
JitsiMeetView *view = [JitsiMeetView viewForExternalAPIScope:scope];
if (!view) {
return;
}
id<JitsiMeetViewDelegate> delegate = view.delegate;
if (!delegate) {
return;
}
if ([delegate respondsToSelector:@selector(launchNativeInviteForSearchController:)]) {
InviteSearchController* searchController = [searchControllers objectForKey:scope];
if (!searchController) {
searchController = [self makeInviteSearchController];
}
[delegate launchNativeInviteForSearchController:searchController];
}
}
RCT_EXPORT_METHOD(inviteSucceeded:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidSucceed];
[searchControllers removeObjectForKey:inviteScope];
}
RCT_EXPORT_METHOD(inviteFailedForItems:(NSArray<NSDictionary *> *)items inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController inviteDidFailForItems:items];
}
RCT_EXPORT_METHOD(receivedResults:(NSArray *)results forQuery:(NSString *)query inviteScope:(NSString *)inviteScope) {
InviteSearchController* searchController = [searchControllers objectForKey:inviteScope];
[searchController didReceiveResults:results forQuery:query];
}
- (InviteSearchController *)makeInviteSearchController {
InviteSearchController* searchController = [[InviteSearchController alloc] initWithSearchModule:self];
[searchControllers setObject:searchController forKey:searchController.identifier];
return searchController;
}
- (void)performQuery:(NSString * _Nonnull)query inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformQueryAction body:@{ @"query": query, @"inviteScope": inviteScope }];
}
- (void)cancelSearchForInviteScope:(NSString * _Nonnull)inviteScope {
[searchControllers removeObjectForKey:inviteScope];
}
- (void)submitSelectedItems:(NSArray<NSDictionary *> * _Nonnull)items inviteScope:(NSString * _Nonnull)inviteScope {
[self sendEventWithName:InviteSearchPerformSubmitInviteAction body:@{ @"selectedItems": items, @"inviteScope": inviteScope }];
}
@end
@implementation InviteSearchController
- (instancetype)initWithSearchModule:(InviteSearch *)module {
self = [super init];
if (self) {
_identifier = [[NSUUID UUID] UUIDString];
self.items = [[NSMutableDictionary alloc] init];
self.module = module;
}
return self;
}
- (void)performQuery:(NSString *)query {
[self.module performQuery:query inviteScope:self.identifier];
}
- (void)cancelSearch {
[self.module cancelSearchForInviteScope:self.identifier];
}
- (void)submitSelectedItemIds:(NSArray<NSString *> * _Nonnull)ids {
NSMutableArray* items = [[NSMutableArray alloc] init];
for (NSString* itemId in ids) {
id item = [self.items objectForKey:itemId];
if (item) {
[items addObject:item];
}
}
[self.module submitSelectedItems:items inviteScope:self.identifier];
}
- (void)didReceiveResults:(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 inviteSearchController:self didReceiveResults:results forQuery:query];
}
- (void)inviteDidSucceed {
[self.delegate inviteDidSucceedForSearchController:self];
}
- (void)inviteDidFailForItems:(NSArray<NSDictionary *> *)items {
if (!items) {
items = @[];
}
[self.delegate inviteDidFailForItems:items fromSearchController:self];
}
@end

View File

@@ -16,4 +16,3 @@
#import <JitsiMeet/JitsiMeetView.h>
#import <JitsiMeet/JitsiMeetViewDelegate.h>
#import <JitsiMeet/InviteSearch.h>

View File

@@ -21,13 +21,9 @@
@interface JitsiMeetView : UIView
@property (nonatomic) BOOL addPeopleEnabled;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic, nullable, weak) id<JitsiMeetViewDelegate> delegate;
@property (nonatomic) BOOL dialOutEnabled;
@property (copy, nonatomic, nullable) NSURL *defaultURL;
@property (nonatomic) BOOL pictureInPictureEnabled;

View File

@@ -268,8 +268,6 @@ static NSMapTable<NSString *, JitsiMeetView *> *views;
props[@"defaultURL"] = [self.defaultURL absoluteString];
}
props[@"addPeopleEnabled"] = @(self.addPeopleEnabled);
props[@"dialOutEnabled"] = @(self.dialOutEnabled);
props[@"externalAPIScope"] = externalAPIScope;
props[@"pictureInPictureEnabled"] = @(self.pictureInPictureEnabled);
props[@"welcomePageEnabled"] = @(self.welcomePageEnabled);

View File

@@ -14,8 +14,6 @@
* limitations under the License.
*/
@class InviteSearchController;
@protocol JitsiMeetViewDelegate <NSObject>
@optional
@@ -57,15 +55,6 @@
*/
- (void)conferenceWillLeave:(NSDictionary *)data;
/**
* 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)launchNativeInviteForSearchController:(InviteSearchController *)searchController;
/**
* Called when entering Picture-in-Picture is requested by the user. The app
* should now activate its Picture-in-Picture implementation (and resize the

View File

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

View 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)
}
}
}

View File

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

View File

@@ -1,20 +0,0 @@
{
"en": "",
"bg": "",
"de": "",
"es": "",
"fr": "",
"hy": "",
"it": "",
"oc": "",
"pl": "",
"ptBR": "",
"ru": "",
"sk": "",
"sl": "",
"sv": "",
"tr": "",
"zhCN": "",
"nb": "",
"eo": ""
}

View File

@@ -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": ""
}
}

View File

@@ -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.",
@@ -73,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.",
@@ -103,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": {
@@ -284,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",
@@ -445,13 +445,10 @@
"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.",
@@ -459,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"
},
@@ -467,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.",
@@ -477,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": {
@@ -501,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": {
@@ -547,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"
}
}

View File

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

View File

@@ -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'
@@ -550,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.

View File

@@ -30,13 +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 {
checkAutoEnableDesktopSharing,
clearButtonPopup,
dockToolbox,
setButtonPopupTimeout,
setToolbarButton,
showDialPadButton,
showEtherpadButton,
showSharedVideoButton,
showToolbox
} from '../../react/features/toolbox';
@@ -94,6 +100,9 @@ const UIListeners = new Map([
], [
UIEvents.SHARED_VIDEO_CLICKED,
() => sharedVideoManager && sharedVideoManager.toggleSharedVideo()
], [
UIEvents.TOGGLE_FULLSCREEN,
() => UI.toggleFullScreen()
], [
UIEvents.TOGGLE_CHAT,
() => UI.toggleChat()
@@ -111,6 +120,9 @@ const UIListeners = new Map([
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);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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, '&gt;')
.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

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

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

View File

@@ -227,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);
}
},
/**

View File

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

View File

@@ -95,7 +95,7 @@ RemoteVideo.prototype.addRemoteVideoContainer = function() {
this.updateRemoteVideoMenu();
this.VideoLayout.resizeThumbnails(true);
this.VideoLayout.resizeThumbnails(false, true);
this.addAudioLevelIndicator();

View File

@@ -66,6 +66,9 @@ const VideoLayout = {
init(emitter) {
eventEmitter = emitter;
// Unregister listeners in case of reinitialization
this.unregisterListeners();
localVideoThumbnail = new LocalVideo(VideoLayout, emitter);
// sets default video type of local video
@@ -74,7 +77,7 @@ const VideoLayout = {
// if we do not resize the thumbs here, if there is no video device
// the local video thumb maybe one pixel
this.resizeThumbnails(true);
this.resizeThumbnails(false, true);
this.handleVideoThumbClicked = this.handleVideoThumbClicked.bind(this);
@@ -101,6 +104,20 @@ const VideoLayout = {
registerListeners() {
eventEmitter.addListener(UIEvents.LOCAL_FLIPX_CHANGED,
onLocalFlipXChanged);
eventEmitter.addListener(UIEvents.CONTACT_CLICKED,
this.handleVideoThumbClicked);
},
/**
* Unregistering listeners for UI events in Video layout component.
*
* @returns {void}
*/
unregisterListeners() {
if (this._onContactClicked) {
eventEmitter.removeListener(UIEvents.CONTACT_CLICKED,
this.handleVideoThumbClicked);
}
},
initLargeVideo() {
@@ -468,7 +485,7 @@ const VideoLayout = {
remoteVideo.setVideoType(VIDEO_CONTAINER_TYPE);
}
VideoLayout.resizeThumbnails(true);
VideoLayout.resizeThumbnails(false, true);
// Initialize the view
remoteVideo.updateView();
@@ -480,7 +497,7 @@ const VideoLayout = {
logger.info(`${resourceJid} video is now active`, videoElement);
VideoLayout.resizeThumbnails(
false, () => {
false, false, () => {
if (videoElement) {
$(videoElement).show();
}
@@ -575,16 +592,19 @@ const VideoLayout = {
* Resizes thumbnails.
*/
resizeThumbnails(
animate = false,
forceUpdate = false,
onComplete = null) {
const { localVideo, remoteVideo }
= Filmstrip.calculateThumbnailSize();
Filmstrip.resizeThumbnails(localVideo, remoteVideo, forceUpdate);
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
Filmstrip.resizeThumbnails(localVideo, remoteVideo,
animate, forceUpdate)
.then(() => {
if (onComplete && typeof onComplete === 'function') {
onComplete();
}
});
return { localVideo,
remoteVideo };
@@ -876,7 +896,7 @@ const VideoLayout = {
}
// Resize the thumbnails first.
this.resizeThumbnails(forceUpdate);
this.resizeThumbnails(false, forceUpdate);
// Resize the video area element.
$('#videospace').animate({
@@ -1092,7 +1112,7 @@ const VideoLayout = {
* Currently used by tests (torture).
*/
getLargeVideoID() {
return largeVideo && largeVideo.id;
return largeVideo.id;
},
/**

View File

@@ -15,9 +15,9 @@ const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
* Map of shortcuts. When a shortcut is registered it enters the mapping.
* @type {Map}
* @type {{}}
*/
const _shortcuts = new Map();
const _shortcuts = {};
/**
* Map of registered keyboard keys and translation keys describing the
@@ -49,8 +49,8 @@ const KeyboardShortcut = {
if (!($(':focus').is('input[type=text]')
|| $(':focus').is('input[type=password]')
|| $(':focus').is('textarea'))) {
if (_shortcuts.has(key)) {
_shortcuts.get(key).function(e);
if (_shortcuts.hasOwnProperty(key)) {
_shortcuts[key].function(e);
} else if (!isNaN(num) && num >= 0 && num <= 9) {
APP.UI.clickOnVideo(num);
}
@@ -90,17 +90,6 @@ const KeyboardShortcut = {
enabled = value;
},
/**
* Opens the {@KeyboardShortcutsDialog} dialog.
*
* @returns {void}
*/
openDialog() {
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
},
/**
* Registers a new shortcut.
*
@@ -117,11 +106,11 @@ const KeyboardShortcut = {
shortcutAttr,
exec,
helpDescription) {
_shortcuts.set(shortcutChar, {
_shortcuts[shortcutChar] = {
character: shortcutChar,
function: exec,
shortcutAttr
});
shortcutAttr,
function: exec
};
if (helpDescription) {
this._addShortcutToHelp(shortcutChar, helpDescription);
@@ -135,7 +124,7 @@ const KeyboardShortcut = {
* no longer be usable
*/
unregisterShortcut(shortcutChar) {
_shortcuts.delete(shortcutChar);
_shortcuts.remove(shortcutChar);
_shortcutsHelp.delete(shortcutChar);
},
@@ -144,12 +133,7 @@ const KeyboardShortcut = {
* @returns {string} e.key or something close if not supported
*/
_getKeyboardKey(e) {
// If e.key is a string, then it is assumed it already plainly states
// the key pressed. This may not be true in all cases, such as with Edge
// and "?", when the browser cannot properly map a key press event to a
// keyboard key. To be safe, when a key is "Unidentified" it must be
// further analyzed by jitsi to a key using e.which.
if (typeof e.key === 'string' && e.key !== 'Unidentified') {
if (typeof e.key === 'string') {
return e.key;
}
if (e.type === 'keypress'
@@ -193,7 +177,9 @@ const KeyboardShortcut = {
_initGlobalShortcuts() {
this.registerShortcut('?', null, () => {
sendAnalytics(createShortcutEvent('help'));
this.openDialog();
APP.store.dispatch(toggleDialog(KeyboardShortcutsDialog, {
shortcutDescriptions: _shortcutsHelp
}));
}, 'keyboardShortcuts.toggleShortcuts');
// register SPACE shortcut in two steps to insure visibility of help

View File

@@ -22,6 +22,7 @@ const LEGACY_INCOMING_METHODS = [
'email',
'toggle-audio',
'toggle-chat',
'toggle-contact-list',
'toggle-film-strip',
'toggle-share-screen',
'toggle-video',

View File

@@ -1,50 +0,0 @@
/**
* Implements in memory logs storage, used for testing/debugging.
*/
export default class JitsiMeetInMemoryLogStorage {
/**
* Creates new <tt>JitsiMeetInMemoryLogStorage</tt>
*/
constructor() {
/**
* Array of the log entries to keep.
* @type {array}
*/
this.logs = [];
}
/**
* @returns {boolean} <tt>true</tt> when this storage is ready or
* <tt>false</tt> otherwise.
*/
isReady() {
return true;
}
/**
* Called by the <tt>LogCollector</tt> to store a series of log lines into
* batch.
* @param {string|object[]} logEntries an array containing strings
* representing log lines or aggregated lines objects.
*/
storeLogs(logEntries) {
for (let i = 0, len = logEntries.length; i < len; i++) {
const logEntry = logEntries[i];
if (typeof logEntry === 'object') {
this.logs.push(logEntry.text);
} else {
// Regular message
this.logs.push(logEntry);
}
}
}
/**
* @returns {array} the collected log entries.
*/
getLogs() {
return this.logs;
}
}

482
package-lock.json generated
View File

@@ -91,7 +91,7 @@
"@atlaskit/layer": "2.7.2",
"@atlaskit/spinner": "4.0.0",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "6.2.2",
"@atlaskit/tooltip": "6.0.0",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"keycode": "2.1.9",
@@ -109,19 +109,6 @@
"styled-components": "1.4.6"
}
},
"@atlaskit/tooltip": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.2.2.tgz",
"integrity": "sha1-zHE3McH4BNnYAXYRGzk6c3DXUdo=",
"requires": {
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"styled-components": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-1.4.6.tgz",
@@ -422,91 +409,49 @@
}
},
"@atlaskit/inline-dialog": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.3.0.tgz",
"integrity": "sha512-4bEeC5rZwtb4YO9BxW1UCJYCp/dyCVXqcygRW1BDnYVbveAI8wdym6qEi4BRvIwXCT4qgNhsVsqcxSrn0X6CKQ==",
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-5.0.2.tgz",
"integrity": "sha1-xwPi9seo0M+nrcPXgJK0bE7ShkQ=",
"requires": {
"@atlaskit/layer": "2.9.1",
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/layer": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-2.9.1.tgz",
"integrity": "sha512-nyIVGeS2OhuGR5gIMTYUfRmCG8z/9KMgUzTpbpsB70sH6+d4KSFhfkz+KhKNIa8gvKI6zBc+3UBYSlUW1t1qmQ==",
"requires": {
"styled-components": "1.4.6"
}
}
}
},
"@atlaskit/inline-message": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-4.0.0.tgz",
"integrity": "sha512-JhZf3Oux7kp+7RMFIGuaVblGiwWPd8EfCQ2kJXSvxYFTRo798gPFBKz4LU/NKQU3Fl2Rht6o5qmvjbdbzoxcZg==",
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-message/-/inline-message-3.0.1.tgz",
"integrity": "sha1-VIF4Yg+CHx/nFBI+E4dQh9Upn78=",
"requires": {
"@atlaskit/button": "7.0.0",
"@atlaskit/icon": "11.0.0",
"@atlaskit/inline-dialog": "6.0.0",
"@atlaskit/theme": "3.0.0",
"@atlaskit/button": "3.6.0",
"@atlaskit/icon": "7.1.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/theme": "2.4.0",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/analytics-next": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/analytics-next/-/analytics-next-2.0.0.tgz",
"integrity": "sha512-HHRifSrETd/hR7vj3Vqscz4374G1hhEnSehFkPvP+2BIL64TwsqlDLjwL/y1j+/5tFpSYRjfIAVBM/RcCuuzzg==",
"requires": {
"prop-types": "15.6.0"
}
},
"@atlaskit/button": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-7.0.0.tgz",
"integrity": "sha512-FXozSxBcmigSt3+ff+ca8kfEox+mP6dJsXG4i/fkYfsmZr8WXYnyPWndWvOJYs7jvqqP9FMIGSJJQeOJHI60Ow==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@atlaskit/button/-/button-3.6.0.tgz",
"integrity": "sha1-tx5uySqBkHWAfqqbItyV59NSgqE=",
"requires": {
"@atlaskit/analytics-next": "2.0.0",
"@atlaskit/theme": "3.0.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"classnames": "2.2.5",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/icon": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-11.0.0.tgz",
"integrity": "sha512-KxGxDwbuWeGZ5ivP1JFlPOhHipQQzgm0lAUGzziaYi6yOaNmfwmNjpj2xBpOhlkkymvhnFkzV5JSwVUH+0rW1w==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@atlaskit/icon/-/icon-7.1.0.tgz",
"integrity": "sha1-czQVCEhzUmPeShO8mPTUTU8r6N8=",
"requires": {
"@atlaskit/theme": "3.0.0",
"babel-runtime": "6.26.0",
"styled-components": "1.4.6",
"uuid": "3.1.0"
}
},
"@atlaskit/inline-dialog": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/inline-dialog/-/inline-dialog-6.0.0.tgz",
"integrity": "sha512-ITGozbuJg6UFzZHQjrWOsx17jKMFR4TE6YdTYA2UTKmKJalHvH414APuNyZdIz8XCFFnQo8NuGdkAjiBYCKG0w==",
"requires": {
"@atlaskit/layer": "3.1.0",
"@atlaskit/theme": "3.0.0",
"styled-components": "1.4.6"
}
},
"@atlaskit/layer": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@atlaskit/layer/-/layer-3.1.0.tgz",
"integrity": "sha512-QdOiu/c0u+YZKuC5z1hYZj/TW0axO5pL2kWBU2MLyMnThfHA+8UCMvIKtpI5/DoMevR5bEuLTX6ivos15l2ulA==",
"requires": {
"react-scrolllock": "2.0.2",
"styled-components": "1.4.6"
}
},
"@atlaskit/theme": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
@@ -893,50 +838,16 @@
}
},
"@atlaskit/tooltip": {
"version": "9.1.1",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-9.1.1.tgz",
"integrity": "sha512-LGMg+OP9qRIMzr8DJtMUBAkHbCN9Vwvh30QWGu/pXktBaxSM5JbxGnUughnoE1JFYyeycJGmyZ0ibtQDnmiB7w==",
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/tooltip/-/tooltip-6.0.0.tgz",
"integrity": "sha1-rn3xMCmvO1iZqcuKNPPIT8K7mpc=",
"requires": {
"@atlaskit/layer-manager": "3.0.1",
"@atlaskit/theme": "3.0.0",
"react-deprecate": "0.1.0",
"react-transition-group": "2.3.0",
"@atlaskit/layer": "2.7.2",
"@atlaskit/theme": "2.4.0",
"@atlaskit/util-shared-styles": "2.10.6",
"babel-runtime": "6.26.0",
"prop-types": "15.6.0",
"styled-components": "1.4.6"
},
"dependencies": {
"@atlaskit/layer-manager": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@atlaskit/layer-manager/-/layer-manager-3.0.1.tgz",
"integrity": "sha512-UPACU1LI+mseZ+GM5HDAHVAONemCQlVMVDlGAW77jYyG4rrCwQUzjZNS8OULvCfHZyULDrvT/w9FqG6eKPTo+A==",
"requires": {
"focusin": "2.0.0",
"prop-types": "15.6.0",
"react-transition-group": "2.3.0",
"styled-components": "1.4.6",
"tabbable": "1.1.2"
}
},
"@atlaskit/theme": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@atlaskit/theme/-/theme-3.0.0.tgz",
"integrity": "sha512-j9n0x+WjpwLelPR6Mf64meuPSWsHBK7hEU6q3dLK5YerqXaocu9Z5Dcnv6E8F+zfzGvPn/9ISgO2L3ts5Gv/3g==",
"requires": {
"prop-types": "15.6.0",
"styled-components": "1.4.6"
}
},
"react-transition-group": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.3.0.tgz",
"integrity": "sha512-OU3/swEL8y233u5ajTn3FIcQQ/b3XWjLXB6e2LnM1OK5JATtsyfJvPTZ8c/dawHNqjUltcdHRSpgMtPe7v07pw==",
"requires": {
"chain-function": "1.0.0",
"dom-helpers": "3.3.1",
"loose-envify": "1.3.1",
"prop-types": "15.6.0",
"warning": "3.0.0"
}
}
}
},
"@atlaskit/util-common": {
@@ -4784,11 +4695,6 @@
"strip-eof": "1.0.0"
}
},
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@@ -6330,21 +6236,6 @@
"globule": "1.2.0"
}
},
"generate-function": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz",
"integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=",
"dev": true
},
"generate-object-property": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz",
"integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=",
"dev": true,
"requires": {
"is-property": "1.0.2"
}
},
"get-caller-file": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz",
@@ -6894,6 +6785,11 @@
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.6.2.tgz",
"integrity": "sha512-pH3vDzpczdsKHdZ9xxR3O46unSjisgVx0IImay7Zz2EdhRVbCkj+nthx9OuuWEhakx9FAO+fNVGrF0rZ2oMOvw=="
},
"immutable": {
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz",
"integrity": "sha1-wkOZUUVbs5kT2vKBN28VMOEErfM="
},
"import-local": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-0.1.1.tgz",
@@ -7170,25 +7066,6 @@
"is-extglob": "1.0.0"
}
},
"is-my-ip-valid": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz",
"integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==",
"dev": true
},
"is-my-json-valid": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz",
"integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==",
"dev": true,
"requires": {
"generate-function": "2.0.0",
"generate-object-property": "1.2.0",
"is-my-ip-valid": "1.0.0",
"jsonpointer": "4.0.1",
"xtend": "4.0.1"
}
},
"is-number": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
@@ -7250,12 +7127,6 @@
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=",
"dev": true
},
"is-regex": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
@@ -7515,12 +7386,6 @@
"resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
"integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
},
"jsonpointer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz",
"integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=",
"dev": true
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -7608,7 +7473,7 @@
}
},
"lib-jitsi-meet": {
"version": "github:jitsi/lib-jitsi-meet#307777207ec219df344b8fed2fd2071d2238f9f8",
"version": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"requires": {
"async": "0.9.0",
"current-executing-script": "0.1.3",
@@ -7774,12 +7639,6 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.mergewith": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
"integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
"dev": true
},
"lodash.pad": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz",
@@ -8456,8 +8315,7 @@
"nan": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.9.2.tgz",
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw==",
"optional": true
"integrity": "sha512-ltW65co7f3PQWBDbqVvaU1WtFJUsNW7sWWm4HINhbMQIyVyzIeyZ8toX5TC5eeooE6piZoaEh4cZkueSKG3KYw=="
},
"natural-compare": {
"version": "1.4.0",
@@ -8630,9 +8488,9 @@
}
},
"node-sass": {
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.8.3.tgz",
"integrity": "sha512-tfFWhUsCk/Y19zarDcPo5xpj+IW3qCfOjVdHtYeG6S1CKbQOh1zqylnQK6cV3z9k80yxAnFX9Y+a9+XysDhhfg==",
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/node-sass/-/node-sass-3.13.1.tgz",
"integrity": "sha1-ckD7v/I5YwS0IjUn7TAgWJwAT8I=",
"dev": true,
"requires": {
"async-foreach": "0.1.3",
@@ -8644,45 +8502,15 @@
"in-publish": "2.0.0",
"lodash.assign": "4.2.0",
"lodash.clonedeep": "4.5.0",
"lodash.mergewith": "4.6.1",
"meow": "3.7.0",
"mkdirp": "0.5.1",
"nan": "2.10.0",
"nan": "2.9.2",
"node-gyp": "3.6.2",
"npmlog": "4.1.2",
"request": "2.79.0",
"sass-graph": "2.2.4",
"stdout-stream": "1.4.0",
"true-case-path": "1.0.2"
"request": "2.83.0",
"sass-graph": "2.2.4"
},
"dependencies": {
"assert-plus": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz",
"integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=",
"dev": true
},
"aws-sign2": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz",
"integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=",
"dev": true
},
"boom": {
"version": "2.10.1",
"resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz",
"integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=",
"dev": true,
"requires": {
"hoek": "2.16.3"
}
},
"caseless": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz",
"integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=",
"dev": true
},
"cross-spawn": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz",
@@ -8693,26 +8521,6 @@
"which": "1.3.0"
}
},
"cryptiles": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz",
"integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=",
"dev": true,
"requires": {
"boom": "2.10.1"
}
},
"form-data": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz",
"integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=",
"dev": true,
"requires": {
"asynckit": "0.4.0",
"combined-stream": "1.0.6",
"mime-types": "2.1.18"
}
},
"gauge": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
@@ -8729,47 +8537,6 @@
"wide-align": "1.1.2"
}
},
"har-validator": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz",
"integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=",
"dev": true,
"requires": {
"chalk": "1.1.3",
"commander": "2.14.1",
"is-my-json-valid": "2.17.2",
"pinkie-promise": "2.0.1"
}
},
"hawk": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz",
"integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=",
"dev": true,
"requires": {
"boom": "2.10.1",
"cryptiles": "2.0.5",
"hoek": "2.16.3",
"sntp": "1.0.9"
}
},
"hoek": {
"version": "2.16.3",
"resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz",
"integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=",
"dev": true
},
"http-signature": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz",
"integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=",
"dev": true,
"requires": {
"assert-plus": "0.2.0",
"jsprim": "1.4.1",
"sshpk": "1.13.1"
}
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
@@ -8779,12 +8546,6 @@
"number-is-nan": "1.0.1"
}
},
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
"dev": true
},
"npmlog": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
@@ -8797,49 +8558,6 @@
"set-blocking": "2.0.0"
}
},
"qs": {
"version": "6.3.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
"integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=",
"dev": true
},
"request": {
"version": "2.79.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz",
"integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=",
"dev": true,
"requires": {
"aws-sign2": "0.6.0",
"aws4": "1.6.0",
"caseless": "0.11.0",
"combined-stream": "1.0.6",
"extend": "3.0.1",
"forever-agent": "0.6.1",
"form-data": "2.1.4",
"har-validator": "2.0.6",
"hawk": "3.1.3",
"http-signature": "1.1.1",
"is-typedarray": "1.0.0",
"isstream": "0.1.2",
"json-stringify-safe": "5.0.1",
"mime-types": "2.1.18",
"oauth-sign": "0.8.2",
"qs": "6.3.2",
"stringstream": "0.0.5",
"tough-cookie": "2.3.3",
"tunnel-agent": "0.4.3",
"uuid": "3.1.0"
}
},
"sntp": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz",
"integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=",
"dev": true,
"requires": {
"hoek": "2.16.3"
}
},
"string-width": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
@@ -8850,12 +8568,6 @@
"is-fullwidth-code-point": "1.0.0",
"strip-ansi": "3.0.1"
}
},
"tunnel-agent": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz",
"integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=",
"dev": true
}
}
},
@@ -8932,6 +8644,14 @@
"gauge": "1.2.7"
}
},
"nuclear-js": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/nuclear-js/-/nuclear-js-1.4.0.tgz",
"integrity": "sha1-bJwAGwZz8K6dj4sYjE2gTtaTp74=",
"requires": {
"immutable": "3.8.2"
}
},
"num2fraction": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz",
@@ -10290,11 +10010,6 @@
"resolved": "https://registry.npmjs.org/react-deep-force-update/-/react-deep-force-update-1.1.1.tgz",
"integrity": "sha1-vNMUeAJ7ZLMznxCJIatSC0MT3Cw="
},
"react-deprecate": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/react-deprecate/-/react-deprecate-0.1.0.tgz",
"integrity": "sha512-9ooyaovhANHgfuOxXRgrEiEfWjEhvygeSxrRTGxNlXErnXnyHBGjxCxrKYsT/Gsc62lS9rFOBeK0c2wwdyUnvQ=="
},
"react-devtools-core": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-2.5.2.tgz",
@@ -10441,7 +10156,9 @@
"integrity": "sha512-vLNJIedXQZN4p3ChFsAgVHacnJqQMnLl+wBsnZuliRkmsjEHo8kQOA9fnLih/OoiDi1O3eHQvXC5L8f+RYiKgw=="
},
"react-native-calendar-events": {
"version": "github:jitsi/react-native-calendar-events#dabfabc4bacc1424a8b93ebdda6f3820dee198cb"
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/react-native-calendar-events/-/react-native-calendar-events-1.4.3.tgz",
"integrity": "sha1-KYBOi0TWlG5pq1ogkC2USe0xXEc="
},
"react-native-callstats": {
"version": "3.27.0",
@@ -10454,7 +10171,9 @@
}
},
"react-native-fetch-blob": {
"version": "github:flatfox-ag/react-native-fetch-blob#01f38a4537baecd3ea0cb93c27e84553f3fc5231",
"version": "0.10.8",
"resolved": "https://registry.npmjs.org/react-native-fetch-blob/-/react-native-fetch-blob-0.10.8.tgz",
"integrity": "sha1-T8JWq64MtfEOfEHyjBGz/zMNcqk=",
"requires": {
"base-64": "0.1.0",
"glob": "7.0.6"
@@ -10502,9 +10221,9 @@
"integrity": "sha1-QeDsKqfdjxLzo+6Dr51jxLZw+KE="
},
"react-native-sound": {
"version": "0.10.9",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.9.tgz",
"integrity": "sha1-awCw9K/QF83gn7udFx3xtdW4Uag="
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/react-native-sound/-/react-native-sound-0.10.4.tgz",
"integrity": "sha512-V9v4CjKgv8ekQRLOJSoKA7pxJ03F4Ih3T/RfMIlMWLktz7v/O4sdJPjRBLOzZRqAnr9FWTLbSk1ZCjioXh3mjQ=="
},
"react-native-vector-icons": {
"version": "4.4.2",
@@ -10539,7 +10258,7 @@
}
},
"react-native-webrtc": {
"version": "github:jitsi/react-native-webrtc#52fe4646401408e0569e972cabf08f3c21b7a107",
"version": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
"requires": {
"base64-js": "1.2.3",
"event-target-shim": "1.1.1",
@@ -10575,14 +10294,6 @@
}
}
},
"react-scrolllock": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/react-scrolllock/-/react-scrolllock-2.0.2.tgz",
"integrity": "sha512-Kz0cTwHnGdTEvvNmKWKhFYrwpap8jGLbdTSjzZE1X37OJW72TVDWsL4yQ9oKS/uAsHUlbrg7AZt8Z1vgcRHmFQ==",
"requires": {
"exenv": "1.2.2"
}
},
"react-syntax-highlighter": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-3.0.2.tgz",
@@ -11756,47 +11467,6 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
"integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
},
"stdout-stream": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz",
"integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=",
"dev": true,
"requires": {
"readable-stream": "2.3.6"
},
"dependencies": {
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"dev": true
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"dev": true,
"requires": {
"core-util-is": "1.0.2",
"inherits": "2.0.3",
"isarray": "1.0.0",
"process-nextick-args": "2.0.0",
"safe-buffer": "5.1.1",
"string_decoder": "1.1.1",
"util-deprecate": "1.0.2"
}
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"requires": {
"safe-buffer": "5.1.1"
}
}
}
},
"stream-browserify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
@@ -12277,30 +11947,6 @@
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz",
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM="
},
"true-case-path": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz",
"integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=",
"dev": true,
"requires": {
"glob": "6.0.4"
},
"dependencies": {
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
"integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=",
"dev": true,
"requires": {
"inflight": "1.0.6",
"inherits": "2.0.3",
"minimatch": "3.0.4",
"once": "1.4.0",
"path-is-absolute": "1.0.1"
}
}
}
},
"tsscmp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz",

View File

@@ -23,8 +23,8 @@
"@atlaskit/field-text-area": "1.2.0",
"@atlaskit/flag": "6.1.0",
"@atlaskit/icon": "10.0.0",
"@atlaskit/inline-dialog": "5.3.0",
"@atlaskit/inline-message": "4.0.0",
"@atlaskit/inline-dialog": "5.0.2",
"@atlaskit/inline-message": "3.0.1",
"@atlaskit/layer-manager": "2.8.0",
"@atlaskit/lozenge": "3.4.2",
"@atlaskit/modal-dialog": "3.4.0",
@@ -32,7 +32,7 @@
"@atlaskit/spinner": "4.0.0",
"@atlaskit/tabs": "4.0.1",
"@atlaskit/theme": "2.4.0",
"@atlaskit/tooltip": "9.1.1",
"@atlaskit/tooltip": "6.0.0",
"autosize": "1.18.13",
"es6-iterator": "2.0.3",
"es6-symbol": "3.1.1",
@@ -46,9 +46,10 @@
"jquery-i18next": "1.2.0",
"js-md5": "0.6.1",
"jwt-decode": "2.2.0",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#307777207ec219df344b8fed2fd2071d2238f9f8",
"lib-jitsi-meet": "github:jitsi/lib-jitsi-meet#0503ec4d3f175b154b1c6fd7037520ce0768fa58",
"lodash": "4.17.4",
"moment": "2.19.4",
"nuclear-js": "1.4.0",
"postis": "2.2.0",
"prop-types": "15.6.0",
"react": "16.2.0",
@@ -56,17 +57,17 @@
"react-i18next": "4.8.0",
"react-native": "0.51.0",
"react-native-background-timer": "2.0.0",
"react-native-calendar-events": "github:jitsi/react-native-calendar-events#dabfabc4bacc1424a8b93ebdda6f3820dee198cb",
"react-native-calendar-events": "1.4.3",
"react-native-callstats": "3.27.0",
"react-native-fetch-blob": "github:flatfox-ag/react-native-fetch-blob#exception_fixes",
"react-native-fetch-blob": "0.10.8",
"react-native-img-cache": "1.5.2",
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#cc76092fc4335488a28a9529c8b50afae2c3ecdc",
"react-native-prompt": "1.0.0",
"react-native-sound": "0.10.9",
"react-native-sound": "0.10.4",
"react-native-vector-icons": "4.4.2",
"react-native-webrtc": "github:jitsi/react-native-webrtc#52fe4646401408e0569e972cabf08f3c21b7a107",
"react-native-webrtc": "github:jitsi/react-native-webrtc#626818af40384356617f70366133317b6a475171",
"react-redux": "5.0.6",
"redux": "3.7.2",
"redux-thunk": "2.2.0",
@@ -98,7 +99,7 @@
"file-loader": "1.1.5",
"flow-bin": "0.57.3",
"imports-loader": "0.7.1",
"node-sass": "4.8.3",
"node-sass": "3.13.1",
"precommit-hook": "3.0.0",
"string-replace-loader": "1.3.0",
"style-loader": "0.19.0",

View File

@@ -2,10 +2,54 @@
import React, { Component } from 'react';
import ToolboxAlwaysOnTop from './ToolboxAlwaysOnTop';
import StatelessToolbar from '../toolbox/components/StatelessToolbar';
import StatelessToolbarButton
from '../toolbox/components/StatelessToolbarButton';
const { api } = window.alwaysOnTop;
/**
* Map with toolbar button descriptors.
*/
const TOOLBAR_BUTTONS = {
/**
* The descriptor of the camera toolbar button.
*/
camera: {
classNames: [ 'button', 'icon-camera' ],
enabled: true,
id: 'toolbar_button_camera',
onClick() {
api.executeCommand('toggleVideo');
}
},
/**
* The descriptor of the toolbar button which hangs up the call/conference.
*/
hangup: {
classNames: [ 'button', 'icon-hangup', 'button_hangup' ],
enabled: true,
id: 'toolbar_button_hangup',
onClick() {
api.executeCommand('hangup');
window.close();
}
},
/**
* The descriptor of the microphone toolbar button.
*/
microphone: {
classNames: [ 'button', 'icon-microphone' ],
enabled: true,
id: 'toolbar_button_mute',
onClick() {
api.executeCommand('toggleAudio');
}
}
};
/**
* The timeout in ms for hidding the toolbar.
*/
@@ -341,16 +385,62 @@ export default class AlwaysOnTop extends Component<*, State> {
* @returns {ReactElement}
*/
render() {
const className
= `toolbar_primary always-on-top ${
this.state.visible ? 'fadeIn' : 'fadeOut'}`;
return (
<div id = 'alwaysOnTop'>
<ToolboxAlwaysOnTop
audioAvailable = { this.state.audioAvailable }
audioMuted = { this.state.audioMuted }
className = { this.state.visible ? 'fadeIn' : 'fadeOut' }
<StatelessToolbar
className = { className }
onMouseOut = { this._onMouseOut }
onMouseOver = { this._onMouseOver }
videoAvailable = { this.state.videoAvailable }
videoMuted = { this.state.videoMuted } />
onMouseOver = { this._onMouseOver }>
{
Object.entries(TOOLBAR_BUTTONS).map(
([ key, button ]) => {
// XXX The following silences a couple of flow
// errors:
if (button === null
|| typeof button !== 'object') {
return null;
}
const { onClick } = button;
let enabled = false;
let toggled = false;
switch (key) {
case 'microphone':
enabled = this.state.audioAvailable;
toggled = enabled
? this.state.audioMuted : true;
break;
case 'camera':
enabled = this.state.videoAvailable;
toggled = enabled
? this.state.videoMuted : true;
break;
default: // hangup button
toggled = false;
enabled = true;
}
const updatedButton = {
...button,
enabled,
toggled
};
return (
<StatelessToolbarButton
button = { updatedButton }
key = { key }
onClick = { onClick } />
);
}
)
}
</StatelessToolbar>
{
this._renderVideoNotAvailableScreen()
}

View File

@@ -1,159 +0,0 @@
// @flow
import React, { Component } from 'react';
// FIXME: AlwaysOnTop imports the button directly in order to avoid bringing in
// other components that use lib-jitsi-meet, which always on top does not
// import.
import ToolbarButton from '../toolbox/components/ToolbarButton';
const { api } = window.alwaysOnTop;
/**
* The type of the React {@code Component} props of {@link ToolboxAlwaysOnTop}.
*/
type Props = {
/**
* Whether or not microphone access is available.
*/
audioAvailable: boolean,
/**
* Whether or not the user is currently audio muted.
*/
audioMuted: boolean,
/**
* Additional CSS class names to add to the root of the toolbar.
*/
className: string,
/**
* Callback invoked when no longer moused over the toolbar.
*/
onMouseOut: Function,
/**
* Callback invoked when the mouse has moved over the toolbar.
*/
onMouseOver: Function,
/**
* Whether or not camera access is available.
*/
videoAvailable: boolean,
/**
* Whether or not the user is currently video muted.
*/
videoMuted: boolean
};
/**
* Represents the toolbar in the Always On Top window.
*
* @extends Component
*/
export default class ToolboxAlwaysOnTop extends Component<Props> {
/**
* Initializes a new {@code ToolboxAlwaysOnTop} instance.
*
* @param {Props} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: Props) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onToolbarHangup = this._onToolbarHangup.bind(this);
this._onToolbarToggleAudio = this._onToolbarToggleAudio.bind(this);
this._onToolbarToggleVideo = this._onToolbarToggleVideo.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
const {
audioAvailable,
audioMuted,
className = '',
onMouseOut,
onMouseOver,
videoAvailable,
videoMuted
} = this.props;
const videoMuteIcon = `${videoMuted || !videoAvailable
? 'icon-camera-disabled toggled' : 'icon-camera'} ${
videoAvailable ? '' : 'disabled'}`;
const audioMuteIcon = `${audioMuted || !audioAvailable
? 'icon-mic-disabled toggled' : 'icon-microphone'} ${
audioAvailable ? '' : 'disabled'}`;
return (
<div
className = { `always-on-top-toolbox ${className}` }
onMouseOut = { onMouseOut }
onMouseOver = { onMouseOver }>
<ToolbarButton
accessibilityLabel = 'Video mute'
iconName = { videoMuteIcon }
onClick = { this._onToolbarToggleVideo } />
<ToolbarButton
accessibilityLabel = 'Hangup'
iconName = 'icon-hangup'
onClick = { this._onToolbarHangup } />
<ToolbarButton
accessibilityLabel = 'Audio mute'
iconName = { audioMuteIcon }
onClick = { this._onToolbarToggleAudio } />
</div>
);
}
_onToolbarHangup: () => void;
/**
* Ends the conference call and closes the always on top window.
*
* @private
* @returns {void}
*/
_onToolbarHangup() {
api.executeCommand('hangup');
window.close();
}
_onToolbarToggleAudio: () => void;
/**
* Toggles audio mute if audio is avaiable.
*
* @private
* @returns {void}
*/
_onToolbarToggleAudio() {
if (this.props.audioAvailable) {
api.executeCommand('toggleAudio');
}
}
_onToolbarToggleVideo: () => void;
/**
* Toggles video mute if video is avaiable.
*
* @private
* @returns {void}
*/
_onToolbarToggleVideo() {
if (this.props.videoAvailable) {
api.executeCommand('toggleVideo');
}
}
}

View File

@@ -97,25 +97,6 @@ export function createAudioOnlyChangedEvent(enabled) {
};
}
/**
* Creates an event for an action on the deep linking page.
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createDeepLinkingPageEvent(
action, actionSubject, attributes = {}) {
return {
action,
actionSubject,
source: 'deepLinkingPage',
attributes
};
}
/**
* Creates an event which indicates that a device was changed.
*
@@ -148,21 +129,16 @@ export function createFeedbackOpenEvent() {
}
/**
* Creates an event for an action regarding the AddPeopleDialog (invites).
* Creates an event which indicates that the invite dialog was closed. This is
* not a TYPE_UI event, since it is not necessarily the result of a user
* interaction.
*
* @param {string} action - The action that the event represents.
* @param {string} actionSubject - The subject that was acted upon.
* @param {boolean} attributes - Additional attributes to attach to the event.
* @returns {Object} The event in a format suitable for sending via
* sendAnalytics.
*/
export function createInviteDialogEvent(
action, actionSubject, attributes = {}) {
export function createInviteDialogClosedEvent() {
return {
action,
actionSubject,
attributes,
source: 'inviteDialog'
action: 'invite.dialog.closed'
};
}

View File

@@ -124,8 +124,10 @@ function _appNavigateToMandatoryLocation(
});
}
const profile = getState()['features/base/profile'];
return promise.then(() =>
dispatch(setConfig(config)));
dispatch(setConfig(_mergeConfigWithProfile(config, profile))));
}
}
@@ -288,3 +290,23 @@ function _loadConfig({ contextRoot, host, protocol, room }) {
throw error;
});
}
/**
* Merges the downloaded config with the current profile values. The profile
* values are named the same way as the config values in the config.js so
* a clean merge is possible.
*
* @param {Object|undefined} config - The downloaded config.
* @param {Object} profile - The persisted profile.
* @returns {Object}
*/
function _mergeConfigWithProfile(config, profile) {
if (!config) {
return;
}
return {
...config,
...profile
};
}

View File

@@ -37,10 +37,6 @@ export class App extends AbstractApp {
static propTypes = {
...AbstractApp.propTypes,
addPeopleEnabled: PropTypes.bool,
dialOutEnabled: PropTypes.bool,
/**
* Whether Picture-in-Picture is enabled. If {@code true}, a toolbar
* button is rendered in the {@link Conference} view to afford entering

View File

@@ -1,5 +1,4 @@
// @flow
/* @flow */
import { NativeModules } from 'react-native';
export * from './getRouteToRender';
@@ -12,13 +11,3 @@ export * from './getRouteToRender';
export function getName() {
return NativeModules.AppInfo.name;
}
/**
* Returns the path to the Jitsi Meet SDK bundle on iOS. On Android it will be
* undefined.
*
* @returns {string|undefined}
*/
export function getSdkBundlePath() {
return NativeModules.AppInfo.sdkBundlePath;
}

View File

@@ -1,10 +1,12 @@
/* @flow */
import { Platform } from '../base/react';
import { toState } from '../base/redux';
import { getDeepLinkingPage } from '../deep-linking';
import {
NoMobileApp,
PluginRequiredBrowser,
UnsupportedDesktopBrowser
UnsupportedDesktopBrowser,
UnsupportedMobileBrowser
} from '../unsupported-browser';
import {
@@ -22,18 +24,43 @@ declare var loggingConfig: Object;
*
* @private
* @param {Object} state - Object containing current redux state.
* @returns {Promise<ReactElement>|void}
* @returns {ReactElement|void}
* @type {Function[]}
*/
const _INTERCEPT_COMPONENT_RULES = [
getDeepLinkingPage,
/**
* This rule describes case when user opens application using mobile
* browser. In order to promote the app, we choose to suggest the mobile
* app even if the browser supports the app (e.g. Google Chrome with
* WebRTC support on Android).
*
* @param {Object} state - The redux state of the app.
* @returns {UnsupportedMobileBrowser|void} If the rule is satisfied then
* we should intercept existing component by UnsupportedMobileBrowser.
*/
// eslint-disable-next-line no-unused-vars
state => {
const OS = Platform.OS;
if (OS === 'android' || OS === 'ios') {
const mobileAppPromo
= typeof interfaceConfig === 'object'
&& interfaceConfig.MOBILE_APP_PROMO;
return (
typeof mobileAppPromo === 'undefined' || Boolean(mobileAppPromo)
? UnsupportedMobileBrowser
: NoMobileApp);
}
},
state => {
const { webRTCReady } = state['features/base/lib-jitsi-meet'];
switch (typeof webRTCReady) {
case 'boolean':
if (webRTCReady === false) {
return Promise.resolve(UnsupportedDesktopBrowser);
return UnsupportedDesktopBrowser;
}
break;
@@ -42,10 +69,8 @@ const _INTERCEPT_COMPONENT_RULES = [
break;
default:
return Promise.resolve(PluginRequiredBrowser);
return PluginRequiredBrowser;
}
return Promise.resolve();
}
];
@@ -55,19 +80,16 @@ const _INTERCEPT_COMPONENT_RULES = [
*
* @param {(Object|Function)} stateOrGetState - The redux state or
* {@link getState} function.
* @returns {Promise<Route>}
* @returns {Route}
*/
export function _getRouteToRender(stateOrGetState: Object | Function): Object {
export function _getRouteToRender(stateOrGetState: Object | Function) {
const route = _super_getRouteToRender(stateOrGetState);
// Intercepts route components if any of component interceptor rules is
// satisfied.
return _interceptComponent(stateOrGetState, route.component).then(
(component: React$Element<*>) => {
route.component = component;
route.component = _interceptComponent(stateOrGetState, route.component);
return route;
}, () => Promise.resolve(route));
return route;
}
/**
@@ -77,24 +99,23 @@ export function _getRouteToRender(stateOrGetState: Object | Function): Object {
* {@link getState} function.
* @param {ReactElement} component - Current route component to render.
* @private
* @returns {Promise<ReactElement>} If any of the pre-defined rules is
* satisfied, returns intercepted component.
* @returns {ReactElement} If any of the pre-defined rules is satisfied, returns
* intercepted component.
*/
function _interceptComponent(
stateOrGetState: Object | Function,
component: React$Element<*>) {
let result;
const state = toState(stateOrGetState);
const promises = [];
for (const rule of _INTERCEPT_COMPONENT_RULES) {
result = rule(state);
if (result) {
break;
}
}
_INTERCEPT_COMPONENT_RULES.forEach(rule => {
promises.push(rule(state));
});
return Promise.all(promises).then(
results =>
results.find(result => typeof result !== 'undefined') || component,
() => Promise.resolve(component));
return result || component;
}
/**

View File

@@ -79,18 +79,7 @@ function _navigate({ getState }) {
const { app } = state['features/app'];
const routeToRender = _getRouteToRender(state);
// XXX Web changed _getRouteToRender to return Promsie instead of Route.
// Unfortunately, the commit left mobile to return Route.
let routeToRenderPromise;
if (routeToRender && typeof routeToRender.then === 'function') {
routeToRenderPromise = routeToRender;
}
if (!routeToRenderPromise) {
routeToRenderPromise = Promise.resolve(routeToRender);
}
routeToRenderPromise.then(app._navigate.bind(app));
return app._navigate(routeToRender);
}
/**

View File

@@ -62,17 +62,6 @@ export const CONFERENCE_WILL_LEAVE = Symbol('CONFERENCE_WILL_LEAVE');
*/
export const DATA_CHANNEL_OPENED = Symbol('DATA_CHANNEL_OPENED');
/**
* The type of action which signals that the user has been kicked out from
* the conference.
*
* {
* type: KICKED_OUT,
* conference: JitsiConference
* }
*/
export const KICKED_OUT = Symbol('KICKED_OUT');
/**
* The type of (redux) action which signals that the lock state of a specific
* {@code JitsiConference} changed.
@@ -107,18 +96,6 @@ export const P2P_STATUS_CHANGED = Symbol('P2P_STATUS_CHANGED');
*/
export const SET_AUDIO_ONLY = Symbol('SET_AUDIO_ONLY');
/**
* The type of (redux) action which sets the desktop sharing enabled flag for
* the current conference.
*
* {
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }
*/
export const SET_DESKTOP_SHARING_ENABLED
= Symbol('SET_DESKTOP_SHARING_ENABLED');
/**
* The type of (redux) action which updates the current known status of the
* Follow Me feature.

View File

@@ -28,11 +28,9 @@ import {
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
DATA_CHANNEL_OPENED,
KICKED_OUT,
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_LASTN,
SET_PASSWORD,
@@ -79,10 +77,6 @@ function _addConferenceListeners(conference, dispatch) {
JitsiConferenceEvents.CONFERENCE_LEFT,
(...args) => dispatch(conferenceLeft(conference, ...args)));
conference.on(
JitsiConferenceEvents.KICKED,
() => dispatch(kickedOut(conference)));
conference.on(
JitsiConferenceEvents.LOCK_STATE_CHANGED,
(...args) => dispatch(lockStateChanged(conference, ...args)));
@@ -363,23 +357,6 @@ export function dataChannelOpened() {
};
}
/**
* Signals that we've been kicked out of the conference.
*
* @param {JitsiConference} conference - The {@link JitsiConference} instance
* for which the event is being signaled.
* @returns {{
* type: KICKED_OUT,
* conference: JitsiConference
* }}
*/
export function kickedOut(conference: Object) {
return {
type: KICKED_OUT,
conference
};
}
/**
* Signals that the lock state of a specific JitsiConference changed.
*
@@ -456,22 +433,6 @@ export function setAudioOnly(audioOnly: boolean) {
};
}
/**
* Sets the flag for indicating if desktop sharing is enabled.
*
* @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
* @returns {{
* type: SET_DESKTOP_SHARING_ENABLED,
* desktopSharingEnabled: boolean
* }}
*/
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
return {
type: SET_DESKTOP_SHARING_ENABLED,
desktopSharingEnabled
};
}
/**
* Enables or disables the Follow Me feature.
*

View File

@@ -22,11 +22,14 @@ import { TRACK_ADDED, TRACK_REMOVED } from '../tracks';
import {
conferenceLeft,
createConference,
setAudioOnly,
setLastN,
toggleAudioOnly
} from './actions';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
DATA_CHANNEL_OPENED,
SET_AUDIO_ONLY,
SET_LASTN,
@@ -55,6 +58,10 @@ MiddlewareRegistry.register(store => next => action => {
case CONNECTION_ESTABLISHED:
return _connectionEstablished(store, next, action);
case CONFERENCE_FAILED:
case CONFERENCE_LEFT:
return _conferenceFailedOrLeft(store, next, action);
case CONFERENCE_JOINED:
return _conferenceJoined(store, next, action);
@@ -109,6 +116,43 @@ function _connectionEstablished({ dispatch }, next, action) {
return result;
}
/**
* Does extra sync up on properties that may need to be updated after the
* conference failed or was left.
*
* @param {Store} store - The redux store in which the specified action is being
* dispatched.
* @param {Dispatch} next - The redux dispatch function to dispatch the
* specified action to the specified store.
* @param {Action} action - The redux action {@link CONFERENCE_FAILED} or
* {@link CONFERENCE_LEFT} which is being dispatched in the specified store.
* @private
* @returns {Object} The value returned by {@code next(action)}.
*/
function _conferenceFailedOrLeft({ dispatch, getState }, next, action) {
const result = next(action);
const state = getState();
const { audioOnly } = state['features/base/conference'];
const { startAudioOnly } = state['features/base/profile'];
// FIXME: Consider implementing a standalone audio-only feature that handles
// all these state changes.
if (audioOnly) {
if (!startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(false));
logger.log('Audio only disabled');
dispatch(setAudioOnly(false));
}
} else if (startAudioOnly) {
sendAnalytics(createAudioOnlyChangedEvent(true));
logger.log('Audio only enabled');
dispatch(setAudioOnly(true));
}
return result;
}
/**
* Does extra sync up on properties that may need to be updated after the
* conference was joined.
@@ -218,34 +262,25 @@ function _pinParticipant({ getState }, next, action) {
* @returns {Object} The value returned by {@code next(action)}.
*/
function _setAudioOnly({ dispatch, getState }, next, action) {
const { audioOnly: oldValue } = getState()['features/base/conference'];
const result = next(action);
const { audioOnly: newValue } = getState()['features/base/conference'];
// Send analytics. We could've done it in the action creator setAudioOnly.
// I don't know why it has to happen as early as possible but the analytics
// were originally sent before the SET_AUDIO_ONLY action was even dispatched
// in the redux store so I'm now sending the analytics as early as possible.
if (oldValue !== newValue) {
sendAnalytics(createAudioOnlyChangedEvent(newValue));
logger.log(`Audio-only ${newValue ? 'enabled' : 'disabled'}`);
}
const { audioOnly } = getState()['features/base/conference'];
// Set lastN to 0 in case audio-only is desired; leave it as undefined,
// otherwise, and the default lastN value will be chosen automatically.
dispatch(setLastN(newValue ? 0 : undefined));
dispatch(setLastN(audioOnly ? 0 : undefined));
// Mute/unmute the local video.
dispatch(
setVideoMuted(
newValue,
audioOnly,
VIDEO_MUTISM_AUTHORITY.AUDIO_ONLY,
/* ensureTrack */ true));
if (typeof APP !== 'undefined') {
// TODO This should be a temporary solution that lasts only until
// video tracks and all ui is moved into react/redux on the web.
APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, newValue);
APP.UI.emitEvent(UIEvents.TOGGLE_AUDIO_ONLY, audioOnly);
}
return result;

View File

@@ -14,7 +14,6 @@ import {
LOCK_STATE_CHANGED,
P2P_STATUS_CHANGED,
SET_AUDIO_ONLY,
SET_DESKTOP_SHARING_ENABLED,
SET_FOLLOW_ME,
SET_PASSWORD,
SET_RECEIVE_VIDEO_QUALITY,
@@ -58,9 +57,6 @@ ReducerRegistry.register('features/base/conference', (state = {}, action) => {
case SET_AUDIO_ONLY:
return _setAudioOnly(state, action);
case SET_DESKTOP_SHARING_ENABLED:
return _setDesktopSharingEnabled(state, action);
case SET_FOLLOW_ME:
return {
...state,
@@ -333,21 +329,6 @@ function _setAudioOnly(state, action) {
return set(state, 'audioOnly', action.audioOnly);
}
/**
* Reduces a specific Redux action SET_DESKTOP_SHARING_ENABLED of the feature
* base/conference.
*
* @param {Object} state - The Redux state of the feature base/conference.
* @param {Action} action - The Redux action SET_DESKTOP_SHARING_ENABLED to
* reduce.
* @private
* @returns {Object} The new state of the feature base/conference after the
* reduction of the specified action.
*/
function _setDesktopSharingEnabled(state, action) {
return set(state, 'desktopSharingEnabled', action.desktopSharingEnabled);
}
/**
* Reduces a specific Redux action SET_PASSWORD of the feature base/conference.
*

View File

@@ -20,6 +20,7 @@ const WHITELISTED_KEYS = [
'_peerConnStatusRtcMuteTimeout',
'abTesting',
'alwaysVisibleToolbar',
'autoEnableDesktopSharing',
'autoRecord',
'autoRecordToken',
'avgRtpStatsN',
@@ -56,24 +57,20 @@ const WHITELISTED_KEYS = [
'enableLipSync',
'enableLocalVideoFlip',
'enableRecording',
'enableRemb',
'enableStatsID',
'enableTalkWhileMuted',
'enableTcc',
'enableUserRolesBasedOnToken',
'etherpad_base',
'failICE',
'firefox_fake_device',
'forceJVB121Ratio',
'gatherStats',
'googleApiApplicationClientID',
'hiddenDomain',
'hosts',
'iAmRecorder',
'iAmSipGateway',
'iceTransportPolicy',
'ignoreStartMuted',
'minParticipants',
'nick',
'openBridgeChannel',
'p2p',

View File

@@ -1,4 +1,4 @@
// @flow
/* @flow */
import type { Dispatch } from 'redux';
@@ -30,38 +30,34 @@ export function connect() {
// XXX Lib-jitsi-meet does not accept uppercase letters.
const room = state['features/base/conference'].room.toLowerCase();
const { initPromise } = state['features/base/lib-jitsi-meet'];
// XXX For web based version we use conference initialization logic
// from the old app (at the moment of writing).
return initPromise.then(() => APP.conference.init({
roomName: room
})).catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
return APP.conference.init({ roomName: room })
.catch(error => {
APP.API.notifyConferenceLeft(APP.conference.roomName);
logger.error(error);
// TODO The following are in fact Errors raised by
// JitsiMeetJS.init() which should be taken care of in
// features/base/lib-jitsi-meet but we are not there yet on the
// Web at the time of this writing.
switch (error.name) {
case WEBRTC_NOT_READY:
case WEBRTC_NOT_SUPPORTED:
dispatch(libInitError(error));
}
});
// TODO The following are in fact Errors raised by
// JitsiMeetJS.init() which should be taken care of in
// features/base/lib-jitsi-meet but we are not there yet on the
// Web at the time of this writing.
switch (error.name) {
case WEBRTC_NOT_READY:
case WEBRTC_NOT_SUPPORTED:
dispatch(libInitError(error));
}
});
};
}
/**
* Closes connection.
*
* @param {boolean} [requestFeedback] - Whether or not to attempt showing a
* request for call feedback.
* @returns {Function}
*/
export function disconnect(requestFeedback: boolean = false) {
export function disconnect() {
// XXX For web based version we use conference hanging up logic from the old
// app.
return () => APP.conference.hangup(requestFeedback);
return () => APP.conference.hangup();
}

View File

@@ -52,24 +52,6 @@ export function getURLWithoutParams(url: URL): URL {
return url;
}
/**
* Gets a URL string without hash and query/search params from a specific
* {@code URL}.
*
* @param {URL} url - The {@code URL} which may have hash and query/search
* params.
* @returns {string}
*/
export function getURLWithoutParamsNormalized(url: URL): string {
const urlWithoutParams = getURLWithoutParams(url).href;
if (urlWithoutParams) {
return urlWithoutParams.toLowerCase();
}
return '';
}
/**
* Converts a specific id to jid if it's not jid yet.
*

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