improve: handle attachment actions in a quote and how to jump to message (#5363)

* improve: handle attachment actions in a quote

* actions with video

* actions with audio

* show alert when trying to jump to a message inside a not allowed room

* jump to message from long press

* disable the reply onPress when is a quote or forward

* update tests

* fix 02-broadcast e2e

* fix the e2e tests

* remove the await from handleResumeDownload and remove the esline-disable
This commit is contained in:
Reinaldo Neto 2023-11-30 12:03:03 -03:00 committed by GitHub
parent 2989b3c2ee
commit b217435ffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 352 additions and 100 deletions

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@ import { TActionSheetOptionsItem, useActionSheet, ACTION_SHEET_ANIMATION_DURATIO
import Header, { HEADER_HEIGHT, IHeader } from './Header';
import events from '../../lib/methods/helpers/log/events';
import { IApplicationState, IEmoji, ILoggedUser, TAnyMessageModel, TSubscriptionModel } from '../../definitions';
import { getPermalinkMessage } from '../../lib/methods';
import { getPermalinkMessage, getQuoteMessageLink } from '../../lib/methods';
import { compareServerVersion, getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers';
import { Services } from '../../lib/services';
@ -28,6 +28,7 @@ export interface IMessageActionsProps {
reactionInit: (message: TAnyMessageModel) => void;
onReactionPress: (shortname: IEmoji, messageId: string) => void;
replyInit: (message: TAnyMessageModel, mention: boolean) => void;
jumpToMessage?: (messageUrl?: string, isFromReply?: boolean) => Promise<void>;
isMasterDetail: boolean;
isReadOnly: boolean;
serverVersion?: string | null;
@ -62,6 +63,7 @@ const MessageActions = React.memo(
reactionInit,
onReactionPress,
replyInit,
jumpToMessage,
isReadOnly,
Message_AllowDeleting,
Message_AllowDeleting_BlockDeleteInMinutes,
@ -374,6 +376,16 @@ const MessageActions = React.memo(
const options: TActionSheetOptionsItem[] = [];
const videoConfBlock = message.t === 'videoconf';
// Jump to message
const quoteMessageLink = getQuoteMessageLink(message.attachments);
if (quoteMessageLink && jumpToMessage) {
options.push({
title: I18n.t('Jump_to_message'),
icon: 'jump-to-message',
onPress: () => jumpToMessage(quoteMessageLink, true)
});
}
// Quote
if (!isReadOnly && !videoConfBlock) {
options.push({

View File

@ -116,7 +116,15 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
}
return (
<Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} />
<Reply
key={index}
index={index}
attachment={file}
timeFormat={timeFormat}
getCustomEmoji={getCustomEmoji}
msg={msg}
showAttachment={showAttachment}
/>
);
});
return <>{attachmentsElements}</>;

View File

@ -5,7 +5,13 @@ import Markdown from '../markdown';
import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { IAttachment, IUserMessage } from '../../definitions';
import { TDownloadState, downloadMediaFile, getMediaCache } from '../../lib/methods/handleMediaDownload';
import {
TDownloadState,
downloadMediaFile,
getMediaCache,
isDownloadActive,
resumeMediaFile
} from '../../lib/methods/handleMediaDownload';
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
import AudioPlayer from '../AudioPlayer';
import { useAppSelector } from '../../lib/hooks';
@ -23,9 +29,7 @@ interface IMessageAudioProps {
const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMessageAudioProps) => {
const [downloadState, setDownloadState] = useState<TDownloadState>('loading');
const [fileUri, setFileUri] = useState('');
const { baseUrl, user, id, rid } = useContext(MessageContext);
const { cdnPrefix } = useAppSelector(state => ({
cdnPrefix: state.settings.CDN_PREFIX as string
}));
@ -38,8 +42,12 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMe
return url;
};
const onPlayButtonPress = () => {
const onPlayButtonPress = async () => {
if (downloadState === 'to-download') {
const isAudioCached = await handleGetMediaCache();
if (isAudioCached) {
return;
}
handleDownload();
}
};
@ -79,20 +87,44 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMe
}
};
const handleGetMediaCache = async () => {
const cachedAudioResult = await getMediaCache({
type: 'audio',
mimeType: file.audio_type,
urlToCache: getUrl()
});
if (cachedAudioResult?.exists) {
setFileUri(cachedAudioResult.uri);
setDownloadState('downloaded');
}
return !!cachedAudioResult?.exists;
};
const handleResumeDownload = async () => {
try {
setDownloadState('loading');
const url = getUrl();
if (url) {
const videoUri = await resumeMediaFile({
downloadUrl: url
});
setFileUri(videoUri);
setDownloadState('downloaded');
}
} catch (e) {
setDownloadState('to-download');
}
};
useEffect(() => {
const handleCache = async () => {
const cachedAudioResult = await getMediaCache({
type: 'audio',
mimeType: file.audio_type,
urlToCache: getUrl()
});
if (cachedAudioResult?.exists) {
setFileUri(cachedAudioResult.uri);
setDownloadState('downloaded');
const isAudioCached = await handleGetMediaCache();
if (isAudioCached) {
return;
}
if (isReply) {
setDownloadState('to-download');
const audioUrl = getUrl();
if (audioUrl && isDownloadActive(audioUrl)) {
handleResumeDownload();
return;
}
await handleAutoDownload();
@ -106,14 +138,7 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style, msg }: IMe
return (
<>
<Markdown msg={msg} style={[isReply && style]} username={user.username} getCustomEmoji={getCustomEmoji} />
<AudioPlayer
msgId={id}
fileUri={fileUri}
downloadState={downloadState}
disabled={isReply}
onPlayButtonPress={onPlayButtonPress}
rid={rid}
/>
<AudioPlayer msgId={id} fileUri={fileUri} downloadState={downloadState} onPlayButtonPress={onPlayButtonPress} rid={rid} />
</>
);
};

View File

@ -5,7 +5,13 @@ import FastImage from 'react-native-fast-image';
import { IAttachment, IUserMessage } from '../../definitions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../lib/methods/handleMediaDownload';
import {
cancelDownload,
downloadMediaFile,
getMediaCache,
isDownloadActive,
resumeMediaFile
} from '../../lib/methods/handleMediaDownload';
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
import { useTheme } from '../../theme';
import Markdown from '../markdown';
@ -86,25 +92,12 @@ const ImageContainer = ({
useEffect(() => {
const handleCache = async () => {
if (img) {
const cachedImageResult = await getMediaCache({
type: 'image',
mimeType: imageCached.image_type,
urlToCache: imgUrlToCache
});
if (cachedImageResult?.exists) {
setImageCached(prev => ({
...prev,
title_link: cachedImageResult?.uri
}));
setLoading(false);
setCached(true);
return;
}
if (isReply) {
setLoading(false);
const isImageCached = await handleGetMediaCache();
if (isImageCached) {
return;
}
if (isDownloadActive(imgUrlToCache)) {
handleResumeDownload();
return;
}
setLoading(false);
@ -131,6 +124,41 @@ const ImageContainer = ({
}
};
const updateImageCached = (imgUri: string) => {
setImageCached(prev => ({
...prev,
title_link: imgUri
}));
setCached(true);
};
const handleGetMediaCache = async () => {
const cachedImageResult = await getMediaCache({
type: 'image',
mimeType: imageCached.image_type,
urlToCache: imgUrlToCache
});
if (cachedImageResult?.exists) {
updateImageCached(cachedImageResult.uri);
setLoading(false);
}
return !!cachedImageResult?.exists;
};
const handleResumeDownload = async () => {
try {
setLoading(true);
const imageUri = await resumeMediaFile({
downloadUrl: imgUrlToCache
});
updateImageCached(imageUri);
} catch (e) {
setCached(false);
} finally {
setLoading(false);
}
};
const handleDownload = async () => {
try {
setLoading(true);
@ -139,11 +167,7 @@ const ImageContainer = ({
type: 'image',
mimeType: imageCached.image_type
});
setImageCached(prev => ({
...prev,
title_link: imageUri
}));
setCached(true);
updateImageCached(imageUri);
} catch (e) {
setCached(false);
} finally {
@ -151,7 +175,7 @@ const ImageContainer = ({
}
};
const onPress = () => {
const onPress = async () => {
if (loading && isDownloadActive(imgUrlToCache)) {
cancelDownload(imgUrlToCache);
setLoading(false);
@ -159,6 +183,15 @@ const ImageContainer = ({
return;
}
if (!cached && !loading) {
const isImageCached = await handleGetMediaCache();
if (isImageCached && showAttachment) {
showAttachment(imageCached);
return;
}
if (isDownloadActive(imgUrlToCache)) {
handleResumeDownload();
return;
}
handleDownload();
return;
}
@ -172,7 +205,7 @@ const ImageContainer = ({
return (
<View>
<Markdown msg={msg} style={[isReply && style]} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
<Button disabled={isReply} onPress={onPress}>
<Button onPress={onPress}>
<MessageImage imgUri={img} cached={cached} loading={loading} />
</Button>
</View>
@ -180,7 +213,7 @@ const ImageContainer = ({
}
return (
<Button disabled={isReply} onPress={onPress}>
<Button onPress={onPress}>
<MessageImage imgUri={img} cached={cached} loading={loading} />
</Button>
);

View File

@ -92,6 +92,7 @@ interface IMessageReply {
index: number;
getCustomEmoji: TGetCustomEmoji;
msg?: string;
showAttachment?: (file: IAttachment) => void;
}
const Title = React.memo(
@ -198,10 +199,10 @@ const Fields = React.memo(
);
const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji, msg }: IMessageReply) => {
({ attachment, timeFormat, index, getCustomEmoji, msg, showAttachment }: IMessageReply) => {
const [loading, setLoading] = useState(false);
const { theme } = useTheme();
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
const { baseUrl, user } = useContext(MessageContext);
if (!attachment) {
return null;
@ -209,9 +210,6 @@ const Reply = React.memo(
const onPress = async () => {
let url = attachment.title_link || attachment.author_link;
if (attachment.message_link) {
return jumpToMessage(attachment.message_link);
}
if (!url) {
return;
}
@ -245,7 +243,7 @@ const Reply = React.memo(
}
]}
background={Touchable.Ripple(themes[theme].bannerBackground)}
disabled={loading}
disabled={loading || attachment.message_link}
>
<View style={styles.attachmentContainer}>
<Title attachment={attachment} timeFormat={timeFormat} theme={theme} />
@ -257,6 +255,7 @@ const Reply = React.memo(
timeFormat={timeFormat}
style={[{ color: themes[theme].auxiliaryTintColor, fontSize: 14, marginBottom: 8 }]}
isReply
showAttachment={showAttachment}
/>
<Fields attachment={attachment} getCustomEmoji={getCustomEmoji} theme={theme} />
{loading ? (

View File

@ -7,7 +7,13 @@ import { TGetCustomEmoji } from '../../definitions/IEmoji';
import I18n from '../../i18n';
import { themes } from '../../lib/constants';
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../lib/methods/handleMediaDownload';
import {
cancelDownload,
downloadMediaFile,
getMediaCache,
isDownloadActive,
resumeMediaFile
} from '../../lib/methods/handleMediaDownload';
import { isIOS } from '../../lib/methods/helpers';
import EventEmitter from '../../lib/methods/helpers/events';
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
@ -93,26 +99,12 @@ const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IM
useEffect(() => {
const handleVideoSearchAndDownload = async () => {
if (video) {
const cachedVideoResult = await getMediaCache({
type: 'video',
mimeType: file.video_type,
urlToCache: video
});
const downloadActive = isDownloadActive(video);
if (cachedVideoResult?.exists) {
setVideoCached(prev => ({
...prev,
video_url: cachedVideoResult?.uri
}));
setLoading(false);
setCached(true);
if (downloadActive) {
cancelDownload(video);
}
const isVideoCached = await handleGetMediaCache();
if (isVideoCached) {
return;
}
if (isReply) {
setLoading(false);
if (isDownloadActive(video)) {
handleResumeDownload();
return;
}
await handleAutoDownload();
@ -134,6 +126,41 @@ const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IM
setLoading(false);
};
const updateVideoCached = (videoUri: string) => {
setVideoCached(prev => ({
...prev,
video_url: videoUri
}));
setCached(true);
};
const handleGetMediaCache = async () => {
const cachedVideoResult = await getMediaCache({
type: 'video',
mimeType: file.video_type,
urlToCache: video
});
if (cachedVideoResult?.exists) {
updateVideoCached(cachedVideoResult.uri);
setLoading(false);
}
return !!cachedVideoResult?.exists;
};
const handleResumeDownload = async () => {
try {
setLoading(true);
const videoUri = await resumeMediaFile({
downloadUrl: video
});
updateVideoCached(videoUri);
} catch (e) {
setCached(false);
} finally {
setLoading(false);
}
};
const handleDownload = async () => {
setLoading(true);
try {
@ -142,11 +169,7 @@ const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IM
type: 'video',
mimeType: file.video_type
});
setVideoCached(prev => ({
...prev,
video_url: videoUri
}));
setCached(true);
updateVideoCached(videoUri);
} catch {
setCached(false);
} finally {
@ -160,6 +183,15 @@ const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IM
return;
}
if (!loading && !cached && file.video_type && isTypeSupported(file.video_type)) {
const isVideoCached = await handleGetMediaCache();
if (isVideoCached && showAttachment) {
showAttachment(videoCached);
return;
}
if (isDownloadActive(video)) {
handleResumeDownload();
return;
}
handleDownload();
return;
}
@ -197,7 +229,6 @@ const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IM
<>
<Markdown msg={msg} username={user.username} getCustomEmoji={getCustomEmoji} style={[isReply && style]} theme={theme} />
<Touchable
disabled={isReply}
onPress={onPress}
style={[styles.button, messageStyles.mustWrapBlur, { backgroundColor: themes[theme].videoBackground }]}
background={Touchable.Ripple(themes[theme].bannerBackground)}

View File

@ -761,5 +761,6 @@
"Enable_writing_in_room": "Enable writing in room",
"Disable_writing_in_room": "Disable writing in room",
"Pinned_a_message": "Pinned a message:",
"Jump_to_message": "Jump to message",
"Missed_call": "Missed call"
}

View File

@ -0,0 +1,94 @@
import { getQuoteMessageLink } from './getQuoteMessageLink';
const imageAttachment = [
{
ts: '1970-01-01T00:00:00.000Z',
title: 'IMG_0058.MP4',
title_link: '/file-upload/34q5BbCRW3wCauiDt/IMG_0058.MP4',
title_link_download: true,
video_url: '/file-upload/34q5BbCRW3wCauiDt/IMG_0058.MP4',
video_type: 'video/mp4',
video_size: 4867328,
type: 'file',
fields: [],
attachments: []
}
];
const imageAttachmentWithAQuote = [
...imageAttachment,
{
text: '[ ](https://mobile.rocket.chat/group/channel-etc?msg=cIqhbvkOSgiCOK4Wh) \nhttps://www.youtube.com/watch?v=5yx6BWlEVcY',
md: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://mobile.rocket.chat/group/channel-etc?msg=cIqhbvkOSgiCOK4Wh' },
label: [{ type: 'PLAIN_TEXT', value: ' ' }]
}
},
{ type: 'PLAIN_TEXT', value: ' ' }
]
},
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://www.youtube.com/watch?v=5yx6BWlEVcY' },
label: [{ type: 'PLAIN_TEXT', value: 'https://www.youtube.com/watch?v=5yx6BWlEVcY' }]
}
}
]
}
],
message_link: 'https://mobile.rocket.chat/group/channel-etc?msg=n5WaK5NRJN42Hg26w',
author_name: 'user-two',
author_icon: '/avatar/user-two',
attachments: [
{
text: 'https://www.youtube.com/watch?v=5yx6BWlEVcY',
md: [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: { type: 'PLAIN_TEXT', value: 'https://www.youtube.com/watch?v=5yx6BWlEVcY' },
label: [{ type: 'PLAIN_TEXT', value: 'https://www.youtube.com/watch?v=5yx6BWlEVcY' }]
}
}
]
}
],
message_link: 'https://mobile.rocket.chat/group/channel-etc?msg=cIqhbvkOSgiCOK4Wh',
author_name: 'user-two',
author_icon: '/avatar/user-two',
ts: '2023-11-23T14:10:18.520Z',
fields: [],
attachments: []
}
],
ts: '2023-11-23T17:47:51.676Z',
fields: []
}
];
describe('Test the getQuoteMessageLink', () => {
it('return undefined from a message without attachment', () => {
expect(getQuoteMessageLink([])).toBe(undefined);
});
it('return undefined from a message with image attachment', () => {
expect(getQuoteMessageLink(imageAttachment)).toBe(undefined);
});
it('return the message link from an image message with a quote', () => {
const expectedResult = 'https://mobile.rocket.chat/group/channel-etc?msg=n5WaK5NRJN42Hg26w';
expect(getQuoteMessageLink(imageAttachmentWithAQuote)).toBe(expectedResult);
});
});

View File

@ -0,0 +1,7 @@
import { IAttachment } from '../../definitions/IAttachment';
// https://github.com/RocketChat/Rocket.Chat/blame/edb4e2c91f4e8f90b0420be61270a75d49709732/packages/core-typings/src/IMessage/MessageAttachment/MessageQuoteAttachment.ts#L16
export const getQuoteMessageLink = (attachments?: IAttachment[]) => {
const attachmentWithMessageLink = attachments?.find(attachment => 'message_link' in attachment);
return attachmentWithMessageLink?.message_link;
};

View File

@ -10,7 +10,7 @@ const getSingleMessage = (messageId: string): Promise<IMessage> =>
}
return reject();
} catch (e) {
return reject();
return reject(e);
}
});

View File

@ -217,3 +217,21 @@ export function downloadMediaFile({
}
});
}
export function resumeMediaFile({ downloadUrl }: { downloadUrl: string }): Promise<string> {
return new Promise(async (resolve, reject) => {
let downloadKey = '';
try {
downloadKey = mediaDownloadKey(downloadUrl);
const result = await downloadQueue[downloadKey].resumeAsync();
if (result?.uri) {
return resolve(result.uri);
}
return reject();
} catch {
return reject();
} finally {
delete downloadQueue[downloadKey];
}
});
}

View File

@ -42,3 +42,4 @@ export * from './isRoomFederated';
export * from './checkSupportedVersions';
export * from './getServerInfo';
export * from './isImageBase64';
export * from './getQuoteMessageLink';

View File

@ -972,7 +972,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
return true;
};
jumpToMessageByUrl = async (messageUrl?: string) => {
jumpToMessageByUrl = async (messageUrl?: string, isFromReply?: boolean) => {
if (!messageUrl) {
return;
}
@ -980,14 +980,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
const parsedUrl = parse(messageUrl, true);
const messageId = parsedUrl.query.msg;
if (messageId) {
await this.jumpToMessage(messageId);
await this.jumpToMessage(messageId, isFromReply);
}
} catch (e) {
log(e);
}
};
jumpToMessage = async (messageId: string) => {
jumpToMessage = async (messageId: string, isFromReply?: boolean) => {
try {
sendLoadingEvent({ visible: true, onCancel: this.cancelJumpToMessage });
const message = await RoomServices.getMessageInfo(messageId);
@ -1019,8 +1019,12 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
await Promise.race([this.list.current?.jumpToMessage(message.id), new Promise(res => setTimeout(res, 5000))]);
this.cancelJumpToMessage();
}
} catch (e) {
log(e);
} catch (error: any) {
if (isFromReply && error.data?.errorType === 'error-not-allowed') {
showErrorAlert(I18n.t('The_room_does_not_exist'), I18n.t('Room_not_found'));
} else {
log(error);
}
this.cancelJumpToMessage();
}
};
@ -1505,6 +1509,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
replyInit={this.onReplyInit}
reactionInit={this.onReactionInit}
onReactionPress={this.onReactionPress}
jumpToMessage={this.jumpToMessageByUrl}
isReadOnly={readOnly}
/>
<MessageErrorActions ref={ref => (this.messageErrorActions = ref)} tmid={this.tmid} />

View File

@ -165,6 +165,13 @@ async function tryTapping(
}
}
async function jumpToQuotedMessage(theElement: Detox.IndexableNativeElement | Detox.NativeElement): Promise<void> {
const deviceType = device.getPlatform();
const { textMatcher } = platformTypes[deviceType];
await tryTapping(theElement, 2000, true);
await element(by[textMatcher]('Jump to message')).atIndex(0).tap();
}
async function tapAndWaitFor(
elementToTap: Detox.IndexableNativeElement | Detox.NativeElement,
elementToWaitFor: Detox.IndexableNativeElement | Detox.NativeElement,
@ -255,5 +262,6 @@ export {
checkRoomTitle,
checkServer,
platformTypes,
expectValidRegisterOrRetry
expectValidRegisterOrRetry,
jumpToQuotedMessage
};

View File

@ -11,7 +11,8 @@ import {
TTextMatcher,
sleep,
checkRoomTitle,
mockMessage
mockMessage,
jumpToQuotedMessage
} from '../../helpers/app';
import { createRandomUser, ITestUser } from '../../helpers/data_setup';
import random from '../../helpers/random';
@ -144,7 +145,7 @@ describe('Broadcast room', () => {
await waitFor(element(by[textMatcher](message)))
.toExist()
.withTimeout(10000);
await element(by[textMatcher](message)).tap();
await jumpToQuotedMessage(element(by[textMatcher](message)));
await sleep(300); // wait for animation
await checkRoomTitle(room);
});

View File

@ -1,7 +1,16 @@
import { device, waitFor, element, by, expect } from 'detox';
import data from '../../data';
import { navigateToLogin, tapBack, login, sleep, platformTypes, TTextMatcher, navigateToRoom } from '../../helpers/app';
import {
navigateToLogin,
tapBack,
login,
sleep,
platformTypes,
TTextMatcher,
navigateToRoom,
jumpToQuotedMessage
} from '../../helpers/app';
let textMatcher: TTextMatcher;
let alertButtonType: string;
@ -74,7 +83,7 @@ describe('Room', () => {
.toExist()
.withTimeout(5000);
await sleep(2000);
await element(by[textMatcher]('1')).atIndex(0).tap();
await jumpToQuotedMessage(element(by[textMatcher]('1')).atIndex(0));
await waitForLoading();
await waitFor(element(by[textMatcher]('1')).atIndex(0))
.toExist()
@ -230,7 +239,7 @@ describe('Threads', () => {
await waitFor(element(by[textMatcher]("Go to jumping-thread's thread")).atIndex(0))
.toExist()
.withTimeout(5000);
await element(by[textMatcher]("Go to jumping-thread's thread")).atIndex(0).tap();
await jumpToQuotedMessage(element(by[textMatcher]("Go to jumping-thread's thread")).atIndex(0));
await expectThreadMessages("Go to jumping-thread's thread");
await tapBack();
});
@ -260,7 +269,7 @@ describe('Threads', () => {
await waitFor(element(by[textMatcher]('quoted')))
.toExist()
.withTimeout(5000);
await element(by[textMatcher]('quoted')).atIndex(0).tap();
await jumpToQuotedMessage(element(by[textMatcher]('quoted')).atIndex(0));
await expectThreadMessages('quoted');
await tapBack();
});