From bf7c08d4d12fd27656bfecfd2e5fc41662e5d011 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 May 2023 11:55:30 -0500 Subject: [PATCH 1/5] DetailedStatus: remove unused props --- .../features/status/components/detailed-status.tsx | 6 +----- app/soapbox/features/status/index.tsx | 11 ----------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 15f2e86e8..cd81dd0d2 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -15,14 +15,10 @@ import { getActualStatus } from 'soapbox/utils/status'; import StatusInteractionBar from './status-interaction-bar'; -import type { List as ImmutableList } from 'immutable'; -import type { Attachment as AttachmentEntity, Group, Status as StatusEntity } from 'soapbox/types/entities'; +import type { Group, Status as StatusEntity } from 'soapbox/types/entities'; interface IDetailedStatus { status: StatusEntity - onOpenMedia: (media: ImmutableList, index: number) => void - onOpenVideo: (media: ImmutableList, start: number) => void - onToggleHidden: (status: StatusEntity) => void showMedia: boolean onOpenCompareHistoryModal: (status: StatusEntity) => void onToggleMediaVisibility: () => void diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index 6b41148c4..e7dbc8018 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -230,14 +230,6 @@ const Thread: React.FC = (props) => { dispatch(mentionCompose(account)); }; - const handleOpenMedia = (media: ImmutableList, index: number) => { - dispatch(openModal('MEDIA', { media, status, index })); - }; - - const handleOpenVideo = (media: ImmutableList, time: number) => { - dispatch(openModal('VIDEO', { media, time })); - }; - const handleHotkeyOpenMedia = (e?: KeyboardEvent) => { const { onOpenMedia, onOpenVideo } = props; const firstAttachment = status?.media_attachments.get(0); @@ -478,9 +470,6 @@ const Thread: React.FC = (props) => { Date: Thu, 11 May 2023 11:56:19 -0500 Subject: [PATCH 2/5] statusSchema: add HTML fields --- app/soapbox/schemas/status.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/soapbox/schemas/status.ts b/app/soapbox/schemas/status.ts index ea55d5085..4905fb5a3 100644 --- a/app/soapbox/schemas/status.ts +++ b/app/soapbox/schemas/status.ts @@ -1,5 +1,9 @@ +import escapeTextContentForBrowser from 'escape-html'; import { z } from 'zod'; +import emojify from 'soapbox/features/emoji'; +import { stripCompatibilityFeatures } from 'soapbox/utils/html'; + import { accountSchema } from './account'; import { attachmentSchema } from './attachment'; import { cardSchema } from './card'; @@ -8,7 +12,7 @@ import { groupSchema } from './group'; import { mentionSchema } from './mention'; import { pollSchema } from './poll'; import { tagSchema } from './tag'; -import { contentSchema, dateSchema, filteredArray } from './utils'; +import { contentSchema, dateSchema, filteredArray, makeCustomEmojiMap } from './utils'; const tombstoneSchema = z.object({ reason: z.enum(['deleted']), @@ -59,6 +63,17 @@ const baseStatusSchema = z.object({ const statusSchema = baseStatusSchema.extend({ quote: baseStatusSchema.nullable().catch(null), reblog: baseStatusSchema.nullable().catch(null), +}).transform((status) => { + const emojiMap = makeCustomEmojiMap(status.emojis); + + const contentHtml = stripCompatibilityFeatures(emojify(status.content, emojiMap)); + const spoilerHtml = emojify(escapeTextContentForBrowser(status.spoiler_text), emojiMap); + + return { + ...status, + contentHtml, + spoilerHtml, + }; }); type Status = z.infer; From 752f06b92562ef7b7bbe848b49fd9ce0a03249b9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 11 May 2023 12:02:55 -0500 Subject: [PATCH 3/5] actions: improve types --- app/soapbox/actions/blocks.ts | 7 ++--- app/soapbox/actions/familiar-followers.ts | 34 +++-------------------- app/soapbox/actions/interactions.ts | 4 +-- 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/app/soapbox/actions/blocks.ts b/app/soapbox/actions/blocks.ts index d3f625884..ef2f40359 100644 --- a/app/soapbox/actions/blocks.ts +++ b/app/soapbox/actions/blocks.ts @@ -6,9 +6,8 @@ import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; -import type { AnyAction } from '@reduxjs/toolkit'; import type { AxiosError } from 'axios'; -import type { RootState } from 'soapbox/store'; +import type { AppDispatch, RootState } from 'soapbox/store'; const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; @@ -18,7 +17,7 @@ const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; -const fetchBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => { +const fetchBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; const nextLinkName = getNextLinkName(getState); @@ -54,7 +53,7 @@ function fetchBlocksFail(error: AxiosError) { }; } -const expandBlocks = () => (dispatch: React.Dispatch, getState: () => RootState) => { +const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; const nextLinkName = getNextLinkName(getState); diff --git a/app/soapbox/actions/familiar-followers.ts b/app/soapbox/actions/familiar-followers.ts index 2d8aa6786..2c82126c6 100644 --- a/app/soapbox/actions/familiar-followers.ts +++ b/app/soapbox/actions/familiar-followers.ts @@ -1,40 +1,14 @@ -import { RootState } from 'soapbox/store'; +import { AppDispatch, RootState } from 'soapbox/store'; import api from '../api'; -import { ACCOUNTS_IMPORT, importFetchedAccounts } from './importer'; - -import type { APIEntity } from 'soapbox/types/entities'; +import { importFetchedAccounts } from './importer'; export const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST'; export const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS'; export const FAMILIAR_FOLLOWERS_FETCH_FAIL = 'FAMILIAR_FOLLOWERS_FETCH_FAIL'; -type FamiliarFollowersFetchRequestAction = { - type: typeof FAMILIAR_FOLLOWERS_FETCH_REQUEST - id: string -} - -type FamiliarFollowersFetchRequestSuccessAction = { - type: typeof FAMILIAR_FOLLOWERS_FETCH_SUCCESS - id: string - accounts: Array -} - -type FamiliarFollowersFetchRequestFailAction = { - type: typeof FAMILIAR_FOLLOWERS_FETCH_FAIL - id: string - error: any -} - -type AccountsImportAction = { - type: typeof ACCOUNTS_IMPORT - accounts: Array -} - -export type FamiliarFollowersActions = FamiliarFollowersFetchRequestAction | FamiliarFollowersFetchRequestSuccessAction | FamiliarFollowersFetchRequestFailAction | AccountsImportAction - -export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: React.Dispatch, getState: () => RootState) => { +export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: FAMILIAR_FOLLOWERS_FETCH_REQUEST, id: accountId, @@ -44,7 +18,7 @@ export const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: R .then(({ data }) => { const accounts = data.find(({ id }: { id: string }) => id === accountId).accounts; - dispatch(importFetchedAccounts(accounts) as AccountsImportAction); + dispatch(importFetchedAccounts(accounts)); dispatch({ type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS, id: accountId, diff --git a/app/soapbox/actions/interactions.ts b/app/soapbox/actions/interactions.ts index 40d981139..b2517301f 100644 --- a/app/soapbox/actions/interactions.ts +++ b/app/soapbox/actions/interactions.ts @@ -160,7 +160,7 @@ const favourite = (status: StatusEntity) => dispatch(favouriteRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function(response) { + api(getState).post(`/api/v1/statuses/${status.id}/favourite`).then(function(response) { dispatch(favouriteSuccess(status)); }).catch(function(error) { dispatch(favouriteFail(status, error)); @@ -173,7 +173,7 @@ const unfavourite = (status: StatusEntity) => dispatch(unfavouriteRequest(status)); - api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(() => { + api(getState).post(`/api/v1/statuses/${status.id}/unfavourite`).then(() => { dispatch(unfavouriteSuccess(status)); }).catch(error => { dispatch(unfavouriteFail(status, error)); From fa0bf8f5df3c63ebc543b373aa64549ca9bbfdea Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 18 May 2023 17:09:15 -0500 Subject: [PATCH 4/5] Improve statusSchema --- app/soapbox/schemas/event.ts | 20 +++++++++++ app/soapbox/schemas/location.ts | 23 ++++++++++++ app/soapbox/schemas/status.ts | 62 ++++++++++++++++++++++++++------- 3 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 app/soapbox/schemas/event.ts create mode 100644 app/soapbox/schemas/location.ts diff --git a/app/soapbox/schemas/event.ts b/app/soapbox/schemas/event.ts new file mode 100644 index 000000000..e74a80760 --- /dev/null +++ b/app/soapbox/schemas/event.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +import { attachmentSchema } from './attachment'; +import { locationSchema } from './location'; + +const eventSchema = z.object({ + name: z.string().catch(''), + start_time: z.string().datetime().nullable().catch(null), + end_time: z.string().datetime().nullable().catch(null), + join_mode: z.enum(['free', 'restricted', 'invite']).nullable().catch(null), + participants_count: z.number().catch(0), + location: locationSchema.nullable().catch(null), + join_state: z.enum(['pending', 'reject', 'accept']).nullable().catch(null), + banner: attachmentSchema.nullable().catch(null), + links: z.array(attachmentSchema).nullable().catch(null), +}); + +type Event = z.infer; + +export { eventSchema, type Event }; \ No newline at end of file diff --git a/app/soapbox/schemas/location.ts b/app/soapbox/schemas/location.ts new file mode 100644 index 000000000..cbc237222 --- /dev/null +++ b/app/soapbox/schemas/location.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +const locationSchema = z.object({ + url: z.string().url().catch(''), + description: z.string().catch(''), + country: z.string().catch(''), + locality: z.string().catch(''), + region: z.string().catch(''), + postal_code: z.string().catch(''), + street: z.string().catch(''), + origin_id: z.string().catch(''), + origin_provider: z.string().catch(''), + type: z.string().catch(''), + timezone: z.string().catch(''), + geom: z.object({ + coordinates: z.tuple([z.number(), z.number()]).nullable().catch(null), + srid: z.string().catch(''), + }).nullable().catch(null), +}); + +type Location = z.infer; + +export { locationSchema, type Location }; \ No newline at end of file diff --git a/app/soapbox/schemas/status.ts b/app/soapbox/schemas/status.ts index 4905fb5a3..edb585aec 100644 --- a/app/soapbox/schemas/status.ts +++ b/app/soapbox/schemas/status.ts @@ -2,22 +2,19 @@ import escapeTextContentForBrowser from 'escape-html'; import { z } from 'zod'; import emojify from 'soapbox/features/emoji'; -import { stripCompatibilityFeatures } from 'soapbox/utils/html'; +import { stripCompatibilityFeatures, unescapeHTML } from 'soapbox/utils/html'; import { accountSchema } from './account'; import { attachmentSchema } from './attachment'; import { cardSchema } from './card'; import { customEmojiSchema } from './custom-emoji'; +import { eventSchema } from './event'; import { groupSchema } from './group'; import { mentionSchema } from './mention'; import { pollSchema } from './poll'; import { tagSchema } from './tag'; import { contentSchema, dateSchema, filteredArray, makeCustomEmojiMap } from './utils'; -const tombstoneSchema = z.object({ - reason: z.enum(['deleted']), -}); - const baseStatusSchema = z.object({ account: accountSchema, application: z.object({ @@ -43,27 +40,44 @@ const baseStatusSchema = z.object({ mentions: filteredArray(mentionSchema), muted: z.coerce.boolean(), pinned: z.coerce.boolean(), - pleroma: z.object({}).optional().catch(undefined), poll: pollSchema.nullable().catch(null), quote: z.literal(null).catch(null), quotes_count: z.number().catch(0), - reblog: z.literal(null).catch(null), reblogged: z.coerce.boolean(), reblogs_count: z.number().catch(0), replies_count: z.number().catch(0), sensitive: z.coerce.boolean(), spoiler_text: contentSchema, tags: filteredArray(tagSchema), - tombstone: tombstoneSchema.nullable().optional(), + tombstone: z.object({ + reason: z.enum(['deleted']), + }).nullable().optional().catch(undefined), uri: z.string().url().catch(''), url: z.string().url().catch(''), visibility: z.string().catch('public'), }); -const statusSchema = baseStatusSchema.extend({ - quote: baseStatusSchema.nullable().catch(null), - reblog: baseStatusSchema.nullable().catch(null), -}).transform((status) => { +type BaseStatus = z.infer; +type TransformableStatus = Omit; + +/** Creates search index from the status. */ +const buildSearchIndex = (status: TransformableStatus): string => { + const pollOptionTitles = status.poll ? status.poll.options.map(({ title }) => title) : []; + const mentionedUsernames = status.mentions.map(({ acct }) => `@${acct}`); + + const fields = [ + status.spoiler_text, + status.content, + ...pollOptionTitles, + ...mentionedUsernames, + ]; + + const searchContent = unescapeHTML(fields.join('\n\n')) || ''; + return new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent || ''; +}; + +/** Add internal fields to the status. */ +const transformStatus = (status: T) => { const emojiMap = makeCustomEmojiMap(status.emojis); const contentHtml = stripCompatibilityFeatures(emojify(status.content, emojiMap)); @@ -73,8 +87,30 @@ const statusSchema = baseStatusSchema.extend({ ...status, contentHtml, spoilerHtml, + search_index: buildSearchIndex(status), + hidden: false, }; -}); +}; + +const embeddedStatusSchema = baseStatusSchema + .transform(transformStatus) + .nullable() + .catch(null); + +const statusSchema = baseStatusSchema.extend({ + quote: embeddedStatusSchema, + reblog: embeddedStatusSchema, + pleroma: z.object({ + event: eventSchema, + quote: embeddedStatusSchema, + }), +}).transform(({ pleroma, ...status }) => { + return { + ...status, + event: pleroma.event, + quote: pleroma.quote || status.quote, + }; +}).transform(transformStatus); type Status = z.infer; From 6062a06746fb4e4a64b1cb830b36091a652e41f4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 19 May 2023 11:13:44 -0500 Subject: [PATCH 5/5] Improve schemas for statuses --- app/soapbox/schemas/account.ts | 6 +++--- app/soapbox/schemas/attachment.ts | 6 ++++++ app/soapbox/schemas/status.ts | 28 ++++++++++++++++++++++++---- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/soapbox/schemas/account.ts b/app/soapbox/schemas/account.ts index 919013329..f2e5f9c15 100644 --- a/app/soapbox/schemas/account.ts +++ b/app/soapbox/schemas/account.ts @@ -49,7 +49,7 @@ const accountSchema = z.object({ verified: z.boolean().default(false), website: z.string().catch(''), - /** + /* * Internal fields */ display_name_html: z.string().catch(''), @@ -57,7 +57,7 @@ const accountSchema = z.object({ note_emojified: z.string().catch(''), relationship: relationshipSchema.nullable().catch(null), - /** + /* * Misc */ other_settings: z.any(), @@ -99,7 +99,7 @@ const accountSchema = z.object({ // Notes account.note_emojified = emojify(account.note, customEmojiMap); - /** + /* * Todo * - internal fields * - donor diff --git a/app/soapbox/schemas/attachment.ts b/app/soapbox/schemas/attachment.ts index 44b9cb126..3df39d542 100644 --- a/app/soapbox/schemas/attachment.ts +++ b/app/soapbox/schemas/attachment.ts @@ -62,6 +62,12 @@ const audioAttachmentSchema = baseAttachmentSchema.extend({ type: z.literal('audio'), meta: z.object({ duration: z.number().optional().catch(undefined), + colors: z.object({ + background: z.string().optional().catch(undefined), + foreground: z.string().optional().catch(undefined), + accent: z.string().optional().catch(undefined), + duration: z.number().optional().catch(undefined), + }).optional().catch(undefined), }).catch({}), }); diff --git a/app/soapbox/schemas/status.ts b/app/soapbox/schemas/status.ts index edb585aec..323ca5789 100644 --- a/app/soapbox/schemas/status.ts +++ b/app/soapbox/schemas/status.ts @@ -40,6 +40,9 @@ const baseStatusSchema = z.object({ mentions: filteredArray(mentionSchema), muted: z.coerce.boolean(), pinned: z.coerce.boolean(), + pleroma: z.object({ + quote_visible: z.boolean().catch(true), + }).optional().catch(undefined), poll: pollSchema.nullable().catch(null), quote: z.literal(null).catch(null), quotes_count: z.number().catch(0), @@ -58,7 +61,7 @@ const baseStatusSchema = z.object({ }); type BaseStatus = z.infer; -type TransformableStatus = Omit; +type TransformableStatus = Omit; /** Creates search index from the status. */ const buildSearchIndex = (status: TransformableStatus): string => { @@ -76,6 +79,11 @@ const buildSearchIndex = (status: TransformableStatus): string => { return new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent || ''; }; +type Translation = { + content: string + provider: string +} + /** Add internal fields to the status. */ const transformStatus = (status: T) => { const emojiMap = makeCustomEmojiMap(status.emojis); @@ -89,6 +97,11 @@ const transformStatus = (status: T) => { spoilerHtml, search_index: buildSearchIndex(status), hidden: false, + filtered: [], + showFiltered: false, // TODO: this should be removed from the schema and done somewhere else + approval_status: 'approval' as const, + translation: undefined as Translation | undefined, + expectsCard: false, }; }; @@ -103,12 +116,19 @@ const statusSchema = baseStatusSchema.extend({ pleroma: z.object({ event: eventSchema, quote: embeddedStatusSchema, - }), + quote_visible: z.boolean().catch(true), + }).optional().catch(undefined), }).transform(({ pleroma, ...status }) => { return { ...status, - event: pleroma.event, - quote: pleroma.quote || status.quote, + event: pleroma?.event, + quote: pleroma?.quote || status.quote || null, + // There's apparently no better way to do this... + // Just trying to remove the `event` and `quote` keys from the object. + pleroma: pleroma ? (() => { + const { event, quote, ...rest } = pleroma; + return rest; + })() : undefined, }; }).transform(transformStatus);