From 1db5cd456c2eb9c9fab335c91eb0f4d9665bb268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 16 Feb 2026 16:32:02 +0100 Subject: [PATCH] [MM-67253] Add support for DMs and GMs for autotranslations (#9505) * [MM-67253] Add support for DMs and GMs for autotranslations * Address copilot comments --- app/constants/permissions.ts | 4 + app/queries/servers/channel.test.ts | 171 +++++++++++++- app/queries/servers/channel.ts | 42 +++- app/queries/servers/role.test.ts | 218 ++++++++++++++++++ app/queries/servers/role.ts | 26 +++ app/screens/channel_info/index.ts | 13 +- .../channel_settings/channel_settings.tsx | 6 +- app/screens/channel_settings/index.ts | 9 +- types/api/config.d.ts | 7 +- 9 files changed, 469 insertions(+), 27 deletions(-) diff --git a/app/constants/permissions.ts b/app/constants/permissions.ts index 22ff91433..cfe74073a 100644 --- a/app/constants/permissions.ts +++ b/app/constants/permissions.ts @@ -159,4 +159,8 @@ export default { EDIT_BOOKMARK_PRIVATE_CHANNEL: 'edit_bookmark_private_channel', DELETE_BOOKMARK_PUBLIC_CHANNEL: 'delete_bookmark_public_channel', DELETE_BOOKMARK_PRIVATE_CHANNEL: 'delete_bookmark_private_channel', + + // Autotranslations + MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION: 'manage_public_channel_auto_translation', + MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION: 'manage_private_channel_auto_translation', }; diff --git a/app/queries/servers/channel.test.ts b/app/queries/servers/channel.test.ts index f9f54b87e..64d634b1f 100644 --- a/app/queries/servers/channel.test.ts +++ b/app/queries/servers/channel.test.ts @@ -70,7 +70,7 @@ import {prepareChannels, observeIsChannelAutotranslated, } from './channel'; import {queryRoles} from './role'; -import {getCurrentChannelId, observeConfigBooleanValue, observeCurrentChannelId, observeCurrentUserId} from './system'; +import {getCurrentChannelId, observeCurrentChannelId, observeCurrentUserId} from './system'; import {observeTeammateNameDisplay} from './user'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -79,7 +79,15 @@ import type ChannelInfoModel from '@typings/database/models/servers/channel_info import type ChannelMembershipModel from '@typings/database/models/servers/channel_membership'; jest.mock('./role'); -jest.mock('./system'); +jest.mock('./system', () => { + const actual = jest.requireActual('./system'); + return { + ...actual, + getCurrentChannelId: jest.fn(), + observeCurrentChannelId: jest.fn(), + observeCurrentUserId: jest.fn(), + }; +}); jest.mock('./user'); jest.mock('@utils/role'); @@ -1669,7 +1677,14 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( const channelId = 'ch_observe'; it('should emit true when EnableAutoTranslation is true and channel has autotranslation', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: true}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1681,7 +1696,14 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( }); it('should emit false when EnableAutoTranslation is false', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(false)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'false'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: true}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1693,7 +1715,14 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( }); it('should emit false when channel has autotranslation false', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: false}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1705,20 +1734,80 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( }); it('should emit false when channel is not found', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const subscriptionNext = jest.fn(); const result = observeChannelAutotranslation(database, 'nonexistent'); result.subscribe({next: subscriptionNext}); expect(subscriptionNext).toHaveBeenCalledWith(false); }); + + it('should emit false when channel is DM and RestrictDMAndGMAutotranslation is true', async () => { + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'true'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + const channel = TestHelper.fakeChannel({ + id: channelId, + type: General.DM_CHANNEL, + autotranslation: true, + }); + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + + const subscriptionNext = jest.fn(); + const result = observeChannelAutotranslation(database, channelId); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit true when channel is DM and RestrictDMAndGMAutotranslation is false', async () => { + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + const channel = TestHelper.fakeChannel({ + id: channelId, + type: General.DM_CHANNEL, + autotranslation: true, + }); + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + + const subscriptionNext = jest.fn(); + const result = observeChannelAutotranslation(database, channelId); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(true); + }); }); describe('observeIsChannelAutotranslated', () => { const channelId = 'ch_is_autotranslated'; it('should emit true when config and channel has autotranslation true and myChannel has autotranslation disabled false', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: true}); const myChannel = TestHelper.fakeChannelMember({id: channelId, channel_id: channelId, autotranslation_disabled: false}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1732,7 +1821,14 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( }); it('should emit false when channel autotranslation is false', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: false}); const myChannel = TestHelper.fakeChannelMember({id: channelId, channel_id: channelId, autotranslation_disabled: false}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1746,7 +1842,14 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( }); it('should emit false when myChannel autotranslation disabled is true', async () => { - jest.mocked(observeConfigBooleanValue).mockReturnValue(of$(true)); + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); const channel = TestHelper.fakeChannel({id: channelId, team_id: 'team1', autotranslation: true}); const myChannel = TestHelper.fakeChannelMember({id: channelId, channel_id: channelId, autotranslation_disabled: true}); await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); @@ -1758,5 +1861,55 @@ describe('queryMyChannelsByChannelIds and queryMyChannelsWithAutotranslation', ( expect(subscriptionNext).toHaveBeenCalledWith(false); }); + + it('should emit false when channel is DM and RestrictDMAndGMAutotranslation is true', async () => { + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'true'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + const channel = TestHelper.fakeChannel({ + id: channelId, + type: General.DM_CHANNEL, + autotranslation: true, + }); + const myChannel = TestHelper.fakeChannelMember({id: channelId, channel_id: channelId, autotranslation_disabled: false}); + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + await operator.handleMyChannel({channels: [channel], myChannels: [myChannel], prepareRecordsOnly: false}); + + const subscriptionNext = jest.fn(); + const result = observeIsChannelAutotranslated(database, channelId); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit true when channel is DM and RestrictDMAndGMAutotranslation is false', async () => { + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + const channel = TestHelper.fakeChannel({ + id: channelId, + type: General.DM_CHANNEL, + autotranslation: true, + }); + const myChannel = TestHelper.fakeChannelMember({id: channelId, channel_id: channelId, autotranslation_disabled: false}); + await operator.handleChannel({channels: [channel], prepareRecordsOnly: false}); + await operator.handleMyChannel({channels: [channel], myChannels: [myChannel], prepareRecordsOnly: false}); + + const subscriptionNext = jest.fn(); + const result = observeIsChannelAutotranslated(database, channelId); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(true); + }); }); }); diff --git a/app/queries/servers/channel.ts b/app/queries/servers/channel.ts index 21b334dbf..d05d0988b 100644 --- a/app/queries/servers/channel.ts +++ b/app/queries/servers/channel.ts @@ -11,7 +11,7 @@ import {General, Permissions} from '@constants'; import {MM_TABLES} from '@constants/database'; import {sanitizeLikeString} from '@helpers/database'; import EphemeralStore from '@store/ephemeral_store'; -import {isDefaultChannel} from '@utils/channel'; +import {isDefaultChannel, isDMorGM} from '@utils/channel'; import {hasPermission} from '@utils/role'; import {isSystemAdmin} from '@utils/user'; @@ -287,9 +287,24 @@ export const observeMyChannelRoles = (database: Database, channelId: string) => export const observeChannelAutotranslation = (database: Database, channelId: string) => { const enableAutoTranslation = observeConfigBooleanValue(database, 'EnableAutoTranslation'); + const restrictDMAndGMAutotranslation = observeConfigBooleanValue(database, 'RestrictDMAndGMAutotranslation'); const channel = observeChannel(database, channelId); - return combineLatest([enableAutoTranslation, channel]).pipe( - switchMap(([et, c]) => of$(Boolean(et && c?.autotranslation))), + return combineLatest([enableAutoTranslation, restrictDMAndGMAutotranslation, channel]).pipe( + map$(([et, r, c]) => { + if (!et) { + return false; + } + + if (!c?.autotranslation) { + return false; + } + + if (isDMorGM(c) && r) { + return false; + } + + return true; + }), distinctUntilChanged(), ); }; @@ -798,7 +813,26 @@ export const observeIsReadOnlyChannel = (database: Database, channelId: string) export const observeIsChannelAutotranslated = (database: Database, channelId: string) => { const enableAutoTranslation = observeConfigBooleanValue(database, 'EnableAutoTranslation'); + const restrictDMAndGMAutotranslation = observeConfigBooleanValue(database, 'RestrictDMAndGMAutotranslation'); const channel = observeChannel(database, channelId); const myChannel = observeMyChannel(database, channelId); - return combineLatest([enableAutoTranslation, channel, myChannel]).pipe(map(([et, c, mc]) => Boolean(et && c?.autotranslation && !mc?.autotranslationDisabled))); + return combineLatest([enableAutoTranslation, restrictDMAndGMAutotranslation, channel, myChannel]).pipe(map(([et, r, c, mc]) => { + if (!et) { + return false; + } + + if (!c?.autotranslation) { + return false; + } + + if (mc?.autotranslationDisabled) { + return false; + } + + if (isDMorGM(c) && r) { + return false; + } + + return true; + })); }; diff --git a/app/queries/servers/role.test.ts b/app/queries/servers/role.test.ts index 4bb102f0b..172c15947 100644 --- a/app/queries/servers/role.test.ts +++ b/app/queries/servers/role.test.ts @@ -14,6 +14,7 @@ import { observePermissionForPost, observeCanManageChannelMembers, observeCanManageChannelSettings, + observeCanManageChannelAutotranslations, } from './role'; import type ServerDataOperator from '@database/operator/server_data_operator'; @@ -427,4 +428,221 @@ describe('Role Queries', () => { }); }); }); + + describe('observeCanManageChannelAutotranslations', () => { + it('should emit false when EnableAutoTranslation is false', async () => { + const mockUser = TestHelper.fakeUserModel({id: 'user1', roles: 'system_user'}); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'false'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.OPEN_CHANNEL, + delete_at: 0, + })], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit false when channel is not found', async () => { + const mockUser = TestHelper.fakeUserModel({id: 'user1', roles: 'system_user'}); + + await operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'nonexistent', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit false when channel is deleted', async () => { + const mockUser = TestHelper.fakeUserModel({id: 'user1', roles: 'system_user'}); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.OPEN_CHANNEL, + delete_at: 123, + })], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit false when channel is DM and RestrictDMAndGMAutotranslation is true', async () => { + const mockUser = TestHelper.fakeUserModel({id: 'user1', roles: 'system_user'}); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'true'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.DM_CHANNEL, + delete_at: 0, + })], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(false); + }); + + it('should emit true when channel is DM and RestrictDMAndGMAutotranslation is false', async () => { + const mockUser = TestHelper.fakeUserModel({id: 'user1', roles: 'system_user'}); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.DM_CHANNEL, + delete_at: 0, + })], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(true); + }); + + it('should emit true for open channel when user has MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION', async () => { + const mockUser = TestHelper.fakeUserModel({ + id: 'user1', + roles: 'channel_admin', + }); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.OPEN_CHANNEL, + delete_at: 0, + })], + prepareRecordsOnly: false, + }), + operator.handleRole({ + roles: [{ + id: 'channel_admin', + name: 'channel_admin', + permissions: [Permissions.MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION], + }], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(true); + }); + + it('should emit true for private channel when user has MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION', async () => { + const mockUser = TestHelper.fakeUserModel({ + id: 'user1', + roles: 'channel_admin', + }); + + await Promise.all([ + operator.handleConfigs({ + configs: [ + {id: 'EnableAutoTranslation', value: 'true'}, + {id: 'RestrictDMAndGMAutotranslation', value: 'false'}, + ], + configsToDelete: [], + prepareRecordsOnly: false, + }), + operator.handleChannel({ + channels: [TestHelper.fakeChannel({ + id: 'channel1', + type: General.PRIVATE_CHANNEL, + delete_at: 0, + })], + prepareRecordsOnly: false, + }), + operator.handleRole({ + roles: [{ + id: 'channel_admin', + name: 'channel_admin', + permissions: [Permissions.MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION], + }], + prepareRecordsOnly: false, + }), + ]); + + const subscriptionNext = jest.fn(); + const result = observeCanManageChannelAutotranslations(database, 'channel1', mockUser); + result.subscribe({next: subscriptionNext}); + + expect(subscriptionNext).toHaveBeenCalledWith(true); + }); + }); }); diff --git a/app/queries/servers/role.ts b/app/queries/servers/role.ts index 4561c1d56..681d472fb 100644 --- a/app/queries/servers/role.ts +++ b/app/queries/servers/role.ts @@ -10,6 +10,7 @@ import {isDefaultChannel, isDMorGM} from '@utils/channel'; import {hasPermission} from '@utils/role'; import {observeChannel, observeMyChannelRoles} from './channel'; +import {observeConfigBooleanValue} from './system'; import {observeMyTeam, observeMyTeamRoles} from './team'; import type ChannelModel from '@typings/database/models/servers/channel'; @@ -115,3 +116,28 @@ export function observeCanManageChannelSettings(database: Database, channelId: s distinctUntilChanged(), ); } + +export function observeCanManageChannelAutotranslations(database: Database, channelId: string, user: UserModel) { + const featureEnabled = observeConfigBooleanValue(database, 'EnableAutoTranslation'); + const channel = observeChannel(database, channelId); + const restrictDMAndGMAutotranslation = observeConfigBooleanValue(database, 'RestrictDMAndGMAutotranslation'); + return combineLatest([featureEnabled, channel, restrictDMAndGMAutotranslation]).pipe( + switchMap(([enabled, c, isDmGmRestricted]) => { + if (!enabled) { + return of$(false); + } + + if (!c || c.deleteAt !== 0) { + return of$(false); + } + + if (isDMorGM(c)) { + return of$(!isDmGmRestricted); + } + + const permission = c.type === General.OPEN_CHANNEL ? Permissions.MANAGE_PUBLIC_CHANNEL_AUTO_TRANSLATION : Permissions.MANAGE_PRIVATE_CHANNEL_AUTO_TRANSLATION; + return observePermissionForChannel(database, c, user, permission, false); + }), + distinctUntilChanged(), + ); +} diff --git a/app/screens/channel_info/index.ts b/app/screens/channel_info/index.ts index f8beb4288..670e187b8 100644 --- a/app/screens/channel_info/index.ts +++ b/app/screens/channel_info/index.ts @@ -12,7 +12,7 @@ import {withServerUrl} from '@context/server'; import {observeIsPlaybooksEnabled} from '@playbooks/database/queries/version'; import {observeChannelAutotranslation, observeCurrentChannel} from '@queries/servers/channel'; import {observeCanAddBookmarks} from '@queries/servers/channel_bookmark'; -import {observeCanManageChannelMembers, observeCanManageChannelSettings, observePermissionForChannel, observePermissionForTeam} from '@queries/servers/role'; +import {observeCanManageChannelAutotranslations, observeCanManageChannelMembers, observeCanManageChannelSettings, observePermissionForChannel, observePermissionForTeam} from '@queries/servers/role'; import { observeConfigBooleanValue, observeConfigValue, @@ -137,7 +137,10 @@ const observeHasChannelSettingsActions = ( switchMap((version) => of$(isMinimumServerVersion(version || '', 9, 1))), ); - const isChannelAutotranslateEnabled = observeConfigBooleanValue(database, 'EnableAutoTranslation'); + const canManageChannelAutotranslations = combineLatest([channelId, currentUser]).pipe( + switchMap(([cId, u]) => (u ? observeCanManageChannelAutotranslations(database, cId, u) : of$(false))), + distinctUntilChanged(), + ); const team = observeCurrentTeam(database); const isArchived = channel.pipe(switchMap((c) => of$((c?.deleteAt || 0) > 0))); @@ -206,16 +209,16 @@ const observeHasChannelSettingsActions = ( canUnarchive, canEnableDisableCalls, convertGMOptionAvailable, - isChannelAutotranslateEnabled, + canManageChannelAutotranslations, ]).pipe( - switchMap(([manageSettings, convert, archive, unarchive, enableCalls, convertGM, autotranslateEnabled]) => { + switchMap(([manageSettings, convert, archive, unarchive, enableCalls, convertGM, manageAutotranslations]) => { return of$( manageSettings || // Channel info or Channel autotranslations convert || // Convert to private archive || unarchive || // Archive channel enableCalls || // Enable/Disable calls convertGM || // Convert GM to channel - (manageSettings && autotranslateEnabled), // Channel autotranslations + manageAutotranslations, // Channel autotranslations ); }), ); diff --git a/app/screens/channel_settings/channel_settings.tsx b/app/screens/channel_settings/channel_settings.tsx index 95a8728a9..ddbfc0943 100644 --- a/app/screens/channel_settings/channel_settings.tsx +++ b/app/screens/channel_settings/channel_settings.tsx @@ -33,7 +33,7 @@ type Props = { convertGMOptionAvailable: boolean; displayName: string; isCallsEnabledInChannel: boolean; - isChannelAutotranslateEnabled: boolean; + canManageAutotranslations: boolean; type?: ChannelType; } @@ -66,7 +66,7 @@ const ChannelSettings = ({ convertGMOptionAvailable, displayName, isCallsEnabledInChannel, - isChannelAutotranslateEnabled, + canManageAutotranslations, type, }: Props) => { const theme = useTheme(); @@ -116,7 +116,7 @@ const ChannelSettings = ({ {convertGMOptionAvailable && } - {canManageSettings && isChannelAutotranslateEnabled && + {canManageAutotranslations && } {(canArchive || canUnarchive) && diff --git a/app/screens/channel_settings/index.ts b/app/screens/channel_settings/index.ts index 484240dea..60a42efd8 100644 --- a/app/screens/channel_settings/index.ts +++ b/app/screens/channel_settings/index.ts @@ -10,9 +10,8 @@ import {observeCallsConfig} from '@calls/state'; import {General, Permissions} from '@constants'; import {withServerUrl} from '@context/server'; import {observeChannel} from '@queries/servers/channel'; -import {observePermissionForChannel, observePermissionForTeam, observeCanManageChannelSettings} from '@queries/servers/role'; +import {observePermissionForChannel, observePermissionForTeam, observeCanManageChannelSettings, observeCanManageChannelAutotranslations} from '@queries/servers/role'; import { - observeConfigBooleanValue, observeConfigValue, observeCurrentTeamId, } from '@queries/servers/system'; @@ -175,7 +174,9 @@ const enhanced = withObservables(['channelId'], ({channelId, serverUrl, database ); // Channel autotranslation observable - const isChannelAutotranslateEnabled = observeConfigBooleanValue(database, 'EnableAutoTranslation'); + const canManageAutotranslations = currentUser.pipe( + switchMap((u) => (u ? observeCanManageChannelAutotranslations(database, channelId, u) : of$(false))), + ); return { canArchive, @@ -186,7 +187,7 @@ const enhanced = withObservables(['channelId'], ({channelId, serverUrl, database convertGMOptionAvailable, displayName: channel.pipe(switchMap((c) => of$(c?.displayName || ''))), isCallsEnabledInChannel, - isChannelAutotranslateEnabled, + canManageAutotranslations, isGuestUser, type, }; diff --git a/types/api/config.d.ts b/types/api/config.d.ts index a2fddb958..b5d5a93a2 100644 --- a/types/api/config.d.ts +++ b/types/api/config.d.ts @@ -13,7 +13,6 @@ interface ClientConfig { AndroidMinVersion: string; AppDownloadLink: string; AsymmetricSigningPublicKey: string; - AutoTranslationLanguages: string; AvailableLocales: string; BannerColor: string; BannerText: string; @@ -48,7 +47,6 @@ interface ClientConfig { EmailLoginButtonTextColor: string; EmailNotificationContentsType: string; EnableBanner: string; - EnableAutoTranslation: string; EnableBotAccountCreation: string; EnableBurnOnRead: string; EnableChannelViewedMessages: string; @@ -218,6 +216,11 @@ interface ClientConfig { WebsocketSecurePort: string; WebsocketURL: string; BurnOnReadDurationSeconds: string; + + // Autotranslations + AutoTranslationLanguages: string; + EnableAutoTranslation: string; + RestrictDMAndGMAutotranslation: string; } type SecurityClientConfig = Pick