Compare commits

...

6 Commits

Author SHA1 Message Date
paweldomas c12c945d1a ref(main.json): change 'Decline' to 'Dismiss' 2018-07-13 14:22:37 -05:00
paweldomas 8e514dc07e feat(android|incoming call view): styling and 'hasVideo'
Uses 'react-native-linear-gradient' for doing gradients on Android
(it's not hooked up for the iOS as it's not used there yet).

Adds 'hasVideo' to the IncomingCallInfo and IncomingCallApp's properties
to toggle between "incoming call" and "incoming video call" labels.
2018-07-13 13:11:17 -05:00
Saúl Ibarra Corretgé bdeb56d5f8 WIP 2018-07-06 14:11:09 +02:00
Lyubo Marinov 4906453cb9 [Android] Coding style: comments, consistency, formatting, sorting order
Moves all IncomingCallXXX classes in the package incoming_call in order
to not explode the package org.jitsi.meet.sdk, to be consistent with the
JavaScript source code (but, unfortunately, incoming-call cannot be used
as a Java package name), and bring clarity to which are the "incoming
call"-related Java classes/APIs.
2018-07-06 14:03:48 +02:00
Shuai Li 102d4237a5 Adds styles. 2018-07-06 14:03:48 +02:00
Shuai Li 45c2a657af Initial commit for incoming call screen. 2018-07-06 14:03:48 +02:00
25 changed files with 1100 additions and 181 deletions

View File

@ -28,6 +28,7 @@ dependencies {
compile project(':react-native-fetch-blob')
compile project(':react-native-immersive')
compile project(':react-native-keep-awake')
compile project(':react-native-linear-gradient')
compile project(':react-native-locale-detector')
compile project(':react-native-sound')
compile project(':react-native-vector-icons')

View File

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

View File

@ -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;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
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
@ -37,66 +10,30 @@ import java.util.regex.Pattern;
* object it will dim the screen and disable touch controls. The functionality
* is used with the conference audio-only mode.
*/
class ExternalAPIModule extends ReactContextBaseJavaModule {
/**
* 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);
}
}
class ExternalAPIModule
extends AbstractExternalAPIModule<JitsiMeetViewListener> {
/**
* Initializes a new module instance. There shall be a single instance of
* this module throughout the lifetime of the application.
* Initializes a new {@code ExternalAPIModule} 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.
* @param reactContext the {@link ReactApplicationContext} where this module
* is created.
*/
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}.
* @param scope
*/
@Override
@ReactMethod
public void sendEvent(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.
public void sendEvent(String name, ReadableMap data, String scope) {
JitsiMeetView view = JitsiMeetView.findViewByExternalAPIScope(scope);
if (view == null) {
return;
if (view != null) {
maybeSetViewURL(name, data, view);
}
// XXX The JitsiMeetView property URL was introduced in order to address
// 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;
super.sendEvent(name, data, scope);
}
}

View File

@ -45,6 +45,7 @@ public class ReactInstanceManagerHolder {
new PictureInPictureModule(reactContext),
new ProximityModule(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.net.NAT64AddrInfoModule(reactContext)
);
@ -97,7 +98,7 @@ public class ReactInstanceManagerHolder {
? reactContext.getNativeModule(nativeModuleClass) : null;
}
static ReactInstanceManager getReactInstanceManager() {
public static ReactInstanceManager getReactInstanceManager() {
return reactInstanceManager;
}
@ -109,7 +110,7 @@ public class ReactInstanceManagerHolder {
*
* @param application {@code Application} instance which is running.
*/
static void initReactInstanceManager(Application application) {
public static void initReactInstanceManager(Application application) {
if (reactInstanceManager != null) {
return;
}
@ -119,6 +120,7 @@ public class ReactInstanceManagerHolder {
.setApplication(application)
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index.android")
.addPackage(new com.BV.LinearGradient.LinearGradientPackage())
.addPackage(new com.calendarevents.CalendarEventsPackage())
.addPackage(new com.corbt.keepawake.KCKeepAwakePackage())
.addPackage(new com.facebook.react.shell.MainReactPackage())

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,8 @@ include ':react-native-immersive'
project(':react-native-immersive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-immersive/android')
include ':react-native-keep-awake'
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'
project(':react-native-locale-detector').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-locale-detector/android')
include ':react-native-sound'

View File

@ -643,5 +643,12 @@
"rejected": "Rejected",
"ignored": "Ignored",
"expired": "Expired"
},
"incomingCall": {
"answer": "Answer",
"audioCallTitle": "Incoming call",
"decline": "Dismiss",
"productLabel": "from Jitsi Meet",
"videoCallTitle": "Incoming video call"
}
}

8
package-lock.json generated
View File

@ -12770,6 +12770,14 @@
"resolved": "https://registry.npmjs.org/react-native-keep-awake/-/react-native-keep-awake-2.0.6.tgz",
"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": {
"version": "github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9",
"from": "react-native-locale-detector@github:jitsi/react-native-locale-detector#845281e9fd4af756f6d0f64afe5cce08c63e5ee9"

View File

@ -64,6 +64,7 @@
"react-native-immersive": "1.1.0",
"react-native-keep-awake": "2.0.6",
"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-sound": "0.10.9",
"react-native-vector-icons": "4.4.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export { default as IncomingCallPage } from './IncomingCallPage';
export { default as IncomingCallApp } from './IncomingCallApp';

View File

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

View File

@ -0,0 +1,5 @@
export * from './components';
import './middleware';
import './route';
import './reducer';

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import { AppRegistry, Linking, NativeModules } from 'react-native';
import { App } from './features/app';
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
@ -159,3 +160,6 @@ class Root extends Component {
// Register the main/root Component.
AppRegistry.registerComponent('App', () => Root);
// Register the incoming call Component.
AppRegistry.registerComponent('IncomingCallApp', () => IncomingCallApp);