mattermost-mobile/app/utils/post/index.ts
Rajat Dabade 84eded1bde
Mobile drafts (#8280)
* refactor: started with draft, done until new tabs for draft

* refactor: change the query and added the screen for draft

* added condition for fetching draft for channel delete or not

* refactor: added draft screen

* linter fixes

* Added draft post component

* added avatar and header display name for the draft post list

* added channel info component

* channel info completed

* proper naming

* added image file markdown acknowledgement support

* draft actions

* Fix the draft receiver in drafts

* separated send message handler

* Done with send drafts

* done with delete drafts

* change save to send draft

* handle lengthy message with show more button

* done with persistent message edit, send and delete drafts

* added alert for sending message

* added update at time for the drafts

* en.json extract fix

* Updated dependencies for useCallback

* refactor: added drafts list to animated list

* added swipeable component and delete conformation for drafts

* done with rendering of images in markdown for drafts

* en.json issue fixed

* fix en.json issue

* refactor: en.json fix

* addressed review comments

* updated image metadata handling code

* linter fixes

* added the empty draft screen

* linter fix

* style fix

* back button an android takes to the channel list page

* en.json fix

* draft actions theme compatible

* CSS fix for draft channel_info and avatar component

* removed the badge icon and change font style drafts

* fix send alert sender name for GMs

* updated snapshot

* added testId to the drafts components

* updated send draft test id

* clicking on draft takes to the channel

* Added toptip for draft tours

* intl extract

* Rebase to main and reverted local testing changes

* Added tooltip for drafts

* addressed review comments

* reset navigation when click on a draft in draft tabs

* fix the theme issue and navigation issue

* reverted back the draft click navigation changes

* observing draft when hitting back button

* removed the unwanted animiation

* updated regex for parsing markdown

* removed unnecessary checks and change folder name

* removed react memo and merge unwanted observes function

* removed unnecessary comments

* changed the name for observing and querying draft function

* removed memo from component level

* Text to FormattedText component

* Text to formatted text, change image name

* added confirmation modal for deleting draft from bottomsheet

* using common send_handler for both draft and post

* removed magic number for tooltip and bottomsheet

* renamed channel_info to draft_post_header

* text to formattedText for Edit drafts

* removed unnecessary changes

* minor fixes

* mounting draft only when there is draft

* map to reduce

* renamed SwipeableDraft to DraftSwipeAction

* name fixes

* isValidUrl to isParsableUrl and added test

* added test and addressed minor review comments

* added inline component for the duplicate code

* inlt fixes

* clearDraft is not optional

* optimised categories_list.tsx component

* Swipeable to ReanimatedSwipeable, TouchableWithoutFeedback to Pressable and folder name changes

* Added comment and disabled eslint rule for showing warning

* fixed component file name

* minor'

* Removed deprecated Animated createAnimatedComponent flatlist

* added test for missing protocol check

* import change for SwipeableMethod

* active tab for tablet view

* Updated the drafts icons

* Updated compass-icon version to v0.1.48

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
2025-01-13 18:40:03 +05:30

254 lines
9.8 KiB
TypeScript
Raw 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} 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) {
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;
}
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 Post.IGNORE_POST_TYPES.includes(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,
);
}