Process missing media dimensions

media-dim
Alex Gleason 2025-03-12 14:00:04 -05:00
rodzic 311eaddb29
commit ecdfa13053
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
11 zmienionych plików z 104 dodań i 39 usunięć

Wyświetl plik

@ -1,6 +1,6 @@
import { importEntities } from 'soapbox/entity-store/actions.ts';
import { Entities } from 'soapbox/entity-store/entities.ts';
import { Group, accountSchema, groupSchema } from 'soapbox/schemas/index.ts';
import { Group, accountSchema, groupSchema, statusSchema } from 'soapbox/schemas/index.ts';
import { filteredArray } from 'soapbox/schemas/utils.ts';
import { getSettings } from '../settings.ts';
@ -95,9 +95,12 @@ const importFetchedGroups = (groups: APIEntity[]) => {
};
const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
(dispatch: AppDispatch) => {
async (dispatch: AppDispatch) => {
const result = await statusSchema.safeParseAsync(status);
// Skip broken statuses
if (isBroken(status)) return;
if (!result.success) return;
status = result.data;
if (status.reblog?.id) {
dispatch(importFetchedStatus(status.reblog));
@ -135,31 +138,18 @@ const importFetchedStatus = (status: APIEntity, idempotencyKey?: string) =>
dispatch(importStatus(status, idempotencyKey));
};
// Sometimes Pleroma can return an empty account,
// or a repost can appear of a deleted account. Skip these statuses.
const isBroken = (status: APIEntity) => {
try {
// Skip empty accounts
// https://gitlab.com/soapbox-pub/soapbox/-/issues/424
if (!status.account.id) return true;
// Skip broken reposts
// https://gitlab.com/soapbox-pub/rebased/-/issues/28
if (status.reblog && !status.reblog.account.id) return true;
return false;
} catch (e) {
return true;
}
};
const importFetchedStatuses = (statuses: APIEntity[]) =>
(dispatch: AppDispatch, getState: () => RootState) => {
async (dispatch: AppDispatch, getState: () => RootState) => {
const accounts: APIEntity[] = [];
const normalStatuses: APIEntity[] = [];
const polls: APIEntity[] = [];
function processStatus(status: APIEntity) {
async function processStatus(status: APIEntity) {
const result = await statusSchema.safeParseAsync(status);
// Skip broken statuses
if (isBroken(status)) return;
if (!result.success) return;
status = result.data;
normalStatuses.push(status);
accounts.push(status.account);
@ -186,7 +176,7 @@ const importFetchedStatuses = (statuses: APIEntity[]) =>
}
}
statuses.forEach(processStatus);
await Promise.all((statuses.map(processStatus)));
dispatch(importPolls(polls));
dispatch(importFetchedAccounts(accounts));

Wyświetl plik

@ -173,7 +173,7 @@ const expandTimeline = (timelineId: string, path: string, params: Record<string,
const { next, prev } = response.pagination();
const data: APIEntity[] = await response.json();
dispatch(importFetchedStatuses(data));
await dispatch(importFetchedStatuses(data));
const statusesFromGroups = (data as Status[]).filter((status) => !!status.group);
dispatch(fetchGroupRelationships(statusesFromGroups.map((status: any) => status.group?.id)));

Wyświetl plik

@ -59,7 +59,7 @@ function useBookmark() {
try {
const response = await api.post(`/api/v1/statuses/${statusId}/bookmark`);
const result = statusSchema.parse(await response.json());
const result = await statusSchema.parseAsync(await response.json());
if (result) {
dispatch(importEntities([result], Entities.STATUSES, 'bookmarks', 'start'));
}

Wyświetl plik

@ -67,7 +67,7 @@ export function useReaction() {
try {
const response = await api.put(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`);
const result = statusSchema.parse(await response.json());
const result = await statusSchema.parseAsync(await response.json());
if (result) {
dispatch(importEntities([result], Entities.STATUSES));
}
@ -82,7 +82,7 @@ export function useReaction() {
try {
const response = await api.delete(`/api/v1/pleroma/statuses/${status.id}/reactions/${emoji}`);
const result = statusSchema.parse(await response.json());
const result = await statusSchema.parseAsync(await response.json());
if (result) {
dispatch(importEntities([result], Entities.STATUSES));
}

Wyświetl plik

@ -73,8 +73,9 @@ const StatusMedia: React.FC<IStatusMedia> = ({
blurhash={video.blurhash ?? undefined}
src={video.url}
alt={video.description}
aspectRatio={Number(video.meta?.original?.aspect)}
height={285}
aspectRatio={video.meta?.original?.aspect}
height={video?.meta?.original?.height}
width={video?.meta?.original?.width}
visible={showMedia}
inline
/>

Wyświetl plik

@ -6,7 +6,7 @@ import { useApi } from 'soapbox/hooks/useApi.ts';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { useGetState } from 'soapbox/hooks/useGetState.ts';
import { filteredArray } from 'soapbox/schemas/utils.ts';
import { filteredArrayAsync } from 'soapbox/schemas/utils.ts';
import { realNumberSchema } from 'soapbox/utils/numbers.tsx';
import { entitiesFetchFail, entitiesFetchRequest, entitiesFetchSuccess, invalidateEntityList } from '../actions.ts';
@ -58,7 +58,7 @@ function useEntities<TEntity extends Entity>(
const next = useListState(path, 'next');
const prev = useListState(path, 'prev');
const fetchPage = async(req: EntityFn<void>, pos: 'start' | 'end', overwrite = false): Promise<void> => {
const fetchPage = async (req: EntityFn<void>, pos: 'start' | 'end', overwrite = false): Promise<void> => {
// Get `isFetching` state from the store again to prevent race conditions.
const isFetching = selectListState(getState(), path, 'fetching');
if (isFetching) return;
@ -67,7 +67,7 @@ function useEntities<TEntity extends Entity>(
try {
const response = await req();
const json = await response.json();
const entities = filteredArray(schema).parse(json);
const entities = await filteredArrayAsync(schema).parseAsync(json);
const parsedCount = realNumberSchema.safeParse(response.headers.get('x-total-count'));
const totalCount = parsedCount.success ? parsedCount.data : undefined;
const linkHeader = response.headers.get('link');

Wyświetl plik

@ -47,7 +47,7 @@ function useEntity<TEntity extends Entity>(
try {
const response = await setPromise(entityFn());
const json = await response.json();
const entity = schema.parse(json);
const entity = await schema.parseAsync(json);
dispatch(importEntities([entity], entityType));
} catch (e) {
setError(e);

Wyświetl plik

@ -83,14 +83,75 @@ const attachmentSchema = z.discriminatedUnion('type', [
gifvAttachmentSchema,
audioAttachmentSchema,
unknownAttachmentSchema,
]).transform((attachment) => {
]).transform(async (attachment) => {
if (!attachment.preview_url) {
attachment.preview_url = attachment.url;
}
if (attachment.type === 'image') {
if (!attachment.meta.original) {
try {
const { width, height } = await getImageDimensions(attachment.url);
attachment.meta.original = { width, height, aspect: width / height };
} catch {
// Image metadata is not available
}
}
}
if (attachment.type === 'video') {
if (!attachment.meta.original) {
try {
const { width, height } = await getVideoDimensions(attachment.url);
attachment.meta.original = { width, height, aspect: width / height };
} catch {
// Video metadata is not available
}
}
}
return attachment;
});
async function getImageDimensions(url: string): Promise<{ width: number; height: number }> {
const response = await fetch(url);
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
resolve({ width: img.width, height: img.height });
URL.revokeObjectURL(img.src); // Cleanup
};
img.onerror = reject;
img.src = URL.createObjectURL(blob);
});
}
async function getVideoDimensions(url: string): Promise<{ width: number; height: number }> {
const response = await fetch(url);
const blob = await response.blob();
return await new Promise((resolve, reject) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
video.currentTime = 0.1; // Force processing of video frames
};
video.onseeked = () => {
resolve({ width: video.videoWidth, height: video.videoHeight });
URL.revokeObjectURL(video.src);
};
video.onerror = reject;
video.src = URL.createObjectURL(blob);
});
}
type Attachment = z.infer<typeof attachmentSchema>;
export { attachmentSchema, type Attachment };

Wyświetl plik

@ -13,7 +13,7 @@ import { groupSchema } from './group.ts';
import { mentionSchema } from './mention.ts';
import { pollSchema } from './poll.ts';
import { tagSchema } from './tag.ts';
import { contentSchema, dateSchema, filteredArray } from './utils.ts';
import { contentSchema, dateSchema, filteredArray, filteredArrayAsync } from './utils.ts';
import type { Resolve } from 'soapbox/utils/types.ts';
@ -48,7 +48,7 @@ const baseStatusSchema = z.object({
in_reply_to_id: z.string().nullable().catch(null),
id: z.string(),
language: z.string().nullable().catch(null),
media_attachments: filteredArray(attachmentSchema),
media_attachments: filteredArrayAsync(attachmentSchema),
mentions: filteredArray(mentionSchema),
muted: z.coerce.boolean(),
pinned: z.coerce.boolean(),

Wyświetl plik

@ -17,6 +17,19 @@ function filteredArray<T extends z.ZodTypeAny>(schema: T) {
));
}
/** Validates individual items in an async array schema, dropping any that aren't valid. */
function filteredArrayAsync<T extends z.ZodTypeAny>(schema: T) {
return z.any().array().catch([])
.transform(async (arr) => {
const results = await Promise.all(arr.map(async (item) => {
const parsed = await schema.safeParseAsync(item);
return parsed.success ? parsed.data : undefined;
}));
return results.filter((item): item is z.infer<T> => Boolean(item));
});
}
/** Validates the string as an emoji. */
const emojiSchema = z.string().refine((v) => /\p{Extended_Pictographic}|[\u{1F1E6}-\u{1F1FF}]{2}/u.test(v));
@ -42,4 +55,4 @@ function coerceObject<T extends z.ZodRawShape>(shape: T) {
/** Validates a hex color code. */
const hexColorSchema = z.string().regex(/^#([a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/i);
export { filteredArray, hexColorSchema, emojiSchema, contentSchema, dateSchema, jsonSchema, mimeSchema, coerceObject };
export { filteredArray, filteredArrayAsync, hexColorSchema, emojiSchema, contentSchema, dateSchema, jsonSchema, mimeSchema, coerceObject };

Wyświetl plik

@ -131,13 +131,13 @@ function buildCSP(env: string | undefined): string {
csp.push(
"connect-src 'self' blob: https: wss: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*",
"img-src 'self' data: blob: https: http://localhost:* http://127.0.0.1:*",
"media-src 'self' https: http://localhost:* http://127.0.0.1:*",
"media-src 'self' blob: https: http://localhost:* http://127.0.0.1:*",
);
} else {
csp.push(
"connect-src 'self' blob: https: wss:",
"img-src 'self' data: blob: https:",
"media-src 'self' https:",
"media-src 'self' blob: https:",
);
}