mattermost-mobile/app/utils/post/index.ts
Daniel Espino García 23565b5135
Add autotranslations (#9324)
* 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

---------

Co-authored-by: Matthew Birtch <mattbirtch@gmail.com>
2026-02-10 17:50:39 +01:00

292 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Alert, type AlertButton} from 'react-native';
import {getUsersCountFromMentions} from '@actions/local/post';
import {General, Post} from '@constants';
import {SPECIAL_MENTIONS_REGEX} from '@constants/autocomplete';
import {POST_TIME_TO_FAIL} from '@constants/post';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE} from '@i18n';
import {getUserById} from '@queries/servers/user';
import {toMilliseconds} from '@utils/datetime';
import {ensureString, includes} from '@utils/types';
import {displayUsername, getUserIdFromChannelName} from '@utils/user';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {IntlShape} from 'react-intl';
export function areConsecutivePosts(post: PostModel, previousPost: PostModel, userLocale: string) {
let consecutive = false;
if (post && previousPost) {
const postFromWebhook = Boolean(post?.props?.from_webhook); // eslint-disable-line camelcase
const prevPostFromWebhook = Boolean(previousPost?.props?.from_webhook); // eslint-disable-line camelcase
const isFromSameUser = previousPost.userId === post.userId;
const isNotSystemMessage = !isSystemMessage(post) && !isSystemMessage(previousPost);
const isInTimeframe = (post.createAt - previousPost.createAt) <= Post.POST_COLLAPSE_TIMEOUT;
// Were the last post and this post made by the same user within some time?
consecutive = previousPost && isFromSameUser && isInTimeframe && !postFromWebhook &&
!prevPostFromWebhook && isNotSystemMessage;
}
if (consecutive) {
const postTranslation = getPostTranslation(post, userLocale);
const previousPostTranslation = getPostTranslation(previousPost, userLocale);
if (postTranslation?.state !== previousPostTranslation?.state) {
consecutive = false;
}
}
return consecutive;
}
export function isFromWebhook(post: PostModel | Post): boolean {
return post.props?.from_webhook === 'true';
}
export function isEdited(post: PostModel): boolean {
return post.editAt > 0;
}
export function isPostEphemeral(post: PostModel): boolean {
return post.type === Post.POST_TYPES.EPHEMERAL || post.type === Post.POST_TYPES.EPHEMERAL_ADD_TO_CHANNEL || post.deleteAt > 0;
}
export function isPostFailed(post: PostModel): boolean {
return Boolean(post.props?.failed) || ((post.pendingPostId === post.id) && (Date.now() > post.updateAt + POST_TIME_TO_FAIL));
}
export function isPostPendingOrFailed(post: PostModel): boolean {
return post.pendingPostId === post.id || isPostFailed(post);
}
export function isSystemMessage(post: PostModel | Post): boolean {
return Boolean(post.type && post.type?.startsWith(Post.POST_TYPES.SYSTEM_MESSAGE_PREFIX));
}
export function fromAutoResponder(post: PostModel): boolean {
return Boolean(post.type && (post.type === Post.POST_TYPES.SYSTEM_AUTO_RESPONDER));
}
export function postUserDisplayName(post: PostModel, author?: UserModel, teammateNameDisplay?: string, enablePostUsernameOverride = false) {
const overrideUsername = ensureString(post.props?.override_username);
if (
isFromWebhook(post) &&
enablePostUsernameOverride &&
overrideUsername
) {
return overrideUsername;
}
return displayUsername(author, author?.locale || DEFAULT_LOCALE, teammateNameDisplay, true);
}
export function shouldIgnorePost(post: Post): boolean {
return includes(Post.IGNORE_POST_TYPES, post.type);
}
export const processPostsFetched = (data: PostResponse) => {
const order = data.order;
const posts = Object.values(data.posts);
const previousPostId = data.prev_post_id;
return {
posts,
order,
previousPostId,
};
};
export const getLastFetchedAtFromPosts = (posts?: Post[]) => {
return posts?.reduce((timestamp: number, p) => {
const maxTimestamp = Math.max(p.create_at, p.update_at, p.delete_at);
return Math.max(maxTimestamp, timestamp);
}, 0) || 0;
};
export const moreThan5minAgo = (time: number) => {
return Date.now() - time > toMilliseconds({minutes: 5});
};
export function hasSpecialMentions(message: string): boolean {
const result = SPECIAL_MENTIONS_REGEX.test(message);
// https://stackoverflow.com/questions/1520800/why-does-a-regexp-with-global-flag-give-wrong-results
SPECIAL_MENTIONS_REGEX.lastIndex = 0;
return result;
}
export async function persistentNotificationsConfirmation(serverUrl: string, value: string, mentionsList: string[], intl: IntlShape, sendMessage: () => void, persistentNotificationMaxRecipients: number, persistentNotificationInterval: number, currentUserId: string, channelName?: string, channelType?: ChannelType) {
let title = '';
let description = '';
let buttons: AlertButton[] = [{
text: intl.formatMessage({
id: 'persistent_notifications.error.okay',
defaultMessage: 'Okay',
}),
style: 'cancel',
}];
if (channelType === General.DM_CHANNEL) {
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const teammateId = getUserIdFromChannelName(currentUserId, channelName!);
const user = await getUserById(database, teammateId);
title = intl.formatMessage({
id: 'persistent_notifications.confirm.title',
defaultMessage: 'Send persistent notifications',
});
description = intl.formatMessage({
id: 'persistent_notifications.dm_channel.description',
defaultMessage: '{username} will be notified every {interval, plural, one {minute} other {{interval} minutes}} until theyve acknowledged or replied to the message.',
}, {
interval: persistentNotificationInterval,
username: user?.username,
});
buttons = [{
text: intl.formatMessage({
id: 'persistent_notifications.confirm.cancel',
defaultMessage: 'Cancel',
}),
style: 'cancel',
},
{
text: intl.formatMessage({
id: 'persistent_notifications.confirm.send',
defaultMessage: 'Send',
}),
onPress: sendMessage,
}];
} else if (hasSpecialMentions(value)) {
description = intl.formatMessage({
id: 'persistent_notifications.error.special_mentions',
defaultMessage: 'Cannot use @channel, @all or @here to mention recipients of persistent notifications.',
});
} else {
// removes the @ from the mention
const formattedMentionsList = mentionsList.map((mention) => mention.slice(1));
const usersCount = await getUsersCountFromMentions(serverUrl, formattedMentionsList);
if (usersCount === 0) {
title = intl.formatMessage({
id: 'persistent_notifications.error.no_mentions.title',
defaultMessage: 'Recipients must be @mentioned',
});
description = intl.formatMessage({
id: 'persistent_notifications.error.no_mentions.description',
defaultMessage: 'There are no recipients mentioned in your message. Youll need add mentions to be able to send persistent notifications.',
});
} else if (usersCount > persistentNotificationMaxRecipients) {
title = intl.formatMessage({
id: 'persistent_notifications.error.max_recipients.title',
defaultMessage: 'Too many recipients',
});
description = intl.formatMessage({
id: 'persistent_notifications.error.max_recipients.description',
defaultMessage: 'You can send persistent notifications to a maximum of {max} recipients. There are {count} recipients mentioned in your message. Youll need to change who youve mentioned before you can send.',
}, {
max: persistentNotificationMaxRecipients,
count: mentionsList.length,
});
} else {
title = intl.formatMessage({
id: 'persistent_notifications.confirm.title',
defaultMessage: 'Send persistent notifications',
});
description = intl.formatMessage({
id: 'persistent_notifications.confirm.description',
defaultMessage: 'Mentioned recipients will be notified every {interval, plural, one {minute} other {{interval} minutes}} until theyve acknowledged or replied to the message.',
}, {
interval: persistentNotificationInterval,
});
buttons = [{
text: intl.formatMessage({
id: 'persistent_notifications.confirm.cancel',
defaultMessage: 'Cancel',
}),
style: 'cancel',
},
{
text: intl.formatMessage({
id: 'persistent_notifications.confirm.send',
defaultMessage: 'Send',
}),
onPress: sendMessage,
}];
}
}
Alert.alert(
title,
description,
buttons,
);
}
export async function sendMessageWithAlert({title, channelName, intl, sendMessageHandler}: {
title: string;
channelName: string;
intl: IntlShape;
sendMessageHandler: () => void;
}) {
const buttons: AlertButton[] = [{
text: intl.formatMessage({
id: 'send_message.confirm.cancel',
defaultMessage: 'Cancel',
}),
style: 'cancel',
}, {
text: intl.formatMessage({
id: 'send_message.confirm.send',
defaultMessage: 'Send',
}),
onPress: sendMessageHandler,
}];
const description = intl.formatMessage({
id: 'send_message.confirm.description',
defaultMessage: 'Are you sure you want to send this message to {channelName} now?',
}, {
channelName,
});
Alert.alert(
title,
description,
buttons,
);
}
export function scheduledPostFromPost(post: Post, schedulingInfo: SchedulingInfo, postPriority?: PostPriority, postFiles?: FileInfo[]): ScheduledPost {
const fileIDs: string[] = [];
if (postFiles) {
postFiles.forEach((file) => {
if (file.id) {
fileIDs.push(file.id);
}
});
}
return {
...post,
scheduled_at: schedulingInfo.scheduled_at,
priority: post.root_id ? undefined : postPriority,
metadata: {
files: postFiles,
},
file_ids: fileIDs,
};
}
export function getPostTranslation(post: Post | PostModel, locale: string): PostTranslation | undefined {
const normalizedLocale = locale.split('-')[0];
return post.metadata?.translations?.[normalizedLocale];
}
export function getPostTranslatedMessage(originalMessage: string, translation: PostTranslation): string {
return translation.object?.message ?? originalMessage;
}