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(/
/g, '\n')
.replace(/<\/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; }