Add autotranslations to permalink previews (#9540)

* Add autotranslations to permalink previews

* Add missing change

* Address feedback and fix test

* Address feedback

* Fix test

* Revert "Fix test"

This reverts commit 8aa88c33eb9f805ed57be5f0d90412c2657cca8f.

* Revert "Address feedback"

This reverts commit c0249d3d0b7a326f777234850e6519e0f98ef41c.
This commit is contained in:
Daniel Espino García 2026-03-05 12:47:18 +01:00 committed by GitHub
parent b4798e78cc
commit 058b87ee4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 278 additions and 16 deletions

View file

@ -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(
<EnhancedPermalinkPreview
embedData={embedData}
location={Screens.CHANNEL}
/>,
{database, serverUrl},
);
await waitFor(() => {
const permalinkPreview = getByTestId('permalink-preview');
expect(permalinkPreview.props.autotranslationsEnabled).toBe(true);
});
});
});

View file

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

View file

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

View file

@ -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 = ({
<FormattedTime
timezone={getUserTimezone(currentUser)}
isMilitaryTime={isMilitaryTime}
value={embedPost.create_at}
value={embedPost?.create_at ?? 0}
style={styles.timestamp}
/>
{autotranslationsEnabled && embedPost?.type === '' && (
<TranslateIcon
translationState={translation?.state}
/>
)}
</View>
</View>

View file

@ -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;