From e74ff698c930280d6be6bdf63c0e70a74386181d Mon Sep 17 00:00:00 2001
From: ShittyKopper <shittykopper@w.on-t.work>
Date: Wed, 14 Feb 2024 18:06:08 +0300
Subject: [PATCH] feat: sort replies to self before other replies

---
 .../frontend/src/components/SkNoteDetailed.vue  |  4 +++-
 packages/frontend/src/components/SkNoteSub.vue  |  4 +++-
 packages/frontend/src/scripts/reply-sorting.ts  | 17 +++++++++++++++++
 3 files changed, 23 insertions(+), 2 deletions(-)
 create mode 100644 packages/frontend/src/scripts/reply-sorting.ts

diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue
index 80d6ac369e..685e0a4b7e 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" :onDeleteCallback="removeReply" :isReply="true"/>
+			<SkNoteSub v-for="note in sortedReplies" :key="note.id" :note="note" :class="$style.reply" :detail="true" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" :isReply="true"/>
 		</div>
 		<div v-else-if="tab === 'renotes'" :class="$style.tab_renotes">
 			<MkPagination :pagination="renotesPagination" :disableAutoLoad="true">
@@ -266,6 +266,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { sortReplies } from '@/scripts/reply-sorting.js';
 
 const props = defineProps<{
 	note: Misskey.entities.Note;
@@ -327,6 +328,7 @@ const allowAnim = ref(defaultStore.state.advancedMfm && defaultStore.state.anima
 const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
 const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
+const sortedReplies = computed(() => sortReplies(props.note, replies.value));
 const quotes = ref<Misskey.entities.Note[]>([]);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
 const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
diff --git a/packages/frontend/src/components/SkNoteSub.vue b/packages/frontend/src/components/SkNoteSub.vue
index 1486742f42..6d806e4fb8 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" :onDeleteCallback="removeReply" :isReply="props.isReply"/>
+		<SkNoteSub v-for="reply in sortedReplies" :key="reply.id" :note="reply" :class="[$style.reply, { [$style.single]: replies.length === 1 }]" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws" :onDeleteCallback="removeReply" :isReply="props.isReply"/>
 	</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>
@@ -114,6 +114,7 @@ import { claimAchievement } from '@/scripts/achievements.js';
 import { getNoteMenu } from '@/scripts/get-note-menu.js';
 import { useNoteCapture } from '@/scripts/use-note-capture.js';
 import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
+import { sortReplies } from '@/scripts/reply-sorting.js';
 
 const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 const hideLine = computed(() => { return props.detail ? true : false; });
@@ -151,6 +152,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 sortedReplies = computed(() => sortReplies(props.note, replies.value));
 
 const isRenote = (
 	props.note.renote != null &&
diff --git a/packages/frontend/src/scripts/reply-sorting.ts b/packages/frontend/src/scripts/reply-sorting.ts
new file mode 100644
index 0000000000..43485ee19c
--- /dev/null
+++ b/packages/frontend/src/scripts/reply-sorting.ts
@@ -0,0 +1,17 @@
+import * as Misskey from 'misskey-js';
+
+// sorts replies to self before other replies to make threads easier to read
+export function sortReplies(root: Misskey.entities.Note, replies: Misskey.entities.Note[]): Misskey.entities.Note[] {
+	const result: Misskey.entities.Note[] = [];
+
+	for (const reply of replies) {
+		if (root.userId === reply.userId) {
+			result.unshift(reply);
+		} else {
+			result.push(reply);
+		}
+	}
+
+	return result;
+}
+
-- 
GitLab