From 683b4aafb2928f2d8dfa626352584542e3df8e3a Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Tue, 19 Dec 2023 09:07:32 +0000
Subject: [PATCH 1/4] real-time updates on note detail view

`useNoteCapture` already subscribes to all updates for a note, so
we can tell it when a note gets replied to, too

Since I'm not actually adding any extra subscription in the client,
just an extra callback, there should be no overhead when replies are
not coming in.

Also, all the timelines already call `useNoteCapture` for each note
displayed, so we know the whole `GlobalEventService` thing works fine.

Many thanks to VueJS for taking care of all the DOM complications
---
 packages/backend/src/core/GlobalEventService.ts     |  3 +++
 packages/backend/src/core/NoteCreateService.ts      |  3 +++
 packages/frontend/src/components/MkNoteDetailed.vue |  5 +++++
 packages/frontend/src/components/MkNoteSub.vue      |  9 +++++++--
 packages/frontend/src/components/SkNoteDetailed.vue |  5 +++++
 packages/frontend/src/components/SkNoteSub.vue      |  9 +++++++--
 packages/frontend/src/scripts/use-note-capture.ts   | 12 ++++++++++++
 7 files changed, 42 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts
index d175f21f2f..95a4eba742 100644
--- a/packages/backend/src/core/GlobalEventService.ts
+++ b/packages/backend/src/core/GlobalEventService.ts
@@ -130,6 +130,9 @@ export interface NoteEventTypes {
 		reaction: string;
 		userId: MiUser['id'];
 	};
+	replied: {
+		id: MiNote['id'];
+	};
 }
 type NoteStreamEventTypes = {
 	[key in keyof NoteEventTypes]: {
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 0b06931213..6406bc4c50 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -780,6 +780,9 @@ export class NoteCreateService implements OnApplicationShutdown {
 
 			// If has in reply to note
 			if (data.reply) {
+				this.globalEventService.publishNoteStream(data.reply.id, 'replied', {
+					id: note.id,
+				});
 				// 通知
 				if (data.reply.userHost === null) {
 					const isThreadMuted = await this.noteThreadMutingsRepository.exist({
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index f29b9db6ae..ae9d8a0d6b 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -372,11 +372,16 @@ const reactionsPagination = computed(() => ({
 	},
 }));
 
+async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+		replies.value.unshift(replyNote);
+}
+
 useNoteCapture({
 	rootEl: el,
 	note: appearNote,
 	pureNote: note,
 	isDeletedRef: isDeleted,
+	onReplyCallback: addReplyTo,
 });
 
 useTooltip(renoteButton, async (showing) => {
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 8d394c0c15..3c840cf598 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -132,6 +132,7 @@ const likeButton = shallowRef<HTMLElement>();
 
 let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const replies = ref<Misskey.entities.Note[]>([]);
 
 const isRenote = (
 	props.note.renote != null &&
@@ -140,10 +141,16 @@ const isRenote = (
 	props.note.poll == null
 );
 
+async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+		replies.value.unshift(replyNote);
+}
+
 useNoteCapture({
 	rootEl: el,
 	note: appearNote,
 	isDeletedRef: isDeleted,
+	// only update replies if we are, in fact, showing replies
+	onReplyCallback: props.detail && props.depth < numberOfReplies.value ? addReplyTo : undefined,
 });
 
 if ($i) {
@@ -250,8 +257,6 @@ watch(() => props.expandAllCws, (expandAllCws) => {
 	if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
 });
 
-let replies = ref<Misskey.entities.Note[]>([]);
-
 function boostVisibility() {
 	os.popupMenu([
 		{
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 8bf9e244e0..ff2058d79f 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -380,11 +380,16 @@ const reactionsPagination = computed(() => ({
 	},
 }));
 
+async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+		replies.value.unshift(replyNote);
+}
+
 useNoteCapture({
 	rootEl: el,
 	note: appearNote,
 	pureNote: note,
 	isDeletedRef: isDeleted,
+	onReplyCallback: addReplyTo,
 });
 
 useTooltip(renoteButton, async (showing) => {
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index fc30dc87aa..f4279fe8a1 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -141,6 +141,7 @@ const likeButton = shallowRef<HTMLElement>();
 
 let appearNote = computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
+const replies = ref<Misskey.entities.Note[]>([]);
 
 const isRenote = (
 	props.note.renote != null &&
@@ -149,10 +150,16 @@ const isRenote = (
 	props.note.poll == null
 );
 
+async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+		replies.value.unshift(replyNote);
+}
+
 useNoteCapture({
 	rootEl: el,
 	note: appearNote,
 	isDeletedRef: isDeleted,
+	// only update replies if we are, in fact, showing replies
+	onReplyCallback: props.detail && props.depth < numberOfReplies.value ? addReplyTo : undefined,
 });
 
 if ($i) {
@@ -259,8 +266,6 @@ watch(() => props.expandAllCws, (expandAllCws) => {
 	if (expandAllCws !== showContent.value) showContent.value = expandAllCws;
 });
 
-let replies = ref<Misskey.entities.Note[]>([]);
-
 function boostVisibility() {
 	os.popupMenu([
 		{
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index ab232598cd..8692d056b0 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -14,6 +14,7 @@ export function useNoteCapture(props: {
 	note: Ref<Misskey.entities.Note>;
 	pureNote: Ref<Misskey.entities.Note>;
 	isDeletedRef: Ref<boolean>;
+	onReplyCallback: (note, replyNote: Misskey.entities.Note) => void | undefined;
 }) {
 	const note = props.note;
 	const pureNote = props.pureNote !== undefined ? props.pureNote : props.note;
@@ -25,6 +26,17 @@ export function useNoteCapture(props: {
 		if ((id !== note.value.id) && (id !== pureNote.value.id)) return;
 
 		switch (type) {
+			case 'replied': {
+				if (!props.onReplyCallback) break;
+
+				const replyNote = await os.api("notes/show", {
+					noteId: body.id,
+				});
+
+				await props.onReplyCallback(pureNote, replyNote);
+				break;
+			}
+
 			case 'reacted': {
 				const reaction = body.reaction;
 

From d06939bd25db133995f1eced8b5420abfd3cbcf0 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Tue, 19 Dec 2023 09:07:38 +0000
Subject: [PATCH 2/4] real-time update: hide deleted replies

---
 packages/frontend/src/components/MkNoteSub.vue | 2 +-
 packages/frontend/src/components/SkNoteSub.vue | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 3c840cf598..fd8904f3c2 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
+<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
 	<div :class="$style.main">
 		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
 		<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index f4279fe8a1..b6e0cd4b35 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
+<div v-show="!isDeleted" v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
 	<div v-if="!hideLine" :class="$style.line"></div>
 	<div :class="$style.main">
 		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>

From 576a87118c18c9354f35ee3cb207d5e6bccdba91 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Thu, 21 Dec 2023 16:00:59 +0000
Subject: [PATCH 3/4] real-time update: adjust replyCount up/down

this also fixes the connecting lines in the Sk-style view

thanks @ShittyKopper for reporting the bug!

NOTE: at this point, the `isDeletedRef` boolean is pretty much
useless, because we're directly removing deleted notes from the
`replies` array and therefore from the DOM (we were just hiding them,
before); I'm intentionally not touching `isDeletedRef` to simplify
merges from upstream
---
 .../frontend/src/components/MkNoteDetailed.vue    | 13 +++++++++++--
 packages/frontend/src/components/MkNoteSub.vue    | 15 +++++++++++++--
 .../frontend/src/components/SkNoteDetailed.vue    | 13 +++++++++++--
 packages/frontend/src/components/SkNoteSub.vue    | 15 +++++++++++++--
 packages/frontend/src/scripts/use-note-capture.ts |  7 +++++--
 5 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ae9d8a0d6b..1ff05fd318 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -170,7 +170,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="!repliesLoaded" style="padding: 16px">
 				<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
 			</div>
-			<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :expandAllCws="props.expandAllCws"/>
+			<MkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" />
 		</div>
 		<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
 			<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
@@ -372,8 +372,17 @@ const reactionsPagination = computed(() => ({
 	},
 }));
 
-async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
+		appearNote.repliesCount += 1;
+}
+
+async function removeReply(id: Misskey.entities.Note['id']) {
+		const replyIdx = replies.value.findIndex(note => note.id === id);
+		if (replyIdx >= 0) {
+			replies.value.splice(replyIdx, 1);
+			appearNote.repliesCount -= 1;
+		}
 }
 
 useNoteCapture({
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index fd8904f3c2..d1e45b30bb 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<template v-if="depth < numberOfReplies">
-		<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws"/>
+		<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/>
 	</template>
 	<div v-else :class="$style.more">
 		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
@@ -110,6 +110,7 @@ const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
 	detail?: boolean;
 	expandAllCws?: boolean;
+	onDeleteCallback?: (id: Misskey.entities.Note['id']) => void;
 
 	// how many notes are in between this one and the note being viewed in detail
 	depth?: number;
@@ -141,8 +142,17 @@ const isRenote = (
 	props.note.poll == null
 );
 
-async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
+		appearNote.repliesCount += 1;
+}
+
+async function removeReply(id: Misskey.entities.Note['id']) {
+		const replyIdx = replies.value.findIndex(note => note.id === id);
+		if (replyIdx >= 0) {
+			replies.value.splice(replyIdx, 1);
+			appearNote.repliesCount -= 1;
+		}
 }
 
 useNoteCapture({
@@ -151,6 +161,7 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 	// only update replies if we are, in fact, showing replies
 	onReplyCallback: props.detail && props.depth < numberOfReplies.value ? addReplyTo : undefined,
+	onDeleteCallback: props.detail && props.depth < numberOfReplies.value ? props.onDeleteCallback : undefined,
 });
 
 if ($i) {
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index ff2058d79f..06783f7fff 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -178,7 +178,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div v-if="!repliesLoaded" style="padding: 16px">
 				<MkButton style="margin: 0 auto;" primary rounded @click="loadReplies">{{ i18n.ts.loadReplies }}</MkButton>
 			</div>
-			<SkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :expandAllCws="props.expandAllCws"/>
+			<SkNoteSub v-for="note in replies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" />
 		</div>
 		<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
 			<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
@@ -380,8 +380,17 @@ const reactionsPagination = computed(() => ({
 	},
 }));
 
-async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
+		appearNote.repliesCount += 1;
+}
+
+async function removeReply(id: Misskey.entities.Note['id']) {
+		const replyIdx = replies.value.findIndex(note => note.id === id);
+		if (replyIdx >= 0) {
+			replies.value.splice(replyIdx, 1);
+			appearNote.repliesCount -= 1;
+		}
 }
 
 useNoteCapture({
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index b6e0cd4b35..bee4074bd0 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -73,7 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 	<template v-if="depth < numberOfReplies">
-		<SkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws"/>
+		<SkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply"/>
 	</template>
 	<div v-else :class="$style.more">
 		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
@@ -119,6 +119,7 @@ const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
 	detail?: boolean;
 	expandAllCws?: boolean;
+	onDeleteCallback?: (id: Misskey.entities.Note['id']) => void;
 
 	// how many notes are in between this one and the note being viewed in detail
 	depth?: number;
@@ -150,8 +151,17 @@ const isRenote = (
 	props.note.poll == null
 );
 
-async function addReplyTo(note, replyNote: Misskey.entities.Note) {
+async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
+		appearNote.repliesCount += 1;
+}
+
+async function removeReply(id: Misskey.entities.Note['id']) {
+		const replyIdx = replies.value.findIndex(note => note.id === id);
+		if (replyIdx >= 0) {
+			replies.value.splice(replyIdx, 1);
+			appearNote.repliesCount -= 1;
+		}
 }
 
 useNoteCapture({
@@ -160,6 +170,7 @@ useNoteCapture({
 	isDeletedRef: isDeleted,
 	// only update replies if we are, in fact, showing replies
 	onReplyCallback: props.detail && props.depth < numberOfReplies.value ? addReplyTo : undefined,
+	onDeleteCallback: props.detail && props.depth < numberOfReplies.value ? props.onDeleteCallback : undefined,
 });
 
 if ($i) {
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts
index 8692d056b0..427bc6ff36 100644
--- a/packages/frontend/src/scripts/use-note-capture.ts
+++ b/packages/frontend/src/scripts/use-note-capture.ts
@@ -14,7 +14,8 @@ export function useNoteCapture(props: {
 	note: Ref<Misskey.entities.Note>;
 	pureNote: Ref<Misskey.entities.Note>;
 	isDeletedRef: Ref<boolean>;
-	onReplyCallback: (note, replyNote: Misskey.entities.Note) => void | undefined;
+	onReplyCallback: (replyNote: Misskey.entities.Note) => void | undefined;
+	onDeleteCallback: (id: Misskey.entities.Note['id']) => void | undefined;
 }) {
 	const note = props.note;
 	const pureNote = props.pureNote !== undefined ? props.pureNote : props.note;
@@ -33,7 +34,7 @@ export function useNoteCapture(props: {
 					noteId: body.id,
 				});
 
-				await props.onReplyCallback(pureNote, replyNote);
+				await props.onReplyCallback(replyNote);
 				break;
 			}
 
@@ -88,6 +89,8 @@ export function useNoteCapture(props: {
 
 			case 'deleted': {
 				props.isDeletedRef.value = true;
+
+				if (props.onDeleteCallback) await props.onDeleteCallback(id);
 				break;
 			}
 

From fea6428245591ca710f718eaff5000194d5ed233 Mon Sep 17 00:00:00 2001
From: dakkar <dakkar@thenautilus.net>
Date: Sat, 23 Dec 2023 14:47:14 +0000
Subject: [PATCH 4/4] add missing `.value`

---
 packages/frontend/src/components/MkNoteDetailed.vue | 4 ++--
 packages/frontend/src/components/MkNoteSub.vue      | 4 ++--
 packages/frontend/src/components/SkNoteDetailed.vue | 4 ++--
 packages/frontend/src/components/SkNoteSub.vue      | 4 ++--
 4 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 1ff05fd318..a793a85ff9 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -374,14 +374,14 @@ const reactionsPagination = computed(() => ({
 
 async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
-		appearNote.repliesCount += 1;
+		appearNote.value.repliesCount += 1;
 }
 
 async function removeReply(id: Misskey.entities.Note['id']) {
 		const replyIdx = replies.value.findIndex(note => note.id === id);
 		if (replyIdx >= 0) {
 			replies.value.splice(replyIdx, 1);
-			appearNote.repliesCount -= 1;
+			appearNote.value.repliesCount -= 1;
 		}
 }
 
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index d1e45b30bb..887a9b5c45 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -144,14 +144,14 @@ const isRenote = (
 
 async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
-		appearNote.repliesCount += 1;
+		appearNote.value.repliesCount += 1;
 }
 
 async function removeReply(id: Misskey.entities.Note['id']) {
 		const replyIdx = replies.value.findIndex(note => note.id === id);
 		if (replyIdx >= 0) {
 			replies.value.splice(replyIdx, 1);
-			appearNote.repliesCount -= 1;
+			appearNote.value.repliesCount -= 1;
 		}
 }
 
diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 06783f7fff..df0259a2c7 100644
--- a/packages/frontend/src/components/SkNoteDetailed.vue
+++ b/packages/frontend/src/components/SkNoteDetailed.vue
@@ -382,14 +382,14 @@ const reactionsPagination = computed(() => ({
 
 async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
-		appearNote.repliesCount += 1;
+		appearNote.value.repliesCount += 1;
 }
 
 async function removeReply(id: Misskey.entities.Note['id']) {
 		const replyIdx = replies.value.findIndex(note => note.id === id);
 		if (replyIdx >= 0) {
 			replies.value.splice(replyIdx, 1);
-			appearNote.repliesCount -= 1;
+			appearNote.value.repliesCount -= 1;
 		}
 }
 
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index bee4074bd0..5928b2676c 100644
--- a/packages/frontend/src/components/SkNoteSub.vue
+++ b/packages/frontend/src/components/SkNoteSub.vue
@@ -153,14 +153,14 @@ const isRenote = (
 
 async function addReplyTo(replyNote: Misskey.entities.Note) {
 		replies.value.unshift(replyNote);
-		appearNote.repliesCount += 1;
+		appearNote.value.repliesCount += 1;
 }
 
 async function removeReply(id: Misskey.entities.Note['id']) {
 		const replyIdx = replies.value.findIndex(note => note.id === id);
 		if (replyIdx >= 0) {
 			replies.value.splice(replyIdx, 1);
-			appearNote.repliesCount -= 1;
+			appearNote.value.repliesCount -= 1;
 		}
 }