diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index b6982fffab..936c17d55b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -87,7 +87,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
@@ -101,7 +110,16 @@ SPDX-License-Identifier: AGPL-3.0-only {{ appearNote.channel.name }} - + @@ -125,11 +143,11 @@ SPDX-License-Identifier: AGPL-3.0-only @@ -29,7 +29,6 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js'; import { useTooltip } from '@/use/use-tooltip.js'; import { $i } from '@/i.js'; import MkReactionEffect from '@/components/MkReactionEffect.vue'; -import { claimAchievement } from '@/utility/achievements.js'; import { i18n } from '@/i18n.js'; import * as sound from '@/utility/sound.js'; import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; @@ -39,10 +38,12 @@ import { DI } from '@/di.js'; import { noteEvents } from '@/use/use-note-capture.js'; const props = defineProps<{ + noteId: Misskey.entities.Note['id']; reaction: string; + reactionEmojis: Misskey.entities.Note['reactionEmojis']; + myReaction: Misskey.entities.Note['myReaction']; count: number; isInitial: boolean; - note: Misskey.entities.Note; }>(); const mock = inject(DI.mock, false); @@ -57,14 +58,16 @@ const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? getUnicodeEmoji(props.reaction)); const canToggle = computed(() => { - return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); + // TODO + //return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value); + return !props.reaction.match(/@\w/) && $i && emoji.value; }); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); async function toggleReaction() { if (!canToggle.value) return; - const oldReaction = props.note.myReaction; + const oldReaction = props.myReaction; if (oldReaction) { const confirm = await os.confirm({ type: 'warning', @@ -82,19 +85,19 @@ async function toggleReaction() { } misskeyApi('notes/reactions/delete', { - noteId: props.note.id, + noteId: props.noteId, }).then(() => { - noteEvents.emit(`unreacted:${props.note.id}`, { + noteEvents.emit(`unreacted:${props.noteId}`, { userId: $i!.id, reaction: props.reaction, emoji: emoji.value, }); if (oldReaction !== props.reaction) { misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: props.noteId, reaction: props.reaction, }).then(() => { - noteEvents.emit(`reacted:${props.note.id}`, { + noteEvents.emit(`reacted:${props.noteId}`, { userId: $i!.id, reaction: props.reaction, emoji: emoji.value, @@ -120,18 +123,19 @@ async function toggleReaction() { } misskeyApi('notes/reactions/create', { - noteId: props.note.id, + noteId: props.noteId, reaction: props.reaction, }).then(() => { - noteEvents.emit(`reacted:${props.note.id}`, { + noteEvents.emit(`reacted:${props.noteId}`, { userId: $i!.id, reaction: props.reaction, emoji: emoji.value, }); }); - if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { - claimAchievement('reactWithoutRead'); - } + // TODO: 上位コンポーネントでやる + //if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) { + // claimAchievement('reactWithoutRead'); + //} } } @@ -175,7 +179,7 @@ onMounted(() => { if (!mock) { useTooltip(buttonEl, async (showing) => { const reactions = await misskeyApiGet('notes/reactions', { - noteId: props.note.id, + noteId: props.noteId, type: props.reaction, limit: 10, _cacheKey_: props.count, diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index e8cf6c36db..725978179e 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -13,7 +13,17 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="$style.transition_x_move" tag="div" :class="$style.root" > - + @@ -27,7 +37,10 @@ import { prefer } from '@/preferences.js'; import { DI } from '@/di.js'; const props = withDefaults(defineProps<{ - note: Misskey.entities.Note; + noteId: Misskey.entities.Note['id']; + reactions: Misskey.entities.Note['reactions']; + reactionEmojis: Misskey.entities.Note['reactionEmojis']; + myReaction: Misskey.entities.Note['myReaction']; maxNumber?: number; }>(), { maxNumber: Infinity, @@ -39,33 +52,33 @@ const emit = defineEmits<{ (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; }>(); -const initialReactions = new Set(Object.keys(props.note.reactions)); +const initialReactions = new Set(Object.keys(props.reactions)); -const reactions = ref<[string, number][]>([]); +const _reactions = ref<[string, number][]>([]); const hasMoreReactions = ref(false); -if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) { - reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; +if (props.myReaction && !Object.keys(_reactions.value).includes(props.myReaction)) { + _reactions.value[props.myReaction] = props.reactions[props.myReaction]; } function onMockToggleReaction(emoji: string, count: number) { if (!mock) return; - const i = reactions.value.findIndex((item) => item[0] === emoji); + const i = _reactions.value.findIndex((item) => item[0] === emoji); if (i < 0) return; - emit('mockUpdateMyReaction', emoji, (count - reactions.value[i][1])); + emit('mockUpdateMyReaction', emoji, (count - _reactions.value[i][1])); } -watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { +watch([() => props.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions.value = Object.keys(newSource).length > maxNumber; - for (let i = 0; i < reactions.value.length; i++) { - const reaction = reactions.value[i][0]; + for (let i = 0; i < _reactions.value.length; i++) { + const reaction = _reactions.value[i][0]; if (reaction in newSource && newSource[reaction] !== 0) { - reactions.value[i][1] = newSource[reaction]; - newReactions.push(reactions.value[i]); + _reactions.value[i][1] = newSource[reaction]; + newReactions.push(_reactions.value[i]); } } @@ -79,11 +92,11 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe newReactions = newReactions.slice(0, props.maxNumber); - if (props.note.myReaction && !newReactions.map(([x]) => x).includes(props.note.myReaction)) { - newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); + if (props.myReaction && !newReactions.map(([x]) => x).includes(props.myReaction)) { + newReactions.push([props.myReaction, newSource[props.myReaction]]); } - reactions.value = newReactions; + _reactions.value = newReactions; }, { immediate: true, deep: true }); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index 5800beb3a0..9b28573712 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -321,6 +321,7 @@ refreshEndpointAndChannel(); const paginator = usePagination({ ctx: paginationQuery, + useShallowRef: true, }); onUnmounted(() => { diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue index 680fe08c14..62a220d2f1 100644 --- a/packages/frontend/src/pages/welcome.timeline.note.vue +++ b/packages/frontend/src/pages/welcome.timeline.note.vue @@ -27,7 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts index c0d7bbfc65..4d5cb6be0f 100644 --- a/packages/frontend/src/use/use-note-capture.ts +++ b/packages/frontend/src/use/use-note-capture.ts @@ -6,7 +6,7 @@ import { onUnmounted } from 'vue'; import * as Misskey from 'misskey-js'; import { EventEmitter } from 'eventemitter3'; -import type { Ref, ShallowRef } from 'vue'; +import type { Ref } from 'vue'; import { useStream } from '@/stream.js'; import { $i } from '@/i.js'; import { store } from '@/store.js'; @@ -28,7 +28,7 @@ const pollingQueue = new Map(); -function pollingEnqueue(note: Misskey.entities.Note) { +function pollingEnqueue(note: Pick) { if (pollingQueue.has(note.id)) { const data = pollingQueue.get(note.id)!; pollingQueue.set(note.id, { @@ -44,7 +44,7 @@ function pollingEnqueue(note: Misskey.entities.Note) { } } -function pollingDequeue(note: Misskey.entities.Note) { +function pollingDequeue(note: Pick) { const data = pollingQueue.get(note.id); if (data == null) return; @@ -85,28 +85,31 @@ window.setInterval(() => { }, POLLING_INTERVAL); function pollingSubscribe(props: { - note: Ref; + note: Pick; + reactionsRef: Ref; + reactionCountRef: Ref; + reactionEmojisRef: Ref; isDeletedRef: Ref; }) { - const note = props.note; + const { note, reactionsRef, reactionCountRef, reactionEmojisRef } = props; function onFetched(data: Pick): void { - note.value.reactions = data.reactions; - note.value.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0); - note.value.reactionEmojis = data.reactionEmojis; + reactionsRef.value = data.reactions; + reactionCountRef.value = Object.values(data.reactions).reduce((a, b) => a + b, 0); + reactionEmojisRef.value = data.reactionEmojis; } - pollingEnqueue(note.value); - fetchEvent.on(note.value.id, onFetched); + pollingEnqueue(note); + fetchEvent.on(note.id, onFetched); onUnmounted(() => { - pollingDequeue(note.value); - fetchEvent.off(note.value.id, onFetched); + pollingDequeue(note); + fetchEvent.off(note.id, onFetched); }); } function realtimeSubscribe(props: { - note: Ref; + note: Pick; isDeletedRef: Ref; }): void { const note = props.note; @@ -115,7 +118,7 @@ function realtimeSubscribe(props: { function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; - if (id !== note.value.id) return; + if (id !== note.id) return; switch (type) { case 'reacted': { @@ -152,12 +155,12 @@ function realtimeSubscribe(props: { } function capture(withHandler = false): void { - connection.send('sr', { id: note.value.id }); + connection.send('sr', { id: note.id }); if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); } function decapture(withHandler = false): void { - connection.send('un', { id: note.value.id }); + connection.send('un', { id: note.id }); if (withHandler) connection.off('noteUpdated', onStreamNoteUpdated); } @@ -175,39 +178,42 @@ function realtimeSubscribe(props: { } export function useNoteCapture(props: { - note: Ref; - parentNote: Ref | null; + note: Pick; + parentNote: Misskey.entities.Note | null; + reactionsRef: Ref; + reactionCountRef: Ref; + reactionEmojisRef: Ref; + myReactionRef: Ref; + pollChoicesRef: Ref['choices'] | null>; isDeletedRef: Ref; }) { - const note = props.note; - const parentNote = props.parentNote; + const { note, parentNote, reactionsRef, reactionCountRef, reactionEmojisRef, myReactionRef, pollChoicesRef } = props; - noteEvents.on(`reacted:${note.value.id}`, onReacted); - noteEvents.on(`unreacted:${note.value.id}`, onUnreacted); - noteEvents.on(`pollVoted:${note.value.id}`, onPollVoted); - noteEvents.on(`deleted:${note.value.id}`, onDeleted); + noteEvents.on(`reacted:${note.id}`, onReacted); + noteEvents.on(`unreacted:${note.id}`, onUnreacted); + noteEvents.on(`pollVoted:${note.id}`, onPollVoted); + noteEvents.on(`deleted:${note.id}`, onDeleted); let latestReactedKey: string | null = null; let latestUnreactedKey: string | null = null; let latestPollVotedKey: string | null = null; function onReacted(ctx: { userId: Misskey.entities.User['id']; reaction: string; emoji?: { name: string; url: string; }; }): void { - console.log('reacted', ctx); const newReactedKey = `${ctx.userId}:${ctx.reaction}`; if (newReactedKey === latestReactedKey) return; latestReactedKey = newReactedKey; - if (ctx.emoji && !(ctx.emoji.name in note.value.reactionEmojis)) { - note.value.reactionEmojis[ctx.emoji.name] = ctx.emoji.url; + if (ctx.emoji && !(ctx.emoji.name in reactionEmojisRef.value)) { + reactionEmojisRef.value[ctx.emoji.name] = ctx.emoji.url; } - const currentCount = note.value.reactions[ctx.reaction] || 0; + const currentCount = reactionsRef.value[ctx.reaction] || 0; - note.value.reactions[ctx.reaction] = currentCount + 1; - note.value.reactionCount += 1; + reactionsRef.value[ctx.reaction] = currentCount + 1; + reactionCountRef.value += 1; if ($i && (ctx.userId === $i.id)) { - note.value.myReaction = ctx.reaction; + myReactionRef.value = ctx.reaction; } } @@ -216,14 +222,14 @@ export function useNoteCapture(props: { if (newUnreactedKey === latestUnreactedKey) return; latestUnreactedKey = newUnreactedKey; - const currentCount = note.value.reactions[ctx.reaction] || 0; + const currentCount = reactionsRef.value[ctx.reaction] || 0; - note.value.reactions[ctx.reaction] = Math.max(0, currentCount - 1); - note.value.reactionCount = Math.max(0, note.value.reactionCount - 1); - if (note.value.reactions[ctx.reaction] === 0) delete note.value.reactions[ctx.reaction]; + reactionsRef.value[ctx.reaction] = Math.max(0, currentCount - 1); + reactionCountRef.value = Math.max(0, reactionCountRef.value - 1); + if (reactionsRef.value[ctx.reaction] === 0) delete reactionsRef.value[ctx.reaction]; if ($i && (ctx.userId === $i.id)) { - note.value.myReaction = null; + myReactionRef.value = null; } } @@ -232,7 +238,7 @@ export function useNoteCapture(props: { if (newPollVotedKey === latestPollVotedKey) return; latestPollVotedKey = newPollVotedKey; - const choices = [...note.value.poll.choices]; + const choices = [...pollChoicesRef.value]; choices[ctx.choice] = { ...choices[ctx.choice], votes: choices[ctx.choice].votes + 1, @@ -241,7 +247,7 @@ export function useNoteCapture(props: { } : {}), }; - note.value.poll.choices = choices; + pollChoicesRef.value = choices; } function onDeleted(): void { @@ -249,22 +255,22 @@ export function useNoteCapture(props: { } onUnmounted(() => { - noteEvents.off(`reacted:${note.value.id}`, onReacted); - noteEvents.off(`unreacted:${note.value.id}`, onUnreacted); - noteEvents.off(`pollVoted:${note.value.id}`, onPollVoted); - noteEvents.off(`deleted:${note.value.id}`, onDeleted); + noteEvents.off(`reacted:${note.id}`, onReacted); + noteEvents.off(`unreacted:${note.id}`, onUnreacted); + noteEvents.off(`pollVoted:${note.id}`, onPollVoted); + noteEvents.off(`deleted:${note.id}`, onDeleted); }); // 投稿からある程度経過している(=タイムラインを遡って表示した)ノートは、イベントが発生する可能性が低いためそもそも購読しない // ただし「リノートされたばかりの過去のノート」(= parentNoteが存在し、かつparentNoteの投稿日時が最近)はイベント発生が考えられるため購読する // TODO: デバイスとサーバーの時計がズレていると不具合の元になるため、ズレを検知して警告を表示するなどのケアが必要かもしれない if (parentNote == null) { - if ((Date.now() - new Date(note.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min + if ((Date.now() - new Date(note.createdAt).getTime()) > 1000 * 60 * 5) { // 5min // リノートで表示されているノートでもないし、投稿からある程度経過しているので購読しない return; } } else { - if ((Date.now() - new Date(parentNote.value.createdAt).getTime()) > 1000 * 60 * 5) { // 5min + if ((Date.now() - new Date(parentNote.createdAt).getTime()) > 1000 * 60 * 5) { // 5min // リノートで表示されているノートだが、リノートされてからある程度経過しているので購読しない return; } diff --git a/packages/frontend/src/use/use-pagination.ts b/packages/frontend/src/use/use-pagination.ts index e8d25c3473..12ce2a7f7d 100644 --- a/packages/frontend/src/use/use-pagination.ts +++ b/packages/frontend/src/use/use-pagination.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed, isRef, onMounted, ref, watch } from 'vue'; +import { computed, isRef, onMounted, ref, shallowRef, triggerRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import type { ComputedRef, Ref, ShallowRef } from 'vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -40,9 +40,10 @@ export type PagingCtx(props: { ctx: PagingCtx; + useShallowRef?: boolean; }) { - const items = ref([]); - const queue = ref([]); + const items = props.useShallowRef ? shallowRef([]) : ref([]); + const queue = props.useShallowRef ? shallowRef([]) : ref([]); const fetching = ref(true); const moreFetching = ref(false); const canFetchMore = ref(false); @@ -142,8 +143,10 @@ export function usePagination(props: { }).then(res => { if (options.toQueue) { queue.value.unshift(...res.toReversed()); + if (props.useShallowRef) triggerRef(queue); } else { items.value.unshift(...res.toReversed()); + if (props.useShallowRef) triggerRef(items); } }); } @@ -155,18 +158,22 @@ export function usePagination(props: { function unshiftItems(newItems: T[]) { items.value.unshift(...newItems); + if (props.useShallowRef) triggerRef(items); } function pushItems(oldItems: T[]) { items.value.push(...oldItems); + if (props.useShallowRef) triggerRef(items); } function prepend(item: T) { items.value.unshift(item); + if (props.useShallowRef) triggerRef(items); } function enqueue(item: T) { queue.value.unshift(item); + if (props.useShallowRef) triggerRef(queue); } function releaseQueue() {