diff options
| author | Hazelnoot <acomputerdog@gmail.com> | 2025-05-28 02:04:08 -0400 |
|---|---|---|
| committer | Hazelnoot <acomputerdog@gmail.com> | 2025-06-04 10:46:48 -0400 |
| commit | c18edd106b8f0fef560d354a08f971555410c44f (patch) | |
| tree | bc311dd29c68464b710583696bbed6e8b27e2c2b | |
| parent | add relations from abuse_user_report->user_profile to speed up admin/abuse-us... (diff) | |
| download | sharkey-c18edd106b8f0fef560d354a08f971555410c44f.tar.gz sharkey-c18edd106b8f0fef560d354a08f971555410c44f.tar.bz2 sharkey-c18edd106b8f0fef560d354a08f971555410c44f.zip | |
implement de-duplication for MkUrlPreview
| -rw-r--r-- | packages/frontend/src/components/MkUrlPreview.vue | 93 |
1 files changed, 87 insertions, 6 deletions
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue index 69a1540600..eb4a4efa33 100644 --- a/packages/frontend/src/components/MkUrlPreview.vue +++ b/packages/frontend/src/components/MkUrlPreview.vue @@ -4,7 +4,8 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<template v-if="player.url && playerEnabled"> +<div v-if="groupLockout" style="display: none"></div> +<template v-else-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`" @@ -76,7 +77,7 @@ SPDX-License-Identifier: AGPL-3.0-only </I18n> <p v-else-if="linkAttribution" :class="$style.linkAttribution"><MkEllipsis/></p> - <template v-if="showActions"> + <template v-if="showActions && !groupLockout"> <div v-if="tweetId" :class="$style.action"> <MkButton :small="true" inline @click="tweetExpanded = true"> <i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }} @@ -99,8 +100,53 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </template> +<script lang="ts"> +/** + * Links a group of previews to de-duplicate the results. + * Between all MkUrlPreview instances that share a group, each URL and Note are guaranteed to appear only once. + */ +export class PreviewGroup { + private readonly urls = new Map<string, number>(); + private readonly noteIds = new Map<string, number>(); + + public claimUrl(url: string, componentUid: number): boolean { + return this.claim(this.urls, url, componentUid); + } + + public claimNoteId(noteId: string, componentUid: number): boolean { + return this.claim(this.noteIds, noteId, componentUid); + } + + private claim(group: Map<string, number>, key: string, uid: number): boolean { + const claim = group.get(key); + + // Already claimed + if (claim != null && claim !== uid) { + return false; + } + + group.set(key, uid); + return true; + } + + public releaseUrl(url: string, componentUid: number): void { + this.release(this.urls, url, componentUid); + } + + public releaseNoteId(noteId: string, componentUid: number): void { + this.release(this.noteIds, noteId, componentUid); + } + + private release(group: Map<string, number>, key: string, uid: number): void { + if (group.get(key) === uid) { + group.delete(key); + } + } +} +</script> + <script lang="ts" setup> -import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue'; +import { defineAsyncComponent, onDeactivated, onUnmounted, ref, getCurrentInstance } from 'vue'; import { url as local } from '@@/js/config.js'; import { versatileLang } from '@@/js/intl-const.js'; import * as Misskey from 'misskey-js'; @@ -119,6 +165,11 @@ import DynamicNoteSimple from '@/components/DynamicNoteSimple.vue'; import { $i } from '@/i'; import { userPage } from '@/filters/user.js'; +const uid = getCurrentInstance()?.uid ?? -1; +if (uid === -1) { + console.warn('[MkUrlPreview] Component has null instance??'); +} + type SummalyResult = Awaited<ReturnType<typeof summaly>>; const props = withDefaults(defineProps<{ @@ -128,17 +179,24 @@ const props = withDefaults(defineProps<{ showAsQuote?: boolean; showActions?: boolean; skipNoteIds?: (string | undefined)[]; + group?: PreviewGroup; }>(), { detail: false, compact: false, showAsQuote: false, showActions: true, skipNoteIds: undefined, + group: undefined, }); +const emit = defineEmits<{ + (event: 'loaded', preview: SummalyResult & { haveNoteLocally?: boolean } | null, note: Misskey.entities.Note | null): void; +}>(); + const MOBILE_THRESHOLD = 500; const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD); +const groupLockout = ref<boolean>(false); const hidePreview = ref<boolean>(false); const maybeRelativeUrl = maybeMakeRelative(props.url, local); const self = maybeRelativeUrl !== props.url; @@ -170,6 +228,7 @@ const tweetHeight = ref(150); const unknownUrl = ref(false); const theNote = ref<Misskey.entities.Note | null>(null); const fetchingTheNote = ref(false); +const preview = ref<SummalyResult & { haveNoteLocally?: boolean } | null>(null); onDeactivated(() => { playerEnabled.value = false; @@ -190,6 +249,9 @@ async function fetchNote() { hidePreview.value = true; return; } + if (props.group && !props.group.claimNoteId(response['object'].id, uid)) { + groupLockout.value = true; + } theNote.value = response['object']; } catch (err) { if (_DEV_) { @@ -216,7 +278,16 @@ if (requestUrl.hostname === 'music.youtube.com' && requestUrl.pathname.match('^/ requestUrl.hash = ''; -function refresh(withFetch = false) { +const refresh = (withFetch = false) => { + // Release URL/noteID when refreshing, in case it changes. + // (Could happen since redirects are allowed.) + if (preview.value && props.group) { + props.group.releaseUrl(preview.value.url, uid); + } + if (theNote.value && props.group) { + props.group.releaseNoteId(theNote.value.id, uid); + } + const params = new URLSearchParams({ url: requestUrl.href, lang: versatileLang, @@ -243,6 +314,7 @@ function refresh(withFetch = false) { userId: string, } } | null) => { + preview.value = info; unknownUrl.value = info == null; title.value = info?.title ?? null; description.value = info?.description ?? null; @@ -257,6 +329,7 @@ function refresh(withFetch = false) { }; sensitive.value = info?.sensitive ?? false; activityPub.value = info?.activityPub ?? null; + groupLockout.value = info != null && props.group != null && !props.group.claimUrl(info.url, uid); linkAttribution.value = info?.linkAttribution ?? null; if (linkAttribution.value) { try { @@ -275,8 +348,9 @@ function refresh(withFetch = false) { }) .finally(() => { fetching.value = null; + emit('loaded', preview.value, theNote.value); }); -} +}; function adjustTweetHeight(message: MessageEvent) { if (message.origin !== 'https://platform.twitter.com') return; @@ -301,6 +375,13 @@ window.addEventListener('message', adjustTweetHeight); onUnmounted(() => { window.removeEventListener('message', adjustTweetHeight); + + if (preview.value && props.group) { + props.group.releaseUrl(preview.value.url, uid); + } + if (theNote.value && props.group) { + props.group.releaseNoteId(theNote.value.id, uid); + } }); // Load initial data @@ -388,7 +469,7 @@ refresh(); .body { position: relative; box-sizing: border-box; - padding: 16px; + padding: 16px !important; // Unfortunately needed to win a specificity race with MkNoteSimple / SkNoteSimple } .header { |