fix: use MkPagination for replies
This commit is contained in:
parent
35c7dccb49
commit
39a229b875
6 changed files with 172 additions and 171 deletions
|
@ -14,7 +14,7 @@
|
|||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["*.vue"],
|
||||
"include": ["*.vue", "packages/client/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
<MkPagination
|
||||
ref="repliesPagingComponent"
|
||||
v-if="tab === 'replies' && note.repliesCount > 0"
|
||||
v-slot="{ items }"
|
||||
:pagination="repliesPagination"
|
||||
>
|
||||
<MkNoteSub
|
||||
v-for="note in directReplies"
|
||||
v-if="directReplies && tab === 'replies'"
|
||||
v-for="note in items"
|
||||
:key="note.id"
|
||||
:note="note"
|
||||
class="reply"
|
||||
:conversation="replies"
|
||||
:auto-conversation="true"
|
||||
:detailed-view="true"
|
||||
:parent-id="note.id"
|
||||
/>
|
||||
<MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" />
|
||||
</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>
|
||||
|
|
|
@ -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,10 +148,13 @@
|
|||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="conversation">
|
||||
<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"
|
||||
v-if="replyLevel < 11 && depth < 5"
|
||||
v-for="reply in replies.slice(0, REPLIES_LIMIT)"
|
||||
:key="reply.id"
|
||||
:note="reply"
|
||||
class="reply"
|
||||
|
@ -162,6 +165,14 @@
|
|||
: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 {
|
||||
|
|
|
@ -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,6 +294,10 @@ const fetchMore = async (): Promise<void> => {
|
|||
? {
|
||||
sinceId: items.value[0].id,
|
||||
}
|
||||
: props.pagination.ascending
|
||||
? {
|
||||
sinceId: items.value[items.value.length - 1].id,
|
||||
}
|
||||
: {
|
||||
untilId: items.value[items.value.length - 1].id,
|
||||
}),
|
||||
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue