feat: Add caller and ringer to video conf calls (#5046)
* add expo camera and use camera on call init action sheet * fix permissions * set colors when calling * update @react-native-community/hooks lib * move to useWindowDimensions * create action to handle video-conf calls * create videoConf reducer * add typed-redux-saga lib * fix return * change videoConf saga to TS * fix TS target * update action and types * create actionSheetRef * add notifyUser api * export video conf types * add action prop * use new reducer prop * add videoConferenceCancel and add allowRinging to videoConferenceStart * temp-patch * add locales * add handler to videoconf message * fix rest types * add message types * path to remove component from dom * remove notification when is videoconf * create sound hook * create dots loader * update call translation * the end is near * move to confirmed * better code reading * fix call type * fix tests * update podfile * wip * fix call order * move colors * move to jsx * fix colors * add pt-br * remove patch and point * fix colors * fix expo camera * move to style * remove unused styles * update types and style * wip * rename IncomingCallComponent * add custom notification * wip * fix naming * fix styles * fix import * fix styles * change colors * fixa ringing * fix import * organize * fix sizes * use realName * fix spacing * fix icon size * fix header gap * changeColor * fix safeArea * set calling only on direct calls * change ringer to be a component * cancel call on swipe * remove join on direct calls * add props * update package
This commit is contained in:
parent
fea4f164d5
commit
223550d88c
|
@ -49,5 +49,9 @@ allprojects {
|
|||
maven {
|
||||
url "$rootDir/../node_modules/detox/Detox-android"
|
||||
}
|
||||
maven {
|
||||
// expo-camera bundles a custom com.google.android:cameraview
|
||||
url "$rootDir/../node_modules/expo-camera/android/maven"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,3 +84,13 @@ export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DEC
|
|||
|
||||
export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET', 'UPDATE']);
|
||||
export const ROLES = createRequestTypes('ROLES', ['SET', 'UPDATE', 'REMOVE']);
|
||||
export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [
|
||||
'HANDLE_INCOMING_WEBSOCKET_MESSAGES',
|
||||
'SET',
|
||||
'REMOVE',
|
||||
'CLEAR',
|
||||
'INIT_CALL',
|
||||
'CANCEL_CALL',
|
||||
'ACCEPT_CALL',
|
||||
'SET_CALLING'
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import { Action } from 'redux';
|
||||
|
||||
import { ICallInfo } from '../reducers/videoConf';
|
||||
import { VIDEO_CONF } from './actionsTypes';
|
||||
|
||||
interface IHandleVideoConfIncomingWebsocketMessages extends Action {
|
||||
data: any;
|
||||
}
|
||||
|
||||
export type TCallProps = { mic: boolean; cam: boolean; direct: boolean; rid: string; uid: string };
|
||||
type TInitCallAction = Action & { payload: TCallProps };
|
||||
type TSetCallingAction = Action & { payload: boolean };
|
||||
type TCancelCallAction = Action & { payload: { callId?: string } };
|
||||
type TAcceptCallAction = Action & { payload: { callId: string } };
|
||||
|
||||
export interface IVideoConfGenericAction extends Action {
|
||||
payload: ICallInfo;
|
||||
}
|
||||
|
||||
export type TActionVideoConf = IHandleVideoConfIncomingWebsocketMessages &
|
||||
IVideoConfGenericAction &
|
||||
TSetCallingAction &
|
||||
Action &
|
||||
TInitCallAction &
|
||||
TCancelCallAction &
|
||||
TAcceptCallAction;
|
||||
|
||||
export function handleVideoConfIncomingWebsocketMessages(data: any): IHandleVideoConfIncomingWebsocketMessages {
|
||||
return {
|
||||
type: VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
export function setVideoConfCall(payload: ICallInfo): IVideoConfGenericAction {
|
||||
return {
|
||||
type: VIDEO_CONF.SET,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function removeVideoConfCall(payload: ICallInfo): IVideoConfGenericAction {
|
||||
return {
|
||||
type: VIDEO_CONF.REMOVE,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function clearVideoConfCalls(): Action {
|
||||
return {
|
||||
type: VIDEO_CONF.CLEAR
|
||||
};
|
||||
}
|
||||
|
||||
export function initVideoCall(payload: TCallProps): TInitCallAction {
|
||||
return {
|
||||
type: VIDEO_CONF.INIT_CALL,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function cancelCall(payload: { callId?: string }): TCancelCallAction {
|
||||
return {
|
||||
type: VIDEO_CONF.CANCEL_CALL,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function acceptCall(payload: { callId: string }): TAcceptCallAction {
|
||||
return {
|
||||
type: VIDEO_CONF.ACCEPT_CALL,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
export function setCalling(payload: boolean): TSetCallingAction {
|
||||
return {
|
||||
type: VIDEO_CONF.SET_CALLING,
|
||||
payload
|
||||
};
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import hoistNonReactStatics from 'hoist-non-react-statics';
|
||||
import React, { ForwardedRef, forwardRef, useContext, useRef } from 'react';
|
||||
import React, { createRef, ForwardedRef, forwardRef, useContext } from 'react';
|
||||
|
||||
import { TIconsName } from '../CustomIcon';
|
||||
import ActionSheet from './ActionSheet';
|
||||
|
@ -47,23 +47,27 @@ export const withActionSheet = (Component: React.ComponentType<any>): typeof Com
|
|||
return WithActionSheetComponent;
|
||||
};
|
||||
|
||||
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
||||
const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
|
||||
const actionSheetRef: React.Ref<IActionSheetProvider> = createRef();
|
||||
|
||||
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
||||
const getContext = () => ({
|
||||
showActionSheet: (options: TActionSheetOptions) => {
|
||||
ref.current?.showActionSheet(options);
|
||||
actionSheetRef.current?.showActionSheet(options);
|
||||
},
|
||||
hideActionSheet: () => {
|
||||
ref.current?.hideActionSheet();
|
||||
actionSheetRef.current?.hideActionSheet();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Provider value={getContext()}>
|
||||
<ActionSheet ref={ref}>
|
||||
<ActionSheet ref={actionSheetRef}>
|
||||
<>{children}</>
|
||||
</ActionSheet>
|
||||
</Provider>
|
||||
);
|
||||
});
|
||||
|
||||
export const hideActionSheetRef = (): void => {
|
||||
actionSheetRef?.current?.hideActionSheet();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import React from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import { useAppSelector } from '../lib/hooks';
|
||||
import { useTheme } from '../theme';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { CustomIcon } from './CustomIcon';
|
||||
import { BUTTON_HIT_SLOP } from './message/utils';
|
||||
import AvatarContainer from './Avatar';
|
||||
import StatusContainer from './Status';
|
||||
import DotsLoader from './DotsLoader';
|
||||
|
||||
type TCallHeader = {
|
||||
mic: boolean;
|
||||
cam: boolean;
|
||||
setCam: Function;
|
||||
setMic: Function;
|
||||
title: string;
|
||||
avatar: string;
|
||||
uid: string;
|
||||
name: string;
|
||||
direct: boolean;
|
||||
};
|
||||
|
||||
export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name, direct }: TCallHeader): React.ReactElement => {
|
||||
const style = useStyle();
|
||||
const { colors } = useTheme();
|
||||
const calling = useAppSelector(state => state.videoConf.calling);
|
||||
|
||||
const handleColors = (enabled: boolean) => {
|
||||
if (calling) {
|
||||
if (enabled) return { button: colors.conferenceCallCallBackButton, icon: colors.gray300 };
|
||||
return { button: 'transparent', icon: colors.gray100 };
|
||||
}
|
||||
if (enabled) return { button: colors.conferenceCallEnabledIconBackground, icon: colors.conferenceCallEnabledIcon };
|
||||
return { button: 'transparent', icon: colors.conferenceCallDisabledIcon };
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<View style={style.actionSheetHeader}>
|
||||
<View style={style.rowContainer}>
|
||||
<Text style={style.actionSheetHeaderTitle}>{title}</Text>
|
||||
{calling && direct ? <DotsLoader /> : null}
|
||||
</View>
|
||||
<View style={style.actionSheetHeaderButtons}>
|
||||
<Touchable
|
||||
onPress={() => setCam(!cam)}
|
||||
style={[style.iconCallContainerRight, { backgroundColor: handleColors(cam).button }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
disabled={calling}
|
||||
>
|
||||
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={24} color={handleColors(cam).icon} />
|
||||
</Touchable>
|
||||
<Touchable
|
||||
onPress={() => setMic(!mic)}
|
||||
style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
disabled={calling}
|
||||
>
|
||||
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={24} color={handleColors(mic).icon} />
|
||||
</Touchable>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.actionSheetUsernameContainer}>
|
||||
<AvatarContainer text={avatar} size={36} />
|
||||
{direct ? <StatusContainer size={16} id={uid} style={style.statusContainerMargin} /> : null}
|
||||
<Text style={{ ...style.actionSheetUsername, marginLeft: !direct ? 8 : 0 }} numberOfLines={1}>
|
||||
{name}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
function useStyle() {
|
||||
const { colors } = useTheme();
|
||||
const style = StyleSheet.create({
|
||||
actionSheetHeader: { flexDirection: 'row', alignItems: 'center' },
|
||||
actionSheetHeaderTitle: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textBold,
|
||||
color: colors.n900
|
||||
},
|
||||
actionSheetHeaderButtons: { flex: 1, alignItems: 'center', flexDirection: 'row', justifyContent: 'flex-end' },
|
||||
iconCallContainer: {
|
||||
padding: 6,
|
||||
borderRadius: 4
|
||||
},
|
||||
iconCallContainerRight: {
|
||||
padding: 6,
|
||||
borderRadius: 4,
|
||||
marginRight: 6
|
||||
},
|
||||
actionSheetUsernameContainer: { flexDirection: 'row', paddingTop: 8, alignItems: 'center' },
|
||||
actionSheetUsername: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textBold,
|
||||
color: colors.passcodePrimary,
|
||||
flexShrink: 1
|
||||
},
|
||||
rowContainer: { flexDirection: 'row' },
|
||||
statusContainerMargin: { marginLeft: 8, marginRight: 6 }
|
||||
});
|
||||
return style;
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { StyleProp, View, ViewStyle, StyleSheet } from 'react-native';
|
||||
import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
|
||||
|
||||
import { useTheme } from '../../theme';
|
||||
|
||||
const SIZE = 8;
|
||||
const MARGIN = 4;
|
||||
const dots = [1, 2, 3];
|
||||
const INTERVAL = 300;
|
||||
const ANIMATION_DURATION = 400;
|
||||
const ANIMATION_SCALE = 1.4;
|
||||
|
||||
function Dot({ active }: { active: boolean }): JSX.Element {
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
useEffect(() => {
|
||||
scale.value = withTiming(active ? ANIMATION_SCALE : 1, {
|
||||
duration: ANIMATION_DURATION
|
||||
});
|
||||
}, [active]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: scale.value }]
|
||||
}));
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
const style: StyleProp<ViewStyle> = {
|
||||
height: SIZE,
|
||||
width: SIZE,
|
||||
borderRadius: SIZE / 2,
|
||||
marginHorizontal: MARGIN,
|
||||
backgroundColor: active ? colors.dotActiveBg : colors.dotBg
|
||||
};
|
||||
|
||||
return <Animated.View style={[style, animatedStyle]} />;
|
||||
}
|
||||
|
||||
function DotsLoader(): JSX.Element {
|
||||
const [active, setActive] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActive(prevActive => (prevActive > 2 ? 1 : prevActive + 1));
|
||||
}, INTERVAL);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.dotsContainer}>
|
||||
{dots.map(i => (
|
||||
<Dot key={i} active={i === active} />
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dotsContainer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginLeft: 6 }
|
||||
});
|
||||
|
||||
export default DotsLoader;
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Text, View, useWindowDimensions } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { acceptCall, cancelCall } from '../../../actions/videoConf';
|
||||
import { ISubscription, SubscriptionType } from '../../../definitions';
|
||||
import i18n from '../../../i18n';
|
||||
import { useAppSelector } from '../../../lib/hooks';
|
||||
import { useEndpointData } from '../../../lib/hooks/useEndpointData';
|
||||
import { hideNotification } from '../../../lib/methods/helpers/notifications';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { CustomIcon } from '../../CustomIcon';
|
||||
import { CallHeader } from '../../CallHeader';
|
||||
import { useStyle } from './style';
|
||||
import useUserData from '../../../lib/hooks/useUserData';
|
||||
import Ringer, { ERingerSounds } from '../../Ringer';
|
||||
|
||||
export interface INotifierComponent {
|
||||
notification: {
|
||||
text: string;
|
||||
payload: {
|
||||
sender: { username: string };
|
||||
type: SubscriptionType;
|
||||
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
|
||||
title: string;
|
||||
avatar: string;
|
||||
};
|
||||
isMasterDetail: boolean;
|
||||
}
|
||||
|
||||
const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
|
||||
|
||||
const IncomingCallHeader = React.memo(
|
||||
({ uid, callId, avatar, roomName }: { callId: string; avatar: string; uid: string; roomName: string }) => {
|
||||
const [mic, setMic] = useState(true);
|
||||
const [cam, setCam] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
|
||||
const styles = useStyle();
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height, width } = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.container,
|
||||
(isMasterDetail || isLandscape) && styles.small,
|
||||
{
|
||||
marginTop: insets.top
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CallHeader
|
||||
title={i18n.t('Incoming_call_from')}
|
||||
cam={cam}
|
||||
setCam={setCam}
|
||||
mic={mic}
|
||||
setMic={setMic}
|
||||
avatar={avatar}
|
||||
name={roomName}
|
||||
uid={uid}
|
||||
direct={true}
|
||||
/>
|
||||
<View style={styles.row}>
|
||||
<Touchable hitSlop={BUTTON_HIT_SLOP} onPress={hideNotification} style={styles.closeButton}>
|
||||
<CustomIcon name='close' size={20} color={colors.gray300} />
|
||||
</Touchable>
|
||||
<Touchable
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
onPress={() => {
|
||||
hideNotification();
|
||||
dispatch(cancelCall({ callId }));
|
||||
}}
|
||||
style={styles.cancelButton}
|
||||
>
|
||||
<Text style={styles.buttonText}>{i18n.t('decline')}</Text>
|
||||
</Touchable>
|
||||
<Touchable
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
onPress={() => {
|
||||
hideNotification();
|
||||
dispatch(acceptCall({ callId }));
|
||||
}}
|
||||
style={styles.acceptButton}
|
||||
>
|
||||
<Text style={styles.buttonText}>{i18n.t('accept')}</Text>
|
||||
</Touchable>
|
||||
</View>
|
||||
<Ringer ringer={ERingerSounds.RINGTONE} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const IncomingCallNotification = ({
|
||||
notification: { rid, callId }
|
||||
}: {
|
||||
notification: { rid: string; callId: string };
|
||||
}): React.ReactElement | null => {
|
||||
const { result } = useEndpointData('video-conference.info', { callId });
|
||||
|
||||
const user = useUserData(rid);
|
||||
|
||||
if (result?.success && user.username) {
|
||||
return <IncomingCallHeader callId={callId} avatar={user.avatar} roomName={user.username} uid={user.uid} />;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default IncomingCallNotification;
|
|
@ -0,0 +1,57 @@
|
|||
import { PixelRatio, StyleSheet } from 'react-native';
|
||||
|
||||
import { useTheme } from '../../../theme';
|
||||
import sharedStyles from '../../../views/Styles';
|
||||
|
||||
export const useStyle = () => {
|
||||
const { colors } = useTheme();
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
height: 160 * PixelRatio.getFontScale(),
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 18,
|
||||
marginHorizontal: 10,
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.focusedBackground,
|
||||
borderColor: colors.separatorColor,
|
||||
flex: 1
|
||||
},
|
||||
small: {
|
||||
width: '50%',
|
||||
alignSelf: 'center'
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 12
|
||||
},
|
||||
closeButton: {
|
||||
backgroundColor: colors.passcodeButtonActive,
|
||||
marginRight: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
width: 36,
|
||||
height: 36
|
||||
},
|
||||
cancelButton: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.cancelCallButton,
|
||||
marginRight: 8,
|
||||
flex: 2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
buttonText: {
|
||||
...sharedStyles.textMedium,
|
||||
color: 'white'
|
||||
},
|
||||
acceptButton: {
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.acceptCallButton,
|
||||
flex: 2,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
};
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { connect } from 'react-redux';
|
||||
import { Notifier } from 'react-native-notifier';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
|
@ -14,6 +13,7 @@ import { ROW_HEIGHT } from '../RoomItem';
|
|||
import { goRoom } from '../../lib/methods/helpers/goRoom';
|
||||
import { useOrientation } from '../../dimensions';
|
||||
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
|
||||
import { hideNotification } from '../../lib/methods/helpers/notifications';
|
||||
|
||||
export interface INotifierComponent {
|
||||
notification: {
|
||||
|
@ -21,6 +21,7 @@ export interface INotifierComponent {
|
|||
payload: {
|
||||
sender: { username: string };
|
||||
type: SubscriptionType;
|
||||
message?: { message: string; t?: string };
|
||||
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
|
||||
title: string;
|
||||
avatar: string;
|
||||
|
@ -72,8 +73,6 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const hideNotification = () => Notifier.hideNotification();
|
||||
|
||||
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
|
||||
const { theme } = useTheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { memo, useEffect } from 'react';
|
||||
import React, { ElementType, memo, useEffect } from 'react';
|
||||
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
|
||||
|
||||
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
|
||||
|
@ -15,24 +15,32 @@ const InAppNotification = memo(() => {
|
|||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
|
||||
}));
|
||||
|
||||
const show = (notification: INotifierComponent['notification']) => {
|
||||
if (appState !== 'foreground') {
|
||||
return;
|
||||
const show = (
|
||||
notification: INotifierComponent['notification'] & {
|
||||
customComponent?: ElementType;
|
||||
customTime?: number;
|
||||
customNotification?: boolean;
|
||||
hideOnPress?: boolean;
|
||||
swipeEnabled?: boolean;
|
||||
}
|
||||
) => {
|
||||
if (appState !== 'foreground') return;
|
||||
|
||||
const { payload } = notification;
|
||||
const state = Navigation.navigationRef.current?.getRootState();
|
||||
const route = getActiveRoute(state);
|
||||
if (payload.rid) {
|
||||
if (payload.rid === subscribedRoom || route?.name === 'JitsiMeetView') {
|
||||
return;
|
||||
}
|
||||
if (payload?.rid || notification.customNotification) {
|
||||
if (payload?.rid === subscribedRoom || route?.name === 'JitsiMeetView' || payload?.message?.t === 'videoconf') return;
|
||||
|
||||
Notifier.showNotification({
|
||||
showEasing: Easing.inOut(Easing.quad),
|
||||
Component: NotifierComponent,
|
||||
Component: notification.customComponent || NotifierComponent,
|
||||
componentProps: {
|
||||
notification
|
||||
}
|
||||
},
|
||||
duration: notification.customTime || 3000, // default 3s,
|
||||
hideOnPress: notification.hideOnPress ?? true,
|
||||
swipeEnabled: notification.swipeEnabled ?? true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,43 @@
|
|||
import { Audio } from 'expo-av';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View } from 'react-native';
|
||||
|
||||
export enum ERingerSounds {
|
||||
DIALTONE = 'dialtone',
|
||||
RINGTONE = 'ringtone'
|
||||
}
|
||||
|
||||
const Ringer = React.memo(({ ringer }: { ringer: ERingerSounds }) => {
|
||||
console.log('Ringer', ringer);
|
||||
|
||||
const sound = useRef<Audio.Sound | null>(null);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
let expo = null;
|
||||
switch (ringer) {
|
||||
case ERingerSounds.DIALTONE:
|
||||
expo = await Audio.Sound.createAsync(require(`./dialtone.mp3`));
|
||||
break;
|
||||
case ERingerSounds.RINGTONE:
|
||||
expo = await Audio.Sound.createAsync(require(`./ringtone.mp3`));
|
||||
break;
|
||||
default:
|
||||
expo = await Audio.Sound.createAsync(require(`./dialtone.mp3`));
|
||||
break;
|
||||
}
|
||||
sound.current = expo.sound;
|
||||
await sound.current.playAsync();
|
||||
await sound.current.setIsLoopingAsync(true);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => stopSound(), []);
|
||||
|
||||
const stopSound = () => {
|
||||
sound?.current?.unloadAsync();
|
||||
};
|
||||
|
||||
return <View />;
|
||||
});
|
||||
|
||||
export default Ringer;
|
Binary file not shown.
|
@ -50,6 +50,11 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
|
|||
prefix = `${useRealName ? name : lastMessage.u.username}: `;
|
||||
}
|
||||
|
||||
if (lastMessage.t === 'videoconf') {
|
||||
prefix = '';
|
||||
lastMessage.msg = I18n.t('Call_started');
|
||||
}
|
||||
|
||||
return `${prefix}${lastMessage.msg}`;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,103 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import i18n from '../../../../i18n';
|
||||
import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subscription';
|
||||
import { useAppSelector } from '../../../../lib/hooks';
|
||||
import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers';
|
||||
import { Services } from '../../../../lib/services';
|
||||
import { useTheme } from '../../../../theme';
|
||||
import { useActionSheet } from '../../../ActionSheet';
|
||||
import AvatarContainer from '../../../Avatar';
|
||||
import Button from '../../../Button';
|
||||
import { CustomIcon } from '../../../CustomIcon';
|
||||
import StatusContainer from '../../../Status';
|
||||
import { BUTTON_HIT_SLOP } from '../../../message/utils';
|
||||
import useStyle from './styles';
|
||||
|
||||
const useUserData = (rid: string) => {
|
||||
const [user, setUser] = useState({ username: '', avatar: '', uid: '', type: '' });
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const room = await getSubscriptionByRoomId(rid);
|
||||
if (room) {
|
||||
const uid = (await getUidDirectMessage(room)) as string;
|
||||
const avt = getRoomAvatar(room);
|
||||
setUser({ uid, username: room?.name || '', avatar: avt, type: room?.t || '' });
|
||||
} else {
|
||||
try {
|
||||
const result = await Services.getUserInfo(rid);
|
||||
if (result.success) {
|
||||
setUser({
|
||||
username: result.user.name || result.user.username,
|
||||
avatar: result.user.username,
|
||||
uid: result.user._id,
|
||||
type: 'd'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
|
||||
const style = useStyle();
|
||||
const { colors } = useTheme();
|
||||
const [mic, setMic] = useState(true);
|
||||
const [cam, setCam] = useState(false);
|
||||
const username = useAppSelector(state => state.login.user.username);
|
||||
|
||||
const { hideActionSheet } = useActionSheet();
|
||||
const user = useUserData(rid);
|
||||
|
||||
const handleColor = (enabled: boolean) => (enabled ? colors.conferenceCallEnabledIcon : colors.conferenceCallDisabledIcon);
|
||||
|
||||
return (
|
||||
<View style={style.actionSheetContainer}>
|
||||
<View style={style.actionSheetHeader}>
|
||||
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
|
||||
<View style={style.actionSheetHeaderButtons}>
|
||||
<Touchable
|
||||
onPress={() => setCam(!cam)}
|
||||
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
|
||||
</Touchable>
|
||||
<Touchable
|
||||
onPress={() => setMic(!mic)}
|
||||
style={[style.iconCallContainer, mic && style.enabledBackground]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
|
||||
</Touchable>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.actionSheetUsernameContainer}>
|
||||
<AvatarContainer text={user.avatar} size={36} rid={rid} type={user.type} />
|
||||
<StatusContainer size={16} id={user.uid} style={{ marginLeft: 8, marginRight: 6 }} />
|
||||
<Text style={style.actionSheetUsername} numberOfLines={1}>
|
||||
{user.username}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={style.actionSheetPhotoContainer}>
|
||||
<AvatarContainer size={62} text={username} />
|
||||
</View>
|
||||
<Button
|
||||
onPress={() => {
|
||||
hideActionSheet();
|
||||
setTimeout(() => {
|
||||
initCall({ cam, mic });
|
||||
}, 100);
|
||||
}}
|
||||
title={i18n.t('Call')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
|
@ -1,20 +1,15 @@
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import i18n from '../../../../i18n';
|
||||
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
||||
import useStyle from './styles';
|
||||
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
||||
|
||||
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
|
||||
const VideoConferenceDirect = React.memo(() => {
|
||||
const style = useStyle();
|
||||
|
||||
return (
|
||||
<VideoConferenceBaseContainer variant='incoming'>
|
||||
<Touchable style={style.callToActionButton} onPress={() => videoConfJoin(blockId)}>
|
||||
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
|
||||
</Touchable>
|
||||
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>
|
||||
</VideoConferenceBaseContainer>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import sharedStyles from '../../../../views/Styles';
|
|||
|
||||
export default function useStyle() {
|
||||
const { colors } = useTheme();
|
||||
return StyleSheet.create({
|
||||
const style = StyleSheet.create({
|
||||
container: { height: 108, flex: 1, borderWidth: 1, borderRadius: 4, marginTop: 8, borderColor: colors.conferenceCallBorder },
|
||||
callInfoContainer: { flex: 1, alignItems: 'center', paddingLeft: 16, flexDirection: 'row' },
|
||||
infoContainerText: {
|
||||
|
@ -88,40 +88,10 @@ export default function useStyle() {
|
|||
...sharedStyles.textRegular,
|
||||
color: colors.passcodeSecondary
|
||||
},
|
||||
actionSheetContainer: {
|
||||
paddingHorizontal: 24,
|
||||
flex: 1
|
||||
},
|
||||
actionSheetHeaderTitle: {
|
||||
fontSize: 14,
|
||||
...sharedStyles.textBold,
|
||||
color: colors.passcodePrimary
|
||||
},
|
||||
actionSheetUsername: {
|
||||
fontSize: 16,
|
||||
...sharedStyles.textBold,
|
||||
color: colors.passcodePrimary,
|
||||
flexShrink: 1
|
||||
},
|
||||
enabledBackground: {
|
||||
backgroundColor: colors.conferenceCallEnabledIconBackground
|
||||
},
|
||||
iconCallContainer: {
|
||||
padding: 6,
|
||||
borderRadius: 4
|
||||
},
|
||||
actionSheetHeader: { flexDirection: 'row', alignItems: 'center' },
|
||||
actionSheetHeaderButtons: { flex: 1, alignItems: 'center', flexDirection: 'row', justifyContent: 'flex-end' },
|
||||
actionSheetUsernameContainer: { flexDirection: 'row', paddingTop: 8, alignItems: 'center' },
|
||||
actionSheetPhotoContainer: {
|
||||
height: 220,
|
||||
width: 148,
|
||||
backgroundColor: colors.conferenceCallPhotoBackground,
|
||||
borderRadius: 8,
|
||||
margin: 24,
|
||||
alignSelf: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
return style;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function VideoConferenceBlock({ callId, blockId }: { callId: stri
|
|||
|
||||
if ('endedAt' in result) return <VideoConferenceEnded createdBy={createdBy} rid={rid} type={type} users={users} />;
|
||||
|
||||
if (type === 'direct' && status === 0) return <VideoConferenceDirect blockId={blockId} />;
|
||||
if (type === 'direct' && status === 0) return <VideoConferenceDirect />;
|
||||
|
||||
return <VideoConferenceOutgoing blockId={blockId} users={users} />;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ const MarkdownPreview = ({ msg, numberOfLines = 1, style = [], testID }: IMarkdo
|
|||
accessibilityLabel={m}
|
||||
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
|
||||
numberOfLines={numberOfLines}
|
||||
testID={testID || `markdown-preview-${m}`}>
|
||||
testID={testID || `markdown-preview-${m}`}
|
||||
>
|
||||
{m}
|
||||
</Text>
|
||||
);
|
||||
|
|
|
@ -107,3 +107,5 @@ export type VideoConfListProps = {
|
|||
};
|
||||
|
||||
export type VideoConfInfoProps = { callId: string };
|
||||
|
||||
export type VideoConfCall = VideoConference & { capabilities: VideoConferenceCapabilities };
|
||||
|
|
|
@ -16,6 +16,7 @@ import { TActionSortPreferences } from '../../actions/sortPreferences';
|
|||
import { TActionUserTyping } from '../../actions/usersTyping';
|
||||
import { TActionPermissions } from '../../actions/permissions';
|
||||
import { TActionEnterpriseModules } from '../../actions/enterpriseModules';
|
||||
import { TActionVideoConf } from '../../actions/videoConf';
|
||||
// REDUCERS
|
||||
import { IActiveUsers } from '../../reducers/activeUsers';
|
||||
import { IApp } from '../../reducers/app';
|
||||
|
@ -34,6 +35,7 @@ import { IShare } from '../../reducers/share';
|
|||
import { IInquiry } from '../../ee/omnichannel/reducers/inquiry';
|
||||
import { IPermissionsState } from '../../reducers/permissions';
|
||||
import { IEnterpriseModules } from '../../reducers/enterpriseModules';
|
||||
import { IVideoConf } from '../../reducers/videoConf';
|
||||
|
||||
export interface IApplicationState {
|
||||
settings: TSettingsState;
|
||||
|
@ -57,6 +59,7 @@ export interface IApplicationState {
|
|||
encryption: IEncryption;
|
||||
permissions: IPermissionsState;
|
||||
roles: IRoles;
|
||||
videoConf: IVideoConf;
|
||||
}
|
||||
|
||||
export type TApplicationActions = TActionActiveUsers &
|
||||
|
@ -75,4 +78,5 @@ export type TApplicationActions = TActionActiveUsers &
|
|||
TActionApp &
|
||||
TActionInquiry &
|
||||
TActionPermissions &
|
||||
TActionEnterpriseModules;
|
||||
TActionEnterpriseModules &
|
||||
TActionVideoConf;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
VideoConfCall,
|
||||
VideoConfCancelProps,
|
||||
VideoConference,
|
||||
VideoConferenceCapabilities,
|
||||
|
@ -24,7 +25,7 @@ export type VideoConferenceEndpoints = {
|
|||
};
|
||||
|
||||
'video-conference.info': {
|
||||
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
|
||||
GET: (params: VideoConfInfoProps) => VideoConfCall;
|
||||
};
|
||||
|
||||
'video-conference.list': {
|
||||
|
|
|
@ -691,11 +691,12 @@
|
|||
"Waiting_for_answer": "Waiting for answer",
|
||||
"Call_ended": "Call ended",
|
||||
"Call_was_not_answered": "Call was not answered",
|
||||
"Call_back": "Call back",
|
||||
"Call_again": "Call again",
|
||||
"Call_ongoing": "Call ongoing",
|
||||
"Call_rejected": "Call rejected",
|
||||
"Call_back": "Call Back",
|
||||
"Call_again": "Call Again",
|
||||
"Call_ongoing": "Call Ongoing",
|
||||
"Joined": "Joined",
|
||||
"Calling": "Calling...",
|
||||
"Calling": "Calling",
|
||||
"Start_a_call": "Start a call",
|
||||
"Call": "Call",
|
||||
"Reply_in_direct_message": "Reply in direct message",
|
||||
|
@ -724,5 +725,9 @@
|
|||
"Presence_Cap_Warning_Title": "User status temporarily disabled",
|
||||
"Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.",
|
||||
"Learn_more": "Learn more",
|
||||
"and_N_more": "and {{count}} more"
|
||||
"and_N_more": "and {{count}} more",
|
||||
"decline": "Decline",
|
||||
"accept": "Accept",
|
||||
"Incoming_call_from": "Incoming call from",
|
||||
"Call_started": "Call started"
|
||||
}
|
|
@ -695,7 +695,7 @@
|
|||
"Call_again": "Soita uudelleen",
|
||||
"Call_ongoing": "Puhelu käynnissä",
|
||||
"Joined": "Liitytty",
|
||||
"Calling": "Soitetaan...",
|
||||
"Calling": "Soitetaan",
|
||||
"Start_a_call": "Aloita puhelu",
|
||||
"Call": "Soita",
|
||||
"Reply_in_direct_message": "Vastaa suoralla viestillä",
|
||||
|
|
|
@ -695,7 +695,7 @@
|
|||
"Call_again": "Ligue novamente",
|
||||
"Call_ongoing": "Chamada em andamento",
|
||||
"Joined": "Ingressou",
|
||||
"Calling": "Chamando...",
|
||||
"Calling": "Chamando",
|
||||
"Start_a_call": "Inicie uma chamada",
|
||||
"Call": "Ligar",
|
||||
"Reply_in_direct_message": "Responder por mensagem direta",
|
||||
|
@ -711,6 +711,10 @@
|
|||
"Discard_changes_description": "Todas as alterações serão perdidas, se você sair sem salvar.",
|
||||
"Presence_Cap_Warning_Title": "Status do usuário desabilitado temporariamente",
|
||||
"Presence_Cap_Warning_Description": "O limite de conexões ativas para a workspace foi atingido, por isso o serviço responsável pela presença dos usuários está temporariamente desabilitado. Ele pode ser reabilitado manualmente nas configurações da workspace.",
|
||||
"decline": "Recusar",
|
||||
"accept": "Aceitar",
|
||||
"Incoming_call_from": "Chamada recebida de",
|
||||
"Call_started": "Chamada Iniciada",
|
||||
"Learn_more": "Saiba mais",
|
||||
"and_N_more": "e mais {{count}}"
|
||||
}
|
|
@ -693,7 +693,7 @@
|
|||
"Call_again": "Ring igen",
|
||||
"Call_ongoing": "Samtal pågår",
|
||||
"Joined": "Anslöt",
|
||||
"Calling": "Ringer upp...",
|
||||
"Calling": "Ringer upp",
|
||||
"Start_a_call": "Starta ett samtal",
|
||||
"Call": "Ring",
|
||||
"Reply_in_direct_message": "Svara med direktmeddelande",
|
||||
|
|
|
@ -20,6 +20,11 @@ const mentions = {
|
|||
mentionOtherColor: '#F3BE08'
|
||||
};
|
||||
|
||||
const callButtons = {
|
||||
cancelCallButton: '#F5455C',
|
||||
acceptCallButton: '#158D65'
|
||||
};
|
||||
|
||||
export const colors = {
|
||||
light: {
|
||||
backgroundColor: '#ffffff',
|
||||
|
@ -89,7 +94,13 @@ export const colors = {
|
|||
conferenceCallEnabledIconBackground: '#156FF5',
|
||||
conferenceCallPhotoBackground: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#E4E7EA',
|
||||
...mentions
|
||||
dotBg: '#a9cbff',
|
||||
dotActiveBg: '#1d74f5',
|
||||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#1F2329',
|
||||
...mentions,
|
||||
...callButtons
|
||||
},
|
||||
dark: {
|
||||
backgroundColor: '#030b1b',
|
||||
|
@ -158,8 +169,14 @@ export const colors = {
|
|||
conferenceCallEnabledIcon: '#FFFFFF',
|
||||
conferenceCallEnabledIconBackground: '#156FF5',
|
||||
conferenceCallPhotoBackground: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#030b1b', // backgroundColor
|
||||
...mentions
|
||||
textInputSecondaryBackground: '#030b1b',
|
||||
dotBg: '#a9cbff',
|
||||
dotActiveBg: '#1d74f5',
|
||||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#FFFFFF',
|
||||
...mentions,
|
||||
...callButtons
|
||||
},
|
||||
black: {
|
||||
backgroundColor: '#000000',
|
||||
|
@ -228,8 +245,14 @@ export const colors = {
|
|||
conferenceCallEnabledIcon: '#FFFFFF',
|
||||
conferenceCallEnabledIconBackground: '#156FF5',
|
||||
conferenceCallPhotoBackground: '#E4E7EA',
|
||||
textInputSecondaryBackground: '#000000', // backgroundColor
|
||||
...mentions
|
||||
textInputSecondaryBackground: '#000000',
|
||||
dotBg: '#a9cbff',
|
||||
dotActiveBg: '#1d74f5',
|
||||
gray300: '#5f656e',
|
||||
gray100: '#CBCED1',
|
||||
n900: '#FFFFFF',
|
||||
...mentions,
|
||||
...callButtons
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import database from '..';
|
||||
import { TSubscriptionModel } from '../../../definitions';
|
||||
import { TAppDatabase } from '../interfaces';
|
||||
import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
|
||||
|
||||
const getCollection = (db: TAppDatabase) => db.get(SUBSCRIPTIONS_TABLE);
|
||||
|
||||
export const getSubscriptionByRoomId = async (rid: string) => {
|
||||
export const getSubscriptionByRoomId = async (rid: string): Promise<TSubscriptionModel | null> => {
|
||||
const db = database.active;
|
||||
const subCollection = getCollection(db);
|
||||
try {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||
import { select } from 'redux-saga/effects';
|
||||
|
||||
import { IApplicationState } from '../../definitions';
|
||||
|
||||
export const useAppSelector: TypedUseSelectorHook<IApplicationState> = useSelector;
|
||||
|
||||
export function* appSelector<TSelected>(selector: (state: IApplicationState) => TSelected): Generator<any, TSelected, TSelected> {
|
||||
return yield select(selector);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { getSubscriptionByRoomId } from '../database/services/Subscription';
|
||||
import { getRoomAvatar, getUidDirectMessage } from '../methods/helpers';
|
||||
import { SubscriptionType } from '../../definitions';
|
||||
import { Services } from '../services';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
|
||||
const useUserData = (rid: string) => {
|
||||
const [user, setUser] = useState({ username: '', avatar: '', uid: '', type: '', direct: false });
|
||||
const { useRealName } = useAppSelector(state => ({
|
||||
useRealName: state.settings.UI_Use_Real_Name as boolean
|
||||
}));
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const room = await getSubscriptionByRoomId(rid);
|
||||
if (room) {
|
||||
const uid = (await getUidDirectMessage(room)) as string;
|
||||
const avt = getRoomAvatar(room);
|
||||
const username = useRealName && room.fname ? room.fname : room.name;
|
||||
setUser({
|
||||
uid,
|
||||
username,
|
||||
avatar: avt,
|
||||
type: room?.t || '',
|
||||
direct: room?.t === SubscriptionType.DIRECT
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
const result = await Services.getUserInfo(rid);
|
||||
if (result.success) {
|
||||
const { user } = result;
|
||||
const username = useRealName && user.name ? user.name : user.username;
|
||||
setUser({
|
||||
username,
|
||||
avatar: user.username,
|
||||
uid: user._id,
|
||||
type: SubscriptionType.DIRECT,
|
||||
direct: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
//
|
||||
}
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return user;
|
||||
};
|
||||
|
||||
export default useUserData;
|
|
@ -0,0 +1,106 @@
|
|||
import { Camera, CameraType } from 'expo-camera';
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useAppSelector } from '..';
|
||||
import { cancelCall, initVideoCall } from '../../../actions/videoConf';
|
||||
import AvatarContainer from '../../../containers/Avatar';
|
||||
import Button from '../../../containers/Button';
|
||||
import { CallHeader } from '../../../containers/CallHeader';
|
||||
import Ringer, { ERingerSounds } from '../../../containers/Ringer';
|
||||
import i18n from '../../../i18n';
|
||||
import { getUserSelector } from '../../../selectors/login';
|
||||
import { useTheme } from '../../../theme';
|
||||
import { isIOS } from '../../methods/helpers';
|
||||
import useUserData from '../useUserData';
|
||||
|
||||
export default function StartACallActionSheet({ rid }: { rid: string }): React.ReactElement {
|
||||
const { colors } = useTheme();
|
||||
const [mic, setMic] = useState(true);
|
||||
const [cam, setCam] = useState(false);
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
|
||||
const username = useAppSelector(state => getUserSelector(state).username);
|
||||
const calling = useAppSelector(state => state.videoConf.calling);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const user = useUserData(rid);
|
||||
|
||||
// fix safe area bottom padding on iOS
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom = isIOS && insets.bottom ? 8 : 0;
|
||||
|
||||
React.useEffect(
|
||||
() => () => {
|
||||
if (calling) {
|
||||
dispatch(cancelCall({}));
|
||||
}
|
||||
},
|
||||
[calling, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[style.actionSheetContainer, { paddingBottom }]}
|
||||
onLayout={e => setContainerWidth(e.nativeEvent.layout.width / 2)}
|
||||
>
|
||||
{calling ? <Ringer ringer={ERingerSounds.DIALTONE} /> : null}
|
||||
<CallHeader
|
||||
title={calling && user.direct ? i18n.t('Calling') : i18n.t('Start_a_call')}
|
||||
cam={cam}
|
||||
mic={mic}
|
||||
setCam={setCam}
|
||||
setMic={setMic}
|
||||
avatar={user.avatar}
|
||||
name={user.username}
|
||||
uid={user.uid}
|
||||
direct={user.direct}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
style.actionSheetPhotoContainer,
|
||||
{ backgroundColor: cam ? undefined : colors.conferenceCallPhotoBackground, width: containerWidth }
|
||||
]}
|
||||
>
|
||||
{cam ? (
|
||||
<Camera style={[style.cameraContainer, { width: containerWidth }]} type={CameraType.front} />
|
||||
) : (
|
||||
<AvatarContainer size={62} text={username} rid={rid} type={user.type} />
|
||||
)}
|
||||
</View>
|
||||
<Button
|
||||
backgroundColor={calling ? colors.conferenceCallCallBackButton : colors.actionTintColor}
|
||||
color={calling ? colors.gray300 : colors.conferenceCallEnabledIcon}
|
||||
onPress={() => {
|
||||
if (calling) {
|
||||
dispatch(cancelCall({}));
|
||||
} else {
|
||||
dispatch(initVideoCall({ cam, mic, direct: user.direct, rid, uid: user.uid }));
|
||||
}
|
||||
}}
|
||||
title={calling ? i18n.t('Cancel') : i18n.t('Call')}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
actionSheetContainer: {
|
||||
paddingHorizontal: 24,
|
||||
flex: 1
|
||||
},
|
||||
actionSheetPhotoContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'center',
|
||||
flex: 1,
|
||||
marginVertical: 8,
|
||||
borderRadius: 8,
|
||||
overflow: 'hidden'
|
||||
},
|
||||
cameraContainer: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
|
@ -1,17 +1,17 @@
|
|||
import { Camera } from 'expo-camera';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useActionSheet } from '../../containers/ActionSheet';
|
||||
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
|
||||
import { ISubscription, SubscriptionType } from '../../definitions';
|
||||
import i18n from '../../i18n';
|
||||
import { getUserSelector } from '../../selectors/login';
|
||||
import { getSubscriptionByRoomId } from '../database/services/Subscription';
|
||||
import { callJitsi } from '../methods';
|
||||
import { compareServerVersion, showErrorAlert } from '../methods/helpers';
|
||||
import { videoConfStartAndJoin } from '../methods/videoConf';
|
||||
import { Services } from '../services';
|
||||
import { useAppSelector } from './useAppSelector';
|
||||
import { useSnaps } from './useSnaps';
|
||||
import { useActionSheet } from '../../../containers/ActionSheet';
|
||||
import { SubscriptionType } from '../../../definitions';
|
||||
import i18n from '../../../i18n';
|
||||
import { getUserSelector } from '../../../selectors/login';
|
||||
import { getSubscriptionByRoomId } from '../../database/services/Subscription';
|
||||
import { compareServerVersion, showErrorAlert } from '../../methods/helpers';
|
||||
import { handleAndroidBltPermission } from '../../methods/videoConf';
|
||||
import { Services } from '../../services';
|
||||
import { useAppSelector } from '../useAppSelector';
|
||||
import { useSnaps } from '../useSnaps';
|
||||
import StartACallActionSheet from './StartACallActionSheet';
|
||||
|
||||
const availabilityErrors = {
|
||||
NOT_CONFIGURED: 'video-conf-provider-not-configured',
|
||||
|
@ -33,6 +33,8 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
|
|||
const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
|
||||
const user = useAppSelector(state => getUserSelector(state));
|
||||
|
||||
const [permission, requestPermission] = Camera.useCameraPermissions();
|
||||
|
||||
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
|
||||
|
||||
const { showActionSheet } = useActionSheet();
|
||||
|
@ -76,19 +78,17 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
|
|||
return true;
|
||||
};
|
||||
|
||||
const initCall = async ({ cam, mic }: { cam: boolean; mic: boolean }) => {
|
||||
if (isServer5OrNewer) return videoConfStartAndJoin({ rid, cam, mic });
|
||||
const room = (await getSubscriptionByRoomId(rid)) as ISubscription;
|
||||
callJitsi({ room, cam });
|
||||
};
|
||||
|
||||
const showInitCallActionSheet = async () => {
|
||||
const canInit = await canInitAnCall();
|
||||
if (canInit) {
|
||||
showActionSheet({
|
||||
children: <StartACallActionSheet rid={rid} initCall={initCall} />,
|
||||
children: <StartACallActionSheet rid={rid} />,
|
||||
snaps
|
||||
});
|
||||
if (!permission?.granted) {
|
||||
requestPermission();
|
||||
handleAndroidBltPermission();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { Notifier } from 'react-native-notifier';
|
||||
|
||||
export const hideNotification = (): void => Notifier.hideNotification();
|
|
@ -34,6 +34,7 @@ import { E2E_MESSAGE_TYPE } from '../../constants';
|
|||
import { getRoom } from '../getRoom';
|
||||
import { merge } from '../helpers/mergeSubscriptionsRooms';
|
||||
import { getRoomAvatar, getRoomTitle, getSenderName, random } from '../helpers';
|
||||
import { handleVideoConfIncomingWebsocketMessages } from '../../../actions/videoConf';
|
||||
|
||||
const removeListener = (listener: { stop: () => void }) => listener.stop();
|
||||
|
||||
|
@ -409,6 +410,11 @@ export default function subscribeRooms() {
|
|||
log(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (/video-conference/.test(ev)) {
|
||||
const [action, params] = ddpMessage.fields.args;
|
||||
store.dispatch(handleVideoConfIncomingWebsocketMessages({ action, params }));
|
||||
}
|
||||
});
|
||||
|
||||
const stop = () => {
|
||||
|
|
|
@ -19,18 +19,17 @@ const handleBltPermission = async (): Promise<Permission[]> => {
|
|||
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
|
||||
};
|
||||
|
||||
export const handleAndroidBltPermission = async (): Promise<void> => {
|
||||
if (isAndroid) {
|
||||
const bltPermission = await handleBltPermission();
|
||||
await PermissionsAndroid.requestMultiple(bltPermission);
|
||||
}
|
||||
};
|
||||
|
||||
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
|
||||
try {
|
||||
const result = await Services.videoConferenceJoin(callId, cam, mic);
|
||||
if (result.success) {
|
||||
if (isAndroid) {
|
||||
const bltPermission = await handleBltPermission();
|
||||
await PermissionsAndroid.requestMultiple([
|
||||
PermissionsAndroid.PERMISSIONS.CAMERA,
|
||||
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
||||
...bltPermission
|
||||
]);
|
||||
}
|
||||
const { url, providerName } = result;
|
||||
if (providerName === 'jitsi') {
|
||||
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });
|
||||
|
@ -43,15 +42,3 @@ export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean
|
|||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
export const videoConfStartAndJoin = async ({ rid, cam, mic }: { rid: string; cam?: boolean; mic?: boolean }): Promise<void> => {
|
||||
try {
|
||||
const videoConfResponse = await Services.videoConferenceStart(rid);
|
||||
if (videoConfResponse.success) {
|
||||
videoConfJoin(videoConfResponse.data.callId, cam, mic);
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorAlert(i18n.t('error-init-video-conf'));
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -947,7 +947,9 @@ export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean
|
|||
|
||||
export const videoConferenceGetCapabilities = () => sdk.get('video-conference.capabilities');
|
||||
|
||||
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });
|
||||
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId, allowRinging: true });
|
||||
|
||||
export const videoConferenceCancel = (callId: string) => sdk.post('video-conference.cancel', { callId });
|
||||
|
||||
export const saveUserProfileMethod = (
|
||||
params: IProfileParams,
|
||||
|
@ -961,3 +963,6 @@ export const saveUserProfileMethod = (
|
|||
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
|
||||
// RC 0.67.0
|
||||
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish });
|
||||
|
||||
export const notifyUser = (type: string, params: Record<string, any>): Promise<boolean> =>
|
||||
sdk.methodCall('stream-notify-user', type, params);
|
||||
|
|
|
@ -21,6 +21,7 @@ import enterpriseModules from './enterpriseModules';
|
|||
import encryption from './encryption';
|
||||
import permissions from './permissions';
|
||||
import roles from './roles';
|
||||
import videoConf from './videoConf';
|
||||
|
||||
export default combineReducers({
|
||||
settings,
|
||||
|
@ -43,5 +44,6 @@ export default combineReducers({
|
|||
enterpriseModules,
|
||||
encryption,
|
||||
permissions,
|
||||
roles
|
||||
roles,
|
||||
videoConf
|
||||
});
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { clearVideoConfCalls, removeVideoConfCall, setVideoConfCall, setCalling } from '../actions/videoConf';
|
||||
import { mockedStore } from './mockedStore';
|
||||
import { initialState, ICallInfo } from './videoConf';
|
||||
|
||||
describe('test videoConf reducer', () => {
|
||||
it('should return initial state', () => {
|
||||
const state = mockedStore.getState().videoConf;
|
||||
expect(state).toEqual(initialState);
|
||||
});
|
||||
|
||||
const call1: ICallInfo = {
|
||||
callId: '123',
|
||||
rid: '123',
|
||||
action: 'accepted',
|
||||
uid: '123'
|
||||
};
|
||||
|
||||
const call2: ICallInfo = {
|
||||
callId: '321',
|
||||
rid: '321',
|
||||
action: 'accepted',
|
||||
uid: '321'
|
||||
};
|
||||
|
||||
it('should return call1 after call addSettings action with call1 as parameter', () => {
|
||||
mockedStore.dispatch(setVideoConfCall(call1));
|
||||
const state = mockedStore.getState().videoConf;
|
||||
const call = state.calls.find(c => c.callId === call1.callId);
|
||||
expect(call).toEqual(call1);
|
||||
});
|
||||
|
||||
it('should return call2 after call addSettings action with call2 as parameter', () => {
|
||||
mockedStore.dispatch(setVideoConfCall(call2));
|
||||
const state = mockedStore.getState().videoConf;
|
||||
const call = state.calls.find(c => c.callId === call2.callId);
|
||||
expect(call).toEqual(call2);
|
||||
});
|
||||
|
||||
it('should remove call1 after call removeVideoConfCall action with call1 as parameter', () => {
|
||||
mockedStore.dispatch(removeVideoConfCall(call1));
|
||||
const state = mockedStore.getState().videoConf;
|
||||
const call = state.calls.find(c => c.callId === call1.callId);
|
||||
expect(call).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should set calling true after call setCalling action with true as parameter', () => {
|
||||
mockedStore.dispatch(setCalling(true));
|
||||
const state = mockedStore.getState().videoConf;
|
||||
expect(state.calling).toEqual(true);
|
||||
});
|
||||
|
||||
it('should set calling false after call setCalling action with false as parameter', () => {
|
||||
mockedStore.dispatch(setCalling(false));
|
||||
const state = mockedStore.getState().videoConf;
|
||||
expect(state.calling).toEqual(false);
|
||||
});
|
||||
|
||||
it('should return initial state after clearSettings', () => {
|
||||
mockedStore.dispatch(clearVideoConfCalls());
|
||||
const state = mockedStore.getState().videoConf;
|
||||
expect(state).toEqual(initialState);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
import { VIDEO_CONF } from '../actions/actionsTypes';
|
||||
import { TActionVideoConf } from '../actions/videoConf';
|
||||
|
||||
export type TSupportedCallStatus = 'call' | 'canceled' | 'accepted' | 'rejected' | 'confirmed' | 'join' | 'end' | 'calling';
|
||||
|
||||
export interface ICallInfo {
|
||||
callId: string;
|
||||
rid: string;
|
||||
uid: string;
|
||||
action?: TSupportedCallStatus;
|
||||
}
|
||||
|
||||
export interface IVideoConf {
|
||||
calls: ICallInfo[];
|
||||
calling: boolean;
|
||||
}
|
||||
|
||||
export const initialState: IVideoConf = { calls: [], calling: false };
|
||||
|
||||
export default (state = initialState, action: TActionVideoConf): IVideoConf => {
|
||||
switch (action.type) {
|
||||
case VIDEO_CONF.SET:
|
||||
return {
|
||||
...state,
|
||||
calls: [...state.calls, action.payload]
|
||||
};
|
||||
case VIDEO_CONF.REMOVE:
|
||||
return {
|
||||
...state,
|
||||
calls: state.calls.filter(call => call.callId !== action.payload.callId)
|
||||
};
|
||||
case VIDEO_CONF.CLEAR:
|
||||
return initialState;
|
||||
case VIDEO_CONF.SET_CALLING:
|
||||
return { ...state, calling: action.payload };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
|
@ -13,6 +13,7 @@ import deepLinking from './deepLinking';
|
|||
import inviteLinks from './inviteLinks';
|
||||
import createDiscussion from './createDiscussion';
|
||||
import encryption from './encryption';
|
||||
import videoConf from './videoConf';
|
||||
|
||||
const root = function* root() {
|
||||
yield all([
|
||||
|
@ -28,7 +29,8 @@ const root = function* root() {
|
|||
inviteLinks(),
|
||||
createDiscussion(),
|
||||
inquiry(),
|
||||
encryption()
|
||||
encryption(),
|
||||
videoConf()
|
||||
]);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,251 @@
|
|||
import { Action } from 'redux';
|
||||
import { delay, put, takeEvery } from 'redux-saga/effects';
|
||||
import { call } from 'typed-redux-saga';
|
||||
|
||||
import { VIDEO_CONF } from '../actions/actionsTypes';
|
||||
import { removeVideoConfCall, setCalling, setVideoConfCall, TCallProps } from '../actions/videoConf';
|
||||
import { hideActionSheetRef } from '../containers/ActionSheet';
|
||||
import { INAPP_NOTIFICATION_EMITTER } from '../containers/InAppNotification';
|
||||
import IncomingCallNotification from '../containers/InAppNotification/IncomingCallNotification';
|
||||
import i18n from '../i18n';
|
||||
import { getSubscriptionByRoomId } from '../lib/database/services/Subscription';
|
||||
import { appSelector } from '../lib/hooks';
|
||||
import { callJitsi } from '../lib/methods';
|
||||
import { compareServerVersion, showErrorAlert } from '../lib/methods/helpers';
|
||||
import EventEmitter from '../lib/methods/helpers/events';
|
||||
import log from '../lib/methods/helpers/log';
|
||||
import { hideNotification } from '../lib/methods/helpers/notifications';
|
||||
import { showToast } from '../lib/methods/helpers/showToast';
|
||||
import { videoConfJoin } from '../lib/methods/videoConf';
|
||||
import { Services } from '../lib/services';
|
||||
import { notifyUser } from '../lib/services/restApi';
|
||||
import { ICallInfo } from '../reducers/videoConf';
|
||||
|
||||
interface IGenericAction extends Action {
|
||||
type: string;
|
||||
}
|
||||
|
||||
type THandleGeneric = IGenericAction & {
|
||||
data: any;
|
||||
};
|
||||
|
||||
type TInitCallGeneric = IGenericAction & {
|
||||
payload: TCallProps;
|
||||
};
|
||||
|
||||
type TCancelCallGeneric = IGenericAction & {
|
||||
payload?: { callId?: string };
|
||||
};
|
||||
|
||||
type TAcceptCallGeneric = IGenericAction & {
|
||||
payload: { callId: string };
|
||||
};
|
||||
|
||||
// The interval between attempts to call the remote user
|
||||
const CALL_INTERVAL = 3000;
|
||||
// How many attempts to call we're gonna make
|
||||
const CALL_ATTEMPT_LIMIT = 10;
|
||||
|
||||
function* onDirectCall(payload: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
const hasAnotherCall = calls.find(c => c.action === 'call');
|
||||
if (hasAnotherCall && hasAnotherCall.callId !== payload.callId) return;
|
||||
if (!currentCall) {
|
||||
yield put(setVideoConfCall(payload));
|
||||
EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, {
|
||||
// @ts-ignore - Component props do not match Event emitter props
|
||||
customComponent: IncomingCallNotification,
|
||||
customTime: 30000,
|
||||
customNotification: true,
|
||||
hideOnPress: false,
|
||||
swipeEnabled: false,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function* onDirectCallCanceled(payload: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
if (currentCall) {
|
||||
yield put(removeVideoConfCall(currentCall));
|
||||
hideNotification();
|
||||
}
|
||||
}
|
||||
|
||||
function* onDirectCallAccepted({ callId, rid, uid, action }: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const userId = yield* appSelector(state => state.login.user.id);
|
||||
const currentCall = calls.find(c => c.callId === callId);
|
||||
if (currentCall && currentCall.action === 'calling') {
|
||||
yield call(notifyUser, `${uid}/video-conference`, { action: 'confirmed', params: { uid: userId, rid, callId } });
|
||||
yield put(setVideoConfCall({ callId, rid, uid, action }));
|
||||
}
|
||||
}
|
||||
|
||||
function* onDirectCallRejected() {
|
||||
yield call(cancelCall, {});
|
||||
showToast(i18n.t('Call_rejected'));
|
||||
yield call(hideActionSheetRef);
|
||||
}
|
||||
|
||||
function* onDirectCallConfirmed(payload: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
if (currentCall) {
|
||||
yield put(removeVideoConfCall(currentCall));
|
||||
yield call(hideActionSheetRef);
|
||||
videoConfJoin(payload.callId, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
function* onDirectCallJoined(payload: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
if (currentCall && (currentCall.action === 'accepted' || currentCall.action === 'calling')) {
|
||||
yield put(setCalling(false));
|
||||
yield put(removeVideoConfCall(currentCall));
|
||||
yield call(hideActionSheetRef);
|
||||
videoConfJoin(payload.callId, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
function* onDirectCallEnded(payload: ICallInfo) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
if (currentCall) {
|
||||
yield put(removeVideoConfCall(currentCall));
|
||||
hideNotification();
|
||||
}
|
||||
}
|
||||
|
||||
function* handleVideoConfIncomingWebsocketMessages({ data }: { data: any }) {
|
||||
const { action, params } = data.action;
|
||||
|
||||
if (!action || typeof action !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (!params || typeof params !== 'object' || !params.callId || !params.uid || !params.rid) {
|
||||
return;
|
||||
}
|
||||
const prop = { ...params, action };
|
||||
switch (action) {
|
||||
case 'call':
|
||||
yield call(onDirectCall, prop);
|
||||
break;
|
||||
case 'canceled':
|
||||
yield call(onDirectCallCanceled, prop);
|
||||
break;
|
||||
case 'accepted':
|
||||
yield call(onDirectCallAccepted, prop);
|
||||
break;
|
||||
case 'rejected':
|
||||
yield call(onDirectCallRejected, prop);
|
||||
break;
|
||||
case 'confirmed':
|
||||
yield call(onDirectCallConfirmed, prop);
|
||||
break;
|
||||
case 'join':
|
||||
yield call(onDirectCallJoined, prop);
|
||||
break;
|
||||
case 'end':
|
||||
yield call(onDirectCallEnded, prop);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function* initCall({ payload: { mic, cam, direct, rid } }: { payload: TCallProps }) {
|
||||
yield put(setCalling(true));
|
||||
const serverVersion = yield* appSelector(state => state.server.version);
|
||||
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
|
||||
if (isServer5OrNewer) {
|
||||
try {
|
||||
const videoConfResponse = yield* call(Services.videoConferenceStart, rid);
|
||||
if (videoConfResponse.success) {
|
||||
if (direct && videoConfResponse.data.type === 'direct') {
|
||||
yield call(callUser, { rid, uid: videoConfResponse.data.calleeId, callId: videoConfResponse.data.callId });
|
||||
} else {
|
||||
videoConfJoin(videoConfResponse.data.callId, cam, mic);
|
||||
yield call(hideActionSheetRef);
|
||||
yield put(setCalling(false));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(setCalling(false));
|
||||
showErrorAlert(i18n.t('error-init-video-conf'));
|
||||
log(e);
|
||||
}
|
||||
} else {
|
||||
const sub = yield* call(getSubscriptionByRoomId, rid);
|
||||
if (sub) {
|
||||
callJitsi({ room: sub, cam });
|
||||
yield put(setCalling(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* giveUp({ rid, uid, callId, rejected }: { rid: string; uid: string; callId: string; rejected?: boolean }) {
|
||||
yield put(removeVideoConfCall({ rid, uid, callId }));
|
||||
yield call(notifyUser, `${uid}/video-conference`, { action: rejected ? 'rejected' : 'canceled', params: { uid, rid, callId } });
|
||||
if (!rejected) {
|
||||
yield put(setCalling(false));
|
||||
yield call(Services.videoConferenceCancel, callId);
|
||||
}
|
||||
}
|
||||
|
||||
function* cancelCall({ payload }: { payload?: { callId?: string } }) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
if (payload?.callId) {
|
||||
const currentCall = calls.find(c => c.callId === payload.callId);
|
||||
if (currentCall) {
|
||||
yield call(giveUp, { ...currentCall, rejected: true });
|
||||
}
|
||||
} else {
|
||||
const currentCall = calls.find(c => c.action === 'calling');
|
||||
if (currentCall && currentCall.callId) {
|
||||
yield call(giveUp, currentCall);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* callUser({ rid, uid, callId }: { rid: string; uid: string; callId: string }) {
|
||||
const userId = yield* appSelector(state => state.login.user.id);
|
||||
yield put(setVideoConfCall({ rid, uid, callId, action: 'calling' }));
|
||||
for (let attempt = 1; attempt <= CALL_ATTEMPT_LIMIT; attempt++) {
|
||||
if (attempt < CALL_ATTEMPT_LIMIT) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === callId);
|
||||
if (!currentCall || currentCall.action !== 'calling') {
|
||||
break;
|
||||
}
|
||||
yield call(notifyUser, `${uid}/video-conference`, { action: 'call', params: { uid: userId, rid, callId } });
|
||||
yield delay(CALL_INTERVAL);
|
||||
} else {
|
||||
hideActionSheetRef();
|
||||
yield call(giveUp, { uid, rid, callId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function* acceptCall({ payload: { callId } }: { payload: { callId: string } }) {
|
||||
const calls = yield* appSelector(state => state.videoConf.calls);
|
||||
const currentCall = calls.find(c => c.callId === callId);
|
||||
if (currentCall && currentCall.action === 'call') {
|
||||
const userId = yield* appSelector(state => state.login.user.id);
|
||||
yield call(notifyUser, `${currentCall.uid}/video-conference`, {
|
||||
action: 'accepted',
|
||||
params: { uid: userId, rid: currentCall.rid, callId: currentCall.callId }
|
||||
});
|
||||
yield put(setVideoConfCall({ ...currentCall, action: 'accepted' }));
|
||||
hideNotification();
|
||||
}
|
||||
}
|
||||
|
||||
export default function* root(): Generator {
|
||||
yield takeEvery<THandleGeneric>(VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES, handleVideoConfIncomingWebsocketMessages);
|
||||
yield takeEvery<TInitCallGeneric>(VIDEO_CONF.INIT_CALL, initCall);
|
||||
yield takeEvery<TCancelCallGeneric>(VIDEO_CONF.CANCEL_CALL, cancelCall);
|
||||
yield takeEvery<TAcceptCallGeneric>(VIDEO_CONF.ACCEPT_CALL, acceptCall);
|
||||
}
|
|
@ -8,6 +8,8 @@ PODS:
|
|||
- EXAV (13.2.1):
|
||||
- ExpoModulesCore
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXCamera (13.2.1):
|
||||
- ExpoModulesCore
|
||||
- EXFileSystem (15.2.2):
|
||||
- ExpoModulesCore
|
||||
- Expo (48.0.9):
|
||||
|
@ -602,6 +604,7 @@ DEPENDENCIES:
|
|||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||
- EXAV (from `../node_modules/expo-av/ios`)
|
||||
- EXCamera (from `../node_modules/expo-camera/ios`)
|
||||
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- Expo (from `../node_modules/expo`)
|
||||
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||
|
@ -721,6 +724,8 @@ EXTERNAL SOURCES:
|
|||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||
EXAV:
|
||||
:path: "../node_modules/expo-av/ios"
|
||||
EXCamera:
|
||||
:path: "../node_modules/expo-camera/ios"
|
||||
EXFileSystem:
|
||||
:path: "../node_modules/expo-file-system/ios"
|
||||
Expo:
|
||||
|
@ -892,6 +897,7 @@ SPEC CHECKSUMS:
|
|||
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
|
||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||
EXAV: f1f69397ecdcf44cfacd4ff5d338cd1b96891e87
|
||||
EXCamera: a323a5942b5e7fc8349e17d728e91c18840ad561
|
||||
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
|
||||
Expo: 863488a600a4565698a79577117c70b170054d08
|
||||
ExpoAppleAuthentication: 7bd5e4150d59e8df37aa80b425850ae88adf9e65
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"@react-native-community/blur": "^4.1.0",
|
||||
"@react-native-community/cameraroll": "4.1.2",
|
||||
"@react-native-community/datetimepicker": "^6.7.5",
|
||||
"@react-native-community/hooks": "2.6.0",
|
||||
"@react-native-community/hooks": "3.0.0",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-community/picker": "^1.8.1",
|
||||
"@react-native-community/slider": "^4.4.2",
|
||||
|
@ -73,6 +73,7 @@
|
|||
"expo": "^48.0.9",
|
||||
"expo-apple-authentication": "^6.0.1",
|
||||
"expo-av": "^13.2.1",
|
||||
"expo-camera": "^13.2.1",
|
||||
"expo-file-system": "^15.2.2",
|
||||
"expo-haptics": "^12.2.1",
|
||||
"expo-keep-awake": "^12.0.1",
|
||||
|
@ -144,6 +145,7 @@
|
|||
"rn-root-view": "RocketChat/rn-root-view",
|
||||
"semver": "^7.3.8",
|
||||
"transliteration": "^2.3.5",
|
||||
"typed-redux-saga": "^1.5.0",
|
||||
"ua-parser-js": "^1.0.32",
|
||||
"uri-js": "^4.4.1",
|
||||
"url-parse": "1.5.10",
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
diff --git a/node_modules/react-native-notifier/src/Notifier.tsx b/node_modules/react-native-notifier/src/Notifier.tsx
|
||||
index 56c5819..4f31e78 100644
|
||||
--- a/node_modules/react-native-notifier/src/Notifier.tsx
|
||||
+++ b/node_modules/react-native-notifier/src/Notifier.tsx
|
||||
@@ -44,6 +44,7 @@ export class NotifierRoot extends React.PureComponent<ShowNotificationParams, St
|
||||
Component: NotificationComponent,
|
||||
swipeEnabled: DEFAULT_SWIPE_ENABLED,
|
||||
componentProps: {},
|
||||
+ visible: false,
|
||||
};
|
||||
this.isShown = false;
|
||||
this.isHiding = false;
|
||||
@@ -146,6 +147,7 @@ export class NotifierRoot extends React.PureComponent<ShowNotificationParams, St
|
||||
Component: Component ?? NotificationComponent,
|
||||
swipeEnabled: swipeEnabled ?? DEFAULT_SWIPE_ENABLED,
|
||||
componentProps: componentProps,
|
||||
+ visible: true,
|
||||
});
|
||||
|
||||
this.showParams = restParams;
|
||||
@@ -188,6 +190,7 @@ export class NotifierRoot extends React.PureComponent<ShowNotificationParams, St
|
||||
}
|
||||
|
||||
private onHidden() {
|
||||
+ this.setState({ visible: false })
|
||||
this.showParams?.onHidden?.();
|
||||
this.isShown = false;
|
||||
this.isHiding = false;
|
||||
@@ -259,7 +262,7 @@ export class NotifierRoot extends React.PureComponent<ShowNotificationParams, St
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={this.onPress}>
|
||||
<View onLayout={this.onLayout}>
|
||||
- <Component title={title} description={description} {...componentProps} />
|
||||
+ {this.state.visible? <Component title={title} description={description} {...componentProps} /> : null}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Animated.View>
|
||||
diff --git a/node_modules/react-native-notifier/src/types.ts b/node_modules/react-native-notifier/src/types.ts
|
||||
index 229e19c..e16a943 100644
|
||||
--- a/node_modules/react-native-notifier/src/types.ts
|
||||
+++ b/node_modules/react-native-notifier/src/types.ts
|
||||
@@ -95,6 +95,7 @@ export interface StateInterface {
|
||||
swipeEnabled: boolean;
|
||||
Component: ElementType;
|
||||
componentProps: Record<string, any>;
|
||||
+ visible: boolean;
|
||||
}
|
||||
|
||||
export interface NotifierInterface {
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
|
||||
"target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||
|
|
54
yarn.lock
54
yarn.lock
|
@ -830,6 +830,13 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.14.5"
|
||||
|
||||
"@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
|
||||
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.18.6"
|
||||
|
||||
"@babel/helper-module-imports@^7.16.7":
|
||||
version "7.16.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
|
||||
|
@ -837,13 +844,6 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.16.7"
|
||||
|
||||
"@babel/helper-module-imports@^7.18.6":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
|
||||
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.18.6"
|
||||
|
||||
"@babel/helper-module-transforms@^7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d"
|
||||
|
@ -4476,6 +4476,13 @@
|
|||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@koale/useworker@^4.0.2":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@koale/useworker/-/useworker-4.0.2.tgz#cb540a2581cd6025307c3ca6685bc60748773e58"
|
||||
integrity sha512-xPIPADtom8/3/4FLNj7MvNcBM/Z2FleH85Fdx2O869eoKW8+PoEgtSVvoxWjCWMA46Sm9A5/R1TyzNGc+yM0wg==
|
||||
dependencies:
|
||||
dequal "^1.0.0"
|
||||
|
||||
"@mdx-js/mdx@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
|
||||
|
@ -4814,10 +4821,10 @@
|
|||
dependencies:
|
||||
invariant "^2.2.4"
|
||||
|
||||
"@react-native-community/hooks@2.6.0":
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.6.0.tgz#dd5f19601eb3684c6bcdd3df3d0c04cf44c24cff"
|
||||
integrity sha512-emBGKvhJ0h++lLJQ5ejsj+od9G67nEaihjvfSx7/JWvNrQGAhP9U0OZqgb9dkKzor9Ufaj9SGt8RNY97cGzttw==
|
||||
"@react-native-community/hooks@3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-3.0.0.tgz#af5f2ca32eea59b792ce9e3d9a4cf0354f9b195f"
|
||||
integrity sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==
|
||||
|
||||
"@react-native-community/netinfo@6.0.0":
|
||||
version "6.0.0"
|
||||
|
@ -5008,7 +5015,7 @@
|
|||
|
||||
"@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile":
|
||||
version "1.3.0-mobile"
|
||||
resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/454b4ba784095057b8de862eb99340311b672e15"
|
||||
resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/ad71e7daa5bcb1a3b457b5de20fb0fc86581d04d"
|
||||
dependencies:
|
||||
js-sha256 "^0.9.0"
|
||||
lru-cache "^4.1.1"
|
||||
|
@ -7316,7 +7323,7 @@ babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0:
|
|||
cosmiconfig "^6.0.0"
|
||||
resolve "^1.12.0"
|
||||
|
||||
babel-plugin-macros@^3.0.1:
|
||||
babel-plugin-macros@^3.0.1, babel-plugin-macros@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
|
||||
integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
|
||||
|
@ -9424,6 +9431,11 @@ deprecated-react-native-prop-types@^3.0.1:
|
|||
invariant "*"
|
||||
prop-types "*"
|
||||
|
||||
dequal@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.1.tgz#dbbf9795ec626e9da8bd68782f4add1d23700d8b"
|
||||
integrity sha512-Fx8jxibzkJX2aJgyfSdLhr9tlRoTnHKrRJuu2XHlAgKioN2j19/Bcbe0d4mFXYZ3+wpE2KVobUVTfDutcD17xQ==
|
||||
|
||||
dequal@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
|
||||
|
@ -10539,6 +10551,14 @@ expo-av@^13.2.1:
|
|||
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-13.2.1.tgz#ce502a4c5d4a57962fd9f5f1a40c76c39c88f5ee"
|
||||
integrity sha512-mC0mYSzaOaZgXjzhW2l4Ag325JjH6q5IbptfwD7gkMOFYy7VPOMxEMUnetadbs3DDzmgE6vUWrTjUIUbwq59qg==
|
||||
|
||||
expo-camera@^13.2.1:
|
||||
version "13.2.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-13.2.1.tgz#bfd1e2248d10a5da43d43a4cc77e378e5acf25bb"
|
||||
integrity sha512-fZdRyF402jJGGmLVlumrLcr5Em9+Y2SO1MIlxLBtHXnybyHbTRMRAbzVapKX1Aryfujqadh+Kl+sdsWYkMuJjg==
|
||||
dependencies:
|
||||
"@koale/useworker" "^4.0.2"
|
||||
invariant "^2.2.4"
|
||||
|
||||
expo-constants@~14.2.0, expo-constants@~14.2.1:
|
||||
version "14.2.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-14.2.1.tgz#b5b6b8079d2082c31ccf2cbc7cf97a0e83c229c3"
|
||||
|
@ -19827,6 +19847,14 @@ type-is@~1.6.18:
|
|||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typed-redux-saga@^1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/typed-redux-saga/-/typed-redux-saga-1.5.0.tgz#f70b47c92c6e29e0184d0c30d563c18d6ad0ae54"
|
||||
integrity sha512-XHKliNtRNUegYAAztbVDb5Q+FMqYNQPaed6Xq2N8kz8AOmiOCVxW3uIj7TEptR1/ms6M9u3HEDfJr4qqz/PYrw==
|
||||
optionalDependencies:
|
||||
"@babel/helper-module-imports" "^7.14.5"
|
||||
babel-plugin-macros "^3.1.0"
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
|
|
Loading…
Reference in New Issue