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:
Gleidson Daniel Silva 2023-08-14 17:22:46 -03:00 committed by GitHub
parent 0c8b2f565e
commit acbcac29c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 564 additions and 287 deletions

View File

@ -322,7 +322,7 @@ const MessageActions = React.memo(
const db = database.active;
await db.write(async () => {
await message.update(m => {
m.autoTranslate = !m.autoTranslate;
m.autoTranslate = m.autoTranslate !== null ? !m.autoTranslate : false;
m._updatedAt = new Date();
});
});
@ -479,7 +479,7 @@ const MessageActions = React.memo(
// Toggle Auto-translate
if (room.autoTranslate && message.u && message.u._id !== user.id) {
options.push({
title: I18n.t(message.autoTranslate ? 'View_Original' : 'Translate'),
title: I18n.t(message.autoTranslate !== false ? 'View_Original' : 'Translate'),
icon: 'language',
onPress: () => handleToggleTranslation(message)
});

View File

@ -13,6 +13,7 @@ import { IAttachment, TGetCustomEmoji } from '../../definitions';
import CollapsibleQuote from './Components/CollapsibleQuote';
import openLink from '../../lib/methods/helpers/openLink';
import Markdown from '../markdown';
import { getMessageFromAttachment } from './utils';
export type TElement = {
type: string;
@ -56,12 +57,14 @@ const AttachedActions = ({ attachment, getCustomEmoji }: { attachment: IAttachme
const Attachments: React.FC<IMessageAttachments> = React.memo(
({ attachments, timeFormat, showAttachment, style, getCustomEmoji, isReply, author }: IMessageAttachments) => {
const { theme } = useTheme();
const { translateLanguage } = useContext(MessageContext);
if (!attachments || attachments.length === 0) {
return null;
}
const attachmentsElements = attachments.map((file: IAttachment, index: number) => {
const msg = getMessageFromAttachment(file, translateLanguage);
if (file && file.image_url) {
return (
<Image
@ -72,6 +75,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
style={style}
isReply={isReply}
author={author}
msg={msg}
/>
);
}
@ -86,6 +90,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
style={style}
theme={theme}
author={author}
msg={msg}
/>
);
}
@ -99,6 +104,7 @@ const Attachments: React.FC<IMessageAttachments> = React.memo(
getCustomEmoji={getCustomEmoji}
style={style}
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}</>;
},

View File

@ -40,6 +40,7 @@ interface IMessageAudioProps {
getCustomEmoji: TGetCustomEmoji;
scale?: number;
author?: IUserMessage;
msg?: string;
}
interface IMessageAudioState {
@ -348,8 +349,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
render() {
const { loading, paused, currentTime, duration, cached } = this.state;
const { file, getCustomEmoji, theme, scale, isReply, style } = this.props;
const { description } = file;
const { msg, getCustomEmoji, theme, scale, isReply, style } = this.props;
// @ts-ignore can't use declare to type this
const { baseUrl, user } = this.context;
@ -367,7 +367,7 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
return (
<>
<Markdown
msg={description}
msg={msg}
style={[isReply && style]}
username={user.username}
getCustomEmoji={getCustomEmoji}

View File

@ -1,19 +1,18 @@
import React, { useContext, useEffect, useState } from 'react';
import { StyleProp, TextStyle, View } from 'react-native';
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 { useTheme } from '../../theme';
import { formatAttachmentUrl } from '../../lib/methods/helpers/formatAttachmentUrl';
import { cancelDownload, downloadMediaFile, isDownloadActive, getMediaCache } from '../../lib/methods/handleMediaDownload';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
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 MessageContext from './Context';
import Touchable from './Touchable';
import styles from './styles';
interface IMessageButton {
children: React.ReactElement;
@ -29,6 +28,7 @@ interface IMessageImage {
isReply?: boolean;
getCustomEmoji?: TGetCustomEmoji;
author?: IUserMessage;
msg?: string;
}
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(
({ file, imageUrl, showAttachment, getCustomEmoji, style, isReply, author }: IMessageImage) => {
const [imageCached, setImageCached] = useState(file);
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);
const ImageContainer = ({
file,
imageUrl,
showAttachment,
getCustomEmoji,
style,
isReply,
author,
msg
}: IMessageImage): React.ReactElement | null => {
const [imageCached, setImageCached] = useState(file);
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(() => {
const handleCache = async () => {
if (img) {
const cachedImageResult = await getMediaCache({
type: 'image',
mimeType: imageCached.image_type,
urlToCache: imgUrlToCache
});
if (cachedImageResult?.exists) {
setImageCached(prev => ({
...prev,
title_link: cachedImageResult?.uri
}));
setLoading(false);
setCached(true);
return;
}
if (isReply) {
setLoading(false);
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,
useEffect(() => {
const handleCache = async () => {
if (img) {
const cachedImageResult = await getMediaCache({
type: 'image',
mimeType: imageCached.image_type
mimeType: imageCached.image_type,
urlToCache: imgUrlToCache
});
setImageCached(prev => ({
...prev,
title_link: imageUri
}));
setCached(true);
} catch (e) {
setCached(false);
} finally {
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();
}, []);
const onPress = () => {
if (loading && isDownloadActive(imgUrlToCache)) {
cancelDownload(imgUrlToCache);
setLoading(false);
setCached(false);
return;
}
if (!cached && !loading) {
handleDownload();
return;
}
if (!showAttachment) {
return;
}
showAttachment(imageCached);
};
if (!img) {
return null;
}
if (imageCached.description) {
return (
<View>
<Markdown
msg={imageCached.description}
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 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',
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 (
<Button disabled={isReply} onPress={onPress}>
<MessageImage imgUri={img} cached={cached} loading={loading} />
</Button>
<View>
<Markdown msg={msg} style={[isReply && style]} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
<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';
MessageImage.displayName = 'MessageImage';

View File

@ -91,6 +91,7 @@ interface IMessageReply {
timeFormat?: string;
index: number;
getCustomEmoji: TGetCustomEmoji;
msg?: string;
}
const Title = React.memo(
@ -197,7 +198,7 @@ const Fields = React.memo(
);
const Reply = React.memo(
({ attachment, timeFormat, index, getCustomEmoji }: IMessageReply) => {
({ attachment, timeFormat, index, getCustomEmoji, msg }: IMessageReply) => {
const [loading, setLoading] = useState(false);
const { theme } = useTheme();
const { baseUrl, user, jumpToMessage } = useContext(MessageContext);
@ -238,7 +239,7 @@ const Reply = React.memo(
style={[
styles.button,
index > 0 && styles.marginTop,
attachment.description && styles.marginBottom,
msg && styles.marginBottom,
{
borderColor
}
@ -271,7 +272,7 @@ const Reply = React.memo(
) : null}
</View>
</Touchable>
<Markdown msg={attachment.description} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
<Markdown msg={msg} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</>
);
},

View File

@ -1,6 +1,5 @@
import React, { useContext, useEffect, useState } from 'react';
import { StyleProp, StyleSheet, TextStyle, View, Text } from 'react-native';
import { dequal } from 'dequal';
import FastImage from 'react-native-fast-image';
import messageStyles from './styles';
@ -56,6 +55,7 @@ interface IMessageVideo {
getCustomEmoji: TGetCustomEmoji;
style?: StyleProp<TextStyle>[];
isReply?: boolean;
msg?: string;
}
const CancelIndicator = () => {
@ -81,139 +81,130 @@ const Thumbnail = ({ loading, thumbnailUrl, cached }: { loading: boolean; thumbn
</>
);
const Video = React.memo(
({ file, showAttachment, getCustomEmoji, style, isReply }: IMessageVideo) => {
const [videoCached, setVideoCached] = useState(file);
const [loading, setLoading] = useState(true);
const [cached, setCached] = useState(false);
const { baseUrl, user } = useContext(MessageContext);
const { theme } = useTheme();
const video = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
const Video = ({ file, showAttachment, getCustomEmoji, style, isReply, msg }: IMessageVideo): React.ReactElement | null => {
const [videoCached, setVideoCached] = useState(file);
const [loading, setLoading] = useState(true);
const [cached, setCached] = useState(false);
const { baseUrl, user } = useContext(MessageContext);
const { theme } = useTheme();
const video = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
useEffect(() => {
const handleVideoSearchAndDownload = async () => {
if (video) {
const cachedVideoResult = await getMediaCache({
type: 'video',
mimeType: file.video_type,
urlToCache: video
});
const downloadActive = isDownloadActive(video);
if (cachedVideoResult?.exists) {
setVideoCached(prev => ({
...prev,
video_url: cachedVideoResult?.uri
}));
setLoading(false);
setCached(true);
if (downloadActive) {
cancelDownload(video);
}
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,
useEffect(() => {
const handleVideoSearchAndDownload = async () => {
if (video) {
const cachedVideoResult = await getMediaCache({
type: 'video',
mimeType: file.video_type
mimeType: file.video_type,
urlToCache: video
});
setVideoCached(prev => ({
...prev,
video_url: videoUri
}));
setCached(true);
} catch {
setCached(false);
} finally {
setLoading(false);
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();
}, []);
const onPress = async () => {
if (file.video_type && cached && isTypeSupported(file.video_type) && showAttachment) {
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') });
};
if (!baseUrl) {
return null;
}
const handleCancelDownload = () => {
if (loading) {
cancelDownload(video);
setLoading(false);
}
};
const handleAutoDownload = async () => {
const isAutoDownloadEnabled = fetchAutoDownloadEnabled('videoPreferenceDownload');
if (isAutoDownloadEnabled && file.video_type && isTypeSupported(file.video_type)) {
await handleDownload();
return;
}
setLoading(false);
};
const downloadVideoToGallery = async (uri: string) => {
setLoading(true);
const fileDownloaded = await fileDownload(uri, file);
const handleDownload = async () => {
setLoading(true);
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);
}
};
if (fileDownloaded) {
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
return;
}
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') });
};
const onPress = async () => {
if (file.video_type && cached && isTypeSupported(file.video_type) && showAttachment) {
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') });
};
return (
<>
<Markdown
msg={file.description}
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>
</>
);
},
(prevProps, nextProps) => dequal(prevProps.file, nextProps.file)
);
const handleCancelDownload = () => {
if (loading) {
cancelDownload(video);
setLoading(false);
}
};
const downloadVideoToGallery = async (uri: string) => {
setLoading(true);
const fileDownloaded = await fileDownload(uri, file);
setLoading(false);
if (fileDownloaded) {
EventEmitter.emit(LISTENER, { message: I18n.t('saved_to_gallery') });
return;
}
EventEmitter.emit(LISTENER, { message: I18n.t('error-save-video') });
};
return (
<>
<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;

View File

@ -95,7 +95,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
shouldComponentUpdate(nextProps: IMessageContainerProps, nextState: IMessageContainerState) {
const { isManualUnignored } = this.state;
const { threadBadgeColor, isIgnored, highlighted, previousItem } = this.props;
const { threadBadgeColor, isIgnored, highlighted, previousItem, autoTranslateRoom, autoTranslateLanguage } = this.props;
if (nextProps.highlighted !== highlighted) {
return true;
}
@ -111,6 +112,12 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
if (nextProps.previousItem?._id !== previousItem?._id) {
return true;
}
if (nextProps.autoTranslateRoom !== autoTranslateRoom) {
return true;
}
if (nextProps.autoTranslateRoom !== autoTranslateRoom || nextProps.autoTranslateLanguage !== autoTranslateLanguage) {
return true;
}
return false;
}
@ -382,14 +389,17 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
let message = msg;
let isTranslated = false;
const otherUserMessage = u.username !== user.username;
// "autoTranslateRoom" and "autoTranslateLanguage" are properties from the subscription
// "autoTranslateMessage" is a toggle between "View Original" and "Translate" state
if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage) {
if (autoTranslateRoom && autoTranslateMessage && autoTranslateLanguage && otherUserMessage) {
const messageTranslated = getMessageTranslation(item, autoTranslateLanguage);
isTranslated = !!messageTranslated;
message = messageTranslated || message;
}
const canTranslateMessage = autoTranslateRoom && autoTranslateLanguage && autoTranslateMessage !== false && otherUserMessage;
return (
<MessageContext.Provider
value={{
@ -409,7 +419,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, IMessageC
jumpToMessage,
threadBadgeColor,
toggleFollowThread,
replies
replies,
translateLanguage: canTranslateMessage ? autoTranslateLanguage : undefined
}}
>
{/* @ts-ignore*/}

View File

@ -1,4 +1,5 @@
/* eslint-disable complexity */
import { IAttachment } from '../../definitions';
import { MessageTypesValues, TMessageModel } from '../../definitions/IMessage';
import I18n from '../../i18n';
import { DISCUSSION } from './constants';
@ -194,3 +195,14 @@ export const getMessageTranslation = (message: TMessageModel, autoTranslateLangu
}
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;
};

View File

@ -1,4 +1,5 @@
import { IUser } from './IUser';
import { IAttachmentTranslations } from './IMessage';
export interface IAttachment {
ts?: string | Date;
@ -29,6 +30,7 @@ export interface IAttachment {
thumb_url?: string;
collapsed?: boolean;
audio_type?: string;
translations?: IAttachmentTranslations;
}
export interface IServerAttachment {

View File

@ -41,7 +41,7 @@ export interface IEditedBy {
export type TOnLinkPress = (link: string) => void;
export interface ITranslations {
export interface IMessageTranslations {
_id: string;
language: string;
value: string;
@ -136,7 +136,7 @@ export interface IMessage extends IMessageFromServer {
replies?: string[];
unread?: boolean;
autoTranslate?: boolean;
translations?: ITranslations[];
translations?: IMessageTranslations[];
tmsg?: string;
blocks?: any;
e2e?: E2EType;
@ -243,3 +243,7 @@ export type MessageTypesValues =
| 'message_pinned'
| 'message_snippeted'
| 'jitsi_call_started';
export interface IAttachmentTranslations {
[k: string]: string;
}

View File

@ -1,5 +1,5 @@
import { store } from '../../store/auxStore';
import { IAttachment, IMessage } from '../../../definitions';
import { IAttachment, IAttachmentTranslations, IMessage } from '../../../definitions';
import { getAvatarURL } from '../../methods/helpers';
export function createQuoteAttachment(message: IMessage, messageLink: string): IAttachment {
@ -8,7 +8,8 @@ export function createQuoteAttachment(message: IMessage, messageLink: string): I
return {
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,
author_name: message.alias || message.u.username,
author_icon: getAvatarURL({

View File

@ -100,15 +100,20 @@ const AutoTranslateView = (): React.ReactElement => {
title={name || language}
onPress={() => saveAutoTranslateLanguage(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}
/>
));
return (
<SafeAreaView testID='auto-translate-view'>
<SafeAreaView>
<StatusBar />
<FlatList
testID='auto-translate-view'
data={languages}
keyExtractor={item => item.name || item.language}
renderItem={({ item: { language, name } }) => <LanguageItem language={language} name={name} />}
@ -117,9 +122,13 @@ const AutoTranslateView = (): React.ReactElement => {
<List.Separator />
<List.Item
title='Enable_Auto_Translate'
testID='auto-translate-view-switch'
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 />

View File

@ -57,6 +57,8 @@ export interface IListContainerProps {
navigation: any; // TODO: type me
showMessageInMainThread: boolean;
serverVersion: string | null;
autoTranslateRoom?: boolean;
autoTranslateLanguage?: string;
}
interface IListContainerState {
@ -106,7 +108,7 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
shouldComponentUpdate(nextProps: IListContainerProps, nextState: IListContainerState) {
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) {
return true;
}
@ -125,6 +127,9 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
if (!dequal(ignored, nextProps.ignored)) {
return true;
}
if (autoTranslateLanguage !== nextProps.autoTranslateLanguage || autoTranslateRoom !== nextProps.autoTranslateRoom) {
return true;
}
return false;
}

View File

@ -138,7 +138,9 @@ const roomAttrsUpdate = [
'status',
'lastMessage',
'onHold',
't'
't',
'autoTranslate',
'autoTranslateLanguage'
] as TRoomUpdate[];
interface IRoomViewProps extends IActionSheetProvider, IBaseScreen<ChatsStackParamList, 'RoomView'> {
@ -1491,7 +1493,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
render() {
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 { rid, t } = room;
let sysMes;
@ -1520,6 +1522,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
showMessageInMainThread={user.showMessageInMainThread ?? false}
serverVersion={serverVersion}
autoTranslateRoom={canAutoTranslate && 'id' in room && room.autoTranslate}
autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined}
/>
{this.renderFooter()}
{this.renderActions()}

View File

@ -1,4 +1,4 @@
import axios from 'axios';
import axios, { AxiosInstance } from 'axios';
import data from '../data';
import random from './random';
@ -161,30 +161,15 @@ export interface IDeleteCreateUser {
username: string;
}
const deleteCreatedUser = async ({ server, username: usernameToDelete }: IDeleteCreateUser) => {
const serverConnection = axios.create({
baseURL: `${server}/api/v1/`,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
});
console.log(`Logging in as admin in ${server}`);
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;
const deleteCreatedUser = async ({ username: usernameToDelete }: IDeleteCreateUser) => {
try {
const api = await initApi(data.adminUser, data.adminPassword);
const result = await api.get(`users.info?username=${usernameToDelete}`);
const responsePost = await api.post('users.delete', { userId: result.data.user._id, confirmRelinquish: true });
return responsePost.data;
} catch (error) {
console.log(JSON.stringify(error));
}
};
// 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;
};

View File

@ -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`);
});
});