pl-hooks: Move back to separate directory

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-10-12 13:07:03 +02:00
parent 2eab0b68b7
commit dc5fa13e64
12 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,40 @@
import { useQuery } from '@tanstack/react-query';
import { useRelationship } from 'pl-fe/api/hooks/accounts/useRelationship';
import { useClient } from 'pl-fe/hooks';
import { normalizeAccount } from 'pl-fe/pl-hooks/normalizers/normalizeAccount';
import { queryClient } from 'pl-fe/queries/client';
interface UseAccountOpts {
withRelationship?: boolean;
withScrobble?: boolean;
withMoveTarget?: boolean;
}
const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => {
const client = useClient();
const accountQuery = useQuery({
queryKey: ['accounts', 'entities', accountId],
queryFn: () => client.accounts.getAccount(accountId!)
.then(normalizeAccount),
enabled: !!accountId,
});
const relationshipQuery = useRelationship(accountId, {
enabled: opts.withRelationship,
});
let data;
if (accountQuery.data) {
data = {
...accountQuery.data,
relationship: relationshipQuery.relationship,
moved: opts.withMoveTarget && queryClient.getQueryData(['accounts', 'entities', accountQuery.data?.moved_id]) as MinifiedAccount || null,
};
} else data = null;
return { ...accountQuery, data };
};
export { useAccount };

View File

@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks';
import { queryClient } from 'pl-fe/queries/client';
import type { PlApiClient } from 'pl-api';
type Timeline = 'home' | 'notifications';
const useMarker = (timeline: Timeline) => {
const client = useClient();
return useQuery({
queryKey: ['markers', timeline],
queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]),
});
};
const prefetchMarker = (client: PlApiClient, timeline: 'home' | 'notifications') =>
queryClient.prefetchQuery({
queryKey: ['markers', timeline],
queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]),
});
export { useMarker, prefetchMarker, type Timeline };

View File

@ -0,0 +1,26 @@
import { useMutation } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks';
import { queryClient } from 'pl-fe/queries/client';
import type { Timeline } from './useMarkers';
import type { Marker } from 'pl-api';
const useUpdateMarkerMutation = (timeline: Timeline) => {
const client = useClient();
return useMutation({
mutationFn: (lastReadId: string) => client.timelines.saveMarkers({
[timeline]: {
last_read_id: lastReadId,
},
}),
retry: false,
onMutate: (lastReadId) => queryClient.setQueryData<Marker>(['markers', timeline], (marker) => marker ? ({
...marker,
last_read_id: lastReadId,
}) : undefined),
});
};
export { useUpdateMarkerMutation };

View File

@ -0,0 +1,51 @@
import { useQuery } from '@tanstack/react-query';
import { useAppSelector, useClient } from 'pl-fe/hooks';
import { normalizeNotification, type Notification } from 'pl-fe/normalizers';
import { type MinifiedNotification, minifyNotification } from 'pl-fe/pl-hooks/minifiers/minifyNotification';
import { queryClient } from 'pl-fe/queries/client';
import { selectAccount, selectAccounts } from 'pl-fe/selectors';
type Account = ReturnType<typeof selectAccount>;
const importNotification = (notification: MinifiedNotification) => {
queryClient.setQueryData<MinifiedNotification>(
['notifications', 'entities', notification.id],
existingNotification => existingNotification?.duplicate ? existingNotification : notification,
);
};
const useNotification = (notificationId: string) => {
const client = useClient();
const notificationQuery = useQuery({
queryKey: ['notifications', 'entities', notificationId],
queryFn: () => client.notifications.getNotification(notificationId)
.then(normalizeNotification)
.then(minifyNotification),
});
const data: Notification | null = useAppSelector((state) => {
const notification = notificationQuery.data;
if (!notification) return null;
const account = selectAccount(state, notification.account_id)!;
// @ts-ignore
const target = selectAccount(state, notification.target_id)!;
// @ts-ignore
const status = state.statuses.get(notification.status_id)!;
const accounts = selectAccounts(state, notification.account_ids).filter((account): account is Account => account !== undefined);
return {
...notification,
account,
target,
status,
accounts,
};
});
return { ...notificationQuery, data };
};
export { useNotification, importNotification };

