Initial commit for incoming call screen.

This commit is contained in:
Shuai Li 2018-05-03 16:26:03 -07:00 committed by Saúl Ibarra Corretgé
parent 3f3a957f40
commit 45c2a657af
18 changed files with 703 additions and 178 deletions

View File

@ -0,0 +1,180 @@
/*
* 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;
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 module 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.
*/
AbstractExternalAPIModule(ReactApplicationContext reactContext,
Class<T> listenerClass) {
super(reactContext);
this.methodMap = createAPIMethodMap(listenerClass);
}
/**
* 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
*/
@ReactMethod
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);
}
}
}
public abstract T findListenerByExternalAPIScope(String scope);
/**
* 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,16 @@ 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.
*
* @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);
}
/**
@ -139,6 +62,16 @@ class ExternalAPIModule extends ReactContextBaseJavaModule {
}
}
@Override
public 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;
}
/**
* Dispatches an event that occurred on the JavaScript side of the SDK to
* the specified {@link JitsiMeetView}'s listener.
@ -148,107 +81,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

@ -0,0 +1,34 @@
package org.jitsi.meet.sdk;
import com.facebook.react.bridge.ReactApplicationContext;
class IncomingCallExternalAPIModule extends AbstractExternalAPIModule<IncomingCallViewListener> {
/**
* Initializes a new module 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);
}
/**
* 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";
}
@Override
public IncomingCallViewListener findListenerByExternalAPIScope(String scope) {
IncomingCallView view = IncomingCallView.findViewByExternalAPIScope(scope);
return view != null ? view.getListener() : null;
}
}

View File

@ -0,0 +1,105 @@
package org.jitsi.meet.sdk;
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 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;
}
public static final class IncomingCallInfo {
private final String callerName;
private final String callerAvatarUrl;
public IncomingCallInfo(@NonNull String callerName, @NonNull String callerAvatarUrl) {
this.callerName = callerName;
this.callerAvatarUrl = callerAvatarUrl;
}
public String getCallerName() {
return callerName;
}
public String getCallerAvatarUrl() {
return callerAvatarUrl;
}
}
private final String externalAPIScope;
private IncomingCallViewListener listener;
private ReactRootView reactRootView;
public IncomingCallView(@NonNull Context context) {
super(context);
if (ReactInstanceManagerHolder.getReactInstanceManager() == null) {
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());
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,9 @@
package org.jitsi.meet.sdk;
import java.util.Map;
public interface IncomingCallViewListener {
void onIncomingCallAnswered(Map<String, Object> data);
void onIncomingCallDeclined(Map<String, Object> data);
}

View File

@ -42,6 +42,7 @@ public class ReactInstanceManagerHolder {
new AppInfoModule(reactContext),
new AudioModeModule(reactContext),
new ExternalAPIModule(reactContext),
new IncomingCallExternalAPIModule(reactContext),
new PictureInPictureModule(reactContext),
new ProximityModule(reactContext),
new WiFiStatsModule(reactContext),

View File

@ -643,5 +643,10 @@
"rejected": "Rejected",
"ignored": "Ignored",
"expired": "Expired"
},
"incomingCall": {
"title": "Incoming call",
"answer": "Answer",
"decline": "Decline"
}
}

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,41 @@
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 {
/**
* 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
}));
});
}
/**
* 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,119 @@
// @flow
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { connect } from 'react-redux';
import { translate } from '../../../base/i18n';
import {
incomingCallAnswered,
incomingCallDeclined
} from '../actions';
type Props = {
_callerName: string,
_callerAvatarUrl: string,
_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 } = this.props;
return ( // TODO: layout and styles
<View>
<Text>{ this.props._callerName }</Text>
<Text onPress = { this.props._onAnswered }>
{ t('incomingCall.answer') }
</Text>
<Text onPress = { this.props._onDeclined }>
{ t('incomingCall.decline') }
</Text>
</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) {
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
}) || {};
}
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,14 @@
import {
BoxModel,
ColorPalette,
createStyleSheet
} from '../../../base/styles';
const TEXT_COLOR = ColorPalette.white;
export default createStyleSheet({
button: {
color: TEXT_COLOR,
margin: BoxModel.margin
}
});

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