Merge branch 'develop' into feat.media-auto-download-preference
This commit is contained in:
commit
65346b041c
|
@ -14,6 +14,9 @@
|
||||||
<!-- permissions related to jitsi call -->
|
<!-- permissions related to jitsi call -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
|
||||||
|
<!-- android 13 notifications -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="chat.rocket.reactnative.MainApplication"
|
android:name="chat.rocket.reactnative.MainApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
|
|
|
@ -5,10 +5,10 @@ buildscript {
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
// TODO: target 33
|
// TODO: target 33
|
||||||
buildToolsVersion = "31.0.0"
|
buildToolsVersion = "33.0.0"
|
||||||
minSdkVersion = 23
|
minSdkVersion = 23
|
||||||
compileSdkVersion = 31
|
compileSdkVersion = 33
|
||||||
targetSdkVersion = 31
|
targetSdkVersion = 33
|
||||||
if (System.properties['os.arch'] == "aarch64") {
|
if (System.properties['os.arch'] == "aarch64") {
|
||||||
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
// For M1 Users we need to use the NDK 24 which added support for aarch64
|
||||||
ndkVersion = "24.0.8215888"
|
ndkVersion = "24.0.8215888"
|
||||||
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,3 +85,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,7 +1,6 @@
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { StatusBar as StatusBarRN } from 'react-native';
|
import { StatusBar as StatusBarRN } from 'react-native';
|
||||||
|
|
||||||
import { themes } from '../lib/constants';
|
|
||||||
import { useTheme } from '../theme';
|
import { useTheme } from '../theme';
|
||||||
|
|
||||||
const supportedStyles = {
|
const supportedStyles = {
|
||||||
|
@ -15,14 +14,20 @@ interface IStatusBar {
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
|
const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
|
||||||
const { theme } = useTheme();
|
const { theme, colors } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
if (!barStyle) {
|
if (!barStyle) {
|
||||||
barStyle = 'light-content';
|
barStyle = 'light-content';
|
||||||
if (theme === 'light') {
|
if (theme === 'light') {
|
||||||
barStyle = 'dark-content';
|
barStyle = 'dark-content';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return <StatusBarRN backgroundColor={backgroundColor ?? themes[theme].headerBackground} barStyle={barStyle} animated />;
|
StatusBarRN.setBackgroundColor(backgroundColor ?? colors.headerBackground);
|
||||||
|
StatusBarRN.setBarStyle(barStyle, true);
|
||||||
|
}, [theme, barStyle, backgroundColor]);
|
||||||
|
|
||||||
|
return <StatusBarRN />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default StatusBar;
|
export default StatusBar;
|
||||||
|
|
|
@ -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} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
@ -731,5 +732,9 @@
|
||||||
"Wi_Fi_and_mobile_data":"Wi-Fi and mobile data",
|
"Wi_Fi_and_mobile_data":"Wi-Fi and mobile data",
|
||||||
"Wi_Fi": "Wi-Fi",
|
"Wi_Fi": "Wi-Fi",
|
||||||
"Off": "Off",
|
"Off": "Off",
|
||||||
"Audio": "Audio"
|
"Audio": "Audio",
|
||||||
|
"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",
|
||||||
|
@ -719,5 +719,9 @@
|
||||||
"Wi_Fi_and_mobile_data":"Wi-Fi e dados móveis",
|
"Wi_Fi_and_mobile_data":"Wi-Fi e dados móveis",
|
||||||
"Wi_Fi": "Wi-Fi",
|
"Wi_Fi": "Wi-Fi",
|
||||||
"Off": "Desativado",
|
"Off": "Desativado",
|
||||||
"Audio": "Áudio"
|
"Audio": "Áudio",
|
||||||
|
"decline": "Recusar",
|
||||||
|
"accept": "Aceitar",
|
||||||
|
"Incoming_call_from": "Chamada recebida de",
|
||||||
|
"Call_started": "Chamada Iniciada"
|
||||||
}
|
}
|
|
@ -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,8 +94,14 @@ export const colors = {
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#E4E7EA',
|
textInputSecondaryBackground: '#E4E7EA',
|
||||||
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#1F2329',
|
||||||
overlayColor: '#1F2329B2',
|
overlayColor: '#1F2329B2',
|
||||||
...mentions
|
...mentions,
|
||||||
|
...callButtons
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
backgroundColor: '#030b1b',
|
backgroundColor: '#030b1b',
|
||||||
|
@ -159,9 +170,15 @@ export const colors = {
|
||||||
conferenceCallEnabledIcon: '#FFFFFF',
|
conferenceCallEnabledIcon: '#FFFFFF',
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#030b1b', // backgroundColor
|
textInputSecondaryBackground: '#030b1b',
|
||||||
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#FFFFFF',
|
||||||
overlayColor: '#1F2329B2',
|
overlayColor: '#1F2329B2',
|
||||||
...mentions
|
...mentions,
|
||||||
|
...callButtons
|
||||||
},
|
},
|
||||||
black: {
|
black: {
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#000000',
|
||||||
|
@ -230,9 +247,15 @@ export const colors = {
|
||||||
conferenceCallEnabledIcon: '#FFFFFF',
|
conferenceCallEnabledIcon: '#FFFFFF',
|
||||||
conferenceCallEnabledIconBackground: '#156FF5',
|
conferenceCallEnabledIconBackground: '#156FF5',
|
||||||
conferenceCallPhotoBackground: '#E4E7EA',
|
conferenceCallPhotoBackground: '#E4E7EA',
|
||||||
textInputSecondaryBackground: '#000000', // backgroundColor
|
textInputSecondaryBackground: '#000000',
|
||||||
|
dotBg: '#a9cbff',
|
||||||
|
dotActiveBg: '#1d74f5',
|
||||||
|
gray300: '#5f656e',
|
||||||
|
gray100: '#CBCED1',
|
||||||
|
n900: '#FFFFFF',
|
||||||
overlayColor: '#1F2329B2',
|
overlayColor: '#1F2329B2',
|
||||||
...mentions
|
...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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,7 +9,10 @@ function normalizeAttachments(msg: TMsg) {
|
||||||
if (typeof msg.attachments !== typeof [] || !msg.attachments || !msg.attachments.length) {
|
if (typeof msg.attachments !== typeof [] || !msg.attachments || !msg.attachments.length) {
|
||||||
msg.attachments = [];
|
msg.attachments = [];
|
||||||
}
|
}
|
||||||
msg.attachments = msg.attachments.map(att => {
|
|
||||||
|
msg.attachments = msg.attachments
|
||||||
|
.filter(att => !!att)
|
||||||
|
.map(att => {
|
||||||
att.fields = att.fields || [];
|
att.fields = att.fields || [];
|
||||||
if (att.ts) {
|
if (att.ts) {
|
||||||
att.ts = moment(att.ts).toDate();
|
att.ts = moment(att.ts).toDate();
|
||||||
|
|
|
@ -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,10 @@ 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
NotificationAction,
|
NotificationAction,
|
||||||
NotificationCategory
|
NotificationCategory
|
||||||
} from 'react-native-notifications';
|
} from 'react-native-notifications';
|
||||||
|
import { PermissionsAndroid, Platform } from 'react-native';
|
||||||
|
|
||||||
import { INotification } from '../../definitions';
|
import { INotification } from '../../definitions';
|
||||||
import { isIOS } from '../methods/helpers';
|
import { isIOS } from '../methods/helpers';
|
||||||
|
@ -36,9 +37,16 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
|
||||||
});
|
});
|
||||||
const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
|
const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
|
||||||
Notifications.setCategories([notificationCategory]);
|
Notifications.setCategories([notificationCategory]);
|
||||||
|
} else if (Platform.OS === 'android' && Platform.constants.Version >= 33) {
|
||||||
|
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS).then(permissionStatus => {
|
||||||
|
if (permissionStatus === 'granted') {
|
||||||
|
Notifications.registerRemoteNotifications();
|
||||||
} else {
|
} else {
|
||||||
// init
|
// TODO: Ask user to enable notifications
|
||||||
Notifications.android.registerRemoteNotifications();
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Notifications.registerRemoteNotifications();
|
||||||
}
|
}
|
||||||
|
|
||||||
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
|
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -129,11 +129,7 @@ const ChatsStackNavigator = () => {
|
||||||
<ChatsStack.Screen name='TeamChannelsView' component={TeamChannelsView} />
|
<ChatsStack.Screen name='TeamChannelsView' component={TeamChannelsView} />
|
||||||
<ChatsStack.Screen name='CreateChannelView' component={CreateChannelView} />
|
<ChatsStack.Screen name='CreateChannelView' component={CreateChannelView} />
|
||||||
<ChatsStack.Screen name='AddChannelTeamView' component={AddChannelTeamView} />
|
<ChatsStack.Screen name='AddChannelTeamView' component={AddChannelTeamView} />
|
||||||
<ChatsStack.Screen
|
<ChatsStack.Screen name='AddExistingChannelView' component={AddExistingChannelView} />
|
||||||
name='AddExistingChannelView'
|
|
||||||
component={AddExistingChannelView}
|
|
||||||
options={AddExistingChannelView.navigationOptions}
|
|
||||||
/>
|
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} />
|
<ChatsStack.Screen name='MarkdownTableView' component={MarkdownTableView} />
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
|
@ -179,12 +175,7 @@ const SettingsStackNavigator = () => {
|
||||||
>
|
>
|
||||||
<SettingsStack.Screen name='SettingsView' component={SettingsView} />
|
<SettingsStack.Screen name='SettingsView' component={SettingsView} />
|
||||||
<SettingsStack.Screen name='SecurityPrivacyView' component={SecurityPrivacyView} />
|
<SettingsStack.Screen name='SecurityPrivacyView' component={SecurityPrivacyView} />
|
||||||
<SettingsStack.Screen
|
<SettingsStack.Screen name='E2EEncryptionSecurityView' component={E2EEncryptionSecurityView} />
|
||||||
name='E2EEncryptionSecurityView'
|
|
||||||
// @ts-ignore
|
|
||||||
component={E2EEncryptionSecurityView}
|
|
||||||
options={E2EEncryptionSecurityView.navigationOptions}
|
|
||||||
/>
|
|
||||||
<SettingsStack.Screen name='LanguageView' component={LanguageView} />
|
<SettingsStack.Screen name='LanguageView' component={LanguageView} />
|
||||||
<SettingsStack.Screen name='ThemeView' component={ThemeView} />
|
<SettingsStack.Screen name='ThemeView' component={ThemeView} />
|
||||||
<SettingsStack.Screen name='DefaultBrowserView' component={DefaultBrowserView} />
|
<SettingsStack.Screen name='DefaultBrowserView' component={DefaultBrowserView} />
|
||||||
|
|
|
@ -131,12 +131,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<ModalStack.Screen name='InviteUsersView' component={InviteUsersView} />
|
<ModalStack.Screen name='InviteUsersView' component={InviteUsersView} />
|
||||||
<ModalStack.Screen name='AddChannelTeamView' component={AddChannelTeamView} />
|
<ModalStack.Screen name='AddChannelTeamView' component={AddChannelTeamView} />
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen name='AddExistingChannelView' component={AddExistingChannelView} />
|
||||||
name='AddExistingChannelView'
|
|
||||||
// @ts-ignore
|
|
||||||
component={AddExistingChannelView}
|
|
||||||
options={AddExistingChannelView.navigationOptions}
|
|
||||||
/>
|
|
||||||
<ModalStack.Screen name='InviteUsersEditView' component={InviteUsersEditView} />
|
<ModalStack.Screen name='InviteUsersEditView' component={InviteUsersEditView} />
|
||||||
<ModalStack.Screen name='MessagesView' component={MessagesView} />
|
<ModalStack.Screen name='MessagesView' component={MessagesView} />
|
||||||
<ModalStack.Screen name='AutoTranslateView' component={AutoTranslateView} />
|
<ModalStack.Screen name='AutoTranslateView' component={AutoTranslateView} />
|
||||||
|
@ -195,12 +190,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
|
||||||
<ModalStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
<ModalStack.Screen name='UserNotificationPrefView' component={UserNotificationPrefView} />
|
||||||
<ModalStack.Screen name='SecurityPrivacyView' component={SecurityPrivacyView} />
|
<ModalStack.Screen name='SecurityPrivacyView' component={SecurityPrivacyView} />
|
||||||
<ModalStack.Screen name='MediaAutoDownloadView' component={MediaAutoDownloadView} />
|
<ModalStack.Screen name='MediaAutoDownloadView' component={MediaAutoDownloadView} />
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen name='E2EEncryptionSecurityView' component={E2EEncryptionSecurityView} />
|
||||||
name='E2EEncryptionSecurityView'
|
|
||||||
// @ts-ignore
|
|
||||||
component={E2EEncryptionSecurityView}
|
|
||||||
options={E2EEncryptionSecurityView.navigationOptions}
|
|
||||||
/>
|
|
||||||
</ModalStack.Navigator>
|
</ModalStack.Navigator>
|
||||||
</ModalContainer>
|
</ModalContainer>
|
||||||
);
|
);
|
||||||
|
|
|
@ -24,7 +24,7 @@ const _OutsideStack = () => {
|
||||||
<Outside.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation } as StackNavigationOptions}>
|
<Outside.Navigator screenOptions={{ ...defaultHeader, ...themedHeader(theme), ...StackAnimation } as StackNavigationOptions}>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<Outside.Screen name='NewServerView' component={NewServerView} options={NewServerView.navigationOptions} />
|
<Outside.Screen name='NewServerView' component={NewServerView} options={NewServerView.navigationOptions} />
|
||||||
<Outside.Screen name='WorkspaceView' component={WorkspaceView} options={WorkspaceView.navigationOptions} />
|
<Outside.Screen name='WorkspaceView' component={WorkspaceView} />
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
<Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} />
|
<Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} />
|
||||||
<Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} />
|
<Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} />
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { IMessage, TAnyMessageModel, TMessageModel } from '../definitions/IMessa
|
||||||
import { TServerModel } from '../definitions/IServer';
|
import { TServerModel } from '../definitions/IServer';
|
||||||
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
|
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
|
||||||
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
|
||||||
import { IItem } from '../views/TeamChannelsView';
|
|
||||||
import { MasterDetailInsideStackParamList, ModalStackParamList } from './MasterDetailStack/types';
|
import { MasterDetailInsideStackParamList, ModalStackParamList } from './MasterDetailStack/types';
|
||||||
import { TNavigation } from './stackType';
|
import { TNavigation } from './stackType';
|
||||||
|
|
||||||
|
@ -154,12 +153,10 @@ export type ChatsStackParamList = {
|
||||||
teamId?: string;
|
teamId?: string;
|
||||||
};
|
};
|
||||||
AddChannelTeamView: {
|
AddChannelTeamView: {
|
||||||
teamId?: string;
|
teamId: string;
|
||||||
teamChannels: IItem[];
|
|
||||||
};
|
};
|
||||||
AddExistingChannelView: {
|
AddExistingChannelView: {
|
||||||
teamId?: string;
|
teamId: string;
|
||||||
teamChannels: IItem[];
|
|
||||||
};
|
};
|
||||||
MarkdownTableView: {
|
MarkdownTableView: {
|
||||||
renderRows: (drawExtraBorders?: boolean) => JSX.Element;
|
renderRows: (drawExtraBorders?: boolean) => JSX.Element;
|
||||||
|
|
|
@ -40,7 +40,9 @@ const setHeader = ({
|
||||||
const AddChannelTeamView = () => {
|
const AddChannelTeamView = () => {
|
||||||
const navigation = useNavigation<TNavigation>();
|
const navigation = useNavigation<TNavigation>();
|
||||||
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
|
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
|
||||||
const { teamChannels, teamId } = useRoute<TRoute>().params;
|
const {
|
||||||
|
params: { teamId }
|
||||||
|
} = useRoute<TRoute>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHeader({ navigation, isMasterDetail });
|
setHeader({ navigation, isMasterDetail });
|
||||||
|
@ -70,7 +72,7 @@ const AddChannelTeamView = () => {
|
||||||
<List.Separator />
|
<List.Separator />
|
||||||
<List.Item
|
<List.Item
|
||||||
title='Add_Existing'
|
title='Add_Existing'
|
||||||
onPress={() => navigation.navigate('AddExistingChannelView', { teamId, teamChannels })}
|
onPress={() => navigation.navigate('AddExistingChannelView', { teamId })}
|
||||||
testID='add-channel-team-view-add-existing'
|
testID='add-channel-team-view-add-existing'
|
||||||
left={() => <List.Icon name='channel-public' />}
|
left={() => <List.Icon name='channel-public' />}
|
||||||
right={() => <List.Icon name='chevron-right' />}
|
right={() => <List.Icon name='chevron-right' />}
|
||||||
|
|
|
@ -1,217 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
|
||||||
import { RouteProp } from '@react-navigation/native';
|
|
||||||
import { FlatList } from 'react-native';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Q } from '@nozbe/watermelondb';
|
|
||||||
|
|
||||||
import * as List from '../containers/List';
|
|
||||||
import database from '../lib/database';
|
|
||||||
import I18n from '../i18n';
|
|
||||||
import log, { events, logEvent } from '../lib/methods/helpers/log';
|
|
||||||
import SearchBox from '../containers/SearchBox';
|
|
||||||
import * as HeaderButton from '../containers/HeaderButton';
|
|
||||||
import StatusBar from '../containers/StatusBar';
|
|
||||||
import { themes } from '../lib/constants';
|
|
||||||
import { TSupportedThemes, withTheme } from '../theme';
|
|
||||||
import SafeAreaView from '../containers/SafeAreaView';
|
|
||||||
import { sendLoadingEvent } from '../containers/Loading';
|
|
||||||
import { animateNextTransition } from '../lib/methods/helpers/layoutAnimation';
|
|
||||||
import { showErrorAlert } from '../lib/methods/helpers/info';
|
|
||||||
import { ChatsStackParamList } from '../stacks/types';
|
|
||||||
import { TSubscriptionModel, SubscriptionType, IApplicationState } from '../definitions';
|
|
||||||
import { getRoomTitle, hasPermission, debounce } from '../lib/methods/helpers';
|
|
||||||
import { Services } from '../lib/services';
|
|
||||||
|
|
||||||
interface IAddExistingChannelViewState {
|
|
||||||
search: TSubscriptionModel[];
|
|
||||||
channels: TSubscriptionModel[];
|
|
||||||
selected: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAddExistingChannelViewProps {
|
|
||||||
navigation: StackNavigationProp<ChatsStackParamList, 'AddExistingChannelView'>;
|
|
||||||
route: RouteProp<ChatsStackParamList, 'AddExistingChannelView'>;
|
|
||||||
theme?: TSupportedThemes;
|
|
||||||
isMasterDetail: boolean;
|
|
||||||
addTeamChannelPermission?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUERY_SIZE = 50;
|
|
||||||
|
|
||||||
class AddExistingChannelView extends React.Component<IAddExistingChannelViewProps, IAddExistingChannelViewState> {
|
|
||||||
private teamId: string;
|
|
||||||
|
|
||||||
constructor(props: IAddExistingChannelViewProps) {
|
|
||||||
super(props);
|
|
||||||
this.query();
|
|
||||||
this.teamId = props.route?.params?.teamId ?? '';
|
|
||||||
this.state = {
|
|
||||||
search: [],
|
|
||||||
channels: [],
|
|
||||||
selected: []
|
|
||||||
};
|
|
||||||
this.setHeader();
|
|
||||||
}
|
|
||||||
|
|
||||||
setHeader = () => {
|
|
||||||
const { navigation, isMasterDetail } = this.props;
|
|
||||||
const { selected } = this.state;
|
|
||||||
|
|
||||||
const options: StackNavigationOptions = {
|
|
||||||
headerTitle: I18n.t('Add_Existing_Channel')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMasterDetail) {
|
|
||||||
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
options.headerRight = () =>
|
|
||||||
selected.length > 0 && (
|
|
||||||
<HeaderButton.Container>
|
|
||||||
<HeaderButton.Item title={I18n.t('Next')} onPress={this.submit} testID='add-existing-channel-view-submit' />
|
|
||||||
</HeaderButton.Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
navigation.setOptions(options);
|
|
||||||
};
|
|
||||||
|
|
||||||
query = async (stringToSearch = '') => {
|
|
||||||
try {
|
|
||||||
const { addTeamChannelPermission } = this.props;
|
|
||||||
const db = database.active;
|
|
||||||
const channels = await db
|
|
||||||
.get('subscriptions')
|
|
||||||
.query(
|
|
||||||
Q.where('team_id', ''),
|
|
||||||
Q.where('t', Q.oneOf(['c', 'p'])),
|
|
||||||
Q.where('name', Q.like(`%${stringToSearch}%`)),
|
|
||||||
Q.take(QUERY_SIZE),
|
|
||||||
Q.sortBy('room_updated_at', Q.desc)
|
|
||||||
)
|
|
||||||
.fetch();
|
|
||||||
|
|
||||||
const asyncFilter = async (channelsArray: TSubscriptionModel[]) => {
|
|
||||||
const results = await Promise.all(
|
|
||||||
channelsArray.map(async channel => {
|
|
||||||
if (channel.prid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const permissions = await hasPermission([addTeamChannelPermission], channel.rid);
|
|
||||||
if (!permissions[0]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return channelsArray.filter((_v: any, index: number) => results[index]);
|
|
||||||
};
|
|
||||||
const channelFiltered = await asyncFilter(channels);
|
|
||||||
this.setState({ channels: channelFiltered });
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onSearchChangeText = debounce((text: string) => {
|
|
||||||
this.query(text);
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
dismiss = () => {
|
|
||||||
const { navigation } = this.props;
|
|
||||||
return navigation.pop();
|
|
||||||
};
|
|
||||||
|
|
||||||
submit = async () => {
|
|
||||||
const { selected } = this.state;
|
|
||||||
const { navigation } = this.props;
|
|
||||||
|
|
||||||
sendLoadingEvent({ visible: true });
|
|
||||||
try {
|
|
||||||
logEvent(events.CT_ADD_ROOM_TO_TEAM);
|
|
||||||
const result = await Services.addRoomsToTeam({ rooms: selected, teamId: this.teamId });
|
|
||||||
if (result.success) {
|
|
||||||
sendLoadingEvent({ visible: false });
|
|
||||||
// Expect that after you add an existing channel to a team, the user should move back to the team
|
|
||||||
navigation.navigate('RoomView');
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
|
|
||||||
showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
|
|
||||||
sendLoadingEvent({ visible: false });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderHeader = () => (
|
|
||||||
<SearchBox onChangeText={(text: string) => this.onSearchChangeText(text)} testID='add-existing-channel-view-search' />
|
|
||||||
);
|
|
||||||
|
|
||||||
isChecked = (rid: string) => {
|
|
||||||
const { selected } = this.state;
|
|
||||||
return selected.includes(rid);
|
|
||||||
};
|
|
||||||
|
|
||||||
toggleChannel = (rid: string) => {
|
|
||||||
const { selected } = this.state;
|
|
||||||
|
|
||||||
animateNextTransition();
|
|
||||||
if (!this.isChecked(rid)) {
|
|
||||||
logEvent(events.AEC_ADD_CHANNEL);
|
|
||||||
this.setState({ selected: [...selected, rid] }, () => this.setHeader());
|
|
||||||
} else {
|
|
||||||
logEvent(events.AEC_REMOVE_CHANNEL);
|
|
||||||
const filterSelected = selected.filter(el => el !== rid);
|
|
||||||
this.setState({ selected: filterSelected }, () => this.setHeader());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
renderItem = ({ item }: { item: TSubscriptionModel }) => {
|
|
||||||
const isChecked = this.isChecked(item.rid);
|
|
||||||
// TODO: reuse logic inside RoomTypeIcon
|
|
||||||
const icon = item.t === SubscriptionType.DIRECT && !item?.teamId ? 'channel-private' : 'channel-public';
|
|
||||||
return (
|
|
||||||
<List.Item
|
|
||||||
title={getRoomTitle(item)}
|
|
||||||
translateTitle={false}
|
|
||||||
onPress={() => this.toggleChannel(item.rid)}
|
|
||||||
testID={`add-existing-channel-view-item-${item.name}`}
|
|
||||||
left={() => <List.Icon name={icon} />}
|
|
||||||
right={() => (isChecked ? <List.Icon name='check' /> : null)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
renderList = () => {
|
|
||||||
const { search, channels } = this.state;
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<FlatList
|
|
||||||
data={search.length > 0 ? search : channels}
|
|
||||||
extraData={this.state}
|
|
||||||
keyExtractor={item => item.id}
|
|
||||||
ListHeaderComponent={this.renderHeader}
|
|
||||||
renderItem={this.renderItem}
|
|
||||||
ItemSeparatorComponent={List.Separator}
|
|
||||||
contentContainerStyle={{ backgroundColor: themes[theme!].backgroundColor }}
|
|
||||||
keyboardShouldPersistTaps='always'
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<SafeAreaView testID='add-existing-channel-view'>
|
|
||||||
<StatusBar />
|
|
||||||
{this.renderList()}
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IApplicationState) => ({
|
|
||||||
isMasterDetail: state.app.isMasterDetail,
|
|
||||||
addTeamChannelPermission: state.permissions['add-team-channel']
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(AddExistingChannelView));
|
|
|
@ -0,0 +1,177 @@
|
||||||
|
import React, { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
|
||||||
|
import { FlatList } from 'react-native';
|
||||||
|
import { Q } from '@nozbe/watermelondb';
|
||||||
|
|
||||||
|
import * as List from '../../containers/List';
|
||||||
|
import database from '../../lib/database';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||||
|
import SearchBox from '../../containers/SearchBox';
|
||||||
|
import * as HeaderButton from '../../containers/HeaderButton';
|
||||||
|
import StatusBar from '../../containers/StatusBar';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import { sendLoadingEvent } from '../../containers/Loading';
|
||||||
|
import { animateNextTransition } from '../../lib/methods/helpers/layoutAnimation';
|
||||||
|
import { showErrorAlert } from '../../lib/methods/helpers/info';
|
||||||
|
import { ChatsStackParamList } from '../../stacks/types';
|
||||||
|
import { TSubscriptionModel, SubscriptionType } from '../../definitions';
|
||||||
|
import { getRoomTitle, hasPermission, useDebounce } from '../../lib/methods/helpers';
|
||||||
|
import { Services } from '../../lib/services';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
|
||||||
|
type TNavigation = StackNavigationProp<ChatsStackParamList, 'AddExistingChannelView'>;
|
||||||
|
type TRoute = RouteProp<ChatsStackParamList, 'AddExistingChannelView'>;
|
||||||
|
|
||||||
|
const QUERY_SIZE = 50;
|
||||||
|
|
||||||
|
const AddExistingChannelView = () => {
|
||||||
|
const [channels, setChannels] = useState<TSubscriptionModel[]>([]);
|
||||||
|
const [selected, setSelected] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const navigation = useNavigation<TNavigation>();
|
||||||
|
const {
|
||||||
|
params: { teamId }
|
||||||
|
} = useRoute<TRoute>();
|
||||||
|
|
||||||
|
const { addTeamChannelPermission, isMasterDetail } = useAppSelector(state => ({
|
||||||
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
|
addTeamChannelPermission: state.permissions['add-team-channel']
|
||||||
|
}));
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
setHeader();
|
||||||
|
}, [selected.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
query();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setHeader = () => {
|
||||||
|
const options: StackNavigationOptions = {
|
||||||
|
headerTitle: I18n.t('Add_Existing_Channel')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <HeaderButton.CloseModal navigation={navigation} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headerRight = () =>
|
||||||
|
selected.length > 0 && (
|
||||||
|
<HeaderButton.Container>
|
||||||
|
<HeaderButton.Item title={I18n.t('Next')} onPress={submit} testID='add-existing-channel-view-submit' />
|
||||||
|
</HeaderButton.Container>
|
||||||
|
);
|
||||||
|
|
||||||
|
navigation.setOptions(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = async (stringToSearch = '') => {
|
||||||
|
try {
|
||||||
|
const db = database.active;
|
||||||
|
const channels = await db
|
||||||
|
.get('subscriptions')
|
||||||
|
.query(
|
||||||
|
Q.where('team_id', ''),
|
||||||
|
Q.where('t', Q.oneOf(['c', 'p'])),
|
||||||
|
Q.where('name', Q.like(`%${stringToSearch}%`)),
|
||||||
|
Q.take(QUERY_SIZE),
|
||||||
|
Q.sortBy('room_updated_at', Q.desc)
|
||||||
|
)
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
const asyncFilter = async (channelsArray: TSubscriptionModel[]) => {
|
||||||
|
const results = await Promise.all(
|
||||||
|
channelsArray.map(async channel => {
|
||||||
|
if (channel.prid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const permissions = await hasPermission([addTeamChannelPermission], channel.rid);
|
||||||
|
if (!permissions[0]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return channelsArray.filter((_v: any, index: number) => results[index]);
|
||||||
|
};
|
||||||
|
const channelFiltered = await asyncFilter(channels);
|
||||||
|
setChannels(channelFiltered);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearchChangeText = useDebounce((text: string) => {
|
||||||
|
query(text);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const isChecked = (rid: string) => selected.includes(rid);
|
||||||
|
|
||||||
|
const toggleChannel = (rid: string) => {
|
||||||
|
animateNextTransition();
|
||||||
|
if (!isChecked(rid)) {
|
||||||
|
logEvent(events.AEC_ADD_CHANNEL);
|
||||||
|
setSelected([...selected, rid]);
|
||||||
|
} else {
|
||||||
|
logEvent(events.AEC_REMOVE_CHANNEL);
|
||||||
|
const filterSelected = selected.filter(el => el !== rid);
|
||||||
|
setSelected(filterSelected);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
sendLoadingEvent({ visible: true });
|
||||||
|
try {
|
||||||
|
logEvent(events.CT_ADD_ROOM_TO_TEAM);
|
||||||
|
const result = await Services.addRoomsToTeam({ rooms: selected, teamId });
|
||||||
|
if (result.success) {
|
||||||
|
sendLoadingEvent({ visible: false });
|
||||||
|
// Expect that after you add an existing channel to a team, the user should move back to the team
|
||||||
|
navigation.navigate('RoomView');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
|
||||||
|
showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
|
||||||
|
sendLoadingEvent({ visible: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='add-existing-channel-view'>
|
||||||
|
<StatusBar />
|
||||||
|
<FlatList
|
||||||
|
data={channels}
|
||||||
|
extraData={channels}
|
||||||
|
keyExtractor={item => item.id}
|
||||||
|
ListHeaderComponent={
|
||||||
|
<SearchBox onChangeText={(text: string) => onSearchChangeText(text)} testID='add-existing-channel-view-search' />
|
||||||
|
}
|
||||||
|
renderItem={({ item }: { item: TSubscriptionModel }) => {
|
||||||
|
// TODO: reuse logic inside RoomTypeIcon
|
||||||
|
const icon = item.t === SubscriptionType.GROUP && !item?.teamId ? 'channel-private' : 'channel-public';
|
||||||
|
return (
|
||||||
|
<List.Item
|
||||||
|
title={getRoomTitle(item)}
|
||||||
|
translateTitle={false}
|
||||||
|
onPress={() => toggleChannel(item.rid)}
|
||||||
|
testID={`add-existing-channel-view-item-${item.name}`}
|
||||||
|
left={() => <List.Icon name={icon} />}
|
||||||
|
right={() => (isChecked(item.rid) ? <List.Icon name='check' /> : null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
ItemSeparatorComponent={List.Separator}
|
||||||
|
contentContainerStyle={{ backgroundColor: colors.backgroundColor }}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddExistingChannelView;
|
|
@ -1,4 +1,4 @@
|
||||||
import CameraRoll from '@react-native-community/cameraroll';
|
import { CameraRoll } from '@react-native-camera-roll/camera-roll';
|
||||||
import { HeaderBackground, useHeaderHeight } from '@react-navigation/elements';
|
import { HeaderBackground, useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { StackNavigationOptions } from '@react-navigation/stack';
|
import { StackNavigationOptions } from '@react-navigation/stack';
|
||||||
import { ResizeMode, Video } from 'expo-av';
|
import { ResizeMode, Video } from 'expo-av';
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { StyleSheet, Text, View, TextInput as RNTextInput } from 'react-native';
|
|
||||||
import { StackNavigationOptions } from '@react-navigation/stack';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import StatusBar from '../containers/StatusBar';
|
|
||||||
import * as List from '../containers/List';
|
|
||||||
import I18n from '../i18n';
|
|
||||||
import log, { events, logEvent } from '../lib/methods/helpers/log';
|
|
||||||
import { withTheme } from '../theme';
|
|
||||||
import SafeAreaView from '../containers/SafeAreaView';
|
|
||||||
import { FormTextInput } from '../containers/TextInput';
|
|
||||||
import Button from '../containers/Button';
|
|
||||||
import { getUserSelector } from '../selectors/login';
|
|
||||||
import { PADDING_HORIZONTAL } from '../containers/List/constants';
|
|
||||||
import { themes } from '../lib/constants';
|
|
||||||
import { Encryption } from '../lib/encryption';
|
|
||||||
import { logout } from '../actions/login';
|
|
||||||
import { showConfirmationAlert, showErrorAlert } from '../lib/methods/helpers/info';
|
|
||||||
import EventEmitter from '../lib/methods/helpers/events';
|
|
||||||
import { LISTENER } from '../containers/Toast';
|
|
||||||
import { debounce } from '../lib/methods/helpers';
|
|
||||||
import sharedStyles from './Styles';
|
|
||||||
import { IApplicationState, IBaseScreen, IUser } from '../definitions';
|
|
||||||
import { Services } from '../lib/services';
|
|
||||||
import { SettingsStackParamList } from '../stacks/types';
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
paddingHorizontal: PADDING_HORIZONTAL
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 16,
|
|
||||||
...sharedStyles.textMedium
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: 14,
|
|
||||||
paddingVertical: 10,
|
|
||||||
...sharedStyles.textRegular
|
|
||||||
},
|
|
||||||
changePasswordButton: {
|
|
||||||
marginBottom: 4
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
marginBottom: 16
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IE2EEncryptionSecurityViewState {
|
|
||||||
newPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IE2EEncryptionSecurityViewProps extends IBaseScreen<SettingsStackParamList, 'E2EEncryptionSecurityView'> {
|
|
||||||
user: IUser;
|
|
||||||
server: string;
|
|
||||||
encryptionEnabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
class E2EEncryptionSecurityView extends React.Component<IE2EEncryptionSecurityViewProps, IE2EEncryptionSecurityViewState> {
|
|
||||||
private newPasswordInputRef: any = React.createRef();
|
|
||||||
|
|
||||||
static navigationOptions = (): StackNavigationOptions => ({
|
|
||||||
title: I18n.t('E2E_Encryption')
|
|
||||||
});
|
|
||||||
|
|
||||||
state = { newPassword: '' };
|
|
||||||
|
|
||||||
onChangePasswordText = debounce((text: string) => this.setState({ newPassword: text }), 300);
|
|
||||||
|
|
||||||
setNewPasswordRef = (ref: RNTextInput) => (this.newPasswordInputRef = ref);
|
|
||||||
|
|
||||||
changePassword = () => {
|
|
||||||
const { newPassword } = this.state;
|
|
||||||
if (!newPassword.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: I18n.t('Are_you_sure_question_mark'),
|
|
||||||
message: I18n.t('E2E_encryption_change_password_message'),
|
|
||||||
confirmationText: I18n.t('E2E_encryption_change_password_confirmation'),
|
|
||||||
onPress: async () => {
|
|
||||||
logEvent(events.E2E_SEC_CHANGE_PASSWORD);
|
|
||||||
try {
|
|
||||||
const { server } = this.props;
|
|
||||||
await Encryption.changePassword(server, newPassword);
|
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('E2E_encryption_change_password_success') });
|
|
||||||
this.newPasswordInputRef?.clear();
|
|
||||||
this.newPasswordInputRef?.blur();
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
showErrorAlert(I18n.t('E2E_encryption_change_password_error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
resetOwnKey = () => {
|
|
||||||
showConfirmationAlert({
|
|
||||||
title: I18n.t('Are_you_sure_question_mark'),
|
|
||||||
message: I18n.t('E2E_encryption_reset_message'),
|
|
||||||
confirmationText: I18n.t('E2E_encryption_reset_confirmation'),
|
|
||||||
onPress: async () => {
|
|
||||||
logEvent(events.E2E_SEC_RESET_OWN_KEY);
|
|
||||||
try {
|
|
||||||
const res = await Services.e2eResetOwnKey();
|
|
||||||
/**
|
|
||||||
* It might return an empty object when TOTP is enabled,
|
|
||||||
* that's why we're using strict equality to boolean
|
|
||||||
*/
|
|
||||||
if (res === true) {
|
|
||||||
const { dispatch } = this.props;
|
|
||||||
dispatch(logout());
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log(e);
|
|
||||||
showErrorAlert(I18n.t('E2E_encryption_reset_error'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
renderChangePassword = () => {
|
|
||||||
const { newPassword } = this.state;
|
|
||||||
const { theme, encryptionEnabled } = this.props;
|
|
||||||
if (!encryptionEnabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<List.Section>
|
|
||||||
<Text style={[styles.title, { color: themes[theme!].headerTitleColor }]}>
|
|
||||||
{I18n.t('E2E_encryption_change_password_title')}
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.description, { color: themes[theme!].bodyText }]}>
|
|
||||||
{I18n.t('E2E_encryption_change_password_description')}
|
|
||||||
</Text>
|
|
||||||
<FormTextInput
|
|
||||||
inputRef={this.setNewPasswordRef}
|
|
||||||
placeholder={I18n.t('New_Password')}
|
|
||||||
returnKeyType='send'
|
|
||||||
secureTextEntry
|
|
||||||
onSubmitEditing={this.changePassword}
|
|
||||||
testID='e2e-encryption-security-view-password'
|
|
||||||
onChangeText={this.onChangePasswordText}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onPress={this.changePassword}
|
|
||||||
title={I18n.t('Save_Changes')}
|
|
||||||
disabled={!newPassword.trim()}
|
|
||||||
style={styles.changePasswordButton}
|
|
||||||
testID='e2e-encryption-security-view-change-password'
|
|
||||||
/>
|
|
||||||
</List.Section>
|
|
||||||
<List.Separator style={styles.separator} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { theme } = this.props;
|
|
||||||
return (
|
|
||||||
<SafeAreaView testID='e2e-encryption-security-view' style={{ backgroundColor: themes[theme!].backgroundColor }}>
|
|
||||||
<StatusBar />
|
|
||||||
<List.Container>
|
|
||||||
<View style={styles.container}>
|
|
||||||
{this.renderChangePassword()}
|
|
||||||
|
|
||||||
<List.Section>
|
|
||||||
<Text style={[styles.title, { color: themes[theme!].headerTitleColor }]}>
|
|
||||||
{I18n.t('E2E_encryption_reset_title')}
|
|
||||||
</Text>
|
|
||||||
<Text style={[styles.description, { color: themes[theme!].bodyText }]}>
|
|
||||||
{I18n.t('E2E_encryption_reset_description')}
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
onPress={this.resetOwnKey}
|
|
||||||
title={I18n.t('E2E_encryption_reset_button')}
|
|
||||||
type='secondary'
|
|
||||||
backgroundColor={themes[theme!].chatComponentBackground}
|
|
||||||
testID='e2e-encryption-security-view-reset-key'
|
|
||||||
/>
|
|
||||||
</List.Section>
|
|
||||||
</View>
|
|
||||||
</List.Container>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IApplicationState) => ({
|
|
||||||
server: state.server.server,
|
|
||||||
user: getUserSelector(state),
|
|
||||||
encryptionEnabled: state.encryption.enabled
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(E2EEncryptionSecurityView));
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { StyleSheet, Text, TextInput as RNTextInput } from 'react-native';
|
||||||
|
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import * as List from '../../containers/List';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||||
|
import { FormTextInput } from '../../containers/TextInput';
|
||||||
|
import Button from '../../containers/Button';
|
||||||
|
import { Encryption } from '../../lib/encryption';
|
||||||
|
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
|
||||||
|
import EventEmitter from '../../lib/methods/helpers/events';
|
||||||
|
import { LISTENER } from '../../containers/Toast';
|
||||||
|
import { useDebounce } from '../../lib/methods/helpers';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 14,
|
||||||
|
paddingVertical: 12,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
},
|
||||||
|
changePasswordButton: {
|
||||||
|
marginBottom: 4
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
marginBottom: 16
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChangePassword = () => {
|
||||||
|
const [newPassword, setNewPassword] = useState('');
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { encryptionEnabled, server } = useAppSelector(state => ({
|
||||||
|
encryptionEnabled: state.encryption.enabled,
|
||||||
|
server: state.server.server
|
||||||
|
}));
|
||||||
|
const newPasswordInputRef = useRef<RNTextInput | null>(null);
|
||||||
|
|
||||||
|
const onChangePasswordText = useDebounce((text: string) => setNewPassword(text), 300);
|
||||||
|
|
||||||
|
const changePassword = () => {
|
||||||
|
if (!newPassword.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showConfirmationAlert({
|
||||||
|
title: I18n.t('Are_you_sure_question_mark'),
|
||||||
|
message: I18n.t('E2E_encryption_change_password_message'),
|
||||||
|
confirmationText: I18n.t('E2E_encryption_change_password_confirmation'),
|
||||||
|
onPress: async () => {
|
||||||
|
logEvent(events.E2E_SEC_CHANGE_PASSWORD);
|
||||||
|
try {
|
||||||
|
await Encryption.changePassword(server, newPassword);
|
||||||
|
EventEmitter.emit(LISTENER, { message: I18n.t('E2E_encryption_change_password_success') });
|
||||||
|
newPasswordInputRef?.current?.clear();
|
||||||
|
newPasswordInputRef?.current?.blur();
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(I18n.t('E2E_encryption_change_password_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!encryptionEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<List.Section>
|
||||||
|
<Text style={[styles.title, { color: colors.headerTitleColor }]}>{I18n.t('E2E_encryption_change_password_title')}</Text>
|
||||||
|
<Text style={[styles.description, { color: colors.bodyText }]}>
|
||||||
|
{I18n.t('E2E_encryption_change_password_description')}
|
||||||
|
</Text>
|
||||||
|
<FormTextInput
|
||||||
|
inputRef={newPasswordInputRef}
|
||||||
|
placeholder={I18n.t('New_Password')}
|
||||||
|
returnKeyType='send'
|
||||||
|
secureTextEntry
|
||||||
|
onSubmitEditing={changePassword}
|
||||||
|
testID='e2e-encryption-security-view-password'
|
||||||
|
onChangeText={onChangePasswordText}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onPress={changePassword}
|
||||||
|
title={I18n.t('Save_Changes')}
|
||||||
|
disabled={!newPassword.trim()}
|
||||||
|
style={styles.changePasswordButton}
|
||||||
|
testID='e2e-encryption-security-view-change-password'
|
||||||
|
/>
|
||||||
|
</List.Section>
|
||||||
|
<List.Separator style={styles.separator} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangePassword;
|
|
@ -0,0 +1,96 @@
|
||||||
|
import React, { useLayoutEffect } from 'react';
|
||||||
|
import { StyleSheet, Text, View } from 'react-native';
|
||||||
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
|
||||||
|
import StatusBar from '../../containers/StatusBar';
|
||||||
|
import * as List from '../../containers/List';
|
||||||
|
import I18n from '../../i18n';
|
||||||
|
import log, { events, logEvent } from '../../lib/methods/helpers/log';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
|
import Button from '../../containers/Button';
|
||||||
|
import { PADDING_HORIZONTAL } from '../../containers/List/constants';
|
||||||
|
import { logout } from '../../actions/login';
|
||||||
|
import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers/info';
|
||||||
|
import sharedStyles from '../Styles';
|
||||||
|
import { Services } from '../../lib/services';
|
||||||
|
import { SettingsStackParamList } from '../../stacks/types';
|
||||||
|
import ChangePassword from './ChangePassword';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
paddingHorizontal: PADDING_HORIZONTAL
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
...sharedStyles.textMedium
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
fontSize: 14,
|
||||||
|
paddingVertical: 10,
|
||||||
|
...sharedStyles.textRegular
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const E2EEncryptionSecurityView = () => {
|
||||||
|
const navigation = useNavigation<StackNavigationProp<SettingsStackParamList, 'E2EEncryptionSecurityView'>>();
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
|
title: I18n.t('E2E_Encryption')
|
||||||
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
|
const resetOwnKey = () => {
|
||||||
|
showConfirmationAlert({
|
||||||
|
title: I18n.t('Are_you_sure_question_mark'),
|
||||||
|
message: I18n.t('E2E_encryption_reset_message'),
|
||||||
|
confirmationText: I18n.t('E2E_encryption_reset_confirmation'),
|
||||||
|
onPress: async () => {
|
||||||
|
logEvent(events.E2E_SEC_RESET_OWN_KEY);
|
||||||
|
try {
|
||||||
|
const res = await Services.e2eResetOwnKey();
|
||||||
|
/**
|
||||||
|
* It might return an empty object when TOTP is enabled,
|
||||||
|
* that's why we're using strict equality to boolean
|
||||||
|
*/
|
||||||
|
if (res === true) {
|
||||||
|
dispatch(logout());
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
showErrorAlert(I18n.t('E2E_encryption_reset_error'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='e2e-encryption-security-view' style={{ backgroundColor: colors.backgroundColor }}>
|
||||||
|
<StatusBar />
|
||||||
|
<List.Container>
|
||||||
|
<View style={styles.container}>
|
||||||
|
<ChangePassword />
|
||||||
|
|
||||||
|
<List.Section>
|
||||||
|
<Text style={[styles.title, { color: colors.headerTitleColor }]}>{I18n.t('E2E_encryption_reset_title')}</Text>
|
||||||
|
<Text style={[styles.description, { color: colors.bodyText }]}>{I18n.t('E2E_encryption_reset_description')}</Text>
|
||||||
|
<Button
|
||||||
|
onPress={resetOwnKey}
|
||||||
|
title={I18n.t('E2E_encryption_reset_button')}
|
||||||
|
type='secondary'
|
||||||
|
backgroundColor={colors.chatComponentBackground}
|
||||||
|
testID='e2e-encryption-security-view-reset-key'
|
||||||
|
/>
|
||||||
|
</List.Section>
|
||||||
|
</View>
|
||||||
|
</List.Container>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default E2EEncryptionSecurityView;
|
|
@ -94,7 +94,7 @@ class JitsiMeetView extends React.Component<TJitsiMeetViewProps> {
|
||||||
userAgent={userAgent}
|
userAgent={userAgent}
|
||||||
javaScriptEnabled
|
javaScriptEnabled
|
||||||
domStorageEnabled
|
domStorageEnabled
|
||||||
mediaPlaybackRequiresUserAction={false}
|
allowsInlineMediaPlayback
|
||||||
mediaCapturePermissionGrantType={'grant'}
|
mediaCapturePermissionGrantType={'grant'}
|
||||||
/>
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
@ -186,7 +186,7 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { isSearching, showCreate, data } = this.state;
|
const { isSearching, showCreate } = this.state;
|
||||||
const { navigation, isMasterDetail, theme } = this.props;
|
const { navigation, isMasterDetail, theme } = this.props;
|
||||||
|
|
||||||
const { team } = this;
|
const { team } = this;
|
||||||
|
@ -234,7 +234,7 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
|
||||||
<HeaderButton.Item
|
<HeaderButton.Item
|
||||||
iconName='create'
|
iconName='create'
|
||||||
testID='team-channels-view-create'
|
testID='team-channels-view-create'
|
||||||
onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })}
|
onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId })}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<HeaderButton.Item iconName='search' testID='team-channels-view-search' onPress={this.onSearchPress} />
|
<HeaderButton.Item iconName='search' testID='team-channels-view-search' onPress={this.onSearchPress} />
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Text } from 'react-native';
|
||||||
|
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
|
const RegisterDisabledComponent = () => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const { Accounts_iframe_enabled, registrationText } = useAppSelector(state => ({
|
||||||
|
registrationText: state.settings.Accounts_RegistrationForm_LinkReplacementText as string,
|
||||||
|
Accounts_iframe_enabled: state.settings.Accounts_iframe_enabled as boolean
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (Accounts_iframe_enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text style={[styles.registrationText, { color: colors.auxiliaryText }]}>{registrationText}</Text>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RegisterDisabledComponent;
|
|
@ -2,9 +2,8 @@ import React from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
import { themes } from '../../lib/constants';
|
|
||||||
import { isTablet } from '../../lib/methods/helpers';
|
import { isTablet } from '../../lib/methods/helpers';
|
||||||
import { TSupportedThemes } from '../../theme';
|
import { useTheme } from '../../theme';
|
||||||
|
|
||||||
const SIZE = 96;
|
const SIZE = 96;
|
||||||
const MARGIN_TOP = isTablet ? 0 : 64;
|
const MARGIN_TOP = isTablet ? 0 : 64;
|
||||||
|
@ -26,20 +25,19 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IServerAvatar {
|
interface IServerAvatar {
|
||||||
theme: TSupportedThemes;
|
|
||||||
url: string;
|
url: string;
|
||||||
image: string;
|
image: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: missing skeleton
|
// TODO: missing skeleton
|
||||||
const ServerAvatar = React.memo(({ theme, url, image }: IServerAvatar) => (
|
const ServerAvatar = React.memo(({ url, image }: IServerAvatar) => {
|
||||||
<View style={styles.container}>
|
const { colors } = useTheme();
|
||||||
{image && (
|
|
||||||
<FastImage style={[styles.image, { borderColor: themes[theme].borderColor }]} source={{ uri: `${url}/${image}` }} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
));
|
|
||||||
|
|
||||||
ServerAvatar.displayName = 'ServerAvatar';
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
{image && <FastImage style={[styles.image, { borderColor: colors.borderColor }]} source={{ uri: `${url}/${image}` }} />}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
export default ServerAvatar;
|
export default ServerAvatar;
|
||||||
|
|
|
@ -1,53 +1,66 @@
|
||||||
import React from 'react';
|
import React, { useLayoutEffect } from 'react';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/stack';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { connect } from 'react-redux';
|
import { StackNavigationProp } from '@react-navigation/stack';
|
||||||
import { CompositeNavigationProp } from '@react-navigation/core';
|
import { CompositeNavigationProp } from '@react-navigation/core';
|
||||||
|
|
||||||
import { OutsideModalParamList, OutsideParamList } from '../../stacks/types';
|
import { OutsideModalParamList, OutsideParamList } from '../../stacks/types';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import Button from '../../containers/Button';
|
import Button from '../../containers/Button';
|
||||||
import { themes } from '../../lib/constants';
|
import { useTheme } from '../../theme';
|
||||||
import { TSupportedThemes, withTheme } from '../../theme';
|
|
||||||
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
|
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
|
||||||
import { IApplicationState } from '../../definitions';
|
|
||||||
import { IAssetsFavicon512 } from '../../definitions/IAssetsFavicon512';
|
import { IAssetsFavicon512 } from '../../definitions/IAssetsFavicon512';
|
||||||
import { getShowLoginButton } from '../../selectors/login';
|
import { getShowLoginButton } from '../../selectors/login';
|
||||||
import ServerAvatar from './ServerAvatar';
|
import ServerAvatar from './ServerAvatar';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
import { useAppSelector } from '../../lib/hooks';
|
||||||
|
import RegisterDisabledComponent from './RegisterDisabledComponent';
|
||||||
|
|
||||||
interface IWorkSpaceProp {
|
type TNavigation = CompositeNavigationProp<
|
||||||
navigation: CompositeNavigationProp<
|
|
||||||
StackNavigationProp<OutsideParamList, 'WorkspaceView'>,
|
StackNavigationProp<OutsideParamList, 'WorkspaceView'>,
|
||||||
StackNavigationProp<OutsideModalParamList>
|
StackNavigationProp<OutsideModalParamList>
|
||||||
>;
|
>;
|
||||||
theme?: TSupportedThemes;
|
|
||||||
Site_Name: string;
|
|
||||||
Site_Url: string;
|
|
||||||
server: string;
|
|
||||||
Assets_favicon_512: IAssetsFavicon512;
|
|
||||||
registrationForm: string;
|
|
||||||
registrationText: string;
|
|
||||||
showLoginButton: boolean;
|
|
||||||
Accounts_iframe_enabled: boolean;
|
|
||||||
inviteLinkToken: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkspaceView extends React.Component<IWorkSpaceProp, any> {
|
const useWorkspaceViewSelector = () =>
|
||||||
static navigationOptions = (): StackNavigationOptions => ({
|
useAppSelector(state => ({
|
||||||
|
server: state.server.server,
|
||||||
|
Site_Name: state.settings.Site_Name as string,
|
||||||
|
Site_Url: state.settings.Site_Url as string,
|
||||||
|
Assets_favicon_512: state.settings.Assets_favicon_512 as IAssetsFavicon512,
|
||||||
|
registrationForm: state.settings.Accounts_RegistrationForm as string,
|
||||||
|
Accounts_iframe_enabled: state.settings.Accounts_iframe_enabled as boolean,
|
||||||
|
showLoginButton: getShowLoginButton(state),
|
||||||
|
inviteLinkToken: state.inviteLinks.token
|
||||||
|
}));
|
||||||
|
|
||||||
|
const WorkspaceView = () => {
|
||||||
|
const navigation = useNavigation<TNavigation>();
|
||||||
|
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const {
|
||||||
|
Accounts_iframe_enabled,
|
||||||
|
Assets_favicon_512,
|
||||||
|
Site_Name,
|
||||||
|
Site_Url,
|
||||||
|
inviteLinkToken,
|
||||||
|
registrationForm,
|
||||||
|
server,
|
||||||
|
showLoginButton
|
||||||
|
} = useWorkspaceViewSelector();
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
navigation.setOptions({
|
||||||
title: I18n.t('Your_workspace')
|
title: I18n.t('Your_workspace')
|
||||||
});
|
});
|
||||||
|
}, [navigation]);
|
||||||
|
|
||||||
get showRegistrationButton() {
|
const showRegistrationButton = !!(
|
||||||
const { registrationForm, inviteLinkToken, Accounts_iframe_enabled } = this.props;
|
|
||||||
return (
|
|
||||||
!Accounts_iframe_enabled &&
|
!Accounts_iframe_enabled &&
|
||||||
(registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length))
|
(registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length))
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
login = () => {
|
const login = () => {
|
||||||
const { navigation, server, Site_Name, Accounts_iframe_enabled } = this.props;
|
|
||||||
if (Accounts_iframe_enabled) {
|
if (Accounts_iframe_enabled) {
|
||||||
navigation.navigate('AuthenticationWebView', { url: server, authType: 'iframe' });
|
navigation.navigate('AuthenticationWebView', { url: server, authType: 'iframe' });
|
||||||
return;
|
return;
|
||||||
|
@ -55,61 +68,33 @@ class WorkspaceView extends React.Component<IWorkSpaceProp, any> {
|
||||||
navigation.navigate('LoginView', { title: Site_Name });
|
navigation.navigate('LoginView', { title: Site_Name });
|
||||||
};
|
};
|
||||||
|
|
||||||
register = () => {
|
const register = () => {
|
||||||
const { navigation, Site_Name } = this.props;
|
|
||||||
navigation.navigate('RegisterView', { title: Site_Name });
|
navigation.navigate('RegisterView', { title: Site_Name });
|
||||||
};
|
};
|
||||||
|
|
||||||
renderRegisterDisabled = () => {
|
|
||||||
const { Accounts_iframe_enabled, registrationText, theme } = this.props;
|
|
||||||
if (Accounts_iframe_enabled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Text style={[styles.registrationText, { color: themes[theme!].auxiliaryText }]}>{registrationText}</Text>;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { theme, Site_Name, Site_Url, Assets_favicon_512, server, showLoginButton } = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormContainer testID='workspace-view'>
|
<FormContainer testID='workspace-view'>
|
||||||
<FormContainerInner>
|
<FormContainerInner>
|
||||||
<View style={styles.alignItemsCenter}>
|
<View style={styles.alignItemsCenter}>
|
||||||
<ServerAvatar theme={theme!} url={server} image={Assets_favicon_512?.url ?? Assets_favicon_512?.defaultUrl} />
|
<ServerAvatar url={server} image={Assets_favicon_512?.url ?? Assets_favicon_512?.defaultUrl} />
|
||||||
<Text style={[styles.serverName, { color: themes[theme!].titleText }]}>{Site_Name}</Text>
|
<Text style={[styles.serverName, { color: colors.titleText }]}>{Site_Name}</Text>
|
||||||
<Text style={[styles.serverUrl, { color: themes[theme!].auxiliaryText }]}>{Site_Url}</Text>
|
<Text style={[styles.serverUrl, { color: colors.auxiliaryText }]}>{Site_Url}</Text>
|
||||||
</View>
|
</View>
|
||||||
{showLoginButton ? (
|
{showLoginButton ? <Button title={I18n.t('Login')} type='primary' onPress={login} testID='workspace-view-login' /> : null}
|
||||||
<Button title={I18n.t('Login')} type='primary' onPress={this.login} testID='workspace-view-login' />
|
{showRegistrationButton ? (
|
||||||
) : null}
|
|
||||||
{this.showRegistrationButton ? (
|
|
||||||
<Button
|
<Button
|
||||||
title={I18n.t('Create_account')}
|
title={I18n.t('Create_account')}
|
||||||
type='secondary'
|
type='secondary'
|
||||||
backgroundColor={themes[theme!].chatComponentBackground}
|
backgroundColor={colors.chatComponentBackground}
|
||||||
onPress={this.register}
|
onPress={register}
|
||||||
testID='workspace-view-register'
|
testID='workspace-view-register'
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
this.renderRegisterDisabled()
|
<RegisterDisabledComponent />
|
||||||
)}
|
)}
|
||||||
</FormContainerInner>
|
</FormContainerInner>
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const mapStateToProps = (state: IApplicationState) => ({
|
export default WorkspaceView;
|
||||||
server: state.server.server,
|
|
||||||
Site_Name: state.settings.Site_Name as string,
|
|
||||||
Site_Url: state.settings.Site_Url as string,
|
|
||||||
Assets_favicon_512: state.settings.Assets_favicon_512 as IAssetsFavicon512,
|
|
||||||
registrationForm: state.settings.Accounts_RegistrationForm as string,
|
|
||||||
registrationText: state.settings.Accounts_RegistrationForm_LinkReplacementText as string,
|
|
||||||
Accounts_iframe_enabled: state.settings.Accounts_iframe_enabled as boolean,
|
|
||||||
showLoginButton: getShowLoginButton(state),
|
|
||||||
inviteLinkToken: state.inviteLinks.token
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(withTheme(WorkspaceView));
|
|
||||||
|
|
|
@ -382,7 +382,7 @@ describe('Team', () => {
|
||||||
|
|
||||||
describe('Room Members', () => {
|
describe('Room Members', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await tapAndWaitFor(element(by.id('room-actions-members')), element(by.id('room-members-view')), 2000);
|
await tapAndWaitFor(element(by.id('room-actions-members')), element(by.id('room-members-view')), 10000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show all users', async () => {
|
it('should show all users', async () => {
|
||||||
|
|
|
@ -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):
|
||||||
|
@ -383,7 +385,7 @@ PODS:
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-blur (4.1.0):
|
- react-native-blur (4.1.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-cameraroll (4.1.2):
|
- react-native-cameraroll (5.6.0):
|
||||||
- React-Core
|
- React-Core
|
||||||
- react-native-cookies (6.2.1):
|
- react-native-cookies (6.2.1):
|
||||||
- React-Core
|
- React-Core
|
||||||
|
@ -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`)
|
||||||
|
@ -633,7 +636,7 @@ DEPENDENCIES:
|
||||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||||
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
|
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
|
||||||
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
|
- "react-native-blur (from `../node_modules/@react-native-community/blur`)"
|
||||||
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
|
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
|
||||||
- "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)"
|
- "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)"
|
||||||
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
|
||||||
- react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`)
|
- react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`)
|
||||||
|
@ -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:
|
||||||
|
@ -780,7 +785,7 @@ EXTERNAL SOURCES:
|
||||||
react-native-blur:
|
react-native-blur:
|
||||||
:path: "../node_modules/@react-native-community/blur"
|
:path: "../node_modules/@react-native-community/blur"
|
||||||
react-native-cameraroll:
|
react-native-cameraroll:
|
||||||
:path: "../node_modules/@react-native-community/cameraroll"
|
:path: "../node_modules/@react-native-camera-roll/camera-roll"
|
||||||
react-native-cookies:
|
react-native-cookies:
|
||||||
:path: "../node_modules/@react-native-cookies/cookies"
|
:path: "../node_modules/@react-native-cookies/cookies"
|
||||||
react-native-document-picker:
|
react-native-document-picker:
|
||||||
|
@ -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
|
||||||
|
@ -938,7 +944,7 @@ SPEC CHECKSUMS:
|
||||||
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
|
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
|
||||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||||
react-native-blur: ba2f37268542f8a26d809f48c5162705a3261fc6
|
react-native-blur: ba2f37268542f8a26d809f48c5162705a3261fc6
|
||||||
react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866
|
react-native-cameraroll: 755bcc628148a90a7c9cf3f817a252be3a601bc5
|
||||||
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
|
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
|
||||||
react-native-document-picker: f5ec1a712ca2a975c233117f044817bb8393cad4
|
react-native-document-picker: f5ec1a712ca2a975c233117f044817bb8393cad4
|
||||||
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
|
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
|
||||||
|
|
|
@ -43,12 +43,12 @@
|
||||||
"@hookform/resolvers": "^2.9.10",
|
"@hookform/resolvers": "^2.9.10",
|
||||||
"@nozbe/watermelondb": "^0.25.5",
|
"@nozbe/watermelondb": "^0.25.5",
|
||||||
"@react-native-async-storage/async-storage": "^1.17.11",
|
"@react-native-async-storage/async-storage": "^1.17.11",
|
||||||
|
"@react-native-camera-roll/camera-roll": "^5.6.0",
|
||||||
"@react-native-clipboard/clipboard": "^1.8.5",
|
"@react-native-clipboard/clipboard": "^1.8.5",
|
||||||
"@react-native-community/art": "^1.2.0",
|
"@react-native-community/art": "^1.2.0",
|
||||||
"@react-native-community/blur": "^4.1.0",
|
"@react-native-community/blur": "^4.1.0",
|
||||||
"@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. */,
|
||||||
|
|
64
yarn.lock
64
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"
|
||||||
|
@ -4621,6 +4628,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
merge-options "^3.0.4"
|
merge-options "^3.0.4"
|
||||||
|
|
||||||
|
"@react-native-camera-roll/camera-roll@^5.6.0":
|
||||||
|
version "5.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.6.0.tgz#385082d57d694f3fd5ae386f8b8ce24b0969c5f9"
|
||||||
|
integrity sha512-a/GYwnBTxj1yKWB9m/qy8GzjowSocML8NbLT81wdMh0JzZYXCLze51BR2cb8JNDgRPzA9xe7KpD3j9qQOSOjag==
|
||||||
|
|
||||||
"@react-native-clipboard/clipboard@^1.8.5":
|
"@react-native-clipboard/clipboard@^1.8.5":
|
||||||
version "1.8.5"
|
version "1.8.5"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.8.5.tgz#b11276e38ef288b0fd70c0a38506e2deecc5fa5a"
|
resolved "https://registry.yarnpkg.com/@react-native-clipboard/clipboard/-/clipboard-1.8.5.tgz#b11276e38ef288b0fd70c0a38506e2deecc5fa5a"
|
||||||
|
@ -4640,11 +4652,6 @@
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.1.0.tgz#ed1361a569150c2249aae9b734e278fd262b70cd"
|
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.1.0.tgz#ed1361a569150c2249aae9b734e278fd262b70cd"
|
||||||
integrity sha512-esfuAjbAoeysfI3RhmCHlYwlXobXzcsVGZEHgDhVGB88aO9RktY6b13mYbo2FXZ8XnntcccuvXlgckvoIsggWg==
|
integrity sha512-esfuAjbAoeysfI3RhmCHlYwlXobXzcsVGZEHgDhVGB88aO9RktY6b13mYbo2FXZ8XnntcccuvXlgckvoIsggWg==
|
||||||
|
|
||||||
"@react-native-community/cameraroll@4.1.2":
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/cameraroll/-/cameraroll-4.1.2.tgz#489c6bb6137571540d93c543d5fcf8c652b548ec"
|
|
||||||
integrity sha512-jkdhMByMKD2CZ/5MPeBieYn8vkCfC4MOTouPpBpps3I8N6HUYJk+1JnDdktVYl2WINnqXpQptDA2YptVyifYAg==
|
|
||||||
|
|
||||||
"@react-native-community/cli-clean@^10.1.1":
|
"@react-native-community/cli-clean@^10.1.1":
|
||||||
version "10.1.1"
|
version "10.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504"
|
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504"
|
||||||
|
@ -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