diff options
| author | Kagami Sascha Rosylight <saschanaz@outlook.com> | 2023-03-19 08:59:31 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-19 16:59:31 +0900 |
| commit | c091d9e6d558ceb743fb8b647b28dbce8f994d41 (patch) | |
| tree | 7b6eb79f10e0ffc9843d17e46aa556e3f0c0146a /packages/frontend | |
| parent | perf(backend): improve performance of timeline apis (diff) | |
| download | misskey-c091d9e6d558ceb743fb8b647b28dbce8f994d41.tar.gz misskey-c091d9e6d558ceb743fb8b647b28dbce8f994d41.tar.bz2 misskey-c091d9e6d558ceb743fb8b647b28dbce8f994d41.zip | |
feat(frontend/MkUrlPreview): oEmbedのサポート (#10306)
* feat(frontend/MkUrlPreview): oEmbedのサポート
* Update CHANGELOG.md
* Update CHANGELOG.md
* Update CHANGELOG.md
* playerとoEmbedの統合
* Update CHANGELOG.md
* loading=lazyはここでは不要
* border: 0
* プレビュー直後にautoplayできる機能の復旧
* add test
* refactor test
* explain about cache
* expandPreviewはもう使わない
* summaly v4
* update summaly
* scrolling=no to fix pixiv
---------
Co-authored-by: tamaina <tamaina@hotmail.co.jp>
Diffstat (limited to 'packages/frontend')
| -rw-r--r-- | packages/frontend/package.json | 2 | ||||
| -rw-r--r-- | packages/frontend/src/components/MkUrlPreview.vue | 32 | ||||
| -rw-r--r-- | packages/frontend/test/init.ts | 4 | ||||
| -rw-r--r-- | packages/frontend/test/url-preview.test.ts | 140 |
4 files changed, 167 insertions, 11 deletions
diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 594f8781bd..54404c8c53 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -97,7 +97,9 @@ "eslint-plugin-vue": "9.9.0", "happy-dom": "8.9.0", "start-server-and-test": "2.0.0", + "summaly": "github:misskey-dev/summaly", "vitest": "^0.29.2", + "vitest-fetch-mock": "^0.2.2", "vue-eslint-parser": "9.1.0", "vue-tsc": "1.2.0" } diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 5381ecbfa5..094709e093 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -1,7 +1,18 @@ <template> -<template v-if="playerEnabled"> - <div :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> - <iframe v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" :class="$style.playerIframe" :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> +<template v-if="player.url && playerEnabled"> + <div + :class="$style.player" + :style="player.width ? `padding: ${(player.height || 0) / player.width * 100}% 0 0` : `padding: ${(player.height || 0)}px 0 0`" + > + <iframe + v-if="player.url.startsWith('http://') || player.url.startsWith('https://')" + sandbox="allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin" + scrolling="no" + :allow="player.allow.join(';')" + :class="$style.playerIframe" + :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" + :style="{ border: 0 }" + ></iframe> <span v-else>invalid url</span> </div> <div :class="$style.action"> @@ -28,7 +39,7 @@ <header :class="$style.header"> <h1 v-if="unknownUrl" :class="$style.title">{{ url }}</h1> <h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1> - <h1 v-else :class="$style.title" :title="title">{{ title }}</h1> + <h1 v-else :class="$style.title" :title="title ?? undefined">{{ title }}</h1> </header> <p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p> <p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p> @@ -37,7 +48,7 @@ <img v-if="icon" :class="$style.siteIcon" :src="icon"/> <p v-if="unknownUrl" :class="$style.siteName">?</p> <p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p> - <p v-else :class="$style.siteName" :title="sitename">{{ sitename }}</p> + <p v-else :class="$style.siteName" :title="sitename ?? undefined">{{ sitename }}</p> </footer> </article> </component> @@ -59,6 +70,7 @@ <script lang="ts" setup> import { defineAsyncComponent, onUnmounted } from 'vue'; +import type { summaly } from 'summaly'; import { url as local } from '@/config'; import { i18n } from '@/i18n'; import * as os from '@/os'; @@ -66,6 +78,8 @@ import { deviceKind } from '@/scripts/device-kind'; import MkButton from '@/components/MkButton.vue'; import { versatileLang } from '@/scripts/intl-const'; +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + const props = withDefaults(defineProps<{ url: string; detail?: boolean; @@ -91,7 +105,7 @@ let player = $ref({ url: null, width: null, height: null, -}); +} as SummalyResult['player']); let playerEnabled = $ref(false); let tweetId = $ref<string | null>(null); let tweetExpanded = $ref(props.detail); @@ -114,11 +128,7 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`).then(res => { - res.json().then(info => { - if (info.url == null) { - unknownUrl = true; - return; - } + res.json().then((info: SummalyResult) => { title = info.title; description = info.description; thumbnail = info.thumbnail; diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts index 96730e7b56..295107e143 100644 --- a/packages/frontend/test/init.ts +++ b/packages/frontend/test/init.ts @@ -1,4 +1,8 @@ import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); // Set i18n import locales from '../../../locales'; diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts new file mode 100644 index 0000000000..205982a40a --- /dev/null +++ b/packages/frontend/test/url-preview.test.ts @@ -0,0 +1,140 @@ +import { describe, test, assert, afterEach } from 'vitest'; +import { render, cleanup, type RenderResult } from '@testing-library/vue'; +import './init'; +import type { summaly } from 'summaly'; +import { directives } from '@/directives'; +import MkUrlPreview from '@/components/MkUrlPreview.vue'; + +type SummalyResult = Awaited<ReturnType<typeof summaly>>; + +describe('MkMediaImage', () => { + const renderPreviewBy = async (summary: Partial<SummalyResult>): Promise<RenderResult> => { + if (!summary.player) { + summary.player = { + url: null, + width: null, + height: null, + allow: [], + }; + } + + fetchMock.mockOnceIf(/^\/url?/, () => { + return { + status: 200, + body: JSON.stringify(summary), + }; + }); + + const result = render(MkUrlPreview, { + props: { url: summary.url }, + global: { directives }, + }); + + await new Promise<void>(resolve => { + const observer = new MutationObserver(() => { + resolve(); + observer.disconnect(); + }); + observer.observe(result.container, { childList: true, subtree: true }); + }); + + return result; + }; + + const renderAndOpenPreview = async (summary: Partial<SummalyResult>): Promise<HTMLIFrameElement | null> => { + const mkUrlPreview = await renderPreviewBy(summary); + const buttons = mkUrlPreview.getAllByRole('button'); + buttons[0].click(); + // Wait for the click event to be fired + await Promise.resolve(); + + return mkUrlPreview.container.querySelector('iframe'); + }; + + afterEach(() => { + fetchMock.resetMocks(); + cleanup(); + }); + + test('Should render the description', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + description: 'Mocked description', + }); + mkUrlPreview.getByText('Mocked description'); + }); + + test('Having a player should render a button', async () => { + const mkUrlPreview = await renderPreviewBy({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + const buttons = mkUrlPreview.getAllByRole('button'); + assert.strictEqual(buttons.length, 2, 'two buttons'); + }); + + test('Having a player should setup the iframe', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.src, 'https://example.local/player?autoplay=1&auto_play=1'); + assert.strictEqual( + iframe?.sandbox.toString(), + 'allow-popups allow-scripts allow-storage-access-by-user-activation allow-same-origin', + ); + }); + + test('Having a player with `allow` field should set permissions', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: null, + allow: ['fullscreen', 'web-share'], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.allow, 'fullscreen;web-share'); + }); + + test('Having a player width should keep the fixed aspect ratio', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: 400, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '50%'); + }); + + test('Having a player width should keep the fixed height', async () => { + const iframe = await renderAndOpenPreview({ + url: 'https://example.local', + player: { + url: 'https://example.local/player', + width: null, + height: 200, + allow: [], + }, + }); + assert.exists(iframe, 'iframe should exist'); + assert.strictEqual(iframe?.parentElement?.style.paddingTop, '200px'); + }); +}); |