Compare commits
6 Commits
jitihouse/
...
delimy-fea
Author | SHA1 | Date |
---|---|---|
paweldomas | c12c945d1a | |
paweldomas | 8e514dc07e | |
Saúl Ibarra Corretgé | bdeb56d5f8 | |
Lyubo Marinov | 4906453cb9 | |
Shuai Li | 102d4237a5 | |
Shuai Li | 45c2a657af |
|
@ -28,6 +28,7 @@ dependencies {
|
||||||
compile project(':react-native-fetch-blob')
|
compile project(':react-native-fetch-blob')
|
||||||
compile project(':react-native-immersive')
|
compile project(':react-native-immersive')
|
||||||
compile project(':react-native-keep-awake')
|
compile project(':react-native-keep-awake')
|
||||||
|
compile project(':react-native-linear-gradient')
|
||||||
compile project(':react-native-locale-detector')
|
compile project(':react-native-locale-detector')
|
||||||
compile project(':react-native-sound')
|
compile project(':react-native-sound')
|
||||||
compile project(':react-native-vector-icons')
|
compile project(':react-native-vector-icons')
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* Copyright @ 2017-present Atlassian Pty Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jitsi.meet.sdk;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
||||||
|
import com.facebook.react.bridge.UiThreadUtil;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public abstract class AbstractExternalAPIModule<T>
|
||||||
|
extends ReactContextBaseJavaModule {
|
||||||
|
|
||||||
|
private static Map<String, Method> createAPIMethodMap(
|
||||||
|
Class<?> listenerClass) {
|
||||||
|
Map<String, Method> result = new HashMap<>();
|
||||||
|
// Figure out the mapping between the JitsiMeetViewListener methods
|
||||||
|
// and the events i.e. redux action types.
|
||||||
|
Pattern onPattern = Pattern.compile("^on[A-Z]+");
|
||||||
|
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
|
||||||
|
|
||||||
|
for (Method method : listenerClass.getDeclaredMethods()) {
|
||||||
|
// * The method must be public (because it is declared by an
|
||||||
|
// interface).
|
||||||
|
// * The method must be/return void.
|
||||||
|
if (!Modifier.isPublic(method.getModifiers())
|
||||||
|
|| !Void.TYPE.equals(method.getReturnType())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The method name must start with "on" followed by a
|
||||||
|
// capital/uppercase letter (in agreement with the camelcase
|
||||||
|
// coding style customary to Java in general and the projects of
|
||||||
|
// the Jitsi community in particular).
|
||||||
|
String name = method.getName();
|
||||||
|
|
||||||
|
if (!onPattern.matcher(name).find()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// * The method must accept/have exactly 1 parameter of a type
|
||||||
|
// assignable from HashMap.
|
||||||
|
Class<?>[] parameterTypes = method.getParameterTypes();
|
||||||
|
|
||||||
|
if (parameterTypes.length != 1
|
||||||
|
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the method name to an event name.
|
||||||
|
name
|
||||||
|
= camelcasePattern.matcher(name.substring(2))
|
||||||
|
.replaceAll("$1_$2")
|
||||||
|
.toUpperCase(Locale.ROOT);
|
||||||
|
result.put(name, method);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, Method> methodMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code AbstractExternalAPIModule} instance. There shall
|
||||||
|
* be a single instance of a module throughout the lifetime of the
|
||||||
|
* application.
|
||||||
|
*
|
||||||
|
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||||
|
* is created.
|
||||||
|
*/
|
||||||
|
public AbstractExternalAPIModule(
|
||||||
|
ReactApplicationContext reactContext,
|
||||||
|
Class<T> listenerClass) {
|
||||||
|
super(reactContext);
|
||||||
|
|
||||||
|
this.methodMap = createAPIMethodMap(listenerClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T findListenerByExternalAPIScope(String scope);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||||
|
* the specified {@code View}'s listener on the UI thread.
|
||||||
|
*
|
||||||
|
* @param name The name of the event.
|
||||||
|
* @param data The details/specifics of the event to send determined
|
||||||
|
* by/associated with the specified {@code name}.
|
||||||
|
* @param scope
|
||||||
|
*/
|
||||||
|
public void sendEvent(
|
||||||
|
final String name,
|
||||||
|
final ReadableMap data,
|
||||||
|
final String scope) {
|
||||||
|
// Make sure listener is invoked on the UI thread. It was requested by
|
||||||
|
// SDK consumers.
|
||||||
|
if (UiThreadUtil.isOnUiThread()) {
|
||||||
|
sendEventOnUiThread(name, data, scope);
|
||||||
|
} else {
|
||||||
|
UiThreadUtil.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
sendEventOnUiThread(name, data, scope);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEventOnUiThread(final String name,
|
||||||
|
final ReadableMap data,
|
||||||
|
final String scope) {
|
||||||
|
// The JavaScript App needs to provide uniquely identifying information
|
||||||
|
// to the native AbstractExternalAPI module so that the latter may match
|
||||||
|
// the former to the native View which hosts it.
|
||||||
|
T listener = findListenerByExternalAPIScope(scope);
|
||||||
|
|
||||||
|
if (listener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Method method = methodMap.get(name);
|
||||||
|
|
||||||
|
if (method != null) {
|
||||||
|
try {
|
||||||
|
method.invoke(listener, toHashMap(data));
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// FIXME There was a multicatch for IllegalAccessException and
|
||||||
|
// InvocationTargetException, but Android Studio complained
|
||||||
|
// with: "Multi-catch with these reflection exceptions requires
|
||||||
|
// API level 19 (current min is 16) because they get compiled to
|
||||||
|
// the common but new super type ReflectiveOperationException.
|
||||||
|
// As a workaround either create individual catch statements, or
|
||||||
|
// catch Exception."
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code HashMap} instance with the key-value
|
||||||
|
* associations of a specific {@code ReadableMap}.
|
||||||
|
*
|
||||||
|
* @param readableMap the {@code ReadableMap} specifying the key-value
|
||||||
|
* associations with which the new {@code HashMap} instance is to be
|
||||||
|
* initialized.
|
||||||
|
* @return a new {@code HashMap} instance initialized with the key-value
|
||||||
|
* associations of the specified {@code readableMap}.
|
||||||
|
*/
|
||||||
|
private HashMap<String, Object> toHashMap(ReadableMap readableMap) {
|
||||||
|
HashMap<String, Object> hashMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
|
||||||
|
i.hasNextKey();) {
|
||||||
|
String key = i.nextKey();
|
||||||
|
|
||||||
|
hashMap.put(key, readableMap.getString(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return hashMap;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,35 +1,8 @@
|
||||||
/*
|
|
||||||
* 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;
|
package org.jitsi.meet.sdk;
|
||||||
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext;
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
||||||
import com.facebook.react.bridge.ReactMethod;
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
import com.facebook.react.bridge.ReadableMap;
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
import com.facebook.react.bridge.ReadableMapKeySetIterator;
|
|
||||||
import com.facebook.react.bridge.UiThreadUtil;
|
|
||||||
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.lang.reflect.Modifier;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Module implementing a simple API to enable a proximity sensor-controlled
|
* Module implementing a simple API to enable a proximity sensor-controlled
|
||||||
|
@ -37,66 +10,30 @@ import java.util.regex.Pattern;
|
||||||
* object it will dim the screen and disable touch controls. The functionality
|
* object it will dim the screen and disable touch controls. The functionality
|
||||||
* is used with the conference audio-only mode.
|
* is used with the conference audio-only mode.
|
||||||
*/
|
*/
|
||||||
class ExternalAPIModule extends ReactContextBaseJavaModule {
|
class ExternalAPIModule
|
||||||
/**
|
extends AbstractExternalAPIModule<JitsiMeetViewListener> {
|
||||||
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e.
|
|
||||||
* redux action types.
|
|
||||||
*/
|
|
||||||
private static final Map<String, Method> JITSI_MEET_VIEW_LISTENER_METHODS
|
|
||||||
= new HashMap<>();
|
|
||||||
|
|
||||||
static {
|
|
||||||
// Figure out the mapping between the JitsiMeetViewListener methods
|
|
||||||
// and the events i.e. redux action types.
|
|
||||||
Pattern onPattern = Pattern.compile("^on[A-Z]+");
|
|
||||||
Pattern camelcasePattern = Pattern.compile("([a-z0-9]+)([A-Z0-9]+)");
|
|
||||||
|
|
||||||
for (Method method : JitsiMeetViewListener.class.getDeclaredMethods()) {
|
|
||||||
// * The method must be public (because it is declared by an
|
|
||||||
// interface).
|
|
||||||
// * The method must be/return void.
|
|
||||||
if (!Modifier.isPublic(method.getModifiers())
|
|
||||||
|| !Void.TYPE.equals(method.getReturnType())) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * The method name must start with "on" followed by a
|
|
||||||
// capital/uppercase letter (in agreement with the camelcase
|
|
||||||
// coding style customary to Java in general and the projects of
|
|
||||||
// the Jitsi community in particular).
|
|
||||||
String name = method.getName();
|
|
||||||
|
|
||||||
if (!onPattern.matcher(name).find()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * The method must accept/have exactly 1 parameter of a type
|
|
||||||
// assignable from HashMap.
|
|
||||||
Class<?>[] parameterTypes = method.getParameterTypes();
|
|
||||||
|
|
||||||
if (parameterTypes.length != 1
|
|
||||||
|| !parameterTypes[0].isAssignableFrom(HashMap.class)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the method name to an event name.
|
|
||||||
name
|
|
||||||
= camelcasePattern.matcher(name.substring(2))
|
|
||||||
.replaceAll("$1_$2")
|
|
||||||
.toUpperCase(Locale.ROOT);
|
|
||||||
JITSI_MEET_VIEW_LISTENER_METHODS.put(name, method);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes a new module instance. There shall be a single instance of
|
* Initializes a new {@code ExternalAPIModule} instance. There shall be a
|
||||||
* this module throughout the lifetime of the application.
|
* single instance of this module throughout the lifetime of the
|
||||||
|
* application.
|
||||||
*
|
*
|
||||||
* @param reactContext the {@link ReactApplicationContext} where this module
|
* @param reactContext the {@link ReactApplicationContext} where this module
|
||||||
* is created.
|
* is created.
|
||||||
*/
|
*/
|
||||||
public ExternalAPIModule(ReactApplicationContext reactContext) {
|
public ExternalAPIModule(ReactApplicationContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext, JitsiMeetViewListener.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected JitsiMeetViewListener findListenerByExternalAPIScope(
|
||||||
|
String scope) {
|
||||||
|
// The JavaScript App needs to provide uniquely identifying information
|
||||||
|
// to the native ExternalAPI module so that the latter may match the
|
||||||
|
// former to the native JitsiMeetView which hosts it.
|
||||||
|
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
||||||
|
|
||||||
|
return view != null ? view.getListener() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,107 +85,15 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
|
||||||
* by/associated with the specified {@code name}.
|
* by/associated with the specified {@code name}.
|
||||||
* @param scope
|
* @param scope
|
||||||
*/
|
*/
|
||||||
|
@Override
|
||||||
@ReactMethod
|
@ReactMethod
|
||||||
public void sendEvent(final String name,
|
public void sendEvent(String name, ReadableMap data, String scope) {
|
||||||
final ReadableMap data,
|
|
||||||
final String scope) {
|
|
||||||
// The JavaScript App needs to provide uniquely identifying information
|
|
||||||
// to the native ExternalAPI module so that the latter may match the
|
|
||||||
// former to the native JitsiMeetView which hosts it.
|
|
||||||
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
||||||
|
|
||||||
if (view == null) {
|
if (view != null) {
|
||||||
return;
|
maybeSetViewURL(name, data, view);
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX The JitsiMeetView property URL was introduced in order to address
|
super.sendEvent(name, data, scope);
|
||||||
// an exception in the Picture-in-Picture functionality which arose
|
|
||||||
// because of delays related to bridging between JavaScript and Java. To
|
|
||||||
// reduce these delays do not wait for the call to be transfered to the
|
|
||||||
// UI thread.
|
|
||||||
maybeSetViewURL(name, data, view);
|
|
||||||
|
|
||||||
// Make sure JitsiMeetView's listener is invoked on the UI thread. It
|
|
||||||
// was requested by SDK consumers.
|
|
||||||
if (UiThreadUtil.isOnUiThread()) {
|
|
||||||
sendEventOnUiThread(name, data, scope);
|
|
||||||
} else {
|
|
||||||
UiThreadUtil.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
sendEventOnUiThread(name, data, scope);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
|
||||||
* the specified {@link JitsiMeetView}'s listener on the UI thread.
|
|
||||||
*
|
|
||||||
* @param name The name of the event.
|
|
||||||
* @param data The details/specifics of the event to send determined
|
|
||||||
* by/associated with the specified {@code name}.
|
|
||||||
* @param scope
|
|
||||||
*/
|
|
||||||
private void sendEventOnUiThread(final String name,
|
|
||||||
final ReadableMap data,
|
|
||||||
final String scope) {
|
|
||||||
// The JavaScript App needs to provide uniquely identifying information
|
|
||||||
// to the native ExternalAPI module so that the latter may match the
|
|
||||||
// former to the native JitsiMeetView which hosts it.
|
|
||||||
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
|
|
||||||
|
|
||||||
if (view == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
JitsiMeetViewListener listener = view.getListener();
|
|
||||||
|
|
||||||
if (listener == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Method method = JITSI_MEET_VIEW_LISTENER_METHODS.get(name);
|
|
||||||
|
|
||||||
if (method != null) {
|
|
||||||
try {
|
|
||||||
method.invoke(listener, toHashMap(data));
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
// FIXME There was a multicatch for IllegalAccessException and
|
|
||||||
// InvocationTargetException, but Android Studio complained
|
|
||||||
// with: "Multi-catch with these reflection exceptions requires
|
|
||||||
// API level 19 (current min is 16) because they get compiled to
|
|
||||||
// the common but new super type ReflectiveOperationException.
|
|
||||||
// As a workaround either create individual catch statements, or
|
|
||||||
// catch Exception."
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes a new {@code HashMap} instance with the key-value
|
|
||||||
* associations of a specific {@code ReadableMap}.
|
|
||||||
*
|
|
||||||
* @param readableMap the {@code ReadableMap} specifying the key-value
|
|
||||||
* associations with which the new {@code HashMap} instance is to be
|
|
||||||
* initialized.
|
|
||||||
* @return a new {@code HashMap} instance initialized with the key-value
|
|
||||||
* associations of the specified {@code readableMap}.
|
|
||||||
*/
|
|
||||||
private HashMap<String, Object> toHashMap(ReadableMap readableMap) {
|
|
||||||
HashMap<String, Object> hashMap = new HashMap<>();
|
|
||||||
|
|
||||||
for (ReadableMapKeySetIterator i = readableMap.keySetIterator();
|
|
||||||
i.hasNextKey();) {
|
|
||||||
String key = i.nextKey();
|
|
||||||
|
|
||||||
hashMap.put(key, readableMap.getString(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
return hashMap;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class ReactInstanceManagerHolder {
|
||||||
new PictureInPictureModule(reactContext),
|
new PictureInPictureModule(reactContext),
|
||||||
new ProximityModule(reactContext),
|
new ProximityModule(reactContext),
|
||||||
new WiFiStatsModule(reactContext),
|
new WiFiStatsModule(reactContext),
|
||||||
|
new org.jitsi.meet.sdk.incoming_call.IncomingCallExternalAPIModule(reactContext),
|
||||||
new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
|
new org.jitsi.meet.sdk.invite.InviteModule(reactContext),
|
||||||
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
|
new org.jitsi.meet.sdk.net.NAT64AddrInfoModule(reactContext)
|
||||||
);
|
);
|
||||||
|
@ -97,7 +98,7 @@ public class ReactInstanceManagerHolder {
|
||||||
? reactContext.getNativeModule(nativeModuleClass) : null;
|
? reactContext.getNativeModule(nativeModuleClass) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static ReactInstanceManager getReactInstanceManager() {
|
public static ReactInstanceManager getReactInstanceManager() {
|
||||||
return reactInstanceManager;
|
return reactInstanceManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ public class ReactInstanceManagerHolder {
|
||||||
*
|
*
|
||||||
* @param application {@code Application} instance which is running.
|
* @param application {@code Application} instance which is running.
|
||||||
*/
|
*/
|
||||||
static void initReactInstanceManager(Application application) {
|
public static void initReactInstanceManager(Application application) {
|
||||||
if (reactInstanceManager != null) {
|
if (reactInstanceManager != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -119,6 +120,7 @@ public class ReactInstanceManagerHolder {
|
||||||
.setApplication(application)
|
.setApplication(application)
|
||||||
.setBundleAssetName("index.android.bundle")
|
.setBundleAssetName("index.android.bundle")
|
||||||
.setJSMainModulePath("index.android")
|
.setJSMainModulePath("index.android")
|
||||||
|
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
|
||||||
.addPackage(new com.calendarevents.CalendarEventsPackage())
|
.addPackage(new com.calendarevents.CalendarEventsPackage())
|
||||||
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
|
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
|
||||||
.addPackage(new com.facebook.react.shell.MainReactPackage())
|
.addPackage(new com.facebook.react.shell.MainReactPackage())
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.incoming_call;
|
||||||
|
|
||||||
|
import com.facebook.react.bridge.ReactApplicationContext;
|
||||||
|
import com.facebook.react.bridge.ReactMethod;
|
||||||
|
import com.facebook.react.bridge.ReadableMap;
|
||||||
|
|
||||||
|
import org.jitsi.meet.sdk.AbstractExternalAPIModule;
|
||||||
|
|
||||||
|
public class IncomingCallExternalAPIModule
|
||||||
|
extends AbstractExternalAPIModule<IncomingCallViewListener> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a new {@code IncomingCallExternalAPIModule} instance. There
|
||||||
|
* shall be a single instance of this module throughout the lifetime of the
|
||||||
|
* application.
|
||||||
|
*
|
||||||
|
* @param reactContext The {@link ReactApplicationContext} where this module
|
||||||
|
* is created.
|
||||||
|
*/
|
||||||
|
public IncomingCallExternalAPIModule(ReactApplicationContext reactContext) {
|
||||||
|
super(reactContext, IncomingCallViewListener.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected IncomingCallViewListener findListenerByExternalAPIScope(
|
||||||
|
String scope) {
|
||||||
|
IncomingCallView view
|
||||||
|
= IncomingCallView.findViewByExternalAPIScope(scope);
|
||||||
|
|
||||||
|
return view != null ? view.getListener() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of this module to be used in the React Native bridge.
|
||||||
|
*
|
||||||
|
* @return The name of this module to be used in the React Native bridge.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "IncomingCallExternalAPI";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an event that occurred on the JavaScript side of the SDK to
|
||||||
|
* the specified {@link IncomingCallView}'s listener.
|
||||||
|
*
|
||||||
|
* @param name The name of the event.
|
||||||
|
* @param data The details/specifics of the event to send determined
|
||||||
|
* by/associated with the specified {@code name}.
|
||||||
|
* @param scope
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
@ReactMethod
|
||||||
|
public void sendEvent(String name, ReadableMap data, String scope) {
|
||||||
|
// XXX Overrides the super implementation to exposes it as a
|
||||||
|
// react-native module method to the JavaScript source code.
|
||||||
|
super.sendEvent(name, data, scope);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
* 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.incoming_call;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
public class IncomingCallInfo {
|
||||||
|
private final String callerAvatarUrl;
|
||||||
|
|
||||||
|
private final String callerName;
|
||||||
|
|
||||||
|
private final boolean hasVideo;
|
||||||
|
|
||||||
|
public IncomingCallInfo(
|
||||||
|
@NonNull String callerName,
|
||||||
|
@NonNull String callerAvatarUrl,
|
||||||
|
boolean hasVideo) {
|
||||||
|
this.callerName = callerName;
|
||||||
|
this.callerAvatarUrl = callerAvatarUrl;
|
||||||
|
this.hasVideo = hasVideo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCallerAvatarUrl() {
|
||||||
|
return callerAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCallerName() {
|
||||||
|
return callerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasVideo() {
|
||||||
|
return hasVideo;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
* 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.incoming_call;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import com.facebook.react.ReactRootView;
|
||||||
|
|
||||||
|
import org.jitsi.meet.sdk.ReactInstanceManagerHolder;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
public class IncomingCallView extends FrameLayout {
|
||||||
|
private static final int BACKGROUND_COLOR = 0xFF111111;
|
||||||
|
|
||||||
|
private static final Set<IncomingCallView> views
|
||||||
|
= Collections.newSetFromMap(new WeakHashMap<IncomingCallView, Boolean>());
|
||||||
|
|
||||||
|
public static IncomingCallView findViewByExternalAPIScope(
|
||||||
|
String externalAPIScope) {
|
||||||
|
synchronized (views) {
|
||||||
|
for (IncomingCallView view : views) {
|
||||||
|
if (view.externalAPIScope.equals(externalAPIScope)) {
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String externalAPIScope;
|
||||||
|
private IncomingCallViewListener listener;
|
||||||
|
private ReactRootView reactRootView;
|
||||||
|
|
||||||
|
public IncomingCallView(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
ReactInstanceManagerHolder.initReactInstanceManager(((Activity) context).getApplication());
|
||||||
|
|
||||||
|
setBackgroundColor(BACKGROUND_COLOR);
|
||||||
|
externalAPIScope = UUID.randomUUID().toString();
|
||||||
|
synchronized (views) {
|
||||||
|
views.add(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dispose() {
|
||||||
|
if (reactRootView != null) {
|
||||||
|
removeView(reactRootView);
|
||||||
|
reactRootView.unmountReactApplication();
|
||||||
|
reactRootView = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IncomingCallViewListener getListener() {
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(IncomingCallViewListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void loadIncomingCallInfo(IncomingCallInfo callInfo) {
|
||||||
|
Bundle props = new Bundle();
|
||||||
|
|
||||||
|
props.putString("externalAPIScope", externalAPIScope);
|
||||||
|
props.putString("url", "");
|
||||||
|
props.putString("callerName", callInfo.getCallerName());
|
||||||
|
props.putString("callerAvatarUrl", callInfo.getCallerAvatarUrl());
|
||||||
|
props.putString("hasVideo", String.valueOf(callInfo.hasVideo()));
|
||||||
|
if (reactRootView == null) {
|
||||||
|
reactRootView = new ReactRootView(getContext());
|
||||||
|
reactRootView.startReactApplication(
|
||||||
|
ReactInstanceManagerHolder.getReactInstanceManager(),
|
||||||
|
"IncomingCallApp",
|
||||||
|
props);
|
||||||
|
reactRootView.setBackgroundColor(BACKGROUND_COLOR);
|
||||||
|
addView(reactRootView);
|
||||||
|
} else {
|
||||||
|
reactRootView.setAppProperties(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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.incoming_call;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface IncomingCallViewListener {
|
||||||
|
void onIncomingCallAnswered(Map<String, Object> data);
|
||||||
|
|
||||||
|
void onIncomingCallDeclined(Map<String, Object> data);
|
||||||
|
}
|
|
@ -9,6 +9,8 @@ include ':react-native-immersive'
|
||||||
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
|
||||||
include ':react-native-keep-awake'
|
include ':react-native-keep-awake'
|
||||||
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
|
project(':react-native-keep-awake').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-keep-awake/android')
|
||||||
|
include ':react-native-linear-gradient'
|
||||||
|
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
|
||||||
include ':react-native-locale-detector'
|
include ':react-native-locale-detector'
|
||||||
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
|
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
|
||||||
include ':react-native-sound'
|
include ':react-native-sound'
|
||||||
|
|
|
@ -643,5 +643,12 @@
|
||||||
"rejected": "Rejected",
|
"rejected": "Rejected",
|
||||||
"ignored": "Ignored",
|
"ignored": "Ignored",
|
||||||
"expired": "Expired"
|
"expired": "Expired"
|
||||||
|
},
|
||||||
|
"incomingCall": {
|
||||||
|
"answer": "Answer",
|
||||||
|
"audioCallTitle": "Incoming call",
|
||||||
|
"decline": "Dismiss",
|
||||||
|
"productLabel": "from Jitsi Meet",
|
||||||
|
"videoCallTitle": "Incoming video call"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12770,6 +12770,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-2.0.6.tgz",
|
||||||
"integrity": "sha512-ketZKC6G49W4iblKYCnIA5Tcx78Yu48n/K5XzZUnMm69wAnZxs1054Re2V5xpSwX5VZasOBjW1iI1cTjtB/H5g=="
|
"integrity": "sha512-ketZKC6G49W4iblKYCnIA5Tcx78Yu48n/K5XzZUnMm69wAnZxs1054Re2V5xpSwX5VZasOBjW1iI1cTjtB/H5g=="
|
||||||
},
|
},
|
||||||
|
"react-native-linear-gradient": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-h4nwmcjfeedSiHGBmQkMmCSIqm3196YtT1AtbAqE93jgAcpib0btvoCx8nBUemmhfm+CA5mFEh8p5biA4wFw/A==",
|
||||||
|
"requires": {
|
||||||
|
"prop-types": "^15.5.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-native-locale-detector": {
|
"react-native-locale-detector": {
|
||||||
"version": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
|
"version": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
|
||||||
"from": "react-native-locale-detector@github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"
|
"from": "react-native-locale-detector@github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"react-native-immersive": "1.1.0",
|
"react-native-immersive": "1.1.0",
|
||||||
"react-native-keep-awake": "2.0.6",
|
"react-native-keep-awake": "2.0.6",
|
||||||
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
|
"react-native-locale-detector": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
|
||||||
|
"react-native-linear-gradient": "2.4.0",
|
||||||
"react-native-prompt": "1.0.0",
|
"react-native-prompt": "1.0.0",
|
||||||
"react-native-sound": "0.10.9",
|
"react-native-sound": "0.10.9",
|
||||||
"react-native-vector-icons": "4.4.2",
|
"react-native-vector-icons": "4.4.2",
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* The type of redux action to receive an incoming call.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INCOMING_CALL_RECEIVED,
|
||||||
|
* caller: Object
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INCOMING_CALL_RECEIVED = Symbol('INCOMING_CALL_RECEIVED');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of redux action to answer an incoming call.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INCOMING_CALL_ANSWERED,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INCOMING_CALL_ANSWERED = Symbol('INCOMING_CALL_ANSWERED');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of redux action to decline an incoming call.
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* type: INCOMING_CALL_DECLINED,
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const INCOMING_CALL_DECLINED = Symbol('INCOMING_CALL_DECLINED');
|
|
@ -0,0 +1,49 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import {
|
||||||
|
INCOMING_CALL_RECEIVED,
|
||||||
|
INCOMING_CALL_ANSWERED,
|
||||||
|
INCOMING_CALL_DECLINED
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a received incoming call.
|
||||||
|
*
|
||||||
|
* @param {Object} caller - The caller of an incoming call.
|
||||||
|
* @returns {{
|
||||||
|
* type: INCOMING_CALL_RECEIVED,
|
||||||
|
* caller: Object
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function incomingCallReceived(caller: Object) {
|
||||||
|
return {
|
||||||
|
type: INCOMING_CALL_RECEIVED,
|
||||||
|
caller
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Answers a received incoming call.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: INCOMING_CALL_ANSWERED
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function incomingCallAnswered() {
|
||||||
|
return {
|
||||||
|
type: INCOMING_CALL_ANSWERED
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declines a received incoming call.
|
||||||
|
*
|
||||||
|
* @returns {{
|
||||||
|
* type: INCOMING_CALL_DECLINED
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
export function incomingCallDeclined() {
|
||||||
|
return {
|
||||||
|
type: INCOMING_CALL_DECLINED
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { AbstractButton } from '../../../base/toolbox';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import type { AbstractButtonProps } from '../../../base/toolbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of a button which accepts an incoming call.
|
||||||
|
*/
|
||||||
|
class AnswerButton extends AbstractButton<AbstractButtonProps, *> {
|
||||||
|
accessibilityLabel = 'incomingCall.answer';
|
||||||
|
iconName = 'hangup';
|
||||||
|
label = 'incomingCall.answer';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(AnswerButton);
|
|
@ -0,0 +1,16 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { AbstractButton } from '../../../base/toolbox';
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import type { AbstractButtonProps } from '../../../base/toolbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of a button which rejects an incoming call.
|
||||||
|
*/
|
||||||
|
class DeclineButton extends AbstractButton<AbstractButtonProps, *> {
|
||||||
|
accessibilityLabel = 'incomingCall.decline';
|
||||||
|
iconName = 'hangup';
|
||||||
|
label = 'incomingCall.decline';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(DeclineButton);
|
|
@ -0,0 +1,54 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { App } from '../../../app';
|
||||||
|
import { RouteRegistry } from '../../../base/react';
|
||||||
|
import { incomingCallReceived } from '../actions';
|
||||||
|
|
||||||
|
import IncomingCallPage from './IncomingCallPage';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root application component for incoming call.
|
||||||
|
*
|
||||||
|
* @extends App
|
||||||
|
*/
|
||||||
|
export default class IncomingCallApp extends App {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
...App.propTypes,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the UI should tell if it's a video call or
|
||||||
|
* a regular audio call.
|
||||||
|
*/
|
||||||
|
hasVideo: PropTypes.bool
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates incoming call when component is going to be mounted.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
componentWillMount() {
|
||||||
|
super.componentWillMount();
|
||||||
|
this._init.then(() => {
|
||||||
|
const { dispatch } = this._getStore();
|
||||||
|
|
||||||
|
dispatch(incomingCallReceived({
|
||||||
|
name: this.props.callerName,
|
||||||
|
avatarUrl: this.props.callerAvatarUrl,
|
||||||
|
hasVideo: this.props.hasVideo === 'true'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to {@code IncomingCallPage}.
|
||||||
|
*
|
||||||
|
* @param {Object|string} url - Ingored.
|
||||||
|
* @protected
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_openURL(url) { // eslint-disable-line no-unused-vars
|
||||||
|
this._navigate(RouteRegistry.getRouteByComponent(IncomingCallPage));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,207 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { Image, Text, View } from 'react-native';
|
||||||
|
import LinearGradient from 'react-native-linear-gradient';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import type { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { translate } from '../../../base/i18n';
|
||||||
|
import { Avatar } from '../../../base/participants';
|
||||||
|
|
||||||
|
import {
|
||||||
|
incomingCallAnswered,
|
||||||
|
incomingCallDeclined
|
||||||
|
} from '../actions';
|
||||||
|
import styles, {
|
||||||
|
AVATAR_BORDER_GRADIENT,
|
||||||
|
BACKGROUND_OVERLAY_GRADIENT,
|
||||||
|
CALLER_AVATAR_SIZE
|
||||||
|
} from './styles';
|
||||||
|
import AnswerButton from './AnswerButton';
|
||||||
|
import DeclineButton from './DeclineButton';
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
_callerName: string,
|
||||||
|
_callerAvatarUrl: string,
|
||||||
|
_hasVideo: boolean,
|
||||||
|
_onAnswered: Function,
|
||||||
|
_onDeclined: Function,
|
||||||
|
t: Function
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The React {@code Component} displays an incoming call screen.
|
||||||
|
*/
|
||||||
|
class IncomingCallPage extends Component<Props> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements React's {@link Component#render()}.
|
||||||
|
*
|
||||||
|
* @inheritdoc
|
||||||
|
* @returns {ReactElement}
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const { t, _callerName, _hasVideo } = this.props;
|
||||||
|
const callTitle
|
||||||
|
= _hasVideo
|
||||||
|
? t('incomingCall.videoCallTitle')
|
||||||
|
: t('incomingCall.audioCallTitle');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.pageContainer }>
|
||||||
|
<View style = { styles.backgroundAvatar }>
|
||||||
|
<Image
|
||||||
|
source = {{ uri: this.props._callerAvatarUrl }}
|
||||||
|
style = { styles.backgroundAvatarImage } />
|
||||||
|
</View>
|
||||||
|
<LinearGradient
|
||||||
|
colors = { BACKGROUND_OVERLAY_GRADIENT }
|
||||||
|
style = { styles.backgroundOverlayGradient } />
|
||||||
|
<Text style = { styles.title }>
|
||||||
|
{ callTitle }
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
numberOfLines = { 6 }
|
||||||
|
style = { styles.callerName } >
|
||||||
|
{ _callerName }
|
||||||
|
</Text>
|
||||||
|
<Text style = { styles.productLabel }>
|
||||||
|
{ t('incomingCall.productLabel') }
|
||||||
|
</Text>
|
||||||
|
{ this._renderCallerAvatar() }
|
||||||
|
{ this._renderButtons() }
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders caller avatar.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {React$Node}
|
||||||
|
*/
|
||||||
|
_renderCallerAvatar() {
|
||||||
|
return (
|
||||||
|
<View style = { styles.avatarContainer }>
|
||||||
|
<LinearGradient
|
||||||
|
colors = { AVATAR_BORDER_GRADIENT }
|
||||||
|
style = { styles.avatarBorder } />
|
||||||
|
<View style = { styles.avatar }>
|
||||||
|
<Avatar
|
||||||
|
size = { CALLER_AVATAR_SIZE }
|
||||||
|
uri = { this.props._callerAvatarUrl } />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders buttons.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {React$Node}
|
||||||
|
*/
|
||||||
|
_renderButtons() {
|
||||||
|
const { t, _onAnswered, _onDeclined } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style = { styles.buttonsContainer }>
|
||||||
|
<View style = { styles.buttonWrapper } >
|
||||||
|
<DeclineButton
|
||||||
|
onClick = { _onDeclined }
|
||||||
|
styles = { styles.declineButtonStyles } />
|
||||||
|
<Text style = { styles.buttonText }>
|
||||||
|
{ t('incomingCall.decline') }
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style = { styles.buttonWrapper }>
|
||||||
|
<AnswerButton
|
||||||
|
onClick = { _onAnswered }
|
||||||
|
styles = { styles.answerButtonStyles } />
|
||||||
|
<Text style = { styles.buttonText }>
|
||||||
|
{ t('incomingCall.answer') }
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps dispatching of some action to React component props.
|
||||||
|
*
|
||||||
|
* @param {Function} dispatch - Redux action dispatcher.
|
||||||
|
* @private
|
||||||
|
* @returns {{
|
||||||
|
* _onAnswered: Function,
|
||||||
|
* _onDeclined: Function
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _mapDispatchToProps(dispatch: Dispatch<*>) {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Dispatches an action to answer an incoming call.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onAnswered() {
|
||||||
|
dispatch(incomingCallAnswered());
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an action to decline an incoming call.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
_onDeclined() {
|
||||||
|
dispatch(incomingCallDeclined());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps (parts of) the redux state to the component's props.
|
||||||
|
*
|
||||||
|
* @param {Object} state - The redux state.
|
||||||
|
* @param {Object} ownProps - The component's own props.
|
||||||
|
* @private
|
||||||
|
* @returns {{
|
||||||
|
* _callerName: string,
|
||||||
|
* _callerAvatarUrl: string
|
||||||
|
* }}
|
||||||
|
*/
|
||||||
|
function _mapStateToProps(state) {
|
||||||
|
const { caller } = state['features/mobile/incoming-call'] || {};
|
||||||
|
|
||||||
|
return (caller && {
|
||||||
|
/**
|
||||||
|
* The caller's name.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
_callerName: caller.name,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The caller's avatar url.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
_callerAvatarUrl: caller.avatarUrl,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if it's an audio or a video call.
|
||||||
|
*/
|
||||||
|
_hasVideo: caller.hasVideo
|
||||||
|
|
||||||
|
}) || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate(
|
||||||
|
connect(_mapStateToProps, _mapDispatchToProps)(IncomingCallPage));
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as IncomingCallPage } from './IncomingCallPage';
|
||||||
|
export { default as IncomingCallApp } from './IncomingCallApp';
|
|
@ -0,0 +1,149 @@
|
||||||
|
import {
|
||||||
|
ColorPalette,
|
||||||
|
createStyleSheet
|
||||||
|
} from '../../../base/styles';
|
||||||
|
|
||||||
|
export const AVATAR_BORDER_GRADIENT = [ '#4C9AFF', '#0052CC' ];
|
||||||
|
|
||||||
|
export const BACKGROUND_OVERLAY_GRADIENT = [ '#0052CC', '#4C9AFF' ];
|
||||||
|
|
||||||
|
const BUTTON_SIZE = 56;
|
||||||
|
|
||||||
|
export const CALLER_AVATAR_SIZE = 128;
|
||||||
|
|
||||||
|
const CALLER_AVATAR_BORDER_WIDTH = 3;
|
||||||
|
|
||||||
|
const CALLER_AVATAR_CIRCLE_SIZE
|
||||||
|
= CALLER_AVATAR_SIZE + (2 * CALLER_AVATAR_BORDER_WIDTH);
|
||||||
|
|
||||||
|
const PAGE_PADDING = 48;
|
||||||
|
|
||||||
|
const LINE_SPACING = 8;
|
||||||
|
|
||||||
|
const _icon = {
|
||||||
|
alignSelf: 'center',
|
||||||
|
color: ColorPalette.white,
|
||||||
|
fontSize: 32
|
||||||
|
};
|
||||||
|
|
||||||
|
const _responseButton = {
|
||||||
|
alignSelf: 'center',
|
||||||
|
borderRadius: BUTTON_SIZE / 2,
|
||||||
|
borderWidth: 0,
|
||||||
|
flex: 0,
|
||||||
|
flexDirection: 'row',
|
||||||
|
height: BUTTON_SIZE,
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: BUTTON_SIZE
|
||||||
|
};
|
||||||
|
|
||||||
|
const _text = {
|
||||||
|
color: ColorPalette.white,
|
||||||
|
fontSize: 16
|
||||||
|
};
|
||||||
|
|
||||||
|
export default createStyleSheet({
|
||||||
|
answerButtonStyles: {
|
||||||
|
iconStyle: {
|
||||||
|
..._icon,
|
||||||
|
transform: [
|
||||||
|
{ rotateZ: '130deg' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
..._responseButton,
|
||||||
|
backgroundColor: ColorPalette.green
|
||||||
|
},
|
||||||
|
underlayColor: ColorPalette.buttonUnderlay
|
||||||
|
},
|
||||||
|
|
||||||
|
avatar: {
|
||||||
|
position: 'absolute',
|
||||||
|
marginLeft: CALLER_AVATAR_BORDER_WIDTH,
|
||||||
|
marginTop: CALLER_AVATAR_BORDER_WIDTH
|
||||||
|
},
|
||||||
|
|
||||||
|
avatarBorder: {
|
||||||
|
borderRadius: CALLER_AVATAR_CIRCLE_SIZE / 2,
|
||||||
|
height: CALLER_AVATAR_CIRCLE_SIZE,
|
||||||
|
position: 'absolute',
|
||||||
|
width: CALLER_AVATAR_CIRCLE_SIZE
|
||||||
|
},
|
||||||
|
|
||||||
|
avatarContainer: {
|
||||||
|
height: CALLER_AVATAR_CIRCLE_SIZE,
|
||||||
|
marginTop: LINE_SPACING * 4,
|
||||||
|
width: CALLER_AVATAR_CIRCLE_SIZE
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundAvatar: {
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundAvatarImage: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundOverlayGradient: {
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
opacity: 0.9,
|
||||||
|
position: 'absolute',
|
||||||
|
right: 0,
|
||||||
|
top: 0
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonsContainer: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row'
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonText: {
|
||||||
|
..._text,
|
||||||
|
alignSelf: 'center',
|
||||||
|
marginTop: 1.5 * LINE_SPACING
|
||||||
|
},
|
||||||
|
|
||||||
|
buttonWrapper: {
|
||||||
|
flex: 1
|
||||||
|
},
|
||||||
|
|
||||||
|
callerName: {
|
||||||
|
..._text,
|
||||||
|
fontSize: 36,
|
||||||
|
marginBottom: LINE_SPACING,
|
||||||
|
marginLeft: PAGE_PADDING,
|
||||||
|
marginRight: PAGE_PADDING,
|
||||||
|
marginTop: LINE_SPACING,
|
||||||
|
textAlign: 'center'
|
||||||
|
},
|
||||||
|
|
||||||
|
declineButtonStyles: {
|
||||||
|
iconStyle: _icon,
|
||||||
|
style: {
|
||||||
|
..._responseButton,
|
||||||
|
backgroundColor: ColorPalette.red
|
||||||
|
},
|
||||||
|
underlayColor: ColorPalette.buttonUnderlay
|
||||||
|
},
|
||||||
|
|
||||||
|
pageContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
paddingBottom: PAGE_PADDING,
|
||||||
|
paddingTop: PAGE_PADDING
|
||||||
|
},
|
||||||
|
|
||||||
|
productLabel: {
|
||||||
|
..._text
|
||||||
|
},
|
||||||
|
|
||||||
|
title: {
|
||||||
|
..._text
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,5 @@
|
||||||
|
export * from './components';
|
||||||
|
|
||||||
|
import './middleware';
|
||||||
|
import './route';
|
||||||
|
import './reducer';
|
|
@ -0,0 +1,58 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
import { NativeModules } from 'react-native';
|
||||||
|
|
||||||
|
import { MiddlewareRegistry } from '../../base/redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
INCOMING_CALL_ANSWERED,
|
||||||
|
INCOMING_CALL_DECLINED
|
||||||
|
} from './actionTypes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware that captures Redux actions and uses the IncomingCallExternalAPI
|
||||||
|
* module to turn them into native events so the application knows about them.
|
||||||
|
*
|
||||||
|
* @param {Store} store - The redux store.
|
||||||
|
* @returns {Function}
|
||||||
|
*/
|
||||||
|
MiddlewareRegistry.register(store => next => action => {
|
||||||
|
switch (action.type) {
|
||||||
|
case INCOMING_CALL_ANSWERED:
|
||||||
|
_sendEvent(store, 'INCOMING_CALL_ANSWERED', /* data */ {});
|
||||||
|
break;
|
||||||
|
case INCOMING_CALL_DECLINED:
|
||||||
|
_sendEvent(store, 'INCOMING_CALL_DECLINED', /* data */ {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a specific event to the native counterpart of the External API. Native
|
||||||
|
* apps may listen to such events via the mechanisms provided by the (native)
|
||||||
|
* mobile Jitsi Meet SDK.
|
||||||
|
*
|
||||||
|
* @param {Object} store - The redux store.
|
||||||
|
* @param {string} name - The name of the event to send.
|
||||||
|
* @param {Object} data - The details/specifics of the event to send determined
|
||||||
|
* by/associated with the specified {@code name}.
|
||||||
|
* @private
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function _sendEvent(
|
||||||
|
{ getState }: { getState: Function },
|
||||||
|
name: string,
|
||||||
|
data: Object) {
|
||||||
|
const { app } = getState()['features/app'];
|
||||||
|
|
||||||
|
if (app) {
|
||||||
|
const { externalAPIScope } = app.props;
|
||||||
|
|
||||||
|
if (externalAPIScope) {
|
||||||
|
NativeModules
|
||||||
|
.IncomingCallExternalAPI.sendEvent(name, data, externalAPIScope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import { assign, ReducerRegistry } from '../../base/redux';
|
||||||
|
|
||||||
|
import { INCOMING_CALL_RECEIVED } from './actionTypes';
|
||||||
|
|
||||||
|
ReducerRegistry.register(
|
||||||
|
'features/mobile/incoming-call',
|
||||||
|
(state = {}, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case INCOMING_CALL_RECEIVED:
|
||||||
|
return assign(state, {
|
||||||
|
caller: action.caller
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
/* @flow */
|
||||||
|
|
||||||
|
import { RouteRegistry } from '../../base/react';
|
||||||
|
|
||||||
|
import { IncomingCallPage } from './components';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register route for {@code IncomingCallPage}.
|
||||||
|
*/
|
||||||
|
RouteRegistry.register({
|
||||||
|
component: IncomingCallPage,
|
||||||
|
path: '/:incoming-call'
|
||||||
|
});
|
|
@ -18,6 +18,7 @@ import { AppRegistry, Linking, NativeModules } from 'react-native';
|
||||||
|
|
||||||
import { App } from './features/app';
|
import { App } from './features/app';
|
||||||
import { equals } from './features/base/redux';
|
import { equals } from './features/base/redux';
|
||||||
|
import { IncomingCallApp } from './features/mobile/incoming-call';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React Native doesn't support specifying props to the main/root component (in
|
* React Native doesn't support specifying props to the main/root component (in
|
||||||
|
@ -159,3 +160,6 @@ class Root extends Component {
|
||||||
|
|
||||||
// Register the main/root Component.
|
// Register the main/root Component.
|
||||||
AppRegistry.registerComponent('App', () => Root);
|
AppRegistry.registerComponent('App', () => Root);
|
||||||
|
|
||||||
|
// Register the incoming call Component.
|
||||||
|
AppRegistry.registerComponent('IncomingCallApp', () => IncomingCallApp);
|
||||||
|
|
Loading…
Reference in New Issue