From 683b4aafb2928f2d8dfa626352584542e3df8e3a Mon Sep 17 00:00:00 2001 From: dakkar Date: Tue, 19 Dec 2023 09:07:32 +0000 Subject: [PATCH] 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 d175f21f2..95a4eba74 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 0b0693121..6406bc4c5 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 f29b9db6a..ae9d8a0d6 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 8d394c0c1..3c840cf59 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -132,6 +132,7 @@ const likeButton = shallowRef(); 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([]); 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([]); - function boostVisibility() { os.popupMenu([ { diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 8bf9e244e..ff2058d79 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 fc30dc87a..f4279fe8a 100644 --- a/packages/frontend/src/components/SkNoteSub.vue +++ b/packages/frontend/src/components/SkNoteSub.vue @@ -141,6 +141,7 @@ const likeButton = shallowRef(); 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([]); 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([]); - 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 ab232598c..8692d056b 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; pureNote: Ref; isDeletedRef: Ref; + 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;