This commit is contained in:
syuilo 2025-05-01 21:46:16 +09:00
parent 66c3666d0c
commit 5ac2116449
3 changed files with 70 additions and 76 deletions

View File

@ -92,7 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:noteId="appearNote.id" :noteId="appearNote.id"
:multiple="appearNote.poll.multiple" :multiple="appearNote.poll.multiple"
:expiresAt="appearNote.poll.expiresAt" :expiresAt="appearNote.poll.expiresAt"
:choices="pollChoices" :choices="$appearNote.pollChoices"
:author="appearNote.user" :author="appearNote.user"
:emojiUrls="appearNote.emojis" :emojiUrls="appearNote.emojis"
:class="$style.poll" :class="$style.poll"
@ -113,9 +113,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkReactionsViewer <MkReactionsViewer
v-if="appearNote.reactionAcceptance !== 'likeOnly'" v-if="appearNote.reactionAcceptance !== 'likeOnly'"
style="margin-top: 6px;" style="margin-top: 6px;"
:reactions="reactions" :reactions="$appearNote.reactions"
:reactionEmojis="reactionEmojis" :reactionEmojis="$appearNote.reactionEmojis"
:myReaction="myReaction" :myReaction="$appearNote.myReaction"
:noteId="appearNote.id" :noteId="appearNote.id"
:maxNumber="16" :maxNumber="16"
@mockUpdateMyReaction="emitUpdReaction" @mockUpdateMyReaction="emitUpdReaction"
@ -143,11 +143,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i> <i class="ti ti-ban"></i>
</button> </button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
<i v-else-if="myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && reactionCount > 0" :class="$style.footerButtonCount">{{ number(reactionCount) }}</p> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number($appearNote.reactionCount) }}</p>
</button> </button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
@ -194,7 +194,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef } from 'vue'; import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
@ -287,11 +287,13 @@ if (noteViewInterruptors.length > 0) {
const isRenote = Misskey.note.isPureRenote(note); const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note); const appearNote = getAppearNote(note);
const reactions = ref(appearNote.reactions); const $appearNote = reactive({
const reactionCount = ref(appearNote.reactionCount); reactions: appearNote.reactions,
const reactionEmojis = ref(appearNote.reactionEmojis); reactionCount: appearNote.reactionCount,
const myReaction = ref(appearNote.myReaction); reactionEmojis: appearNote.reactionEmojis,
const pollChoices = ref(appearNote.poll?.choices); myReaction: appearNote.myReaction,
pollChoices: appearNote.poll?.choices,
});
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
const menuButton = useTemplateRef('menuButton'); const menuButton = useTemplateRef('menuButton');
@ -317,7 +319,7 @@ const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibili
const renoteCollapsed = ref( const renoteCollapsed = ref(
prefer.s.collapseRenotes && isRenote && ( prefer.s.collapseRenotes && isRenote && (
($i && ($i.id === note.userId || $i.id === appearNote.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 ($i && ($i.id === note.userId || $i.id === appearNote.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
(myReaction.value != null) ($appearNote.myReaction != null)
), ),
); );
@ -421,12 +423,7 @@ if (props.mock) {
useNoteCapture({ useNoteCapture({
note: appearNote, note: appearNote,
parentNote: note, parentNote: note,
reactionsRef: reactions, $note: $appearNote,
reactionCountRef: reactionCount,
reactionEmojisRef: reactionEmojis,
myReactionRef: myReaction,
pollChoicesRef: pollChoices,
isDeletedRef: isDeleted,
}); });
} }
@ -456,7 +453,7 @@ if (!props.mock) {
const reactions = await misskeyApiGet('notes/reactions', { const reactions = await misskeyApiGet('notes/reactions', {
noteId: appearNote.id, noteId: appearNote.id,
limit: 10, limit: 10,
_cacheKey_: reactionCount.value, _cacheKey_: $appearNote.reactionCount,
}); });
const users = reactions.map(x => x.user); const users = reactions.map(x => x.user);
@ -467,7 +464,7 @@ if (!props.mock) {
showing, showing,
reaction: '❤️', reaction: '❤️',
users, users,
count: reactionCount.value, count: $appearNote.reactionCount,
targetElement: reactButton.value!, targetElement: reactButton.value!,
}, { }, {
closed: () => dispose(), closed: () => dispose(),
@ -566,7 +563,7 @@ function react(): void {
} }
function undoReact(): void { function undoReact(): void {
const oldReaction = myReaction.value; const oldReaction = $appearNote.myReaction;
if (!oldReaction) return; if (!oldReaction) return;
if (props.mock) { if (props.mock) {
@ -580,7 +577,7 @@ function undoReact(): void {
} }
function toggleReact() { function toggleReact() {
if (myReaction.value == null) { if ($appearNote.myReaction == null) {
react(); react();
} else { } else {
undoReact(); undoReact();

View File

@ -136,9 +136,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkReactionsViewer <MkReactionsViewer
v-if="appearNote.reactionAcceptance !== 'likeOnly'" v-if="appearNote.reactionAcceptance !== 'likeOnly'"
style="margin-top: 6px;" style="margin-top: 6px;"
:reactions="reactions" :reactions="$appearNote.reactions"
:reactionEmojis="reactionEmojis" :reactionEmojis="$appearNote.reactionEmojis"
:myReaction="myReaction" :myReaction="$appearNote.myReaction"
:noteId="appearNote.id" :noteId="appearNote.id"
:maxNumber="16" :maxNumber="16"
@mockUpdateMyReaction="emitUpdReaction" @mockUpdateMyReaction="emitUpdReaction"
@ -161,11 +161,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-ban"></i> <i class="ti ti-ban"></i>
</button> </button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()"> <button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i> <i v-if="appearNote.reactionAcceptance === 'likeOnly' && $appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
<i v-else-if="myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i> <i v-else-if="$appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i> <i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ti ti-plus"></i> <i v-else class="ti ti-plus"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(reactionCount) }}</p> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number($appearNote.reactionCount) }}</p>
</button> </button>
<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()"> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
<i class="ti ti-paperclip"></i> <i class="ti ti-paperclip"></i>
@ -229,7 +229,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, provide, ref, useTemplateRef } from 'vue'; import { computed, inject, onMounted, provide, reactive, ref, useTemplateRef } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js'; import { isLink } from '@@/js/is-link.js';
@ -308,11 +308,13 @@ if (noteViewInterruptors.length > 0) {
const isRenote = Misskey.note.isPureRenote(note); const isRenote = Misskey.note.isPureRenote(note);
const appearNote = getAppearNote(note); const appearNote = getAppearNote(note);
const reactions = ref(appearNote.reactions); const $appearNote = reactive({
const reactionCount = ref(appearNote.reactionCount); reactions: appearNote.reactions,
const reactionEmojis = ref(appearNote.reactionEmojis); reactionCount: appearNote.reactionCount,
const myReaction = ref(appearNote.myReaction); reactionEmojis: appearNote.reactionEmojis,
const pollChoices = ref(appearNote.poll?.choices); myReaction: appearNote.myReaction,
pollChoices: appearNote.poll?.choices,
});
const rootEl = useTemplateRef('rootEl'); const rootEl = useTemplateRef('rootEl');
const menuButton = useTemplateRef('menuButton'); const menuButton = useTemplateRef('menuButton');
@ -376,7 +378,7 @@ provide(DI.mfmEmojiReactCallback, (reaction) => {
const tab = ref(props.initialTab); const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null); const reactionTabType = ref<string | null>(null);
const renotesPagination = computed<Paging>(() => ({ const renotesPagination = computed(() => ({
endpoint: 'notes/renotes', endpoint: 'notes/renotes',
limit: 10, limit: 10,
params: { params: {
@ -384,7 +386,7 @@ const renotesPagination = computed<Paging>(() => ({
}, },
})); }));
const reactionsPagination = computed<Paging>(() => ({ const reactionsPagination = computed(() => ({
endpoint: 'notes/reactions', endpoint: 'notes/reactions',
limit: 10, limit: 10,
params: { params: {
@ -396,12 +398,7 @@ const reactionsPagination = computed<Paging>(() => ({
useNoteCapture({ useNoteCapture({
note: appearNote, note: appearNote,
parentNote: note, parentNote: note,
reactionsRef: reactions, $note: $appearNote,
reactionCountRef: reactionCount,
reactionEmojisRef: reactionEmojis,
myReactionRef: myReaction,
pollChoicesRef: pollChoices,
isDeletedRef: isDeleted,
}); });
useTooltip(renoteButton, async (showing) => { useTooltip(renoteButton, async (showing) => {
@ -429,7 +426,7 @@ if (appearNote.reactionAcceptance === 'likeOnly') {
const reactions = await misskeyApiGet('notes/reactions', { const reactions = await misskeyApiGet('notes/reactions', {
noteId: appearNote.id, noteId: appearNote.id,
limit: 10, limit: 10,
_cacheKey_: reactionCount.value, _cacheKey_: $appearNote.reactionCount,
}); });
const users = reactions.map(x => x.user); const users = reactions.map(x => x.user);
@ -440,7 +437,7 @@ if (appearNote.reactionAcceptance === 'likeOnly') {
showing, showing,
reaction: '❤️', reaction: '❤️',
users, users,
count: reactionCount.value, count: $appearNote.reactionCount,
targetElement: reactButton.value!, targetElement: reactButton.value!,
}, { }, {
closed: () => dispose(), closed: () => dispose(),

View File

@ -6,7 +6,7 @@
import { onUnmounted } from 'vue'; import { onUnmounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import type { Ref } from 'vue'; import type { Reactive, Ref } from 'vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { $i } from '@/i.js'; import { $i } from '@/i.js';
import { store } from '@/store.js'; import { store } from '@/store.js';
@ -86,17 +86,14 @@ window.setInterval(() => {
function pollingSubscribe(props: { function pollingSubscribe(props: {
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>; note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
reactionsRef: Ref<Misskey.entities.Note['reactions']>; $note: ReactiveNoteData;
reactionCountRef: Ref<Misskey.entities.Note['reactionCount']>;
reactionEmojisRef: Ref<Misskey.entities.Note['reactionEmojis']>;
isDeletedRef: Ref<boolean>;
}) { }) {
const { note, reactionsRef, reactionCountRef, reactionEmojisRef } = props; const { note, $note } = props;
function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void { function onFetched(data: Pick<Misskey.entities.Note, 'reactions' | 'reactionEmojis'>): void {
reactionsRef.value = data.reactions; $note.reactions = data.reactions;
reactionCountRef.value = Object.values(data.reactions).reduce((a, b) => a + b, 0); $note.reactionCount = Object.values(data.reactions).reduce((a, b) => a + b, 0);
reactionEmojisRef.value = data.reactionEmojis; $note.reactionEmojis = data.reactionEmojis;
} }
pollingEnqueue(note); pollingEnqueue(note);
@ -177,17 +174,20 @@ function realtimeSubscribe(props: {
}); });
} }
type ReactiveNoteData = Reactive<{
reactions: Misskey.entities.Note['reactions'];
reactionCount: Misskey.entities.Note['reactionCount'];
reactionEmojis: Misskey.entities.Note['reactionEmojis'];
myReaction: Misskey.entities.Note['myReaction'];
pollChoices: NonNullable<Misskey.entities.Note['poll']>['choices'];
}>;
export function useNoteCapture(props: { export function useNoteCapture(props: {
note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>; note: Pick<Misskey.entities.Note, 'id' | 'createdAt'>;
parentNote: Misskey.entities.Note | null; parentNote: Misskey.entities.Note | null;
reactionsRef: Ref<Misskey.entities.Note['reactions']>; $note: ReactiveNoteData;
reactionCountRef: Ref<Misskey.entities.Note['reactionCount']>;
reactionEmojisRef: Ref<Misskey.entities.Note['reactionEmojis']>;
myReactionRef: Ref<Misskey.entities.Note['myReaction']>;
pollChoicesRef: Ref<NonNullable<Misskey.entities.Note['poll']>['choices'] | null>;
isDeletedRef: Ref<boolean>;
}) { }) {
const { note, parentNote, reactionsRef, reactionCountRef, reactionEmojisRef, myReactionRef, pollChoicesRef } = props; const { note, parentNote, $note } = props;
noteEvents.on(`reacted:${note.id}`, onReacted); noteEvents.on(`reacted:${note.id}`, onReacted);
noteEvents.on(`unreacted:${note.id}`, onUnreacted); noteEvents.on(`unreacted:${note.id}`, onUnreacted);
@ -203,17 +203,17 @@ export function useNoteCapture(props: {
if (newReactedKey === latestReactedKey) return; if (newReactedKey === latestReactedKey) return;
latestReactedKey = newReactedKey; latestReactedKey = newReactedKey;
if (ctx.emoji && !(ctx.emoji.name in reactionEmojisRef.value)) { if (ctx.emoji && !(ctx.emoji.name in $note.reactionEmojis)) {
reactionEmojisRef.value[ctx.emoji.name] = ctx.emoji.url; $note.reactionEmojis[ctx.emoji.name] = ctx.emoji.url;
} }
const currentCount = reactionsRef.value[ctx.reaction] || 0; const currentCount = $note.reactions[ctx.reaction] || 0;
reactionsRef.value[ctx.reaction] = currentCount + 1; $note.reactions[ctx.reaction] = currentCount + 1;
reactionCountRef.value += 1; $note.reactionCount += 1;
if ($i && (ctx.userId === $i.id)) { if ($i && (ctx.userId === $i.id)) {
myReactionRef.value = ctx.reaction; $note.myReaction = ctx.reaction;
} }
} }
@ -222,14 +222,14 @@ export function useNoteCapture(props: {
if (newUnreactedKey === latestUnreactedKey) return; if (newUnreactedKey === latestUnreactedKey) return;
latestUnreactedKey = newUnreactedKey; latestUnreactedKey = newUnreactedKey;
const currentCount = reactionsRef.value[ctx.reaction] || 0; const currentCount = $note.reactions[ctx.reaction] || 0;
reactionsRef.value[ctx.reaction] = Math.max(0, currentCount - 1); $note.reactions[ctx.reaction] = Math.max(0, currentCount - 1);
reactionCountRef.value = Math.max(0, reactionCountRef.value - 1); $note.reactionCount = Math.max(0, $note.reactionCount - 1);
if (reactionsRef.value[ctx.reaction] === 0) delete reactionsRef.value[ctx.reaction]; if ($note.reactions[ctx.reaction] === 0) delete $note.reactions[ctx.reaction];
if ($i && (ctx.userId === $i.id)) { if ($i && (ctx.userId === $i.id)) {
myReactionRef.value = null; $note.myReaction = null;
} }
} }
@ -238,7 +238,7 @@ export function useNoteCapture(props: {
if (newPollVotedKey === latestPollVotedKey) return; if (newPollVotedKey === latestPollVotedKey) return;
latestPollVotedKey = newPollVotedKey; latestPollVotedKey = newPollVotedKey;
const choices = [...pollChoicesRef.value]; const choices = [...$note.pollChoices];
choices[ctx.choice] = { choices[ctx.choice] = {
...choices[ctx.choice], ...choices[ctx.choice],
votes: choices[ctx.choice].votes + 1, votes: choices[ctx.choice].votes + 1,
@ -247,11 +247,11 @@ export function useNoteCapture(props: {
} : {}), } : {}),
}; };
pollChoicesRef.value = choices; $note.pollChoices = choices;
} }
function onDeleted(): void { function onDeleted(): void {
props.isDeletedRef.value = true; $note.isDeleted = true;
} }
onUnmounted(() => { onUnmounted(() => {