mirror of https://github.com/jitsi/jitsi-meet
It's a separate view (on the native side) and app (on the JavaScript side) so applications can use it independently. Co-authored-by: Shuai Li <sli@atlassian.com> Co-authored-by: Pawel Domas <pawel.domas@jitsi.org>pull/3260/head
parent
39e236a42c
commit
ea22d12581
@ -0,0 +1,69 @@ |
||||
/* |
||||
* 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 { |
||||
/** |
||||
* URL for the caller avatar. |
||||
*/ |
||||
private final String callerAvatarUrl; |
||||
|
||||
/** |
||||
* Caller's name. |
||||
*/ |
||||
private final String callerName; |
||||
|
||||
/** |
||||
* Whether this is a regular call or a video call. |
||||
*/ |
||||
private final boolean hasVideo; |
||||
|
||||
public IncomingCallInfo( |
||||
@NonNull String callerName, |
||||
@NonNull String callerAvatarUrl, |
||||
boolean hasVideo) { |
||||
this.callerName = callerName; |
||||
this.callerAvatarUrl = callerAvatarUrl; |
||||
this.hasVideo = hasVideo; |
||||
} |
||||
|
||||
/** |
||||
* Gets the caller's avatar URL. |
||||
* @return - The URL as a string. |
||||
*/ |
||||
public String getCallerAvatarUrl() { |
||||
return callerAvatarUrl; |
||||
} |
||||
|
||||
/** |
||||
* Gets the caller's name. |
||||
* @return - The caller's name. |
||||
*/ |
||||
public String getCallerName() { |
||||
return callerName; |
||||
} |
||||
|
||||
/** |
||||
* Gets whether the call is a video call or not. |
||||
* @return - True if this call has video, false otherwise. |
||||
*/ |
||||
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.content.Context; |
||||
import android.os.Bundle; |
||||
import android.support.annotation.NonNull; |
||||
|
||||
import com.facebook.react.bridge.ReadableMap; |
||||
|
||||
import org.jitsi.meet.sdk.BaseReactView; |
||||
import org.jitsi.meet.sdk.ListenerUtils; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Map; |
||||
|
||||
|
||||
public class IncomingCallView extends BaseReactView { |
||||
/** |
||||
* The {@code Method}s of {@code JitsiMeetViewListener} by event name i.e. |
||||
* redux action types. |
||||
*/ |
||||
private static final Map<String, Method> LISTENER_METHODS |
||||
= ListenerUtils.slurpListenerMethods(IncomingCallViewListener.class); |
||||
|
||||
/** |
||||
* {@link IncomingCallViewListener} instance for reporting events occurring |
||||
* in Jitsi Meet. |
||||
*/ |
||||
private IncomingCallViewListener listener; |
||||
|
||||
public IncomingCallView(@NonNull Context context) { |
||||
super(context); |
||||
} |
||||
|
||||
/** |
||||
* Handler for {@link ExternalAPIModule} events. |
||||
* |
||||
* @param name - Name of the event. |
||||
* @param data - Event data. |
||||
*/ |
||||
@Override |
||||
public void onExternalAPIEvent(String name, ReadableMap data) { |
||||
IncomingCallViewListener listener = getListener(); |
||||
if (listener != null) { |
||||
ListenerUtils.runListenerMethod( |
||||
listener, LISTENER_METHODS, name, data); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Gets the {@link IncomingCallViewListener} set on this |
||||
* {@code IncomingCallView}. |
||||
* |
||||
* @return The {@code IncomingCallViewListener} set on this |
||||
* {@code IncomingCallView}. |
||||
*/ |
||||
public IncomingCallViewListener getListener() { |
||||
return listener; |
||||
} |
||||
|
||||
/** |
||||
* Sets the information for the incoming call this {@code IncomingCallView} |
||||
* represents. |
||||
* |
||||
* @param callInfo - {@link IncomingCallInfo} object representing the caller |
||||
* information. |
||||
*/ |
||||
public void setIncomingCallInfo(IncomingCallInfo callInfo) { |
||||
Bundle props = new Bundle(); |
||||
|
||||
props.putString("callerAvatarUrl", callInfo.getCallerAvatarUrl()); |
||||
props.putString("callerName", callInfo.getCallerName()); |
||||
props.putBoolean("hasVideo", callInfo.hasVideo()); |
||||
|
||||
createReactRootView("IncomingCallApp", props); |
||||
} |
||||
|
||||
/** |
||||
* Sets a specific {@link IncomingCallViewListener} on this |
||||
* {@code IncomingCallView}. |
||||
* |
||||
* @param listener The {@code IncomingCallViewListener} to set on this |
||||
* {@code IncomingCallView}. |
||||
*/ |
||||
public void setListener(IncomingCallViewListener listener) { |
||||
this.listener = listener; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,41 @@ |
||||
/* |
||||
* 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; |
||||
|
||||
/** |
||||
* Interface for listening to events coming from Jitsi Meet, related to |
||||
* {@link IncomingCallView}; |
||||
*/ |
||||
public interface IncomingCallViewListener { |
||||
/** |
||||
* Called when the user presses the "answer" button on the |
||||
* {@link IncomingCallView}. |
||||
* |
||||
* @param data - Unused at the moment. |
||||
*/ |
||||
void onIncomingCallAnswered(Map<String, Object> data); |
||||
|
||||
/** |
||||
* Called when the user presses the "decline" button on the |
||||
* {@link IncomingCallView}. |
||||
* |
||||
* @param data - Unused at the moment. |
||||
*/ |
||||
void onIncomingCallDeclined(Map<String, Object> data); |
||||
} |
@ -0,0 +1,27 @@ |
||||
/** |
||||
* 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'); |
||||
|
||||
/** |
||||
* 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'); |
@ -0,0 +1,49 @@ |
||||
// @flow
|
||||
|
||||
import { |
||||
INCOMING_CALL_ANSWERED, |
||||
INCOMING_CALL_DECLINED, |
||||
INCOMING_CALL_RECEIVED |
||||
} from './actionTypes'; |
||||
|
||||
/** |
||||
* 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 |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* 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 |
||||
}; |
||||
} |
@ -0,0 +1,39 @@ |
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { AbstractButton } from '../../../base/toolbox'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import type { AbstractButtonProps } from '../../../base/toolbox'; |
||||
|
||||
import { incomingCallAnswered } from '../actions'; |
||||
|
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function |
||||
}; |
||||
|
||||
/** |
||||
* An implementation of a button which accepts an incoming call. |
||||
*/ |
||||
class AnswerButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'incomingCall.answer'; |
||||
iconName = 'hangup'; |
||||
label = 'incomingCall.answer'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and answers the incoming call. |
||||
* |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
this.props.dispatch(incomingCallAnswered()); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default translate(connect()(AnswerButton)); |
@ -0,0 +1,39 @@ |
||||
// @flow
|
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { AbstractButton } from '../../../base/toolbox'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import type { AbstractButtonProps } from '../../../base/toolbox'; |
||||
|
||||
import { incomingCallDeclined } from '../actions'; |
||||
|
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function |
||||
}; |
||||
|
||||
/** |
||||
* An implementation of a button which rejects an incoming call. |
||||
*/ |
||||
class DeclineButton extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'incomingCall.decline'; |
||||
iconName = 'hangup'; |
||||
label = 'incomingCall.decline'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and declines the incoming call. |
||||
* |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
this.props.dispatch(incomingCallDeclined()); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default translate(connect()(DeclineButton)); |
@ -0,0 +1,63 @@ |
||||
// @flow
|
||||
|
||||
import { BaseApp } from '../../../base/app'; |
||||
|
||||
import { incomingCallReceived } from '../actions'; |
||||
|
||||
import IncomingCallPage from './IncomingCallPage'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* URL of the avatar for the caller. |
||||
*/ |
||||
callerAvatarUrl: string, |
||||
|
||||
/** |
||||
* Name of the caller. |
||||
*/ |
||||
callerName: string, |
||||
|
||||
/** |
||||
* Whether this is a video call or not. |
||||
*/ |
||||
hasVideo: boolean |
||||
}; |
||||
|
||||
/** |
||||
* Root application component for incoming call. |
||||
* |
||||
* @extends BaseApp |
||||
*/ |
||||
export default class IncomingCallApp extends BaseApp<Props> { |
||||
_init: Promise<*>; |
||||
|
||||
/** |
||||
* Navigates to {@link IncomingCallPage} upon mount. |
||||
* |
||||
* NOTE: This was implmented here instead of in a middleware for |
||||
* the APP_WILL_MOUNT action because that would run also for {@link App}. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
componentWillMount() { |
||||
super.componentWillMount(); |
||||
|
||||
this._init.then(() => { |
||||
const { dispatch } = this.state.store; |
||||
const { |
||||
callerAvatarUrl: avatarUrl, |
||||
callerName: name, |
||||
hasVideo |
||||
} = this.props; |
||||
|
||||
dispatch(incomingCallReceived({ |
||||
avatarUrl, |
||||
name, |
||||
hasVideo |
||||
})); |
||||
|
||||
super._navigate({ component: IncomingCallPage }); |
||||
}); |
||||
} |
||||
} |
@ -0,0 +1,182 @@ |
||||
// @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 { translate } from '../../../base/i18n'; |
||||
import { Avatar } from '../../../base/participants'; |
||||
|
||||
import AnswerButton from './AnswerButton'; |
||||
import DeclineButton from './DeclineButton'; |
||||
import styles, { |
||||
AVATAR_BORDER_GRADIENT, |
||||
BACKGROUND_OVERLAY_GRADIENT, |
||||
CALLER_AVATAR_SIZE |
||||
} from './styles'; |
||||
|
||||
/** |
||||
* The type of React {@code Component} props of {@link IncomingCallPage}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* Caller's avatar URL. |
||||
*/ |
||||
_callerAvatarUrl: string, |
||||
|
||||
/** |
||||
* Caller's name. |
||||
*/ |
||||
_callerName: string, |
||||
|
||||
/** |
||||
* Whether the call has video or not. |
||||
*/ |
||||
_hasVideo: boolean, |
||||
|
||||
/** |
||||
* Helper for translating strings. |
||||
*/ |
||||
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 } = this.props; |
||||
|
||||
return ( |
||||
<View style = { styles.buttonsContainer }> |
||||
<View style = { styles.buttonWrapper } > |
||||
<DeclineButton |
||||
styles = { styles.declineButtonStyles } /> |
||||
<Text style = { styles.buttonText }> |
||||
{ t('incomingCall.decline') } |
||||
</Text> |
||||
</View> |
||||
<View style = { styles.buttonWrapper }> |
||||
<AnswerButton |
||||
styles = { styles.answerButtonStyles } /> |
||||
<Text style = { styles.buttonText }> |
||||
{ t('incomingCall.answer') } |
||||
</Text> |
||||
</View> |
||||
</View> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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, |
||||
* _hasVideo: boolean |
||||
* }} |
||||
*/ |
||||
function _mapStateToProps(state) { |
||||
const { caller } = state['features/mobile/incoming-call'] || {}; |
||||
|
||||
return { |
||||
/** |
||||
* The caller's avatar url. |
||||
* |
||||
* @private |
||||
* @type {string} |
||||
*/ |
||||
_callerAvatarUrl: caller.avatarUrl, |
||||
|
||||
/** |
||||
* The caller's name. |
||||
* |
||||
* @private |
||||
* @type {string} |
||||
*/ |
||||
_callerName: caller.name, |
||||
|
||||
/** |
||||
* Whether the call has video or not. |
||||
* |
||||
* @private |
||||
* @type {boolean} |
||||
*/ |
||||
_hasVideo: caller.hasVideo |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(IncomingCallPage)); |
@ -0,0 +1,2 @@ |
||||
export { default as IncomingCallApp } from './IncomingCallApp'; |
||||
export { default as IncomingCallPage } from './IncomingCallPage'; |
@ -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,4 @@ |
||||
export * from './components'; |
||||
|
||||
import './middleware'; |
||||
import './reducer'; |
@ -0,0 +1,31 @@ |
||||
// @flow
|
||||
|
||||
import { MiddlewareRegistry } from '../../base/redux'; |
||||
import { getSymbolDescription } from '../../base/util'; |
||||
|
||||
import { sendEvent } from '../external-api'; |
||||
|
||||
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 => { |
||||
const result = next(action); |
||||
|
||||
switch (action.type) { |
||||
case INCOMING_CALL_ANSWERED: |
||||
case INCOMING_CALL_DECLINED: |
||||
sendEvent(store, getSymbolDescription(action.type), /* data */ {}); |
||||
break; |
||||
} |
||||
|
||||
return result; |
||||
}); |
@ -0,0 +1,26 @@ |
||||
/* @flow */ |
||||
|
||||
import { assign, ReducerRegistry } from '../../base/redux'; |
||||
|
||||
import { |
||||
INCOMING_CALL_ANSWERED, |
||||
INCOMING_CALL_DECLINED, |
||||
INCOMING_CALL_RECEIVED |
||||
} from './actionTypes'; |
||||
|
||||
ReducerRegistry.register( |
||||
'features/mobile/incoming-call', (state = {}, action) => { |
||||
switch (action.type) { |
||||
case INCOMING_CALL_ANSWERED: |
||||
case INCOMING_CALL_DECLINED: |
||||
return assign(state, { |
||||
caller: undefined |
||||
}); |
||||
case INCOMING_CALL_RECEIVED: |
||||
return assign(state, { |
||||
caller: action.caller |
||||
}); |
||||
} |
||||
|
||||
return state; |
||||
}); |
Loading…
Reference in new issue