Files
Mastodon/app/javascript/mastodon/actions/importer/statuses.ts
T
2026-06-26 12:26:18 +00:00

188 lines
5.0 KiB
TypeScript

import escapeTextContentForBrowser from 'escape-html';
import type { ApiMediaAttachmentJSON } from '@/mastodon/api_types/media_attachments';
import type {
ApiFilterResultJSON,
ApiStatusJSON,
} from '@/mastodon/api_types/statuses';
import type {
FilterResult,
MediaAttachmentShape,
StatusShape,
} from '@/mastodon/models/status';
const domParser = new DOMParser();
export function normalizeStatus(
status: ApiStatusJSON,
normalOldStatus?: StatusShape,
{ bogusQuotePolicy = false, expandSpoilers = false } = {},
) {
const normalStatus: StatusShape = {
hidden: normalOldStatus?.hidden ?? false,
collapsed: normalOldStatus?.collapsed ?? null,
content: '',
contentHtml: '',
muted: false,
pinned: false,
bookmarked: false,
favourited: false,
quote_approval: null,
reblogged: false,
...status,
account: status.account.id,
media_attachments: [],
poll: status.poll?.id,
reblog: status.reblog?.id,
quote: undefined,
filtered: [],
application: {
name: 'Web',
...status.application,
},
};
if (bogusQuotePolicy) {
normalStatus.quote_approval = null;
}
if (status.quote?.quoted_status) {
normalStatus.quote = {
...status.quote,
quoted_status: status.quote.quoted_status.id,
};
}
if (status.card) {
normalStatus.card = {
...status.card,
authors: status.card.authors.map((author) => ({
name: author.name,
url: author.url,
accountId: author.account?.id,
})),
};
}
if (status.filtered) {
normalStatus.filtered = status.filtered.map(normalizeFilterResult);
}
// Only calculate these values when status first encountered and
// when the underlying values change. Otherwise keep the ones
// already in the reducer
if (
normalOldStatus?.content === normalStatus.content &&
normalOldStatus.spoiler_text === normalStatus.spoiler_text
) {
normalStatus.search_index = normalOldStatus.search_index;
normalStatus.contentHtml = normalOldStatus.contentHtml;
normalStatus.spoilerHtml = normalOldStatus.spoilerHtml;
normalStatus.spoiler_text = normalOldStatus.spoiler_text;
normalStatus.hidden = normalOldStatus.hidden;
if (normalOldStatus.translation) {
normalStatus.translation = normalOldStatus.translation;
}
} else {
// If the status has a CW but no contents, treat the CW as if it were the
// status' contents, to avoid having a CW toggle with seemingly no effect.
if (
normalStatus.spoiler_text &&
!normalStatus.content &&
!normalStatus.quote
) {
normalStatus.content = normalStatus.spoiler_text;
normalStatus.spoiler_text = '';
}
const spoilerText = normalStatus.spoiler_text ?? '';
const searchContent = [spoilerText, status.content]
.concat(
status.poll?.options
? status.poll.options.map((option) => option.title)
: [],
)
.concat(status.media_attachments.map((att) => att.description))
.join('\n\n')
.replace(/<br\s*\/?>/g, '\n')
.replace(/<\/p><p>/g, '\n\n');
normalStatus.search_index = domParser.parseFromString(
searchContent,
'text/html',
).documentElement.textContent;
normalStatus.contentHtml = normalStatus.content;
normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText);
normalStatus.hidden = expandSpoilers
? false
: spoilerText.length > 0 || normalStatus.sensitive;
// Remove quote fallback link from the DOM so it doesn't mess with paragraph margins
if (normalStatus.quote && normalStatus.contentHtml) {
normalStatus.contentHtml = stripQuoteFallback(normalStatus.contentHtml);
}
if (
normalStatus.url &&
!(
normalStatus.url.startsWith('http://') ||
normalStatus.url.startsWith('https://')
)
) {
normalStatus.url = null;
}
normalStatus.url ??= normalStatus.uri;
normalStatus.media_attachments = status.media_attachments.map(
(attachment) =>
normalizeMediaAttachment(
attachment,
normalOldStatus?.media_attachments,
),
);
}
if (normalOldStatus) {
normalStatus.quote_approval ??= normalOldStatus.quote_approval;
}
return normalStatus;
}
function normalizeMediaAttachment(
attachment: ApiMediaAttachmentJSON,
oldAttachments?: MediaAttachmentShape[],
): MediaAttachmentShape {
const { remote_url } = attachment;
return {
...attachment,
remote_url:
remote_url && /^https?:\/\//.test(remote_url) ? remote_url : null,
translation: oldAttachments?.find(
(oldAttachment) =>
oldAttachment.id === attachment.id &&
oldAttachment.description === attachment.description,
)?.translation,
};
}
function normalizeFilterResult(input: ApiFilterResultJSON): FilterResult {
return {
...input,
filter: input.filter.id,
};
}
function stripQuoteFallback(text: string) {
const wrapper = document.createElement('div');
wrapper.innerHTML = text;
wrapper.querySelector('.quote-inline')?.remove();
return wrapper.innerHTML;
}