diff --git a/app/components/post_list/post/body/content/permalink_preview/index.test.tsx b/app/components/post_list/post/body/content/permalink_preview/index.test.tsx index ec55c5681..1d1999feb 100644 --- a/app/components/post_list/post/body/content/permalink_preview/index.test.tsx +++ b/app/components/post_list/post/body/content/permalink_preview/index.test.tsx @@ -136,6 +136,7 @@ describe('PermalinkPreview Enhanced Component', () => { expect(permalinkPreview.props.author).toBeDefined(); expect(permalinkPreview.props.currentUser).toBeDefined(); expect(permalinkPreview.props.isMilitaryTime).toBe(false); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); }); }); @@ -169,6 +170,7 @@ describe('PermalinkPreview Enhanced Component', () => { expect(permalinkPreview.props.author).toBeUndefined(); expect(permalinkPreview.props.currentUser).toBeDefined(); expect(permalinkPreview.props.isMilitaryTime).toBe(false); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); }); }); @@ -209,6 +211,7 @@ describe('PermalinkPreview Enhanced Component', () => { expect(permalinkPreview.props.teammateNameDisplay).toBe('username'); expect(permalinkPreview.props.currentUser).toBeDefined(); expect(permalinkPreview.props.isMilitaryTime).toBe(false); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); }); }); @@ -241,6 +244,7 @@ describe('PermalinkPreview Enhanced Component', () => { expect(permalinkPreview.props.author).toBeUndefined(); expect(permalinkPreview.props.currentUser).toBeDefined(); expect(permalinkPreview.props.isMilitaryTime).toBe(false); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); }); }); @@ -281,6 +285,7 @@ describe('PermalinkPreview Enhanced Component', () => { expect(permalinkPreview.props.teammateNameDisplay).toBe('full_name'); expect(permalinkPreview.props.currentUser).toBeDefined(); expect(permalinkPreview.props.isMilitaryTime).toBe(false); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); }); }); @@ -319,6 +324,58 @@ describe('PermalinkPreview Enhanced Component', () => { await waitFor(() => { const permalinkPreview = getByTestId('permalink-preview'); expect(permalinkPreview.props.isMilitaryTime).toBe(true); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(false); + }); + }); + + it('should pass autotranslationsEnabled true when channel has autotranslation enabled', async () => { + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + + const channelId = 'channel-123'; + const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: true}); + const myChannel = TestHelper.fakeMyChannel({ + id: channelId, + channel_id: channelId, + user_id: 'current-user', + autotranslation_disabled: false, + }); + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + await operator.handleMyChannel({channels: [channel], myChannels: [myChannel], prepareRecordsOnly: false}); + + const embedData: PermalinkEmbedData = { + post_id: 'post-123', + post: TestHelper.fakePost({ + id: 'post-123', + user_id: 'author-user', + message: 'Test message', + channel_id: channelId, + }), + team_name: 'test-team', + channel_display_name: 'Test Channel', + channel_type: 'O', + channel_id: channelId, + }; + + await createPostWithPermalinkEmbed(operator, database, embedData); + + const {getByTestId} = renderWithEverything( + , + {database, serverUrl}, + ); + + await waitFor(() => { + const permalinkPreview = getByTestId('permalink-preview'); + expect(permalinkPreview.props.autotranslationsEnabled).toBe(true); }); }); }); diff --git a/app/components/post_list/post/body/content/permalink_preview/index.tsx b/app/components/post_list/post/body/content/permalink_preview/index.tsx index e67c492f3..cbb74aeb6 100644 --- a/app/components/post_list/post/body/content/permalink_preview/index.tsx +++ b/app/components/post_list/post/body/content/permalink_preview/index.tsx @@ -6,6 +6,7 @@ import {of as of$} from 'rxjs'; import {map, switchMap, distinctUntilChanged} from 'rxjs/operators'; import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference'; +import {observeIsChannelAutotranslated} from '@queries/servers/channel'; import {observePost} from '@queries/servers/post'; import {queryDisplayNamePreferences} from '@queries/servers/preference'; import {observeUser, observeTeammateNameDisplay, observeCurrentUser} from '@queries/servers/user'; @@ -27,12 +28,17 @@ const enhance = withObservables(['embedData'], ({database, embedData}: WithDatab const post = embedData?.post_id ? observePost(database, embedData.post_id) : of$(undefined); const isOriginPostDeleted = post.pipe( switchMap((p) => { - const initialDeleted = Boolean(embedData?.post?.delete_at > 0 || embedData?.post?.state === 'DELETED'); + const deleteAt = embedData?.post?.delete_at ?? 0; + const initialDeleted = Boolean(deleteAt > 0 || embedData?.post?.state === 'DELETED'); return of$(p ? p.deleteAt > 0 : initialDeleted); }), distinctUntilChanged(), ); + const channelId = embedData?.post?.channel_id; + + const autotranslationsEnabled = channelId ? observeIsChannelAutotranslated(database, channelId) : of$(false); + return { teammateNameDisplay, currentUser, @@ -40,6 +46,7 @@ const enhance = withObservables(['embedData'], ({database, embedData}: WithDatab author, post, isOriginPostDeleted, + autotranslationsEnabled, }; }); diff --git a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.test.tsx b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.test.tsx index 67256df80..161bfdb0b 100644 --- a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.test.tsx +++ b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.test.tsx @@ -3,9 +3,11 @@ import {fireEvent} from '@testing-library/react-native'; import React from 'react'; +import {View} from 'react-native'; import {showPermalink} from '@actions/remote/permalink'; import Markdown from '@components/markdown'; +import TranslateIcon from '@components/post_list/post/header/translate_icon'; import {Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {renderWithEverything} from '@test/intl-test-helper'; @@ -28,6 +30,14 @@ jest.mocked(Markdown).mockImplementation((props) => React.createElement('Text', {testID: 'markdown'}, props.value), ); +jest.mock('@components/post_list/post/header/translate_icon', () => ({ + __esModule: true, + default: jest.fn(), +})); +jest.mocked(TranslateIcon).mockImplementation(() => + React.createElement(View, {testID: 'translate-icon'}, null), +); + describe('components/post_list/post/body/content/permalink_preview/PermalinkPreview', () => { let database: Database; let operator: ServerDataOperator; @@ -81,7 +91,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev edit_at: 0, metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -118,6 +128,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev isOriginPostDeleted: false, parentLocation: Screens.CHANNEL, parentPostId: 'parent-post-123', + autotranslationsEnabled: false, }; it('should render permalink preview correctly', () => { @@ -141,7 +152,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev ...baseProps, embedData: { ...baseProps.embedData, - post: undefined as unknown as Post, + post: undefined, }, post: undefined, }; @@ -214,7 +225,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev message: 'Test message', metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -262,7 +273,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev message: longMessage, metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -290,7 +301,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev message: '', metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -319,7 +330,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev create_at: 1234567890000, metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -366,7 +377,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev metadata: { files: [fileInfo], embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -412,7 +423,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev metadata: { files: fileInfos, embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -442,7 +453,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev message: 'Post without files', metadata: { embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -473,7 +484,7 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev metadata: { files: [], embeds: [{ - type: 'opengraph' as PostEmbedType, + type: 'opengraph', url: 'https://example.com', data: { title: 'Example Title', @@ -492,4 +503,175 @@ describe('components/post_list/post/body/content/permalink_preview/PermalinkPrev expect(queryByTestId('permalink-files-container')).toBeNull(); }); }); + + describe('autotranslationsEnabled', () => { + it('should not render TranslateIcon when autotranslationsEnabled is false', () => { + const {queryByTestId} = renderPermalinkPreview({ + ...baseProps, + autotranslationsEnabled: false, + }); + + expect(queryByTestId('translate-icon')).toBeNull(); + }); + + it('should render TranslateIcon when autotranslationsEnabled is true', () => { + const {getByTestId} = renderPermalinkPreview({ + ...baseProps, + autotranslationsEnabled: true, + }); + + expect(getByTestId('translate-icon')).toBeTruthy(); + }); + + it('should display translated message when autotranslationsEnabled is true and translation is ready', () => { + const originalMessage = 'Original message'; + const translatedMessage = 'Translated message'; + const props = { + ...baseProps, + autotranslationsEnabled: true, + embedData: { + ...baseProps.embedData, + post: TestHelper.fakePost({ + id: 'post-123', + user_id: 'user-123', + message: originalMessage, + create_at: 1234567890000, + edit_at: 0, + metadata: { + translations: { + en: { + state: 'ready', + object: {message: translatedMessage}, + }, + }, + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example Title', + description: 'Example Description', + }, + }], + }, + }), + }, + }; + const {getByText} = renderPermalinkPreview(props); + + expect(getByText(translatedMessage)).toBeTruthy(); + }); + + it('should display original message when autotranslationsEnabled is true but translation is not ready', () => { + const originalMessage = 'Original message only'; + const props = { + ...baseProps, + autotranslationsEnabled: true, + embedData: { + ...baseProps.embedData, + post: TestHelper.fakePost({ + id: 'post-123', + user_id: 'user-123', + message: originalMessage, + create_at: 1234567890000, + edit_at: 0, + metadata: { + translations: { + en: { + state: 'processing', + object: {message: ''}, + }, + }, + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example Title', + description: 'Example Description', + }, + }], + }, + }), + }, + }; + const {getByText} = renderPermalinkPreview(props); + + expect(getByText(originalMessage)).toBeTruthy(); + }); + + it('should display original message when autotranslationsEnabled is false regardless of translation', () => { + const originalMessage = 'Original message'; + const props = { + ...baseProps, + autotranslationsEnabled: false, + embedData: { + ...baseProps.embedData, + post: TestHelper.fakePost({ + id: 'post-123', + user_id: 'user-123', + message: originalMessage, + create_at: 1234567890000, + edit_at: 0, + metadata: { + translations: { + en: { + state: 'ready', + object: {message: 'Translated message'}, + }, + }, + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example Title', + description: 'Example Description', + }, + }], + }, + }), + }, + }; + const {getByText} = renderPermalinkPreview(props); + + expect(getByText(originalMessage)).toBeTruthy(); + }); + + it('should not translate custom posts or show TranslateIcon when autotranslationsEnabled is true', () => { + const originalMessage = 'Custom message'; + const translatedMessage = 'Translated custom message'; + const props = { + ...baseProps, + autotranslationsEnabled: true, + embedData: { + ...baseProps.embedData, + post: TestHelper.fakePost({ + id: 'system-post-123', + user_id: 'user-123', + type: 'custom_llmbot', + message: originalMessage, + create_at: 1234567890000, + edit_at: 0, + metadata: { + translations: { + en: { + state: 'ready', + object: {message: translatedMessage}, + }, + }, + embeds: [{ + type: 'opengraph', + url: 'https://example.com', + data: { + title: 'Example Title', + description: 'Example Description', + }, + }], + }, + }), + }, + }; + const {queryByTestId, getByText} = renderPermalinkPreview(props); + expect(queryByTestId('translate-icon')).toBeNull(); + expect(getByText(originalMessage)).toBeTruthy(); + }); + }); }); diff --git a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx index 4f82205e9..13e3ba366 100644 --- a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx +++ b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx @@ -11,6 +11,7 @@ import EditedIndicator from '@components/edited_indicator'; import FormattedText from '@components/formatted_text'; import FormattedTime from '@components/formatted_time'; import Markdown from '@components/markdown'; +import TranslateIcon from '@components/post_list/post/header/translate_icon'; import ProfilePicture from '@components/profile_picture'; import {View as ViewConstants} from '@constants'; import {useServerUrl} from '@context/server'; @@ -18,6 +19,7 @@ import {useTheme} from '@context/theme'; import {useUserLocale} from '@context/user_locale'; import {useIsTablet, useWindowDimensions} from '@hooks/device'; import {usePreventDoubleTap} from '@hooks/utils'; +import {getPostTranslatedMessage, getPostTranslation} from '@utils/post'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {displayUsername, getUserTimezone} from '@utils/user'; @@ -46,6 +48,7 @@ type PermalinkPreviewProps = { location: AvailableScreens; parentLocation?: string; parentPostId?: string; + autotranslationsEnabled: boolean; }; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { @@ -77,6 +80,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { marginLeft: 12, flexDirection: 'row', alignItems: 'center', + gap: 8, }, authorName: { color: theme.centerChannelColor, @@ -85,7 +89,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { timestamp: { color: changeOpacity(theme.centerChannelColor, 0.64), ...typography('Body', 75), - marginLeft: 8, }, messageContainer: { marginBottom: 8, @@ -130,6 +133,7 @@ const PermalinkPreview = ({ location, parentLocation, parentPostId, + autotranslationsEnabled, }: PermalinkPreviewProps) => { const theme = useTheme(); const serverUrl = useServerUrl(); @@ -170,8 +174,15 @@ const PermalinkPreview = ({ channel_type, } = embedData; + const translation = embedPost ? getPostTranslation(embedPost, locale) : undefined; + const truncatedMessage = useMemo(() => { - const msg = embedPost?.message; + let msg = embedPost?.message ?? ''; + if (autotranslationsEnabled && embedPost?.type === '') { + if (translation?.state === 'ready') { + msg = getPostTranslatedMessage(msg, translation); + } + } if (!msg || typeof msg !== 'string') { return ''; } @@ -183,7 +194,7 @@ const PermalinkPreview = ({ } return cleanMessage; - }, [embedPost?.message]); + }, [autotranslationsEnabled, embedPost?.message, embedPost?.type, translation]); const isEdited = useMemo(() => embedData && embedData.post && embedData.post.edit_at > 0, [embedData]); @@ -254,9 +265,14 @@ const PermalinkPreview = ({ + {autotranslationsEnabled && embedPost?.type === '' && ( + + )} diff --git a/types/api/posts.d.ts b/types/api/posts.d.ts index 7a25057cc..cffbcb4f6 100644 --- a/types/api/posts.d.ts +++ b/types/api/posts.d.ts @@ -51,7 +51,7 @@ type PostPriority = { type PermalinkEmbedData = { post_id: string; - post: Post; + post?: Post; team_name: string; channel_display_name: string; channel_type: string;