diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue index 1354c0e7aa..e398868d54 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -458,12 +458,14 @@ useTooltip(quoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); function boostVisibility() { @@ -512,7 +514,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -528,7 +532,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -543,7 +549,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.channel) { @@ -563,7 +569,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -585,7 +593,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -643,7 +653,7 @@ function react(): void { } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -655,7 +665,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -680,7 +692,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -718,11 +732,9 @@ function showMenu(): void { os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } -async function menuVersions(viaKeyboard = false): Promise { +async function menuVersions(): Promise { const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); - os.popupMenu(menu, menuVersionsButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuVersionsButton.value).then(focus).finally(cleanup); } async function clip(): Promise { diff --git a/packages/frontend/src/components/SkNoteDetailed.vue b/packages/frontend/src/components/SkNoteDetailed.vue index 47c0f86ec0..ed0d689dc9 100644 --- a/packages/frontend/src/components/SkNoteDetailed.vue +++ b/packages/frontend/src/components/SkNoteDetailed.vue @@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only ref="rootEl" v-hotkey="keymap" :class="$style.root" + :tabindex="isDeleted ? '-1' : '0'" >
{{ i18n.ts.loadConversation }} @@ -27,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- @@ -105,7 +106,7 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._animatedMFM.play }} {{ i18n.ts._animatedMFM.stop }}
- +
@@ -135,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only class="_button" :class="$style.noteFooterButton" :style="renoted ? 'color: var(--accent) !important;' : ''" - @mousedown="renoted ? undoRenote() : boostVisibility()" + @mousedown.prevent="renoted ? undoRenote() : boostVisibility()" >

{{ number(appearNote.renoteCount) }}

@@ -162,10 +163,10 @@ SPDX-License-Identifier: AGPL-3.0-only

{{ number(appearNote.reactionCount) }}

