summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHazelnoot <acomputerdog@gmail.com>2025-06-04 11:15:42 -0400
committerHazelnoot <acomputerdog@gmail.com>2025-06-04 11:15:42 -0400
commitbae4c07bb3ff2714286719ea47d4fb3601411b74 (patch)
tree5a7793ade5b96eed78f4e68b2864da29804686d6
parentuse correct code style for dynamic classes in PageWithHeader.vue (diff)
downloadsharkey-bae4c07bb3ff2714286719ea47d4fb3601411b74.tar.gz
sharkey-bae4c07bb3ff2714286719ea47d4fb3601411b74.tar.bz2
sharkey-bae4c07bb3ff2714286719ea47d4fb3601411b74.zip
support link attributions in SkUrlPreviewGroup
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue64
-rw-r--r--packages/frontend/src/components/SkUrlPreviewGroup.vue58
2 files changed, 92 insertions, 30 deletions
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 680a9a7141..5d0e6e3df7 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -99,13 +99,24 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
+<script lang="ts">
+// eslint-disable-next-line import/order
+import type { summaly } from '@misskey-dev/summaly';
+
+export type SummalyResult = Awaited<ReturnType<typeof summaly>> & {
+ haveNoteLocally?: boolean,
+ linkAttribution?: {
+ userId: string,
+ }
+};
+</script>
+
<script lang="ts" setup>
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 { 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 '@/utility/device-kind.js';
@@ -119,13 +130,6 @@ import DynamicNoteSimple from '@/components/DynamicNoteSimple.vue';
import { $i } from '@/i';
import { userPage } from '@/filters/user.js';
-type SummalyResult = Awaited<ReturnType<typeof summaly>> & {
- haveNoteLocally?: boolean,
- linkAttribution?: {
- userId: string,
- }
-};
-
const props = withDefaults(defineProps<{
url: string;
detail?: boolean;
@@ -135,6 +139,7 @@ const props = withDefaults(defineProps<{
skipNoteIds?: (string | undefined)[];
previewHint?: SummalyResult;
noteHint?: Misskey.entities.Note | null;
+ attributionHint?: Misskey.entities.User | null;
}>(), {
detail: false,
compact: false,
@@ -143,6 +148,7 @@ const props = withDefaults(defineProps<{
skipNoteIds: undefined,
previewHint: undefined,
noteHint: undefined,
+ attributionHint: undefined,
});
const MOBILE_THRESHOLD = 500;
@@ -179,11 +185,34 @@ const tweetHeight = ref(150);
const unknownUrl = ref(false);
const theNote = ref<Misskey.entities.Note | null>(null);
const fetchingTheNote = ref(false);
+const fetchingAttribution = ref<Promise<void> | null>(null);
onDeactivated(() => {
playerEnabled.value = false;
});
+async function fetchAttribution(initial: boolean): Promise<void> {
+ if (!linkAttribution.value) return;
+ if (attributionUser.value) return;
+ if (fetchingAttribution.value) return fetchingAttribution.value;
+
+ return fetchingAttribution.value ??= (async (userId: string): Promise<void> => {
+ try {
+ if (initial && props.attributionHint !== undefined) {
+ attributionUser.value = props.attributionHint;
+ } else {
+ attributionUser.value = await misskeyApi('users/show', { userId });
+ }
+ } catch {
+ // makes the loading ellipsis vanish.
+ linkAttribution.value = null;
+ } finally {
+ // Reset promise to mark as done
+ fetchingAttribution.value = null;
+ }
+ })(linkAttribution.value.userId);
+}
+
async function fetchNote(initial: boolean) {
if (!props.showAsQuote) return;
if (!activityPub.value) return;
@@ -275,20 +304,15 @@ function refresh(withFetch = false, initial = false) {
sensitive.value = info?.sensitive ?? false;
activityPub.value = info?.activityPub ?? null;
linkAttribution.value = info?.linkAttribution ?? null;
- if (linkAttribution.value) {
- try {
- const response = await misskeyApi('users/show', { userId: linkAttribution.value.userId });
- attributionUser.value = response;
- } catch {
- // makes the loading ellipsis vanish.
- linkAttribution.value = null;
- }
- }
+ // These will be populated by the fetch* functions
+ attributionUser.value = null;
theNote.value = null;
- if (info?.haveNoteLocally) {
- await fetchNote(initial);
- }
+
+ await Promise.all([
+ fetchAttribution(initial),
+ fetchNote(initial),
+ ]);
})
.finally(() => {
fetching.value = null;
diff --git a/packages/frontend/src/components/SkUrlPreviewGroup.vue b/packages/frontend/src/components/SkUrlPreviewGroup.vue
index 5175e16cfc..32b11d9db4 100644
--- a/packages/frontend/src/components/SkUrlPreviewGroup.vue
+++ b/packages/frontend/src/components/SkUrlPreviewGroup.vue
@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:url="preview.url"
:previewHint="preview"
:noteHint="preview.note"
+ :attributionHint="preview.attributionUser"
:detail="detail"
:compact="compact"
:showAsQuote="showAsQuote"
@@ -29,7 +30,7 @@ import * as mfm from '@transfem-org/sfm-js';
import { computed, ref, watch } from 'vue';
import { versatileLang } from '@@/js/intl-const';
import promiseLimit from 'promise-limit';
-import type { summaly } from '@misskey-dev/summaly';
+import type { SummalyResult } from '@/components/MkUrlPreview.vue';
import { extractPreviewUrls } from '@/utility/extract-preview-urls';
import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm';
import { $i } from '@/i';
@@ -37,11 +38,13 @@ import { misskeyApi } from '@/utility/misskey-api';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { getNoteUrls } from '@/utility/getNoteUrls';
-type Summary = Awaited<ReturnType<typeof summaly>> & {
- haveNoteLocally?: boolean;
+type Summary = SummalyResult & {
note?: Misskey.entities.Note | null;
+ attributionUser?: Misskey.entities.User | null;
};
+type Limiter<T> = ReturnType<typeof promiseLimit<T>>;
+
const props = withDefaults(defineProps<{
sourceUrls?: string[];
sourceNodes?: mfm.MfmNode[];
@@ -90,9 +93,11 @@ const urls = computed<string[]>(() => {
return [];
});
+// todo un-ref these
const isRefreshing = ref<Promise<void> | false>(false);
const cachedNotes = ref(new Map<string, Misskey.entities.Note | null>());
const cachedPreviews = ref(new Map<string, Summary | null>());
+const cachedUsers = new Map<string, Misskey.entities.User | null>();
/**
* Refreshes the group.
@@ -124,6 +129,7 @@ async function doRefresh(): Promise<void> {
}
async function fetchPreviews(): Promise<Summary[]> {
+ const userLimiter = promiseLimit<Misskey.entities.User | null>(4);
const noteLimiter = promiseLimit<Misskey.entities.Note | null>(2);
const summaryLimiter = promiseLimit<Summary | null>(5);
@@ -131,13 +137,11 @@ async function fetchPreviews(): Promise<Summary[]> {
summaryLimiter(async () => {
return await fetchPreview(url);
}).then(async (summary) => {
- if (summary && props.showAsQuote && summary.activityPub && summary.haveNoteLocally) {
- // Have to pull this out to make TS happy
- const noteUri = summary.activityPub;
-
- summary.note = await noteLimiter(async () => {
- return await fetchNote(noteUri);
- });
+ if (summary) {
+ await Promise.all([
+ attachNote(summary, noteLimiter),
+ attachAttribution(summary, userLimiter),
+ ]);
}
return summary;
@@ -171,6 +175,17 @@ async function fetchPreview(url: string): Promise<Summary | null> {
return null;
}
+async function attachNote(summary: Summary, noteLimiter: Limiter<Misskey.entities.Note | null>): Promise<void> {
+ if (props.showAsQuote && summary.activityPub && summary.haveNoteLocally) {
+ // Have to pull this out to make TS happy
+ const noteUri = summary.activityPub;
+
+ summary.note = await noteLimiter(async () => {
+ return await fetchNote(noteUri);
+ });
+ }
+}
+
async function fetchNote(noteUri: string): Promise<Misskey.entities.Note | null> {
const cached = cachedNotes.value.get(noteUri);
if (cached) {
@@ -194,6 +209,29 @@ async function fetchNote(noteUri: string): Promise<Misskey.entities.Note | null>
return null;
}
+async function attachAttribution(summary: Summary, userLimiter: Limiter<Misskey.entities.User | null>): Promise<void> {
+ if (summary.linkAttribution) {
+ // Have to pull this out to make TS happy
+ const userId = summary.linkAttribution.userId;
+
+ summary.attributionUser = await userLimiter(async () => {
+ return await fetchUser(userId);
+ });
+ }
+}
+
+async function fetchUser(userId: string): Promise<Misskey.entities.User | null> {
+ const cached = cachedUsers.get(userId);
+ if (cached) {
+ return cached;
+ }
+
+ const user = await misskeyApi('users/show', { userId }).catch(() => null);
+
+ cachedUsers.set(userId, user);
+ return user;
+}
+
function deduplicatePreviews(previews: Summary[]): Summary[] {
// eslint-disable-next-line no-param-reassign
previews = previews