summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authorKagami Sascha Rosylight <saschanaz@outlook.com>2023-03-19 08:59:31 +0100
committerGitHub <noreply@github.com>2023-03-19 16:59:31 +0900
commitc091d9e6d558ceb743fb8b647b28dbce8f994d41 (patch)
tree7b6eb79f10e0ffc9843d17e46aa556e3f0c0146a /packages/frontend
parentperf(backend): improve performance of timeline apis (diff)
downloadmisskey-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.json2
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue32
-rw-r--r--packages/frontend/test/init.ts4
-rw-r--r--packages/frontend/test/url-preview.test.ts140
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');
+ });
+});