View File

@ -0,0 +1,68 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { useClient } from 'pl-fe/hooks';
import { importEntities } from 'pl-fe/pl-hooks/importer';
import { deduplicateNotifications } from 'pl-fe/pl-hooks/normalizers/deduplicateNotifications';
import { queryClient } from 'pl-fe/queries/client';
import { flattenPages } from 'pl-fe/utils/queries';
import type { Notification as BaseNotification, PaginatedResponse, PlApiClient } from 'pl-api';
import type { NotificationType } from 'pl-fe/utils/notification';
type UseNotificationParams = {
types?: Array<NotificationType>;
excludeTypes?: Array<NotificationType>;
}
const getQueryKey = (params: UseNotificationParams) => [
'notifications',
'lists',
params.types ? params.types.join('|') : params.excludeTypes ? ('exclude:' + params.excludeTypes.join('|')) : 'all',
];
const importNotifications = (response: PaginatedResponse<BaseNotification>) => {
const deduplicatedNotifications = deduplicateNotifications(response.items);
importEntities({
notifications: deduplicatedNotifications,
});
return {
items: deduplicatedNotifications.filter(({ duplicate }) => !duplicate).map(({ id }) => id),
previous: response.previous,
next: response.next,
};
};
const useNotificationList = (params: UseNotificationParams) => {
const client = useClient();
const notificationsQuery = useInfiniteQuery({
queryKey: getQueryKey(params),
queryFn: ({ pageParam }) => (pageParam.next ? pageParam.next() : client.notifications.getNotifications({
types: params.types,
exclude_types: params.excludeTypes,
})).then(importNotifications),
initialPageParam: { previous: null, next: null } as Pick<PaginatedResponse<BaseNotification>, 'previous' | 'next'>,
getNextPageParam: (response) => response,
});
const data = flattenPages<string>(notificationsQuery.data) || [];
return {
...notificationsQuery,
data,
};
};
const prefetchNotifications = (client: PlApiClient, params: UseNotificationParams) =>
queryClient.prefetchInfiniteQuery({
queryKey: getQueryKey(params),
queryFn: () => client.notifications.getNotifications({
types: params.types,
exclude_types: params.excludeTypes,
}).then(importNotifications),
initialPageParam: { previous: null, next: null } as Pick<PaginatedResponse<BaseNotification>, 'previous' | 'next'>,
});
export { useNotificationList, prefetchNotifications };

View File

