diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ed930437e..d2e6702a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,11 +26,13 @@ - Enhance: リアクション選択時に音を鳴らせるように - Enhance: サウンドにドライブのファイルを使用できるように - Enhance: Shareページで投稿を完了すると、親ウィンドウ(親フレーム)にpostMessageするように +- Enhance: チャンネル、クリップ、ページ、Play、ギャラリーにURLのコピーボタンを設置 #11305 - fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: コードエディタが正しく表示されない問題を修正 - Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正 - Fix: 一度に大量の通知が入った際に通知音が音割れする問題を修正 +- Fix: 共有機能をサポートしていないブラウザの場合は共有ボタンを非表示にする #11305 - Fix: 通知のグルーピング設定を変更してもリロードされるまで表示が変わらない問題を修正 #12470 ### Server diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index 580816abaa..935ca33eb5 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -48,16 +48,12 @@ import { scrollToTop } from '@/scripts/scroll.js'; import { globalEvents } from '@/events.js'; import { injectPageMetadata } from '@/scripts/page-metadata.js'; import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js'; +import { PageHeaderItem } from '@/types/page-header.js'; const props = withDefaults(defineProps<{ tabs?: Tab[]; tab?: string; - actions?: { - text: string; - icon: string; - highlighted?: boolean; - handler: (ev: MouseEvent) => void; - }[]; + actions?: PageHeaderItem[]; thin?: boolean; displayMyAvatar?: boolean; }>(), { diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue index 1d41fe7529..dc374e2925 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -86,6 +86,9 @@ import { defaultStore } from '@/store.js'; import MkNote from '@/components/MkNote.vue'; import MkInfo from '@/components/MkInfo.vue'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; +import { PageHeaderItem } from '@/types/page-header.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const router = useRouter(); @@ -167,24 +170,40 @@ async function search() { const headerActions = $computed(() => { if (channel && channel.userId) { - const share = { - icon: 'ti ti-share', - text: i18n.ts.share, - handler: async (): Promise => { - navigator.share({ - title: channel.name, - text: channel.description, - url: `${url}/channels/${channel.id}`, - }); - }, - }; + const headerItems: PageHeaderItem[] = []; - const canEdit = ($i && $i.id === channel.userId) || iAmModerator; - return canEdit ? [share, { - icon: 'ti ti-settings', - text: i18n.ts.edit, - handler: edit, - }] : [share]; + headerItems.push({ + icon: 'ti ti-link', + text: i18n.ts.copyUrl, + handler: async (): Promise => { + copyToClipboard(`${url}/channels/${channel.id}`); + os.success(); + }, + }); + + if (isSupportShare()) { + headerItems.push({ + icon: 'ti ti-share', + text: i18n.ts.share, + handler: async (): Promise => { + navigator.share({ + title: channel.name, + text: channel.description, + url: `${url}/channels/${channel.id}`, + }); + }, + }); + } + + if (($i && $i.id === channel.userId) || iAmModerator) { + headerItems.push({ + icon: 'ti ti-settings', + text: i18n.ts.edit, + handler: edit, + }); + } + + return headerItems.length > 0 ? headerItems : null; } else { return null; } diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 4573bbb81c..b32c8a3864 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -36,6 +36,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { url } from '@/config.js'; import MkButton from '@/components/MkButton.vue'; import { clipsCache } from '@/cache'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ clipId: string, @@ -118,6 +120,13 @@ const headerActions = $computed(() => clip && isOwned ? [{ clipsCache.delete(); }, }, ...(clip.isPublic ? [{ + icon: 'ti ti-link', + text: i18n.ts.copyUrl, + handler: async (): Promise => { + copyToClipboard(`${url}/clips/${clip.id}`); + os.success(); + }, +}] : []), ...(clip.isPublic && isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, handler: async (): Promise => { diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue index ebf117ffbf..4755eb5062 100644 --- a/packages/frontend/src/pages/flash/flash.vue +++ b/packages/frontend/src/pages/flash/flash.vue @@ -18,7 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only {{ flash.likedCount }} {{ flash.likedCount }} - + +
@@ -70,6 +71,8 @@ import MkFolder from '@/components/MkFolder.vue'; import MkCode from '@/components/MkCode.vue'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ id: string; @@ -89,6 +92,11 @@ function fetchFlash() { }); } +function copyLink() { + copyToClipboard(`${url}/play/${flash.id}`); + os.success(); +} + function share() { navigator.share({ title: flash.title, diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue index 3863348eae..5b551f75b5 100644 --- a/packages/frontend/src/pages/gallery/post.vue +++ b/packages/frontend/src/pages/gallery/post.vue @@ -29,7 +29,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
@@ -74,6 +75,8 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { defaultStore } from '@/store.js'; import { $i } from '@/account.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const router = useRouter(); @@ -102,6 +105,11 @@ function fetchPost() { }); } +function copyLink() { + copyToClipboard(`${url}/gallery/${post.id}`); + os.success(); +} + function share() { navigator.share({ title: post.title, diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index 98cbaab2bb..2bc053ccfe 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -34,7 +34,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + +
@@ -90,6 +91,8 @@ import { definePageMetadata } from '@/scripts/page-metadata.js'; import { pageViewInterruptors, defaultStore } from '@/store.js'; import { deepClone } from '@/scripts/clone.js'; import { $i } from '@/account.js'; +import { isSupportShare } from '@/scripts/navigator.js'; +import copyToClipboard from '@/scripts/copy-to-clipboard.js'; const props = defineProps<{ pageName: string; @@ -136,6 +139,11 @@ function share() { }); } +function copyLink() { + copyToClipboard(`${url}/@${page.user.username}/pages/${page.name}`); + os.success(); +} + function shareWithNote() { os.post({ initialText: `${page.title || page.name} ${url}/@${page.user.username}/pages/${page.name}`, diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index d0753872ff..763f6ff513 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -18,6 +18,7 @@ import { getUserMenu } from '@/scripts/get-user-menu.js'; import { clipsCache } from '@/cache.js'; import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; +import { isSupportShare } from '@/scripts/navigator.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -280,11 +281,11 @@ export function getNoteMenu(props: { window.open(appearNote.url ?? appearNote.uri, '_blank'); }, } : undefined, - { + ...(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, @@ -484,7 +485,7 @@ export function getRenoteMenu(props: { }]); } - if (!appearNote.channel || appearNote.channel?.allowRenoteToExternal) { + if (!appearNote.channel || appearNote.channel.allowRenoteToExternal) { normalRenoteItems.push(...[{ text: i18n.ts.renote, icon: 'ti ti-repeat', diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts new file mode 100644 index 0000000000..b13186a10e --- /dev/null +++ b/packages/frontend/src/scripts/navigator.ts @@ -0,0 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isSupportShare(): boolean { + return 'share' in navigator; +} diff --git a/packages/frontend/src/types/page-header.ts b/packages/frontend/src/types/page-header.ts new file mode 100644 index 0000000000..295b97a7fd --- /dev/null +++ b/packages/frontend/src/types/page-header.ts @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type PageHeaderItem = { + text: string; + icon: string; + highlighted?: boolean; + handler: (ev: MouseEvent) => void; +};