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

View File

@ -116,7 +116,15 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
} }
return ( 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}</>; return <>{attachmentsElements}</>;

View File

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

View File

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

View File

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

View File

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

View File

@ -761,5 +761,6 @@
"Enable_writing_in_room": "Enable writing in room", "Enable_writing_in_room": "Enable writing in room",
"Disable_writing_in_room": "Disable writing in room", "Disable_writing_in_room": "Disable writing in room",
"Pinned_a_message": "Pinned a message:", "Pinned_a_message": "Pinned a message:",
"Jump_to_message": "Jump to message",
"Missed_call": "Missed call" "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(); return reject();
} catch (e) { } 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 './checkSupportedVersions';
export * from './getServerInfo'; export * from './getServerInfo';
export * from './isImageBase64'; export * from './isImageBase64';
export * from './getQuoteMessageLink';

View File

@ -972,7 +972,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
return true; return true;
}; };
jumpToMessageByUrl = async (messageUrl?: string) => { jumpToMessageByUrl = async (messageUrl?: string, isFromReply?: boolean) => {
if (!messageUrl) { if (!messageUrl) {
return; return;
} }
@ -980,14 +980,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
const parsedUrl = parse(messageUrl, true); const parsedUrl = parse(messageUrl, true);
const messageId = parsedUrl.query.msg; const messageId = parsedUrl.query.msg;
if (messageId) { if (messageId) {
await this.jumpToMessage(messageId); await this.jumpToMessage(messageId, isFromReply);
} }
} catch (e) { } catch (e) {
log(e); log(e);
} }
}; };
jumpToMessage = async (messageId: string) => { jumpToMessage = async (messageId: string, isFromReply?: boolean) => {
try { try {
sendLoadingEvent({ visible: true, onCancel: this.cancelJumpToMessage }); sendLoadingEvent({ visible: true, onCancel: this.cancelJumpToMessage });
const message = await RoomServices.getMessageInfo(messageId); 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))]); await Promise.race([this.list.current?.jumpToMessage(message.id), new Promise(res => setTimeout(res, 5000))]);
this.cancelJumpToMessage(); this.cancelJumpToMessage();
} }
} catch (e) { } catch (error: any) {
log(e); 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(); this.cancelJumpToMessage();
} }
}; };
@ -1505,6 +1509,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
replyInit={this.onReplyInit} replyInit={this.onReplyInit}
reactionInit={this.onReactionInit} reactionInit={this.onReactionInit}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
jumpToMessage={this.jumpToMessageByUrl}
isReadOnly={readOnly} isReadOnly={readOnly}
/> />
<MessageErrorActions ref={ref => (this.messageErrorActions = ref)} tmid={this.tmid} /> <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( async function tapAndWaitFor(
elementToTap: Detox.IndexableNativeElement | Detox.NativeElement, elementToTap: Detox.IndexableNativeElement | Detox.NativeElement,
elementToWaitFor: Detox.IndexableNativeElement | Detox.NativeElement, elementToWaitFor: Detox.IndexableNativeElement | Detox.NativeElement,
@ -255,5 +262,6 @@ export {
checkRoomTitle, checkRoomTitle,
checkServer, checkServer,
platformTypes, platformTypes,
expectValidRegisterOrRetry expectValidRegisterOrRetry,
jumpToQuotedMessage
}; };

View File

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

View File

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