diff --git a/src/i18n/en.json b/src/i18n/en.json
index 53b1c687a..5260b89a3 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -238,8 +238,13 @@
"LONG_PRESS_ACTIONS": {
"COPY": "Copy",
"REPLY": "Reply",
- "DELETE_MESSAGE": "Delete message"
+ "DELETE_MESSAGE": "Delete message",
+ "TRANSLATE": "Translate"
},
+ "TRANSLATE_MESSAGE_SUCCESS": "Message translated",
+ "TRANSLATE_MESSAGE_ERROR": "Failed to translate message",
+ "VIEW_ORIGINAL": "View original",
+ "VIEW_TRANSLATED": "View translated",
"EMAIL_HEADER": {
"FROM": "From",
"TO": "To",
diff --git a/src/screens/chat-screen/components/message-components/TextBubble.tsx b/src/screens/chat-screen/components/message-components/TextBubble.tsx
index 41028866d..921fc4bf0 100644
--- a/src/screens/chat-screen/components/message-components/TextBubble.tsx
+++ b/src/screens/chat-screen/components/message-components/TextBubble.tsx
@@ -1,9 +1,11 @@
-import React from 'react';
+import React, { useState } from 'react';
+import { Pressable } from 'react-native';
import Animated from 'react-native-reanimated';
import { tailwind } from '@/theme';
import { Message } from '@/types';
import { MarkdownBubble } from './MarkdownBubble';
import { EmailMeta } from './EmailMeta';
+import i18n from '@/i18n';
export type TextBubbleProps = {
item: Message;
@@ -16,6 +18,36 @@ export const TextBubble = (props: TextBubbleProps) => {
const { private: isPrivate, content, contentAttributes, sender } = messageItem;
+ const [showOriginal, setShowOriginal] = useState(false);
+
+ const translations = contentAttributes?.translations;
+ const hasTranslations = translations && Object.keys(translations).length > 0;
+ const translationContent = hasTranslations
+ ? Object.values(translations)[0]
+ : null;
+
+ const displayContent =
+ hasTranslations && !showOriginal && translationContent
+ ? translationContent
+ : content;
+
+ const renderTranslationToggle = () => {
+ if (!hasTranslations) return null;
+
+ return (
+ setShowOriginal(!showOriginal)}>
+
+ {showOriginal
+ ? i18n.t('CONVERSATION.VIEW_TRANSLATED')
+ : i18n.t('CONVERSATION.VIEW_ORIGINAL')}
+
+
+ );
+ };
+
return (
{contentAttributes && }
@@ -23,11 +55,15 @@ export const TextBubble = (props: TextBubbleProps) => {
-
+
+ {renderTranslationToggle()}
) : (
-
+ <>
+
+ {renderTranslationToggle()}
+ >
)}
);
diff --git a/src/screens/chat-screen/components/message-item/MessageItemContainer.tsx b/src/screens/chat-screen/components/message-item/MessageItemContainer.tsx
index 6e0ac1c2f..cc12cefb6 100644
--- a/src/screens/chat-screen/components/message-item/MessageItemContainer.tsx
+++ b/src/screens/chat-screen/components/message-item/MessageItemContainer.tsx
@@ -1,18 +1,16 @@
-import React from 'react';
+import React, { useState } from 'react';
import { Message } from '@/types';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { selectConversationById } from '@/store/conversation/conversationSelectors';
+import { selectLocale } from '@/store/settings/settingsSelectors';
import { useChatWindowContext } from '@/context';
-// import { setQuoteMessage } from '@/store/conversation/sendMessageSlice';
import { conversationActions } from '@/store/conversation/conversationActions';
import { useHaptic } from '@/utils';
-// import { inboxHasFeature, is360DialogWhatsAppChannel, useHaptic } from '@/utils';
-// import { INBOX_FEATURES } from '@/constants';
import { showToast } from '@/utils/toastUtils';
import i18n from '@/i18n';
import Clipboard from '@react-native-clipboard/clipboard';
import { MESSAGE_TYPES } from '@/constants';
-import { CopyIcon, Trash } from '@/svg-icons';
+import { CopyIcon, Trash, TranslateIcon } from '@/svg-icons';
import { MenuOption } from '../message-menu';
import { MessageItem } from './MessageItem';
@@ -27,10 +25,8 @@ export const MessageItemContainer = (props: MessageItemContainerProps) => {
const hapticSelection = useHaptic();
const conversation = useAppSelector(state => selectConversationById(state, conversationId));
-
- // const handleQuoteReplyAttachment = () => {
- // dispatch(setQuoteMessage(props.item as Message));
- // };
+ const locale = useAppSelector(selectLocale);
+ const [translatingMessageId, setTranslatingMessageId] = useState(null);
const handleCopyMessage = (content: string) => {
hapticSelection?.();
@@ -45,22 +41,29 @@ export const MessageItemContainer = (props: MessageItemContainerProps) => {
showToast({ message: i18n.t('CONVERSATION.DELETE_MESSAGE_SUCCESS') });
};
- // const inboxSupportsReplyTo = (channel: string) => {
- // const incoming = inboxHasFeature(INBOX_FEATURES.REPLY_TO, channel);
- // const outgoing =
- // inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING, channel) &&
- // !is360DialogWhatsAppChannel(channel);
-
- // return { incoming, outgoing };
- // };
+ const handleTranslateMessage = async (messageId: number) => {
+ hapticSelection?.();
+ setTranslatingMessageId(messageId);
+ try {
+ await dispatch(
+ conversationActions.translateMessage({
+ conversationId,
+ messageId,
+ targetLanguage: locale || 'en',
+ }),
+ ).unwrap();
+ showToast({ message: i18n.t('CONVERSATION.TRANSLATE_MESSAGE_SUCCESS') });
+ } catch {
+ showToast({ message: i18n.t('CONVERSATION.TRANSLATE_MESSAGE_ERROR') });
+ } finally {
+ setTranslatingMessageId(null);
+ }
+ };
const getMenuOptions = (message: Message): MenuOption[] => {
const { messageType, content, attachments } = message;
- // const { private: isPrivate } = message;
const hasText = !!content;
const hasAttachments = !!(attachments && attachments.length > 0);
- // const channel = conversation?.meta?.channel;
-
const isDeleted = message.contentAttributes?.deleted;
const menuOptions: MenuOption[] = [];
@@ -68,6 +71,11 @@ export const MessageItemContainer = (props: MessageItemContainerProps) => {
return [];
}
+ const hasTranslations =
+ message.contentAttributes?.translations &&
+ Object.keys(message.contentAttributes.translations).length > 0;
+ const isTranslating = translatingMessageId === message.id;
+
if (hasText) {
menuOptions.push({
title: i18n.t('CONVERSATION.LONG_PRESS_ACTIONS.COPY'),
@@ -75,18 +83,16 @@ export const MessageItemContainer = (props: MessageItemContainerProps) => {
handleOnPressMenuOption: () => handleCopyMessage(content),
destructive: false,
});
+ if (!hasTranslations && !isTranslating) {
+ menuOptions.push({
+ title: i18n.t('CONVERSATION.LONG_PRESS_ACTIONS.TRANSLATE'),
+ icon: ,
+ handleOnPressMenuOption: () => handleTranslateMessage(message.id),
+ destructive: false,
+ });
+ }
}
- // TODO: Add reply to message when we have the feature
- // if (!isPrivate && channel && inboxSupportsReplyTo(channel).outgoing) {
- // menuOptions.push({
- // title: i18n.t('CONVERSATION.LONG_PRESS_ACTIONS.REPLY'),
- // icon: null,
- // handleOnPressMenuOption: handleQuoteReplyAttachment,
- // destructive: false,
- // });
- // }
-
if (hasAttachments || hasText) {
menuOptions.push({
title: i18n.t('CONVERSATION.LONG_PRESS_ACTIONS.DELETE_MESSAGE'),
diff --git a/src/store/conversation/conversationActions.ts b/src/store/conversation/conversationActions.ts
index ad56f5fc5..a41c7cb35 100644
--- a/src/store/conversation/conversationActions.ts
+++ b/src/store/conversation/conversationActions.ts
@@ -25,6 +25,7 @@ import type {
SendMessageAPIResponse,
SendMessagePayload,
TogglePriorityPayload,
+ TranslateMessagePayload,
} from './conversationTypes';
import { AxiosError } from 'axios';
import { MESSAGE_STATUS } from '@/constants';
@@ -254,4 +255,18 @@ export const conversationActions = {
return await ConversationService.togglePriority(payload);
},
),
+ translateMessage: createAsyncThunk(
+ 'conversations/translateMessage',
+ async (payload, { rejectWithValue }) => {
+ try {
+ await ConversationService.translateMessage(payload);
+ } catch (error) {
+ const { response } = error as AxiosError;
+ if (!response) {
+ throw error;
+ }
+ return rejectWithValue(response.data);
+ }
+ },
+ ),
};
diff --git a/src/store/conversation/conversationService.ts b/src/store/conversation/conversationService.ts
index 0012de0c4..c474f9b39 100644
--- a/src/store/conversation/conversationService.ts
+++ b/src/store/conversation/conversationService.ts
@@ -29,6 +29,7 @@ import type {
MarkMessageReadOrUnreadResponse,
ToggleConversationStatusAPIResponse,
TogglePriorityPayload,
+ TranslateMessagePayload,
} from './conversationTypes';
import {
@@ -212,4 +213,11 @@ export class ConversationService {
const { conversationId, priority } = payload;
await apiService.post(`conversations/${conversationId}/toggle_priority`, { priority });
}
+
+ static async translateMessage(payload: TranslateMessagePayload): Promise {
+ const { conversationId, messageId, targetLanguage } = payload;
+ await apiService.post(`conversations/${conversationId}/messages/${messageId}/translate`, {
+ target_language: targetLanguage,
+ });
+ }
}
diff --git a/src/store/conversation/conversationTypes.ts b/src/store/conversation/conversationTypes.ts
index 4650f320e..b88ba8613 100644
--- a/src/store/conversation/conversationTypes.ts
+++ b/src/store/conversation/conversationTypes.ts
@@ -238,3 +238,9 @@ export interface TogglePriorityPayload {
conversationId: number;
priority: ConversationPriority;
}
+
+export interface TranslateMessagePayload {
+ conversationId: number;
+ messageId: number;
+ targetLanguage: string;
+}
diff --git a/src/types/Message.ts b/src/types/Message.ts
index 71e3bb876..e7f28fd2b 100644
--- a/src/types/Message.ts
+++ b/src/types/Message.ts
@@ -63,6 +63,7 @@ export type MessageContentAttributes = {
imageType: string;
contentType: ContentType;
isUnsupported: boolean;
+ translations?: Record;
};
export interface Message {