From 70f1e0344343e8942db7faa2ed2ae4d2d39e16a5 Mon Sep 17 00:00:00 2001
From: FLY_MC <me@flymc.cc>
Date: Wed, 12 Mar 2025 05:49:01 +0800
Subject: [PATCH] frontend: fix merge reactions

---
 .../components/MkReactionsViewer.reaction.vue |  8 ++-
 .../src/components/MkReactionsViewer.vue      | 61 +++++++++++++------
 2 files changed, 50 insertions(+), 19 deletions(-)

diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 47d5c4a84f..d791d02bdc 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="buttonEl"
 	v-ripple="canToggle"
 	class="_button"
-	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
+	:class="[$style.root, { [$style.reacted]: isReacted, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
 	@click="toggleReaction()"
 	@contextmenu.prevent.stop="menu"
 >
@@ -59,6 +59,12 @@ const canToggle = computed(() => {
 });
 const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
 
+const isReacted = computed(() => {
+	if (!props.note.myReaction) return false;
+
+	return normalizeReaction(props.note.myReaction) === normalizeReaction(props.reaction);
+});
+
 async function toggleReaction() {
 	if (!canToggle.value) return;
 
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 7edfcc6a72..7f85168236 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -70,37 +70,62 @@ function onMockToggleReaction(emoji: string, count: number) {
 
 watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => {
 	initialReactions.value = new Set(Object.keys(newSource));
+
+	const normalizedCounts = new Map<string, number>();
+	const normalizedOriginals = new Map<string, string>();
+
+	for (const [reaction, count] of Object.entries(newSource)) {
+		const normalized = normalizeReaction(reaction);
+		const currentCount = normalizedCounts.get(normalized) || 0;
+		normalizedCounts.set(normalized, currentCount + count);
+
+		if (!normalizedOriginals.has(normalized) || !reaction.includes('@')) {
+			normalizedOriginals.set(normalized, reaction);
+		}
+	}
+
 	let newReactions: [string, number][] = [];
-	hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
 
 	for (let i = 0; i < reactions.value.length; i++) {
-		const reaction = reactions.value[i][0];
-		if (reaction in newSource && newSource[reaction] !== 0) {
-			reactions.value[i][1] = newSource[reaction];
-			newReactions.push(reactions.value[i]);
+		const [reaction] = reactions.value[i];
+		const normalized = normalizeReaction(reaction);
+
+		if (normalizedCounts.has(normalized) && normalizedCounts.get(normalized)! > 0) {
+			newReactions.push([
+				normalizedOriginals.get(normalized)!,
+				normalizedCounts.get(normalized)!,
+			]);
+			normalizedCounts.delete(normalized);
 		}
 	}
 
-	const newReactionsNames = newReactions.map(([x]) => x);
-	newReactions = [
-		...newReactions,
-		...Object.entries(newSource)
-			.sort(([, a], [, b]) => b - a)
-			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
-	];
+	const remainingEntries = Array.from(normalizedCounts.entries())
+		.filter(([, count]) => count > 0)
+		.sort(([, a], [, b]) => b - a);
 
-	newReactions = newReactions.slice(0, props.maxNumber);
+	for (const [normalized, count] of remainingEntries) {
+		if (newReactions.length < maxNumber) {
+			newReactions.push([normalizedOriginals.get(normalized)!, count]);
+		}
+	}
 
-	if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) {
+	hasMoreReactions.value = Object.keys(newSource).length > maxNumber;
+
+	if (props.note.myReaction) {
 		const normalizedMyReaction = normalizeReaction(props.note.myReaction);
-		const alreadyIncluded = newReactions.some(([x]) => normalizeReaction(x) === normalizedMyReaction);
+		const alreadyIncluded = newReactions.some(([x]) =>
+			normalizeReaction(x) === normalizedMyReaction,
+		);
 
-		if (!alreadyIncluded) {
-			newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
+		if (!alreadyIncluded && newSource[props.note.myReaction]) {
+			newReactions.push([
+				props.note.myReaction,
+				newSource[props.note.myReaction],
+			]);
 		}
 	}
 
-	reactions.value = newReactions;
+	reactions.value = newReactions.slice(0, props.maxNumber);
 }, { immediate: true, deep: true });
 </script>