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 {
|
maven {
|
||||||
url "$rootDir/../node_modules/detox/Detox-android"
|
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 PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET', 'UPDATE']);
|
||||||
export const ROLES = createRequestTypes('ROLES', ['SET', 'UPDATE', 'REMOVE']);
|
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 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 { TIconsName } from '../CustomIcon';
|
||||||
import ActionSheet from './ActionSheet';
|
import ActionSheet from './ActionSheet';
|
||||||
|
@ -47,23 +47,27 @@ export const withActionSheet = (Component: React.ComponentType<any>): typeof Com
|
||||||
return WithActionSheetComponent;
|
return WithActionSheetComponent;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
const actionSheetRef: React.Ref<IActionSheetProvider> = createRef();
|
||||||
const ref: ForwardedRef<IActionSheetProvider> = useRef(null);
|
|
||||||
|
|
||||||
|
export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
|
||||||
const getContext = () => ({
|
const getContext = () => ({
|
||||||
showActionSheet: (options: TActionSheetOptions) => {
|
showActionSheet: (options: TActionSheetOptions) => {
|
||||||
ref.current?.showActionSheet(options);
|
actionSheetRef.current?.showActionSheet(options);
|
||||||
},
|
},
|
||||||
hideActionSheet: () => {
|
hideActionSheet: () => {
|
||||||
ref.current?.hideActionSheet();
|
actionSheetRef.current?.hideActionSheet();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Provider value={getContext()}>
|
<Provider value={getContext()}>
|
||||||
<ActionSheet ref={ref}>
|
<ActionSheet ref={actionSheetRef}>
|
||||||
<>{children}</>
|
<>{children}</>
|
||||||
</ActionSheet>
|
</ActionSheet>
|
||||||
</Provider>
|
</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 { StyleSheet, Text, View } from 'react-native';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Notifier } from 'react-native-notifier';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import Avatar from '../Avatar';
|
import Avatar from '../Avatar';
|
||||||
|
@ -14,6 +13,7 @@ import { ROW_HEIGHT } from '../RoomItem';
|
||||||
import { goRoom } from '../../lib/methods/helpers/goRoom';
|
import { goRoom } from '../../lib/methods/helpers/goRoom';
|
||||||
import { useOrientation } from '../../dimensions';
|
import { useOrientation } from '../../dimensions';
|
||||||
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
|
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
|
||||||
|
import { hideNotification } from '../../lib/methods/helpers/notifications';
|
||||||
|
|
||||||
export interface INotifierComponent {
|
export interface INotifierComponent {
|
||||||
notification: {
|
notification: {
|
||||||
|
@ -21,6 +21,7 @@ export interface INotifierComponent {
|
||||||
payload: {
|
payload: {
|
||||||
sender: { username: string };
|
sender: { username: string };
|
||||||
type: SubscriptionType;
|
type: SubscriptionType;
|
||||||
|
message?: { message: string; t?: string };
|
||||||
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
|
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
|
||||||
title: string;
|
title: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
@ -72,8 +73,6 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const hideNotification = () => Notifier.hideNotification();
|
|
||||||
|
|
||||||
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
|
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const insets = useSafeAreaInsets();
|
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 { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
|
||||||
|
|
||||||
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
|
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
|
||||||
|
@ -15,24 +15,32 @@ const InAppNotification = memo(() => {
|
||||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
|
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const show = (notification: INotifierComponent['notification']) => {
|
const show = (
|
||||||
if (appState !== 'foreground') {
|
notification: INotifierComponent['notification'] & {
|
||||||
return;
|
customComponent?: ElementType;
|
||||||
|
customTime?: number;
|
||||||
|
customNotification?: boolean;
|
||||||
|
hideOnPress?: boolean;
|
||||||
|
swipeEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
) => {
|
||||||
|
if (appState !== 'foreground') return;
|
||||||
|
|
||||||
const { payload } = notification;
|
const { payload } = notification;
|
||||||
const state = Navigation.navigationRef.current?.getRootState();
|
const state = Navigation.navigationRef.current?.getRootState();
|
||||||
const route = getActiveRoute(state);
|
const route = getActiveRoute(state);
|
||||||
if (payload.rid) {
|
if (payload?.rid || notification.customNotification) {
|
||||||
if (payload.rid === subscribedRoom || route?.name === 'JitsiMeetView') {
|
if (payload?.rid === subscribedRoom || route?.name === 'JitsiMeetView' || payload?.message?.t === 'videoconf') return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
Notifier.showNotification({
|
Notifier.showNotification({
|
||||||
showEasing: Easing.inOut(Easing.quad),
|
showEasing: Easing.inOut(Easing.quad),
|
||||||
Component: NotifierComponent,
|
Component: notification.customComponent || NotifierComponent,
|
||||||
componentProps: {
|
componentProps: {
|
||||||
notification
|
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}: `;
|
prefix = `${useRealName ? name : lastMessage.u.username}: `;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lastMessage.t === 'videoconf') {
|
||||||
|
prefix = '';
|
||||||
|
lastMessage.msg = I18n.t('Call_started');
|
||||||
|
}
|
||||||
|
|
||||||
return `${prefix}${lastMessage.msg}`;
|
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 React from 'react';
|
||||||
import { Text } from 'react-native';
|
import { Text } from 'react-native';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
|
||||||
|
|
||||||
import i18n from '../../../../i18n';
|
import i18n from '../../../../i18n';
|
||||||
import { videoConfJoin } from '../../../../lib/methods/videoConf';
|
|
||||||
import useStyle from './styles';
|
import useStyle from './styles';
|
||||||
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
|
||||||
|
|
||||||
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
|
const VideoConferenceDirect = React.memo(() => {
|
||||||
const style = useStyle();
|
const style = useStyle();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoConferenceBaseContainer variant='incoming'>
|
<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>
|
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>
|
||||||
</VideoConferenceBaseContainer>
|
</VideoConferenceBaseContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import sharedStyles from '../../../../views/Styles';
|
||||||
|
|
||||||
export default function useStyle() {
|
export default function useStyle() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
return StyleSheet.create({
|
const style = StyleSheet.create({
|
||||||
container: { height: 108, flex: 1, borderWidth: 1, borderRadius: 4, marginTop: 8, borderColor: colors.conferenceCallBorder },
|
container: { height: 108, flex: 1, borderWidth: 1, borderRadius: 4, marginTop: 8, borderColor: colors.conferenceCallBorder },
|
||||||
callInfoContainer: { flex: 1, alignItems: 'center', paddingLeft: 16, flexDirection: 'row' },
|
callInfoContainer: { flex: 1, alignItems: 'center', paddingLeft: 16, flexDirection: 'row' },
|
||||||
infoContainerText: {
|
infoContainerText: {
|
||||||
|
@ -88,40 +88,10 @@ export default function useStyle() {
|
||||||
...sharedStyles.textRegular,
|
...sharedStyles.textRegular,
|
||||||
color: colors.passcodeSecondary
|
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: {
|
enabledBackground: {
|
||||||
backgroundColor: colors.conferenceCallEnabledIconBackground
|
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 ('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} />;
|
return <VideoConferenceOutgoing blockId={blockId} users={users} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,8 @@ const MarkdownPreview = ({ msg, numberOfLines = 1, style = [], testID }: IMarkdo
|
||||||
accessibilityLabel={m}
|
accessibilityLabel={m}
|
||||||
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
|
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
testID={testID || `markdown-preview-${m}`}>
|
testID={testID || `markdown-preview-${m}`}
|
||||||
|
>
|
||||||
{m}
|
{m}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
|
|
@ -107,3 +107,5 @@ export type VideoConfListProps = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VideoConfInfoProps = { callId: string };
|
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 { TActionUserTyping } from '../../actions/usersTyping';
|
||||||
import { TActionPermissions } from '../../actions/permissions';
|
import { TActionPermissions } from '../../actions/permissions';
|
||||||
import { TActionEnterpriseModules } from '../../actions/enterpriseModules';
|
import { TActionEnterpriseModules } from '../../actions/enterpriseModules';
|
||||||
|
import { TActionVideoConf } from '../../actions/videoConf';
|
||||||
// REDUCERS
|
// REDUCERS
|
||||||
import { IActiveUsers } from '../../reducers/activeUsers';
|
import { IActiveUsers } from '../../reducers/activeUsers';
|
||||||
import { IApp } from '../../reducers/app';
|
import { IApp } from '../../reducers/app';
|
||||||
|
@ -34,6 +35,7 @@ import { IShare } from '../../reducers/share';
|
||||||
import { IInquiry } from '../../ee/omnichannel/reducers/inquiry';
|
import { IInquiry } from '../../ee/omnichannel/reducers/inquiry';
|
||||||
import { IPermissionsState } from '../../reducers/permissions';
|
import { IPermissionsState } from '../../reducers/permissions';
|
||||||
import { IEnterpriseModules } from '../../reducers/enterpriseModules';
|
import { IEnterpriseModules } from '../../reducers/enterpriseModules';
|
||||||
|
import { IVideoConf } from '../../reducers/videoConf';
|
||||||
|
|
||||||
export interface IApplicationState {
|
export interface IApplicationState {
|
||||||
settings: TSettingsState;
|
settings: TSettingsState;
|
||||||
|
@ -57,6 +59,7 @@ export interface IApplicationState {
|
||||||
encryption: IEncryption;
|
encryption: IEncryption;
|
||||||
permissions: IPermissionsState;
|
permissions: IPermissionsState;
|
||||||
roles: IRoles;
|
roles: IRoles;
|
||||||
|
videoConf: IVideoConf;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TApplicationActions = TActionActiveUsers &
|
export type TApplicationActions = TActionActiveUsers &
|
||||||
|
@ -75,4 +78,5 @@ export type TApplicationActions = TActionActiveUsers &
|
||||||
TActionApp &
|
TActionApp &
|
||||||
TActionInquiry &
|
TActionInquiry &
|
||||||
TActionPermissions &
|
TActionPermissions &
|
||||||
TActionEnterpriseModules;
|
TActionEnterpriseModules &
|
||||||
|
TActionVideoConf;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
VideoConfCall,
|
||||||
VideoConfCancelProps,
|
VideoConfCancelProps,
|
||||||
VideoConference,
|
VideoConference,
|
||||||
VideoConferenceCapabilities,
|
VideoConferenceCapabilities,
|
||||||
|
@ -24,7 +25,7 @@ export type VideoConferenceEndpoints = {
|
||||||
};
|
};
|
||||||
|
|
||||||
'video-conference.info': {
|
'video-conference.info': {
|
||||||
GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
|
GET: (params: VideoConfInfoProps) => VideoConfCall;
|
||||||
};
|
};
|
||||||
|
|
||||||
'video-conference.list': {
|
'video-conference.list': {
|
||||||
|
|
|
@ -691,11 +691,12 @@
|
||||||
"Waiting_for_answer": "Waiting for answer",
|
"Waiting_for_answer": "Waiting for answer",
|
||||||
"Call_ended": "Call ended",
|
"Call_ended": "Call ended",
|
||||||
"Call_was_not_answered": "Call was not answered",
|
"Call_was_not_answered": "Call was not answered",
|
||||||
"Call_back": "Call back",
|
"Call_rejected": "Call rejected",
|
||||||
"Call_again": "Call again",
|
"Call_back": "Call Back",
|
||||||
"Call_ongoing": "Call ongoing",
|
"Call_again": "Call Again",
|
||||||
|
"Call_ongoing": "Call Ongoing",
|
||||||
"Joined": "Joined",
|
"Joined": "Joined",
|
||||||
"Calling": "Calling...",
|
"Calling": "Calling",
|
||||||
"Start_a_call": "Start a call",
|
"Start_a_call": "Start a call",
|
||||||
"Call": "Call",
|
"Call": "Call",
|
||||||
"Reply_in_direct_message": "Reply in direct message",
|
"Reply_in_direct_message": "Reply in direct message",
|
||||||
|
@ -724,5 +725,9 @@
|
||||||
"Presence_Cap_Warning_Title": "User status temporarily disabled",
|
"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.",
|
"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",
|
"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_again": "Soita uudelleen",
|
||||||
"Call_ongoing": "Puhelu käynnissä",
|
"Call_ongoing": "Puhelu käynnissä",
|
||||||
"Joined": "Liitytty",
|
"Joined": "Liitytty",
|
||||||
"Calling": "Soitetaan...",
|
"Calling": "Soitetaan",
|
||||||
"Start_a_call": "Aloita puhelu",
|
"Start_a_call": "Aloita puhelu",
|
||||||
"Call": "Soita",
|
"Call": "Soita",
|
||||||
"Reply_in_direct_message": "Vastaa suoralla viestillä",
|
"Reply_in_direct_message": "Vastaa suoralla viestillä",
|
||||||
|
|
|
@ -695,7 +695,7 @@
|
||||||
"Call_again": "Ligue novamente",
|
"Call_again": "Ligue novamente",
|
||||||
"Call_ongoing": "Chamada em andamento",
|
"Call_ongoing": "Chamada em andamento",
|
||||||
"Joined": "Ingressou",
|
"Joined": "Ingressou",
|
||||||
"Calling": "Chamando...",
|
"Calling": "Chamando",
|
||||||
"Start_a_call": "Inicie uma chamada",
|
"Start_a_call": "Inicie uma chamada",
|
||||||
"Call": "Ligar",
|
"Call": "Ligar",
|
||||||
"Reply_in_direct_message": "Responder por mensagem direta",
|
"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.",
|
"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_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.",
|
"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",
|
"Learn_more": "Saiba mais",
|
||||||
"and_N_more": "e mais {{count}}"
|
"and_N_more": "e mais {{count}}"
|
||||||
}
|
}
|
|
@ -693,7 +693,7 @@
|
||||||
"Call_again": "Ring igen",
|
"Call_again": "Ring igen",
|
||||||
"Call_ongoing": "Samtal pågår",
|
"Call_ongoing": "Samtal pågår",
|
||||||
"Joined": "Anslöt",
|
"Joined": "Anslöt",
|
||||||
"Calling": "Ringer upp...",
|
"Calling": "Ringer upp",
|
||||||
"Start_a_call": "Starta ett samtal",
|
"Start_a_call": "Starta ett samtal",
|
||||||
"Call": "Ring",
|
"Call": "Ring",
|
||||||
"Reply_in_direct_message": "Svara med direktmeddelande",
|
"Reply_in_direct_message": "Svara med direktmeddelande",
|
||||||
|
|
|
@ -20,6 +20,11 @@ const mentions = {
|
||||||
mentionOtherColor: '#F3BE08'
|
mentionOtherColor: '#F3BE08'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const callButtons = {
|
||||||
|
cancelCallButton: '#F5455C',
|
||||||
|
acceptCallButton: '#158D65'
|
||||||
|
};
|
||||||
|
|
||||||
export const colors = {
|
export const colors = {
|
||||||
light: {
|
light: {
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
|
@ -89,7 +94,13 @@ export const colors = {
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#E4E7EA',
|
textInputSecondaryBackground: '#E4E7EA',
|
||||||
...mentions
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#1F2329',
|
||||||
|
...mentions,
|
||||||
|
...callButtons
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: '#030b1b',
|
backgroundColor: '#030b1b',
|
||||||
|
@ -158,8 +169,14 @@ export const colors = {
|
||||||
conferenceCallEnabledIcon: '#FFFFFF',
|
conferenceCallEnabledIcon: '#FFFFFF',
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#030b1b', // backgroundColor
|
textInputSecondaryBackground: '#030b1b',
|
||||||
...mentions
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#FFFFFF',
|
||||||
|
...mentions,
|
||||||
|
...callButtons
|
||||||
},
|
},
|
||||||
black: {
|
black: {
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
|
@ -228,8 +245,14 @@ export const colors = {
|
||||||
conferenceCallEnabledIcon: '#FFFFFF',
|
conferenceCallEnabledIcon: '#FFFFFF',
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#000000', // backgroundColor
|
textInputSecondaryBackground: '#000000',
|
||||||
...mentions
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#FFFFFF',
|
||||||
|
...mentions,
|
||||||
|
...callButtons
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import database from '..';
|
import database from '..';
|
||||||
|
import { TSubscriptionModel } from '../../../definitions';
|
||||||
import { TAppDatabase } from '../interfaces';
|
import { TAppDatabase } from '../interfaces';
|
||||||
import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
|
import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
|
||||||
|
|
||||||
const getCollection = (db: TAppDatabase) => db.get(SUBSCRIPTIONS_TABLE);
|
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 db = database.active;
|
||||||
const subCollection = getCollection(db);
|
const subCollection = getCollection(db);
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
import { TypedUseSelectorHook, useSelector } from 'react-redux';
|
||||||
|
import { select } from 'redux-saga/effects';
|
||||||
|
|
||||||
import { IApplicationState } from '../../definitions';
|
import { IApplicationState } from '../../definitions';
|
||||||
|
|
||||||
export const useAppSelector: TypedUseSelectorHook<IApplicationState> = useSelector;
|
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 React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useActionSheet } from '../../containers/ActionSheet';
|
import { useActionSheet } from '../../../containers/ActionSheet';
|
||||||
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
|
import { SubscriptionType } from '../../../definitions';
|
||||||
import { ISubscription, SubscriptionType } from '../../definitions';
|
import i18n from '../../../i18n';
|
||||||
import i18n from '../../i18n';
|
import { getUserSelector } from '../../../selectors/login';
|
||||||
import { getUserSelector } from '../../selectors/login';
|
import { getSubscriptionByRoomId } from '../../database/services/Subscription';
|
||||||
import { getSubscriptionByRoomId } from '../database/services/Subscription';
|
import { compareServerVersion, showErrorAlert } from '../../methods/helpers';
|
||||||
import { callJitsi } from '../methods';
|
import { handleAndroidBltPermission } from '../../methods/videoConf';
|
||||||
import { compareServerVersion, showErrorAlert } from '../methods/helpers';
|
import { Services } from '../../services';
|
||||||
import { videoConfStartAndJoin } from '../methods/videoConf';
|
import { useAppSelector } from '../useAppSelector';
|
||||||
import { Services } from '../services';
|
import { useSnaps } from '../useSnaps';
|
||||||
import { useAppSelector } from './useAppSelector';
|
import StartACallActionSheet from './StartACallActionSheet';
|
||||||
import { useSnaps } from './useSnaps';
|
|
||||||
|
|
||||||
const availabilityErrors = {
|
const availabilityErrors = {
|
||||||
NOT_CONFIGURED: 'video-conf-provider-not-configured',
|
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 jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
|
||||||
const user = useAppSelector(state => getUserSelector(state));
|
const user = useAppSelector(state => getUserSelector(state));
|
||||||
|
|
||||||
|
const [permission, requestPermission] = Camera.useCameraPermissions();
|
||||||
|
|
||||||
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
|
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
|
||||||
|
|
||||||
const { showActionSheet } = useActionSheet();
|
const { showActionSheet } = useActionSheet();
|
||||||
|
@ -76,19 +78,17 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
|
||||||
return true;
|
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 showInitCallActionSheet = async () => {
|
||||||
const canInit = await canInitAnCall();
|
const canInit = await canInitAnCall();
|
||||||
if (canInit) {
|
if (canInit) {
|
||||||
showActionSheet({
|
showActionSheet({
|
||||||
children: <StartACallActionSheet rid={rid} initCall={initCall} />,
|
children: <StartACallActionSheet rid={rid} />,
|
||||||
snaps
|
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 { getRoom } from '../getRoom';
|
||||||
import { merge } from '../helpers/mergeSubscriptionsRooms';
|
import { merge } from '../helpers/mergeSubscriptionsRooms';
|
||||||
import { getRoomAvatar, getRoomTitle, getSenderName, random } from '../helpers';
|
import { getRoomAvatar, getRoomTitle, getSenderName, random } from '../helpers';
|
||||||
|
import { handleVideoConfIncomingWebsocketMessages } from '../../../actions/videoConf';
|
||||||
|
|
||||||
const removeListener = (listener: { stop: () => void }) => listener.stop();
|
const removeListener = (listener: { stop: () => void }) => listener.stop();
|
||||||
|
|
||||||
|
@ -409,6 +410,11 @@ export default function subscribeRooms() {
|
||||||
log(e);
|
log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/video-conference/.test(ev)) {
|
||||||
|
const [action, params] = ddpMessage.fields.args;
|
||||||
|
store.dispatch(handleVideoConfIncomingWebsocketMessages({ action, params }));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const stop = () => {
|
const stop = () => {
|
||||||
|
|
|
@ -19,18 +19,17 @@ const handleBltPermission = async (): Promise<Permission[]> => {
|
||||||
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
|
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> => {
|
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const result = await Services.videoConferenceJoin(callId, cam, mic);
|
const result = await Services.videoConferenceJoin(callId, cam, mic);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
if (isAndroid) {
|
|
||||||
const bltPermission = await handleBltPermission();
|
|
||||||
await PermissionsAndroid.requestMultiple([
|
|
||||||
PermissionsAndroid.PERMISSIONS.CAMERA,
|
|
||||||
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
||||||
...bltPermission
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
const { url, providerName } = result;
|
const { url, providerName } = result;
|
||||||
if (providerName === 'jitsi') {
|
if (providerName === 'jitsi') {
|
||||||
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });
|
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });
|
||||||
|
@ -43,15 +42,3 @@ export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean
|
||||||
log(e);
|
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 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 = (
|
export const saveUserProfileMethod = (
|
||||||
params: IProfileParams,
|
params: IProfileParams,
|
||||||
|
@ -961,3 +963,6 @@ export const saveUserProfileMethod = (
|
||||||
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
|
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
|
||||||
// RC 0.67.0
|
// RC 0.67.0
|
||||||
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish });
|
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 encryption from './encryption';
|
||||||
import permissions from './permissions';
|
import permissions from './permissions';
|
||||||
import roles from './roles';
|
import roles from './roles';
|
||||||
|
import videoConf from './videoConf';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
settings,
|
settings,
|
||||||
|
@ -43,5 +44,6 @@ export default combineReducers({
|
||||||
enterpriseModules,
|
enterpriseModules,
|
||||||
encryption,
|
encryption,
|
||||||
permissions,
|
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 inviteLinks from './inviteLinks';
|
||||||
import createDiscussion from './createDiscussion';
|
import createDiscussion from './createDiscussion';
|
||||||
import encryption from './encryption';
|
import encryption from './encryption';
|
||||||
|
import videoConf from './videoConf';
|
||||||
|
|
||||||
const root = function* root() {
|
const root = function* root() {
|
||||||
yield all([
|
yield all([
|
||||||
|
@ -28,7 +29,8 @@ const root = function* root() {
|
||||||
inviteLinks(),
|
inviteLinks(),
|
||||||
createDiscussion(),
|
createDiscussion(),
|
||||||
inquiry(),
|
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):
|
- EXAV (13.2.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
|
- EXCamera (13.2.1):
|
||||||
|
- ExpoModulesCore
|
||||||
- EXFileSystem (15.2.2):
|
- EXFileSystem (15.2.2):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- Expo (48.0.9):
|
- Expo (48.0.9):
|
||||||
|
@ -602,6 +604,7 @@ DEPENDENCIES:
|
||||||
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
|
||||||
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
|
||||||
- EXAV (from `../node_modules/expo-av/ios`)
|
- EXAV (from `../node_modules/expo-av/ios`)
|
||||||
|
- EXCamera (from `../node_modules/expo-camera/ios`)
|
||||||
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
|
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
|
||||||
|
@ -721,6 +724,8 @@ EXTERNAL SOURCES:
|
||||||
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
|
||||||
EXAV:
|
EXAV:
|
||||||
:path: "../node_modules/expo-av/ios"
|
:path: "../node_modules/expo-av/ios"
|
||||||
|
EXCamera:
|
||||||
|
:path: "../node_modules/expo-camera/ios"
|
||||||
EXFileSystem:
|
EXFileSystem:
|
||||||
:path: "../node_modules/expo-file-system/ios"
|
:path: "../node_modules/expo-file-system/ios"
|
||||||
Expo:
|
Expo:
|
||||||
|
@ -892,6 +897,7 @@ SPEC CHECKSUMS:
|
||||||
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
|
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
|
||||||
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
|
||||||
EXAV: f1f69397ecdcf44cfacd4ff5d338cd1b96891e87
|
EXAV: f1f69397ecdcf44cfacd4ff5d338cd1b96891e87
|
||||||
|
EXCamera: a323a5942b5e7fc8349e17d728e91c18840ad561
|
||||||
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
|
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
|
||||||
Expo: 863488a600a4565698a79577117c70b170054d08
|
Expo: 863488a600a4565698a79577117c70b170054d08
|
||||||
ExpoAppleAuthentication: 7bd5e4150d59e8df37aa80b425850ae88adf9e65
|
ExpoAppleAuthentication: 7bd5e4150d59e8df37aa80b425850ae88adf9e65
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
"@react-native-community/blur": "^4.1.0",
|
"@react-native-community/blur": "^4.1.0",
|
||||||
"@react-native-community/cameraroll": "4.1.2",
|
"@react-native-community/cameraroll": "4.1.2",
|
||||||
"@react-native-community/datetimepicker": "^6.7.5",
|
"@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/netinfo": "6.0.0",
|
||||||
"@react-native-community/picker": "^1.8.1",
|
"@react-native-community/picker": "^1.8.1",
|
||||||
"@react-native-community/slider": "^4.4.2",
|
"@react-native-community/slider": "^4.4.2",
|
||||||
|
@ -73,6 +73,7 @@
|
||||||
"expo": "^48.0.9",
|
"expo": "^48.0.9",
|
||||||
"expo-apple-authentication": "^6.0.1",
|
"expo-apple-authentication": "^6.0.1",
|
||||||
"expo-av": "^13.2.1",
|
"expo-av": "^13.2.1",
|
||||||
|
"expo-camera": "^13.2.1",
|
||||||
"expo-file-system": "^15.2.2",
|
"expo-file-system": "^15.2.2",
|
||||||
"expo-haptics": "^12.2.1",
|
"expo-haptics": "^12.2.1",
|
||||||
"expo-keep-awake": "^12.0.1",
|
"expo-keep-awake": "^12.0.1",
|
||||||
|
@ -144,6 +145,7 @@
|
||||||
"rn-root-view": "RocketChat/rn-root-view",
|
"rn-root-view": "RocketChat/rn-root-view",
|
||||||
"semver": "^7.3.8",
|
"semver": "^7.3.8",
|
||||||
"transliteration": "^2.3.5",
|
"transliteration": "^2.3.5",
|
||||||
|
"typed-redux-saga": "^1.5.0",
|
||||||
"ua-parser-js": "^1.0.32",
|
"ua-parser-js": "^1.0.32",
|
||||||
"uri-js": "^4.4.1",
|
"uri-js": "^4.4.1",
|
||||||
"url-parse": "1.5.10",
|
"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 */
|
/* Basic Options */
|
||||||
// "incremental": true, /* Enable incremental compilation */
|
// "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'. */,
|
"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. */
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||||
|
|
54
yarn.lock
54
yarn.lock
|
@ -830,6 +830,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.14.5"
|
"@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":
|
"@babel/helper-module-imports@^7.16.7":
|
||||||
version "7.16.7"
|
version "7.16.7"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
|
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
|
||||||
|
@ -837,13 +844,6 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.16.7"
|
"@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":
|
"@babel/helper-module-transforms@^7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d"
|
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/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@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":
|
"@mdx-js/mdx@^1.6.22":
|
||||||
version "1.6.22"
|
version "1.6.22"
|
||||||
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
|
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
|
||||||
|
@ -4814,10 +4821,10 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
|
|
||||||
"@react-native-community/hooks@2.6.0":
|
"@react-native-community/hooks@3.0.0":
|
||||||
version "2.6.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.6.0.tgz#dd5f19601eb3684c6bcdd3df3d0c04cf44c24cff"
|
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-3.0.0.tgz#af5f2ca32eea59b792ce9e3d9a4cf0354f9b195f"
|
||||||
integrity sha512-emBGKvhJ0h++lLJQ5ejsj+od9G67nEaihjvfSx7/JWvNrQGAhP9U0OZqgb9dkKzor9Ufaj9SGt8RNY97cGzttw==
|
integrity sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==
|
||||||
|
|
||||||
"@react-native-community/netinfo@6.0.0":
|
"@react-native-community/netinfo@6.0.0":
|
||||||
version "6.0.0"
|
version "6.0.0"
|
||||||
|
@ -5008,7 +5015,7 @@
|
||||||
|
|
||||||
"@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile":
|
"@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile":
|
||||||
version "1.3.0-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:
|
dependencies:
|
||||||
js-sha256 "^0.9.0"
|
js-sha256 "^0.9.0"
|
||||||
lru-cache "^4.1.1"
|
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"
|
cosmiconfig "^6.0.0"
|
||||||
resolve "^1.12.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"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
|
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
|
||||||
integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
|
integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
|
||||||
|
@ -9424,6 +9431,11 @@ deprecated-react-native-prop-types@^3.0.1:
|
||||||
invariant "*"
|
invariant "*"
|
||||||
prop-types "*"
|
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:
|
dequal@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
|
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"
|
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-13.2.1.tgz#ce502a4c5d4a57962fd9f5f1a40c76c39c88f5ee"
|
||||||
integrity sha512-mC0mYSzaOaZgXjzhW2l4Ag325JjH6q5IbptfwD7gkMOFYy7VPOMxEMUnetadbs3DDzmgE6vUWrTjUIUbwq59qg==
|
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:
|
expo-constants@~14.2.0, expo-constants@~14.2.1:
|
||||||
version "14.2.1"
|
version "14.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-14.2.1.tgz#b5b6b8079d2082c31ccf2cbc7cf97a0e83c229c3"
|
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"
|
media-typer "0.3.0"
|
||||||
mime-types "~2.1.24"
|
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:
|
typedarray-to-buffer@^3.1.5:
|
||||||
version "3.1.5"
|
version "3.1.5"
|
||||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||||
|
|
Loading…
Reference in New Issue