diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b1305bfd6..895f3da03 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -13,7 +13,10 @@
-
+
+
+
+
): typeof Com
return WithActionSheetComponent;
};
-export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
- const ref: ForwardedRef = useRef(null);
+const actionSheetRef: React.Ref = createRef();
+export const ActionSheetProvider = React.memo(({ children }: { children: React.ReactElement | React.ReactElement[] }) => {
const getContext = () => ({
showActionSheet: (options: TActionSheetOptions) => {
- ref.current?.showActionSheet(options);
+ actionSheetRef.current?.showActionSheet(options);
},
hideActionSheet: () => {
- ref.current?.hideActionSheet();
+ actionSheetRef.current?.hideActionSheet();
}
});
return (
-
+
<>{children}>
);
});
+
+export const hideActionSheetRef = (): void => {
+ actionSheetRef?.current?.hideActionSheet();
+};
diff --git a/app/containers/CallHeader.tsx b/app/containers/CallHeader.tsx
new file mode 100644
index 000000000..713f27cbc
--- /dev/null
+++ b/app/containers/CallHeader.tsx
@@ -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 (
+
+
+
+ {title}
+ {calling && direct ? : null}
+
+
+ setCam(!cam)}
+ style={[style.iconCallContainerRight, { backgroundColor: handleColors(cam).button }]}
+ hitSlop={BUTTON_HIT_SLOP}
+ disabled={calling}
+ >
+
+
+ setMic(!mic)}
+ style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
+ hitSlop={BUTTON_HIT_SLOP}
+ disabled={calling}
+ >
+
+
+
+
+
+
+ {direct ? : null}
+
+ {name}
+
+
+
+ );
+};
+
+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;
+}
diff --git a/app/containers/DotsLoader/index.tsx b/app/containers/DotsLoader/index.tsx
new file mode 100644
index 000000000..7dd534534
--- /dev/null
+++ b/app/containers/DotsLoader/index.tsx
@@ -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 = {
+ height: SIZE,
+ width: SIZE,
+ borderRadius: SIZE / 2,
+ marginHorizontal: MARGIN,
+ backgroundColor: active ? colors.dotActiveBg : colors.dotBg
+ };
+
+ return ;
+}
+
+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 (
+
+ {dots.map(i => (
+
+ ))}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ dotsContainer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginLeft: 6 }
+});
+
+export default DotsLoader;
diff --git a/app/containers/InAppNotification/IncomingCallNotification/index.tsx b/app/containers/InAppNotification/IncomingCallNotification/index.tsx
new file mode 100644
index 000000000..0195da813
--- /dev/null
+++ b/app/containers/InAppNotification/IncomingCallNotification/index.tsx
@@ -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;
+ 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 (
+
+
+
+
+
+
+ {
+ hideNotification();
+ dispatch(cancelCall({ callId }));
+ }}
+ style={styles.cancelButton}
+ >
+ {i18n.t('decline')}
+
+ {
+ hideNotification();
+ dispatch(acceptCall({ callId }));
+ }}
+ style={styles.acceptButton}
+ >
+ {i18n.t('accept')}
+
+
+
+
+ );
+ }
+);
+
+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 ;
+ }
+ return null;
+};
+
+export default IncomingCallNotification;
diff --git a/app/containers/InAppNotification/IncomingCallNotification/style.tsx b/app/containers/InAppNotification/IncomingCallNotification/style.tsx
new file mode 100644
index 000000000..81cfcead4
--- /dev/null
+++ b/app/containers/InAppNotification/IncomingCallNotification/style.tsx
@@ -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'
+ }
+ });
+};
diff --git a/app/containers/InAppNotification/NotifierComponent.tsx b/app/containers/InAppNotification/NotifierComponent.tsx
index 8b32ae1c6..2db7f6908 100644
--- a/app/containers/InAppNotification/NotifierComponent.tsx
+++ b/app/containers/InAppNotification/NotifierComponent.tsx
@@ -2,7 +2,6 @@ import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { connect } from 'react-redux';
-import { Notifier } from 'react-native-notifier';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Avatar from '../Avatar';
@@ -14,6 +13,7 @@ import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
+import { hideNotification } from '../../lib/methods/helpers/notifications';
export interface INotifierComponent {
notification: {
@@ -21,6 +21,7 @@ export interface INotifierComponent {
payload: {
sender: { username: string };
type: SubscriptionType;
+ message?: { message: string; t?: string };
} & Pick;
title: string;
avatar: string;
@@ -72,8 +73,6 @@ const styles = StyleSheet.create({
}
});
-const hideNotification = () => Notifier.hideNotification();
-
const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifierComponent) => {
const { theme } = useTheme();
const insets = useSafeAreaInsets();
diff --git a/app/containers/InAppNotification/index.tsx b/app/containers/InAppNotification/index.tsx
index d6581f27f..8337f8f3c 100644
--- a/app/containers/InAppNotification/index.tsx
+++ b/app/containers/InAppNotification/index.tsx
@@ -1,4 +1,4 @@
-import React, { memo, useEffect } from 'react';
+import React, { ElementType, memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
@@ -15,24 +15,32 @@ const InAppNotification = memo(() => {
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}));
- const show = (notification: INotifierComponent['notification']) => {
- if (appState !== 'foreground') {
- return;
+ const show = (
+ notification: INotifierComponent['notification'] & {
+ customComponent?: ElementType;
+ customTime?: number;
+ customNotification?: boolean;
+ hideOnPress?: boolean;
+ swipeEnabled?: boolean;
}
+ ) => {
+ if (appState !== 'foreground') return;
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
- if (payload.rid) {
- if (payload.rid === subscribedRoom || route?.name === 'JitsiMeetView') {
- return;
- }
+ if (payload?.rid || notification.customNotification) {
+ if (payload?.rid === subscribedRoom || route?.name === 'JitsiMeetView' || payload?.message?.t === 'videoconf') return;
+
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
- Component: NotifierComponent,
+ Component: notification.customComponent || NotifierComponent,
componentProps: {
notification
- }
+ },
+ duration: notification.customTime || 3000, // default 3s,
+ hideOnPress: notification.hideOnPress ?? true,
+ swipeEnabled: notification.swipeEnabled ?? true
});
}
};
diff --git a/app/containers/Ringer/dialtone.mp3 b/app/containers/Ringer/dialtone.mp3
new file mode 100644
index 000000000..5a6aa99f3
Binary files /dev/null and b/app/containers/Ringer/dialtone.mp3 differ
diff --git a/app/containers/Ringer/index.tsx b/app/containers/Ringer/index.tsx
new file mode 100644
index 000000000..2b91c7943
--- /dev/null
+++ b/app/containers/Ringer/index.tsx
@@ -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(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 ;
+});
+
+export default Ringer;
diff --git a/app/containers/Ringer/ringtone.mp3 b/app/containers/Ringer/ringtone.mp3
new file mode 100644
index 000000000..6be6a7456
Binary files /dev/null and b/app/containers/Ringer/ringtone.mp3 differ
diff --git a/app/containers/RoomItem/LastMessage.tsx b/app/containers/RoomItem/LastMessage.tsx
index 66fe0ab5a..1c660e856 100644
--- a/app/containers/RoomItem/LastMessage.tsx
+++ b/app/containers/RoomItem/LastMessage.tsx
@@ -50,6 +50,11 @@ const formatMsg = ({ lastMessage, type, showLastMessage, username, useRealName }
prefix = `${useRealName ? name : lastMessage.u.username}: `;
}
+ if (lastMessage.t === 'videoconf') {
+ prefix = '';
+ lastMessage.msg = I18n.t('Call_started');
+ }
+
return `${prefix}${lastMessage.msg}`;
};
diff --git a/app/containers/StatusBar.tsx b/app/containers/StatusBar.tsx
index 03349c92a..2d0807033 100644
--- a/app/containers/StatusBar.tsx
+++ b/app/containers/StatusBar.tsx
@@ -1,7 +1,6 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { StatusBar as StatusBarRN } from 'react-native';
-import { themes } from '../lib/constants';
import { useTheme } from '../theme';
const supportedStyles = {
@@ -15,14 +14,20 @@ interface IStatusBar {
}
const StatusBar = React.memo(({ barStyle, backgroundColor }: IStatusBar) => {
- const { theme } = useTheme();
- if (!barStyle) {
- barStyle = 'light-content';
- if (theme === 'light') {
- barStyle = 'dark-content';
+ const { theme, colors } = useTheme();
+
+ useEffect(() => {
+ if (!barStyle) {
+ barStyle = 'light-content';
+ if (theme === 'light') {
+ barStyle = 'dark-content';
+ }
}
- }
- return ;
+ StatusBarRN.setBackgroundColor(backgroundColor ?? colors.headerBackground);
+ StatusBarRN.setBarStyle(barStyle, true);
+ }, [theme, barStyle, backgroundColor]);
+
+ return ;
});
export default StatusBar;
diff --git a/app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx b/app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx
deleted file mode 100644
index f9ecd88d9..000000000
--- a/app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx
+++ /dev/null
@@ -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 (
-
-
- {i18n.t('Start_a_call')}
-
- setCam(!cam)}
- style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
- hitSlop={BUTTON_HIT_SLOP}
- >
-
-
- setMic(!mic)}
- style={[style.iconCallContainer, mic && style.enabledBackground]}
- hitSlop={BUTTON_HIT_SLOP}
- >
-
-
-
-
-
-
-
-
- {user.username}
-
-
-
-
-
-
- );
-}
diff --git a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx
index d6fc28662..89ab8b38a 100644
--- a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx
+++ b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx
@@ -1,20 +1,15 @@
import React from 'react';
import { Text } from 'react-native';
-import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
-import { videoConfJoin } from '../../../../lib/methods/videoConf';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
-const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
+const VideoConferenceDirect = React.memo(() => {
const style = useStyle();
return (
- videoConfJoin(blockId)}>
- {i18n.t('Join')}
-
{i18n.t('Waiting_for_answer')}
);
diff --git a/app/containers/UIKit/VideoConferenceBlock/components/styles.ts b/app/containers/UIKit/VideoConferenceBlock/components/styles.ts
index df607ff8d..1f5da3554 100644
--- a/app/containers/UIKit/VideoConferenceBlock/components/styles.ts
+++ b/app/containers/UIKit/VideoConferenceBlock/components/styles.ts
@@ -5,7 +5,7 @@ import sharedStyles from '../../../../views/Styles';
export default function useStyle() {
const { colors } = useTheme();
- return StyleSheet.create({
+ const style = StyleSheet.create({
container: { height: 108, flex: 1, borderWidth: 1, borderRadius: 4, marginTop: 8, borderColor: colors.conferenceCallBorder },
callInfoContainer: { flex: 1, alignItems: 'center', paddingLeft: 16, flexDirection: 'row' },
infoContainerText: {
@@ -88,40 +88,10 @@ export default function useStyle() {
...sharedStyles.textRegular,
color: colors.passcodeSecondary
},
- actionSheetContainer: {
- paddingHorizontal: 24,
- flex: 1
- },
- actionSheetHeaderTitle: {
- fontSize: 14,
- ...sharedStyles.textBold,
- color: colors.passcodePrimary
- },
- actionSheetUsername: {
- fontSize: 16,
- ...sharedStyles.textBold,
- color: colors.passcodePrimary,
- flexShrink: 1
- },
enabledBackground: {
backgroundColor: colors.conferenceCallEnabledIconBackground
- },
- iconCallContainer: {
- padding: 6,
- borderRadius: 4
- },
- actionSheetHeader: { flexDirection: 'row', alignItems: 'center' },
- actionSheetHeaderButtons: { flex: 1, alignItems: 'center', flexDirection: 'row', justifyContent: 'flex-end' },
- actionSheetUsernameContainer: { flexDirection: 'row', paddingTop: 8, alignItems: 'center' },
- actionSheetPhotoContainer: {
- height: 220,
- width: 148,
- backgroundColor: colors.conferenceCallPhotoBackground,
- borderRadius: 8,
- margin: 24,
- alignSelf: 'center',
- justifyContent: 'center',
- alignItems: 'center'
}
});
+
+ return style;
}
diff --git a/app/containers/UIKit/VideoConferenceBlock/index.tsx b/app/containers/UIKit/VideoConferenceBlock/index.tsx
index b94f0814d..e99aca350 100644
--- a/app/containers/UIKit/VideoConferenceBlock/index.tsx
+++ b/app/containers/UIKit/VideoConferenceBlock/index.tsx
@@ -14,7 +14,7 @@ export default function VideoConferenceBlock({ callId, blockId }: { callId: stri
if ('endedAt' in result) return ;
- if (type === 'direct' && status === 0) return ;
+ if (type === 'direct' && status === 0) return ;
return ;
}
diff --git a/app/definitions/IVideoConference.ts b/app/definitions/IVideoConference.ts
index 1ba428cb3..21d4759a4 100644
--- a/app/definitions/IVideoConference.ts
+++ b/app/definitions/IVideoConference.ts
@@ -107,3 +107,5 @@ export type VideoConfListProps = {
};
export type VideoConfInfoProps = { callId: string };
+
+export type VideoConfCall = VideoConference & { capabilities: VideoConferenceCapabilities };
diff --git a/app/definitions/redux/index.ts b/app/definitions/redux/index.ts
index 71c103c16..b7105032e 100644
--- a/app/definitions/redux/index.ts
+++ b/app/definitions/redux/index.ts
@@ -16,6 +16,7 @@ import { TActionSortPreferences } from '../../actions/sortPreferences';
import { TActionUserTyping } from '../../actions/usersTyping';
import { TActionPermissions } from '../../actions/permissions';
import { TActionEnterpriseModules } from '../../actions/enterpriseModules';
+import { TActionVideoConf } from '../../actions/videoConf';
// REDUCERS
import { IActiveUsers } from '../../reducers/activeUsers';
import { IApp } from '../../reducers/app';
@@ -34,6 +35,7 @@ import { IShare } from '../../reducers/share';
import { IInquiry } from '../../ee/omnichannel/reducers/inquiry';
import { IPermissionsState } from '../../reducers/permissions';
import { IEnterpriseModules } from '../../reducers/enterpriseModules';
+import { IVideoConf } from '../../reducers/videoConf';
export interface IApplicationState {
settings: TSettingsState;
@@ -57,6 +59,7 @@ export interface IApplicationState {
encryption: IEncryption;
permissions: IPermissionsState;
roles: IRoles;
+ videoConf: IVideoConf;
}
export type TApplicationActions = TActionActiveUsers &
@@ -75,4 +78,5 @@ export type TApplicationActions = TActionActiveUsers &
TActionApp &
TActionInquiry &
TActionPermissions &
- TActionEnterpriseModules;
+ TActionEnterpriseModules &
+ TActionVideoConf;
diff --git a/app/definitions/rest/v1/videoConference.ts b/app/definitions/rest/v1/videoConference.ts
index 2d93cd7b6..cafd2c5d9 100644
--- a/app/definitions/rest/v1/videoConference.ts
+++ b/app/definitions/rest/v1/videoConference.ts
@@ -1,4 +1,5 @@
import {
+ VideoConfCall,
VideoConfCancelProps,
VideoConference,
VideoConferenceCapabilities,
@@ -24,7 +25,7 @@ export type VideoConferenceEndpoints = {
};
'video-conference.info': {
- GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities };
+ GET: (params: VideoConfInfoProps) => VideoConfCall;
};
'video-conference.list': {
diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json
index 88fa29ba1..0ff3895b7 100644
--- a/app/i18n/locales/en.json
+++ b/app/i18n/locales/en.json
@@ -691,11 +691,12 @@
"Waiting_for_answer": "Waiting for answer",
"Call_ended": "Call ended",
"Call_was_not_answered": "Call was not answered",
- "Call_back": "Call back",
- "Call_again": "Call again",
- "Call_ongoing": "Call ongoing",
+ "Call_rejected": "Call rejected",
+ "Call_back": "Call Back",
+ "Call_again": "Call Again",
+ "Call_ongoing": "Call Ongoing",
"Joined": "Joined",
- "Calling": "Calling...",
+ "Calling": "Calling",
"Start_a_call": "Start a call",
"Call": "Call",
"Reply_in_direct_message": "Reply in direct message",
@@ -731,5 +732,9 @@
"Wi_Fi_and_mobile_data":"Wi-Fi and mobile data",
"Wi_Fi": "Wi-Fi",
"Off": "Off",
- "Audio": "Audio"
+ "Audio": "Audio",
+ "decline": "Decline",
+ "accept": "Accept",
+ "Incoming_call_from": "Incoming call from",
+ "Call_started": "Call started"
}
\ No newline at end of file
diff --git a/app/i18n/locales/fi.json b/app/i18n/locales/fi.json
index 1ef0a60ec..4e08db223 100644
--- a/app/i18n/locales/fi.json
+++ b/app/i18n/locales/fi.json
@@ -695,7 +695,7 @@
"Call_again": "Soita uudelleen",
"Call_ongoing": "Puhelu käynnissä",
"Joined": "Liitytty",
- "Calling": "Soitetaan...",
+ "Calling": "Soitetaan",
"Start_a_call": "Aloita puhelu",
"Call": "Soita",
"Reply_in_direct_message": "Vastaa suoralla viestillä",
diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json
index 49560a2f5..f893fe82b 100644
--- a/app/i18n/locales/pt-BR.json
+++ b/app/i18n/locales/pt-BR.json
@@ -695,7 +695,7 @@
"Call_again": "Ligue novamente",
"Call_ongoing": "Chamada em andamento",
"Joined": "Ingressou",
- "Calling": "Chamando...",
+ "Calling": "Chamando",
"Start_a_call": "Inicie uma chamada",
"Call": "Ligar",
"Reply_in_direct_message": "Responder por mensagem direta",
@@ -719,5 +719,9 @@
"Wi_Fi_and_mobile_data":"Wi-Fi e dados móveis",
"Wi_Fi": "Wi-Fi",
"Off": "Desativado",
- "Audio": "Áudio"
+ "Audio": "Áudio",
+ "decline": "Recusar",
+ "accept": "Aceitar",
+ "Incoming_call_from": "Chamada recebida de",
+ "Call_started": "Chamada Iniciada"
}
\ No newline at end of file
diff --git a/app/i18n/locales/sv.json b/app/i18n/locales/sv.json
index 8db9c4282..7ab871414 100644
--- a/app/i18n/locales/sv.json
+++ b/app/i18n/locales/sv.json
@@ -693,7 +693,7 @@
"Call_again": "Ring igen",
"Call_ongoing": "Samtal pågår",
"Joined": "Anslöt",
- "Calling": "Ringer upp...",
+ "Calling": "Ringer upp",
"Start_a_call": "Starta ett samtal",
"Call": "Ring",
"Reply_in_direct_message": "Svara med direktmeddelande",
diff --git a/app/lib/constants/colors.ts b/app/lib/constants/colors.ts
index 6176bd843..eba53bca4 100644
--- a/app/lib/constants/colors.ts
+++ b/app/lib/constants/colors.ts
@@ -20,6 +20,11 @@ const mentions = {
mentionOtherColor: '#F3BE08'
};
+const callButtons = {
+ cancelCallButton: '#F5455C',
+ acceptCallButton: '#158D65'
+};
+
export const colors = {
light: {
backgroundColor: '#ffffff',
@@ -89,8 +94,14 @@ export const colors = {
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
textInputSecondaryBackground: '#E4E7EA',
+ dotBg: '#a9cbff',
+ dotActiveBg: '#1d74f5',
+ gray300: '#5f656e',
+ gray100: '#CBCED1',
+ n900: '#1F2329',
overlayColor: '#1F2329B2',
- ...mentions
+ ...mentions,
+ ...callButtons
},
dark: {
backgroundColor: '#030b1b',
@@ -159,9 +170,15 @@ export const colors = {
conferenceCallEnabledIcon: '#FFFFFF',
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
- textInputSecondaryBackground: '#030b1b', // backgroundColor
+ textInputSecondaryBackground: '#030b1b',
+ dotBg: '#a9cbff',
+ dotActiveBg: '#1d74f5',
+ gray300: '#5f656e',
+ gray100: '#CBCED1',
+ n900: '#FFFFFF',
overlayColor: '#1F2329B2',
- ...mentions
+ ...mentions,
+ ...callButtons
},
black: {
backgroundColor: '#000000',
@@ -230,9 +247,15 @@ export const colors = {
conferenceCallEnabledIcon: '#FFFFFF',
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
- textInputSecondaryBackground: '#000000', // backgroundColor
+ textInputSecondaryBackground: '#000000',
+ dotBg: '#a9cbff',
+ dotActiveBg: '#1d74f5',
+ gray300: '#5f656e',
+ gray100: '#CBCED1',
+ n900: '#FFFFFF',
overlayColor: '#1F2329B2',
- ...mentions
+ ...mentions,
+ ...callButtons
}
};
diff --git a/app/lib/database/services/Subscription.ts b/app/lib/database/services/Subscription.ts
index 4f68f4320..89d403ddf 100644
--- a/app/lib/database/services/Subscription.ts
+++ b/app/lib/database/services/Subscription.ts
@@ -1,10 +1,11 @@
import database from '..';
+import { TSubscriptionModel } from '../../../definitions';
import { TAppDatabase } from '../interfaces';
import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
const getCollection = (db: TAppDatabase) => db.get(SUBSCRIPTIONS_TABLE);
-export const getSubscriptionByRoomId = async (rid: string) => {
+export const getSubscriptionByRoomId = async (rid: string): Promise => {
const db = database.active;
const subCollection = getCollection(db);
try {
diff --git a/app/lib/hooks/useAppSelector.ts b/app/lib/hooks/useAppSelector.ts
index 58c524b6b..98231ab67 100644
--- a/app/lib/hooks/useAppSelector.ts
+++ b/app/lib/hooks/useAppSelector.ts
@@ -1,5 +1,10 @@
import { TypedUseSelectorHook, useSelector } from 'react-redux';
+import { select } from 'redux-saga/effects';
import { IApplicationState } from '../../definitions';
export const useAppSelector: TypedUseSelectorHook = useSelector;
+
+export function* appSelector(selector: (state: IApplicationState) => TSelected): Generator {
+ return yield select(selector);
+}
diff --git a/app/lib/hooks/useUserData.ts b/app/lib/hooks/useUserData.ts
new file mode 100644
index 000000000..92258f69f
--- /dev/null
+++ b/app/lib/hooks/useUserData.ts
@@ -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;
diff --git a/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx b/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx
new file mode 100644
index 000000000..5459ed8d9
--- /dev/null
+++ b/app/lib/hooks/useVideoConf/StartACallActionSheet.tsx
@@ -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 (
+ setContainerWidth(e.nativeEvent.layout.width / 2)}
+ >
+ {calling ? : null}
+
+
+ {cam ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+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
+ }
+});
diff --git a/app/lib/hooks/useVideoConf.tsx b/app/lib/hooks/useVideoConf/index.tsx
similarity index 73%
rename from app/lib/hooks/useVideoConf.tsx
rename to app/lib/hooks/useVideoConf/index.tsx
index 1f1a327bf..e7e80b216 100644
--- a/app/lib/hooks/useVideoConf.tsx
+++ b/app/lib/hooks/useVideoConf/index.tsx
@@ -1,17 +1,17 @@
+import { Camera } from 'expo-camera';
import React, { useEffect, useState } from 'react';
-import { useActionSheet } from '../../containers/ActionSheet';
-import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
-import { ISubscription, SubscriptionType } from '../../definitions';
-import i18n from '../../i18n';
-import { getUserSelector } from '../../selectors/login';
-import { getSubscriptionByRoomId } from '../database/services/Subscription';
-import { callJitsi } from '../methods';
-import { compareServerVersion, showErrorAlert } from '../methods/helpers';
-import { videoConfStartAndJoin } from '../methods/videoConf';
-import { Services } from '../services';
-import { useAppSelector } from './useAppSelector';
-import { useSnaps } from './useSnaps';
+import { useActionSheet } from '../../../containers/ActionSheet';
+import { SubscriptionType } from '../../../definitions';
+import i18n from '../../../i18n';
+import { getUserSelector } from '../../../selectors/login';
+import { getSubscriptionByRoomId } from '../../database/services/Subscription';
+import { compareServerVersion, showErrorAlert } from '../../methods/helpers';
+import { handleAndroidBltPermission } from '../../methods/videoConf';
+import { Services } from '../../services';
+import { useAppSelector } from '../useAppSelector';
+import { useSnaps } from '../useSnaps';
+import StartACallActionSheet from './StartACallActionSheet';
const availabilityErrors = {
NOT_CONFIGURED: 'video-conf-provider-not-configured',
@@ -33,6 +33,8 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
const user = useAppSelector(state => getUserSelector(state));
+ const [permission, requestPermission] = Camera.useCameraPermissions();
+
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const { showActionSheet } = useActionSheet();
@@ -76,19 +78,17 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
return true;
};
- const initCall = async ({ cam, mic }: { cam: boolean; mic: boolean }) => {
- if (isServer5OrNewer) return videoConfStartAndJoin({ rid, cam, mic });
- const room = (await getSubscriptionByRoomId(rid)) as ISubscription;
- callJitsi({ room, cam });
- };
-
const showInitCallActionSheet = async () => {
const canInit = await canInitAnCall();
if (canInit) {
showActionSheet({
- children: ,
+ children: ,
snaps
});
+ if (!permission?.granted) {
+ requestPermission();
+ handleAndroidBltPermission();
+ }
}
};
diff --git a/app/lib/methods/helpers/normalizeMessage.ts b/app/lib/methods/helpers/normalizeMessage.ts
index bb31d5858..6753fb9c6 100644
--- a/app/lib/methods/helpers/normalizeMessage.ts
+++ b/app/lib/methods/helpers/normalizeMessage.ts
@@ -9,14 +9,17 @@ function normalizeAttachments(msg: TMsg) {
if (typeof msg.attachments !== typeof [] || !msg.attachments || !msg.attachments.length) {
msg.attachments = [];
}
- msg.attachments = msg.attachments.map(att => {
- att.fields = att.fields || [];
- if (att.ts) {
- att.ts = moment(att.ts).toDate();
- }
- att = normalizeAttachments(att as TMsg);
- return att;
- });
+
+ msg.attachments = msg.attachments
+ .filter(att => !!att)
+ .map(att => {
+ att.fields = att.fields || [];
+ if (att.ts) {
+ att.ts = moment(att.ts).toDate();
+ }
+ att = normalizeAttachments(att as TMsg);
+ return att;
+ });
return msg;
}
diff --git a/app/lib/methods/helpers/notifications.ts b/app/lib/methods/helpers/notifications.ts
new file mode 100644
index 000000000..eb24d80dd
--- /dev/null
+++ b/app/lib/methods/helpers/notifications.ts
@@ -0,0 +1,3 @@
+import { Notifier } from 'react-native-notifier';
+
+export const hideNotification = (): void => Notifier.hideNotification();
diff --git a/app/lib/methods/subscriptions/rooms.ts b/app/lib/methods/subscriptions/rooms.ts
index a3532b58b..c62dcbe76 100644
--- a/app/lib/methods/subscriptions/rooms.ts
+++ b/app/lib/methods/subscriptions/rooms.ts
@@ -34,6 +34,7 @@ import { E2E_MESSAGE_TYPE } from '../../constants';
import { getRoom } from '../getRoom';
import { merge } from '../helpers/mergeSubscriptionsRooms';
import { getRoomAvatar, getRoomTitle, getSenderName, random } from '../helpers';
+import { handleVideoConfIncomingWebsocketMessages } from '../../../actions/videoConf';
const removeListener = (listener: { stop: () => void }) => listener.stop();
@@ -409,6 +410,10 @@ export default function subscribeRooms() {
log(e);
}
}
+ if (/video-conference/.test(ev)) {
+ const [action, params] = ddpMessage.fields.args;
+ store.dispatch(handleVideoConfIncomingWebsocketMessages({ action, params }));
+ }
});
const stop = () => {
diff --git a/app/lib/methods/videoConf.ts b/app/lib/methods/videoConf.ts
index 3385ea7ef..0603dcdb9 100644
--- a/app/lib/methods/videoConf.ts
+++ b/app/lib/methods/videoConf.ts
@@ -19,18 +19,17 @@ const handleBltPermission = async (): Promise => {
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
};
+export const handleAndroidBltPermission = async (): Promise => {
+ if (isAndroid) {
+ const bltPermission = await handleBltPermission();
+ await PermissionsAndroid.requestMultiple(bltPermission);
+ }
+};
+
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise => {
try {
const result = await Services.videoConferenceJoin(callId, cam, mic);
if (result.success) {
- if (isAndroid) {
- const bltPermission = await handleBltPermission();
- await PermissionsAndroid.requestMultiple([
- PermissionsAndroid.PERMISSIONS.CAMERA,
- PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
- ...bltPermission
- ]);
- }
const { url, providerName } = result;
if (providerName === 'jitsi') {
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });
@@ -43,15 +42,3 @@ export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean
log(e);
}
};
-
-export const videoConfStartAndJoin = async ({ rid, cam, mic }: { rid: string; cam?: boolean; mic?: boolean }): Promise => {
- 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);
- }
-};
diff --git a/app/lib/notifications/push.ts b/app/lib/notifications/push.ts
index 4515da941..77ebe1f41 100644
--- a/app/lib/notifications/push.ts
+++ b/app/lib/notifications/push.ts
@@ -7,6 +7,7 @@ import {
NotificationAction,
NotificationCategory
} from 'react-native-notifications';
+import { PermissionsAndroid, Platform } from 'react-native';
import { INotification } from '../../definitions';
import { isIOS } from '../methods/helpers';
@@ -36,9 +37,16 @@ export const pushNotificationConfigure = (onNotification: (notification: INotifi
});
const notificationCategory = new NotificationCategory('MESSAGE', [notificationAction]);
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 {
+ // TODO: Ask user to enable notifications
+ }
+ });
} else {
- // init
- Notifications.android.registerRemoteNotifications();
+ Notifications.registerRemoteNotifications();
}
Notifications.events().registerRemoteNotificationsRegistered((event: Registered) => {
diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts
index 13067e85e..8059195a8 100644
--- a/app/lib/services/restApi.ts
+++ b/app/lib/services/restApi.ts
@@ -947,7 +947,9 @@ export const videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean
export const videoConferenceGetCapabilities = () => sdk.get('video-conference.capabilities');
-export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });
+export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId, allowRinging: true });
+
+export const videoConferenceCancel = (callId: string) => sdk.post('video-conference.cancel', { callId });
export const saveUserProfileMethod = (
params: IProfileParams,
@@ -961,3 +963,6 @@ export const saveUserProfileMethod = (
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
// RC 0.67.0
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish });
+
+export const notifyUser = (type: string, params: Record): Promise =>
+ sdk.methodCall('stream-notify-user', type, params);
diff --git a/app/reducers/index.js b/app/reducers/index.js
index 7aad41aa0..1e05aa24c 100644
--- a/app/reducers/index.js
+++ b/app/reducers/index.js
@@ -21,6 +21,7 @@ import enterpriseModules from './enterpriseModules';
import encryption from './encryption';
import permissions from './permissions';
import roles from './roles';
+import videoConf from './videoConf';
export default combineReducers({
settings,
@@ -43,5 +44,6 @@ export default combineReducers({
enterpriseModules,
encryption,
permissions,
- roles
+ roles,
+ videoConf
});
diff --git a/app/reducers/videoConf.test.ts b/app/reducers/videoConf.test.ts
new file mode 100644
index 000000000..f4fd51cef
--- /dev/null
+++ b/app/reducers/videoConf.test.ts
@@ -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);
+ });
+});
diff --git a/app/reducers/videoConf.ts b/app/reducers/videoConf.ts
new file mode 100644
index 000000000..22686232f
--- /dev/null
+++ b/app/reducers/videoConf.ts
@@ -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;
+ }
+};
diff --git a/app/sagas/index.js b/app/sagas/index.js
index ab9508dfb..12d2ba83f 100644
--- a/app/sagas/index.js
+++ b/app/sagas/index.js
@@ -13,6 +13,7 @@ import deepLinking from './deepLinking';
import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion';
import encryption from './encryption';
+import videoConf from './videoConf';
const root = function* root() {
yield all([
@@ -28,7 +29,8 @@ const root = function* root() {
inviteLinks(),
createDiscussion(),
inquiry(),
- encryption()
+ encryption(),
+ videoConf()
]);
};
diff --git a/app/sagas/videoConf.ts b/app/sagas/videoConf.ts
new file mode 100644
index 000000000..b3b5fe439
--- /dev/null
+++ b/app/sagas/videoConf.ts
@@ -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(VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES, handleVideoConfIncomingWebsocketMessages);
+ yield takeEvery(VIDEO_CONF.INIT_CALL, initCall);
+ yield takeEvery(VIDEO_CONF.CANCEL_CALL, cancelCall);
+ yield takeEvery(VIDEO_CONF.ACCEPT_CALL, acceptCall);
+}
diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx
index 3228a986b..023153331 100644
--- a/app/stacks/InsideStack.tsx
+++ b/app/stacks/InsideStack.tsx
@@ -129,11 +129,7 @@ const ChatsStackNavigator = () => {
-
+
{/* @ts-ignore */}
{/* @ts-ignore */}
@@ -179,12 +175,7 @@ const SettingsStackNavigator = () => {
>
-
+
diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx
index 3fdc096f7..3440fcf60 100644
--- a/app/stacks/MasterDetailStack/index.tsx
+++ b/app/stacks/MasterDetailStack/index.tsx
@@ -131,12 +131,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
{/* @ts-ignore */}
-
+
@@ -195,12 +190,7 @@ const ModalStackNavigator = React.memo(({ navigation }: INavigation) => {
-
+
);
diff --git a/app/stacks/OutsideStack.tsx b/app/stacks/OutsideStack.tsx
index 97c9a6d44..1ba6dd679 100644
--- a/app/stacks/OutsideStack.tsx
+++ b/app/stacks/OutsideStack.tsx
@@ -24,7 +24,7 @@ const _OutsideStack = () => {
{/* @ts-ignore */}
-
+
{/* @ts-ignore */}
diff --git a/app/stacks/types.ts b/app/stacks/types.ts
index dd4a6cdc8..6e265097d 100644
--- a/app/stacks/types.ts
+++ b/app/stacks/types.ts
@@ -10,7 +10,6 @@ import { IMessage, TAnyMessageModel, TMessageModel } from '../definitions/IMessa
import { TServerModel } from '../definitions/IServer';
import { ISubscription, SubscriptionType, TSubscriptionModel } from '../definitions/ISubscription';
import { TChangeAvatarViewContext } from '../definitions/TChangeAvatarViewContext';
-import { IItem } from '../views/TeamChannelsView';
import { MasterDetailInsideStackParamList, ModalStackParamList } from './MasterDetailStack/types';
import { TNavigation } from './stackType';
@@ -154,12 +153,10 @@ export type ChatsStackParamList = {
teamId?: string;
};
AddChannelTeamView: {
- teamId?: string;
- teamChannels: IItem[];
+ teamId: string;
};
AddExistingChannelView: {
- teamId?: string;
- teamChannels: IItem[];
+ teamId: string;
};
MarkdownTableView: {
renderRows: (drawExtraBorders?: boolean) => JSX.Element;
diff --git a/app/views/AddChannelTeamView.tsx b/app/views/AddChannelTeamView.tsx
index 7b924b59e..e2da06e98 100644
--- a/app/views/AddChannelTeamView.tsx
+++ b/app/views/AddChannelTeamView.tsx
@@ -40,7 +40,9 @@ const setHeader = ({
const AddChannelTeamView = () => {
const navigation = useNavigation();
const isMasterDetail = useSelector((state: IApplicationState) => state.app.isMasterDetail);
- const { teamChannels, teamId } = useRoute().params;
+ const {
+ params: { teamId }
+ } = useRoute();
useEffect(() => {
setHeader({ navigation, isMasterDetail });
@@ -70,7 +72,7 @@ const AddChannelTeamView = () => {
navigation.navigate('AddExistingChannelView', { teamId, teamChannels })}
+ onPress={() => navigation.navigate('AddExistingChannelView', { teamId })}
testID='add-channel-team-view-add-existing'
left={() => }
right={() => }
diff --git a/app/views/AddExistingChannelView.tsx b/app/views/AddExistingChannelView.tsx
deleted file mode 100644
index cefa750ab..000000000
--- a/app/views/AddExistingChannelView.tsx
+++ /dev/null
@@ -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;
- route: RouteProp;
- theme?: TSupportedThemes;
- isMasterDetail: boolean;
- addTeamChannelPermission?: string[];
-}
-
-const QUERY_SIZE = 50;
-
-class AddExistingChannelView extends React.Component {
- 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 = () => ;
- }
-
- options.headerRight = () =>
- selected.length > 0 && (
-
-
-
- );
-
- 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 = () => (
- 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 (
- this.toggleChannel(item.rid)}
- testID={`add-existing-channel-view-item-${item.name}`}
- left={() => }
- right={() => (isChecked ? : null)}
- />
- );
- };
-
- renderList = () => {
- const { search, channels } = this.state;
- const { theme } = this.props;
- return (
- 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 (
-
-
- {this.renderList()}
-
- );
- }
-}
-
-const mapStateToProps = (state: IApplicationState) => ({
- isMasterDetail: state.app.isMasterDetail,
- addTeamChannelPermission: state.permissions['add-team-channel']
-});
-
-export default connect(mapStateToProps)(withTheme(AddExistingChannelView));
diff --git a/app/views/AddExistingChannelView/index.tsx b/app/views/AddExistingChannelView/index.tsx
new file mode 100644
index 000000000..c42751f61
--- /dev/null
+++ b/app/views/AddExistingChannelView/index.tsx
@@ -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;
+type TRoute = RouteProp;
+
+const QUERY_SIZE = 50;
+
+const AddExistingChannelView = () => {
+ const [channels, setChannels] = useState([]);
+ const [selected, setSelected] = useState([]);
+
+ const { colors } = useTheme();
+
+ const navigation = useNavigation();
+ const {
+ params: { teamId }
+ } = useRoute();
+
+ 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 = () => ;
+ }
+
+ options.headerRight = () =>
+ selected.length > 0 && (
+
+
+
+ );
+
+ 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 (
+
+
+ item.id}
+ ListHeaderComponent={
+ 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 (
+ toggleChannel(item.rid)}
+ testID={`add-existing-channel-view-item-${item.name}`}
+ left={() => }
+ right={() => (isChecked(item.rid) ? : null)}
+ />
+ );
+ }}
+ ItemSeparatorComponent={List.Separator}
+ contentContainerStyle={{ backgroundColor: colors.backgroundColor }}
+ keyboardShouldPersistTaps='always'
+ />
+
+ );
+};
+
+export default AddExistingChannelView;
diff --git a/app/views/AttachmentView.tsx b/app/views/AttachmentView.tsx
index 68e0fbaa2..992b022c3 100644
--- a/app/views/AttachmentView.tsx
+++ b/app/views/AttachmentView.tsx
@@ -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 { StackNavigationOptions } from '@react-navigation/stack';
import { ResizeMode, Video } from 'expo-av';
diff --git a/app/views/E2EEncryptionSecurityView.tsx b/app/views/E2EEncryptionSecurityView.tsx
deleted file mode 100644
index 6383a2e07..000000000
--- a/app/views/E2EEncryptionSecurityView.tsx
+++ /dev/null
@@ -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 {
- user: IUser;
- server: string;
- encryptionEnabled: boolean;
-}
-
-class E2EEncryptionSecurityView extends React.Component {
- 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 (
- <>
-
-
- {I18n.t('E2E_encryption_change_password_title')}
-
-
- {I18n.t('E2E_encryption_change_password_description')}
-
-
-
-
-
- >
- );
- };
-
- render() {
- const { theme } = this.props;
- return (
-
-
-
-
- {this.renderChangePassword()}
-
-
-
- {I18n.t('E2E_encryption_reset_title')}
-
-
- {I18n.t('E2E_encryption_reset_description')}
-
-
-
-
-
-
- );
- }
-}
-
-const mapStateToProps = (state: IApplicationState) => ({
- server: state.server.server,
- user: getUserSelector(state),
- encryptionEnabled: state.encryption.enabled
-});
-
-export default connect(mapStateToProps)(withTheme(E2EEncryptionSecurityView));
diff --git a/app/views/E2EEncryptionSecurityView/ChangePassword.tsx b/app/views/E2EEncryptionSecurityView/ChangePassword.tsx
new file mode 100644
index 000000000..f55f4d216
--- /dev/null
+++ b/app/views/E2EEncryptionSecurityView/ChangePassword.tsx
@@ -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(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 (
+ <>
+
+ {I18n.t('E2E_encryption_change_password_title')}
+
+ {I18n.t('E2E_encryption_change_password_description')}
+
+
+
+
+
+ >
+ );
+};
+
+export default ChangePassword;
diff --git a/app/views/E2EEncryptionSecurityView/index.tsx b/app/views/E2EEncryptionSecurityView/index.tsx
new file mode 100644
index 000000000..eb739144e
--- /dev/null
+++ b/app/views/E2EEncryptionSecurityView/index.tsx
@@ -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>();
+ 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 (
+
+
+
+
+
+
+
+ {I18n.t('E2E_encryption_reset_title')}
+ {I18n.t('E2E_encryption_reset_description')}
+
+
+
+
+
+ );
+};
+
+export default E2EEncryptionSecurityView;
diff --git a/app/views/JitsiMeetView.tsx b/app/views/JitsiMeetView.tsx
index 1c699fde7..0221848a4 100644
--- a/app/views/JitsiMeetView.tsx
+++ b/app/views/JitsiMeetView.tsx
@@ -94,7 +94,7 @@ class JitsiMeetView extends React.Component {
userAgent={userAgent}
javaScriptEnabled
domStorageEnabled
- mediaPlaybackRequiresUserAction={false}
+ allowsInlineMediaPlayback
mediaCapturePermissionGrantType={'grant'}
/>
diff --git a/app/views/TeamChannelsView.tsx b/app/views/TeamChannelsView.tsx
index bce566cb6..243978b90 100644
--- a/app/views/TeamChannelsView.tsx
+++ b/app/views/TeamChannelsView.tsx
@@ -186,7 +186,7 @@ class TeamChannelsView extends React.Component {
- const { isSearching, showCreate, data } = this.state;
+ const { isSearching, showCreate } = this.state;
const { navigation, isMasterDetail, theme } = this.props;
const { team } = this;
@@ -234,7 +234,7 @@ class TeamChannelsView extends React.Component navigation.navigate('AddChannelTeamView', { teamId: this.teamId, teamChannels: data })}
+ onPress={() => navigation.navigate('AddChannelTeamView', { teamId: this.teamId })}
/>
) : null}
diff --git a/app/views/WorkspaceView/RegisterDisabledComponent.tsx b/app/views/WorkspaceView/RegisterDisabledComponent.tsx
new file mode 100644
index 000000000..c1f927209
--- /dev/null
+++ b/app/views/WorkspaceView/RegisterDisabledComponent.tsx
@@ -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 {registrationText};
+};
+
+export default RegisterDisabledComponent;
diff --git a/app/views/WorkspaceView/ServerAvatar.tsx b/app/views/WorkspaceView/ServerAvatar.tsx
index 5234878fe..df5f5d8bf 100644
--- a/app/views/WorkspaceView/ServerAvatar.tsx
+++ b/app/views/WorkspaceView/ServerAvatar.tsx
@@ -2,9 +2,8 @@ import React from 'react';
import { StyleSheet, View } from 'react-native';
import FastImage from 'react-native-fast-image';
-import { themes } from '../../lib/constants';
import { isTablet } from '../../lib/methods/helpers';
-import { TSupportedThemes } from '../../theme';
+import { useTheme } from '../../theme';
const SIZE = 96;
const MARGIN_TOP = isTablet ? 0 : 64;
@@ -26,20 +25,19 @@ const styles = StyleSheet.create({
});
interface IServerAvatar {
- theme: TSupportedThemes;
url: string;
image: string;
}
// TODO: missing skeleton
-const ServerAvatar = React.memo(({ theme, url, image }: IServerAvatar) => (
-
- {image && (
-
- )}
-
-));
+const ServerAvatar = React.memo(({ url, image }: IServerAvatar) => {
+ const { colors } = useTheme();
-ServerAvatar.displayName = 'ServerAvatar';
+ return (
+
+ {image && }
+
+ );
+});
export default ServerAvatar;
diff --git a/app/views/WorkspaceView/index.tsx b/app/views/WorkspaceView/index.tsx
index 9958e9032..84491840f 100644
--- a/app/views/WorkspaceView/index.tsx
+++ b/app/views/WorkspaceView/index.tsx
@@ -1,53 +1,66 @@
-import React from 'react';
+import React, { useLayoutEffect } from 'react';
import { Text, View } from 'react-native';
-import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/stack';
-import { connect } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
+import { StackNavigationProp } from '@react-navigation/stack';
import { CompositeNavigationProp } from '@react-navigation/core';
import { OutsideModalParamList, OutsideParamList } from '../../stacks/types';
import I18n from '../../i18n';
import Button from '../../containers/Button';
-import { themes } from '../../lib/constants';
-import { TSupportedThemes, withTheme } from '../../theme';
+import { useTheme } from '../../theme';
import FormContainer, { FormContainerInner } from '../../containers/FormContainer';
-import { IApplicationState } from '../../definitions';
import { IAssetsFavicon512 } from '../../definitions/IAssetsFavicon512';
import { getShowLoginButton } from '../../selectors/login';
import ServerAvatar from './ServerAvatar';
import styles from './styles';
+import { useAppSelector } from '../../lib/hooks';
+import RegisterDisabledComponent from './RegisterDisabledComponent';
-interface IWorkSpaceProp {
- navigation: CompositeNavigationProp<
- StackNavigationProp,
- StackNavigationProp
- >;
- 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;
-}
+type TNavigation = CompositeNavigationProp<
+ StackNavigationProp,
+ StackNavigationProp
+>;
-class WorkspaceView extends React.Component {
- static navigationOptions = (): StackNavigationOptions => ({
- title: I18n.t('Your_workspace')
- });
+const useWorkspaceViewSelector = () =>
+ 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
+ }));
- get showRegistrationButton() {
- const { registrationForm, inviteLinkToken, Accounts_iframe_enabled } = this.props;
- return (
- !Accounts_iframe_enabled &&
- (registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length))
- );
- }
+const WorkspaceView = () => {
+ const navigation = useNavigation();
- login = () => {
- const { navigation, server, Site_Name, Accounts_iframe_enabled } = this.props;
+ 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')
+ });
+ }, [navigation]);
+
+ const showRegistrationButton = !!(
+ !Accounts_iframe_enabled &&
+ (registrationForm === 'Public' || (registrationForm === 'Secret URL' && inviteLinkToken?.length))
+ );
+
+ const login = () => {
if (Accounts_iframe_enabled) {
navigation.navigate('AuthenticationWebView', { url: server, authType: 'iframe' });
return;
@@ -55,61 +68,33 @@ class WorkspaceView extends React.Component {
navigation.navigate('LoginView', { title: Site_Name });
};
- register = () => {
- const { navigation, Site_Name } = this.props;
+ const register = () => {
navigation.navigate('RegisterView', { title: Site_Name });
};
- renderRegisterDisabled = () => {
- const { Accounts_iframe_enabled, registrationText, theme } = this.props;
- if (Accounts_iframe_enabled) {
- return null;
- }
+ return (
+
+
+
+
+ {Site_Name}
+ {Site_Url}
+
+ {showLoginButton ? : null}
+ {showRegistrationButton ? (
+
+ ) : (
+
+ )}
+
+
+ );
+};
- return {registrationText};
- };
-
- render() {
- const { theme, Site_Name, Site_Url, Assets_favicon_512, server, showLoginButton } = this.props;
-
- return (
-
-
-
-
- {Site_Name}
- {Site_Url}
-
- {showLoginButton ? (
-
- ) : null}
- {this.showRegistrationButton ? (
-
- ) : (
- this.renderRegisterDisabled()
- )}
-
-
- );
- }
-}
-
-const mapStateToProps = (state: IApplicationState) => ({
- 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));
+export default WorkspaceView;
diff --git a/e2e/tests/team/02-team.spec.ts b/e2e/tests/team/02-team.spec.ts
index 10d202e65..a66b41e60 100644
--- a/e2e/tests/team/02-team.spec.ts
+++ b/e2e/tests/team/02-team.spec.ts
@@ -382,7 +382,7 @@ describe('Team', () => {
describe('Room Members', () => {
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 () => {
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 397db9568..a0ba8421b 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -8,6 +8,8 @@ PODS:
- EXAV (13.2.1):
- ExpoModulesCore
- ReactCommon/turbomodule/core
+ - EXCamera (13.2.1):
+ - ExpoModulesCore
- EXFileSystem (15.2.2):
- ExpoModulesCore
- Expo (48.0.9):
@@ -383,7 +385,7 @@ PODS:
- React-Core
- react-native-blur (4.1.0):
- React-Core
- - react-native-cameraroll (4.1.2):
+ - react-native-cameraroll (5.6.0):
- React-Core
- react-native-cookies (6.2.1):
- React-Core
@@ -602,6 +604,7 @@ DEPENDENCIES:
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAV (from `../node_modules/expo-av/ios`)
+ - EXCamera (from `../node_modules/expo-camera/ios`)
- EXFileSystem (from `../node_modules/expo-file-system/ios`)
- Expo (from `../node_modules/expo`)
- ExpoAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
@@ -633,7 +636,7 @@ DEPENDENCIES:
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-background-timer (from `../node_modules/react-native-background-timer`)
- "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-document-picker (from `../node_modules/react-native-document-picker`)
- 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"
EXAV:
:path: "../node_modules/expo-av/ios"
+ EXCamera:
+ :path: "../node_modules/expo-camera/ios"
EXFileSystem:
:path: "../node_modules/expo-file-system/ios"
Expo:
@@ -780,7 +785,7 @@ EXTERNAL SOURCES:
react-native-blur:
:path: "../node_modules/@react-native-community/blur"
react-native-cameraroll:
- :path: "../node_modules/@react-native-community/cameraroll"
+ :path: "../node_modules/@react-native-camera-roll/camera-roll"
react-native-cookies:
:path: "../node_modules/@react-native-cookies/cookies"
react-native-document-picker:
@@ -892,6 +897,7 @@ SPEC CHECKSUMS:
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
EXAV: f1f69397ecdcf44cfacd4ff5d338cd1b96891e87
+ EXCamera: a323a5942b5e7fc8349e17d728e91c18840ad561
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
Expo: 863488a600a4565698a79577117c70b170054d08
ExpoAppleAuthentication: 7bd5e4150d59e8df37aa80b425850ae88adf9e65
@@ -938,7 +944,7 @@ SPEC CHECKSUMS:
React-logger: 3f8ebad1be1bf3299d1ab6d7f971802d7395c7ef
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
react-native-blur: ba2f37268542f8a26d809f48c5162705a3261fc6
- react-native-cameraroll: 2957f2bce63ae896a848fbe0d5352c1bd4d20866
+ react-native-cameraroll: 755bcc628148a90a7c9cf3f817a252be3a601bc5
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
react-native-document-picker: f5ec1a712ca2a975c233117f044817bb8393cad4
react-native-mmkv-storage: cfb6854594cfdc5f7383a9e464bb025417d1721c
diff --git a/package.json b/package.json
index f67c2c336..bc483eaf6 100644
--- a/package.json
+++ b/package.json
@@ -43,12 +43,12 @@
"@hookform/resolvers": "^2.9.10",
"@nozbe/watermelondb": "^0.25.5",
"@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-community/art": "^1.2.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/hooks": "2.6.0",
+ "@react-native-community/hooks": "3.0.0",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/picker": "^1.8.1",
"@react-native-community/slider": "^4.4.2",
@@ -73,6 +73,7 @@
"expo": "^48.0.9",
"expo-apple-authentication": "^6.0.1",
"expo-av": "^13.2.1",
+ "expo-camera": "^13.2.1",
"expo-file-system": "^15.2.2",
"expo-haptics": "^12.2.1",
"expo-keep-awake": "^12.0.1",
@@ -144,6 +145,7 @@
"rn-root-view": "RocketChat/rn-root-view",
"semver": "^7.3.8",
"transliteration": "^2.3.5",
+ "typed-redux-saga": "^1.5.0",
"ua-parser-js": "^1.0.32",
"uri-js": "^4.4.1",
"url-parse": "1.5.10",
diff --git a/patches/react-native-notifier+1.6.1.patch b/patches/react-native-notifier+1.6.1.patch
new file mode 100644
index 000000000..830b4c947
--- /dev/null
+++ b/patches/react-native-notifier+1.6.1.patch
@@ -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
+
+
+-
++ {this.state.visible? : null}
+
+
+
+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;
++ visible: boolean;
+ }
+
+ export interface NotifierInterface {
diff --git a/tsconfig.json b/tsconfig.json
index ae6926a81..edd328f50 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -4,7 +4,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
- "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
+ "target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true /* Allow javascript files to be compiled. */,
diff --git a/yarn.lock b/yarn.lock
index 79927ade6..c5978b61f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -830,6 +830,13 @@
dependencies:
"@babel/types" "^7.14.5"
+"@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.18.6":
+ version "7.18.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
+ integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
+ dependencies:
+ "@babel/types" "^7.18.6"
+
"@babel/helper-module-imports@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
@@ -837,13 +844,6 @@
dependencies:
"@babel/types" "^7.16.7"
-"@babel/helper-module-imports@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
- integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
- dependencies:
- "@babel/types" "^7.18.6"
-
"@babel/helper-module-transforms@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d"
@@ -4476,6 +4476,13 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
+"@koale/useworker@^4.0.2":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/@koale/useworker/-/useworker-4.0.2.tgz#cb540a2581cd6025307c3ca6685bc60748773e58"
+ integrity sha512-xPIPADtom8/3/4FLNj7MvNcBM/Z2FleH85Fdx2O869eoKW8+PoEgtSVvoxWjCWMA46Sm9A5/R1TyzNGc+yM0wg==
+ dependencies:
+ dequal "^1.0.0"
+
"@mdx-js/mdx@^1.6.22":
version "1.6.22"
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
@@ -4621,6 +4628,11 @@
dependencies:
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":
version "1.8.5"
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"
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":
version "10.1.1"
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-10.1.1.tgz#4c73ce93a63a24d70c0089d4025daac8184ff504"
@@ -4814,10 +4821,10 @@
dependencies:
invariant "^2.2.4"
-"@react-native-community/hooks@2.6.0":
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.6.0.tgz#dd5f19601eb3684c6bcdd3df3d0c04cf44c24cff"
- integrity sha512-emBGKvhJ0h++lLJQ5ejsj+od9G67nEaihjvfSx7/JWvNrQGAhP9U0OZqgb9dkKzor9Ufaj9SGt8RNY97cGzttw==
+"@react-native-community/hooks@3.0.0":
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-3.0.0.tgz#af5f2ca32eea59b792ce9e3d9a4cf0354f9b195f"
+ integrity sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==
"@react-native-community/netinfo@6.0.0":
version "6.0.0"
@@ -5008,7 +5015,7 @@
"@rocket.chat/sdk@RocketChat/Rocket.Chat.js.SDK#mobile":
version "1.3.0-mobile"
- resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/454b4ba784095057b8de862eb99340311b672e15"
+ resolved "https://codeload.github.com/RocketChat/Rocket.Chat.js.SDK/tar.gz/ad71e7daa5bcb1a3b457b5de20fb0fc86581d04d"
dependencies:
js-sha256 "^0.9.0"
lru-cache "^4.1.1"
@@ -7316,7 +7323,7 @@ babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0:
cosmiconfig "^6.0.0"
resolve "^1.12.0"
-babel-plugin-macros@^3.0.1:
+babel-plugin-macros@^3.0.1, babel-plugin-macros@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
@@ -9424,6 +9431,11 @@ deprecated-react-native-prop-types@^3.0.1:
invariant "*"
prop-types "*"
+dequal@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.1.tgz#dbbf9795ec626e9da8bd68782f4add1d23700d8b"
+ integrity sha512-Fx8jxibzkJX2aJgyfSdLhr9tlRoTnHKrRJuu2XHlAgKioN2j19/Bcbe0d4mFXYZ3+wpE2KVobUVTfDutcD17xQ==
+
dequal@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
@@ -10539,6 +10551,14 @@ expo-av@^13.2.1:
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-13.2.1.tgz#ce502a4c5d4a57962fd9f5f1a40c76c39c88f5ee"
integrity sha512-mC0mYSzaOaZgXjzhW2l4Ag325JjH6q5IbptfwD7gkMOFYy7VPOMxEMUnetadbs3DDzmgE6vUWrTjUIUbwq59qg==
+expo-camera@^13.2.1:
+ version "13.2.1"
+ resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-13.2.1.tgz#bfd1e2248d10a5da43d43a4cc77e378e5acf25bb"
+ integrity sha512-fZdRyF402jJGGmLVlumrLcr5Em9+Y2SO1MIlxLBtHXnybyHbTRMRAbzVapKX1Aryfujqadh+Kl+sdsWYkMuJjg==
+ dependencies:
+ "@koale/useworker" "^4.0.2"
+ invariant "^2.2.4"
+
expo-constants@~14.2.0, expo-constants@~14.2.1:
version "14.2.1"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-14.2.1.tgz#b5b6b8079d2082c31ccf2cbc7cf97a0e83c229c3"
@@ -19827,6 +19847,14 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
+typed-redux-saga@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/typed-redux-saga/-/typed-redux-saga-1.5.0.tgz#f70b47c92c6e29e0184d0c30d563c18d6ad0ae54"
+ integrity sha512-XHKliNtRNUegYAAztbVDb5Q+FMqYNQPaed6Xq2N8kz8AOmiOCVxW3uIj7TEptR1/ms6M9u3HEDfJr4qqz/PYrw==
+ optionalDependencies:
+ "@babel/helper-module-imports" "^7.14.5"
+ babel-plugin-macros "^3.1.0"
+
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"