diff options
Diffstat (limited to 'packages/frontend/src/components/MkUrlPreview.vue')
| -rw-r--r-- | packages/frontend/src/components/MkUrlPreview.vue | 165 |
1 files changed, 98 insertions, 67 deletions
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 2f1933a87b..a14c2ecef9 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <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" + sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-storage-access-by-user-activation allow-same-origin" scrolling="no" :allow="player.allow == null ? 'autoplay;encrypted-media;fullscreen' : player.allow.filter(x => ['autoplay', 'clipboard-write', 'fullscreen', 'encrypted-media', 'picture-in-picture', 'web-share'].includes(x)).join(';')" :class="$style.playerIframe" @@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin" scrolling="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }" - :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`" + :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${store.s.darkMode ? 'dark' : 'light'}&id=${tweetId}`" ></iframe> </div> <div :class="$style.action"> @@ -43,10 +43,10 @@ SPDX-License-Identifier: AGPL-3.0-only </MkButton> </div> </template> -<div v-else-if="theNote" :class="[$style.link, { [$style.compact]: compact }]"><XNoteSimple :note="theNote" :class="$style.body"/></div> +<div v-else-if="theNote" :class="[$style.link, { [$style.compact]: compact }]"><DynamicNoteSimple :note="theNote" :class="$style.body"/></div> <div v-else-if="!hidePreview"> - <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="maybeRelativeUrl" rel="nofollow noopener" :target="target" :title="url"> - <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : { backgroundImage: `url('${thumbnail}')` }"> + <component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="maybeRelativeUrl" rel="nofollow noopener" :target="target" :title="url" @click.prevent="self ? true : warningExternalWebsite(url)" @click.stop> + <div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : { backgroundImage: `url('${thumbnail}')` }"> </div> <article :class="$style.body"> <header :class="$style.header"> @@ -71,6 +71,11 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }} </MkButton> </div> + <div v-if="showAsQuote && activityPub && !theNote && $i" :class="$style.action"> + <MkButton :small="true" :disabled="!!fetching || fetchingTheNote" inline @click="() => refresh(true)"> + <i class="ti ti-note"></i> {{ i18n.ts.fetchLinkedNote }} + </MkButton> + </div> <div v-if="!playerEnabled && player.url" :class="$style.action"> <MkButton :small="true" inline @click="playerEnabled = true"> <i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }} @@ -84,27 +89,23 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, onDeactivated, onUnmounted, ref, watch } from 'vue'; +import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; import { url as local } from '@@/js/config.js'; import { versatileLang } from '@@/js/intl-const.js'; import * as Misskey from 'misskey-js'; -import type { summaly } from '@transfem-org/summaly'; -import type MkNoteSimple from '@/components/MkNoteSimple.vue'; -import type SkNoteSimple from '@/components/SkNoteSimple.vue'; +import { maybeMakeRelative } from '@@/js/url.js'; +import type { summaly } from '@misskey-dev/summaly'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; -import { deviceKind } from '@/scripts/device-kind.js'; +import { deviceKind } from '@/utility/device-kind.js'; import MkButton from '@/components/MkButton.vue'; -import { transformPlayerUrl } from '@/scripts/player-url-transform.js'; -import { defaultStore } from '@/store.js'; -import { misskeyApi } from '@/scripts/misskey-api.js'; -import { maybeMakeRelative } from '@@/js/url.js'; - -const XNoteSimple = defineAsyncComponent<typeof MkNoteSimple | typeof SkNoteSimple>(() => - defaultStore.state.noteDesign === 'misskey' - ? import('@/components/MkNoteSimple.vue') - : import('@/components/SkNoteSimple.vue'), -); +import { transformPlayerUrl } from '@/utility/player-url-transform.js'; +import { store } from '@/store.js'; +import { prefer } from '@/preferences.js'; +import { misskeyApi } from '@/utility/misskey-api.js'; +import { warningExternalWebsite } from '@/utility/warning-external-website.js'; +import DynamicNoteSimple from '@/components/DynamicNoteSimple.vue'; +import { $i } from '@/i'; type SummalyResult = Awaited<ReturnType<typeof summaly>>; @@ -131,7 +132,7 @@ const maybeRelativeUrl = maybeMakeRelative(props.url, local); const self = maybeRelativeUrl !== props.url; const attr = self ? 'to' : 'href'; const target = self ? null : '_blank'; -const fetching = ref(true); +const fetching = ref<Promise<void> | null>(null); const title = ref<string | null>(null); const description = ref<string | null>(null); const thumbnail = ref<string | null>(null); @@ -139,11 +140,12 @@ const icon = ref<string | null>(null); const sitename = ref<string | null>(null); const sensitive = ref<boolean>(false); const activityPub = ref<string | null>(null); -const player = ref({ +const player = ref<SummalyResult['player']>({ url: null, width: null, height: null, -} as SummalyResult['player']); + allow: [], +}); const playerEnabled = ref(false); const tweetId = ref<string | null>(null); const tweetExpanded = ref(props.detail); @@ -151,29 +153,38 @@ const embedId = `embed${Math.random().toString().replace(/\D/, '')}`; const tweetHeight = ref(150); const unknownUrl = ref(false); const theNote = ref<Misskey.entities.Note | null>(null); +const fetchingTheNote = ref(false); onDeactivated(() => { playerEnabled.value = false; }); -watch(activityPub, async (uri) => { - if (!props.showAsQuote) return; - if (!uri) return; - try { - const response = await misskeyApi('ap/show', { uri }); - if (response.type !== 'Note') return; - const theNoteId = response['object'].id; - if (theNoteId && props.skipNoteIds && props.skipNoteIds.includes(theNoteId)) { - hidePreview.value = true; - return; - } - theNote.value = response['object']; - } catch (err) { - if (_DEV_) { - console.error(`failed to extract note for preview of ${uri}`, err); - } +async function fetchNote() { + if (!props.showAsQuote) return; + if (!activityPub.value) return; + if (theNote.value) return; + if (fetchingTheNote.value) return; + + fetchingTheNote.value = true; + try { + const response = await misskeyApi('ap/show', { uri: activityPub.value }); + if (response.type !== 'Note') return; + const theNoteId = response['object'].id; + if (theNoteId && props.skipNoteIds && props.skipNoteIds.includes(theNoteId)) { + hidePreview.value = true; + return; } -}); + theNote.value = response['object']; + } catch (err) { + if (_DEV_) { + console.error(`failed to extract note for preview of ${activityPub.value}`, err); + } + activityPub.value = null; + theNote.value = null; + } finally { + fetchingTheNote.value = false; + } +} const requestUrl = new URL(props.url); if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url'); @@ -189,36 +200,52 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; -window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLang}`) - .then(res => { - if (!res.ok) { - if (_DEV_) { - console.warn(`[HTTP${res.status}] Failed to fetch url preview`); - } - return null; - } +function refresh(withFetch = false) { + const params = new URLSearchParams({ + url: requestUrl.href, + lang: versatileLang, + }); + if (withFetch) { + params.set('fetch', 'true'); + } - return res.json(); - }) - .then((info: SummalyResult | null) => { - if (!info || info.url == null) { - fetching.value = false; - unknownUrl.value = true; - return; - } + const headers = $i ? { Authorization: `Bearer ${$i.token}` } : undefined; + return fetching.value ??= window.fetch(`/url?${params.toString()}`, { headers }) + .then(res => { + if (!res.ok) { + if (_DEV_) { + console.warn(`[HTTP${res.status}] Failed to fetch url preview`); + } + return null; + } - fetching.value = false; - unknownUrl.value = false; + return res.json(); + }) + .then(async (info: SummalyResult & { haveNoteLocally?: boolean } | null) => { + unknownUrl.value = info == null; + title.value = info?.title ?? null; + description.value = info?.description ?? null; + thumbnail.value = info?.thumbnail ?? null; + icon.value = info?.icon ?? null; + sitename.value = info?.sitename ?? null; + player.value = info?.player ?? { + url: null, + width: null, + height: null, + allow: [], + }; + sensitive.value = info?.sensitive ?? false; + activityPub.value = info?.activityPub ?? null; - title.value = info.title; - description.value = info.description; - thumbnail.value = info.thumbnail; - icon.value = info.icon; - sitename.value = info.sitename; - player.value = info.player; - sensitive.value = info.sensitive ?? false; - activityPub.value = info.activityPub; - }); + theNote.value = null; + if (info?.haveNoteLocally) { + await fetchNote(); + } + }) + .finally(() => { + fetching.value = null; + }); +} function adjustTweetHeight(message: MessageEvent) { if (message.origin !== 'https://platform.twitter.com') return; @@ -244,6 +271,9 @@ window.addEventListener('message', adjustTweetHeight); onUnmounted(() => { window.removeEventListener('message', adjustTweetHeight); }); + +// Load initial data +refresh(); </script> <style lang="scss" module> @@ -285,6 +315,7 @@ onUnmounted(() => { box-shadow: 0 0 0 1px var(--MI_THEME-divider); border-radius: var(--MI-radius-sm); overflow: clip; + text-align: left; &:hover { text-decoration: none; |