summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkNoteDetailed.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components/MkNoteDetailed.vue')
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue120
1 files changed, 81 insertions, 39 deletions
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index ed1c0a9e96..9a3e595789 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="rootEl"
v-hotkey="keymap"
:class="$style.root"
+ :tabindex="isDeleted ? '-1' : '0'"
>
<div v-if="appearNote.reply && appearNote.reply.replyId">
<div v-if="!conversationLoaded" style="padding: 16px">
@@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</I18n>
</span>
<div :class="$style.renoteInfo">
- <button ref="renoteTime" class="_button" :class="$style.renoteTime" @click="showRenoteMenu()">
+ <button ref="renoteTime" class="_button" :class="$style.renoteTime" @mousedown.prevent="showRenoteMenu()">
<i v-if="isMyRenote" class="ti ti-dots" style="margin-right: 4px;"></i>
<MkTime :time="note.createdAt"/>
</button>
@@ -92,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-if="appearNote.files && appearNote.files.length > 0">
- <MkMediaList :mediaList="appearNote.files"/>
+ <MkMediaList ref="galleryEl" :mediaList="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :noteId="appearNote.id" :poll="appearNote.poll" :class="$style.poll"/>
<div v-if="isEnabledUrlPreview">
@@ -118,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="renoteButton"
class="_button"
:class="$style.noteFooterButton"
- @mousedown="renote()"
+ @mousedown.prevent="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
@@ -133,10 +134,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else class="ti ti-plus"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
</button>
- <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown="clip()">
+ <button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
<i class="ti ti-paperclip"></i>
</button>
- <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="showMenu()">
+ <button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
<i class="ti ti-dots"></i>
</button>
</footer>
@@ -208,7 +209,7 @@ import MkPoll from '@/components/MkPoll.vue';
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.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 { notePage } from '@/filters/note.js';
@@ -221,6 +222,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, getRenoteMenu } from '@/scripts/get-note-menu.js';
import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
@@ -233,6 +235,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js';
+import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -280,6 +283,7 @@ const renoteTime = shallowRef<HTMLElement>();
const reactButton = shallowRef<HTMLElement>();
const clipButton = shallowRef<HTMLElement>();
const appearNote = computed(() => isRenote ? note.value.renote as Misskey.entities.Note : note.value);
+const galleryEl = shallowRef<InstanceType<typeof MkMediaList>>();
const isMyRenote = $i && ($i.id === note.value.userId);
const showContent = ref(false);
const isDeleted = ref(false);
@@ -293,14 +297,31 @@ const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
+const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
+ type: 'lookup',
+ url: `https://${host}/notes/${appearNote.value.id}`,
+}));
+
const keymap = {
- 'r': () => reply(true),
- 'e|a|plus': () => react(true),
- 'q': () => renote(true),
- 'esc': blur,
- 'm|o': () => showMenu(true),
- 's': () => showContent.value !== showContent.value,
-};
+ 'r': () => reply(),
+ 'e|a|plus': () => react(),
+ 'q': () => renote(),
+ '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', {
@@ -346,12 +367,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(),
+ });
});
if (appearNote.value.reactionAcceptance === 'likeOnly') {
@@ -366,40 +389,39 @@ 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(viaKeyboard = false) {
- pleaseLogin();
+function renote() {
+ pleaseLogin(undefined, pleaseLoginContext.value);
showMovedDialog();
const { menu } = getRenoteMenu({ note: note.value, renoteButton });
- os.popupMenu(menu, renoteButton.value, {
- viaKeyboard,
- });
+ os.popupMenu(menu, renoteButton.value);
}
-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');
@@ -408,12 +430,14 @@ function react(viaKeyboard = false): void {
noteId: appearNote.value.id,
reaction: '❤️',
});
- 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();
@@ -470,20 +494,18 @@ 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 clip() {
+async function clip(): Promise<void> {
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',
@@ -494,9 +516,7 @@ function showRenoteMenu(viaKeyboard = false): void {
});
isDeleted.value = true;
},
- }], renoteTime.value, {
- viaKeyboard: viaKeyboard,
- });
+ }], renoteTime.value);
}
function focus() {
@@ -538,6 +558,28 @@ function loadConversation() {
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;
+ }
+ }
}
.replyTo {