Merge branch 'develop' into misskey-js

This commit is contained in:
Kagami Sascha Rosylight 2023-03-19 10:26:30 +01:00 committed by GitHub
commit 9044fa5d1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1127 additions and 357 deletions

View file

@ -15,7 +15,7 @@
"@rollup/plugin-alias": "4.0.3",
"@rollup/plugin-json": "6.0.0",
"@rollup/pluginutils": "5.0.2",
"@syuilo/aiscript": "0.13.0",
"@syuilo/aiscript": "0.13.1",
"@tabler/icons-webfont": "2.10.0",
"@vitejs/plugin-vue": "4.0.0",
"@vue/compiler-sfc": "3.2.47",
@ -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"
}

View file

@ -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;

View file

@ -19,7 +19,7 @@
<div style="text-align: center;">
{{ i18n.ts._aboutMisskey.about }}<br><a href="https://misskey-hub.net/docs/misskey.html" target="_blank" class="_link">{{ i18n.ts.learnMore }}</a>
</div>
<div style="text-align: center;">
<div v-if="$i != null" style="text-align: center;">
<MkButton primary rounded inline @click="iLoveMisskey">I <Mfm text="$[jelly ❤]"/> #Misskey</MkButton>
</div>
<FormSection>

View file

@ -0,0 +1,25 @@
<template>
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :content-max="500">
<div class="_gaps">
<MkAd v-for="ad in instance.ads" :key="ad.id" :specify="ad"/>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue';
import * as os from '@/os';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
definePageMetadata({
title: i18n.ts.ads,
icon: 'ti ti-ad',
});
</script>

View file

@ -34,6 +34,17 @@ function menu(ev) {
copyToClipboard(`:${props.emoji.name}:`);
os.success();
},
}, {
text: i18n.ts.info,
icon: 'ti ti-info-circle',
action: () => {
os.apiGet('emoji', { name: props.emoji.name }).then(res => {
os.alert({
type: 'info',
text: `License: ${res.license}`,
});
});
},
}], ev.currentTarget ?? ev.target);
}
</script>

View file

@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkInput from '@/components/MkInput.vue';
import { useRouter } from '@/router';
const PRESET_DEFAULT = `/// @ 0.13.0
const PRESET_DEFAULT = `/// @ 0.13.1
var name = ""
@ -51,7 +51,7 @@ Ui:render([
])
`;
const PRESET_OMIKUJI = `/// @ 0.13.0
const PRESET_OMIKUJI = `/// @ 0.13.1
//
//
@ -94,7 +94,7 @@ Ui:render([
])
`;
const PRESET_SHUFFLE = `/// @ 0.13.0
const PRESET_SHUFFLE = `/// @ 0.13.1
//
let string = "ペペロンチーノ"
@ -173,7 +173,7 @@ var cursor = 0
do()
`;
const PRESET_QUIZ = `/// @ 0.13.0
const PRESET_QUIZ = `/// @ 0.13.1
let title = '地理クイズ'
let qas = [{
@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({
Ui:render(qaEls)
`;
const PRESET_TIMELINE = `/// @ 0.13.0
const PRESET_TIMELINE = `/// @ 0.13.1
// API
@fetch() {

View file

@ -197,6 +197,9 @@ export const routes = [{
}, {
path: '/about-misskey',
component: page(() => import('./pages/about-misskey.vue')),
}, {
path: '/ads',
component: page(() => import('./pages/ads.vue')),
}, {
path: '/theme-editor',
component: page(() => import('./pages/theme-editor.vue')),

View file

@ -29,6 +29,11 @@ export function openInstanceMenu(ev: MouseEvent) {
icon: 'ti ti-chart-line',
to: '/about#charts',
}, null, {
type: 'link',
text: i18n.ts.ads,
icon: 'ti ti-ad',
to: '/ads',
}, {
type: 'parent',
text: i18n.ts.tools,
icon: 'ti ti-tool',

View file

@ -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';

View file

@ -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');
});
});