diff --git a/app/actions/local/reactions.ts b/app/actions/local/reactions.ts index 846db4cdb..3303c712e 100644 --- a/app/actions/local/reactions.ts +++ b/app/actions/local/reactions.ts @@ -3,6 +3,7 @@ import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database'; import DatabaseManager from '@database/manager'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {safeParseJSON} from '@utils/helpers'; import type SystemModel from '@typings/database/models/servers/system'; @@ -29,16 +30,17 @@ export const addRecentReaction = async (serverUrl: string, emojiNames: string[], try { const recentEmojis = new Set(recent); - for (const name of emojiNames) { - if (recentEmojis.has(name)) { - recentEmojis.delete(name); + const aliases = emojiNames.map((e) => getEmojiFirstAlias(e)); + for (const alias of aliases) { + if (recentEmojis.has(alias)) { + recentEmojis.delete(alias); } } recent = Array.from(recentEmojis); - for (const name of emojiNames) { - recent.unshift(name); + for (const alias of aliases) { + recent.unshift(alias); } return operator.handleSystem({ systems: [{ diff --git a/app/actions/remote/reactions.ts b/app/actions/remote/reactions.ts index 3444df6ab..360333be0 100644 --- a/app/actions/remote/reactions.ts +++ b/app/actions/remote/reactions.ts @@ -1,14 +1,15 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Model, Q} from '@nozbe/watermelondb'; +import {Model} from '@nozbe/watermelondb'; import {addRecentReaction} from '@actions/local/reactions'; -import {MM_TABLES} from '@constants/database'; import DatabaseManager from '@database/manager'; import NetworkManager from '@init/network_manager'; import {queryRecentPostsInChannel, queryRecentPostsInThread} from '@queries/servers/post'; +import {queryReaction} from '@queries/servers/reaction'; import {queryCurrentChannelId, queryCurrentUserId} from '@queries/servers/system'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {forceLogoutIfNecessary} from './session'; @@ -30,29 +31,41 @@ export const addReaction = async (serverUrl: string, postId: string, emojiName: try { const currentUserId = await queryCurrentUserId(operator.database); - const reaction = await client.addReaction(currentUserId, postId, emojiName); - const models: Model[] = []; + const emojiAlias = getEmojiFirstAlias(emojiName); + const reacted = await queryReaction(operator.database, emojiAlias, postId, currentUserId).fetchCount() > 0; + if (!reacted) { + const reaction = await client.addReaction(currentUserId, postId, emojiAlias); + const models: Model[] = []; - const reactions = await operator.handleReactions({ - postsReactions: [{ + const reactions = await operator.handleReactions({ + postsReactions: [{ + post_id: postId, + reactions: [reaction], + }], + prepareRecordsOnly: true, + skipSync: true, // this prevents the handler from deleting previous reactions + }); + models.push(...reactions); + + const recent = await addRecentReaction(serverUrl, [emojiName], true); + if (Array.isArray(recent)) { + models.push(...recent); + } + + if (models.length) { + await operator.batchRecords(models); + } + + return {reaction}; + } + return { + reaction: { + user_id: currentUserId, post_id: postId, - reactions: [reaction], - }], - prepareRecordsOnly: true, - skipSync: true, // this prevents the handler from deleting previous reactions - }); - models.push(...reactions); - - const recent = await addRecentReaction(serverUrl, [emojiName], true); - if (Array.isArray(recent)) { - models.push(...recent); - } - - if (models.length) { - await operator.batchRecords(models); - } - - return {reaction}; + emoji_name: emojiAlias, + create_at: Date.now(), + } as Reaction, + }; } catch (error) { forceLogoutIfNecessary(serverUrl, error as ClientErrorProps); return {error}; @@ -74,14 +87,11 @@ export const removeReaction = async (serverUrl: string, postId: string, emojiNam try { const currentUserId = await queryCurrentUserId(database); - await client.removeReaction(currentUserId, postId, emojiName); + const emojiAlias = getEmojiFirstAlias(emojiName); + await client.removeReaction(currentUserId, postId, emojiAlias); // should return one or no reaction - const reaction = await database.get(MM_TABLES.SERVER.REACTION).query( - Q.where('emoji_name', emojiName), - Q.where('post_id', postId), - Q.where('user_id', currentUserId), - ).fetch(); + const reaction = await queryReaction(database, emojiAlias, postId, currentUserId).fetch(); if (reaction.length) { await database.write(async () => { diff --git a/app/components/post_list/post/body/reactions/reactions.tsx b/app/components/post_list/post/body/reactions/reactions.tsx index f12619100..8baa12c84 100644 --- a/app/components/post_list/post/body/reactions/reactions.tsx +++ b/app/components/post_list/post/body/reactions/reactions.tsx @@ -10,6 +10,7 @@ import CompassIcon from '@components/compass_icon'; import {MAX_ALLOWED_REACTIONS} from '@constants/emoji'; import {useServerUrl} from '@context/server'; import {showModal, showModalOverCurrentContext} from '@screens/navigation'; +import {getEmojiFirstAlias} from '@utils/emoji/helpers'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; @@ -59,12 +60,12 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, const intl = useIntl(); const serverUrl = useServerUrl(); const pressed = useRef(false); - const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => r.emojiName))); + const [sortedReactions, setSortedReactions] = useState(new Set(reactions.map((r) => getEmojiFirstAlias(r.emojiName)))); const styles = getStyleSheet(theme); useEffect(() => { // This helps keep the reactions in the same position at all times until unmounted - const rs = reactions.map((r) => r.emojiName); + const rs = reactions.map((r) => getEmojiFirstAlias(r.emojiName)); const sorted = new Set([...sortedReactions]); const added = rs.filter((r) => !sorted.has(r)); added.forEach(sorted.add, sorted); @@ -78,14 +79,20 @@ const Reactions = ({currentUserId, canAddReaction, canRemoveReaction, disabled, const reactionsByName = reactions.reduce((acc, reaction) => { if (reaction) { - if (acc.has(reaction.emojiName)) { - acc.get(reaction.emojiName)!.push(reaction); + const emojiAlias = getEmojiFirstAlias(reaction.emojiName); + if (acc.has(emojiAlias)) { + const rs = acc.get(emojiAlias); + // eslint-disable-next-line max-nested-callbacks + const present = rs!.findIndex((r) => r.userId === reaction.userId) > -1; + if (!present) { + rs!.push(reaction); + } } else { - acc.set(reaction.emojiName, [reaction]); + acc.set(emojiAlias, [reaction]); } if (reaction.userId === currentUserId) { - highlightedReactions.push(reaction.emojiName); + highlightedReactions.push(emojiAlias); } } diff --git a/app/queries/servers/reaction.ts b/app/queries/servers/reaction.ts new file mode 100644 index 000000000..8b7176618 --- /dev/null +++ b/app/queries/servers/reaction.ts @@ -0,0 +1,15 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {Database, Q} from '@nozbe/watermelondb'; + +import {MM_TABLES} from '@constants/database'; +const {SERVER: {REACTION}} = MM_TABLES; + +export const queryReaction = (database: Database, emojiName: string, postId: string, userId: string) => { + return database.get(REACTION).query( + Q.where('emoji_name', emojiName), + Q.where('post_id', postId), + Q.where('user_id', userId), + ); +}; diff --git a/app/utils/emoji/helpers.ts b/app/utils/emoji/helpers.ts index 0fb435f9b..f3ea5b334 100644 --- a/app/utils/emoji/helpers.ts +++ b/app/utils/emoji/helpers.ts @@ -193,6 +193,10 @@ export function doesMatchNamedEmoji(emojiName: string) { return false; } +export const getEmojiFirstAlias = (emoji: string) => { + return getEmojiByName(emoji, [])?.short_names?.[0] || emoji; +}; + export function getEmojiByName(emojiName: string, customEmojis: CustomEmojiModel[]) { if (EmojiIndicesByAlias.has(emojiName)) { return Emojis[EmojiIndicesByAlias.get(emojiName)!];