- - @@ -244,7 +245,7 @@ import MkPoll from '@/components/MkPoll.vue'; import MkUsersTooltip from '@/components/MkUsersTooltip.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue'; import SkInstanceTicker from '@/components/SkInstanceTicker.vue'; -import { pleaseLogin } from '@/scripts/please-login.js'; +import { pleaseLogin, type OpenOnRemoteOptions } from '@/scripts/please-login.js'; import { checkWordMute } from '@/scripts/check-word-mute.js'; import { userPage } from '@/filters/user.js'; import number from '@/filters/number.js'; @@ -257,6 +258,7 @@ import { reactionPicker } from '@/scripts/reaction-picker.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; +import { host } from '@/config.js'; import { getNoteClipMenu, getNoteMenu } from '@/scripts/get-note-menu.js'; import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js'; import { useNoteCapture } from '@/scripts/use-note-capture.js'; @@ -272,6 +274,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkButton from '@/components/MkButton.vue'; import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js'; import { isEnabledUrlPreview } from '@/instance.js'; +import { type Keymap } from '@/scripts/hotkey.js'; const props = withDefaults(defineProps<{ note: Misskey.entities.Note; @@ -324,6 +327,7 @@ const quoteButton = shallowRef(); const clipButton = shallowRef(); const likeButton = shallowRef(); const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value); +const galleryEl = shallowRef>(); const isMyRenote = $i && ($i.id === note.value.userId); const showContent = ref(defaultStore.state.uncollapseCW); const isDeleted = ref(false); @@ -358,14 +362,31 @@ if ($i) { let renoting = false; +const pleaseLoginContext = computed(() => ({ + type: 'lookup', + url: `https://${host}/notes/${appearNote.value.id}`, +})); + const keymap = { - 'r': () => reply(true), - 'e|a|plus': () => react(true), - '(q)': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, - 'esc': blur, - 'm|o': () => showMenu(true), - 's': () => showContent.value !== showContent.value, -}; + 'r': () => reply(), + 'e|a|plus': () => react(), + 'q': () => { if (canRenote.value && !renoted.value && !renoting) renote(defaultStore.state.visibilityOnBoost); }, + 'm': () => showMenu(), + 'c': () => { + if (!defaultStore.state.showClipButtonInNoteFooter) return; + clip(); + }, + 'o': () => galleryEl.value?.openGallery(), + 'v|enter': () => { + if (appearNote.value.cw != null) { + showContent.value = !showContent.value; + } + }, + 'esc': { + allowRepeat: true, + callback: () => blur(), + }, +} as const satisfies Keymap; provide('react', (reaction: string) => { misskeyApi('notes/reactions/create', { @@ -425,12 +446,14 @@ useTooltip(renoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: renoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); useTooltip(quoteButton, async (showing) => { @@ -444,12 +467,14 @@ useTooltip(quoteButton, async (showing) => { if (users.length < 1) return; - os.popup(MkUsersTooltip, { + const { dispose } = os.popup(MkUsersTooltip, { showing, users, count: appearNote.value.renoteCount, targetElement: quoteButton.value, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); function boostVisibility() { @@ -474,18 +499,20 @@ if (appearNote.value.reactionAcceptance === 'likeOnly') { if (users.length < 1) return; - os.popup(MkReactionsViewerDetails, { + const { dispose } = os.popup(MkReactionsViewerDetails, { showing, reaction: '❤️', users, count: appearNote.value.reactionCount, targetElement: reactButton.value!, - }, {}, 'closed'); + }, { + closed: () => dispose(), + }); }); } function renote(visibility: Visibility, localOnly: boolean = false) { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); renoting = true; @@ -496,7 +523,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -512,7 +541,9 @@ function renote(visibility: Visibility, localOnly: boolean = false) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } misskeyApi('notes/create', { @@ -527,7 +558,7 @@ function renote(visibility: Visibility, localOnly: boolean = false) { } function quote() { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.channel) { @@ -547,7 +578,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -569,7 +602,9 @@ function quote() { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } os.toast(i18n.ts.quoted); @@ -578,20 +613,19 @@ function quote() { } } -function reply(viaKeyboard = false): void { - pleaseLogin(); +function reply(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); os.post({ reply: appearNote.value, channel: appearNote.value.channel, - animation: !viaKeyboard, }).then(() => { focus(); }); } -function react(viaKeyboard = false): void { - pleaseLogin(); +function react(): void { + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); if (appearNote.value.reactionAcceptance === 'likeOnly') { sound.playMisskeySfx('reaction'); @@ -600,12 +634,14 @@ function react(viaKeyboard = false): void { noteId: appearNote.value.id, override: defaultLike.value, }); - const el = reactButton.value as HTMLElement | null | undefined; + const el = reactButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } else { blur(); @@ -626,7 +662,7 @@ function react(viaKeyboard = false): void { } function like(): void { - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); showMovedDialog(); sound.playMisskeySfx('reaction'); misskeyApi('notes/like', { @@ -638,7 +674,9 @@ function like(): void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -663,7 +701,9 @@ function undoRenote() : void { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); const y = rect.top + (el.offsetHeight / 2); - os.popup(MkRippleEffect, { x, y }, {}, 'end'); + const { dispose } = os.popup(MkRippleEffect, { x, y }, { + end: () => dispose(), + }); } } @@ -696,27 +736,23 @@ function onContextmenu(ev: MouseEvent): void { } } -function showMenu(viaKeyboard = false): void { +function showMenu(): void { const { menu, cleanup } = getNoteMenu({ note: note.value, translating, translation, isDeleted }); - os.popupMenu(menu, menuButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuButton.value).then(focus).finally(cleanup); } -async function menuVersions(viaKeyboard = false): Promise { +async function menuVersions(): Promise { const { menu, cleanup } = await getNoteVersionsMenu({ note: note.value, menuVersionsButton }); - os.popupMenu(menu, menuVersionsButton.value, { - viaKeyboard, - }).then(focus).finally(cleanup); + os.popupMenu(menu, menuVersionsButton.value).then(focus).finally(cleanup); } -async function clip() { +async function clip(): Promise { os.popupMenu(await getNoteClipMenu({ note: note.value, isDeleted }), clipButton.value).then(focus); } -function showRenoteMenu(viaKeyboard = false): void { +function showRenoteMenu(): void { if (!isMyRenote) return; - pleaseLogin(); + pleaseLogin(undefined, pleaseLoginContext.value); os.popupMenu([{ text: i18n.ts.unrenote, icon: 'ti ti-trash', @@ -727,9 +763,7 @@ function showRenoteMenu(viaKeyboard = false): void { }); isDeleted.value = true; }, - }], renoteTime.value, { - viaKeyboard: viaKeyboard, - }); + }], renoteTime.value); } function focus() { @@ -829,6 +863,28 @@ onUnmounted(() => { transition: box-shadow 0.1s ease; overflow: clip; contain: content; + + &:focus-visible { + outline: none; + + &::after { + content: ""; + pointer-events: none; + display: block; + position: absolute; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + margin: auto; + width: calc(100% - 8px); + height: calc(100% - 8px); + border: dashed 2px var(--focus); + border-radius: var(--radius); + box-sizing: border-box; + } + } } .footer {