Merge branch 'develop' into chore.try-flashlist

This commit is contained in:
Diego Mello 2022-11-28 18:22:11 -03:00
commit 2ee4efd255
61 changed files with 1046 additions and 333 deletions

View File

@ -1,10 +1,11 @@
import React from 'react';
import { ViewStyle } from 'react-native';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar {
server?: string;
style?: any;
style?: ViewStyle;
text?: string;
avatar?: string;
emoji?: string;

View File

@ -1,7 +1,9 @@
export const mappedIcons = {
'lamp-bulb': 59812,
'lamp-bulb': 59836,
'phone-in': 59835,
'basketball': 59776,
'percentage': 59777,
'glasses': 59812,
'burger': 59813,
'leaf': 59814,
'airplane': 59815,

File diff suppressed because one or more lines are too long

View File

@ -12,7 +12,6 @@ import { themes } from '../../lib/constants';
import { useTheme } from '../../theme';
import { ROW_HEIGHT } from '../RoomItem';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import Navigation from '../../lib/navigation/appNavigation';
import { useOrientation } from '../../dimensions';
import { IApplicationState, ISubscription, SubscriptionType } from '../../definitions';
@ -98,12 +97,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
prid
};
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
goRoom({ item, isMasterDetail, jumpToMessageId: _id });
goRoom({ item, isMasterDetail, jumpToMessageId: _id, popToRoot: true });
hideNotification();
};
@ -124,6 +118,7 @@ const NotifierComponent = React.memo(({ notification, isMasterDetail }: INotifie
onPress={onPress}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
testID={`in-app-notification-${text}`}
>
<>
<Avatar text={avatar} size={AVATAR_SIZE} type={type} rid={rid} style={styles.avatar} />

View File

@ -1,56 +1,50 @@
import React, { memo, useEffect } from 'react';
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
import { connect } from 'react-redux';
import { dequal } from 'dequal';
import NotifierComponent, { INotifierComponent } from './NotifierComponent';
import EventEmitter from '../../lib/methods/helpers/events';
import Navigation from '../../lib/navigation/appNavigation';
import { getActiveRoute } from '../../lib/methods/helpers/navigation';
import { IApplicationState } from '../../definitions';
import { IRoom } from '../../reducers/room';
import { useAppSelector } from '../../lib/hooks';
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
const InAppNotification = memo(
({ rooms, appState }: { rooms: IRoom['rooms']; appState: string }) => {
const show = (notification: INotifierComponent['notification']) => {
if (appState !== 'foreground') {
const InAppNotification = memo(() => {
const { appState, subscribedRoom } = useAppSelector(state => ({
subscribedRoom: state.room.subscribedRoom,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
}));
const show = (notification: INotifierComponent['notification']) => {
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;
}
const { payload } = notification;
const state = Navigation.navigationRef.current?.getRootState();
const route = getActiveRoute(state);
if (payload.rid) {
if (rooms.includes(payload.rid) || route?.name === 'JitsiMeetView') {
return;
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
Notifier.showNotification({
showEasing: Easing.inOut(Easing.quad),
Component: NotifierComponent,
componentProps: {
notification
}
});
}
});
}
};
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [subscribedRoom, appState]);
useEffect(() => {
const listener = EventEmitter.addEventListener(INAPP_NOTIFICATION_EMITTER, show);
return () => {
EventEmitter.removeListener(INAPP_NOTIFICATION_EMITTER, listener);
};
}, [rooms]);
return <NotifierRoot />;
},
(prevProps, nextProps) => dequal(prevProps.rooms, nextProps.rooms)
);
const mapStateToProps = (state: IApplicationState) => ({
rooms: state.room.rooms,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background'
return <NotifierRoot />;
});
export default connect(mapStateToProps)(InAppNotification);
export default InAppNotification;

View File

@ -353,9 +353,10 @@ const MessageActions = React.memo(
const getOptions = (message: TAnyMessageModel) => {
const options: TActionSheetOptionsItem[] = [];
const videoConfBlock = message.t === 'videoconf';
// Quote
if (!isReadOnly) {
if (!isReadOnly && !videoConfBlock) {
options.push({
title: I18n.t('Quote'),
icon: 'quote',
@ -373,7 +374,7 @@ const MessageActions = React.memo(
}
// Reply in DM
if (room.t !== 'd' && room.t !== 'l' && createDirectMessagePermission) {
if (room.t !== 'd' && room.t !== 'l' && createDirectMessagePermission && !videoConfBlock) {
options.push({
title: I18n.t('Reply_in_direct_message'),
icon: 'arrow-back',
@ -396,11 +397,13 @@ const MessageActions = React.memo(
});
// Copy
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
if (!videoConfBlock) {
options.push({
title: I18n.t('Copy'),
icon: 'copy',
onPress: () => handleCopy(message)
});
}
// Share
options.push({
@ -410,7 +413,7 @@ const MessageActions = React.memo(
});
// Edit
if (allowEdit(message)) {
if (allowEdit(message) && !videoConfBlock) {
options.push({
title: I18n.t('Edit'),
icon: 'edit',
@ -419,7 +422,7 @@ const MessageActions = React.memo(
}
// Pin
if (Message_AllowPinning && permissions?.hasPinPermission) {
if (Message_AllowPinning && permissions?.hasPinPermission && !videoConfBlock) {
options.push({
title: I18n.t(message.pinned ? 'Unpin' : 'Pin'),
icon: 'pin',
@ -428,7 +431,7 @@ const MessageActions = React.memo(
}
// Star
if (Message_AllowStarring) {
if (Message_AllowStarring && !videoConfBlock) {
options.push({
title: I18n.t(message.starred ? 'Unstar' : 'Star'),
icon: message.starred ? 'star-filled' : 'star',

View File

@ -238,7 +238,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
async componentDidMount() {
const db = database.active;
const { rid, tmid, navigation, sharing, usedCannedResponse, isMasterDetail } = this.props;
const { rid, tmid, navigation, sharing, usedCannedResponse } = this.props;
let msg;
try {
const threadsCollection = db.get('threads');
@ -273,7 +273,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
EventEmiter.addEventListener(KEY_COMMAND, this.handleCommands);
}
if (isMasterDetail && usedCannedResponse) {
if (usedCannedResponse) {
this.onChangeText(usedCannedResponse);
}

View File

@ -0,0 +1,80 @@
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 { videoConfStartAndJoin } from '../../../../lib/methods/videoConf';
import { useTheme } from '../../../../theme';
import { useActionSheet } from '../../../ActionSheet';
import AvatarContainer from '../../../Avatar';
import Button from '../../../Button';
import { CustomIcon } from '../../../CustomIcon';
import { BUTTON_HIT_SLOP } from '../../../message/utils';
import StatusContainer from '../../../Status';
import useStyle from './styles';
export default function CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement {
const style = useStyle();
const { colors } = useTheme();
const [user, setUser] = useState({ username: '', avatar: '', uid: '', rid: '' });
const [phone, setPhone] = useState(true);
const [camera, setCamera] = useState(false);
const username = useAppSelector(state => state.login.user.username);
const { hideActionSheet } = useActionSheet();
useEffect(() => {
(async () => {
const room = await getSubscriptionByRoomId(rid);
const uid = (await getUidDirectMessage(room)) as string;
const avt = getRoomAvatar(room);
setUser({ uid, username: room?.name || '', avatar: avt, rid: room?.id || '' });
})();
}, [rid]);
const handleColor = (enabled: boolean) => (enabled ? colors.conferenceCallEnabledIcon : colors.conferenceCallDisabledIcon);
return (
<View style={style.actionSheetContainer}>
<View style={style.actionSheetHeader}>
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
<View style={style.actionSheetHeaderButtons}>
<Touchable
onPress={() => setCamera(!camera)}
style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={camera ? 'camera' : 'camera-disabled'} size={16} color={handleColor(camera)} />
</Touchable>
<Touchable
onPress={() => setPhone(!phone)}
style={[style.iconCallContainer, phone && style.enabledBackground]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={phone ? 'microphone' : 'microphone-disabled'} size={16} color={handleColor(phone)} />
</Touchable>
</View>
</View>
<View style={style.actionSheetUsernameContainer}>
<AvatarContainer text={user.avatar} size={36} />
<StatusContainer size={16} id={user.uid} style={{ marginLeft: 8, marginRight: 6 }} />
<Text style={style.actionSheetUsername}>{user.username}</Text>
</View>
<View style={style.actionSheetPhotoContainer}>
<AvatarContainer size={62} text={username} />
</View>
<Button
onPress={() => {
hideActionSheet();
setTimeout(() => {
videoConfStartAndJoin(user.rid, camera);
}, 100);
}}
title={i18n.t('Call')}
/>
</View>
);
}

View File

@ -0,0 +1,27 @@
import React from 'react';
import { Text, View } from 'react-native';
import i18n from '../../../../i18n';
import useStyle from './styles';
import AvatarContainer from '../../../Avatar';
const MAX_USERS = 3;
export type TCallUsers = { _id: string; username: string; name: string; avatarETag: string }[];
export const CallParticipants = ({ users }: { users: TCallUsers }): React.ReactElement => {
const style = useStyle();
return (
<>
{users.map(({ username }, index) =>
index < MAX_USERS ? <AvatarContainer style={{ marginRight: 4 }} key={index} size={28} text={username} /> : null
)}
{users.length > MAX_USERS ? (
<View style={style.plusUsers}>
<Text style={style.plusUsersText}>{users.length > 9 ? '+9' : `+${users.length}`}</Text>
</View>
) : null}
<Text style={style.joined}>{i18n.t('Joined')}</Text>
</>
);
};

View File

@ -0,0 +1,55 @@
import React from 'react';
import { View, Text } from 'react-native';
import i18n from '../../../../i18n';
import { useTheme } from '../../../../theme';
import { CustomIcon, TIconsName } from '../../../CustomIcon';
import useStyle from './styles';
type VideoConfMessageIconProps = {
variant: 'ended' | 'incoming' | 'outgoing';
children: React.ReactElement | React.ReactElement[];
};
export const VideoConferenceBaseContainer = ({ variant, children }: VideoConfMessageIconProps): React.ReactElement => {
const { colors } = useTheme();
const style = useStyle();
const iconStyle: { [key: string]: { icon: TIconsName; color: string; backgroundColor: string; label: string } } = {
ended: {
icon: 'phone-end',
color: colors.conferenceCallEndedPhoneIcon,
backgroundColor: colors.conferenceCallEndedPhoneBackground,
label: i18n.t('Call_ended')
},
incoming: {
icon: 'phone-in',
color: colors.conferenceCallIncomingPhoneIcon,
backgroundColor: colors.conferenceCallIncomingPhoneBackground,
label: i18n.t('Calling')
},
outgoing: {
icon: 'phone',
color: colors.conferenceCallOngoingPhoneIcon,
backgroundColor: colors.conferenceCallOngoingPhoneBackground,
label: i18n.t('Call_ongoing')
}
};
return (
<View style={style.container}>
<View style={style.callInfoContainer}>
<View
style={{
...style.iconContainer,
backgroundColor: iconStyle[variant].backgroundColor
}}
>
<CustomIcon name={iconStyle[variant].icon} size={variant === 'incoming' ? 16 : 24} color={iconStyle[variant].color} />
</View>
<Text style={style.infoContainerText}>{iconStyle[variant].label}</Text>
</View>
<View style={style.callToActionContainer}>{children}</View>
</View>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='incoming'>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<Text style={style.callBack}>{i18n.t('Waiting_for_answer')}</Text>
</VideoConferenceBaseContainer>
);
});
export default VideoConferenceDirect;

View File

@ -0,0 +1,64 @@
import React from 'react';
import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import { IUser } from '../../../../definitions';
import { VideoConferenceType } from '../../../../definitions/IVideoConference';
import i18n from '../../../../i18n';
import { useAppSelector } from '../../../../lib/hooks';
import { useSnaps } from '../../../../lib/hooks/useSnaps';
import { useActionSheet } from '../../../ActionSheet';
import CallAgainActionSheet from './CallAgainActionSheet';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
export default function VideoConferenceEnded({
users,
type,
createdBy,
rid
}: {
users: TCallUsers;
type: VideoConferenceType;
createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
rid: string;
}): React.ReactElement {
const style = useStyle();
const username = useAppSelector(state => state.login.user.username);
const { showActionSheet } = useActionSheet();
const snaps = useSnaps([1250]);
const onlyAuthorOnCall = users.length === 1 && users.some(user => user.username === createdBy.username);
return (
<VideoConferenceBaseContainer variant='ended'>
{type === 'direct' ? (
<>
<Touchable
style={style.callToActionCallBack}
onPress={() =>
showActionSheet({
children: <CallAgainActionSheet rid={rid} />,
snaps
})
}
>
<Text style={style.callToActionCallBackText}>
{createdBy.username === username ? i18n.t('Call_back') : i18n.t('Call_again')}
</Text>
</Touchable>
<Text style={style.callBack}>{i18n.t('Call_was_not_answered')}</Text>
</>
) : (
<>
{users.length && !onlyAuthorOnCall ? (
<CallParticipants users={users} />
) : (
<Text style={style.notAnswered}>{i18n.t('Call_was_not_answered')}</Text>
)}
</>
)}
</VideoConferenceBaseContainer>
);
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import { Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import i18n from '../../../../i18n';
import { useVideoConf } from '../../../../lib/hooks/useVideoConf';
import { CallParticipants, TCallUsers } from './CallParticipants';
import useStyle from './styles';
import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer';
export default function VideoConferenceOutgoing({ users, blockId }: { users: TCallUsers; blockId: string }): React.ReactElement {
const style = useStyle();
const { joinCall } = useVideoConf();
return (
<VideoConferenceBaseContainer variant='outgoing'>
<Touchable style={style.callToActionButton} onPress={() => joinCall(blockId)}>
<Text style={style.callToActionButtonText}>{i18n.t('Join')}</Text>
</Touchable>
<CallParticipants users={users} />
</VideoConferenceBaseContainer>
);
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { useTheme } from '../../../../theme';
export default function VideoConferenceSkeletonLoading(): React.ReactElement {
const { colors } = useTheme();
return (
<SkeletonPlaceholder backgroundColor={colors.conferenceCallBackground}>
<SkeletonPlaceholder.Item borderWidth={1} borderRadius={4} marginTop={8}>
<SkeletonPlaceholder.Item alignItems={'center'} flexDirection='row' marginTop={16} marginLeft={16}>
<SkeletonPlaceholder.Item width={28} height={28} />
<SkeletonPlaceholder.Item width={75} height={16} marginLeft={8} borderRadius={0} />
</SkeletonPlaceholder.Item>
<SkeletonPlaceholder.Item
width={'100%'}
height={48}
marginTop={16}
borderBottomLeftRadius={4}
borderBottomRightRadius={4}
borderTopLeftRadius={0}
borderTopRightRadius={0}
/>
</SkeletonPlaceholder.Item>
</SkeletonPlaceholder>
);
}

View File

@ -0,0 +1,126 @@
import { StyleSheet } from 'react-native';
import { useTheme } from '../../../../theme';
import sharedStyles from '../../../../views/Styles';
export default function useStyle() {
const { colors } = useTheme();
return 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: {
fontSize: 12,
marginLeft: 8,
...sharedStyles.textBold,
color: colors.auxiliaryTintColor
},
iconContainer: {
width: 28,
height: 28,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 4
},
callToActionContainer: {
height: 48,
backgroundColor: colors.conferenceCallBackground,
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 16
},
callToActionButtonText: {
fontSize: 12,
...sharedStyles.textSemibold,
color: colors.buttonText
},
callToActionCallBackText: {
fontSize: 12,
...sharedStyles.textSemibold,
color: colors.conferenceCallCallBackText
},
callToActionButton: {
backgroundColor: colors.tintColor,
minWidth: 50,
alignItems: 'center',
justifyContent: 'center',
height: 32,
borderRadius: 4,
marginRight: 8,
paddingHorizontal: 8
},
joined: {
fontSize: 12,
...sharedStyles.textRegular,
color: colors.passcodeSecondary,
marginLeft: 8
},
plusUsers: {
width: 28,
height: 28,
backgroundColor: colors.conferenceCallPlusUsersButton,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center'
},
plusUsersText: {
fontSize: 14,
...sharedStyles.textSemibold,
color: colors.conferenceCallPlusUsersText,
alignSelf: 'center'
},
callBack: {
fontSize: 12,
...sharedStyles.textRegular,
color: colors.passcodeSecondary
},
callToActionCallBack: {
backgroundColor: colors.conferenceCallPlusUsersButton,
minWidth: 50,
alignItems: 'center',
justifyContent: 'center',
height: 32,
borderRadius: 4,
marginRight: 8,
paddingHorizontal: 8
},
notAnswered: {
fontSize: 12,
...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
},
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'
}
});
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import { useEndpointData } from '../../../lib/hooks/useEndpointData';
import VideoConferenceDirect from './components/VideoConferenceDirect';
import VideoConferenceEnded from './components/VideoConferenceEnded';
import VideoConferenceOutgoing from './components/VideoConferenceOutgoing';
import VideoConferenceSkeletonLoading from './components/VideoConferenceSkeletonLoading';
export default function VideoConferenceBlock({ callId, blockId }: { callId: string; blockId: string }): React.ReactElement {
const { result } = useEndpointData('video-conference.info', { callId });
if (result?.success) {
const { users, type, status, createdBy, rid } = result;
if ('endedAt' in result) return <VideoConferenceEnded createdBy={createdBy} rid={rid} type={type} users={users} />;
if (type === 'direct' && status === 0) return <VideoConferenceDirect blockId={blockId} />;
return <VideoConferenceOutgoing blockId={blockId} users={users} />;
}
return <VideoConferenceSkeletonLoading />;
}

View File

@ -29,6 +29,7 @@ import { DatePicker } from './DatePicker';
import { Overflow } from './Overflow';
import { ThemeContext } from '../../theme';
import { IActions, IButton, IElement, IInputIndex, IParser, ISection } from './interfaces';
import VideoConferenceBlock from './VideoConferenceBlock';
const styles = StyleSheet.create({
input: {
@ -149,6 +150,10 @@ class MessageParser extends UiKitParserMessage<React.ReactElement> {
const [{ loading, value }, action] = useBlockContext(element, context);
return <MultiSelect {...element} value={value} onChange={action} context={context} loading={loading} />;
}
video_conf(element: IElement & { callId: string }) {
return <VideoConferenceBlock callId={element.callId} blockId={element.blockId!} />;
}
}
// plain_text and mrkdwn functions are created in MessageParser and the ModalParser's constructor use the same functions

View File

@ -1,11 +1,9 @@
/* eslint-disable no-shadow */
import React, { useContext, useState } from 'react';
import { BlockContext } from '@rocket.chat/ui-kit';
import React, { useContext, useState } from 'react';
import { useVideoConf } from '../../lib/hooks/useVideoConf';
import { IText } from './interfaces';
import { videoConfJoin } from '../../lib/methods/videoConf';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import i18n from '../../i18n';
export const textParser = ([{ text }]: IText[]) => text;
@ -42,7 +40,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false);
const { showActionSheet } = useActionSheet();
const { joinCall } = useVideoConf();
const error = errors && actionId && errors[actionId];
@ -60,20 +58,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
try {
if (appId === 'videoconf-core' && blockId) {
setLoading(false);
const options: TActionSheetOptionsItem[] = [
{
title: i18n.t('Video_call'),
icon: 'camera',
onPress: () => videoConfJoin(blockId, true)
},
{
title: i18n.t('Voice_call'),
icon: 'microphone',
onPress: () => videoConfJoin(blockId, false)
}
];
showActionSheet({ options });
return;
return joinCall(blockId);
}
await action({
blockId,

View File

@ -1,14 +1,11 @@
import React from 'react';
import { StyleProp, Text, TextStyle } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { themes } from '../../lib/constants';
import { useTheme } from '../../theme';
import { IUserChannel } from './interfaces';
import styles from './styles';
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
import { ChatsStackParamList } from '../../stacks/types';
import { useAppSelector } from '../../lib/hooks';
import { goRoom } from '../../lib/methods/helpers/goRoom';
@ -22,7 +19,6 @@ interface IHashtag {
const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => {
const { theme } = useTheme();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const navigation = useNavigation<StackNavigationProp<ChatsStackParamList, 'RoomView'>>();
const handlePress = async () => {
const index = channels?.findIndex(channel => channel.name === hashtag);
@ -33,7 +29,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
};
const room = navParam.rid && (await getSubscriptionByRoomId(navParam.rid));
if (room) {
goRoom({ item: room, isMasterDetail, navigationMethod: isMasterDetail ? navigation.replace : navigation.push });
goRoom({ item: room, isMasterDetail });
} else {
navToRoomInfo(navParam);
}

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useRef } from 'react';
import { messageBlockWithContext } from '../UIKit/MessageBlock';
import { IMessageBlocks } from './interfaces';
@ -6,25 +6,29 @@ import { IMessageBlocks } from './interfaces';
const Blocks = React.memo(({ blocks, id: mid, rid, blockAction }: IMessageBlocks) => {
if (blocks && blocks.length > 0) {
const appId = blocks[0]?.appId || '';
return React.createElement(
messageBlockWithContext({
action: async ({ actionId, value, blockId }: { actionId: string; value: string; blockId: string }) => {
if (blockAction) {
await blockAction({
actionId,
appId,
value,
blockId,
rid,
mid
});
}
},
appId,
rid
}),
{ blocks }
// eslint-disable-next-line react-hooks/rules-of-hooks
const comp = useRef(
React.createElement(
messageBlockWithContext({
action: async ({ actionId, value, blockId }: { actionId: string; value: string; blockId: string }) => {
if (blockAction) {
await blockAction({
actionId,
appId,
value,
blockId,
rid,
mid
});
}
},
appId,
rid
}),
{ blocks }
)
);
return comp.current;
}
return null;
});

View File

@ -276,7 +276,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
get isInfo(): string | boolean {
const { item } = this.props;
if (['e2e', 'discussion-created', 'jitsi_call_started'].includes(item.t)) {
if (['e2e', 'discussion-created', 'jitsi_call_started', 'videoconf'].includes(item.t)) {
return false;
}
return item.t;

View File

@ -229,6 +229,7 @@ export type MessageTypesValues =
| 'room-allowed-reacting'
| 'room-disallowed-reacting'
| 'command'
| 'videoconf'
| LivechatMessageTypes
| TeamMessageTypes
| VoipMessageTypesValues

View File

@ -0,0 +1,95 @@
import type { AtLeast } from './utils';
import type { IRocketChatRecord } from './IRocketChatRecord';
import type { IRoom } from './IRoom';
import type { IUser } from './IUser';
import type { IMessage } from './IMessage';
export enum VideoConferenceStatus {
CALLING = 0,
STARTED = 1,
EXPIRED = 2,
ENDED = 3,
DECLINED = 4
}
export type DirectCallInstructions = {
type: 'direct';
callee: IUser['_id'];
callId: string;
};
export type ConferenceInstructions = {
type: 'videoconference';
callId: string;
rid: IRoom['_id'];
};
export type LivechatInstructions = {
type: 'livechat';
callId: string;
};
export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type'];
export interface IVideoConferenceUser extends Pick<Required<IUser>, '_id' | 'username' | 'name' | 'avatarETag'> {
ts: Date;
}
export interface IVideoConference extends IRocketChatRecord {
type: VideoConferenceType;
rid: string;
users: IVideoConferenceUser[];
status: VideoConferenceStatus;
messages: {
started?: IMessage['_id'];
ended?: IMessage['_id'];
};
url?: string;
createdBy: Pick<IUser, '_id' | 'username' | 'name'>;
createdAt: Date;
endedBy?: Pick<IUser, '_id' | 'username' | 'name'>;
endedAt?: Date;
providerName: string;
providerData?: Record<string, any>;
ringing?: boolean;
}
export interface IDirectVideoConference extends IVideoConference {
type: 'direct';
}
export interface IGroupVideoConference extends IVideoConference {
type: 'videoconference';
anonymousUsers: number;
title: string;
}
export interface ILivechatVideoConference extends IVideoConference {
type: 'livechat';
}
export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference;
export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions;
export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference =>
call?.type === 'direct';
export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference =>
call?.type === 'videoconference';
export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference =>
call?.type === 'livechat';
type GroupVideoConferenceCreateData = Omit<IGroupVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type DirectVideoConferenceCreateData = Omit<IDirectVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
type LivechatVideoConferenceCreateData = Omit<ILivechatVideoConference, 'createdBy'> & { createdBy: IUser['_id'] };
export type VideoConferenceCreateData = AtLeast<
DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData,
'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData'
>;

View File

@ -1,3 +1,5 @@
import { VideoConference } from '../../IVideoConference';
export type VideoConferenceEndpoints = {
'video-conference/jitsi.update-timeout': {
POST: (params: { roomId: string }) => void;
@ -8,4 +10,18 @@ export type VideoConferenceEndpoints = {
'video-conference.start': {
POST: (params: { roomId: string }) => { url: string };
};
'video-conference.cancel': {
POST: (params: { callId: string }) => void;
};
'video-conference.info': {
GET: (params: { callId: string }) => VideoConference & {
capabilities: {
mic?: boolean;
cam?: boolean;
title?: boolean;
};
};
};
};

25
app/definitions/utils.ts Normal file
View File

@ -0,0 +1,25 @@
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<T>;
export type ExtractKeys<T, K extends keyof T, U> = T[K] extends U ? K : never;
export type ValueOf<T> = T[keyof T];
export type UnionToIntersection<T> = (T extends any ? (x: T) => void : never) extends (x: infer U) => void ? U : never;
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
// `T extends any` is a trick to apply a operator to each member of a union
export type KeyOfEach<T> = T extends any ? keyof T : never;
// Taken from https://effectivetypescript.com/2020/04/09/jsonify/
export type Jsonify<T> = T extends Date
? string
: T extends object
? {
[k in keyof T]: Jsonify<T[k]>;
}
: T;
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export type RequiredField<T, K extends keyof T> = T & Required<Pick<T, K>>;

View File

@ -73,19 +73,14 @@ const QueueListView = React.memo(() => {
const onPressItem = (item = {} as IOmnichannelRoom) => {
logEvent(events.QL_GO_ROOM);
if (isMasterDetail) {
navigation.navigate('DrawerNavigator');
} else {
navigation.navigate('RoomsListView');
}
goRoom({
item: {
...item,
// we're calling v as visitor on our mergeSubscriptionsRooms
visitor: item.v
},
isMasterDetail
isMasterDetail,
popToRoot: true
});
};

View File

@ -862,6 +862,16 @@
"Select_Members": "Select Members",
"Also_send_thread_message_to_channel_behavior": "Also send thread message to channel",
"Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Allow users to select the Also send to channel behavior",
"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",
"Joined": "Joined",
"Calling": "Calling...",
"Start_a_call": "Start a call",
"Call": "Call",
"Reply_in_direct_message": "Reply in Direct Message",
"room_archived": "archived room",
"room_unarchived": "unarchived room"

View File

@ -784,6 +784,16 @@
"Select_Members": "Selecionar Membros",
"Also_send_thread_message_to_channel_behavior": "Também enviar mensagem do tópico para o canal",
"Accounts_Default_User_Preferences_alsoSendThreadToChannel_Description": "Permitir que os usuários selecionem o comportamento Também enviar para o canal",
"Waiting_for_answer": "Esperando por resposta",
"Call_ended": "Chamada finalizada",
"Call_was_not_answered": "A chamada não foi atendida",
"Call_back": "Ligue de volta",
"Call_again": "Ligue novamente",
"Call_ongoing": "Chamada em andamento",
"Joined": "Ingressou",
"Calling": "Chamando...",
"Start_a_call": "Inicie uma chamada",
"Call": "Ligar",
"Reply_in_direct_message": "Responder por mensagem direta",
"room_archived": "{{username}} arquivou a sala",
"room_unarchived": "{{username}} desarquivou a sala"

View File

@ -70,6 +70,22 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
conferenceCallBorder: '#F2F3F5',
conferenceCallBackground: '#F7F8FA',
conferenceCallOngoingPhoneBackground: '#C0F6E4',
conferenceCallIncomingPhoneBackground: '#D1EBFE',
conferenceCallEndedPhoneBackground: '#E4E7EA',
conferenceCallOngoingPhoneIcon: '#158D65',
conferenceCallIncomingPhoneIcon: '#095AD2',
conferenceCallEndedPhoneIcon: '#6C727A',
conferenceCallPlusUsersButton: '#E4E7EA',
conferenceCallPlusUsersText: '#6C727A',
conferenceCallCallBackButton: '#EEEFF1',
conferenceCallCallBackText: '#1F2329',
conferenceCallDisabledIcon: '#6C727A',
conferenceCallEnabledIcon: '#FFFFFF',
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
textInputSecondaryBackground: '#E4E7EA',
...mentions
},
@ -123,6 +139,22 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
conferenceCallBorder: '#1F2329',
conferenceCallBackground: '#1F2329',
conferenceCallOngoingPhoneBackground: '#106D4F',
conferenceCallIncomingPhoneBackground: '#D1EBFE',
conferenceCallEndedPhoneBackground: '#6C727A',
conferenceCallOngoingPhoneIcon: '#F7F8FA',
conferenceCallIncomingPhoneIcon: '#095AD2',
conferenceCallEndedPhoneIcon: '#F7F8FA',
conferenceCallPlusUsersButton: '#2F343D',
conferenceCallPlusUsersText: '#9EA2A8',
conferenceCallCallBackButton: '#E4E7EA',
conferenceCallCallBackText: '#FFFFFF',
conferenceCallDisabledIcon: '#6C727A',
conferenceCallEnabledIcon: '#FFFFFF',
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
textInputSecondaryBackground: '#030b1b', // backgroundColor
...mentions
},
@ -176,6 +208,22 @@ export const colors = {
collapsibleQuoteBorder: '#CBCED1',
collapsibleChevron: '#6C727A',
cancelButton: '#E4E7EA',
conferenceCallBorder: '#1F2329',
conferenceCallBackground: '#1F2329',
conferenceCallOngoingPhoneBackground: '#106D4F',
conferenceCallIncomingPhoneBackground: '#D1EBFE',
conferenceCallEndedPhoneBackground: '#6C727A',
conferenceCallOngoingPhoneIcon: '#F7F8FA',
conferenceCallIncomingPhoneIcon: '#095AD2',
conferenceCallEndedPhoneIcon: '#F7F8FA',
conferenceCallPlusUsersButton: '#2F343D',
conferenceCallPlusUsersText: '#9EA2A8',
conferenceCallCallBackButton: '#E4E7EA',
conferenceCallCallBackText: '#FFFFFF',
conferenceCallDisabledIcon: '#6C727A',
conferenceCallEnabledIcon: '#FFFFFF',
conferenceCallEnabledIconBackground: '#156FF5',
conferenceCallPhotoBackground: '#E4E7EA',
textInputSecondaryBackground: '#000000', // backgroundColor
...mentions
}

14
app/lib/hooks/useSnaps.ts Normal file
View File

@ -0,0 +1,14 @@
import { useDimensions } from '@react-native-community/hooks';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// Not sure if it's worth adding this here in the context of the actionSheet
/**
* Return the snaps based on the size you pass (aka: Size of action sheet)
* @param {Number[]} snaps Sizes you want to pass, pass only one if you want the action sheet to start at a specific size
*/
export const useSnaps = (snaps: number[]): string[] => {
const insets = useSafeAreaInsets();
const { screen } = useDimensions();
const percentage = insets.bottom + insets.top > 75 ? 110 : 100;
return snaps.map(snap => `${((percentage * snap) / (screen.height * screen.scale)).toFixed(2)}%`);
};

View File

@ -0,0 +1,27 @@
import { useCallback } from 'react';
import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet';
import i18n from '../../i18n';
import { videoConfJoin } from '../methods/videoConf';
export const useVideoConf = (): { joinCall: (blockId: string) => void } => {
const { showActionSheet } = useActionSheet();
const joinCall = useCallback(blockId => {
const options: TActionSheetOptionsItem[] = [
{
title: i18n.t('Video_call'),
icon: 'camera',
onPress: () => videoConfJoin(blockId, true)
},
{
title: i18n.t('Voice_call'),
icon: 'microphone',
onPress: () => videoConfJoin(blockId, false)
}
];
showActionSheet({ options });
}, []);
return { joinCall };
};

View File

@ -1,4 +1,5 @@
import { ChatsStackParamList } from '../../../stacks/types';
import { CommonActions } from '@react-navigation/native';
import Navigation from '../../navigation/appNavigation';
import { IOmnichannelRoom, SubscriptionType, IVisitor, TSubscriptionModel, ISubscription } from '../../../definitions';
import { getRoomTitle, getUidDirectMessage } from './helpers';
@ -19,19 +20,14 @@ export type TGoRoomItem = IGoRoomItem | TSubscriptionModel | ISubscription | IOm
const navigate = ({
item,
isMasterDetail,
popToRoot,
...props
}: {
item: TGoRoomItem;
isMasterDetail: boolean;
navigationMethod?: () => ChatsStackParamList;
popToRoot: boolean;
}) => {
let navigationMethod = props.navigationMethod ?? Navigation.navigate;
if (isMasterDetail) {
navigationMethod = Navigation.replace;
}
navigationMethod('RoomView', {
const routeParams = {
rid: item.rid,
name: getRoomTitle(item),
t: item.t,
@ -40,6 +36,44 @@ const navigate = ({
visitor: item.visitor,
roomUserId: getUidDirectMessage(item),
...props
};
if (isMasterDetail) {
if (popToRoot) {
Navigation.navigate('DrawerNavigator');
}
return Navigation.dispatch((state: any) => {
const routesRoomView = state.routes.filter((r: any) => r.name !== 'RoomView');
return CommonActions.reset({
...state,
routes: [
...routesRoomView,
{
name: 'RoomView',
params: routeParams
}
],
index: routesRoomView.length
});
});
}
if (popToRoot) {
Navigation.navigate('RoomsListView');
}
return Navigation.dispatch((state: any) => {
const routesRoomsListView = state.routes.filter((r: any) => r.name === 'RoomsListView');
return CommonActions.reset({
...state,
routes: [
...routesRoomsListView,
{
name: 'RoomView',
params: routeParams
}
],
index: routesRoomsListView.length
});
});
};
@ -51,13 +85,14 @@ interface IOmnichannelRoomVisitor extends IOmnichannelRoom {
export const goRoom = async ({
item,
isMasterDetail = false,
popToRoot = false,
...props
}: {
item: TGoRoomItem;
isMasterDetail: boolean;
navigationMethod?: any;
jumpToMessageId?: string;
usedCannedResponse?: string;
popToRoot?: boolean;
}): Promise<void> => {
if (!('id' in item) && item.t === SubscriptionType.DIRECT && item?.search) {
// if user is using the search we need first to join/create room
@ -72,6 +107,7 @@ export const goRoom = async ({
t: SubscriptionType.DIRECT
},
isMasterDetail,
popToRoot,
...props
});
}
@ -80,5 +116,5 @@ export const goRoom = async ({
}
}
return navigate({ item, isMasterDetail, ...props });
return navigate({ item, isMasterDetail, popToRoot, ...props });
};

View File

@ -125,8 +125,8 @@ export default class RoomSubscription {
if (ev === 'typing') {
const { user } = reduxStore.getState().login;
const { UI_Use_Real_Name } = reduxStore.getState().settings;
const { rooms } = reduxStore.getState().room;
if (rooms[0] !== _rid) {
const { subscribedRoom } = reduxStore.getState().room;
if (subscribedRoom !== _rid) {
return;
}
const [name, typing] = ddpMessage.fields.args;

View File

@ -197,8 +197,8 @@ const createOrUpdateSubscription = async (subscription: ISubscription, room: ISe
}
}
const { rooms } = store.getState().room;
if (tmp.lastMessage && !rooms.includes(tmp.rid)) {
const { subscribedRoom } = store.getState().room;
if (tmp.lastMessage && subscribedRoom !== tmp.rid) {
const lastMessage = buildMessage(tmp.lastMessage);
const messagesCollection = db.get('messages');
let messageRecord = {} as TMessageModel | null;

View File

@ -21,11 +21,16 @@ function popToTop() {
navigationRef.current?.dispatch(StackActions.popToTop());
}
function dispatch(params: any) {
navigationRef.current?.dispatch(params);
}
export default {
navigationRef,
routeNameRef,
navigate,
back,
replace,
popToTop
popToTop,
dispatch
};

View File

@ -12,13 +12,13 @@ describe('test room reducer', () => {
it('should return modified store after subscribeRoom', () => {
mockedStore.dispatch(subscribeRoom('GENERAL'));
const state = mockedStore.getState().room;
expect(state.rooms).toEqual(['GENERAL']);
expect(state.subscribedRoom).toEqual('GENERAL');
});
it('should return empty store after remove unsubscribeRoom', () => {
mockedStore.dispatch(unsubscribeRoom('GENERAL'));
const state = mockedStore.getState().room;
expect(state.rooms).toEqual([]);
expect(state.subscribedRoom).toEqual('');
});
it('should return initial state after leaveRoom', () => {

View File

@ -6,13 +6,13 @@ export type IRoomRecord = string[];
export interface IRoom {
rid: string;
isDeleting: boolean;
rooms: IRoomRecord;
subscribedRoom: string;
}
export const initialState: IRoom = {
rid: '',
isDeleting: false,
rooms: []
subscribedRoom: ''
};
export default function (state = initialState, action: TActionsRoom): IRoom {
@ -20,12 +20,12 @@ export default function (state = initialState, action: TActionsRoom): IRoom {
case ROOM.SUBSCRIBE:
return {
...state,
rooms: [action.rid, ...state.rooms]
subscribedRoom: action.rid
};
case ROOM.UNSUBSCRIBE:
return {
...state,
rooms: state.rooms.filter(rid => rid !== action.rid)
subscribedRoom: state.subscribedRoom === action.rid ? '' : state.subscribedRoom
};
case ROOM.LEAVE:
return {

View File

@ -4,7 +4,6 @@ import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { CREATE_CHANNEL, LOGIN } from '../actions/actionsTypes';
import { createChannelFailure, createChannelSuccess } from '../actions/createChannel';
import { showErrorAlert } from '../lib/methods/helpers/info';
import Navigation from '../lib/navigation/appNavigation';
import database from '../lib/database';
import I18n from '../i18n';
import { events, logEvent } from '../lib/methods/helpers/log';
@ -78,10 +77,7 @@ const handleRequest = function* handleRequest({ data }) {
const handleSuccess = function* handleSuccess({ data }) {
const isMasterDetail = yield select(state => state.app.isMasterDetail);
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
}
goRoom({ item: data, isMasterDetail });
goRoom({ item: data, isMasterDetail, popToRoot: true });
};
const handleFailure = function handleFailure({ err, isTeam }) {

View File

@ -1,7 +1,6 @@
import { all, delay, put, select, take, takeLatest } from 'redux-saga/effects';
import UserPreferences from '../lib/methods/userPreferences';
import Navigation from '../lib/navigation/appNavigation';
import * as types from '../actions/actionsTypes';
import { selectServerRequest, serverInitAdd } from '../actions/server';
import { inviteLinksRequest, inviteLinksSetToken } from '../actions/inviteLinks';
@ -36,14 +35,6 @@ const handleInviteLink = function* handleInviteLink({ params, requireLogin = fal
}
};
const popToRoot = function popToRoot({ isMasterDetail }) {
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
};
const navigate = function* navigate({ params }) {
yield put(appStart({ root: RootEnum.ROOT_INSIDE }));
if (params.path || params.rid) {
@ -65,27 +56,9 @@ const navigate = function* navigate({ params }) {
};
const isMasterDetail = yield select(state => state.app.isMasterDetail);
const focusedRooms = yield select(state => state.room.rooms);
const jumpToMessageId = params.messageId;
if (focusedRooms.includes(room.rid)) {
// if there's one room on the list or last room is the one
if (focusedRooms.length === 1 || focusedRooms[0] === room.rid) {
if (jumpToThreadId) {
// With this conditional when there is a jumpToThreadId we can avoid the thread open again
// above other thread and the room could call again the thread
popToRoot({ isMasterDetail });
}
yield goRoom({ item, isMasterDetail, jumpToMessageId, jumpToThreadId });
} else {
popToRoot({ isMasterDetail });
yield goRoom({ item, isMasterDetail, jumpToMessageId, jumpToThreadId });
}
} else {
popToRoot({ isMasterDetail });
yield goRoom({ item, isMasterDetail, jumpToMessageId, jumpToThreadId });
}
yield goRoom({ item, isMasterDetail, jumpToMessageId, jumpToThreadId, popToRoot: true });
if (params.isCall) {
callJitsi(item);
}

View File

@ -1,7 +1,6 @@
import { select, takeLatest } from 'redux-saga/effects';
import { Q } from '@nozbe/watermelondb';
import Navigation from '../lib/navigation/appNavigation';
import { MESSAGES } from '../actions/actionsTypes';
import database from '../lib/database';
import log from '../lib/methods/helpers/log';
@ -16,18 +15,13 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
const subscriptions = yield subsCollection.query(Q.where('name', username)).fetch();
const isMasterDetail = yield select(state => state.app.isMasterDetail);
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
if (subscriptions.length) {
goRoom({ item: subscriptions[0], isMasterDetail, message });
goRoom({ item: subscriptions[0], isMasterDetail, popToRoot: true, message });
} else {
const result = yield Services.createDirectMessage(username);
if (result?.success) {
goRoom({ item: result?.room, isMasterDetail, message });
goRoom({ item: result?.room, isMasterDetail, popToRoot: true, message });
}
}
} catch (e) {

View File

@ -58,13 +58,13 @@ const handleRoomsRequest = function* handleRoomsRequest({ params }) {
const subsToCreate = subscriptions.filter(i1 => !existingSubs.find(i2 => i1._id === i2._id));
const subsToDelete = existingSubs.filter(i1 => !subscriptions.find(i2 => i1._id === i2._id));
const openedRooms = yield select(state => state.room.rooms);
const subscribedRoom = yield select(state => state.room.subscribedRoom);
const lastMessages = subscriptions
/** Checks for opened rooms and filter them out.
* It prevents this process to try persisting the same last message on the room messages fetch.
* This race condition is easy to reproduce on push notification tap.
*/
.filter(sub => !openedRooms.includes(sub.rid))
.filter(sub => subscribedRoom !== sub.rid)
.map(sub => sub.lastMessage && buildMessage(sub.lastMessage))
.filter(lm => lm);
const lastMessagesIds = lastMessages.map(lm => lm._id).filter(lm => lm);

View File

@ -17,7 +17,6 @@ import { TSupportedThemes, withTheme } from '../theme';
import SafeAreaView from '../containers/SafeAreaView';
import { sendLoadingEvent } from '../containers/Loading';
import { animateNextTransition } from '../lib/methods/helpers/layoutAnimation';
import { goRoom } from '../lib/methods/helpers/goRoom';
import { showErrorAlert } from '../lib/methods/helpers/info';
import { ChatsStackParamList } from '../stacks/types';
import { TSubscriptionModel, SubscriptionType, IApplicationState } from '../definitions';
@ -126,7 +125,7 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
submit = async () => {
const { selected } = this.state;
const { isMasterDetail } = this.props;
const { navigation } = this.props;
sendLoadingEvent({ visible: true });
try {
@ -134,9 +133,8 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
const result = await Services.addRoomsToTeam({ rooms: selected, teamId: this.teamId });
if (result.success) {
sendLoadingEvent({ visible: false });
// @ts-ignore
// TODO: Verify goRoom interface for return of call
goRoom({ item: result, isMasterDetail });
// 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);

View File

@ -8,14 +8,12 @@ import SafeAreaView from '../containers/SafeAreaView';
import StatusBar from '../containers/StatusBar';
import Button from '../containers/Button';
import { TSupportedThemes, useTheme } from '../theme';
import Navigation from '../lib/navigation/appNavigation';
import { goRoom } from '../lib/methods/helpers/goRoom';
import { themes } from '../lib/constants';
import Markdown from '../containers/markdown';
import { ICannedResponse } from '../definitions/ICannedResponse';
import { ChatsStackParamList } from '../stacks/types';
import sharedStyles from './Styles';
import { getRoomTitle, getUidDirectMessage } from '../lib/methods/helpers';
import { useAppSelector } from '../lib/hooks';
const styles = StyleSheet.create({
@ -97,7 +95,6 @@ const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps)
const { cannedResponse } = route?.params;
const { theme } = useTheme();
const { isMasterDetail } = useAppSelector(state => state.app);
const { rooms } = useAppSelector(state => state.room);
useEffect(() => {
navigation.setOptions({
@ -107,31 +104,9 @@ const CannedResponseDetail = ({ navigation, route }: ICannedResponseDetailProps)
const navigateToRoom = (item: ICannedResponse) => {
const { room } = route.params;
const { name } = room;
const params = {
rid: room.rid,
name: getRoomTitle({
t: room.t,
fname: name
}),
t: room.t,
roomUserId: getUidDirectMessage(room),
usedCannedResponse: item.text
};
if (room.rid) {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
if (rooms.includes(room.rid)) {
({ navigate } = navigation);
}
navigate('RoomView', params);
}
goRoom({ item: room, isMasterDetail, popToRoot: true, usedCannedResponse: item.text });
}
};

View File

@ -12,7 +12,6 @@ import ActivityIndicator from '../../containers/ActivityIndicator';
import SearchHeader from '../../containers/SearchHeader';
import BackgroundContainer from '../../containers/BackgroundContainer';
import { useTheme } from '../../theme';
import Navigation from '../../lib/navigation/appNavigation';
import { goRoom } from '../../lib/methods/helpers/goRoom';
import * as HeaderButton from '../../containers/HeaderButton';
import * as List from '../../containers/List';
@ -24,7 +23,7 @@ import DropdownItemHeader from './Dropdown/DropdownItemHeader';
import styles from './styles';
import { ICannedResponse } from '../../definitions/ICannedResponse';
import { ChatsStackParamList } from '../../stacks/types';
import { getRoomTitle, getUidDirectMessage, useDebounce } from '../../lib/methods/helpers';
import { useDebounce } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
import { ILivechatDepartment } from '../../definitions/ILivechatDepartment';
import { useAppSelector } from '../../lib/hooks';
@ -73,7 +72,6 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView
const { theme } = useTheme();
const isMasterDetail = useAppSelector(state => state.app.isMasterDetail);
const rooms = useAppSelector(state => state.room.rooms);
const getRoomFromDb = async () => {
const { rid } = route.params;
@ -107,34 +105,8 @@ const CannedResponsesListView = ({ navigation, route }: ICannedResponsesListView
};
const navigateToRoom = (item: ICannedResponse) => {
if (!room) {
return;
}
const { name } = room;
const params = {
rid: room.rid,
name: getRoomTitle({
t: room.t,
fname: name
}),
t: room.t,
roomUserId: getUidDirectMessage(room),
usedCannedResponse: item.text
};
if (room.rid) {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
if (rooms.includes(room.rid)) {
({ navigate } = navigation);
}
navigate('RoomView', params);
}
if (room?.rid) {
goRoom({ item: room, isMasterDetail, popToRoot: true, usedCannedResponse: item.text });
}
};

View File

@ -12,7 +12,6 @@ import StatusBar from '../../containers/StatusBar';
import { withTheme } from '../../theme';
import { getUserSelector } from '../../selectors/login';
import { FormTextInput } from '../../containers/TextInput';
import Navigation from '../../lib/navigation/appNavigation';
import { createDiscussionRequest, ICreateDiscussionRequestData } from '../../actions/createDiscussion';
import SafeAreaView from '../../containers/SafeAreaView';
import { goRoom } from '../../lib/methods/helpers/goRoom';
@ -60,18 +59,13 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
showErrorAlert(msg);
} else {
const { rid, t, prid } = result;
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
} else {
Navigation.navigate('RoomsListView');
}
const item = {
rid,
name: getRoomTitle(result),
t,
prid
};
goRoom({ item, isMasterDetail });
goRoom({ item, isMasterDetail, popToRoot: true });
}
}
}

View File

@ -152,13 +152,8 @@ class DirectoryView extends React.Component<IDirectoryViewProps, IDirectoryViewS
};
goRoom = (item: TGoRoomItem) => {
const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) {
navigation.navigate('DrawerNavigator');
} else {
navigation.navigate('RoomsListView');
}
goRoom({ item, isMasterDetail });
const { isMasterDetail } = this.props;
goRoom({ item, isMasterDetail, popToRoot: true });
};
onPressItem = async (item: IServerRoom) => {

View File

@ -59,6 +59,10 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
const { route } = this.props;
const { userInfo } = this.state;
setTimeout(() => {
this.setState({ loading: false });
}, 1000);
if (isIOS) {
setTimeout(() => {
const onlyAudio = route.params?.onlyAudio ?? false;
@ -69,7 +73,7 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
}
}, 1000);
}
BackHandler.addEventListener('hardwareBackPress', this.endCall);
BackHandler.addEventListener('hardwareBackPress', () => null);
}
componentWillUnmount() {
@ -79,7 +83,7 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
this.jitsiTimeout = null;
BackgroundTimer.stopBackgroundTimer();
}
BackHandler.removeEventListener('hardwareBackPress', this.endCall);
BackHandler.removeEventListener('hardwareBackPress', () => null);
if (isIOS) {
JitsiMeet.endCall();
}
@ -98,6 +102,7 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
// call is not ended and is available to web users.
onConferenceJoined = () => {
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
this.setState({ loading: false });
if (this.rid && !this.videoConf) {
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
if (this.jitsiTimeout) {
@ -114,7 +119,11 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
onConferenceTerminated = () => {
logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
const { navigation } = this.props;
navigation.pop();
// fix to go back when the call ends
setTimeout(() => {
JitsiMeet.endCall();
navigation.pop();
}, 200);
};
render() {

View File

@ -74,10 +74,7 @@ const NewMessageView = () => {
const goRoom = useCallback(
(item: TGoRoomItem) => {
logEvent(events.NEW_MSG_CHAT_WITH_USER);
if (isMasterDetail) {
navigation.pop();
}
navigation.pop();
goRoomMethod({ item, isMasterDetail });
},
[isMasterDetail, navigation]

View File

@ -87,7 +87,7 @@ interface IRoomInfoViewProps {
StackNavigationProp<MasterDetailInsideStackParamList>
>;
route: RouteProp<ChatsStackParamList, 'RoomInfoView'>;
rooms: string[];
subscribedRoom: string;
theme: TSupportedThemes;
isMasterDetail: boolean;
jitsiEnabled: boolean;
@ -353,7 +353,7 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
goRoom = () => {
logEvent(events.RI_GO_ROOM_USER);
const { room } = this.state;
const { rooms, navigation, isMasterDetail } = this.props;
const { navigation, isMasterDetail, subscribedRoom } = this.props;
const params = {
rid: room.rid,
name: getRoomTitle(room),
@ -362,18 +362,14 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
};
if (room.rid) {
// if it's on master detail layout, we close the modal and replace RoomView
if (isMasterDetail) {
Navigation.navigate('DrawerNavigator');
goRoom({ item: params, isMasterDetail });
} else {
let navigate = navigation.push;
// if this is a room focused
if (rooms.includes(room.rid)) {
({ navigate } = navigation);
if (room.rid === subscribedRoom) {
if (isMasterDetail) {
return Navigation.navigate('DrawerNavigator');
}
navigate('RoomView', params);
return navigation.goBack();
}
// if it's on master detail layout, we close the modal and replace RoomView
goRoom({ item: params, isMasterDetail, popToRoot: true });
}
};
@ -513,7 +509,7 @@ class RoomInfoView extends React.Component<IRoomInfoViewProps, IRoomInfoViewStat
}
const mapStateToProps = (state: IApplicationState) => ({
rooms: state.room.rooms,
subscribedRoom: state.room.subscribedRoom,
isMasterDetail: state.app.isMasterDetail,
jitsiEnabled: (state.settings.Jitsi_Enabled as boolean) || false,
editRoomPermission: state.permissions['edit-room'],

View File

@ -15,12 +15,7 @@ import { RoomTypes } from '../../lib/methods';
export type TRoomType = SubscriptionType.CHANNEL | SubscriptionType.GROUP | SubscriptionType.OMNICHANNEL;
const handleGoRoom = (item: TGoRoomItem, isMasterDetail: boolean): void => {
if (isMasterDetail) {
appNavigation.navigate('DrawerNavigator');
} else {
appNavigation.popToTop();
}
goRoom({ item, isMasterDetail });
goRoom({ item, isMasterDetail, popToRoot: true });
};
export const fetchRole = (role: string, selectedUser: TUserModel, roomRoles: any): boolean => {

View File

@ -43,7 +43,6 @@ import SafeAreaView from '../../containers/SafeAreaView';
import { withDimensions } from '../../dimensions';
import { takeInquiry, takeResume } from '../../ee/omnichannel/lib';
import { sendLoadingEvent } from '../../containers/Loading';
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom';
import getThreadName from '../../lib/methods/getThreadName';
import getRoomInfo from '../../lib/methods/getRoomInfo';
import { ContainerTypes } from '../../containers/UIKit/interfaces';
@ -101,6 +100,7 @@ import {
} from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet';
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom';
type TStateAttrsUpdate = keyof IRoomViewState;
@ -911,15 +911,15 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
onDiscussionPress = debounce(
async (item: TAnyMessageModel) => {
const { navigation } = this.props;
const { isMasterDetail } = this.props;
if (!item.drid) return;
const sub = await getRoomInfo(item.drid);
navigation.push('RoomView', {
rid: item.drid as string,
prid: item?.subscription?.id,
name: item.msg,
t: (sub?.t as SubscriptionType) || (this.t as SubscriptionType)
});
if (sub) {
goRoom({
item: sub as TGoRoomItem,
isMasterDetail
});
}
},
1000,
true
@ -988,6 +988,11 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
} else {
this.navToThread(message);
}
} else if (!message.tmid && message.rid === this.rid && this.t === 'thread' && !message.replies) {
/**
* if the user is within a thread and the message that he is trying to jump to, is a message in the main room
*/
return this.navToRoom(message);
} else {
/**
* if it's from server, we don't have it saved locally and so we fetch surroundings
@ -1199,12 +1204,12 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
};
navToRoom = async (message: TAnyMessageModel) => {
const { navigation, isMasterDetail } = this.props;
const { isMasterDetail } = this.props;
const roomInfo = await getRoomInfo(message.rid);
return goRoom({
item: roomInfo as TGoRoomItem,
isMasterDetail,
navigationMethod: navigation.push,
jumpToMessageId: message.id
});
};

View File

@ -6,9 +6,11 @@ import Orientation from 'react-native-orientation-locker';
import { Q } from '@nozbe/watermelondb';
import { withSafeAreaInsets } from 'react-native-safe-area-context';
import { Subscription } from 'rxjs';
import { StackNavigationOptions } from '@react-navigation/stack';
import { StackNavigationOptions, StackNavigationProp } from '@react-navigation/stack';
import { Header } from '@react-navigation/elements';
import { FlashList } from '@shopify/flash-list';
import { CompositeNavigationProp, RouteProp } from '@react-navigation/native';
import { Dispatch } from 'redux';
import database from '../../lib/database';
import RoomItem, { ROW_HEIGHT, ROW_HEIGHT_CONDENSED } from '../../containers/RoomItem';
@ -21,7 +23,7 @@ import StatusBar from '../../containers/StatusBar';
import ActivityIndicator from '../../containers/ActivityIndicator';
import { serverInitAdd } from '../../actions/server';
import { animateNextTransition } from '../../lib/methods/helpers/layoutAnimation';
import { withTheme } from '../../theme';
import { TSupportedThemes, withTheme } from '../../theme';
import EventEmitter from '../../lib/methods/helpers/events';
import { themedHeader } from '../../lib/methods/helpers/navigation';
import {
@ -40,20 +42,12 @@ import { goRoom } from '../../lib/methods/helpers/goRoom';
import SafeAreaView from '../../containers/SafeAreaView';
import { withDimensions } from '../../dimensions';
import { getInquiryQueueSelector } from '../../ee/omnichannel/selectors/inquiry';
import {
IApplicationState,
IBaseScreen,
ISubscription,
IUser,
RootEnum,
SubscriptionType,
TSubscriptionModel
} from '../../definitions';
import { IApplicationState, ISubscription, IUser, RootEnum, SubscriptionType, TSubscriptionModel } from '../../definitions';
import styles from './styles';
import ServerDropdown from './ServerDropdown';
import ListHeader, { TEncryptionBanner } from './ListHeader';
import RoomsListHeaderView from './Header';
import { ChatsStackParamList } from '../../stacks/types';
import { ChatsStackParamList, DrawerParamList } from '../../stacks/types';
import { RoomTypes, search } from '../../lib/methods';
import {
getRoomAvatar,
@ -67,7 +61,16 @@ import {
import { E2E_BANNER_TYPE, DisplayMode, SortBy, MAX_SIDEBAR_WIDTH, themes } from '../../lib/constants';
import { Services } from '../../lib/services';
interface IRoomsListViewProps extends IBaseScreen<ChatsStackParamList, 'RoomsListView'> {
type TNavigation = CompositeNavigationProp<
StackNavigationProp<ChatsStackParamList, 'RoomsListView'>,
CompositeNavigationProp<StackNavigationProp<ChatsStackParamList>, StackNavigationProp<DrawerParamList>>
>;
interface IRoomsListViewProps {
navigation: TNavigation;
route: RouteProp<ChatsStackParamList, 'RoomsListView'>;
theme: TSupportedThemes;
dispatch: Dispatch;
[key: string]: IUser | string | boolean | ISubscription[] | number | object | TEncryptionBanner;
user: IUser;
server: string;
@ -83,7 +86,7 @@ interface IRoomsListViewProps extends IBaseScreen<ChatsStackParamList, 'RoomsLis
StoreLastMessage: boolean;
useRealName: boolean;
isMasterDetail: boolean;
rooms: string[];
subscribedRoom: string;
width: number;
insets: {
left: number;
@ -220,7 +223,7 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
groupByType,
showFavorites,
showUnread,
rooms,
subscribedRoom,
isMasterDetail,
insets,
createTeamPermission,
@ -245,9 +248,9 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
) {
this.getSubscriptions();
}
// Update current item in case of another action triggers an update on rooms reducer
if (isMasterDetail && rooms[0] && item?.rid !== rooms[0] && !dequal(rooms, prevProps.rooms)) {
this.setState({ item: { rid: rooms[0] } as ISubscription });
// Update current item in case of another action triggers an update on room subscribed reducer
if (isMasterDetail && item?.rid !== subscribedRoom && subscribedRoom !== prevProps.subscribedRoom) {
this.setState({ item: { rid: subscribedRoom } as ISubscription });
}
if (insets.left !== prevProps.insets.left || insets.right !== prevProps.insets.right) {
this.setHeader();
@ -645,9 +648,9 @@ class RoomsListView extends React.Component<IRoomsListViewProps, IRoomsListViewS
goRoom = ({ item, isMasterDetail }: { item: ISubscription; isMasterDetail: boolean }) => {
logEvent(events.RL_GO_ROOM);
const { item: currentItem } = this.state;
const { rooms } = this.props;
// @ts-ignore
if (currentItem?.rid === item.rid || rooms?.includes(item.rid)) {
const { subscribedRoom } = this.props;
if (currentItem?.rid === item.rid || subscribedRoom === item.rid) {
return;
}
// Only mark room as focused when in master detail layout
@ -918,7 +921,7 @@ const mapStateToProps = (state: IApplicationState) => ({
showUnread: state.sortPreferences.showUnread,
useRealName: state.settings.UI_Use_Real_Name,
StoreLastMessage: state.settings.Store_Last_Message,
rooms: state.room.rooms,
subscribedRoom: state.room.subscribedRoom,
queueSize: getInquiryQueueSelector(state).length,
inquiryEnabled: state.inquiry.enabled,
encryptionBanner: state.encryption.banner,

View File

@ -322,7 +322,7 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
onPressItem = debounce(
async (item: IItem) => {
logEvent(events.TC_GO_ROOM);
const { navigation, isMasterDetail } = this.props;
const { isMasterDetail } = this.props;
try {
let params = {};
const result = await Services.getRoomInfo(item._id);
@ -335,10 +335,7 @@ class TeamChannelsView extends React.Component<ITeamChannelsViewProps, ITeamChan
teamId: result.room.teamId
};
}
if (isMasterDetail) {
navigation.pop();
}
goRoom({ item: params, isMasterDetail, navigationMethod: navigation.push });
goRoom({ item: params, isMasterDetail, popToRoot: !!isMasterDetail });
} catch (e: any) {
if (e.data.error === 'not-allowed') {
showErrorAlert(I18n.t('error-not-allowed'));

View File

@ -0,0 +1,71 @@
import { expect } from 'detox';
import data from '../../data';
import { navigateToLogin, login, sleep, tapBack } from '../../helpers/app';
import { sendMessage, post } from '../../helpers/data_setup';
describe('InApp Notification', () => {
let dmCreatedRid: string;
before(async () => {
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
await navigateToLogin();
await login(data.users.regular.username, data.users.regular.password);
const result = await post(`im.create`, { username: data.users.alternate.username });
dmCreatedRid = result.data.room.rid;
});
describe('receive in RoomsListView', () => {
const text = 'Message in DM';
it('should have rooms list screen', async () => {
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
it('should send direct message from user alternate to user regular', async () => {
await sleep(1000);
await sendMessage(data.users.alternate, dmCreatedRid, text);
});
it('should tap on InApp Notification', async () => {
await waitFor(element(by.id(`in-app-notification-${text}`)))
.toExist()
.withTimeout(2000);
await sleep(500);
await element(by.id(`in-app-notification-${text}`)).tap();
await sleep(500);
await expect(element(by.id('room-header'))).toExist();
await expect(element(by.id(`room-view-title-${data.users.alternate.username}`))).toExist();
});
});
describe('receive in another room', () => {
const text = 'Another msg';
it('should back to RoomsListView and open the channel Detox Public', async () => {
await tapBack();
await sleep(500);
await element(by.id(`rooms-list-view-item-${data.userRegularChannels.detoxpublic.name}`)).tap();
await waitFor(element(by.id('room-view')))
.toBeVisible()
.withTimeout(5000);
await expect(element(by.id(`room-view-title-${data.userRegularChannels.detoxpublic.name}`))).toExist();
});
it('should receive and tap InAppNotification in another room', async () => {
await sendMessage(data.users.alternate, dmCreatedRid, text);
await waitFor(element(by.id(`in-app-notification-${text}`)))
.toExist()
.withTimeout(2000);
await sleep(500);
await element(by.id(`in-app-notification-${text}`)).tap();
await sleep(500);
await expect(element(by.id('room-header'))).toExist();
await expect(element(by.id(`room-view-title-${data.users.alternate.username}`))).toExist();
});
it('should back to RoomsListView', async () => {
await tapBack();
await sleep(500);
await expect(element(by.id('rooms-list-view'))).toBeVisible();
});
});
});

View File

@ -200,7 +200,7 @@ describe('Room', () => {
const expectThreadMessages = async (message: string) => {
await waitFor(element(by.id('room-view-title-thread 1')))
.toExist()
.withTimeout(5000);
.withTimeout(10000);
await waitFor(element(by[textMatcher](message)).atIndex(0))
.toExist()
.withTimeout(10000);

View File

@ -199,6 +199,14 @@ describe('Team', () => {
});
it('should add existing channel to team', async () => {
await navigateToRoom(team);
await waitFor(element(by.id('room-view-header-team-channels')))
.toExist()
.withTimeout(5000);
await element(by.id('room-view-header-team-channels')).tap();
await waitFor(element(by.id('team-channels-view')))
.toExist()
.withTimeout(5000);
await element(by.id('team-channels-view-create')).tap();
await waitFor(element(by.id('add-channel-team-view')))
.toExist()

View File

@ -2,6 +2,8 @@ PODS:
- boost (1.76.0)
- BugsnagReactNative (7.10.5):
- React-Core
- BVLinearGradient (2.6.2):
- React-Core
- DoubleConversion (1.1.6)
- EXAppleAuthentication (4.2.1):
- ExpoModulesCore
@ -493,8 +495,8 @@ PODS:
- React-Core
- RNCClipboard (1.8.5):
- React-Core
- RNCMaskedView (0.1.11):
- React
- RNCMaskedView (0.2.8):
- React-Core
- RNConfigReader (1.0.0):
- React
- RNCPicker (1.8.1):
@ -589,6 +591,7 @@ PODS:
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- "BugsnagReactNative (from `../node_modules/@bugsnag/react-native`)"
- BVLinearGradient (from `../node_modules/react-native-linear-gradient`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- EXAppleAuthentication (from `../node_modules/expo-apple-authentication/ios`)
- EXAV (from `../node_modules/expo-av/ios`)
@ -657,7 +660,7 @@ DEPENDENCIES:
- RNBootSplash (from `../node_modules/react-native-bootsplash`)
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
- "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)"
- "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)"
- "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)"
- RNConfigReader (from `../node_modules/react-native-config-reader`)
- "RNCPicker (from `../node_modules/@react-native-community/picker`)"
- "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)"
@ -711,6 +714,8 @@ EXTERNAL SOURCES:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
BugsnagReactNative:
:path: "../node_modules/@bugsnag/react-native"
BVLinearGradient:
:path: "../node_modules/react-native-linear-gradient"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
EXAppleAuthentication:
@ -840,7 +845,7 @@ EXTERNAL SOURCES:
RNCClipboard:
:path: "../node_modules/@react-native-clipboard/clipboard"
RNCMaskedView:
:path: "../node_modules/@react-native-community/masked-view"
:path: "../node_modules/@react-native-masked-view/masked-view"
RNConfigReader:
:path: "../node_modules/react-native-config-reader"
RNCPicker:
@ -894,6 +899,7 @@ CHECKOUT OPTIONS:
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
BugsnagReactNative: a97b3132c1854fd7bf92350fabd505e3ebdd7829
BVLinearGradient: 34a999fda29036898a09c6a6b728b0b4189e1a44
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
EXAppleAuthentication: 709a807fe7f48ac6986a2ceed206ee6a8baf28df
EXAV: 88f61c5af8415715b7ee51f084c1020235b85c56
@ -977,7 +983,7 @@ SPEC CHECKSUMS:
RNBootSplash: 7e91ea56c7010aae487489789dbe212e8c905a0c
RNCAsyncStorage: 8616bd5a58af409453ea4e1b246521bb76578d60
RNCClipboard: cc054ad1e8a33d2a74cd13e565588b4ca928d8fd
RNCMaskedView: 0e1bc4bfa8365eba5fbbb71e07fbdc0555249489
RNCMaskedView: bc0170f389056201c82a55e242e5d90070e18e5a
RNConfigReader: 396da6a6444182a76e8ae0930b9436c7575045cb
RNCPicker: 914b557e20b3b8317b084aca9ff4b4edb95f61e4
RNDateTimePicker: 7658208086d86d09e1627b5c34ba0cf237c60140

Binary file not shown.

View File

@ -40,7 +40,6 @@
"@react-native-community/cameraroll": "4.1.2",
"@react-native-community/datetimepicker": "3.5.2",
"@react-native-community/hooks": "2.6.0",
"@react-native-community/masked-view": "0.1.11",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/picker": "^1.8.1",
"@react-native-community/slider": "4.2.2",
@ -48,13 +47,14 @@
"@react-native-firebase/analytics": "^14.11.0",
"@react-native-firebase/app": "^14.11.0",
"@react-native-firebase/crashlytics": "^14.11.0",
"@react-native-masked-view/masked-view": "^0.2.8",
"@react-navigation/drawer": "6.4.1",
"@react-navigation/elements": "^1.3.6",
"@react-navigation/native": "6.0.10",
"@react-navigation/stack": "6.2.1",
"@rocket.chat/message-parser": "^0.31.14",
"@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile",
"@rocket.chat/ui-kit": "^0.31.13",
"@rocket.chat/ui-kit": "^0.31.19",
"@shopify/flash-list": "^1.2.2",
"bytebuffer": "^5.0.1",
"color2k": "1.2.4",
@ -99,6 +99,7 @@
"react-native-image-progress": "^1.1.1",
"react-native-jitsi-meet": "RocketChat/react-native-jitsi-meet",
"react-native-keycommands": "2.0.3",
"react-native-linear-gradient": "^2.6.2",
"react-native-localize": "2.1.1",
"react-native-math-view": "^3.9.5",
"react-native-mime-types": "2.3.0",
@ -119,6 +120,7 @@
"react-native-screens": "3.13.1",
"react-native-scrollable-tab-view": "ptomasroos/react-native-scrollable-tab-view",
"react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.1",
"react-native-skeleton-placeholder": "^5.2.3",
"react-native-slowlog": "^1.0.2",
"react-native-svg": "^12.3.0",
"react-native-ui-lib": "RocketChat/react-native-ui-lib",

View File

@ -4969,11 +4969,6 @@
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.6.0.tgz#dd5f19601eb3684c6bcdd3df3d0c04cf44c24cff"
integrity sha512-emBGKvhJ0h++lLJQ5ejsj+od9G67nEaihjvfSx7/JWvNrQGAhP9U0OZqgb9dkKzor9Ufaj9SGt8RNY97cGzttw==
"@react-native-community/masked-view@0.1.11":
version "0.1.11"
resolved "https://registry.yarnpkg.com/@react-native-community/masked-view/-/masked-view-0.1.11.tgz#2f4c6e10bee0786abff4604e39a37ded6f3980ce"
integrity sha512-rQfMIGSR/1r/SyN87+VD8xHHzDYeHaJq6elOSCAD+0iLagXkSI2pfA0LmSXP21uw5i3em7GkkRjfJ8wpqWXZNw==
"@react-native-community/netinfo@6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@react-native-community/netinfo/-/netinfo-6.0.0.tgz#2a4d7190b508dd0c2293656c9c1aa068f6f60a71"
@ -5023,6 +5018,11 @@
"@expo/config-plugins" "^4.1.5"
stacktrace-js "^2.0.0"
"@react-native-masked-view/masked-view@^0.2.8":
version "0.2.8"
resolved "https://registry.yarnpkg.com/@react-native-masked-view/masked-view/-/masked-view-0.2.8.tgz#34405a4361882dae7c81b1b771fe9f5fbd545a97"
integrity sha512-+1holBPDF1yi/y0uc1WB6lA5tSNHhM7PpTMapT3ypvSnKQ9+C6sy/zfjxNxRA/llBQ1Ci6f94EaK56UCKs5lTA==
"@react-native-picker/picker@^1.8.3":
version "1.16.1"
resolved "https://registry.yarnpkg.com/@react-native-picker/picker/-/picker-1.16.1.tgz#cc5d05b0d651445afa519c67824d8af3e43fa10c"
@ -5165,10 +5165,10 @@
tiny-events "^1.0.1"
universal-websocket-client "^1.0.2"
"@rocket.chat/ui-kit@^0.31.13":
version "0.31.13"
resolved "https://registry.yarnpkg.com/@rocket.chat/ui-kit/-/ui-kit-0.31.13.tgz#bd1c8a8726a74e13d072bb6f67cb2a6d3ba66e3b"
integrity sha512-IWNIRca0fP8Ecka3DIvqZKF7PbjcUaS+LkWUbMa+9lkX6MeumZrpFqsf7MKUpT+ihJr0RD4lBybmEgFH2syDbw==
"@rocket.chat/ui-kit@^0.31.19":
version "0.31.19"
resolved "https://registry.yarnpkg.com/@rocket.chat/ui-kit/-/ui-kit-0.31.19.tgz#737103123bc7e635382217eef75965b7e0f44703"
integrity sha512-8zRKQ5CoC4hIuYHVheO0d7etX9oizmM18fu99r5s/deciL/0MRWocdb4H/QsmbsNrkKCO6Z6wr7f9zzJCNTRHg==
"@segment/loosely-validate-event@^2.0.0":
version "2.0.0"
@ -17200,6 +17200,11 @@ react-native-keycommands@2.0.3:
resolved "https://registry.yarnpkg.com/react-native-keycommands/-/react-native-keycommands-2.0.3.tgz#09b799c1f70832e5cd9fbdb712ddaa3ee683f70e"
integrity sha512-s03K8JvCnfLhBg10Y2aRH3Bp9Uw9QOEr0uzuIj9OkgjjTB8/b+T4K5LSCxGuIAD30IxsEZvGZKjP1DzEMxaRhQ==
react-native-linear-gradient@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/react-native-linear-gradient/-/react-native-linear-gradient-2.6.2.tgz#56598a76832724b2afa7889747635b5c80948f38"
integrity sha512-Z8Xxvupsex+9BBFoSYS87bilNPWcRfRsGC0cpJk72Nxb5p2nEkGSBv73xZbEHnW2mUFvP+huYxrVvjZkr/gRjQ==
react-native-localize@2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.1.1.tgz#da8f8776991d2b748708c408db05152602cefb38"
@ -17351,6 +17356,11 @@ react-native-simple-crypto@RocketChat/react-native-simple-crypto#0.5.1:
base64-js "^1.3.0"
hex-lite "^1.5.0"
react-native-skeleton-placeholder@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/react-native-skeleton-placeholder/-/react-native-skeleton-placeholder-5.2.3.tgz#2dddf1f84d43110b90c22f715b2dbbe2c54732e1"
integrity sha512-nikmTfex3oydnZ4prV62KxibMvcu2l2NegsHGtXhsWwFIX5QaKneBohP7etinUq/c2PkSr3ZlfqooDG2yIHRdg==
react-native-slider@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/react-native-slider/-/react-native-slider-0.11.0.tgz#b68a0bc43c8422b24cd57947cc5ac2bcdb58fadc"