@ -0,0 +1,133 @@
import { useQuery } from '@tanstack/react-query';
import { useIntl } from 'react-intl';
import { useAccount } from 'pl-fe/api/hooks';
import { useAppSelector, useClient } from 'pl-fe/hooks';
import { importEntities } from 'pl-fe/pl-hooks/importer';
import { queryClient } from 'pl-fe/queries/client';
import { selectAccount, selectAccounts } from 'pl-fe/selectors';
import { normalizeStatus, type Status } from '../../normalizers/normalizeStatus';
// import type { Group } from 'pl-fe/normalizers';
type Account = ReturnType<typeof selectAccount>;
// const toServerSideType = (columnType: string): Filter['context'][0] => {
// switch (columnType) {
// case 'home':
// case 'notifications':
// case 'public':
// case 'thread':
// return columnType;
// default:
// if (columnType.includes('list:')) {
// return 'home';
// } else {
// return 'public'; // community, account, hashtag
// }
// }
// };
// type FilterContext = { contextType?: string };
// const getFilters = (state: RootState, query: FilterContext) =>
// state.filters.filter((filter) =>
// (!query?.contextType || filter.context.includes(toServerSideType(query.contextType)))
// && (filter.expires_at === null || Date.parse(filter.expires_at) > new Date().getTime()),
// );
// const escapeRegExp = (string: string) =>
// string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
// const regexFromFilters = (filters: ImmutableList<Filter>) => {
// if (filters.size === 0) return null;
// return new RegExp(filters.map(filter =>
// filter.keywords.map(keyword => {
// let expr = escapeRegExp(keyword.keyword);
// if (keyword.whole_word) {
// if (/^[\w]/.test(expr)) {
// expr = `\\b${expr}`;
// }
// if (/[\w]$/.test(expr)) {
// expr = `${expr}\\b`;
// }
// }
// return expr;
// }).join('|'),
// ).join('|'), 'i');
// };
// const checkFiltered = (index: string, filters: ImmutableList<Filter>) =>
// filters.reduce((result: Array<string>, filter) =>
// result.concat(filter.keywords.reduce((result: Array<string>, keyword) => {
// let expr = escapeRegExp(keyword.keyword);
// if (keyword.whole_word) {
// if (/^[\w]/.test(expr)) {
// expr = `\\b${expr}`;
// }
// if (/[\w]$/.test(expr)) {
// expr = `${expr}\\b`;
// }
// }
// const regex = new RegExp(expr);
// if (regex.test(index)) return result.concat(filter.title);
// return result;
// }, [])), []);
const importStatus = (status: Status) => {
queryClient.setQueryData<Status>(
['statuses', 'entities', status.id],
status,
);
};
const useStatus = (statusId?: string) => {
const client = useClient();
const intl = useIntl();
const statusQuery = useQuery({
queryKey: ['statuses', 'entities', statusId],
queryFn: () => client.statuses.getStatus(statusId!, {
language: intl.locale,
})
.then(status => (importEntities({ statuses: [status] }, { withParents: false }), status))
.then(normalizeStatus),
enabled: !!statusId,
});
const status = statusQuery.data;
const { account } = useAccount(status?.account_id || undefined);
// : (Status & {
// account: Account;
// accounts: Array<Account>;
// reblog: Status | null;
// }) | null
const data = useAppSelector((state) => {
if (!status) return null;
const accounts = selectAccounts(state, status.account_ids).filter((account): account is Account => account !== undefined);
return {
...status,
account: account!,
accounts,
// quote,
// reblog,
// poll
};
});
return { ...statusQuery, data };
};
export { useStatus, importStatus };

View File

@ -0,0 +1,108 @@
import { importAccounts, importGroups, importPolls, importStatuses } from 'pl-fe/actions/importer';
import { importEntities as importEntityStoreEntities } from 'pl-fe/entity-store/actions';
import { Entities } from 'pl-fe/entity-store/entities';
import { queryClient } from 'pl-fe/queries/client';
import { minifyNotification, type MinifiedNotification } from './minifiers/minifyNotification';
import { DeduplicatedNotification } from './normalizers/deduplicateNotifications';
import { normalizeStatus, type Status } from './normalizers/normalizeStatus';
import type {
Account as BaseAccount,
Group as BaseGroup,
Poll as BasePoll,
Relationship as BaseRelationship,
Status as BaseStatus,
} from 'pl-api';
import type { AppDispatch } from 'pl-fe/store';
let dispatch: AppDispatch;
import('pl-fe/store').then(value => dispatch = value.store.dispatch).catch(() => {});
const importNotification = (notification: DeduplicatedNotification) => {
queryClient.setQueryData<MinifiedNotification>(
['notifications', 'entities', notification.id],
existingNotification => existingNotification?.duplicate ? existingNotification : minifyNotification(notification),
);
};
const importStatus = (status: BaseStatus) => {
queryClient.setQueryData<Status>(
['statuses', 'entities', status.id],
_ => normalizeStatus(status),
);
};
const isEmpty = (object: Record<string, any>) => {
for (const i in object) return false;
return true;
};
const importEntities = (entities: {
accounts?: Array<BaseAccount>;
groups?: Array<BaseGroup>;
notifications?: Array<DeduplicatedNotification>;
polls?: Array<BasePoll>;
statuses?: Array<BaseStatus>;
relationships?: Array<BaseRelationship>;
}, options = {
withParents: true,
}) => {
const accounts: Record<string, BaseAccount> = {};
const groups: Record<string, BaseGroup> = {};
const notifications: Record<string, DeduplicatedNotification> = {};
const polls: Record<string, BasePoll> = {};
const relationships: Record<string, BaseRelationship> = {};
const statuses: Record<string, BaseStatus> = {};
const processAccount = (account: BaseAccount, withParent = true) => {
if (withParent) accounts[account.id] = account;
if (account.moved) processAccount(account.moved);
if (account.relationship) relationships[account.relationship.id] = account.relationship;
};
const processNotification = (notification: DeduplicatedNotification, withParent = true) => {
if (withParent) notifications[notification.id] = notification;
processAccount(notification.account);
if (notification.type === 'move') processAccount(notification.target);
if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(notification.type))
// @ts-ignore
processStatus(notification.status);
};
const processStatus = (status: BaseStatus, withParent = true) => {
if (status.account) {
if (withParent) statuses[status.id] = status;
processAccount(status.account);
}
if (status.quote) processStatus(status.quote);
if (status.reblog) processStatus(status.reblog);
if (status.poll) polls[status.poll.id] = status.poll;
if (status.group) groups[status.group.id] = status.group;
};
if (options.withParents) {
entities.groups?.forEach(group => groups[group.id] = group);
entities.polls?.forEach(poll => polls[poll.id] = poll);
entities.relationships?.forEach(relationship => relationships[relationship.id] = relationship);
}
entities.accounts?.forEach((account) => processAccount(account, options.withParents));
entities.notifications?.forEach((notification) => processNotification(notification, options.withParents));
entities.statuses?.forEach((status) => processStatus(status, options.withParents));
if (!isEmpty(accounts)) dispatch(importAccounts(Object.values(accounts)));
if (!isEmpty(groups)) dispatch(importGroups(Object.values(groups)));
if (!isEmpty(notifications)) Object.values(notifications).forEach(importNotification);
if (!isEmpty(polls)) dispatch(importPolls(Object.values(polls)));
if (!isEmpty(relationships)) dispatch(importEntityStoreEntities(Object.values(relationships), Entities.RELATIONSHIPS));
if (!isEmpty(statuses)) dispatch(importStatuses(Object.values(statuses)));
if (!isEmpty(statuses)) Object.values(statuses).forEach(importStatus);
};
export { importEntities };

