diff options
| author | syuilo <4439005+syuilo@users.noreply.github.com> | 2025-05-09 17:40:08 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-09 17:40:08 +0900 |
| commit | 8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11 (patch) | |
| tree | ae0d3573bd5a3175bc6174d33129dc64205a1436 /packages/frontend/src/components/MkNote.vue | |
| parent | refactor (diff) | |
| download | misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.gz misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.tar.bz2 misskey-8c2ab25e5f2040fcbc81bc2a02a279fed40e1c11.zip | |
Feat: No websocket mode (#15851)
* wip
* wip
* wip
* wip
* Update MkTimeline.vue
* wip
* wip
* wip
* Update MkTimeline.vue
* Update use-pagination.ts
* wip
* wip
* Update MkTimeline.vue
* Update MkTimeline.vue
* wip
* wip
* Update MkTimeline.vue
* Update MkTimeline.vue
* Update MkTimeline.vue
* wip
* Update use-pagination.ts
* wip
* Update use-pagination.ts
* Update MkNotifications.vue
* Update MkNotifications.vue
* wip
* wip
* wip
* Update use-note-capture.ts
* Update use-note-capture.ts
* Update use-note-capture.ts
* wip
* wip
* wip
* wip
* Update MkNoteDetailed.vue
* wip
* wip
* Update MkTimeline.vue
* wip
* fix
* Update MkTimeline.vue
* wip
* test
* Revert "test"
This reverts commit 3375619396c54dcda5e564eb1da444c2391208c9.
* Update use-pagination.ts
* test
* Revert "test"
This reverts commit 42c53c830e28485d2fb49061fa7cdeee31bc6a22.
* test
* Revert "test"
This reverts commit c4f8cda4aa1cec9d1eb97557145f3ad3d2d0e469.
* Update style.scss
* Update MkTimeline.vue
* Update MkTimeline.vue
* Update MkTimeline.vue
* ✌️
* Update MkTimeline.vue
* wip
* wip
* test
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkTimeline.vue
* wip
* tweak navbar
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update home.vue
* wip
* refactor
* wip
* wip
* Update note.vue
* Update navbar.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* Update MkPullToRefresh.vue
* wip
* Update MkStreamingNotificationsTimeline.vue
* Update use-pagination.ts
* wip
* improve perf
* wip
* Update MkNotesTimeline.vue
* wip
* megre
* Update use-pagination.ts
* Update use-pagination.ts
* Update MkStreamingNotesTimeline.vue
* Update use-pagination.ts
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
Diffstat (limited to 'packages/frontend/src/components/MkNote.vue')
| -rw-r--r-- | packages/frontend/src/components/MkNote.vue | 176 |
1 files changed, 105 insertions, 71 deletions
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index 980636f551..9b7658292d 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -6,11 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div v-if="!hardMuted && muted === false" - v-show="!isDeleted" ref="rootEl" v-hotkey="keymap" :class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]" - :tabindex="isDeleted ? '-1' : '0'" + tabindex="0" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> <div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div> @@ -87,7 +86,16 @@ SPDX-License-Identifier: AGPL-3.0-only <div v-if="appearNote.files && appearNote.files.length > 0"> <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/> </div> - <MkPoll v-if="appearNote.poll" :noteId="appearNote.id" :poll="appearNote.poll" :author="appearNote.user" :emojiUrls="appearNote.emojis" :class="$style.poll"/> + <MkPoll + v-if="appearNote.poll" + :noteId="appearNote.id" + :multiple="appearNote.poll.multiple" + :expiresAt="appearNote.poll.expiresAt" + :choices="$appearNote.pollChoices" + :author="appearNote.user" + :emojiUrls="appearNote.emojis" + :class="$style.poll" + /> <div v-if="isEnabledUrlPreview"> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/> </div> @@ -101,7 +109,16 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly'" style="margin-top: 6px;" :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction"> + <MkReactionsViewer + v-if="appearNote.reactionAcceptance !== 'likeOnly'" + style="margin-top: 6px;" + :reactions="$appearNote.reactions" + :reactionEmojis="$appearNote.reactionEmojis" + :myReaction="$appearNote.myReaction" + :noteId="appearNote.id" + :maxNumber="16" + @mockUpdateMyReaction="emitUpdReaction" + > <template #more> <MkA :to="`/notes/${appearNote.id}/reactions`" :class="[$style.reactionOmitted]">{{ i18n.ts.more }}</MkA> </template> @@ -125,11 +142,11 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-ban"></i> </button> <button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()"> - <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="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></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="$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 class="ti ti-plus"></i> - <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> + <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount) && $appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number($appearNote.reactionCount) }}</p> </button> <button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()"> <i class="ti ti-paperclip"></i> @@ -176,7 +193,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, useTemplateRef, watch, provide } from 'vue'; +import { computed, inject, onMounted, ref, useTemplateRef, watch, provide, shallowRef, reactive } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import { isLink } from '@@/js/is-link.js'; @@ -210,7 +227,7 @@ import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js'; import { $i } from '@/i.js'; import { i18n } from '@/i18n.js'; import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js'; -import { useNoteCapture } from '@/use/use-note-capture.js'; +import { noteEvents, useNoteCapture } from '@/use/use-note-capture.js'; import { deepClone } from '@/utility/clone.js'; import { useTooltip } from '@/use/use-tooltip.js'; import { claimAchievement } from '@/utility/achievements.js'; @@ -223,6 +240,7 @@ import { getAppearNote } from '@/utility/get-appear-note.js'; import { prefer } from '@/preferences.js'; import { getPluginHandlers } from '@/plugin.js'; import { DI } from '@/di.js'; +import { globalEvents } from '@/events.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -245,29 +263,33 @@ const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true)); const inChannel = inject('inChannel', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); -const note = ref(deepClone(props.note)); +let note = deepClone(props.note); // plugin const noteViewInterruptors = getPluginHandlers('note_view_interruptor'); if (noteViewInterruptors.length > 0) { onMounted(async () => { - let result: Misskey.entities.Note | null = deepClone(note.value); + let result: Misskey.entities.Note | null = deepClone(note); for (const interruptor of noteViewInterruptors) { try { result = await interruptor.handler(result!) as Misskey.entities.Note | null; - if (result === null) { - isDeleted.value = true; - return; - } } catch (err) { console.error(err); } } - note.value = result as Misskey.entities.Note; + note = result as Misskey.entities.Note; }); } -const isRenote = Misskey.note.isPureRenote(note.value); +const isRenote = Misskey.note.isPureRenote(note); +const appearNote = getAppearNote(note); +const $appearNote = reactive({ + reactions: appearNote.reactions, + reactionCount: appearNote.reactionCount, + reactionEmojis: appearNote.reactionEmojis, + myReaction: appearNote.myReaction, + pollChoices: appearNote.poll?.choices, +}); const rootEl = useTemplateRef('rootEl'); const menuButton = useTemplateRef('menuButton'); @@ -275,32 +297,30 @@ const renoteButton = useTemplateRef('renoteButton'); const renoteTime = useTemplateRef('renoteTime'); const reactButton = useTemplateRef('reactButton'); const clipButton = useTemplateRef('clipButton'); -const appearNote = computed(() => getAppearNote(note.value)); const galleryEl = useTemplateRef('galleryEl'); -const isMyRenote = $i && ($i.id === note.value.userId); +const isMyRenote = $i && ($i.id === note.userId); const showContent = ref(false); -const parsed = computed(() => appearNote.value.text ? mfm.parse(appearNote.value.text) : null); -const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null); -const isLong = shouldCollapsed(appearNote.value, urls.value ?? []); -const collapsed = ref(appearNote.value.cw == null && isLong); -const isDeleted = ref(false); -const muted = ref(checkMute(appearNote.value, $i?.mutedWords)); -const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true)); +const parsed = computed(() => appearNote.text ? mfm.parse(appearNote.text) : null); +const urls = computed(() => parsed.value ? extractUrlFromMfm(parsed.value).filter((url) => appearNote.renote?.url !== url && appearNote.renote?.uri !== url) : null); +const isLong = shouldCollapsed(appearNote, urls.value ?? []); +const collapsed = ref(appearNote.cw == null && isLong); +const muted = ref(checkMute(appearNote, $i?.mutedWords)); +const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords, true)); const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord); const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null); const translating = ref(false); -const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance); -const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id)); +const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.user.instance); +const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i?.id)); const renoteCollapsed = ref( prefer.s.collapseRenotes && isRenote && ( - ($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 - (appearNote.value.myReaction != null) + ($i && ($i.id === note.userId || $i.id === appearNote.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131 + ($appearNote.myReaction != null) ), ); const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({ type: 'lookup', - url: `https://${host}/notes/${appearNote.value.id}`, + url: `https://${host}/notes/${appearNote.id}`, })); /* Overload FunctionにLintが対応していないのでコメントアウト @@ -357,7 +377,7 @@ const keymap = { 'v|enter': () => { if (renoteCollapsed.value) { renoteCollapsed.value = false; - } else if (appearNote.value.cw != null) { + } else if (appearNote.cw != null) { showContent.value = !showContent.value; } else if (isLong) { collapsed.value = !collapsed.value; @@ -380,28 +400,28 @@ const keymap = { provide(DI.mfmEmojiReactCallback, (reaction) => { sound.playMisskeySfx('reaction'); misskeyApi('notes/reactions/create', { - noteId: appearNote.value.id, + noteId: appearNote.id, reaction: reaction, + }).then(() => { + noteEvents.emit(`reacted:${appearNote.id}`, { + userId: $i!.id, + reaction: reaction, + }); }); }); -if (props.mock) { - watch(() => props.note, (to) => { - note.value = deepClone(to); - }, { deep: true }); -} else { +if (!props.mock) { useNoteCapture({ - rootEl: rootEl, note: appearNote, - pureNote: note, - isDeletedRef: isDeleted, + parentNote: note, + $note: $appearNote, }); } if (!props.mock) { useTooltip(renoteButton, async (showing) => { const renotes = await misskeyApi('notes/renotes', { - noteId: appearNote.value.id, + noteId: appearNote.id, limit: 11, }); @@ -412,19 +432,19 @@ if (!props.mock) { const { dispose } = os.popup(MkUsersTooltip, { showing, users, - count: appearNote.value.renoteCount, + count: appearNote.renoteCount, targetElement: renoteButton.value, }, { closed: () => dispose(), }); }); - if (appearNote.value.reactionAcceptance === 'likeOnly') { + if (appearNote.reactionAcceptance === 'likeOnly') { useTooltip(reactButton, async (showing) => { const reactions = await misskeyApiGet('notes/reactions', { - noteId: appearNote.value.id, + noteId: appearNote.id, limit: 10, - _cacheKey_: appearNote.value.reactionCount, + _cacheKey_: $appearNote.reactionCount, }); const users = reactions.map(x => x.user); @@ -435,7 +455,7 @@ if (!props.mock) { showing, reaction: '❤️', users, - count: appearNote.value.reactionCount, + count: $appearNote.reactionCount, targetElement: reactButton.value!, }, { closed: () => dispose(), @@ -448,7 +468,7 @@ function renote(viaKeyboard = false) { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); - const { menu } = getRenoteMenu({ note: note.value, renoteButton, mock: props.mock }); + const { menu } = getRenoteMenu({ note: note, renoteButton, mock: props.mock }); os.popupMenu(menu, renoteButton.value, { viaKeyboard, }); @@ -460,8 +480,8 @@ function reply(): void { return; } os.post({ - reply: appearNote.value, - channel: appearNote.value.channel, + reply: appearNote, + channel: appearNote.channel, }).then(() => { focus(); }); @@ -470,7 +490,7 @@ function reply(): void { function react(): void { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); showMovedDialog(); - if (appearNote.value.reactionAcceptance === 'likeOnly') { + if (appearNote.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); if (props.mock) { @@ -478,8 +498,13 @@ function react(): void { } misskeyApi('notes/reactions/create', { - noteId: appearNote.value.id, + noteId: appearNote.id, reaction: '❤️', + }).then(() => { + noteEvents.emit(`reacted:${appearNote.id}`, { + userId: $i!.id, + reaction: '❤️', + }); }); const el = reactButton.value; if (el && prefer.s.animation) { @@ -492,7 +517,7 @@ function react(): void { } } else { blur(); - reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => { + reactionPicker.show(reactButton.value ?? null, note, async (reaction) => { if (prefer.s.confirmOnReact) { const confirm = await os.confirm({ type: 'question', @@ -506,14 +531,23 @@ function react(): void { if (props.mock) { emit('reaction', reaction); + $appearNote.reactions[reaction] = 1; + $appearNote.reactionCount++; + $appearNote.myReaction = reaction; return; } misskeyApi('notes/reactions/create', { - noteId: appearNote.value.id, + noteId: appearNote.id, reaction: reaction, + }).then(() => { + noteEvents.emit(`reacted:${appearNote.id}`, { + userId: $i!.id, + reaction: reaction, + }); }); - if (appearNote.value.text && appearNote.value.text.length > 100 && (Date.now() - new Date(appearNote.value.createdAt).getTime() < 1000 * 3)) { + + if (appearNote.text && appearNote.text.length > 100 && (Date.now() - new Date(appearNote.createdAt).getTime() < 1000 * 3)) { claimAchievement('reactWithoutRead'); } }, () => { @@ -522,8 +556,8 @@ function react(): void { } } -function undoReact(targetNote: Misskey.entities.Note): void { - const oldReaction = targetNote.myReaction; +function undoReact(): void { + const oldReaction = $appearNote.myReaction; if (!oldReaction) return; if (props.mock) { @@ -532,15 +566,15 @@ function undoReact(targetNote: Misskey.entities.Note): void { } misskeyApi('notes/reactions/delete', { - noteId: targetNote.id, + noteId: appearNote.id, }); } function toggleReact() { - if (appearNote.value.myReaction == null) { + if ($appearNote.myReaction == null) { react(); } else { - undoReact(appearNote.value); + undoReact(); } } @@ -556,7 +590,7 @@ function onContextmenu(ev: MouseEvent): void { ev.preventDefault(); react(); } else { - const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value }); os.contextMenu(menu, ev).then(focus).finally(cleanup); } } @@ -566,7 +600,7 @@ function showMenu(): void { return; } - const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted, currentClip: currentClip?.value }); + const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, currentClip: currentClip?.value }); os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } @@ -575,7 +609,7 @@ async function clip(): Promise<void> { return; } - os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); + os.popupMenu(await getNoteClipMenu({ note: note, currentClip: currentClip?.value }), clipButton.value).then(focus); } function showRenoteMenu(): void { @@ -590,9 +624,10 @@ function showRenoteMenu(): void { danger: true, action: () => { misskeyApi('notes/delete', { - noteId: note.value.id, + noteId: note.id, + }).then(() => { + globalEvents.emit('noteDeleted', note.id); }); - isDeleted.value = true; }, }; } @@ -601,23 +636,23 @@ function showRenoteMenu(): void { type: 'link', text: i18n.ts.renoteDetails, icon: 'ti ti-info-circle', - to: notePage(note.value), + to: notePage(note), }; if (isMyRenote) { pleaseLogin({ openOnRemote: pleaseLoginContext.value }); os.popupMenu([ renoteDetailsMenu, - getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), { type: 'divider' }, getUnrenote(), ], renoteTime.value); } else { os.popupMenu([ renoteDetailsMenu, - getCopyNoteLinkMenu(note.value, i18n.ts.copyLinkRenote), + getCopyNoteLinkMenu(note, i18n.ts.copyLinkRenote), { type: 'divider' }, - getAbuseNoteMenu(note.value, i18n.ts.reportAbuseRenote), + getAbuseNoteMenu(note, i18n.ts.reportAbuseRenote), ($i?.isModerator || $i?.isAdmin) ? getUnrenote() : undefined, ], renoteTime.value); } @@ -641,9 +676,8 @@ function focusAfter() { function readPromo() { misskeyApi('promo/read', { - noteId: appearNote.value.id, + noteId: appearNote.id, }); - isDeleted.value = true; } function emitUpdReaction(emoji: string, delta: number) { |