From 69de24d361e3b2431c74bb0c2d4fa746c8fe2f97 Mon Sep 17 00:00:00 2001 From: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 15 Sep 2024 18:29:10 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor(frontend):=20popupMenu=E3=81=AE?= =?UTF-8?q?=E9=A0=85=E7=9B=AE=E4=BD=9C=E6=88=90=E6=99=82=E3=81=AB=E4=B8=89?= =?UTF-8?q?=E9=A0=85=E6=BC=94=E7=AE=97=E5=AD=90=E3=82=92=E3=81=AA=E3=82=8B?= =?UTF-8?q?=E3=81=B9=E3=81=8F=E4=BD=BF=E3=82=8F=E3=81=AA=E3=81=84=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/frontend/src/account.ts | 40 +- packages/frontend/src/components/MkDrive.vue | 30 +- .../frontend/src/components/MkMediaAudio.vue | 6 +- .../frontend/src/components/MkMediaImage.vue | 45 ++- .../frontend/src/components/MkMediaVideo.vue | 6 +- .../src/components/MkPostFormAttaches.vue | 26 +- .../src/components/global/MkCustomEmoji.vue | 31 +- .../src/components/global/MkEmoji.vue | 27 +- packages/frontend/src/navbar.ts | 2 +- packages/frontend/src/pages/clip.vue | 37 +- packages/frontend/src/pages/flash/flash.vue | 25 +- packages/frontend/src/pages/gallery/post.vue | 54 +-- packages/frontend/src/pages/my-lists/list.vue | 2 + packages/frontend/src/pages/page.vue | 107 +++--- packages/frontend/src/pages/timeline.vue | 30 +- .../src/scripts/get-drive-file-menu.ts | 38 +- .../frontend/src/scripts/get-note-menu.ts | 349 ++++++++++-------- .../frontend/src/scripts/get-user-menu.ts | 285 +++++++------- packages/frontend/src/ui/_common_/common.ts | 106 ++++-- packages/frontend/src/ui/deck/column.vue | 109 +++--- packages/frontend/src/ui/deck/tl-column.vue | 58 +-- .../frontend/src/widgets/WidgetTimeline.vue | 20 +- 22 files changed, 835 insertions(+), 598 deletions(-) diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts index f388397466..84d89b1b3f 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -8,7 +8,7 @@ import * as Misskey from 'misskey-js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { i18n } from '@/i18n.js'; import { miLocalStorage } from '@/local-storage.js'; -import { MenuButton } from '@/types/menu.js'; +import type { MenuItem, MenuButton } from '@/types/menu.js'; import { del, get, set } from '@/scripts/idb-proxy.js'; import { apiUrl } from '@@/js/config.js'; import { waiting, popup, popupMenu, success, alert } from '@/os.js'; @@ -288,14 +288,26 @@ export async function openAccountMenu(opts: { }); })); + const menuItems: MenuItem[] = []; + if (opts.withExtraOperation) { - popupMenu([...[{ - type: 'link' as const, + menuItems.push({ + type: 'link', text: i18n.ts.profile, - to: `/@${ $i.username }`, + to: `/@${$i.username}`, avatar: $i, - }, { type: 'divider' as const }, ...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises, { - type: 'parent' as const, + }, { + type: 'divider', + }); + + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); + + menuItems.push({ + type: 'parent', icon: 'ti ti-plus', text: i18n.ts.addAccount, children: [{ @@ -306,18 +318,22 @@ export async function openAccountMenu(opts: { action: () => { createAccount(); }, }], }, { - type: 'link' as const, + type: 'link', icon: 'ti ti-users', text: i18n.ts.manageAccounts, to: '/settings/accounts', - }]], ev.currentTarget ?? ev.target, { - align: 'left', }); } else { - popupMenu([...(opts.includeCurrentAccount ? [createItem($i)] : []), ...accountItemPromises], ev.currentTarget ?? ev.target, { - align: 'left', - }); + if (opts.includeCurrentAccount) { + menuItems.push(createItem($i)); + } + + menuItems.push(...accountItemPromises); } + + popupMenu(menuItems, ev.currentTarget ?? ev.target, { + align: 'left', + }); } if (_DEV_) { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index dbb4917069..d9ca0a72a0 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -620,7 +620,9 @@ function fetchMoreFiles() { } function getMenu() { - const menu: MenuItem[] = [{ + const menu: MenuItem[] = []; + + menu.push({ type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, @@ -638,19 +640,25 @@ function getMenu() { }, { type: 'divider' }, { text: folder.value ? folder.value.name : i18n.ts.drive, type: 'label', - }, folder.value ? { - text: i18n.ts.renameFolder, - icon: 'ti ti-forms', - action: () => { if (folder.value) renameFolder(folder.value); }, - } : undefined, folder.value ? { - text: i18n.ts.deleteFolder, - icon: 'ti ti-trash', - action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, - } : undefined, { + }); + + if (folder.value) { + menu.push({ + text: i18n.ts.renameFolder, + icon: 'ti ti-forms', + action: () => { if (folder.value) renameFolder(folder.value); }, + }, { + text: i18n.ts.deleteFolder, + icon: 'ti ti-trash', + action: () => { deleteFolder(folder.value as Misskey.entities.DriveFolder); }, + }); + } + + menu.push({ text: i18n.ts.createFolder, icon: 'ti ti-folder-plus', action: () => { createFolder(); }, - }]; + }); return menu; } diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue index a080550ddf..b41705d5e6 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -172,9 +172,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: 再生キューに追加 { type: 'switch', @@ -222,7 +220,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.audio.id}`, diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 0d1409e2c8..91e90ec99d 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -60,6 +60,7 @@ import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; import { $i, iAmModerator } from '@/account.js'; +import type { MenuItem } from '@/types/menu.js'; const props = withDefaults(defineProps<{ image: Misskey.entities.DriveFile; @@ -111,27 +112,39 @@ watch(() => props.image, () => { }); function showMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.hide, icon: 'ti ti-eye-off', action: () => { hide.value = true; }, - }, ...(iAmModerator ? [{ - text: i18n.ts.markAsSensitive, - icon: 'ti ti-eye-exclamation', - danger: true, - action: () => { - os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); - }, - }] : []), ...($i?.id === props.image.userId ? [{ - type: 'divider' as const, - }, { - type: 'link' as const, - text: i18n.ts._fileViewer.title, - icon: 'ti ti-info-circle', - to: `/my/drive/file/${props.image.id}`, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (iAmModerator) { + menuItems.push({ + text: i18n.ts.markAsSensitive, + icon: 'ti ti-eye-exclamation', + danger: true, + action: () => { + os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true }); + }, + }); + } + + if ($i?.id === props.image.userId) { + menuItems.push({ + type: 'divider', + }, { + type: 'link', + text: i18n.ts._fileViewer.title, + icon: 'ti ti-info-circle', + to: `/my/drive/file/${props.image.id}`, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index 7c5a365148..1b1915e6c8 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -192,9 +192,7 @@ async function show() { const menuShowing = ref(false); function showMenu(ev: MouseEvent) { - let menu: MenuItem[] = []; - - menu = [ + const menu: MenuItem[] = [ // TODO: 再生キューに追加 { type: 'switch', @@ -247,7 +245,7 @@ function showMenu(ev: MouseEvent) { menu.push({ type: 'divider', }, { - type: 'link' as const, + type: 'link', text: i18n.ts._fileViewer.title, icon: 'ti ti-info-circle', to: `/my/drive/file/${props.video.id}`, diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index 8854babb6b..a7ed8725aa 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -26,6 +26,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); @@ -136,7 +137,10 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { if (menuShowing) return; const isImage = file.type.startsWith('image/'); - os.popupMenu([{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ text: i18n.ts.renameFile, icon: 'ti ti-forms', action: () => { rename(file); }, @@ -148,11 +152,17 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => { describe(file); }, - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () : void => { crop(file); }, - }] : [], { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () : void => { crop(file); }, + }); + } + + menuItems.push({ type: 'divider', }, { text: i18n.ts.attachCancel, @@ -163,7 +173,9 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void { icon: 'ti ti-trash', danger: true, action: () => { detachAndDeleteMedia(file); }, - }], ev.currentTarget ?? ev.target).then(() => menuShowing = false); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target).then(() => menuShowing = false); menuShowing = true; } diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue index dff56cd7f0..66f82a7898 100644 --- a/packages/frontend/src/components/global/MkCustomEmoji.vue +++ b/packages/frontend/src/components/global/MkCustomEmoji.vue @@ -35,6 +35,7 @@ import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ name: string; @@ -85,7 +86,9 @@ const errored = ref(url.value == null); function onClick(ev: MouseEvent) { if (props.menu) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: `:${props.name}:`, }, { @@ -95,14 +98,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(`:${props.name}:`); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ti ti-plus', - action: () => { - react(`:${props.name}:`); - sound.playMisskeySfx('reaction'); - }, - }] : []), { + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ti ti-plus', + action: () => { + react(`:${props.name}:`); + sound.playMisskeySfx('reaction'); + }, + }); + } + + menuItems.push({ text: i18n.ts.info, icon: 'ti ti-info-circle', action: async () => { @@ -114,7 +123,9 @@ function onClick(ev: MouseEvent) { closed: () => dispose(), }); }, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue index fc3745c009..f0acd3bc27 100644 --- a/packages/frontend/src/components/global/MkEmoji.vue +++ b/packages/frontend/src/components/global/MkEmoji.vue @@ -17,6 +17,7 @@ import * as os from '@/os.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as sound from '@/scripts/sound.js'; import { i18n } from '@/i18n.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ emoji: string; @@ -39,7 +40,9 @@ function computeTitle(event: PointerEvent): void { function onClick(ev: MouseEvent) { if (props.menu) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'label', text: props.emoji, }, { @@ -49,14 +52,20 @@ function onClick(ev: MouseEvent) { copyToClipboard(props.emoji); os.success(); }, - }, ...(props.menuReaction && react ? [{ - text: i18n.ts.doReaction, - icon: 'ti ti-plus', - action: () => { - react(props.emoji); - sound.playMisskeySfx('reaction'); - }, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (props.menuReaction && react) { + menuItems.push({ + text: i18n.ts.doReaction, + icon: 'ti ti-plus', + action: () => { + react(props.emoji); + sound.playMisskeySfx('reaction'); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } } diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts index a96a4f0539..ac730f8021 100644 --- a/packages/frontend/src/navbar.ts +++ b/packages/frontend/src/navbar.ts @@ -125,7 +125,7 @@ export const navbarItemDef = reactive({ ui: { title: i18n.ts.switchUi, icon: 'ti ti-devices', - action: (ev) => { + action: (ev: MouseEvent) => { os.popupMenu([{ text: i18n.ts.default, active: ui === 'default' || ui === null, diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 7bfa343b1d..1d286facb2 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -45,6 +45,7 @@ import { clipsCache } from '@/cache.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { genEmbedCode } from '@/scripts/get-embed-code.js'; +import type { MenuItem } from '@/types/menu.js'; const props = defineProps<{ clipId: string, @@ -131,30 +132,38 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{ icon: 'ti ti-share', text: i18n.ts.share, handler: (ev: MouseEvent): void => { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ icon: 'ti ti-link', text: i18n.ts.copyUrl, - action: () => { + handler: () => { copyToClipboard(`${url}/clips/${clip.value!.id}`); os.success(); }, }, { icon: 'ti ti-code', text: i18n.ts.genEmbedCode, - action: () => { + handler: () => { genEmbedCode('clips', clip.value!.id); }, - }, ...(isSupportShare() ? [{ - icon: 'ti ti-share', - text: i18n.ts.share, - action: async () => { - navigator.share({ - title: clip.value!.name, - text: clip.value!.description ?? '', - url: `${url}/clips/${clip.value!.id}`, - }); - }, - }] : [])], ev.currentTarget ?? ev.target); + }); + + if (isSupportShare()) { + menuItems.push({ + icon: 'ti ti-share', + text: i18n.ts.share, + handler: async () => { + navigator.share({ + title: clip.value!.name, + text: clip.value!.description ?? '', + url: `${url}/clips/${clip.value!.id}`, + }); + }, + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }] : []), { icon: 'ti ti-trash', diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index 3b4deaf537..cf10bee0f5 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -80,7 +80,7 @@ import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; import { isSupportShare } from '@/scripts/navigator.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; import { pleaseLogin } from '@/scripts/please-login.js'; const props = defineProps<{ @@ -104,18 +104,23 @@ function fetchFlash() { function share(ev: MouseEvent) { if (!flash.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ti ti-pencil', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ti ti-pencil', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index dfee66d906..d37aa98a95 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -171,35 +171,35 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!post.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id !== post.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !post.value) return; + const menuItems: MenuItem[] = []; - os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); - }), - }, - ] : []), - ] : []), - ]; + if ($i && $i.id !== post.value.userId) { + menuItems.push({ + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); - os.popupMenu(menu, ev.currentTarget ?? ev.target); + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !post.value) return; + + os.apiWithDialog('gallery/posts/delete', { postId: post.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => props.postId, fetchPost, { immediate: true }); diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue index a2ceb222fe..5f195693cc 100644 --- a/packages/frontend/src/pages/my-lists/list.vue +++ b/packages/frontend/src/pages/my-lists/list.vue @@ -134,12 +134,14 @@ async function removeUser(item, ev) { async function showMembershipMenu(item, ev) { const withRepliesRef = ref(item.withReplies); + os.popupMenu([{ type: 'switch', text: i18n.ts.showRepliesToOthersInTimeline, icon: 'ti ti-messages', ref: withRepliesRef, }], ev.currentTarget ?? ev.target); + watch(withRepliesRef, withReplies => { misskeyApi('users/lists/update-membership', { listId: list.value!.id, diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 381b80cd29..7926dab88b 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -121,7 +121,7 @@ import { instance } from '@/instance.js'; import { getStaticImageUrl } from '@/scripts/media-proxy.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { useRouter } from '@/router/supplier.js'; -import { MenuItem } from '@/types/menu'; +import type { MenuItem } from '@/types/menu.js'; const router = useRouter(); @@ -165,18 +165,23 @@ function fetchPage() { function share(ev: MouseEvent) { if (!page.value) return; - os.popupMenu([ - { - text: i18n.ts.shareWithNote, - icon: 'ti ti-pencil', - action: shareWithNote, - }, - ...(isSupportShare() ? [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ + text: i18n.ts.shareWithNote, + icon: 'ti ti-pencil', + action: shareWithNote, + }); + + if (isSupportShare()) { + menuItems.push({ text: i18n.ts.share, icon: 'ti ti-share', action: shareWithNavigator, - }] : []), - ], ev.currentTarget ?? ev.target); + }); + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } function copyLink() { @@ -256,51 +261,59 @@ function reportAbuse() { function showMenu(ev: MouseEvent) { if (!page.value) return; - const menu: MenuItem[] = [ - ...($i && $i.id === page.value.userId ? [ - { - icon: 'ti ti-code', - text: i18n.ts._pages.viewSource, - action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), - }, - ...($i.pinnedPageId === page.value.id ? [{ + const menuItems: MenuItem[] = []; + + if ($i && $i.id === page.value.userId) { + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts.editThisPage, + action: () => router.push(`/pages/edit/${page.value.id}`), + }); + + if ($i.pinnedPageId === page.value.id) { + menuItems.push({ icon: 'ti ti-pinned-off', text: i18n.ts.unpin, action: () => pin(false), - }] : [{ + }); + } else { + menuItems.push({ icon: 'ti ti-pin', text: i18n.ts.pin, action: () => pin(true), - }]), - ] : []), - ...($i && $i.id !== page.value.userId ? [ - { - icon: 'ti ti-exclamation-circle', - text: i18n.ts.reportAbuse, - action: reportAbuse, - }, - ...($i.isModerator || $i.isAdmin ? [ - { - type: 'divider' as const, - }, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: () => os.confirm({ - type: 'warning', - text: i18n.ts.deleteConfirm, - }).then(({ canceled }) => { - if (canceled || !page.value) return; + }); + } + } else if ($i && $i.id !== page.value.userId) { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts._pages.viewSource, + action: () => router.push(`/@${props.username}/pages/${props.pageName}/view-source`), + }, { + icon: 'ti ti-exclamation-circle', + text: i18n.ts.reportAbuse, + action: reportAbuse, + }); - os.apiWithDialog('pages/delete', { pageId: page.value.id }); - }), - }, - ] : []), - ] : []), - ]; + if ($i.isModerator || $i.isAdmin) { + menuItems.push({ + type: 'divider', + }, { + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: () => os.confirm({ + type: 'warning', + text: i18n.ts.deleteConfirm, + }).then(({ canceled }) => { + if (canceled || !page.value) return; - os.popupMenu(menu, ev.currentTarget ?? ev.target); + os.apiWithDialog('pages/delete', { pageId: page.value.id }); + }), + }); + } + } + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); } watch(() => path.value, fetchPage, { immediate: true }); diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index cc1ed3d01f..12e2db2293 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -50,7 +50,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { deepMerge } from '@/scripts/merge.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { miLocalStorage } from '@/local-storage.js'; import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js'; @@ -189,7 +189,7 @@ async function chooseChannel(ev: MouseEvent): Promise { }), (channels.length === 0 ? undefined : { type: 'divider' }), { - type: 'link' as const, + type: 'link', icon: 'ti ti-plus', text: i18n.ts.createNew, to: '/channels', @@ -258,16 +258,24 @@ const headerActions = computed(() => { icon: 'ti ti-dots', text: i18n.ts.options, handler: (ev) => { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'switch', text: i18n.ts.showRenotes, ref: withRenotes, - }, isBasicTimeline(src.value) && hasWithReplies(src.value) ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, - } : undefined, { + }); + + if (isBasicTimeline(src.value) && hasWithReplies(src.value)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ type: 'switch', text: i18n.ts.withSensitive, ref: withSensitive, @@ -276,7 +284,9 @@ const headerActions = computed(() => { text: i18n.ts.fileAttachedOnly, ref: onlyFiles, disabled: isBasicTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false, - }], ev.currentTarget ?? ev.target); + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target); }, }, ]; diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index 108648d640..c8ab9238d3 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -9,7 +9,7 @@ import { i18n } from '@/i18n.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { MenuItem } from '@/types/menu.js'; +import type { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; function rename(file: Misskey.entities.DriveFile) { @@ -87,8 +87,10 @@ async function deleteFile(file: Misskey.entities.DriveFile) { export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Misskey.entities.DriveFolder | null): MenuItem[] { const isImage = file.type.startsWith('image/'); - let menu; - menu = [{ + + const menuItems: MenuItem[] = []; + + menuItems.push({ type: 'link', to: `/my/drive/file/${file.id}`, text: i18n.ts._fileViewer.title, @@ -109,14 +111,20 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss text: i18n.ts.describeFile, icon: 'ti ti-text-caption', action: () => describe(file), - }, ...isImage ? [{ - text: i18n.ts.cropImage, - icon: 'ti ti-crop', - action: () => os.cropImage(file, { - aspectRatio: NaN, - uploadFolder: folder ? folder.id : folder, - }), - }] : [], { type: 'divider' }, { + }); + + if (isImage) { + menuItems.push({ + text: i18n.ts.cropImage, + icon: 'ti ti-crop', + action: () => os.cropImage(file, { + aspectRatio: NaN, + uploadFolder: folder ? folder.id : folder, + }), + }); + } + + menuItems.push({ type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, icon: 'ti ti-pencil', action: () => os.post({ @@ -138,17 +146,17 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss icon: 'ti ti-trash', danger: true, action: () => deleteFile(file), - }]; + }); if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyFileId, action: () => { copyToClipboard(file.id); }, - }]); + }); } - return menu; + return menuItems; } diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index 49f3199887..5705b4e68e 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -99,11 +99,13 @@ export async function getNoteClipMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', + default: null, label: i18n.ts.name, }, description: { type: 'string', required: false, + default: null, multiline: true, label: i18n.ts.description, }, @@ -264,7 +266,7 @@ export function getNoteMenu(props: { title: i18n.ts.numberOfDays, }); - if (canceled) return; + if (canceled || days == null) return; os.apiWithDialog('admin/promo/create', { noteId: appearNote.id, @@ -295,161 +297,23 @@ export function getNoteMenu(props: { props.translation.value = res; } - let menu: MenuItem[]; + const menuItems: MenuItem[] = []; + if ($i) { const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); - menu = [ - ...( - props.currentClip?.userId === $i.id ? [{ - icon: 'ti ti-backspace', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, { type: 'divider' }] : [] - ), { - icon: 'ti ti-info-circle', - text: i18n.ts.details, - action: openDetail, - }, { - icon: 'ti ti-copy', - text: i18n.ts.copyContent, - action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); - }, - } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode), - ...(isSupportShare() ? [{ - icon: 'ti ti-share', - text: i18n.ts.share, - action: share, - }] : []), - $i && $i.policies.canUseTranslator && instance.translatorAvailable ? { - icon: 'ti ti-language-hiragana', - text: i18n.ts.translate, - action: translate, - } : undefined, - { type: 'divider' }, - statePromise.then(state => state.isFavorited ? { - icon: 'ti ti-star-off', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false), - } : { - icon: 'ti ti-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true), - }), - { - type: 'parent' as const, - icon: 'ti ti-paperclip', - text: i18n.ts.clip, - children: () => getNoteClipMenu(props), - }, - statePromise.then(state => state.isMutedThread ? { - icon: 'ti ti-message-off', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false), - } : { - icon: 'ti ti-message-off', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true), - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? { - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => togglePin(false), - } : { - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => togglePin(true), - } : undefined, - { - type: 'parent' as const, - icon: 'ti ti-user', - text: i18n.ts.user, - children: async () => { - const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); - const { menu, cleanup } = getUserMenu(user); - cleanups.push(cleanup); - return menu; - }, - }, - /* - ...($i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - { - icon: 'ti ti-speakerphone', - text: i18n.ts.promote, - action: promote - }] - : [] - ),*/ - ...(appearNote.userId !== $i.id ? [ - { type: 'divider' }, - appearNote.userId !== $i.id ? getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse) : undefined, - ] - : [] - ), - ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ - { type: 'divider' }, - { - type: 'parent' as const, - icon: 'ti ti-device-tv', - text: i18n.ts.channel, - children: async () => { - const channelChildMenu = [] as MenuItem[]; + if (props.currentClip?.userId === $i.id) { + menuItems.push({ + icon: 'ti ti-backspace', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, { type: 'divider' }); + } - const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); - - if (channel.pinnedNoteIds.includes(appearNote.id)) { - channelChildMenu.push({ - icon: 'ti ti-pinned-off', - text: i18n.ts.unpin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), - }), - }); - } else { - channelChildMenu.push({ - icon: 'ti ti-pin', - text: i18n.ts.pin, - action: () => os.apiWithDialog('channels/update', { - channelId: appearNote.channel!.id, - pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], - }), - }); - } - return channelChildMenu; - }, - }, - ] - : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - { type: 'divider' }, - appearNote.userId === $i.id ? { - icon: 'ti ti-edit', - text: i18n.ts.deleteAndEdit, - action: delEdit, - } : undefined, - { - icon: 'ti ti-trash', - text: i18n.ts.delete, - danger: true, - action: del, - }] - : [] - )] - .filter(x => x !== undefined); - } else { - menu = [{ + menuItems.push({ icon: 'ti ti-info-circle', text: i18n.ts.details, action: openDetail, @@ -457,35 +321,194 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink), - (appearNote.url || appearNote.uri) ? { - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } + + if (isSupportShare()) { + menuItems.push({ + icon: 'ti ti-share', + text: i18n.ts.share, + action: share, + }); + } + + if ($i.policies.canUseTranslator && instance.translatorAvailable) { + menuItems.push({ + icon: 'ti ti-language-hiragana', + text: i18n.ts.translate, + action: translate, + }); + } + + menuItems.push({ type: 'divider' }); + + menuItems.push(statePromise.then(state => state.isFavorited ? { + icon: 'ti ti-star-off', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'ti ti-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + })); + + menuItems.push({ + type: 'parent', + icon: 'ti ti-paperclip', + text: i18n.ts.clip, + children: () => getNoteClipMenu(props), + }); + + menuItems.push(statePromise.then(state => state.isMutedThread ? { + icon: 'ti ti-message-off', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'ti ti-message-off', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + })); + + if (appearNote.userId === $i.id) { + if (($i.pinnedNoteIds ?? []).includes(appearNote.id)) { + menuItems.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => togglePin(false), + }); + } else { + menuItems.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => togglePin(true), + }); + } + } + + menuItems.push({ + type: 'parent', + icon: 'ti ti-user', + text: i18n.ts.user, + children: async () => { + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); + const { menu, cleanup } = getUserMenu(user); + cleanups.push(cleanup); + return menu; }, - } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)] - .filter(x => x !== undefined); + }); + + if (appearNote.userId !== $i.id) { + menuItems.push({ type: 'divider' }); + menuItems.push(getAbuseNoteMenu(appearNote, i18n.ts.reportAbuse)); + } + + if (appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin)) { + menuItems.push({ type: 'divider' }); + menuItems.push({ + type: 'parent', + icon: 'ti ti-device-tv', + text: i18n.ts.channel, + children: async () => { + const channelChildMenu = [] as MenuItem[]; + + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; + }, + }); + } + + if (appearNote.userId === $i.id || $i.isModerator || $i.isAdmin) { + menuItems.push({ type: 'divider' }); + if (appearNote.userId === $i.id) { + menuItems.push({ + icon: 'ti ti-edit', + text: i18n.ts.deleteAndEdit, + action: delEdit, + }); + } + menuItems.push({ + icon: 'ti ti-trash', + text: i18n.ts.delete, + danger: true, + action: del, + }); + } + } else { + menuItems.push({ + icon: 'ti ti-info-circle', + text: i18n.ts.details, + action: openDetail, + }, { + icon: 'ti ti-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)); + + if (appearNote.url || appearNote.uri) { + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); + }, + }); + } else { + menuItems.push(getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)); + } } if (noteActions.length > 0) { - menu = menu.concat([{ type: 'divider' }, ...noteActions.map(action => ({ + menuItems.push({ type: 'divider' }); + + menuItems.push(...noteActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(appearNote); }, - }))]); + }))); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyNoteId, action: () => { copyToClipboard(appearNote.id); + os.success(); }, - }]); + }); } const cleanup = () => { @@ -496,7 +519,7 @@ export function getNoteMenu(props: { }; return { - menu, + menu: menuItems, cleanup, }; } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33316b4ab6..5b4c06fec8 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -148,133 +148,154 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); } - let menu: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + menuItems.push({ icon: 'ti ti-at', text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host ?? host}`); }, - }, ...( notesSearchAvailable && (user.host == null || canSearchNonLocalNotes) ? [{ - icon: 'ti ti-search', - text: i18n.ts.searchThisUsersNotes, - action: () => { - router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); - }, - }] : []) - , ...(iAmModerator ? [{ - icon: 'ti ti-user-exclamation', - text: i18n.ts.moderation, - action: () => { - router.push(`/admin/user/${user.id}`); - }, - }] : []), { + }); + + if (notesSearchAvailable && (user.host == null || canSearchNonLocalNotes)) { + menuItems.push({ + icon: 'ti ti-search', + text: i18n.ts.searchThisUsersNotes, + action: () => { + router.push(`/search?username=${encodeURIComponent(user.username)}${user.host != null ? '&host=' + encodeURIComponent(user.host) : ''}`); + }, + }); + } + + if (iAmModerator) { + menuItems.push({ + icon: 'ti ti-user-exclamation', + text: i18n.ts.moderation, + action: () => { + router.push(`/admin/user/${user.id}`); + }, + }); + } + + menuItems.push({ icon: 'ti ti-rss', text: i18n.ts.copyRSS, action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }, ...(user.host != null && user.url != null ? [{ - icon: 'ti ti-external-link', - text: i18n.ts.showOnRemote, - action: () => { - if (user.url == null) return; - window.open(user.url, '_blank', 'noopener'); - }, - }] : [{ - icon: 'ti ti-code', - text: i18n.ts.genEmbedCode, - type: 'parent' as const, - children: [{ - text: i18n.ts.noteOfThisUser, + }); + + if (user.host != null && user.url != null) { + menuItems.push({ + icon: 'ti ti-external-link', + text: i18n.ts.showOnRemote, action: () => { - genEmbedCode('user-timeline', user.id); + if (user.url == null) return; + window.open(user.url, '_blank', 'noopener'); }, - }], // TODO: ユーザーカードの埋め込みなど - }]), { + }); + } else { + menuItems.push({ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent', + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }); + } + + menuItems.push({ icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, ...($i ? [{ - icon: 'ti ti-mail', - text: i18n.ts.sendMessage, - action: () => { - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; - os.post({ specified: user, initialText: `${canonical} ` }); - }, - }, { type: 'divider' }, { - icon: 'ti ti-pencil', - text: i18n.ts.editMemo, - action: () => { - editMemo(); - }, - }, { - type: 'parent', - icon: 'ti ti-list', - text: i18n.ts.addToList, - children: async () => { - const lists = await userListsCache.fetch(); - return lists.map(list => { - const isListed = ref(list.userIds.includes(user.id)); - cleanups.push(watch(isListed, () => { - if (isListed.value) { - os.apiWithDialog('users/lists/push', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.push(user.id); - }); - } else { - os.apiWithDialog('users/lists/pull', { - listId: list.id, - userId: user.id, - }).then(() => { - list.userIds.splice(list.userIds.indexOf(user.id), 1); - }); - } - })); + }); - return { - type: 'switch', - text: list.name, - ref: isListed, - }; - }); - }, - }, { - type: 'parent', - icon: 'ti ti-antenna', - text: i18n.ts.addToAntenna, - children: async () => { - const antennas = await antennasCache.fetch(); - const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; - return antennas.filter((a) => a.src === 'users').map(antenna => ({ - text: antenna.name, - action: async () => { - await os.apiWithDialog('antennas/update', { - antennaId: antenna.id, - name: antenna.name, - keywords: antenna.keywords, - excludeKeywords: antenna.excludeKeywords, - src: antenna.src, - userListId: antenna.userListId, - users: [...antenna.users, canonical], - caseSensitive: antenna.caseSensitive, - withReplies: antenna.withReplies, - withFile: antenna.withFile, - notify: antenna.notify, - }); - antennasCache.delete(); - }, - })); - }, - }] : [])] as any; + if ($i) { + menuItems.push({ + icon: 'ti ti-mail', + text: i18n.ts.sendMessage, + action: () => { + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`; + os.post({ specified: user, initialText: `${canonical} ` }); + }, + }, { type: 'divider' }, { + icon: 'ti ti-pencil', + text: i18n.ts.editMemo, + action: editMemo, + }, { + type: 'parent', + icon: 'ti ti-list', + text: i18n.ts.addToList, + children: async () => { + const lists = await userListsCache.fetch(); + return lists.map(list => { + const isListed = ref(list.userIds?.includes(user.id) ?? false); + cleanups.push(watch(isListed, () => { + if (isListed.value) { + os.apiWithDialog('users/lists/push', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.push(user.id); + }); + } else { + os.apiWithDialog('users/lists/pull', { + listId: list.id, + userId: user.id, + }).then(() => { + list.userIds?.splice(list.userIds?.indexOf(user.id), 1); + }); + } + })); + + return { + type: 'switch', + text: list.name, + ref: isListed, + }; + }); + }, + }, { + type: 'parent', + icon: 'ti ti-antenna', + text: i18n.ts.addToAntenna, + children: async () => { + const antennas = await antennasCache.fetch(); + const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; + return antennas.filter((a) => a.src === 'users').map(antenna => ({ + text: antenna.name, + action: async () => { + await os.apiWithDialog('antennas/update', { + antennaId: antenna.id, + name: antenna.name, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + src: antenna.src, + userListId: antenna.userListId, + users: [...antenna.users, canonical], + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + }); + antennasCache.delete(); + }, + })); + }, + }); + } if ($i && meId !== user.id) { if (iAmModerator) { - menu = menu.concat([{ + menuItems.push({ type: 'parent', icon: 'ti ti-badges', text: i18n.ts.roles, @@ -312,13 +333,14 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }, })); }, - }]); + }); } // フォローしたとしても user.isFollowing はリアルタイム更新されないので不便なため //if (user.isFollowing) { - const withRepliesRef = ref(user.withReplies); - menu = menu.concat([{ + const withRepliesRef = ref(user.withReplies ?? false); + + menuItems.push({ type: 'switch', icon: 'ti ti-messages', text: i18n.ts.showRepliesToOthersInTimeline, @@ -327,7 +349,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: user.notify === 'none' ? 'ti ti-bell' : 'ti ti-bell-off', text: user.notify === 'none' ? i18n.ts.notifyNotes : i18n.ts.unnotifyNotes, action: toggleNotify, - }]); + }); + watch(withRepliesRef, (withReplies) => { misskeyApi('following/update', { userId: user.id, @@ -338,7 +361,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter }); //} - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: user.isMuted ? 'ti ti-eye' : 'ti ti-eye-off', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, action: toggleMute, @@ -350,70 +373,68 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter icon: 'ti ti-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, action: toggleBlock, - }]); + }); if (user.isFollowed) { - menu = menu.concat([{ + menuItems.push({ icon: 'ti ti-link-off', text: i18n.ts.breakFollow, action: invalidateFollow, - }]); + }); } - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-exclamation-circle', text: i18n.ts.reportAbuse, action: reportAbuse, - }]); + }); } if (user.host !== null) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-refresh', text: i18n.ts.updateRemoteUser, action: userInfoUpdate, - }]); + }); } if (defaultStore.state.devMode) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-id', text: i18n.ts.copyUserId, action: () => { copyToClipboard(user.id); }, - }]); + }); } if ($i && meId === user.id) { - menu = menu.concat([{ type: 'divider' }, { + menuItems.push({ type: 'divider' }, { icon: 'ti ti-pencil', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); }, - }]); + }); } if (userActions.length > 0) { - menu = menu.concat([{ type: 'divider' }, ...userActions.map(action => ({ + menuItems.push({ type: 'divider' }, ...userActions.map(action => ({ icon: 'ti ti-plug', text: action.title, action: () => { action.handler(user); }, - }))]); + }))); } - const cleanup = () => { - if (_DEV_) console.log('user menu cleanup', cleanups); - for (const cl of cleanups) { - cl(); - } - }; - return { - menu, - cleanup, + menu: menuItems, + cleanup: () => { + if (_DEV_) console.log('user menu cleanup', cleanups); + for (const cl of cleanups) { + cl(); + } + }, }; } diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts index b067e721a5..f908803f01 100644 --- a/packages/frontend/src/ui/_common_/common.ts +++ b/packages/frontend/src/ui/_common_/common.ts @@ -41,7 +41,9 @@ function toolsMenuItems(): MenuItem[] { } export function openInstanceMenu(ev: MouseEvent) { - os.popupMenu([{ + const menuItems: MenuItem[] = []; + + menuItems.push({ text: instance.name ?? host, type: 'label', }, { @@ -69,12 +71,18 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.ads, icon: 'ti ti-ad', to: '/ads', - }, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? { - type: 'link', - to: '/invite', - text: i18n.ts.invite, - icon: 'ti ti-user-plus', - } : undefined, { + }); + + if ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) { + menuItems.push({ + type: 'link', + to: '/invite', + text: i18n.ts.invite, + icon: 'ti ti-user-plus', + }); + } + + menuItems.push({ type: 'parent', text: i18n.ts.tools, icon: 'ti ti-tool', @@ -84,43 +92,69 @@ export function openInstanceMenu(ev: MouseEvent) { text: i18n.ts.inquiry, icon: 'ti ti-help-circle', to: '/contact', - }, (instance.impressumUrl) ? { - type: 'a', - text: i18n.ts.impressum, - icon: 'ti ti-file-invoice', - href: instance.impressumUrl, - target: '_blank', - } : undefined, (instance.tosUrl) ? { - type: 'a', - text: i18n.ts.termsOfService, - icon: 'ti ti-notebook', - href: instance.tosUrl, - target: '_blank', - } : undefined, (instance.privacyPolicyUrl) ? { - type: 'a', - text: i18n.ts.privacyPolicy, - icon: 'ti ti-shield-lock', - href: instance.privacyPolicyUrl, - target: '_blank', - } : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : { type: 'divider' }, { + }); + + if (instance.impressumUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.impressum, + icon: 'ti ti-file-invoice', + href: instance.impressumUrl, + target: '_blank', + }); + } + + if (instance.tosUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.termsOfService, + icon: 'ti ti-notebook', + href: instance.tosUrl, + target: '_blank', + }); + } + + if (instance.privacyPolicyUrl) { + menuItems.push({ + type: 'a', + text: i18n.ts.privacyPolicy, + icon: 'ti ti-shield-lock', + href: instance.privacyPolicyUrl, + target: '_blank', + }); + } + + if (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) { + menuItems.push({ type: 'divider' }); + } + + menuItems.push({ type: 'a', text: i18n.ts.document, icon: 'ti ti-bulb', href: 'https://misskey-hub.net/docs/for-users/', target: '_blank', - }, ($i) ? { - text: i18n.ts._initialTutorial.launchTutorial, - icon: 'ti ti-presentation', - action: () => { - const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { - closed: () => dispose(), - }); - }, - } : undefined, { + }); + + if ($i) { + menuItems.push({ + text: i18n.ts._initialTutorial.launchTutorial, + icon: 'ti ti-presentation', + action: () => { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, { + closed: () => dispose(), + }); + }, + }); + } + + menuItems.push({ type: 'link', text: i18n.ts.aboutMisskey, to: '/about-misskey', - }], ev.currentTarget ?? ev.target, { + }); + + os.popupMenu(menuItems, ev.currentTarget ?? ev.target, { align: 'left', }); } diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index 893301122e..62852994b5 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -104,7 +104,27 @@ function toggleActive() { } function getMenu() { - let items: MenuItem[] = [{ + const menuItems: MenuItem[] = []; + + if (props.menu) { + menuItems.push(...props.menu, { + type: 'divider', + }); + } + + if (props.refresher) { + menuItems.push({ + icon: 'ti ti-refresh', + text: i18n.ts.reload, + action: () => { + if (props.refresher) { + props.refresher(); + } + }, + }); + } + + menuItems.push({ icon: 'ti ti-settings', text: i18n.ts._deck.configureColumn, action: async () => { @@ -129,74 +149,73 @@ function getMenu() { if (canceled) return; updateColumn(props.column.id, result); }, + }); + + const moveToMenuItems: MenuItem[] = []; + + moveToMenuItems.push({ + icon: 'ti ti-arrow-left', + text: i18n.ts._deck.swapLeft, + action: () => { + swapLeftColumn(props.column.id); + }, }, { - type: 'parent', - text: i18n.ts.move + '...', - icon: 'ti ti-arrows-move', - children: [{ - icon: 'ti ti-arrow-left', - text: i18n.ts._deck.swapLeft, - action: () => { - swapLeftColumn(props.column.id); - }, - }, { - icon: 'ti ti-arrow-right', - text: i18n.ts._deck.swapRight, - action: () => { - swapRightColumn(props.column.id); - }, - }, props.isStacked ? { + icon: 'ti ti-arrow-right', + text: i18n.ts._deck.swapRight, + action: () => { + swapRightColumn(props.column.id); + }, + }); + + if (props.isStacked) { + moveToMenuItems.push({ icon: 'ti ti-arrow-up', text: i18n.ts._deck.swapUp, action: () => { swapUpColumn(props.column.id); }, - } : undefined, props.isStacked ? { + }, { icon: 'ti ti-arrow-down', text: i18n.ts._deck.swapDown, action: () => { swapDownColumn(props.column.id); }, - } : undefined], + }); + } + + menuItems.push({ + type: 'parent', + text: i18n.ts.move + '...', + icon: 'ti ti-arrows-move', + children: moveToMenuItems, }, { icon: 'ti ti-stack-2', text: i18n.ts._deck.stackLeft, action: () => { stackLeftColumn(props.column.id); }, - }, props.isStacked ? { - icon: 'ti ti-window-maximize', - text: i18n.ts._deck.popRight, - action: () => { - popRightColumn(props.column.id); - }, - } : undefined, { type: 'divider' }, { + }); + + if (props.isStacked) { + menuItems.push({ + icon: 'ti ti-window-maximize', + text: i18n.ts._deck.popRight, + action: () => { + popRightColumn(props.column.id); + }, + }); + } + + menuItems.push({ type: 'divider' }, { icon: 'ti ti-trash', text: i18n.ts.remove, danger: true, action: () => { removeColumn(props.column.id); }, - }]; + }); - if (props.menu) { - items.unshift({ type: 'divider' }); - items = props.menu.concat(items); - } - - if (props.refresher) { - items = [{ - icon: 'ti ti-refresh', - text: i18n.ts.reload, - action: () => { - if (props.refresher) { - props.refresher(); - } - }, - }, ...items]; - } - - return items; + return menuItems; } function showSettingsMenu(ev: MouseEvent) { diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue index e210ee7b7a..8f6bcb0a1e 100644 --- a/packages/frontend/src/ui/deck/tl-column.vue +++ b/packages/frontend/src/ui/deck/tl-column.vue @@ -113,29 +113,41 @@ function onNote() { sound.playMisskeySfxFile(soundSetting.value); } -const menu = computed(() => [{ - icon: 'ti ti-pencil', - text: i18n.ts.timeline, - action: setType, -}, { - icon: 'ti ti-bell', - text: i18n.ts._deck.newNoteNotificationSettings, - action: () => soundSettingsButton(soundSetting), -}, { - type: 'switch', - text: i18n.ts.showRenotes, - ref: withRenotes, -}, hasWithReplies(props.column.tl) ? { - type: 'switch', - text: i18n.ts.showRepliesToOthersInTimeline, - ref: withReplies, - disabled: onlyFiles, -} : undefined, { - type: 'switch', - text: i18n.ts.fileAttachedOnly, - ref: onlyFiles, - disabled: hasWithReplies(props.column.tl) ? withReplies : false, -}]); +const menu = computed(() => { + const menuItems: MenuItem[] = []; + + menuItems.push({ + icon: 'ti ti-pencil', + text: i18n.ts.timeline, + action: setType, + }, { + icon: 'ti ti-bell', + text: i18n.ts._deck.newNoteNotificationSettings, + action: () => soundSettingsButton(soundSetting), + }, { + type: 'switch', + text: i18n.ts.showRenotes, + ref: withRenotes, + }); + + if (hasWithReplies(props.column.tl)) { + menuItems.push({ + type: 'switch', + text: i18n.ts.showRepliesToOthersInTimeline, + ref: withReplies, + disabled: onlyFiles, + }); + } + + menuItems.push({ + type: 'switch', + text: i18n.ts.fileAttachedOnly, + ref: onlyFiles, + disabled: hasWithReplies(props.column.tl) ? withReplies : false, + }); + + return menuItems; +})