View File

View File

@ -0,0 +1,79 @@
import omit from 'lodash/omit';
import { DeduplicatedNotification } from '../normalizers/deduplicateNotifications';
import type { AccountWarning, RelationshipSeveranceEvent } from 'pl-api';
const minifyNotification = (notification: DeduplicatedNotification) => {
// @ts-ignore
const minifiedNotification: {
duplicate: boolean;
account_id: string;
account_ids: string[];
created_at: string;
id: string;
group_key: string;
} & (
| { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' }
| {
type: 'mention';
subtype?: 'reply';
status_id: string;
}
| {
type: 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder';
status_id: string;
}
| {
type: 'admin.report';
report: Report;
}
| {
type: 'severed_relationships';
relationship_severance_event: RelationshipSeveranceEvent;
}
| {
type: 'moderation_warning';
moderation_warning: AccountWarning;
}
| {
type: 'move';
target_id: string;
}
| {
type: 'emoji_reaction';
emoji: string;
emoji_url: string | null;
status_id: string;
}
| {
type: 'chat_mention';
chat_message_id: string;
}
| {
type: 'participation_accepted' | 'participation_request';
status_id: string;
participation_message: string | null;
}
) = {
...omit(notification, ['account', 'accounts', 'status', 'target', 'chat_message']),
account_id: notification.account.id,
account_ids: notification.accounts.map(({ id }) => id),
created_at: notification.created_at,
id: notification.id,
type: notification.type,
};
// @ts-ignore
if (notification.status) minifiedNotification.status_id = notification.status.id;
// @ts-ignore
if (notification.target) minifiedNotification.target_id = notification.target.id;
// @ts-ignore
if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id;
return minifiedNotification;
};
type MinifiedNotification = ReturnType<typeof minifyNotification>;
export { minifyNotification, type MinifiedNotification };

View File

@ -0,0 +1,45 @@
import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification';
import type { Account as BaseAccount, Notification as BaseNotification } from 'pl-api';
type DeduplicatedNotification = BaseNotification & {
accounts: Array<BaseAccount>;
duplicate?: boolean;
}
const STATUS_NOTIFICATION_TYPES = [
'favourite',
'reblog',
'emoji_reaction',
'event_reminder',
'participation_accepted',
'participation_request',
];
const deduplicateNotifications = (notifications: Array<BaseNotification>) => {
const deduplicatedNotifications: DeduplicatedNotification[] = [];
for (const notification of notifications) {
if (STATUS_NOTIFICATION_TYPES.includes(notification.type)) {
const existingNotification = deduplicatedNotifications
.find(deduplicated =>
deduplicated.type === notification.type
&& ((notification.type === 'emoji_reaction' && deduplicated.type === 'emoji_reaction') ? notification.emoji === deduplicated.emoji : true)
&& getNotificationStatus(deduplicated)?.id === getNotificationStatus(notification)?.id,
);
if (existingNotification) {
existingNotification.accounts.push(notification.account);
deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: true });
} else {
deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false });
}
} else {
deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false });
}
}
return deduplicatedNotifications;
};
export { deduplicateNotifications, type DeduplicatedNotification };

