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:
parent
2989b3c2ee
commit
b217435ffe
File diff suppressed because one or more lines are too long
|
@ -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({
|
||||||
|
|
|
@ -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}</>;
|
||||||
|
|
|
@ -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,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(() => {
|
useEffect(() => {
|
||||||
const handleCache = async () => {
|
const handleCache = async () => {
|
||||||
const cachedAudioResult = await getMediaCache({
|
const isAudioCached = await handleGetMediaCache();
|
||||||
type: 'audio',
|
if (isAudioCached) {
|
||||||
mimeType: file.audio_type,
|
|
||||||
urlToCache: getUrl()
|
|
||||||
});
|
|
||||||
if (cachedAudioResult?.exists) {
|
|
||||||
setFileUri(cachedAudioResult.uri);
|
|
||||||
setDownloadState('downloaded');
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
|
@ -10,7 +10,7 @@ const getSingleMessage = (messageId: string): Promise<IMessage> =>
|
||||||
}
|
}
|
||||||
return reject();
|
return reject();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return reject();
|
return reject(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue