fix: fix bugs related to auto-translate and add tests (#5144)
* fix re-render on autoTranslateRoom sub update
* create autoTranslate tests
* create getMessageFromAttachment
* fix autoTranslate null value
* add translateLanguage to context
* fix type
* fix shouldComponentUpdate
* add autoTranslate and autoTranslateLanguage to subscription
* use getMessageFromAttachment instead att.description
* remove dequal
* add tryCatch
* 🙏
This commit is contained in:
parent
0c8b2f565e
commit
acbcac29c8
|
@ -322,7 +322,7 @@ const MessageActions = React.memo(
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
await db.write(async () => {
|
await db.write(async () => {
|
||||||
await message.update(m => {
|
await message.update(m => {
|
||||||
m.autoTranslate = !m.autoTranslate;
|
m.autoTranslate = m.autoTranslate !== null ? !m.autoTranslate : false;
|
||||||
m._updatedAt = new Date();
|
m._updatedAt = new Date();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -479,7 +479,7 @@ const MessageActions = React.memo(
|
||||||
// Toggle Auto-translate
|
// Toggle Auto-translate
|
||||||
if (room.autoTranslate && message.u && message.u._id !== user.id) {
|
if (room.autoTranslate && message.u && message.u._id !== user.id) {
|
||||||
options.push({
|
options.push({
|
||||||
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
|
title: I18n.t(message.autoTranslate !== false ? 'View_Original' : 'Translate'),
|
||||||
icon: 'language',
|
icon: 'language',
|
||||||
onPress: () => handleToggleTranslation(message)
|
onPress: () => handleToggleTranslation(message)
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { IAttachment, TGetCustomEmoji } from '../../definitions';
|
||||||
import CollapsibleQuote from './Components/CollapsibleQuote';
|
import CollapsibleQuote from './Components/CollapsibleQuote';
|
||||||
import openLink from '../../lib/methods/helpers/openLink';
|
import openLink from '../../lib/methods/helpers/openLink';
|
||||||
import Markdown from '../markdown';
|
import Markdown from '../markdown';
|
||||||
|
import { getMessageFromAttachment } from './utils';
|
||||||
|
|
||||||
export type TElement = {
|
export type TElement = {
|
||||||
type: string;
|
type: string;
|
||||||
|
@ -56,12 +57,14 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme
|
||||||
const Attachments: React.FC<IMessageAttachments> = React.memo(
|
const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, author }: IMessageAttachments) => {
|
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, author }: IMessageAttachments) => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
const { translateLanguage } = useContext(MessageContext);
|
||||||
|
|
||||||
if (!attachments || attachments.length === 0) {
|
if (!attachments || attachments.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentsElements = attachments.map((file: IAttachment, index: number) => {
|
const attachmentsElements = attachments.map((file: IAttachment, index: number) => {
|
||||||
|
const msg = getMessageFromAttachment(file, translateLanguage);
|
||||||
if (file && file.image_url) {
|
if (file && file.image_url) {
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
|
@ -72,6 +75,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
style={style}
|
style={style}
|
||||||
isReply={isReply}
|
isReply={isReply}
|
||||||
author={author}
|
author={author}
|
||||||
|
msg={msg}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -86,6 +90,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
style={style}
|
style={style}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
author={author}
|
author={author}
|
||||||
|
msg={msg}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -99,6 +104,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
style={style}
|
style={style}
|
||||||
isReply={isReply}
|
isReply={isReply}
|
||||||
|
msg={msg}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -112,7 +118,9 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} />;
|
return (
|
||||||
|
<Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} msg={msg} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
return <>{attachmentsElements}</>;
|
return <>{attachmentsElements}</>;
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,6 +40,7 @@ interface IMessageAudioProps {
|
||||||
getCustomEmoji: TGetCustomEmoji;
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
scale?: number;
|
scale?: number;
|
||||||
author?: IUserMessage;
|
author?: IUserMessage;
|
||||||
|
msg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMessageAudioState {
|
interface IMessageAudioState {
|
||||||
|
@ -348,8 +349,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { loading, paused, currentTime, duration, cached } = this.state;
|
const { loading, paused, currentTime, duration, cached } = this.state;
|
||||||
const { file, getCustomEmoji, theme, scale, isReply, style } = this.props;
|
const { msg, getCustomEmoji, theme, scale, isReply, style } = this.props;
|
||||||
const { description } = file;
|
|
||||||
// @ts-ignore can't use declare to type this
|
// @ts-ignore can't use declare to type this
|
||||||
const { baseUrl, user } = this.context;
|
const { baseUrl, user } = this.context;
|
||||||
|
|
||||||
|
@ -367,7 +367,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Markdown
|
<Markdown
|
||||||
msg={description}
|
msg={msg}
|
||||||
style={[isReply && style]}
|
style={[isReply && style]}
|
||||||
username={user.username}
|
username={user.username}
|
||||||
getCustomEmoji={getCustomEmoji}
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { StyleProp, TextStyle, View } from 'react-native';
|
import { StyleProp, TextStyle, View } from 'react-native';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import { dequal } from 'dequal';
|
|
||||||
|
|
||||||
import Touchable from './Touchable';
|
|
||||||
import Markdown from '../markdown';
|
|
||||||
import styles from './styles';
|
|
||||||
import MessageContext from './Context';
|
|
||||||
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
|
||||||
import { IAttachment, IUserMessage } from '../../definitions';
|
import { IAttachment, IUserMessage } from '../../definitions';
|
||||||
import { useTheme } from '../../theme';
|
import { TGetCustomEmoji } from '../../definitions/IEmoji';
|
||||||
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
|
|
||||||
import { cancelDownload, downloadMediaFile, isDownloadActive, getMediaCache } from '../../lib/methods/handleMediaDownload';
|
|
||||||
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
|
import { fetchAutoDownloadEnabled } from '../../lib/methods/autoDownloadPreference';
|
||||||
|
import { cancelDownload, downloadMediaFile, getMediaCache, isDownloadActive } from '../../lib/methods/handleMediaDownload';
|
||||||
|
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
|
||||||
|
import { useTheme } from '../../theme';
|
||||||
|
import Markdown from '../markdown';
|
||||||
import BlurComponent from './Components/BlurComponent';
|
import BlurComponent from './Components/BlurComponent';
|
||||||
|
import MessageContext from './Context';
|
||||||
|
import Touchable from './Touchable';
|
||||||
|
import styles from './styles';
|
||||||
|
|
||||||
interface IMessageButton {
|
interface IMessageButton {
|
||||||
children: React.ReactElement;
|
children: React.ReactElement;
|
||||||
|
@ -29,6 +28,7 @@ interface IMessageImage {
|
||||||
isReply?: boolean;
|
isReply?: boolean;
|
||||||
getCustomEmoji?: TGetCustomEmoji;
|
getCustomEmoji?: TGetCustomEmoji;
|
||||||
author?: IUserMessage;
|
author?: IUserMessage;
|
||||||
|
msg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = React.memo(({ children, onPress, disabled }: IMessageButton) => {
|
const Button = React.memo(({ children, onPress, disabled }: IMessageButton) => {
|
||||||
|
@ -61,124 +61,124 @@ export const MessageImage = React.memo(({ imgUri, cached, loading }: { imgUri: s
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ImageContainer = React.memo(
|
const ImageContainer = ({
|
||||||
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, author }: IMessageImage) => {
|
file,
|
||||||
const [imageCached, setImageCached] = useState(file);
|
imageUrl,
|
||||||
const [cached, setCached] = useState(false);
|
showAttachment,
|
||||||
const [loading, setLoading] = useState(true);
|
getCustomEmoji,
|
||||||
const { theme } = useTheme();
|
style,
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
isReply,
|
||||||
const getUrl = (link?: string) => imageUrl || formatAttachmentUrl(link, user.id, user.token, baseUrl);
|
author,
|
||||||
const img = getUrl(file.image_url);
|
msg
|
||||||
// The param file.title_link is the one that point to image with best quality, however we still need to test the imageUrl
|
}: IMessageImage): React.ReactElement | null => {
|
||||||
// And we cannot be certain whether the file.title_link actually exists.
|
const [imageCached, setImageCached] = useState(file);
|
||||||
const imgUrlToCache = getUrl(imageCached.title_link || imageCached.image_url);
|
const [cached, setCached] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const { baseUrl, user } = useContext(MessageContext);
|
||||||
|
const getUrl = (link?: string) => imageUrl || formatAttachmentUrl(link, user.id, user.token, baseUrl);
|
||||||
|
const img = getUrl(file.image_url);
|
||||||
|
// The param file.title_link is the one that point to image with best quality, however we still need to test the imageUrl
|
||||||
|
// And we cannot be certain whether the file.title_link actually exists.
|
||||||
|
const imgUrlToCache = getUrl(imageCached.title_link || imageCached.image_url);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleCache = async () => {
|
const handleCache = async () => {
|
||||||
if (img) {
|
if (img) {
|
||||||
const cachedImageResult = await getMediaCache({
|
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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isDownloadActive(imgUrlToCache)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
await handleAutoDownload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handleCache();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!img) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAutoDownload = async () => {
|
|
||||||
const isCurrentUserAuthor = author?._id === user.id;
|
|
||||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('imagesPreferenceDownload');
|
|
||||||
if (isAutoDownloadEnabled || isCurrentUserAuthor) {
|
|
||||||
await handleDownload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = async () => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const imageUri = await downloadMediaFile({
|
|
||||||
downloadUrl: imgUrlToCache,
|
|
||||||
type: 'image',
|
type: 'image',
|
||||||
mimeType: imageCached.image_type
|
mimeType: imageCached.image_type,
|
||||||
|
urlToCache: imgUrlToCache
|
||||||
});
|
});
|
||||||
setImageCached(prev => ({
|
if (cachedImageResult?.exists) {
|
||||||
...prev,
|
setImageCached(prev => ({
|
||||||
title_link: imageUri
|
...prev,
|
||||||
}));
|
title_link: cachedImageResult?.uri
|
||||||
setCached(true);
|
}));
|
||||||
} catch (e) {
|
setLoading(false);
|
||||||
setCached(false);
|
setCached(true);
|
||||||
} finally {
|
return;
|
||||||
|
}
|
||||||
|
if (isReply) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isDownloadActive(imgUrlToCache)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
await handleAutoDownload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
handleCache();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onPress = () => {
|
if (!img) {
|
||||||
if (loading && isDownloadActive(imgUrlToCache)) {
|
return null;
|
||||||
cancelDownload(imgUrlToCache);
|
}
|
||||||
setLoading(false);
|
|
||||||
setCached(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cached && !loading) {
|
|
||||||
handleDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!showAttachment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showAttachment(imageCached);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (imageCached.description) {
|
const handleAutoDownload = async () => {
|
||||||
return (
|
const isCurrentUserAuthor = author?._id === user.id;
|
||||||
<View>
|
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('imagesPreferenceDownload');
|
||||||
<Markdown
|
if (isAutoDownloadEnabled || isCurrentUserAuthor) {
|
||||||
msg={imageCached.description}
|
await handleDownload();
|
||||||
style={[isReply && style]}
|
|
||||||
username={user.username}
|
|
||||||
getCustomEmoji={getCustomEmoji}
|
|
||||||
theme={theme}
|
|
||||||
/>
|
|
||||||
<Button disabled={isReply} onPress={onPress}>
|
|
||||||
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const imageUri = await downloadMediaFile({
|
||||||
|
downloadUrl: imgUrlToCache,
|
||||||
|
type: 'image',
|
||||||
|
mimeType: imageCached.image_type
|
||||||
|
});
|
||||||
|
setImageCached(prev => ({
|
||||||
|
...prev,
|
||||||
|
title_link: imageUri
|
||||||
|
}));
|
||||||
|
setCached(true);
|
||||||
|
} catch (e) {
|
||||||
|
setCached(false);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPress = () => {
|
||||||
|
if (loading && isDownloadActive(imgUrlToCache)) {
|
||||||
|
cancelDownload(imgUrlToCache);
|
||||||
|
setLoading(false);
|
||||||
|
setCached(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!cached && !loading) {
|
||||||
|
handleDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!showAttachment) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showAttachment(imageCached);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (msg) {
|
||||||
return (
|
return (
|
||||||
<Button disabled={isReply} onPress={onPress}>
|
<View>
|
||||||
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
<Markdown msg={msg} style={[isReply && style]} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||||
</Button>
|
<Button disabled={isReply} onPress={onPress}>
|
||||||
|
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
(prevProps, nextProps) => dequal(prevProps.file, nextProps.file)
|
|
||||||
);
|
return (
|
||||||
|
<Button disabled={isReply} onPress={onPress}>
|
||||||
|
<MessageImage imgUri={img} cached={cached} loading={loading} />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
ImageContainer.displayName = 'MessageImageContainer';
|
ImageContainer.displayName = 'MessageImageContainer';
|
||||||
MessageImage.displayName = 'MessageImage';
|
MessageImage.displayName = 'MessageImage';
|
||||||
|
|
|
@ -91,6 +91,7 @@ interface IMessageReply {
|
||||||
timeFormat?: string;
|
timeFormat?: string;
|
||||||
index: number;
|
index: number;
|
||||||
getCustomEmoji: TGetCustomEmoji;
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
|
msg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Title = React.memo(
|
const Title = React.memo(
|
||||||
|
@ -197,7 +198,7 @@ const Fields = React.memo(
|
||||||
);
|
);
|
||||||
|
|
||||||
const Reply = React.memo(
|
const Reply = React.memo(
|
||||||
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
|
({ attachment, timeFormat, index, getCustomEmoji, msg }: 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, jumpToMessage } = useContext(MessageContext);
|
||||||
|
@ -238,7 +239,7 @@ const Reply = React.memo(
|
||||||
style={[
|
style={[
|
||||||
styles.button,
|
styles.button,
|
||||||
index > 0 && styles.marginTop,
|
index > 0 && styles.marginTop,
|
||||||
attachment.description && styles.marginBottom,
|
msg && styles.marginBottom,
|
||||||
{
|
{
|
||||||
borderColor
|
borderColor
|
||||||
}
|
}
|
||||||
|
@ -271,7 +272,7 @@ const Reply = React.memo(
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
<Markdown msg={attachment.description} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
<Markdown msg={msg} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { StyleProp, StyleSheet, TextStyle, View, Text } from 'react-native';
|
import { StyleProp, StyleSheet, TextStyle, View, Text } from 'react-native';
|
||||||
import { dequal } from 'dequal';
|
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
|
|
||||||
import messageStyles from './styles';
|
import messageStyles from './styles';
|
||||||
|
@ -56,6 +55,7 @@ interface IMessageVideo {
|
||||||
getCustomEmoji: TGetCustomEmoji;
|
getCustomEmoji: TGetCustomEmoji;
|
||||||
style?: StyleProp<TextStyle>[];
|
style?: StyleProp<TextStyle>[];
|
||||||
isReply?: boolean;
|
isReply?: boolean;
|
||||||
|
msg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CancelIndicator = () => {
|
const CancelIndicator = () => {
|
||||||
|
@ -81,139 +81,130 @@ const Thumbnail = ({ loading, thumbnailUrl, cached }: { loading: boolean; thumbn
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Video = React.memo(
|
const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IMessageVideo): React.ReactElement | null => {
|
||||||
({ file, showAttachment, getCustomEmoji, style, isReply }: IMessageVideo) => {
|
const [videoCached, setVideoCached] = useState(file);
|
||||||
const [videoCached, setVideoCached] = useState(file);
|
const [loading, setLoading] = useState(true);
|
||||||
const [loading, setLoading] = useState(true);
|
const [cached, setCached] = useState(false);
|
||||||
const [cached, setCached] = useState(false);
|
const { baseUrl, user } = useContext(MessageContext);
|
||||||
const { baseUrl, user } = useContext(MessageContext);
|
const { theme } = useTheme();
|
||||||
const { theme } = useTheme();
|
const video = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||||
const video = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleVideoSearchAndDownload = async () => {
|
const handleVideoSearchAndDownload = async () => {
|
||||||
if (video) {
|
if (video) {
|
||||||
const cachedVideoResult = await getMediaCache({
|
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);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isReply) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await handleAutoDownload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
handleVideoSearchAndDownload();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!baseUrl) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleAutoDownload = async () => {
|
|
||||||
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
|
|
||||||
if (isAutoDownloadEnabled && file.video_type && isTypeSupported(file.video_type)) {
|
|
||||||
await handleDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDownload = async () => {
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
const videoUri = await downloadMediaFile({
|
|
||||||
downloadUrl: video,
|
|
||||||
type: 'video',
|
type: 'video',
|
||||||
mimeType: file.video_type
|
mimeType: file.video_type,
|
||||||
|
urlToCache: video
|
||||||
});
|
});
|
||||||
setVideoCached(prev => ({
|
const downloadActive = isDownloadActive(video);
|
||||||
...prev,
|
if (cachedVideoResult?.exists) {
|
||||||
video_url: videoUri
|
setVideoCached(prev => ({
|
||||||
}));
|
...prev,
|
||||||
setCached(true);
|
video_url: cachedVideoResult?.uri
|
||||||
} catch {
|
}));
|
||||||
setCached(false);
|
setLoading(false);
|
||||||
} finally {
|
setCached(true);
|
||||||
setLoading(false);
|
if (downloadActive) {
|
||||||
|
cancelDownload(video);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isReply) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handleAutoDownload();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
handleVideoSearchAndDownload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onPress = async () => {
|
if (!baseUrl) {
|
||||||
if (file.video_type && cached && isTypeSupported(file.video_type) && showAttachment) {
|
return null;
|
||||||
showAttachment(videoCached);
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!loading && !cached && file.video_type && isTypeSupported(file.video_type)) {
|
|
||||||
handleDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (loading && !cached) {
|
|
||||||
handleCancelDownload();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isIOS && file.video_url) {
|
|
||||||
await downloadVideoToGallery(video);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('Unsupported_format') });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancelDownload = () => {
|
const handleAutoDownload = async () => {
|
||||||
if (loading) {
|
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
|
||||||
cancelDownload(video);
|
if (isAutoDownloadEnabled && file.video_type && isTypeSupported(file.video_type)) {
|
||||||
setLoading(false);
|
await handleDownload();
|
||||||
}
|
return;
|
||||||
};
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const downloadVideoToGallery = async (uri: string) => {
|
const handleDownload = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const fileDownloaded = await fileDownload(uri, file);
|
try {
|
||||||
|
const videoUri = await downloadMediaFile({
|
||||||
|
downloadUrl: video,
|
||||||
|
type: 'video',
|
||||||
|
mimeType: file.video_type
|
||||||
|
});
|
||||||
|
setVideoCached(prev => ({
|
||||||
|
...prev,
|
||||||
|
video_url: videoUri
|
||||||
|
}));
|
||||||
|
setCached(true);
|
||||||
|
} catch {
|
||||||
|
setCached(false);
|
||||||
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (fileDownloaded) {
|
const onPress = async () => {
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
|
if (file.video_type && cached && isTypeSupported(file.video_type) && showAttachment) {
|
||||||
return;
|
showAttachment(videoCached);
|
||||||
}
|
return;
|
||||||
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') });
|
}
|
||||||
};
|
if (!loading && !cached && file.video_type && isTypeSupported(file.video_type)) {
|
||||||
|
handleDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (loading && !cached) {
|
||||||
|
handleCancelDownload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isIOS && file.video_url) {
|
||||||
|
await downloadVideoToGallery(video);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventEmitter.emit(LISTENER, { message: I18n.t('Unsupported_format') });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const handleCancelDownload = () => {
|
||||||
<>
|
if (loading) {
|
||||||
<Markdown
|
cancelDownload(video);
|
||||||
msg={file.description}
|
setLoading(false);
|
||||||
username={user.username}
|
}
|
||||||
getCustomEmoji={getCustomEmoji}
|
};
|
||||||
style={[isReply && style]}
|
|
||||||
theme={theme}
|
const downloadVideoToGallery = async (uri: string) => {
|
||||||
/>
|
setLoading(true);
|
||||||
<Touchable
|
const fileDownloaded = await fileDownload(uri, file);
|
||||||
disabled={isReply}
|
setLoading(false);
|
||||||
onPress={onPress}
|
|
||||||
style={[styles.button, messageStyles.mustWrapBlur, { backgroundColor: themes[theme].videoBackground }]}
|
if (fileDownloaded) {
|
||||||
background={Touchable.Ripple(themes[theme].bannerBackground)}
|
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
|
||||||
>
|
return;
|
||||||
<Thumbnail loading={loading} cached={cached} />
|
}
|
||||||
</Touchable>
|
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') });
|
||||||
</>
|
};
|
||||||
);
|
|
||||||
},
|
return (
|
||||||
(prevProps, nextProps) => dequal(prevProps.file, nextProps.file)
|
<>
|
||||||
);
|
<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)}
|
||||||
|
>
|
||||||
|
<Thumbnail loading={loading} cached={cached} />
|
||||||
|
</Touchable>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Video;
|
export default Video;
|
||||||
|
|
|
@ -95,7 +95,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) {
|
shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) {
|
||||||
const { isManualUnignored } = this.state;
|
const { isManualUnignored } = this.state;
|
||||||
const { threadBadgeColor, isIgnored, highlighted, previousItem } = this.props;
|
const { threadBadgeColor, isIgnored, highlighted, previousItem, autoTranslateRoom, autoTranslateLanguage } = this.props;
|
||||||
|
|
||||||
if (nextProps.highlighted !== highlighted) {
|
if (nextProps.highlighted !== highlighted) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -111,6 +112,12 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
if (nextProps.previousItem?._id !== previousItem?._id) {
|
if (nextProps.previousItem?._id !== previousItem?._id) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextProps.autoTranslateRoom !== autoTranslateRoom) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (nextProps.autoTranslateRoom !== autoTranslateRoom || nextProps.autoTranslateLanguage !== autoTranslateLanguage) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,14 +389,17 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
|
|
||||||
let message = msg;
|
let message = msg;
|
||||||
let isTranslated = false;
|
let isTranslated = false;
|
||||||
|
const otherUserMessage = u.username !== user.username;
|
||||||
// "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription
|
// "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription
|
||||||
// "autoTranslateMessage" is a toggle between "View Original" and "Translate" state
|
// "autoTranslateMessage" is a toggle between "View Original" and "Translate" state
|
||||||
if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage) {
|
if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) {
|
||||||
const messageTranslated = getMessageTranslation(item, autoTranslateLanguage);
|
const messageTranslated = getMessageTranslation(item, autoTranslateLanguage);
|
||||||
isTranslated = !!messageTranslated;
|
isTranslated = !!messageTranslated;
|
||||||
message = messageTranslated || message;
|
message = messageTranslated || message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && autoTranslateMessage !== false && otherUserMessage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContext.Provider
|
<MessageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -409,7 +419,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
|
||||||
jumpToMessage,
|
jumpToMessage,
|
||||||
threadBadgeColor,
|
threadBadgeColor,
|
||||||
toggleFollowThread,
|
toggleFollowThread,
|
||||||
replies
|
replies,
|
||||||
|
translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* @ts-ignore*/}
|
{/* @ts-ignore*/}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable complexity */
|
/* eslint-disable complexity */
|
||||||
|
import { IAttachment } from '../../definitions';
|
||||||
import { MessageTypesValues, TMessageModel } from '../../definitions/IMessage';
|
import { MessageTypesValues, TMessageModel } from '../../definitions/IMessage';
|
||||||
import I18n from '../../i18n';
|
import I18n from '../../i18n';
|
||||||
import { DISCUSSION } from './constants';
|
import { DISCUSSION } from './constants';
|
||||||
|
@ -194,3 +195,14 @@ export const getMessageTranslation = (message: TMessageModel, autoTranslateLangu
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getMessageFromAttachment = (attachment: IAttachment, translateLanguage?: string): string | undefined => {
|
||||||
|
let msg = attachment.description;
|
||||||
|
if (translateLanguage) {
|
||||||
|
const translatedMessage = attachment.translations?.[translateLanguage];
|
||||||
|
if (translatedMessage) {
|
||||||
|
msg = translatedMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return msg;
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { IUser } from './IUser';
|
import { IUser } from './IUser';
|
||||||
|
import { IAttachmentTranslations } from './IMessage';
|
||||||
|
|
||||||
export interface IAttachment {
|
export interface IAttachment {
|
||||||
ts?: string | Date;
|
ts?: string | Date;
|
||||||
|
@ -29,6 +30,7 @@ export interface IAttachment {
|
||||||
thumb_url?: string;
|
thumb_url?: string;
|
||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
audio_type?: string;
|
audio_type?: string;
|
||||||
|
translations?: IAttachmentTranslations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IServerAttachment {
|
export interface IServerAttachment {
|
||||||
|
|
|
@ -41,7 +41,7 @@ export interface IEditedBy {
|
||||||
|
|
||||||
export type TOnLinkPress = (link: string) => void;
|
export type TOnLinkPress = (link: string) => void;
|
||||||
|
|
||||||
export interface ITranslations {
|
export interface IMessageTranslations {
|
||||||
_id: string;
|
_id: string;
|
||||||
language: string;
|
language: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
@ -136,7 +136,7 @@ export interface IMessage extends IMessageFromServer {
|
||||||
replies?: string[];
|
replies?: string[];
|
||||||
unread?: boolean;
|
unread?: boolean;
|
||||||
autoTranslate?: boolean;
|
autoTranslate?: boolean;
|
||||||
translations?: ITranslations[];
|
translations?: IMessageTranslations[];
|
||||||
tmsg?: string;
|
tmsg?: string;
|
||||||
blocks?: any;
|
blocks?: any;
|
||||||
e2e?: E2EType;
|
e2e?: E2EType;
|
||||||
|
@ -243,3 +243,7 @@ export type MessageTypesValues =
|
||||||
| 'message_pinned'
|
| 'message_pinned'
|
||||||
| 'message_snippeted'
|
| 'message_snippeted'
|
||||||
| 'jitsi_call_started';
|
| 'jitsi_call_started';
|
||||||
|
|
||||||
|
export interface IAttachmentTranslations {
|
||||||
|
[k: string]: string;
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { store } from '../../store/auxStore';
|
import { store } from '../../store/auxStore';
|
||||||
import { IAttachment, IMessage } from '../../../definitions';
|
import { IAttachment, IAttachmentTranslations, IMessage } from '../../../definitions';
|
||||||
import { getAvatarURL } from '../../methods/helpers';
|
import { getAvatarURL } from '../../methods/helpers';
|
||||||
|
|
||||||
export function createQuoteAttachment(message: IMessage, messageLink: string): IAttachment {
|
export function createQuoteAttachment(message: IMessage, messageLink: string): IAttachment {
|
||||||
|
@ -8,7 +8,8 @@ export function createQuoteAttachment(message: IMessage, messageLink: string): I
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: message.msg,
|
text: message.msg,
|
||||||
...('translations' in message && { translations: message?.translations }),
|
// this type is wrong
|
||||||
|
...('translations' in message && { translations: message?.translations as unknown as IAttachmentTranslations }),
|
||||||
message_link: messageLink,
|
message_link: messageLink,
|
||||||
author_name: message.alias || message.u.username,
|
author_name: message.alias || message.u.username,
|
||||||
author_icon: getAvatarURL({
|
author_icon: getAvatarURL({
|
||||||
|
|
|
@ -100,15 +100,20 @@ const AutoTranslateView = (): React.ReactElement => {
|
||||||
title={name || language}
|
title={name || language}
|
||||||
onPress={() => saveAutoTranslateLanguage(language)}
|
onPress={() => saveAutoTranslateLanguage(language)}
|
||||||
testID={`auto-translate-view-${language}`}
|
testID={`auto-translate-view-${language}`}
|
||||||
right={() => (selectedLanguage === language ? <List.Icon name='check' color={colors.tintColor} /> : null)}
|
right={() =>
|
||||||
|
selectedLanguage === language ? (
|
||||||
|
<List.Icon testID={`auto-translate-view-${language}-check`} name='check' color={colors.tintColor} />
|
||||||
|
) : null
|
||||||
|
}
|
||||||
translateTitle={false}
|
translateTitle={false}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView testID='auto-translate-view'>
|
<SafeAreaView>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<FlatList
|
<FlatList
|
||||||
|
testID='auto-translate-view'
|
||||||
data={languages}
|
data={languages}
|
||||||
keyExtractor={item => item.name || item.language}
|
keyExtractor={item => item.name || item.language}
|
||||||
renderItem={({ item: { language, name } }) => <LanguageItem language={language} name={name} />}
|
renderItem={({ item: { language, name } }) => <LanguageItem language={language} name={name} />}
|
||||||
|
@ -117,9 +122,13 @@ const AutoTranslateView = (): React.ReactElement => {
|
||||||
<List.Separator />
|
<List.Separator />
|
||||||
<List.Item
|
<List.Item
|
||||||
title='Enable_Auto_Translate'
|
title='Enable_Auto_Translate'
|
||||||
testID='auto-translate-view-switch'
|
|
||||||
right={() => (
|
right={() => (
|
||||||
<Switch value={enableAutoTranslate} trackColor={SWITCH_TRACK_COLOR} onValueChange={toggleAutoTranslate} />
|
<Switch
|
||||||
|
testID='auto-translate-view-switch'
|
||||||
|
value={enableAutoTranslate}
|
||||||
|
trackColor={SWITCH_TRACK_COLOR}
|
||||||
|
onValueChange={toggleAutoTranslate}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<List.Separator />
|
<List.Separator />
|
||||||
|
|
|
@ -57,6 +57,8 @@ export interface IListContainerProps {
|
||||||
navigation: any; // TODO: type me
|
navigation: any; // TODO: type me
|
||||||
showMessageInMainThread: boolean;
|
showMessageInMainThread: boolean;
|
||||||
serverVersion: string | null;
|
serverVersion: string | null;
|
||||||
|
autoTranslateRoom?: boolean;
|
||||||
|
autoTranslateLanguage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IListContainerState {
|
interface IListContainerState {
|
||||||
|
@ -106,7 +108,7 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) {
|
shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) {
|
||||||
const { refreshing, highlightedMessage } = this.state;
|
const { refreshing, highlightedMessage } = this.state;
|
||||||
const { hideSystemMessages, tunread, ignored, loading } = this.props;
|
const { hideSystemMessages, tunread, ignored, loading, autoTranslateLanguage, autoTranslateRoom } = this.props;
|
||||||
if (loading !== nextProps.loading) {
|
if (loading !== nextProps.loading) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -125,6 +127,9 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
|
||||||
if (!dequal(ignored, nextProps.ignored)) {
|
if (!dequal(ignored, nextProps.ignored)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (autoTranslateLanguage !== nextProps.autoTranslateLanguage || autoTranslateRoom !== nextProps.autoTranslateRoom) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -138,7 +138,9 @@ const roomAttrsUpdate = [
|
||||||
'status',
|
'status',
|
||||||
'lastMessage',
|
'lastMessage',
|
||||||
'onHold',
|
'onHold',
|
||||||
't'
|
't',
|
||||||
|
'autoTranslate',
|
||||||
|
'autoTranslateLanguage'
|
||||||
] as TRoomUpdate[];
|
] as TRoomUpdate[];
|
||||||
|
|
||||||
interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackParamList, 'RoomView'> {
|
interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackParamList, 'RoomView'> {
|
||||||
|
@ -1491,7 +1493,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
console.count(`${this.constructor.name}.render calls`);
|
console.count(`${this.constructor.name}.render calls`);
|
||||||
const { room, loading } = this.state;
|
const { room, loading, canAutoTranslate } = this.state;
|
||||||
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, serverVersion } = this.props;
|
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, serverVersion } = this.props;
|
||||||
const { rid, t } = room;
|
const { rid, t } = room;
|
||||||
let sysMes;
|
let sysMes;
|
||||||
|
@ -1520,6 +1522,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
||||||
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
|
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
|
||||||
showMessageInMainThread={user.showMessageInMainThread ?? false}
|
showMessageInMainThread={user.showMessageInMainThread ?? false}
|
||||||
serverVersion={serverVersion}
|
serverVersion={serverVersion}
|
||||||
|
autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate}
|
||||||
|
autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}
|
||||||
/>
|
/>
|
||||||
{this.renderFooter()}
|
{this.renderFooter()}
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import axios from 'axios';
|
import axios, { AxiosInstance } from 'axios';
|
||||||
|
|
||||||
import data from '../data';
|
import data from '../data';
|
||||||
import random from './random';
|
import random from './random';
|
||||||
|
@ -161,30 +161,15 @@ export interface IDeleteCreateUser {
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteCreatedUser = async ({ server, username: usernameToDelete }: IDeleteCreateUser) => {
|
const deleteCreatedUser = async ({ username: usernameToDelete }: IDeleteCreateUser) => {
|
||||||
const serverConnection = axios.create({
|
try {
|
||||||
baseURL: `${server}/api/v1/`,
|
const api = await initApi(data.adminUser, data.adminPassword);
|
||||||
headers: {
|
const result = await api.get(`users.info?username=${usernameToDelete}`);
|
||||||
'Content-Type': 'application/json;charset=UTF-8'
|
const responsePost = await api.post('users.delete', { userId: result.data.user._id, confirmRelinquish: true });
|
||||||
}
|
return responsePost.data;
|
||||||
});
|
} catch (error) {
|
||||||
console.log(`Logging in as admin in ${server}`);
|
console.log(JSON.stringify(error));
|
||||||
const response = await serverConnection.post('login', {
|
}
|
||||||
user: data.adminUser,
|
|
||||||
password: data.adminPassword
|
|
||||||
});
|
|
||||||
const { authToken, userId } = response.data.data;
|
|
||||||
serverConnection.defaults.headers.common['X-User-Id'] = userId;
|
|
||||||
serverConnection.defaults.headers.common['X-Auth-Token'] = authToken;
|
|
||||||
|
|
||||||
console.log(`Get user info: users.info?username=${usernameToDelete}`);
|
|
||||||
const result = await serverConnection.get(`users.info?username=${usernameToDelete}`);
|
|
||||||
const userIdToDelete = result.data.user._id;
|
|
||||||
|
|
||||||
const body = { userId: userIdToDelete, confirmRelinquish: false };
|
|
||||||
console.log(`Delete user: users.delete ${JSON.stringify(body)}`);
|
|
||||||
const responsePost = await serverConnection.post('users.delete', body);
|
|
||||||
return responsePost.data;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete created users to avoid use all the Seats Available on the server
|
// Delete created users to avoid use all the Seats Available on the server
|
||||||
|
@ -195,3 +180,20 @@ export const deleteCreatedUsers = async (deleteUsersAfterAll: IDeleteCreateUser[
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initApi = async (user: string, password: string): Promise<AxiosInstance> => {
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: `${server}/api/v1/`,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json;charset=UTF-8'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const response = await api.post('login', {
|
||||||
|
user,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
const { authToken, userId } = response.data.data;
|
||||||
|
api.defaults.headers.common['X-User-Id'] = userId;
|
||||||
|
api.defaults.headers.common['X-Auth-Token'] = authToken;
|
||||||
|
return api;
|
||||||
|
};
|
|
@ -0,0 +1,227 @@
|
||||||
|
import { by, device, element, expect, waitFor } from 'detox';
|
||||||
|
|
||||||
|
import { TTextMatcher, login, navigateToLogin, platformTypes, searchRoom, tapBack, tryTapping } from '../../helpers/app';
|
||||||
|
import { ITestUser, createRandomRoom, createRandomUser, initApi } from '../../helpers/data_setup';
|
||||||
|
import random from '../../helpers/random';
|
||||||
|
|
||||||
|
const roomId = '64b846e4760e618aa9f91ab7';
|
||||||
|
|
||||||
|
const sendMessageOnTranslationTestRoom = async (msg: string): Promise<{ user: ITestUser; msgId: string }> => {
|
||||||
|
const user = await createRandomUser();
|
||||||
|
const api = await initApi(user.username, user.password);
|
||||||
|
|
||||||
|
const msgId = random();
|
||||||
|
|
||||||
|
await api.post('channels.join', { roomId, joinCode: null });
|
||||||
|
await api.post('chat.sendMessage', {
|
||||||
|
message: { _id: msgId, rid: roomId, msg, tshow: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
return { user, msgId };
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteMessageOnTranslationTestRoom = async ({ user, msgId }: { user: ITestUser; msgId: string }): Promise<void> => {
|
||||||
|
const api = await initApi(user.username, user.password);
|
||||||
|
await api.post('chat.delete', {
|
||||||
|
msgId,
|
||||||
|
roomId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
async function navigateToRoom(roomName: string) {
|
||||||
|
await searchRoom(`${roomName}`);
|
||||||
|
await element(by.id(`rooms-list-view-item-${roomName}`)).tap();
|
||||||
|
await waitFor(element(by.id('room-view')))
|
||||||
|
.toBeVisible()
|
||||||
|
.withTimeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForVisible(id: string) {
|
||||||
|
return waitFor(element(by.id(id)))
|
||||||
|
.toBeVisible()
|
||||||
|
.withTimeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForVisibleTextMatcher(msg: string, textMatcher: TTextMatcher) {
|
||||||
|
return waitFor(element(by[textMatcher](msg)).atIndex(0))
|
||||||
|
.toExist()
|
||||||
|
.withTimeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function waitForNotVisible(id: string) {
|
||||||
|
return waitFor(element(by.id(id)))
|
||||||
|
.not.toBeVisible()
|
||||||
|
.withTimeout(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Auto Translate', () => {
|
||||||
|
let textMatcher: TTextMatcher;
|
||||||
|
|
||||||
|
const languages = {
|
||||||
|
default: 'en',
|
||||||
|
translated: 'pt'
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldMessage = {
|
||||||
|
[languages.default]: 'dog',
|
||||||
|
[languages.translated]: 'cachorro'
|
||||||
|
};
|
||||||
|
|
||||||
|
const newMessage = {
|
||||||
|
[languages.default]: 'cat',
|
||||||
|
[languages.translated]: 'gato'
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachmentMessage = {
|
||||||
|
[languages.default]: 'attachment',
|
||||||
|
[languages.translated]: 'anexo'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const user = await createRandomUser();
|
||||||
|
await createRandomRoom(user);
|
||||||
|
await device.launchApp({ permissions: { notifications: 'YES' }, delete: true });
|
||||||
|
({ textMatcher } = platformTypes[device.getPlatform()]);
|
||||||
|
await navigateToLogin();
|
||||||
|
await login(user.username, user.password);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should join translation-test room', async () => {
|
||||||
|
await navigateToRoom('translation-test');
|
||||||
|
await element(by.id('room-view-join-button')).tap();
|
||||||
|
await waitForNotVisible('room-view-join-button');
|
||||||
|
await tapBack();
|
||||||
|
await navigateToRoom('translation-test');
|
||||||
|
await waitForVisible('messagebox');
|
||||||
|
await expect(element(by.id('room-view-join'))).not.toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see old message not translated before enable auto translate', async () => {
|
||||||
|
await waitForVisibleTextMatcher(oldMessage[languages.default] as string, textMatcher);
|
||||||
|
await waitForVisibleTextMatcher(attachmentMessage[languages.default] as string, textMatcher);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable auto translate', async () => {
|
||||||
|
await element(by.id('room-header')).tap();
|
||||||
|
|
||||||
|
await waitForVisible('room-actions-view');
|
||||||
|
await element(by.id('room-actions-view')).swipe('up');
|
||||||
|
|
||||||
|
await waitForVisible('room-actions-auto-translate');
|
||||||
|
await element(by.id('room-actions-auto-translate')).tap();
|
||||||
|
|
||||||
|
await waitForVisible('auto-translate-view-switch');
|
||||||
|
await element(by.id('auto-translate-view-switch')).tap();
|
||||||
|
|
||||||
|
// verify default language is checked
|
||||||
|
await waitFor(element(by.id(`auto-translate-view-${languages.default}`)))
|
||||||
|
.toBeVisible()
|
||||||
|
.whileElement(by.id('auto-translate-view'))
|
||||||
|
.scroll(750, 'down');
|
||||||
|
await waitForVisible(`auto-translate-view-${languages.default}-check`);
|
||||||
|
|
||||||
|
// enable translated language
|
||||||
|
await waitFor(element(by.id(`auto-translate-view-${languages.translated}`)))
|
||||||
|
.toBeVisible()
|
||||||
|
.whileElement(by.id('auto-translate-view'))
|
||||||
|
.scroll(750, 'down');
|
||||||
|
await waitForNotVisible(`auto-translate-view-${languages.translated}-check`);
|
||||||
|
await element(by.id(`auto-translate-view-${languages.translated}`)).tap();
|
||||||
|
await waitForVisible(`auto-translate-view-${languages.translated}-check`);
|
||||||
|
|
||||||
|
// verify default language is unchecked
|
||||||
|
await waitFor(element(by.id(`auto-translate-view-${languages.default}`)))
|
||||||
|
.toBeVisible()
|
||||||
|
.whileElement(by.id('auto-translate-view'))
|
||||||
|
.scroll(750, 'up');
|
||||||
|
await waitForNotVisible(`auto-translate-view-${languages.default}-check`);
|
||||||
|
|
||||||
|
await tapBack();
|
||||||
|
await tapBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see old message translated after enable auto translate', async () => {
|
||||||
|
await waitForVisibleTextMatcher(oldMessage[languages.translated] as string, textMatcher);
|
||||||
|
await waitForVisibleTextMatcher(attachmentMessage[languages.translated] as string, textMatcher);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see new message translated', async () => {
|
||||||
|
const randomMatcher = random();
|
||||||
|
const data = await sendMessageOnTranslationTestRoom(`${newMessage[languages.default]} - ${randomMatcher}`);
|
||||||
|
await waitForVisibleTextMatcher(`${newMessage[languages.translated]} - ${randomMatcher}`, textMatcher);
|
||||||
|
await deleteMessageOnTranslationTestRoom(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see original message', async () => {
|
||||||
|
const randomMatcher = random();
|
||||||
|
const data = await sendMessageOnTranslationTestRoom(`${newMessage[languages.default]} - ${randomMatcher}`);
|
||||||
|
await waitForVisibleTextMatcher(`${newMessage[languages.translated]} - ${randomMatcher}`, textMatcher);
|
||||||
|
|
||||||
|
await tryTapping(element(by[textMatcher](`${newMessage[languages.translated]} - ${randomMatcher}`)).atIndex(0), 2000, true);
|
||||||
|
|
||||||
|
await waitForVisible('action-sheet-handle');
|
||||||
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
|
||||||
|
await waitForVisibleTextMatcher('View original', textMatcher);
|
||||||
|
await element(by[textMatcher]('View original')).atIndex(0).tap();
|
||||||
|
|
||||||
|
await waitForVisibleTextMatcher(`${newMessage[languages.default]} - ${randomMatcher}`, textMatcher);
|
||||||
|
|
||||||
|
await deleteMessageOnTranslationTestRoom(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disable auto translate and see original message', async () => {
|
||||||
|
const randomMatcher = random();
|
||||||
|
const data = await sendMessageOnTranslationTestRoom(`${newMessage[languages.default]} - ${randomMatcher}`);
|
||||||
|
|
||||||
|
await waitForVisibleTextMatcher(`${newMessage[languages.translated]} - ${randomMatcher}`, textMatcher);
|
||||||
|
|
||||||
|
await element(by.id('room-header')).tap();
|
||||||
|
await waitForVisible('room-actions-view');
|
||||||
|
await element(by.id('room-actions-view')).swipe('up');
|
||||||
|
|
||||||
|
await waitForVisible('room-actions-auto-translate');
|
||||||
|
await element(by.id('room-actions-auto-translate')).tap();
|
||||||
|
|
||||||
|
await waitForVisible('auto-translate-view-switch');
|
||||||
|
await element(by.id('auto-translate-view-switch')).tap();
|
||||||
|
|
||||||
|
await tapBack();
|
||||||
|
await tapBack();
|
||||||
|
|
||||||
|
await waitForVisibleTextMatcher(`${newMessage[languages.default]} - ${randomMatcher}`, textMatcher);
|
||||||
|
|
||||||
|
await deleteMessageOnTranslationTestRoom(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should don't see action to View original when disable auto translate`, async () => {
|
||||||
|
await waitForVisibleTextMatcher(oldMessage[languages.default] as string, textMatcher);
|
||||||
|
await tryTapping(element(by[textMatcher](oldMessage[languages.default] as string)).atIndex(0), 2000, true);
|
||||||
|
|
||||||
|
await waitForVisible('action-sheet-handle');
|
||||||
|
await element(by.id('action-sheet-handle')).swipe('up', 'fast', 0.5);
|
||||||
|
|
||||||
|
await waitForNotVisible('View original');
|
||||||
|
// close action sheet
|
||||||
|
await element(by.id('room-header')).tap();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should the language selected when activating auto translate again must be the old one', async () => {
|
||||||
|
await element(by.id('room-header')).tap();
|
||||||
|
await waitForVisible('room-actions-view');
|
||||||
|
await element(by.id('room-actions-view')).swipe('up');
|
||||||
|
|
||||||
|
await waitForVisible('room-actions-auto-translate');
|
||||||
|
await element(by.id('room-actions-auto-translate')).tap();
|
||||||
|
|
||||||
|
await waitForVisible('auto-translate-view-switch');
|
||||||
|
await element(by.id('auto-translate-view-switch')).tap();
|
||||||
|
|
||||||
|
// verify translated language is checked and is the old one
|
||||||
|
await waitFor(element(by.id(`auto-translate-view-${languages.translated}`)))
|
||||||
|
.toBeVisible()
|
||||||
|
.whileElement(by.id('auto-translate-view'))
|
||||||
|
.scroll(750, 'down');
|
||||||
|
await waitForVisible(`auto-translate-view-${languages.translated}-check`);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue