From 35c7dccb498ac8f068199f09f982e80997ed7bcb Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 15:56:13 +0800
Subject: [PATCH 01/14] fix: use MkPagination in notes for Quote, Boost,
 Reaction

---
 .../backend/src/models/repositories/note.ts   | 22 ++++-
 packages/backend/src/models/schema/note.ts    | 10 ++
 .../server/api/endpoints/notes/reactions.ts   |  2 -
 .../src/server/api/endpoints/notes/renotes.ts | 15 ++-
 .../client/src/components/MkNoteDetailed.vue  | 93 ++++++++++---------
 .../client/src/components/MkReactedUsers.vue  | 31 +++++--
 .../client/src/components/MkRenoteButton.vue  | 16 +---
 packages/firefish-js/src/api.types.ts         |  8 +-
 packages/firefish-js/src/entities.ts          |  2 +
 9 files changed, 131 insertions(+), 68 deletions(-)

diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index c877048709..5974fc24af 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -1,4 +1,4 @@
-import { In } from "typeorm";
+import { In, IsNull, Not } from "typeorm";
 import * as mfm from "mfm-js";
 import { Note } from "@/models/entities/note.js";
 import type { User } from "@/models/entities/user.js";
@@ -10,6 +10,7 @@ import {
 	Followings,
 	Polls,
 	Channels,
+	Notes,
 } from "../index.js";
 import type { Packed } from "@/misc/schema.js";
 import { countReactions, decodeReaction, nyaify } from "backend-rs";
@@ -101,7 +102,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 				return true;
 			} else {
 				// 指定されているかどうか
-				return note.visibleUserIds.some((id: any) => meId === id);
+				return note.visibleUserIds.some((id) => meId === id);
 			}
 		}
 
@@ -211,8 +212,25 @@ export const NoteRepository = db.getRepository(Note).extend({
 			localOnly: note.localOnly || undefined,
 			visibleUserIds:
 				note.visibility === "specified" ? note.visibleUserIds : undefined,
+			// FIXME: Deleting a post does not decrease these two numbers, causing the number to be wrong
 			renoteCount: note.renoteCount,
 			repliesCount: note.repliesCount,
+			// TODO: add it to database and use note.quoteCount
+			quoteCount: Notes.count({
+				where: {
+					renoteId: note.id,
+					text: Not(IsNull()),
+				},
+			}),
+			meRenoteCount: me
+				? Notes.count({
+						where: {
+							renoteId: note.id,
+							text: IsNull(),
+							userId: me.id,
+						},
+					})
+				: undefined,
 			reactions: countReactions(note.reactions),
 			reactionEmojis: reactionEmoji,
 			emojis: noteEmoji,
diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts
index fff872b69f..e0048f7d00 100644
--- a/packages/backend/src/models/schema/note.ts
+++ b/packages/backend/src/models/schema/note.ts
@@ -208,5 +208,15 @@ export const packedNoteSchema = {
 			optional: true,
 			nullable: true,
 		},
+		meRenoteCount: {
+			type: "number",
+			optional: true,
+			nullable: false,
+		},
+		quoteCount: {
+			type: "number",
+			optional: false,
+			nullable: false,
+		},
 	},
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/notes/reactions.ts b/packages/backend/src/server/api/endpoints/notes/reactions.ts
index 386a3a08df..d6ff8888af 100644
--- a/packages/backend/src/server/api/endpoints/notes/reactions.ts
+++ b/packages/backend/src/server/api/endpoints/notes/reactions.ts
@@ -42,8 +42,6 @@ export const paramDef = {
 		type: { type: "string", nullable: true },
 		limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
 		offset: { type: "integer", default: 0 },
-		sinceId: { type: "string", format: "misskey:id" },
-		untilId: { type: "string", format: "misskey:id" },
 	},
 	required: ["noteId"],
 } as const;
diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index 683004ebe8..282777b2e4 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -42,6 +42,12 @@ export const paramDef = {
 		limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
 		sinceId: { type: "string", format: "misskey:id" },
 		untilId: { type: "string", format: "misskey:id" },
+		filter: {
+			type: "string",
+			enum: ["boost", "quote"],
+			nullable: true,
+			default: null,
+		},
 	},
 	required: ["noteId"],
 } as const;
@@ -53,7 +59,7 @@ export default define(meta, paramDef, async (ps, user) => {
 		throw err;
 	});
 
-	let query = makePaginationQuery(
+	const query = makePaginationQuery(
 		Notes.createQueryBuilder("note"),
 		ps.sinceId,
 		ps.untilId,
@@ -61,6 +67,13 @@ export default define(meta, paramDef, async (ps, user) => {
 		.andWhere("note.renoteId = :renoteId", { renoteId: note.id })
 		.innerJoinAndSelect("note.user", "user");
 
+	if (ps.filter === "boost") {
+		query.andWhere("note.text IS NULL");
+	}
+	if (ps.filter === "quote") {
+		query.andWhere("note.text IS NOT NULL");
+	}
+
 	if (ps.userId) {
 		query.andWhere("user.id = :userId", { userId: ps.userId });
 	}
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index b6c0d784ba..71e4aeb783 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -64,11 +64,11 @@
 					)
 				}}
 			</option>
-			<option v-if="directQuotes && directQuotes.length > 0" value="quotes">
+			<option v-if="note.quoteCount > 0" value="quotes">
 				<!-- <i :class="icon('ph-quotes')"></i> -->
 				{{
 					wordWithCount(
-						directQuotes.length,
+						note.quoteCount,
 						i18n.ts.quote,
 						i18n.ts.quotes,
 					)
@@ -92,32 +92,33 @@
 		/>
 		<MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" />
 
-		<MkNoteSub
-			v-for="note in directQuotes"
-			v-if="directQuotes && tab === 'quotes'"
-			:key="note.id"
-			:note="note"
-			class="reply"
-			:conversation="replies"
-			:detailed-view="true"
-			:parent-id="note.id"
-		/>
-		<MkLoading v-else-if="tab === 'quotes' && directQuotes && directQuotes.length > 0" />
+		<MkPagination
+			v-if="tab === 'quotes'"
+			v-slot="{ items }"
+			:pagination="quotePagination"
+		>
+			<MkNoteSub
+				v-for="note in items"
+				:key="note.id"
+				:note="note"
+				class="reply"
+				:conversation="items"
+				:detailed-view="true"
+				:parent-id="note.id"
+			/>
+		</MkPagination>
 
-		<!-- <MkPagination
+		<MkPagination
 			v-if="tab === 'renotes'"
 			v-slot="{ items }"
-			ref="pagingComponent"
-			:pagination="pagination"
-		> -->
-		<MkUserCardMini
-			v-for="item in renotes"
-			v-if="tab === 'renotes' && renotes"
-			:key="item.user.id"
-			:user="item.user"
-		/>
-		<!-- </MkPagination> -->
-		<MkLoading v-else-if="tab === 'renotes' && note.renoteCount > 0" />
+			:pagination="renotePagination"
+		>
+			<MkUserCardMini
+				v-for="item in items"
+				:key="item.user.id"
+				:user="item.user"
+			/>
+		</MkPagination>
 
 		<div v-if="tab === 'clips' && clips.length > 0" class="_content clips">
 			<MkA
@@ -186,6 +187,7 @@ import { getNoteMenu } from "@/scripts/get-note-menu";
 import { useNoteCapture } from "@/scripts/use-note-capture";
 import { deepClone } from "@/scripts/clone";
 import { useStream } from "@/stream";
+import MkPagination, { Paging } from "@/components/MkPagination.vue";
 // import icon from "@/scripts/icon";
 
 const props = defineProps<{
@@ -247,7 +249,6 @@ const replies = ref<entities.Note[]>([]);
 const directReplies = ref<null | entities.Note[]>([]);
 const directQuotes = ref<null | entities.Note[]>([]);
 const clips = ref();
-const renotes = ref();
 const isRenote = ref(note.value.renoteId != null);
 let isScrolling: boolean;
 
@@ -401,24 +402,32 @@ os.api("notes/clips", {
 	clips.value = res;
 });
 
-// const pagination = {
-// 	endpoint: "notes/renotes",
-// 	noteId: note.id,
-// 	limit: 10,
-// };
+const renotePagination = {
+	endpoint: "notes/renotes" as const,
+	limit: 30,
+	params: {
+		noteId: note.value.id,
+		filter: "boost" as const,
+	},
+};
+const quotePagination = {
+	endpoint: "notes/renotes" as const,
+	limit: 30,
+	params: {
+		noteId: note.value.id,
+		filter: "quote" as const,
+	},
+};
 
-// const pagingComponent = $ref<InstanceType<typeof MkPagination>>();
-
-renotes.value = null;
 function loadTab() {
-	if (tab.value === "renotes" && !renotes.value) {
-		os.api("notes/renotes", {
-			noteId: note.value.id,
-			limit: 100,
-		}).then((res) => {
-			renotes.value = res;
-		});
-	}
+	// if (tab.value === "renotes" && !renotes.value) {
+	// 	os.api("notes/renotes", {
+	// 		noteId: note.value.id,
+	// 		limit: 100,
+	// 	}).then((res) => {
+	// 		renotes.value = res;
+	// 	});
+	// }
 }
 
 async function onNoteUpdated(
diff --git a/packages/client/src/components/MkReactedUsers.vue b/packages/client/src/components/MkReactedUsers.vue
index ae7a1d6d7d..76b86ed46f 100644
--- a/packages/client/src/components/MkReactedUsers.vue
+++ b/packages/client/src/components/MkReactedUsers.vue
@@ -23,7 +23,13 @@
 				}}</span>
 			</button>
 		</div>
-		<MkUserCardMini v-for="user in users" :key="user.id" :user="user" />
+		<MkPagination
+			ref="pagingComponent"
+			:pagination="pagination"
+			v-slot="{ items }"
+		>
+			<MkUserCardMini v-for="{ user: user } in items" :key="user.id" :user="user" />
+		</MkPagination>
 	</div>
 	<div v-else>
 		<MkLoading />
@@ -36,6 +42,9 @@ import type { entities } from "firefish-js";
 import MkReactionIcon from "@/components/MkReactionIcon.vue";
 import MkUserCardMini from "@/components/MkUserCardMini.vue";
 import * as os from "@/os";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 
 const props = defineProps<{
 	noteId: entities.Note["id"];
@@ -44,16 +53,22 @@ const props = defineProps<{
 const note = ref<entities.Note>();
 const tab = ref<string | null>(null);
 const reactions = ref<string[]>();
-const users = ref();
 
-async function updateUsers(): void {
-	const res = await os.api("notes/reactions", {
+const pagingComponent = ref<MkPaginationType<"notes/reactions"> | null>(null);
+
+const pagination = {
+	endpoint: "notes/reactions" as const,
+	params: {
 		noteId: props.noteId,
 		type: tab.value,
-		limit: 30,
-	});
+	},
+	offsetMode: true,
+	limit: 30,
+};
 
-	users.value = res.map((x) => x.user);
+function updateUsers(): void {
+	pagination.params.type = tab.value;
+	pagingComponent.value?.reload();
 }
 
 watch(tab, updateUsers);
@@ -64,7 +79,7 @@ onMounted(() => {
 	}).then(async (res) => {
 		reactions.value = Object.keys(res.reactions);
 		note.value = res;
-		await updateUsers();
+		// updateUsers();
 	});
 });
 </script>
diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue
index 7250757da4..23911eef37 100644
--- a/packages/client/src/components/MkRenoteButton.vue
+++ b/packages/client/src/components/MkRenoteButton.vue
@@ -27,7 +27,7 @@ import Ripple from "@/components/MkRipple.vue";
 import XDetails from "@/components/MkUsersTooltip.vue";
 import { pleaseLogin } from "@/scripts/please-login";
 import * as os from "@/os";
-import { isSignedIn, me } from "@/me";
+import { me } from "@/me";
 import { useTooltip } from "@/scripts/use-tooltip";
 import { i18n } from "@/i18n";
 import { defaultStore } from "@/store";
@@ -72,17 +72,9 @@ useTooltip(buttonRef, async (showing) => {
 	);
 });
 
-const hasRenotedBefore = ref(false);
-
-if (isSignedIn) {
-	os.api("notes/renotes", {
-		noteId: props.note.id,
-		userId: me!.id,
-		limit: 1,
-	}).then((res) => {
-		hasRenotedBefore.value = res.length > 0;
-	});
-}
+const hasRenotedBefore = ref(
+	props.note.meRenoteCount && props.note.meRenoteCount > 0,
+);
 
 const renote = (viaKeyboard = false, ev?: MouseEvent) => {
 	pleaseLogin();
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index c0a7c9019b..6c0e11bec5 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -773,7 +773,12 @@ export type Endpoints = {
 		res: null;
 	};
 	"notes/reactions": {
-		req: { noteId: Note["id"]; type?: string | null; limit?: number };
+		req: {
+			noteId: Note["id"];
+			type?: string | null;
+			limit?: number;
+			offset?: number;
+		};
 		res: NoteReaction[];
 	};
 	"notes/reactions/create": {
@@ -787,6 +792,7 @@ export type Endpoints = {
 			sinceId?: Note["id"];
 			untilId?: Note["id"];
 			noteId: Note["id"];
+			filter?: "boost" | "quote";
 		};
 		res: Note[];
 	};
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index 457d7ac935..aace6de166 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -174,9 +174,11 @@ export type Note = {
 	channelId?: Channel["id"];
 	channel?: Channel;
 	myReaction?: string;
+	meRenoteCount?: number;
 	reactions: Record<string, number>;
 	renoteCount: number;
 	repliesCount: number;
+	quoteCount: number;
 	poll?: {
 		expiresAt: DateString | null;
 		multiple: boolean;

From 39a229b8753df37a15842bb957cdf2dff57ead3b Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 20:55:47 +0800
Subject: [PATCH 02/14] fix: use MkPagination for replies

---
 biome.json                                    |   2 +-
 .../api/common/make-pagination-query.ts       |   4 +-
 .../client/src/components/MkNoteDetailed.vue  | 149 +++++-------------
 packages/client/src/components/MkNoteSub.vue  | 103 +++++++++---
 .../client/src/components/MkPagination.vue    |  68 ++++----
 .../client/src/scripts/use-note-capture.ts    |  17 +-
 6 files changed, 172 insertions(+), 171 deletions(-)

diff --git a/biome.json b/biome.json
index 21b711f457..487165266a 100644
--- a/biome.json
+++ b/biome.json
@@ -14,7 +14,7 @@
 	},
 	"overrides": [
 		{
-			"include": ["*.vue"],
+			"include": ["*.vue", "packages/client/*.ts"],
 			"linter": {
 				"rules": {
 					"style": {
diff --git a/packages/backend/src/server/api/common/make-pagination-query.ts b/packages/backend/src/server/api/common/make-pagination-query.ts
index a2c3275693..83827b3df1 100644
--- a/packages/backend/src/server/api/common/make-pagination-query.ts
+++ b/packages/backend/src/server/api/common/make-pagination-query.ts
@@ -1,6 +1,6 @@
-import type { SelectQueryBuilder } from "typeorm";
+import type { ObjectLiteral, SelectQueryBuilder } from "typeorm";
 
-export function makePaginationQuery<T>(
+export function makePaginationQuery<T extends ObjectLiteral>(
 	q: SelectQueryBuilder<T>,
 	sinceId?: string,
 	untilId?: string,
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index 71e4aeb783..30f1af0829 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -33,7 +33,7 @@
 			@contextmenu.stop="onContextmenu"
 		></MkNote>
 
-		<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
+		<MkTab v-model="tab" :style="'underline'">
 			<option value="replies">
 				<!-- <i :class="icon('ph-arrow-u-up-left')"></i> -->
 				{{
@@ -80,17 +80,22 @@
 			</option>
 		</MkTab>
 
-		<MkNoteSub
-			v-for="note in directReplies"
-			v-if="directReplies && tab === 'replies'"
-			:key="note.id"
-			:note="note"
-			class="reply"
-			:conversation="replies"
-			:detailed-view="true"
-			:parent-id="note.id"
-		/>
-		<MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" />
+		<MkPagination
+			ref="repliesPagingComponent"
+			v-if="tab === 'replies' && note.repliesCount > 0"
+			v-slot="{ items }"
+			:pagination="repliesPagination"
+		>
+			<MkNoteSub
+				v-for="note in items"
+				:key="note.id"
+				:note="note"
+				class="reply"
+				:auto-conversation="true"
+				:detailed-view="true"
+				:parent-id="note.id"
+			/>
+		</MkPagination>
 
 		<MkPagination
 			v-if="tab === 'quotes'"
@@ -102,7 +107,7 @@
 				:key="note.id"
 				:note="note"
 				class="reply"
-				:conversation="items"
+				:auto-conversation="true"
 				:detailed-view="true"
 				:parent-id="note.id"
 			/>
@@ -167,8 +172,8 @@
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onUnmounted, onUpdated, ref } from "vue";
-import type { StreamTypes, entities } from "firefish-js";
+import { onMounted, onUpdated, ref } from "vue";
+import type { entities } from "firefish-js";
 import MkTab from "@/components/MkTab.vue";
 import MkNote from "@/components/MkNote.vue";
 import MkNoteSub from "@/components/MkNoteSub.vue";
@@ -186,8 +191,9 @@ import { i18n } from "@/i18n";
 import { getNoteMenu } from "@/scripts/get-note-menu";
 import { useNoteCapture } from "@/scripts/use-note-capture";
 import { deepClone } from "@/scripts/clone";
-import { useStream } from "@/stream";
-import MkPagination, { Paging } from "@/components/MkPagination.vue";
+import MkPagination, {
+	type MkPaginationType,
+} from "@/components/MkPagination.vue";
 // import icon from "@/scripts/icon";
 
 const props = defineProps<{
@@ -195,8 +201,6 @@ const props = defineProps<{
 	pinned?: boolean;
 }>();
 
-const stream = useStream();
-
 const tab = ref("replies");
 
 const note = ref(deepClone(props.note));
@@ -227,6 +231,10 @@ if (noteViewInterruptors.length > 0) {
 	});
 }
 
+const repliesPagingComponent = ref<MkPaginationType<"notes/replies"> | null>(
+	null,
+);
+
 const el = ref<HTMLElement | null>(null);
 const noteEl = ref();
 const menuButton = ref<HTMLElement>();
@@ -245,9 +253,6 @@ const muted = ref(
 const translation = ref(null);
 const translating = ref(false);
 const conversation = ref<null | entities.Note[]>([]);
-const replies = ref<entities.Note[]>([]);
-const directReplies = ref<null | entities.Note[]>([]);
-const directQuotes = ref<null | entities.Note[]>([]);
 const clips = ref();
 const isRenote = ref(note.value.renoteId != null);
 let isScrolling: boolean;
@@ -270,6 +275,10 @@ useNoteCapture({
 	rootEl: el,
 	note,
 	isDeletedRef: isDeleted,
+	onReplied: (replyNote) => {
+		note.value.repliesCount += 1;
+		repliesPagingComponent.value?.append(replyNote);
+	},
 });
 
 function reply(_viaKeyboard = false): void {
@@ -358,32 +367,6 @@ function blur() {
 	noteEl.value.blur();
 }
 
-directReplies.value = null;
-os.api("notes/children", {
-	noteId: note.value.id,
-	limit: 30,
-	depth: 12,
-}).then((res) => {
-	// biome-ignore lint/style/noParameterAssign: assign it intentially
-	res = res
-		.filter((n) => n.userId !== note.value.userId)
-		.reverse()
-		.concat(res.filter((n) => n.userId === note.value.userId));
-	// res = res.reduce((acc: entities.Note[], resNote) => {
-	// 	if (resNote.userId === note.value.userId) {
-	// 		return [...acc, resNote];
-	// 	}
-	// 	return [resNote, ...acc];
-	// }, []);
-	replies.value = res;
-	directReplies.value = res
-		.filter((resNote) => resNote.replyId === note.value.id)
-		.reverse();
-	directQuotes.value = res.filter(
-		(resNote) => resNote.renoteId === note.value.id,
-	);
-});
-
 conversation.value = null;
 if (note.value.replyId) {
 	os.api("notes/conversation", {
@@ -402,6 +385,15 @@ os.api("notes/clips", {
 	clips.value = res;
 });
 
+const repliesPagination = {
+	endpoint: "notes/replies" as const,
+	limit: 10,
+	params: {
+		noteId: note.value.id,
+	},
+	ascending: true,
+};
+
 const renotePagination = {
 	endpoint: "notes/renotes" as const,
 	limit: 30,
@@ -419,68 +411,11 @@ const quotePagination = {
 	},
 };
 
-function loadTab() {
-	// if (tab.value === "renotes" && !renotes.value) {
-	// 	os.api("notes/renotes", {
-	// 		noteId: note.value.id,
-	// 		limit: 100,
-	// 	}).then((res) => {
-	// 		renotes.value = res;
-	// 	});
-	// }
-}
-
-async function onNoteUpdated(
-	noteData: StreamTypes.NoteUpdatedEvent,
-): Promise<void> {
-	const { type, id, body } = noteData;
-
-	let found = -1;
-	if (id === note.value.id) {
-		found = 0;
-	} else {
-		for (let i = 0; i < replies.value.length; i++) {
-			const reply = replies.value[i];
-			if (reply.id === id) {
-				found = i + 1;
-				break;
-			}
-		}
-	}
-
-	if (found === -1) {
-		return;
-	}
-
-	switch (type) {
-		case "replied": {
-			const { id: createdId } = body;
-			const replyNote = await os.api("notes/show", {
-				noteId: createdId,
-			});
-
-			replies.value.splice(found, 0, replyNote);
-			if (found === 0) {
-				directReplies.value!.push(replyNote);
-			}
-			break;
-		}
-		case "deleted":
-			if (found === 0) {
-				isDeleted.value = true;
-			} else {
-				replies.value.splice(found - 1, 1);
-			}
-			break;
-	}
-}
-
 document.addEventListener("wheel", () => {
 	isScrolling = true;
 });
 
 onMounted(() => {
-	stream.on("noteUpdated", onNoteUpdated);
 	isScrolling = false;
 	noteEl.value.scrollIntoView();
 });
@@ -493,10 +428,6 @@ onUpdated(() => {
 		}
 	}
 });
-
-onUnmounted(() => {
-	stream.off("noteUpdated", onNoteUpdated);
-});
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index c7cf06d2e5..a71580c0be 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -22,7 +22,7 @@
 			<div class="avatar-container">
 				<MkAvatar class="avatar" :user="appearNote.user" />
 				<div
-					v-if="!conversation || replies.length > 0"
+					v-if="conversation == null || replies.length > 0"
 					class="line"
 				></div>
 			</div>
@@ -148,20 +148,31 @@
 				</footer>
 			</div>
 		</div>
-		<template v-if="conversation">
-			<MkNoteSub
-				v-for="reply in replies"
-				v-if="replyLevel < 11 && depth < 5"
-				:key="reply.id"
-				:note="reply"
-				class="reply"
-				:class="{ single: replies.length == 1 }"
-				:conversation="conversation"
-				:depth="replies.length == 1 ? depth : depth + 1"
-				:reply-level="replyLevel + 1"
-				:parent-id="appearNote.id"
-				:detailed-view="detailedView"
-			/>
+		<MkLoading v-if="conversationLoading" />
+		<template v-else-if="conversation">
+			<template
+				v-if="replyLevel < REPLY_LEVEL_UPPERBOUND && depth < DEPTH_UPPERBOUND"
+			>
+				<MkNoteSub
+					v-for="reply in replies.slice(0, REPLIES_LIMIT)"
+					:key="reply.id"
+					:note="reply"
+					class="reply"
+					:class="{ single: replies.length == 1 }"
+					:conversation="conversation"
+					:depth="replies.length == 1 ? depth : depth + 1"
+					:reply-level="replyLevel + 1"
+					:parent-id="appearNote.id"
+					:detailed-view="detailedView"
+				/>
+				<div v-if="hasMoreReplies" class="more">
+					<div class="line"></div>
+					<MkA class="text _link" :to="notePage(note)"
+						>{{ i18n.ts.continueThread }}
+						<i :class="icon('ph-caret-double-right')"></i
+					></MkA>
+				</div>
+			</template>
 			<div v-else-if="replies.length > 0" class="more">
 				<div class="line"></div>
 				<MkA class="text _link" :to="notePage(note)"
@@ -190,7 +201,7 @@
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, ref } from "vue";
+import { computed, inject, ref, watch } from "vue";
 import type { Ref } from "vue";
 import type { entities } from "firefish-js";
 import XNoteHeader from "@/components/MkNoteHeader.vue";
@@ -221,13 +232,17 @@ import type { NoteTranslation } from "@/types/note";
 
 const router = useRouter();
 
+const REPLIES_LIMIT = 10;
+const REPLY_LEVEL_UPPERBOUND = 11;
+const DEPTH_UPPERBOUND = 5;
+
 const props = withDefaults(
 	defineProps<{
 		note: entities.Note;
 		conversation?: entities.Note[];
-		parentId?;
-		detailedView?;
-
+		autoConversation?: boolean;
+		parentId?: string;
+		detailedView?: boolean;
 		// how many notes are in between this one and the note being viewed in detail
 		depth?: number;
 		// the actual reply level of this note within the conversation thread
@@ -251,6 +266,44 @@ const softMuteReasonI18nSrc = (what?: string) => {
 	return i18n.ts.userSaysSomething;
 };
 
+const conversation = ref(props.conversation);
+const conversationLoading = ref(false);
+const replies = ref<entities.Note[]>([]);
+const hasMoreReplies = ref(false);
+
+function updateReplies() {
+	replies.value = (conversation.value ?? [])
+		.filter(
+			(item) =>
+				item.replyId === props.note.id || item.renoteId === props.note.id,
+		)
+		.reverse();
+	hasMoreReplies.value = replies.value.length >= REPLIES_LIMIT + 1;
+}
+
+watch(conversation, updateReplies, {
+	immediate: true,
+});
+
+if (props.autoConversation) {
+	if (note.value.repliesCount > 0 || note.value.renoteCount > 0) {
+		conversationLoading.value = true;
+		os.api("notes/children", {
+			noteId: note.value.id,
+			limit: REPLIES_LIMIT + 1,
+			depth: REPLY_LEVEL_UPPERBOUND + 1,
+		}).then((res) => {
+			conversation.value = res
+				.filter((n) => n.userId !== note.value.userId)
+				.reverse()
+				.concat(res.filter((n) => n.userId === note.value.userId));
+			conversationLoading.value = false;
+		});
+	} else {
+		conversation.value = [];
+	}
+}
+
 const isRenote =
 	note.value.renote != null &&
 	note.value.text == null &&
@@ -277,13 +330,6 @@ const muted = ref(
 );
 const translation = ref<NoteTranslation | null>(null);
 const translating = ref(false);
-const replies: entities.Note[] =
-	props.conversation
-		?.filter(
-			(item) =>
-				item.replyId === props.note.id || item.renoteId === props.note.id,
-		)
-		.reverse() ?? [];
 const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
 const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
 const lang = localStorage.getItem("lang");
@@ -329,6 +375,11 @@ useNoteCapture({
 	rootEl: el,
 	note: appearNote,
 	isDeletedRef: isDeleted,
+	onReplied: (note) => {
+		if (hasMoreReplies.value === false) {
+			conversation.value = (conversation.value ?? []).concat([note]);
+		}
+	},
 });
 
 function reply(_viaKeyboard = false): void {
diff --git a/packages/client/src/components/MkPagination.vue b/packages/client/src/components/MkPagination.vue
index 03a1f0e35f..2f092cb8ad 100644
--- a/packages/client/src/components/MkPagination.vue
+++ b/packages/client/src/components/MkPagination.vue
@@ -68,7 +68,15 @@
 
 <script lang="ts" setup generic="E extends PagingKey">
 import type { ComponentPublicInstance, ComputedRef } from "vue";
-import { computed, isRef, onActivated, onDeactivated, ref, watch } from "vue";
+import {
+	computed,
+	isRef,
+	onActivated,
+	onDeactivated,
+	ref,
+	unref,
+	watch,
+} from "vue";
 import type { Endpoints, TypeUtils } from "firefish-js";
 import * as os from "@/os";
 import {
@@ -122,6 +130,12 @@ export interface Paging<E extends PagingKey = PagingKey> {
 	 */
 	reversed?: boolean;
 
+	/**
+	 * For not-reversed, not-offsetMode,
+	 * Sort by id in ascending order
+	 */
+	ascending?: boolean;
+
 	offsetMode?: boolean;
 }
 
@@ -169,17 +183,19 @@ const init = async (): Promise<void> => {
 	queue.value = [];
 	fetching.value = true;
 
-	const params = props.pagination.params
-		? isRef<Param>(props.pagination.params)
-			? props.pagination.params.value
-			: props.pagination.params
-		: {};
+	const params = props.pagination.params ? unref(props.pagination.params) : {};
 	await os
 		.api(props.pagination.endpoint, {
 			...params,
 			limit: props.pagination.noPaging
 				? props.pagination.limit || 10
 				: (props.pagination.limit || 10) + 1,
+			...(props.pagination.ascending
+				? {
+						// An initial value smaller than all possible ids must be filled in here.
+						sinceId: "0",
+					}
+				: {}),
 		})
 		.then(
 			(res: Item[]) => {
@@ -196,10 +212,10 @@ const init = async (): Promise<void> => {
 					res.length > (props.pagination.limit || 10)
 				) {
 					res.pop();
-					items.value = props.pagination.reversed ? [...res].reverse() : res;
+					items.value = props.pagination.reversed ? res.toReversed() : res;
 					more.value = true;
 				} else {
-					items.value = props.pagination.reversed ? [...res].reverse() : res;
+					items.value = props.pagination.reversed ? res.toReversed() : res;
 					more.value = false;
 				}
 				offset.value = res.length;
@@ -219,11 +235,7 @@ const reload = (): Promise<void> => {
 };
 
 const refresh = async (): Promise<void> => {
-	const params = props.pagination.params
-		? isRef<Param>(props.pagination.params)
-			? props.pagination.params.value
-			: props.pagination.params
-		: {};
+	const params = props.pagination.params ? unref(props.pagination.params) : {};
 	await os
 		.api(props.pagination.endpoint, {
 			...params,
@@ -269,11 +281,7 @@ const fetchMore = async (): Promise<void> => {
 		return;
 	moreFetching.value = true;
 	backed.value = true;
-	const params = props.pagination.params
-		? isRef<Param>(props.pagination.params)
-			? props.pagination.params.value
-			: props.pagination.params
-		: {};
+	const params = props.pagination.params ? unref(props.pagination.params) : {};
 	await os
 		.api(props.pagination.endpoint, {
 			...params,
@@ -286,9 +294,13 @@ const fetchMore = async (): Promise<void> => {
 					? {
 							sinceId: items.value[0].id,
 						}
-					: {
-							untilId: items.value[items.value.length - 1].id,
-						}),
+					: props.pagination.ascending
+						? {
+								sinceId: items.value[items.value.length - 1].id,
+							}
+						: {
+								untilId: items.value[items.value.length - 1].id,
+							}),
 		})
 		.then(
 			(res: Item[]) => {
@@ -303,12 +315,12 @@ const fetchMore = async (): Promise<void> => {
 				if (res.length > SECOND_FETCH_LIMIT) {
 					res.pop();
 					items.value = props.pagination.reversed
-						? [...res].reverse().concat(items.value)
+						? res.toReversed().concat(items.value)
 						: items.value.concat(res);
 					more.value = true;
 				} else {
 					items.value = props.pagination.reversed
-						? [...res].reverse().concat(items.value)
+						? res.toReversed().concat(items.value)
 						: items.value.concat(res);
 					more.value = false;
 				}
@@ -330,11 +342,7 @@ const fetchMoreAhead = async (): Promise<void> => {
 	)
 		return;
 	moreFetching.value = true;
-	const params = props.pagination.params
-		? isRef<Param>(props.pagination.params)
-			? props.pagination.params.value
-			: props.pagination.params
-		: {};
+	const params = props.pagination.params ? unref(props.pagination.params) : {};
 	await os
 		.api(props.pagination.endpoint, {
 			...params,
@@ -356,12 +364,12 @@ const fetchMoreAhead = async (): Promise<void> => {
 				if (res.length > SECOND_FETCH_LIMIT) {
 					res.pop();
 					items.value = props.pagination.reversed
-						? [...res].reverse().concat(items.value)
+						? res.toReversed().concat(items.value)
 						: items.value.concat(res);
 					more.value = true;
 				} else {
 					items.value = props.pagination.reversed
-						? [...res].reverse().concat(items.value)
+						? res.toReversed().concat(items.value)
 						: items.value.concat(res);
 					more.value = false;
 				}
diff --git a/packages/client/src/scripts/use-note-capture.ts b/packages/client/src/scripts/use-note-capture.ts
index 1bc32d5246..2d0a203fa7 100644
--- a/packages/client/src/scripts/use-note-capture.ts
+++ b/packages/client/src/scripts/use-note-capture.ts
@@ -9,6 +9,7 @@ export function useNoteCapture(props: {
 	rootEl: Ref<HTMLElement | null>;
 	note: Ref<entities.Note>;
 	isDeletedRef: Ref<boolean>;
+	onReplied?: (note: entities.Note) => void;
 }) {
 	const note = props.note;
 	const connection = isSignedIn ? useStream() : null;
@@ -19,6 +20,16 @@ export function useNoteCapture(props: {
 		if (id !== note.value.id) return;
 
 		switch (type) {
+			case "replied": {
+				if (props.onReplied) {
+					const { id: createdId } = body;
+					const replyNote = await os.api("notes/show", {
+						noteId: createdId,
+					});
+					props.onReplied(replyNote);
+				}
+				break;
+			}
 			case "reacted": {
 				const reaction = body.reaction;
 
@@ -34,7 +45,7 @@ export function useNoteCapture(props: {
 
 				note.value.reactions[reaction] = currentCount + 1;
 
-				if (isSignedIn && body.userId === me.id) {
+				if (isSignedIn && body.userId === me!.id) {
 					note.value.myReaction = reaction;
 				}
 				break;
@@ -48,7 +59,7 @@ export function useNoteCapture(props: {
 
 				note.value.reactions[reaction] = Math.max(0, currentCount - 1);
 
-				if (isSignedIn && body.userId === me.id) {
+				if (isSignedIn && body.userId === me!.id) {
 					note.value.myReaction = undefined;
 				}
 				break;
@@ -62,7 +73,7 @@ export function useNoteCapture(props: {
 					choices[choice] = {
 						...choices[choice],
 						votes: choices[choice].votes + 1,
-						...(isSignedIn && body.userId === me.id
+						...(isSignedIn && body.userId === me!.id
 							? {
 									isVoted: true,
 								}

From e5e2eedfb4e936a339d550c2110df56e37f51184 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 22:04:25 +0800
Subject: [PATCH 03/14] fix: Some SubNote incorrectly added replies

---
 packages/client/src/components/MkNoteDetailed.vue   | 2 ++
 packages/client/src/components/MkNoteSub.vue        | 6 ++++++
 packages/client/src/components/MkSubNoteContent.vue | 6 +++---
 3 files changed, 11 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index 30f1af0829..55a13956f6 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -94,6 +94,7 @@
 				:auto-conversation="true"
 				:detailed-view="true"
 				:parent-id="note.id"
+				:auto-add-replies="true"
 			/>
 		</MkPagination>
 
@@ -110,6 +111,7 @@
 				:auto-conversation="true"
 				:detailed-view="true"
 				:parent-id="note.id"
+				:auto-add-replies="true"
 			/>
 		</MkPagination>
 
diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index a71580c0be..f83c676cc1 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -164,6 +164,7 @@
 					:reply-level="replyLevel + 1"
 					:parent-id="appearNote.id"
 					:detailed-view="detailedView"
+					:auto-add-replies="true"
 				/>
 				<div v-if="hasMoreReplies" class="more">
 					<div class="line"></div>
@@ -247,10 +248,12 @@ const props = withDefaults(
 		depth?: number;
 		// the actual reply level of this note within the conversation thread
 		replyLevel?: number;
+		autoAddReplies?: boolean;
 	}>(),
 	{
 		depth: 1,
 		replyLevel: 1,
+		autoAddReplies: false,
 	},
 );
 
@@ -376,6 +379,9 @@ useNoteCapture({
 	note: appearNote,
 	isDeletedRef: isDeleted,
 	onReplied: (note) => {
+		if (props.autoAddReplies !== true) {
+			return;
+		}
 		if (hasMoreReplies.value === false) {
 			conversation.value = (conversation.value ?? []).concat([note]);
 		}
diff --git a/packages/client/src/components/MkSubNoteContent.vue b/packages/client/src/components/MkSubNoteContent.vue
index 7d350085bb..a0664c28e9 100644
--- a/packages/client/src/components/MkSubNoteContent.vue
+++ b/packages/client/src/components/MkSubNoteContent.vue
@@ -1,7 +1,7 @@
 <template>
 	<p v-if="note.cw != null" class="cw">
 		<MkA
-			v-if="conversation && note.renoteId == parentId"
+			v-if="conversation && note.renoteId == parentId && parentId != null"
 			:to="
 				detailedView ? `#${parentId}` : `${notePage(note)}#${parentId}`
 			"
@@ -198,8 +198,8 @@ import icon from "@/scripts/icon";
 
 const props = defineProps<{
 	note: entities.Note;
-	parentId?;
-	conversation?;
+	parentId?: string;
+	conversation?: entities.Note[];
 	detailed?: boolean;
 	detailedView?: boolean;
 }>();

From 4e20fe589d5fc7cfc6f0bc400acdba8d3bfd05cc Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sat, 20 Apr 2024 22:29:55 +0800
Subject: [PATCH 04/14] fix: style may be missing in mobile mode

---
 packages/client/src/components/MkNoteSub.vue | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/packages/client/src/components/MkNoteSub.vue b/packages/client/src/components/MkNoteSub.vue
index f83c676cc1..326bb3d502 100644
--- a/packages/client/src/components/MkNoteSub.vue
+++ b/packages/client/src/components/MkNoteSub.vue
@@ -1,6 +1,7 @@
 <template>
+	<MkLoading v-if="conversationLoading" />
 	<article
-		v-if="!muted.muted || muted.what === 'reply'"
+		v-else-if="!muted.muted || muted.what === 'reply'"
 		:id="detailedView ? appearNote.id : undefined"
 		ref="el"
 		v-size="{ max: [450, 500] }"
@@ -289,6 +290,7 @@ watch(conversation, updateReplies, {
 });
 
 if (props.autoConversation) {
+	conversation.value = [];
 	if (note.value.repliesCount > 0 || note.value.renoteCount > 0) {
 		conversationLoading.value = true;
 		os.api("notes/children", {
@@ -302,8 +304,6 @@ if (props.autoConversation) {
 				.concat(res.filter((n) => n.userId === note.value.userId));
 			conversationLoading.value = false;
 		});
-	} else {
-		conversation.value = [];
 	}
 }
 

From cdc3b5181a709bbbfb492b0546d5f03cc26978ff Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sun, 21 Apr 2024 11:24:41 +0800
Subject: [PATCH 05/14] Update api-change.md

---
 docs/api-change.md | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/docs/api-change.md b/docs/api-change.md
index f3ed584c32..79c9b39e52 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -2,6 +2,15 @@
 
 Breaking changes are indicated by the :warning: icon.
 
+## Unreleased
+
+- New optional parameters are added to `notes/renotes` endpoint:
+	- `filter`: `"boost"` for boosts only, `"quote"` for quotes only, `null`(default) for both
+
+- Removed unused optional parameters for `notes/reactions` endpoint:
+	- `sinceId`
+	- `untilId`
+
 ## v20240413
 
 - :warning: Removed `patrons` endpoint.

From 09bcbb0ff02dc01d79021049bb915ae959f4f073 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Sun, 21 Apr 2024 11:27:08 +0800
Subject: [PATCH 06/14] Update docs/api-change.md

---
 docs/api-change.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/api-change.md b/docs/api-change.md
index 79c9b39e52..36b05d77f4 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -5,7 +5,7 @@ Breaking changes are indicated by the :warning: icon.
 ## Unreleased
 
 - New optional parameters are added to `notes/renotes` endpoint:
-	- `filter`: `"boost"` for boosts only, `"quote"` for quotes only, `null`(default) for both
+	- `filter`
 
 - Removed unused optional parameters for `notes/reactions` endpoint:
 	- `sinceId`

From dc53447fa3322f44cd01716491184e80472878a6 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 22 Apr 2024 05:42:31 +0900
Subject: [PATCH 07/14] chore (API): we still use the word 'renote' in our
 code/API

---
 packages/backend/src/server/api/endpoints/notes/renotes.ts | 4 ++--
 packages/client/src/components/MkNoteDetailed.vue          | 2 +-
 packages/firefish-js/src/api.types.ts                      | 2 +-
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index 282777b2e4..e69d1be7b4 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -44,7 +44,7 @@ export const paramDef = {
 		untilId: { type: "string", format: "misskey:id" },
 		filter: {
 			type: "string",
-			enum: ["boost", "quote"],
+			enum: ["all", "renote", "quote"],
 			nullable: true,
 			default: null,
 		},
@@ -67,7 +67,7 @@ export default define(meta, paramDef, async (ps, user) => {
 		.andWhere("note.renoteId = :renoteId", { renoteId: note.id })
 		.innerJoinAndSelect("note.user", "user");
 
-	if (ps.filter === "boost") {
+	if (ps.filter === "renote") {
 		query.andWhere("note.text IS NULL");
 	}
 	if (ps.filter === "quote") {
diff --git a/packages/client/src/components/MkNoteDetailed.vue b/packages/client/src/components/MkNoteDetailed.vue
index 55a13956f6..2c242645b4 100644
--- a/packages/client/src/components/MkNoteDetailed.vue
+++ b/packages/client/src/components/MkNoteDetailed.vue
@@ -401,7 +401,7 @@ const renotePagination = {
 	limit: 30,
 	params: {
 		noteId: note.value.id,
-		filter: "boost" as const,
+		filter: "renote" as const,
 	},
 };
 const quotePagination = {
diff --git a/packages/firefish-js/src/api.types.ts b/packages/firefish-js/src/api.types.ts
index 6c0e11bec5..1ee94b9954 100644
--- a/packages/firefish-js/src/api.types.ts
+++ b/packages/firefish-js/src/api.types.ts
@@ -792,7 +792,7 @@ export type Endpoints = {
 			sinceId?: Note["id"];
 			untilId?: Note["id"];
 			noteId: Note["id"];
-			filter?: "boost" | "quote";
+			filter?: "all" | "renote" | "quote";
 		};
 		res: Note[];
 	};

From b9e88ce490a31a579b7c53f583f47dbb3b0a6838 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 22 Apr 2024 05:37:57 +0900
Subject: [PATCH 08/14] docs: edit api-changes.md

---
 docs/api-change.md | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/docs/api-change.md b/docs/api-change.md
index 36b05d77f4..eec5f0d81c 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -4,10 +4,11 @@ Breaking changes are indicated by the :warning: icon.
 
 ## Unreleased
 
-- New optional parameters are added to `notes/renotes` endpoint:
-	- `filter`
-
-- Removed unused optional parameters for `notes/reactions` endpoint:
+- Added `filter` optional parameter to `notes/renotes` endpoint to filter the types of renotes. It can take the following values:
+	- `all` (default)
+  - `renote`
+  - `quote`
+- :warning: Removed the following optional parameters in `notes/reactions`, as they were not taken into account due to a bug:
 	- `sinceId`
 	- `untilId`
 

From 509690d84d92c53cef1ba60453b8a72b99fab47e Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 22 Apr 2024 05:43:58 +0900
Subject: [PATCH 09/14] chore: meRenoteCount -> myRenoteCount

---
 packages/backend/src/models/repositories/note.ts  | 2 +-
 packages/backend/src/models/schema/note.ts        | 2 +-
 packages/client/src/components/MkRenoteButton.vue | 2 +-
 packages/firefish-js/src/entities.ts              | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/packages/backend/src/models/repositories/note.ts b/packages/backend/src/models/repositories/note.ts
index 5974fc24af..7fa26373b8 100644
--- a/packages/backend/src/models/repositories/note.ts
+++ b/packages/backend/src/models/repositories/note.ts
@@ -222,7 +222,7 @@ export const NoteRepository = db.getRepository(Note).extend({
 					text: Not(IsNull()),
 				},
 			}),
-			meRenoteCount: me
+			myRenoteCount: me
 				? Notes.count({
 						where: {
 							renoteId: note.id,
diff --git a/packages/backend/src/models/schema/note.ts b/packages/backend/src/models/schema/note.ts
index e0048f7d00..6064919960 100644
--- a/packages/backend/src/models/schema/note.ts
+++ b/packages/backend/src/models/schema/note.ts
@@ -208,7 +208,7 @@ export const packedNoteSchema = {
 			optional: true,
 			nullable: true,
 		},
-		meRenoteCount: {
+		myRenoteCount: {
 			type: "number",
 			optional: true,
 			nullable: false,
diff --git a/packages/client/src/components/MkRenoteButton.vue b/packages/client/src/components/MkRenoteButton.vue
index 23911eef37..3925dc8c5a 100644
--- a/packages/client/src/components/MkRenoteButton.vue
+++ b/packages/client/src/components/MkRenoteButton.vue
@@ -73,7 +73,7 @@ useTooltip(buttonRef, async (showing) => {
 });
 
 const hasRenotedBefore = ref(
-	props.note.meRenoteCount && props.note.meRenoteCount > 0,
+	props.note.myRenoteCount && props.note.myRenoteCount > 0,
 );
 
 const renote = (viaKeyboard = false, ev?: MouseEvent) => {
diff --git a/packages/firefish-js/src/entities.ts b/packages/firefish-js/src/entities.ts
index aace6de166..02362194eb 100644
--- a/packages/firefish-js/src/entities.ts
+++ b/packages/firefish-js/src/entities.ts
@@ -174,7 +174,7 @@ export type Note = {
 	channelId?: Channel["id"];
 	channel?: Channel;
 	myReaction?: string;
-	meRenoteCount?: number;
+	myRenoteCount?: number;
 	reactions: Record<string, number>;
 	renoteCount: number;
 	repliesCount: number;

From 8140694a3191a33db9b81afe70677755a36cd38d Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Mon, 22 Apr 2024 05:47:22 +0900
Subject: [PATCH 10/14] chore (backend): add comment

---
 packages/backend/src/server/api/endpoints/notes/renotes.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/packages/backend/src/server/api/endpoints/notes/renotes.ts b/packages/backend/src/server/api/endpoints/notes/renotes.ts
index e69d1be7b4..16304dd269 100644
--- a/packages/backend/src/server/api/endpoints/notes/renotes.ts
+++ b/packages/backend/src/server/api/endpoints/notes/renotes.ts
@@ -67,6 +67,9 @@ export default define(meta, paramDef, async (ps, user) => {
 		.andWhere("note.renoteId = :renoteId", { renoteId: note.id })
 		.innerJoinAndSelect("note.user", "user");
 
+	// "all" doesn't filter out anything, it's just there for
+	// those who prefer to set the parameter explicitly
+
 	if (ps.filter === "renote") {
 		query.andWhere("note.text IS NULL");
 	}

From 6a0ad409cdb1cd81e1cc0d630259d9708e9dc9db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=80=81=E5=91=A8=E9=83=A8=E8=90=BD?=
 <laozhoubuluo@gmail.com>
Date: Tue, 23 Apr 2024 20:07:52 +0800
Subject: [PATCH 11/14] fix: authorize-follow not working due to redeclare
 'acct'

---
 packages/client/src/pages/follow.vue | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/client/src/pages/follow.vue b/packages/client/src/pages/follow.vue
index a8708978ef..3be549537f 100644
--- a/packages/client/src/pages/follow.vue
+++ b/packages/client/src/pages/follow.vue
@@ -24,16 +24,16 @@ async function follow(user): Promise<void> {
 	});
 }
 
-const acct = new URL(location.href).searchParams.get("acct");
-if (acct == null) {
+const acctUri = new URL(location.href).searchParams.get("acct");
+if (acctUri == null) {
 	throw new Error("acct required");
 }
 
 let promise;
 
-if (acct.startsWith("https://")) {
+if (acctUri.startsWith("https://")) {
 	promise = os.api("ap/show", {
-		uri: acct,
+		uri: acctUri,
 	});
 	promise.then((res) => {
 		if (res.type === "User") {
@@ -50,7 +50,7 @@ if (acct.startsWith("https://")) {
 		}
 	});
 } else {
-	promise = os.api("users/show", acct.parse(acct));
+	promise = os.api("users/show", acct.parse(acctUri));
 	promise.then((user) => {
 		follow(user);
 	});

From 50b7c71ed6a701ca001e51a115616e4446c324a6 Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Tue, 23 Apr 2024 23:56:34 +0900
Subject: [PATCH 12/14] chore (backend): use type import

---
 packages/backend/src/server/api/endpoints/i/update.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 4f65c59a9e..9a2b49cb3d 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -13,7 +13,7 @@ import { normalizeForSearch } from "@/misc/normalize-for-search.js";
 import { verifyLink } from "@/services/fetch-rel-me.js";
 import { ApiError } from "@/server/api/error.js";
 import define from "@/server/api/define.js";
-import { DriveFile } from "@/models/entities/drive-file";
+import type { DriveFile } from "@/models/entities/drive-file";
 
 export const meta = {
 	tags: ["account"],

From 067810b1bece8228f88c592e73dde68fdfb33f36 Mon Sep 17 00:00:00 2001
From: Lhcfl <Lhcfl@outlook.com>
Date: Tue, 23 Apr 2024 23:59:03 +0900
Subject: [PATCH 13/14] docs (minor): update api-change.md

---
 docs/api-change.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/api-change.md b/docs/api-change.md
index f55ceab58f..89cf8a9d38 100644
--- a/docs/api-change.md
+++ b/docs/api-change.md
@@ -9,7 +9,7 @@ Breaking changes are indicated by the :warning: icon.
 	- `all` (default)
   - `renote`
   - `quote`
-- :warning: Removed the following optional parameters in `notes/reactions`, as they were not taken into account due to a bug:
+- :warning: Removed the following optional parameters in `notes/reactions`, as they were never taken into account due to a bug:
 	- `sinceId`
 	- `untilId`
 

From 9eccdba0759a92010ec424c61472d14be414741b Mon Sep 17 00:00:00 2001
From: naskya <m@naskya.net>
Date: Wed, 24 Apr 2024 00:23:13 +0900
Subject: [PATCH 14/14] chore (backend-rs): move add_note_to_antenna to misc

---
 packages/backend-rs/index.d.ts                       |  2 +-
 packages/backend-rs/index.js                         |  4 ++--
 .../src/{service => misc}/add_note_to_antenna.rs     | 12 +++++-------
 packages/backend-rs/src/misc/mod.rs                  |  1 +
 packages/backend-rs/src/service/mod.rs               |  1 -
 packages/backend-rs/src/service/stream.rs            |  2 ++
 packages/backend-rs/src/service/stream/antenna.rs    | 10 ++++++++++
 7 files changed, 21 insertions(+), 11 deletions(-)
 rename packages/backend-rs/src/{service => misc}/add_note_to_antenna.rs (69%)
 create mode 100644 packages/backend-rs/src/service/stream/antenna.rs

diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts
index 69de833dcc..dec480d79c 100644
--- a/packages/backend-rs/index.d.ts
+++ b/packages/backend-rs/index.d.ts
@@ -193,6 +193,7 @@ export interface Acct {
 }
 export function stringToAcct(acct: string): Acct
 export function acctToString(acct: Acct): string
+export function addNoteToAntenna(antennaId: string, note: Note): void
 /**
  * @param host punycoded instance host
  * @returns whether the given host should be blocked
@@ -1119,7 +1120,6 @@ export interface Webhook {
   latestSentAt: Date | null
   latestStatus: number | null
 }
-export function addNoteToAntenna(antennaId: string, note: Note): void
 /** Initializes Cuid2 generator. Must be called before any [create_id]. */
 export function initIdGenerator(length: number, fingerprint: string): void
 export function getTimestamp(id: string): number
diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js
index b4d86dccdf..d801e28fce 100644
--- a/packages/backend-rs/index.js
+++ b/packages/backend-rs/index.js
@@ -310,12 +310,13 @@ if (!nativeBinding) {
   throw new Error(`Failed to load native binding`)
 }
 
-const { loadEnv, loadConfig, stringToAcct, acctToString, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, addNoteToAntenna, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
+const { loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getNoteSummary, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initIdGenerator, getTimestamp, genId, secureRndstr } = nativeBinding
 
 module.exports.loadEnv = loadEnv
 module.exports.loadConfig = loadConfig
 module.exports.stringToAcct = stringToAcct
 module.exports.acctToString = acctToString
+module.exports.addNoteToAntenna = addNoteToAntenna
 module.exports.isBlockedServer = isBlockedServer
 module.exports.isSilencedServer = isSilencedServer
 module.exports.isAllowedServer = isAllowedServer
@@ -352,7 +353,6 @@ module.exports.RelayStatusEnum = RelayStatusEnum
 module.exports.UserEmojimodpermEnum = UserEmojimodpermEnum
 module.exports.UserProfileFfvisibilityEnum = UserProfileFfvisibilityEnum
 module.exports.UserProfileMutingnotificationtypesEnum = UserProfileMutingnotificationtypesEnum
-module.exports.addNoteToAntenna = addNoteToAntenna
 module.exports.initIdGenerator = initIdGenerator
 module.exports.getTimestamp = getTimestamp
 module.exports.genId = genId
diff --git a/packages/backend-rs/src/service/add_note_to_antenna.rs b/packages/backend-rs/src/misc/add_note_to_antenna.rs
similarity index 69%
rename from packages/backend-rs/src/service/add_note_to_antenna.rs
rename to packages/backend-rs/src/misc/add_note_to_antenna.rs
index 4f294cc881..2ed698d7e6 100644
--- a/packages/backend-rs/src/service/add_note_to_antenna.rs
+++ b/packages/backend-rs/src/misc/add_note_to_antenna.rs
@@ -1,13 +1,14 @@
 use crate::database::{redis_conn, redis_key};
 use crate::model::entity::note;
-use crate::service::stream::{publish_to_stream, Error, Stream};
+use crate::service::stream;
 use crate::util::id::get_timestamp;
 use redis::{streams::StreamMaxlen, Commands};
 
 type Note = note::Model;
 
 #[crate::export]
-pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error> {
+pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), stream::Error> {
+    // for timeline API
     redis_conn()?.xadd_maxlen(
         redis_key(format!("antennaTimeline:{}", antenna_id)),
         StreamMaxlen::Approx(200),
@@ -15,9 +16,6 @@ pub fn add_note_to_antenna(antenna_id: String, note: &Note) -> Result<(), Error>
         &[("note", &note.id)],
     )?;
 
-    publish_to_stream(
-        &Stream::Antenna { antenna_id },
-        Some("note"),
-        Some(serde_json::to_string(note)?),
-    )
+    // for streaming API
+    stream::antenna::publish(antenna_id, note)
 }
diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs
index 24cec14969..8246b6a31a 100644
--- a/packages/backend-rs/src/misc/mod.rs
+++ b/packages/backend-rs/src/misc/mod.rs
@@ -1,4 +1,5 @@
 pub mod acct;
+pub mod add_note_to_antenna;
 pub mod check_server_block;
 pub mod check_word_mute;
 pub mod convert_host;
diff --git a/packages/backend-rs/src/service/mod.rs b/packages/backend-rs/src/service/mod.rs
index cc239e3f9e..baf29e06ad 100644
--- a/packages/backend-rs/src/service/mod.rs
+++ b/packages/backend-rs/src/service/mod.rs
@@ -1,2 +1 @@
-pub mod add_note_to_antenna;
 pub mod stream;
diff --git a/packages/backend-rs/src/service/stream.rs b/packages/backend-rs/src/service/stream.rs
index 6c5e6be4dd..1e84d5f866 100644
--- a/packages/backend-rs/src/service/stream.rs
+++ b/packages/backend-rs/src/service/stream.rs
@@ -1,3 +1,5 @@
+pub mod antenna;
+
 use crate::config::CONFIG;
 use crate::database::redis_conn;
 use redis::{Commands, RedisError};
diff --git a/packages/backend-rs/src/service/stream/antenna.rs b/packages/backend-rs/src/service/stream/antenna.rs
new file mode 100644
index 0000000000..08ae391caf
--- /dev/null
+++ b/packages/backend-rs/src/service/stream/antenna.rs
@@ -0,0 +1,10 @@
+use crate::model::entity::note;
+use crate::service::stream::{publish_to_stream, Error, Stream};
+
+pub fn publish(antenna_id: String, note: &note::Model) -> Result<(), Error> {
+    publish_to_stream(
+        &Stream::Antenna { antenna_id },
+        Some("note"),
+        Some(serde_json::to_string(note)?),
+    )
+}