View File

@ -0,0 +1,38 @@
import escapeTextContentForBrowser from 'escape-html';
import emojify from 'pl-fe/features/emoji';
import { unescapeHTML } from 'pl-fe/utils/html';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
import type { Account as BaseAccount } from 'pl-api';
const normalizeAccount = ({ moved, ...account }: BaseAccount) => {
const missingAvatar = require('pl-fe/assets/images/avatar-missing.png');
const missingHeader = require('pl-fe/assets/images/header-missing.png');
const note = account.note === '<p></p>' ? '' : account.note;
const emojiMap = makeEmojiMap(account.emojis);
return {
...account,
moved_id: moved?.id || null,
avatar: account.avatar || account.avatar_static || missingAvatar,
avatar_static: account.avatar_static || account.avatar || missingAvatar,
header: account.header || account.header_static || missingHeader,
header_static: account.header_static || account.header || missingHeader,
note,
display_name_html: emojify(escapeTextContentForBrowser(account.display_name), emojiMap),
note_emojified: emojify(account.note, emojiMap),
note_plain: unescapeHTML(account.note),
fields: account.fields.map(field => ({
...field,
name_emojified: emojify(escapeTextContentForBrowser(field.name), emojiMap),
value_emojified: emojify(field.value, emojiMap),
value_plain: unescapeHTML(field.value),
})),
};
};
type Account = ReturnType<typeof normalizeAccount>;
export { normalizeAccount, type Account };

View File

