From 8785621fcf92d482c3f1700c31d268c8d37bf5c9 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:44:33 +0900 Subject: [PATCH] wip --- .../src/core/entities/NoteEntityService.ts | 31 ++++++ .../api/endpoints/notes/show-partial-bulk.ts | 46 +++++++++ packages/frontend/src/components/MkNote.vue | 3 + packages/frontend/src/use/use-note-capture.ts | 96 ++++++++++++++----- 4 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 97f1c3d739..8f9ab77ac4 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -593,4 +593,35 @@ export class NoteEntityService implements OnModuleInit { relations: ['user'], }); } + + @bindThis + public async ogogogo(noteIds: MiNote['id'][]) { + if (noteIds.length === 0) return []; + + const notes = await this.notesRepository.find({ + where: { + id: In(noteIds), + }, + select: { + reactions: true, + reactionAndUserPairCache: true, + }, + }); + + console.log(notes); + + const bufferedReactionsMap = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(noteIds) : null; + + const results = []; + + for (const note of notes) { + const bufferedReactions = bufferedReactionsMap?.get(note.id); + const reactionAndUserPairCache = note.reactionAndUserPairCache.concat(bufferedReactions.pairs.map(x => x.join('/'))); + + results.push({ + id: note.id, + reactions: this.reactionService.convertLegacyReactions(this.reactionsBufferingService.mergeReactions(note.reactions, bufferedReactions.deltas ?? {})), + }); + } + } } diff --git a/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts new file mode 100644 index 0000000000..619d59bb97 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/notes/show-partial-bulk.ts @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; +import { GetterService } from '@/server/api/GetterService.js'; +import { ApiError } from '../../error.js'; + +export const meta = { + tags: ['notes'], + + requireCredential: false, + + res: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'object', + optional: false, nullable: false, + }, + }, + + errors: { + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + noteIds: { type: 'array', items: { type: 'string', format: 'misskey:id' }, maxItems: 100, minItems: 1 }, + }, + required: ['noteIds'], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + ) { + super(meta, paramDef, async (ps, me) => { + + }); + } +} diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 07da1bd4d9..81f69a96b5 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -514,7 +514,10 @@ function react(): void { misskeyApi('notes/reactions/create', { noteId: appearNote.value.id, reaction: reaction, + }).then(() => { + // 別にthenを待たなくても良いかも(楽観的更新) }); + if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts index 111a5e5d89..9634121baf 100644 --- a/packages/frontend/src/use/use-note-capture.ts +++ b/packages/frontend/src/use/use-note-capture.ts @@ -5,12 +5,32 @@ import { onUnmounted } from 'vue'; import * as Misskey from 'misskey-js'; +import { EventEmitter } from 'eventemitter3'; import type { Ref, ShallowRef } from 'vue'; import { useStream } from '@/stream.js'; import { $i } from '@/i.js'; import { store } from '@/store.js'; -export function useNoteCapture(props: { +const noteEvents = new EventEmitter<{ + reacted: Misskey.entities.Note; + unreacted: Misskey.entities.Note; + pollVoted: Misskey.entities.Note; + deleted: Misskey.entities.Note; +}>(); + +const capturedNoteIdMapForPolling = new Map(); + +const POLLING_INTERVAL = 1000 * 10; + +window.setInterval(() => { + const ids = [...capturedNoteIdMapForPolling.keys()]; + if (ids.length === 0) return; + if (window.document.hidden) return; + + console.log('Polling notes', ids); +}, POLLING_INTERVAL); + +function pseudoNoteCapture(props: { rootEl: ShallowRef; note: Ref; pureNote: Ref; @@ -18,7 +38,34 @@ export function useNoteCapture(props: { }) { const note = props.note; const pureNote = props.pureNote; - const connection = $i && store.s.realtimeMode ? useStream() : null; + + function onReacted(): void { + + } + + if (capturedNoteIdMapForPolling.has(note.value.id)) { + capturedNoteIdMapForPolling.set(note.value.id, capturedNoteIdMapForPolling.get(note.value.id)! + 1); + } else { + capturedNoteIdMapForPolling.set(note.value.id, 1); + } + + onUnmounted(() => { + capturedNoteIdMapForPolling.set(note.value.id, capturedNoteIdMapForPolling.get(note.value.id)! - 1); + if (capturedNoteIdMapForPolling.get(note.value.id) === 0) { + capturedNoteIdMapForPolling.delete(note.value.id); + } + }); +} + +function realtimeNoteCapture(props: { + rootEl: ShallowRef; + note: Ref; + pureNote: Ref; + isDeletedRef: Ref; +}): void { + const note = props.note; + const pureNote = props.pureNote; + const connection = useStream(); function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; @@ -85,26 +132,22 @@ export function useNoteCapture(props: { } function capture(withHandler = false): void { - if (connection) { - // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(window.document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id }); - if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id }); - if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); - } + // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する + connection.send(window.document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id }); + if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id }); + if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); } function decapture(withHandler = false): void { - if (connection) { + connection.send('un', { + id: note.value.id, + }); + if (pureNote.value.id !== note.value.id) { connection.send('un', { - id: note.value.id, + id: pureNote.value.id, }); - if (pureNote.value.id !== note.value.id) { - connection.send('un', { - id: pureNote.value.id, - }); - } - if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); } + if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); } function onStreamConnected() { @@ -112,14 +155,23 @@ export function useNoteCapture(props: { } capture(true); - if (connection) { - connection.on('_connected_', onStreamConnected); - } + connection.on('_connected_', onStreamConnected); onUnmounted(() => { decapture(true); - if (connection) { - connection.off('_connected_', onStreamConnected); - } + connection.off('_connected_', onStreamConnected); }); } + +export function useNoteCapture(props: { + rootEl: ShallowRef; + note: Ref; + pureNote: Ref; + isDeletedRef: Ref; +}) { + if ($i && store.s.realtimeMode) { + realtimeNoteCapture(props); + } else { + pseudoNoteCapture(props); + } +}