diff options
Diffstat (limited to 'packages/frontend/src/components/global')
14 files changed, 88 insertions, 43 deletions
diff --git a/packages/frontend/src/components/global/I18n.vue b/packages/frontend/src/components/global/I18n.vue index 162aa2bcf8..6b7723e6ac 100644 --- a/packages/frontend/src/components/global/I18n.vue +++ b/packages/frontend/src/components/global/I18n.vue @@ -1,3 +1,8 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + <template> <render/> </template> diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue index 61d7ac17d9..d1e9113c48 100644 --- a/packages/frontend/src/components/global/MkA.vue +++ b/packages/frontend/src/components/global/MkA.vue @@ -4,13 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu"> +<a ref="el" :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu"> <slot></slot> </a> </template> +<script lang="ts"> +export type MkABehavior = 'window' | 'browser' | null; +</script> + <script lang="ts" setup> -import { computed } from 'vue'; +import { computed, inject, shallowRef } from 'vue'; import * as os from '@/os.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; @@ -20,12 +24,18 @@ import { useRouter } from '@/router/supplier.js'; const props = withDefaults(defineProps<{ to: string; activeClass?: null | string; - behavior?: null | 'window' | 'browser'; + behavior?: MkABehavior; }>(), { activeClass: null, behavior: null, }); +const behavior = props.behavior ?? inject<MkABehavior>('linkNavigationBehavior', null); + +const el = shallowRef<HTMLElement>(); + +defineExpose({ $el: el }); + const router = useRouter(); const active = computed(() => { @@ -76,15 +86,13 @@ function openWindow() { } function nav(ev: MouseEvent) { - if (props.behavior === 'browser') { + if (behavior === 'browser') { location.href = props.to; return; } - if (props.behavior) { - if (props.behavior === 'window') { - return openWindow(); - } + if (behavior === 'window') { + return openWindow(); } if (ev.shiftKey) { diff --git a/packages/frontend/src/components/global/MkAd.stories.impl.ts b/packages/frontend/src/components/global/MkAd.stories.impl.ts index f6cdc2bf23..aef26ab92d 100644 --- a/packages/frontend/src/components/global/MkAd.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAd.stories.impl.ts @@ -4,11 +4,17 @@ */ /* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { expect, userEvent, waitFor, within } from '@storybook/test'; import { StoryObj } from '@storybook/vue3'; import MkAd from './MkAd.vue'; +import { i18n } from '@/i18n.js'; let lock: Promise<undefined> | undefined; +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + const common = { render(args) { return { @@ -30,7 +36,6 @@ const common = { template: '<MkAd v-bind="props" />', }; }, - /* FIXME: disabled because it still didn’t pass after applying #11267 async play({ canvasElement, args }) { if (lock) { console.warn('This test is unexpectedly running twice in parallel, fix it!'); @@ -42,9 +47,11 @@ const common = { lock = new Promise(r => resolve = r); try { + // NOTE: sleep しないと何故か落ちる + await sleep(100); const canvas = within(canvasElement); const a = canvas.getByRole<HTMLAnchorElement>('link'); - await expect(a.href).toMatch(/^https?:\/\/.*#test$/); + // await expect(a.href).toMatch(/^https?:\/\/.*#test$/); const img = within(a).getByRole('img'); await expect(img).toBeInTheDocument(); let buttons = canvas.getAllByRole<HTMLButtonElement>('button'); @@ -52,13 +59,14 @@ const common = { const i = buttons[0]; await expect(i).toBeInTheDocument(); await userEvent.click(i); - await waitFor(() => expect(canvasElement).toHaveTextContent(i18n.ts._ad.back)); + await expect(canvasElement).toHaveTextContent(i18n.ts._ad.back); await expect(a).not.toBeInTheDocument(); await expect(i).not.toBeInTheDocument(); buttons = canvas.getAllByRole<HTMLButtonElement>('button'); - await expect(buttons).toHaveLength(args.__hasReduce ? 2 : 1); - const reduce = args.__hasReduce ? buttons[0] : null; - const back = buttons[args.__hasReduce ? 1 : 0]; + const hasReduceFrequency = args.specify?.ratio !== 0; + await expect(buttons).toHaveLength(hasReduceFrequency ? 2 : 1); + const reduce = hasReduceFrequency ? buttons[0] : null; + const back = buttons[hasReduceFrequency ? 1 : 0]; if (reduce) { await expect(reduce).toBeInTheDocument(); await expect(reduce).toHaveTextContent(i18n.ts._ad.reduceFrequencyOfThisAd); @@ -80,15 +88,16 @@ const common = { lock = undefined; } }, - */ args: { prefer: [], specify: { id: 'someadid', - radio: 1, + ratio: 1, url: '#test', + place: '', + imageUrl: '', + dayOfWeek: 7, }, - __hasReduce: true, }, parameters: { layout: 'centered', @@ -138,6 +147,5 @@ export const ZeroRatio = { ...Square.args.specify, ratio: 0, }, - __hasReduce: false, }, } satisfies StoryObj<typeof MkAd>; diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue index 8f5ed760d5..bdaa8a809f 100644 --- a/packages/frontend/src/components/global/MkAd.vue +++ b/packages/frontend/src/components/global/MkAd.vue @@ -14,10 +14,20 @@ SPDX-License-Identifier: AGPL-3.0-only [$style.form_vertical]: chosen.place === 'vertical', }]" > - <a :href="chosen.url" target="_blank" :class="$style.link"> + <component + :is="self ? 'MkA' : 'a'" + :class="$style.link" + v-bind="self ? { + to: chosen.url.substring(local.length), + } : { + href: chosen.url, + rel: 'nofollow noopener', + target: '_blank', + }" + > <img :src="chosen.imageUrl" :class="$style.img"> <button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button> - </a> + </component> </div> <div v-else :class="$style.menu"> <div :class="$style.menuContainer"> @@ -32,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref } from 'vue'; +import { ref, computed } from 'vue'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; -import { host } from '@/config.js'; +import { url as local, host } from '@/config.js'; import MkButton from '@/components/MkButton.vue'; import { defaultStore } from '@/store.js'; import * as os from '@/os.js'; @@ -96,6 +106,9 @@ const choseAd = (): Ad | null => { }; const chosen = ref(choseAd()); + +const self = computed(() => chosen.value?.url.startsWith(local)); + const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null)); function reduceFrequency(): void { diff --git a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts index 933754ec4c..9d2de9f0be 100644 --- a/packages/frontend/src/components/global/MkAvatar.stories.impl.ts +++ b/packages/frontend/src/components/global/MkAvatar.stories.impl.ts @@ -33,7 +33,7 @@ const common = { }, decorators: [ (Story, context) => ({ - // eslint-disable-next-line quotes + // @ts-expect-error size is for test template: `<div :style="{ display: 'grid', width: '${context.args.size}px', height: '${context.args.size}px' }"><story/></div>`, }), ], @@ -45,6 +45,7 @@ export const ProfilePage = { ...common, args: { ...common.args, + // @ts-expect-error size is for test size: 120, indicator: true, }, diff --git a/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts b/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts index e4e90cddd5..e15dcba760 100644 --- a/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts +++ b/packages/frontend/src/components/global/MkCondensedLine.stories.impl.ts @@ -28,6 +28,7 @@ export const Default = { }; }, args: { + // @ts-expect-error text is for test text: 'This is a condensed line.', }, parameters: { @@ -41,4 +42,5 @@ export const ContainerIs100px = { template: '<div style="width: 100px;"><story/></div>', }), ], + // @ts-expect-error text is for test } satisfies StoryObj<typeof MkCondensedLine>; diff --git a/packages/frontend/src/components/global/MkError.stories.meta.ts b/packages/frontend/src/components/global/MkError.stories.meta.ts index 1abbc56f50..cd7fada189 100644 --- a/packages/frontend/src/components/global/MkError.stories.meta.ts +++ b/packages/frontend/src/components/global/MkError.stories.meta.ts @@ -3,8 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { Meta } from '@storybook/vue3'; +import MkError from './MkError.vue'; + export const argTypes = { - retry: { + onRetry: { action: 'retry', }, -}; +} satisfies Meta<typeof MkError>['argTypes']; diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts index 4ed76f6bc4..cab8d9c704 100644 --- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts +++ b/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { VNode, h, SetupContext } from 'vue'; +import { VNode, h, SetupContext, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkUrl from '@/components/global/MkUrl.vue'; @@ -16,7 +16,7 @@ import MkCode from '@/components/MkCode.vue'; import MkCodeInline from '@/components/MkCodeInline.vue'; import MkGoogle from '@/components/MkGoogle.vue'; import MkSparkle from '@/components/MkSparkle.vue'; -import MkA from '@/components/global/MkA.vue'; +import MkA, { MkABehavior } from '@/components/global/MkA.vue'; import { host } from '@/config.js'; import { defaultStore } from '@/store.js'; import { nyaize as doNyaize } from '@/scripts/nyaize.js'; @@ -43,6 +43,7 @@ type MfmProps = { parsedNodes?: mfm.MfmNode[] | null; enableEmojiMenu?: boolean; enableEmojiMenuReaction?: boolean; + linkNavigationBehavior?: MkABehavior; }; type MfmEvents = { @@ -51,6 +52,8 @@ type MfmEvents = { // eslint-disable-next-line import/no-default-export export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) { + provide('linkNavigationBehavior', props.linkNavigationBehavior); + const isNote = props.isNote ?? true; const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; diff --git a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts index eb74e874dd..1d079edd2c 100644 --- a/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts +++ b/packages/frontend/src/components/global/MkPageHeader.stories.impl.ts @@ -33,7 +33,6 @@ export const Empty = { await waitFor(async () => await wait); }, args: { - static: true, tabs: [], }, parameters: { @@ -71,8 +70,8 @@ export const IconOnly = { ...Icon.args, tabs: [ { - ...Icon.args.tabs[0], - title: undefined, + key: Icon.args.tabs[0].key, + icon: Icon.args.tabs[0].icon, iconOnly: true, }, ], diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue index e93b09721a..fcc46cc345 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -38,7 +38,6 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts"> export type Tab = { key: string; - title: string; onClick?: (ev: MouseEvent) => void; } & ( | { diff --git a/packages/frontend/src/components/global/MkTime.stories.impl.ts b/packages/frontend/src/components/global/MkTime.stories.impl.ts index 355c839113..ffd4a849a2 100644 --- a/packages/frontend/src/components/global/MkTime.stories.impl.ts +++ b/packages/frontend/src/components/global/MkTime.stories.impl.ts @@ -60,7 +60,7 @@ export const RelativeFuture = { export const AbsoluteFuture = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -97,7 +97,7 @@ export const RelativeNow = { export const AbsoluteNow = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -136,7 +136,7 @@ export const RelativeOneHourAgo = { export const AbsoluteOneHourAgo = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -175,7 +175,7 @@ export const RelativeOneDayAgo = { export const AbsoluteOneDayAgo = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -214,7 +214,7 @@ export const RelativeOneWeekAgo = { export const AbsoluteOneWeekAgo = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -253,7 +253,7 @@ export const RelativeOneMonthAgo = { export const AbsoluteOneMonthAgo = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, @@ -292,7 +292,7 @@ export const RelativeOneYearAgo = { export const AbsoluteOneYearAgo = { ...Empty, async play({ canvasElement, args }) { - await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(args.time)); + await expect(canvasElement).toHaveTextContent(dateTimeFormat.format(typeof args.time === 'string' ? new Date(args.time) : args.time ?? undefined)); }, args: { ...Empty.args, diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue index 67532268d3..23fe99bd9c 100644 --- a/packages/frontend/src/components/global/MkTime.vue +++ b/packages/frontend/src/components/global/MkTime.vue @@ -47,7 +47,7 @@ const invalid = Number.isNaN(_time); const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid; // eslint-disable-next-line vue/no-setup-props-destructure -const now = ref((props.origin ?? new Date()).getTime()); +const now = ref(props.origin?.getTime() ?? Date.now()); const ago = computed(() => (now.value - _time) / 1000/*ms*/); const relative = computed<string>(() => { @@ -77,7 +77,7 @@ let tickId: number; let currentInterval: number; function tick() { - now.value = (new Date()).getTime(); + now.value = Date.now(); const nextInterval = ago.value < 60 ? 10000 : ago.value < 3600 ? 60000 : 180000; if (currentInterval !== nextInterval) { diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index 0c3eee63ff..9d4cd559d9 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -6,6 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <component :is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel ?? 'nofollow noopener'" :target="target" + :behavior="props.navigationBehavior" @contextmenu.stop="() => {}" > <template v-if="!self"> @@ -30,11 +31,14 @@ import { url as local } from '@/config.js'; import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; +import { isEnabledUrlPreview } from '@/instance.js'; +import { MkABehavior } from '@/components/global/MkA.vue'; const props = withDefaults(defineProps<{ url: string; rel?: string; showUrlPreview?: boolean; + navigationBehavior?: MkABehavior; }>(), { showUrlPreview: true, }); @@ -44,12 +48,12 @@ const url = new URL(props.url); if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); const el = ref(); -if (props.showUrlPreview) { +if (props.showUrlPreview && isEnabledUrlPreview.value) { useTooltip(el, (showing) => { os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { showing, url: props.url, - source: el.value, + source: el.value instanceof HTMLElement ? el.value : el.value?.$el, }, {}, 'closed'); }); } diff --git a/packages/frontend/src/components/global/MkUserName.stories.impl.ts b/packages/frontend/src/components/global/MkUserName.stories.impl.ts index 88bf4f4e6c..e39061c291 100644 --- a/packages/frontend/src/components/global/MkUserName.stories.impl.ts +++ b/packages/frontend/src/components/global/MkUserName.stories.impl.ts @@ -30,7 +30,7 @@ export const Default = { }; }, async play({ canvasElement }) { - await expect(canvasElement).toHaveTextContent(userDetailed().name); + await expect(canvasElement).toHaveTextContent(userDetailed().name as string); }, args: { user: userDetailed(), |