mattermost-mobile/app/actions/local/systems.ts
Mattermost Build e8f1d0c729
Add autotranslations (#9324) (#9500)
* Add autotranslations

* Develop latest changes and missing features

* i18n-extract

* Fix tests and update api behavior

* Fix minor bugs

* adjustments to shimmer animation, showtranslation modal style

* Update post.tsx

* Update show_translation.tsx

* adjust sizing and opacity of translate icon

* Add channel header translated icon

* Disable auto translation item if it is not a supported language

* Move channel info to the top

* Update edit channel to channel info

* Fix align of option items

* Fix i18n

* Add detox related changes for channel settings changes

* Add tests

* Address changes around the my channel column change

* Address feedback

* Fix test

* Fix bad import

* Fix set my channel autotranslation

* Fix test

* Address feedback

* Add missing files from last commit

* Remove unneeded change

---------


(cherry picked from commit 23565b5135)

Co-authored-by: Daniel Espino García <larkox@gmail.com>
Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
2026-02-11 08:09:39 +02:00

389 lines
14 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Q} from '@nozbe/watermelondb';
import deepEqual from 'deep-equal';
import {DeviceEventEmitter} from 'react-native';
import {Events} from '@constants';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import {PostTypes, BOR_POST_CLEANUP_MIN_RUN_INTERVAL} from '@constants/post';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import {queryAllChannelsForTeam} from '@queries/servers/channel';
import {queryPostsByType} from '@queries/servers/post';
import {
getConfig,
getLicense,
getGlobalDataRetentionPolicy,
getGranularDataRetentionPolicies,
getLastGlobalDataRetentionRun,
getIsDataRetentionEnabled,
getLastBoRPostCleanupRun,
} from '@queries/servers/system';
import PostModel from '@typings/database/models/servers/post';
import {isExpiredBoRPost} from '@utils/bor';
import {logError} from '@utils/log';
import {deletePostsForChannelsWithAutotranslation} from './channel';
import {deletePosts} from './post';
import type {DataRetentionPoliciesRequest} from '@actions/remote/systems';
const {SERVER: {POST}} = MM_TABLES;
export async function storeConfigAndLicense(serverUrl: string, config: ClientConfig, license: ClientLicense) {
try {
// If we have credentials for this server then update the values in the database
const credentials = await getServerCredentials(serverUrl);
if (credentials) {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentLicense = await getLicense(database);
const systems: IdValue[] = [];
if (!deepEqual(license, currentLicense)) {
systems.push({
id: SYSTEM_IDENTIFIERS.LICENSE,
value: JSON.stringify(license),
});
}
if (systems.length) {
await operator.handleSystem({systems, prepareRecordsOnly: false});
DeviceEventEmitter.emit(Events.LICENSE_CHANGED, {serverUrl, license});
}
return await storeConfig(serverUrl, config);
}
} catch (error) {
logError('An error occurred while saving config & license', error);
}
return [];
}
export async function storeConfig(serverUrl: string, config: ClientConfig | undefined, prepareRecordsOnly = false) {
if (!config) {
return [];
}
try {
const {operator, database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const currentConfig = await getConfig(database);
const configsToUpdate: IdValue[] = [];
const configsToDelete: IdValue[] = [];
// Check if EnableAutoTranslation changed from enabled to disabled
const enableAutoTranslationChanged = (currentConfig?.EnableAutoTranslation === 'true') !== (config.EnableAutoTranslation === 'true');
let k: keyof ClientConfig;
for (k in config) {
if (currentConfig?.[k] !== config[k]) {
configsToUpdate.push({
id: k,
value: config[k],
});
}
}
for (k in currentConfig) {
if (config[k] === undefined) {
configsToDelete.push({
id: k,
value: currentConfig[k],
});
}
}
if (configsToDelete.length || configsToUpdate.length) {
const results = await operator.handleConfigs({configs: configsToUpdate, configsToDelete, prepareRecordsOnly});
DeviceEventEmitter.emit(Events.CONFIG_CHANGED, {serverUrl, config});
// If EnableAutoTranslation was disabled, delete posts and disable user autotranslation
if (enableAutoTranslationChanged) {
await deletePostsForChannelsWithAutotranslation(serverUrl, prepareRecordsOnly);
}
return results;
}
} catch (error) {
logError('storeConfig', error);
}
return [];
}
export async function storeDataRetentionPolicies(serverUrl: string, data: DataRetentionPoliciesRequest, prepareRecordsOnly = false) {
try {
const {globalPolicy, teamPolicies, channelPolicies} = data;
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const systems: IdValue[] = [{
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
value: globalPolicy || {},
}, {
id: SYSTEM_IDENTIFIERS.GRANULAR_DATA_RETENTION_POLICIES,
value: {
team: teamPolicies || [],
channel: channelPolicies || [],
},
}];
return operator.handleSystem({
systems,
prepareRecordsOnly,
});
} catch {
return [];
}
}
export async function updateLastDataRetentionRun(serverUrl: string, value?: number, prepareRecordsOnly = false) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const systems: IdValue[] = [{
id: SYSTEM_IDENTIFIERS.LAST_DATA_RETENTION_RUN,
value: value || Date.now(),
}];
return operator.handleSystem({systems, prepareRecordsOnly});
} catch (error) {
logError('Failed updateLastDataRetentionRun', error);
return {error};
}
}
export async function dataRetentionCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const lastRunAt = await getLastGlobalDataRetentionRun(database);
const lastCleanedToday = new Date(lastRunAt).toDateString() === new Date().toDateString();
// Do not run if clean up is already done today
if (lastRunAt && lastCleanedToday) {
return {error: undefined};
}
const isDataRetentionEnabled = await getIsDataRetentionEnabled(database);
const result = await (isDataRetentionEnabled ? dataRetentionPolicyCleanup(serverUrl) : dataRetentionWithoutPolicyCleanup(serverUrl));
if (!result.error) {
await updateLastDataRetentionRun(serverUrl);
}
await database.unsafeVacuum();
return result;
} catch (error) {
logError('An error occurred while performing data retention cleanup', error);
return {error};
}
}
async function dataRetentionPolicyCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const globalPolicy = await getGlobalDataRetentionPolicy(database);
const granularPoliciesData = await getGranularDataRetentionPolicies(database);
// Get global data retention cutoff
let globalRetentionCutoff = 0;
if (globalPolicy?.message_deletion_enabled) {
globalRetentionCutoff = globalPolicy.message_retention_cutoff;
}
// Get Granular data retention policies
let teamPolicies: TeamDataRetentionPolicy[] = [];
let channelPolicies: ChannelDataRetentionPolicy[] = [];
if (granularPoliciesData) {
teamPolicies = granularPoliciesData.team;
channelPolicies = granularPoliciesData.channel;
}
const channelsCutoffs: {[key: string]: number} = {};
// Get channel level cutoff from team policies
for await (const teamPolicy of teamPolicies) {
const {team_id, post_duration} = teamPolicy;
const channelIds = await queryAllChannelsForTeam(database, team_id).fetchIds();
if (channelIds.length) {
const cutoff = getDataRetentionPolicyCutoff(post_duration);
channelIds.forEach((channelId) => {
channelsCutoffs[channelId] = cutoff;
});
}
}
// Get channel level cutoff from channel policies
channelPolicies.forEach(({channel_id, post_duration}) => {
channelsCutoffs[channel_id] = getDataRetentionPolicyCutoff(post_duration);
});
const conditions = [];
const channelIds = Object.keys(channelsCutoffs);
if (channelIds.length) {
// Fetch posts by channel level cutoff
for (const channelId of channelIds) {
const cutoff = channelsCutoffs[channelId];
conditions.push(`(channel_id='${channelId}' AND create_at < ${cutoff})`);
}
// Fetch posts by global cutoff which are not already fetched by channel level cutoff
conditions.push(`(channel_id NOT IN ('${channelIds.join("','")}') AND create_at < ${globalRetentionCutoff})`);
} else {
conditions.push(`create_at < ${globalRetentionCutoff}`);
}
const postIds = await database.get<PostModel>(POST).query(
Q.unsafeSqlQuery(`SELECT * FROM ${POST} where ${conditions.join(' OR ')}`),
).fetchIds();
return dataRetentionCleanPosts(serverUrl, postIds);
} catch (error) {
logError('An error occurred while performing data retention policy cleanup', error);
return {error};
}
}
async function dataRetentionWithoutPolicyCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const cutoff = getDataRetentionPolicyCutoff(14); // 14 days
const postIds = await database.get<PostModel>(POST).query(
Q.where('create_at', Q.lt(cutoff)),
).fetchIds();
return dataRetentionCleanPosts(serverUrl, postIds);
} catch (error) {
logError('An error occurred while performing data retention without policy cleanup', error);
return {error};
}
}
export async function dataRetentionCleanPosts(serverUrl: string, postIds: string[]) {
if (postIds.length) {
const batchSize = 1000;
const deletePromises = [];
for (let i = 0; i < postIds.length; i += batchSize) {
const batch = postIds.slice(i, batchSize);
deletePromises.push(
deletePosts(serverUrl, batch),
);
}
const deleteResult = await Promise.all(deletePromises);
for (const {error} of deleteResult) {
if (error) {
return {error};
}
}
}
return {error: undefined};
}
// Returns cutoff time based on the policy's post_duration
function getDataRetentionPolicyCutoff(postDuration: number) {
const periodDate = new Date();
periodDate.setDate(periodDate.getDate() - postDuration);
periodDate.setHours(0);
periodDate.setMinutes(0);
periodDate.setSeconds(0);
return periodDate.getTime();
}
export async function setLastServerVersionCheck(serverUrl: string, reset = false) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.LAST_SERVER_VERSION_CHECK,
value: reset ? 0 : Date.now(),
}],
prepareRecordsOnly: false,
});
return {error: undefined};
} catch (error) {
logError('setLastServerVersionCheck', error);
return {error};
}
}
export async function setGlobalThreadsTab(serverUrl: string, globalThreadsTab: GlobalThreadsTab) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await operator.handleSystem({
systems: [{
id: SYSTEM_IDENTIFIERS.GLOBAL_THREADS_TAB,
value: globalThreadsTab,
}],
prepareRecordsOnly: false,
});
return {error: undefined};
} catch (error) {
logError('setGlobalThreadsTab', error);
return {error};
}
}
export async function dismissAnnouncement(serverUrl: string, announcementText: string) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
await operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.LAST_DISMISSED_BANNER, value: announcementText}], prepareRecordsOnly: false});
return {error: undefined};
} catch (error) {
logError('An error occurred while dismissing an announcement', error);
return {error};
}
}
export async function expiredBoRPostCleanup(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const lastRunAt = await getLastBoRPostCleanupRun(database);
const shouldRunNow = (Date.now() - lastRunAt) > BOR_POST_CLEANUP_MIN_RUN_INTERVAL;
if (!shouldRunNow) {
return;
}
const {error} = await removeExpiredBoRPosts(serverUrl);
if (!error) {
await updateLastBoRCleanupRun(serverUrl);
}
} catch (error) {
logError('An error occurred running the Burn on Read cleanup task', error);
}
}
async function removeExpiredBoRPosts(serverUrl: string) {
try {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const allBoRPosts = await queryPostsByType(database, PostTypes.BURN_ON_READ).fetch();
const expiredBoRPostIDs = allBoRPosts.
filter((post) => isExpiredBoRPost(post)).
map((post) => post.id);
await dataRetentionCleanPosts(serverUrl, expiredBoRPostIDs);
return {error: undefined};
} catch (error) {
logError('An error occurred while performing BoR post cleanup', error);
return {error};
}
}
async function updateLastBoRCleanupRun(serverUrl: string) {
try {
const {operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const systems: IdValue[] = [{
id: SYSTEM_IDENTIFIERS.LAST_BOR_POST_CLEANUP_RUN,
value: Date.now(),
}];
return operator.handleSystem({systems, prepareRecordsOnly: false});
} catch (error) {
logError('Failed updateLastBoRCleanupRun', error);
return {error};
}
}