@ -0,0 +1,171 @@
/**
* Status normalizer:
* Converts API statuses into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/status/}
*/
import escapeTextContentForBrowser from 'escape-html';
import DOMPurify from 'isomorphic-dompurify';
import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema, type Translation } from 'pl-api';
import emojify from 'pl-fe/features/emoji';
import { queryClient } from 'pl-fe/queries/client';
import { unescapeHTML } from 'pl-fe/utils/html';
import { makeEmojiMap } from 'pl-fe/utils/normalizers';
const domParser = new DOMParser();
type StatusApprovalStatus = Exclude<BaseStatus['approval_status'], null>;
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' | 'mutuals_only' | 'local';
type CalculatedValues = {
search_index: string;
contentHtml: string;
spoilerHtml: string;
contentMapHtml?: Record<string, string>;
spoilerMapHtml?: Record<string, string>;
expanded?: boolean | null;
hidden?: boolean | null;
translation?: Translation | null | false;
currentLanguage?: string;
};
type OldStatus = Pick<BaseStatus, 'content' | 'spoiler_text'> & CalculatedValues;
// Gets titles of poll options from status
const getPollOptionTitles = ({ poll }: Pick<BaseStatus, 'poll'>): readonly string[] => {
if (poll && typeof poll === 'object') {
return poll.options.map(({ title }) => title);
} else {
return [];
}
};
// Gets usernames of mentioned users from status
const getMentionedUsernames = (status: Pick<BaseStatus, 'mentions'>): Array<string> =>
status.mentions.map(({ acct }) => `@${acct}`);
// Creates search text from the status
const buildSearchContent = (status: Pick<BaseStatus, 'poll' | 'mentions' | 'spoiler_text' | 'content'>): string => {
const pollOptionTitles = getPollOptionTitles(status);
const mentionedUsernames = getMentionedUsernames(status);
const fields = [
status.spoiler_text,
status.content,
...pollOptionTitles,
...mentionedUsernames,
];
return unescapeHTML(fields.join('\n\n')) || '';
};
const calculateContent = (text: string, emojiMap: any, hasQuote?: boolean) => emojify(text, emojiMap);
const calculateSpoiler = (text: string, emojiMap: any) => DOMPurify.sanitize(emojify(escapeTextContentForBrowser(text), emojiMap), { USE_PROFILES: { html: true } });
const calculateStatus = (status: BaseStatus, oldStatus?: OldStatus): CalculatedValues => {
if (oldStatus && oldStatus.content === status.content && oldStatus.spoiler_text === status.spoiler_text) {
const {
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
} = oldStatus;
return {
search_index, contentHtml, spoilerHtml, contentMapHtml, spoilerMapHtml, hidden, expanded, translation, currentLanguage,
};
} else {
const searchContent = buildSearchContent(status);
const emojiMap = makeEmojiMap(status.emojis);
return {
search_index: domParser.parseFromString(searchContent, 'text/html').documentElement.textContent || '',
contentHtml: calculateContent(status.content, emojiMap, !!status.quote),
spoilerHtml: calculateSpoiler(status.spoiler_text, emojiMap),
contentMapHtml: status.content_map
? Object.fromEntries(Object.entries(status.content_map)?.map(([key, value]) => [key, calculateContent(value, emojiMap, !!status.quote)]))
: undefined,
spoilerMapHtml: status.spoiler_text_map
? Object.fromEntries(Object.entries(status.spoiler_text_map).map(([key, value]) => [key, calculateSpoiler(value, emojiMap)]))
: undefined,
};
}
};
const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...status }: BaseStatus & { accounts?: Array<BaseAccount> }) => {
const oldStatus = queryClient.getQueryData<OldStatus>(['statuses', 'entities', status.id]);
const calculated = calculateStatus(status, oldStatus);
// Sort the replied-to mention to the top
let mentions = status.mentions.toSorted((a, _b) => {
if (a.id === status.in_reply_to_account_id) {
return -1;
} else {
return 0;
}
});
// Add self to mentions if it's a reply to self
const isSelfReply = account.id === status.in_reply_to_account_id;
const hasSelfMention = status.mentions.some(mention => account.id === mention.id);
if (isSelfReply && !hasSelfMention) {
const selfMention = mentionSchema.parse(status.account);
mentions = [selfMention, ...mentions];
}
// Normalize event
let event: BaseStatus['event'] & ({
banner: MediaAttachment | null;
links: Array<MediaAttachment>;
} | null) = null;
let media_attachments = status.media_attachments;
if (status.event) {
const firstAttachment = status.media_attachments[0];
let banner: MediaAttachment | null = null;
if (firstAttachment?.description === 'Banner' && firstAttachment.type === 'image') {
banner = firstAttachment;
media_attachments = media_attachments.slice(1);
}
const links = media_attachments.filter(attachment => attachment.mime_type === 'text/html');
media_attachments = media_attachments.filter(attachment => attachment.mime_type !== 'text/html');
event = {
...status.event,
banner,
links,
};
}
return {
account_id: account.id,
account_ids: accounts?.map(account => account.id) || [account.id],
reblog_id: reblog?.id || null,
poll_id: poll?.id || null,
group_id: group?.id || null,
translating: false,
expectsCard: false,
showFiltered: null as null | boolean,
...status,
quote_id: quote?.id || status.quote_id || null,
mentions,
expanded: null,
hidden: null,
/** Rewrite `<p></p>` to empty string. */
content: status.content === '<p></p>' ? '' : status.content,
filtered: status.filtered?.map(result => result.filter.title),
event,
media_attachments,
...calculated,
translation: (status.translation || calculated.translation || null) as Translation | null | false,
};
};
type Status = ReturnType<typeof normalizeStatus>;
export {
type StatusApprovalStatus,
type StatusVisibility,
normalizeStatus,
type Status,
};