summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components/MkUrlPreview.vue
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/components/MkUrlPreview.vue')
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue165
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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
+ :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${store.s.darkMode ? 'dark' : 'light'}&amp;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;