diff --git a/package.json b/package.json index 3ef9bce45..fd29adda9 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "html-react-parser": "^5.0.0", "http-link-header": "^1.0.2", "immer": "^10.0.0", - "immutable": "^4.2.1", + "immutable": "^4.3.7", "intersection-observer": "^0.12.2", "intl-messageformat": "10.5.11", "intl-pluralrules": "^2.0.0", diff --git a/src/actions/compose.ts b/src/actions/compose.ts index e7d431589..1f6dea9ba 100644 --- a/src/actions/compose.ts +++ b/src/actions/compose.ts @@ -20,7 +20,7 @@ import { getSettings } from './settings'; import { createStatus } from './statuses'; import type { EditorState } from 'lexical'; -import type { Tag } from 'pl-api'; +import type { CreateStatusParams, Tag } from 'pl-api'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input'; import type { Emoji } from 'soapbox/features/emoji'; import type { Account, Group } from 'soapbox/schemas'; @@ -373,22 +373,31 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => const idempotencyKey = compose.idempotencyKey; const contentType = compose.content_type === 'wysiwyg' ? 'text/markdown' : compose.content_type; - const params: Record = { + const params: CreateStatusParams = { status, - in_reply_to_id: compose.in_reply_to, - quote_id: compose.quote, - media_ids: media.map(item => item.id), + in_reply_to_id: compose.in_reply_to || undefined, + quote_id: compose.quote || undefined, + media_ids: media.map(item => item.id).toArray(), sensitive: compose.sensitive, spoiler_text: compose.spoiler_text, visibility: compose.privacy, content_type: contentType, - poll: compose.poll, - scheduled_at: compose.schedule, - language: compose.language || compose.suggested_language, - to, + scheduled_at: compose.schedule?.toISOString(), + language: compose.language || compose.suggested_language || undefined, + to: to.size ? to.toArray() : undefined, federated: compose.federated, }; + if (compose.poll) { + params.poll = { + options: compose.poll.options.toArray(), + expires_in: compose.poll.expires_in, + multiple: compose.poll.multiple, + hide_totals: compose.poll.hide_totals, + options_map: compose.poll.options_map.toJS(), + }; + } + if (compose.language && compose.textMap.size) { params.status_map = compose.textMap.toJS(); params.status_map[compose.language] = status; @@ -398,14 +407,13 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) => params.spoiler_text_map[compose.language] = compose.spoiler_text; } - if (params.poll) { - const poll = params.poll.toJS(); - poll.options.forEach((option: any, index: number) => poll.options_map[index][compose.language!] = option); - params.poll = poll; + const poll = params.poll; + if (poll?.options_map) { + poll.options.forEach((option: any, index: number) => poll.options_map![index][compose.language!] = option); } } - if (compose.privacy === 'group') { + if (compose.privacy === 'group' && compose.group_id) { params.group_id = compose.group_id; } diff --git a/src/actions/notifications.ts b/src/actions/notifications.ts index a87cdab88..53fc79fcb 100644 --- a/src/actions/notifications.ts +++ b/src/actions/notifications.ts @@ -283,7 +283,10 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an acc.accounts[item.target.id] = item.target; } + // TODO actually check for type + // @ts-ignore if (item.status?.id) { + // @ts-ignore acc.statuses[item.status.id] = item.status; } diff --git a/src/actions/statuses.ts b/src/actions/statuses.ts index b7b9ea2cd..e7c1e65bb 100644 --- a/src/actions/statuses.ts +++ b/src/actions/statuses.ts @@ -64,16 +64,14 @@ const createStatus = (params: CreateStatusParams, idempotencyKey: string, status return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params)) .then((status) => { - // The backend might still be processing the rich media attachment - if (!status.card && shouldHaveCard(status)) { - status.expectsCard = true; - } + // The backend might still be processing the rich media attachment + const expectsCard = !status.card && shouldHaveCard(status); - dispatch(importFetchedStatus(status, idempotencyKey)); + dispatch(importFetchedStatus({ ...status, expectsCard }, idempotencyKey)); dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey, editing: !!statusId }); // Poll the backend for the updated card - if (status.expectsCard) { + if (expectsCard) { const delay = 1000; const poll = (retries = 5) => { diff --git a/src/api/hooks/accounts/useAccountLookup.ts b/src/api/hooks/accounts/useAccountLookup.ts index a16f4e0d1..b9adda5c3 100644 --- a/src/api/hooks/accounts/useAccountLookup.ts +++ b/src/api/hooks/accounts/useAccountLookup.ts @@ -22,7 +22,7 @@ const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = const { entity: account, isUnauthorized, ...result } = useEntityLookup( Entities.ACCOUNTS, (account) => account.acct.toLowerCase() === acct?.toLowerCase(), - () => client.accounts.lookupAccount(acct), + () => client.accounts.lookupAccount(acct!), { schema: accountSchema, enabled: !!acct }, ); diff --git a/src/api/hooks/announcements/useAnnouncements.ts b/src/api/hooks/announcements/useAnnouncements.ts index fa91865d0..06cf52d90 100644 --- a/src/api/hooks/announcements/useAnnouncements.ts +++ b/src/api/hooks/announcements/useAnnouncements.ts @@ -1,8 +1,25 @@ import { useMutation, useQuery } from '@tanstack/react-query'; -import { announcementReactionSchema, announcementSchema, type Announcement, type AnnouncementReaction } from 'pl-api'; +import { announcementReactionSchema, type Announcement as BaseAnnouncement, type AnnouncementReaction } from 'pl-api'; +import emojify from 'soapbox/features/emoji'; import { useClient } from 'soapbox/hooks'; import { queryClient } from 'soapbox/queries/client'; +import { makeCustomEmojiMap } from 'soapbox/schemas/utils'; + +interface Announcement extends BaseAnnouncement { + contentHtml: string; +} + +const transformAnnouncement = (announcement: BaseAnnouncement) => { + const emojiMap = makeCustomEmojiMap(announcement.emojis); + + const contentHtml = emojify(announcement.content, emojiMap); + + return { + ...announcement, + contentHtml, + }; +}; const updateReaction = (reaction: AnnouncementReaction, count: number, me?: boolean, overwrite?: boolean) => announcementReactionSchema.parse({ ...reaction, @@ -25,7 +42,7 @@ const useAnnouncements = () => { const getAnnouncements = async () => { const data = await client.announcements.getAnnouncements(); - return data; + return data.map(transformAnnouncement); }; const { data, ...result } = useQuery>({ @@ -42,18 +59,18 @@ const useAnnouncements = () => { retry: false, onMutate: ({ announcementId: id, name }) => { queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + prevResult.map(value => value.id !== id ? value : { ...value, reactions: updateReactions(value.reactions, name, 1, true), - })), + }), ); }, onError: (_, { announcementId: id, name }) => { queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + prevResult.map(value => value.id !== id ? value : { ...value, reactions: updateReactions(value.reactions, name, -1, false), - })), + }), ); }, }); @@ -66,18 +83,18 @@ const useAnnouncements = () => { retry: false, onMutate: ({ announcementId: id, name }) => { queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + prevResult.map(value => value.id !== id ? value : { ...value, reactions: updateReactions(value.reactions, name, -1, false), - })), + }), ); }, onError: (_, { announcementId: id, name }) => { queryClient.setQueryData(['announcements'], (prevResult: Announcement[]) => - prevResult.map(value => value.id !== id ? value : announcementSchema.parse({ + prevResult.map(value => value.id !== id ? value : { ...value, reactions: updateReactions(value.reactions, name, 1, true), - })), + }), ); }, }); @@ -93,4 +110,4 @@ const useAnnouncements = () => { const compareAnnouncements = (a: Announcement, b: Announcement): number => new Date(a.starts_at || a.published_at).getDate() - new Date(b.starts_at || b.published_at).getDate(); -export { updateReactions, useAnnouncements }; +export { updateReactions, useAnnouncements, type Announcement }; diff --git a/src/api/hooks/groups/useCreateGroup.ts b/src/api/hooks/groups/useCreateGroup.ts index 2217ec800..981df176e 100644 --- a/src/api/hooks/groups/useCreateGroup.ts +++ b/src/api/hooks/groups/useCreateGroup.ts @@ -4,7 +4,7 @@ import { useClient } from 'soapbox/hooks'; import { groupSchema } from 'soapbox/schemas'; interface CreateGroupParams { - display_name?: string; + display_name: string; note?: string; avatar?: File; header?: File; diff --git a/src/api/hooks/statuses/useBookmarkFolders.ts b/src/api/hooks/statuses/useBookmarkFolders.ts index 07d398b97..32ebc49c2 100644 --- a/src/api/hooks/statuses/useBookmarkFolders.ts +++ b/src/api/hooks/statuses/useBookmarkFolders.ts @@ -10,7 +10,7 @@ const useBookmarkFolders = () => { const { entities, ...result } = useEntities( [Entities.BOOKMARK_FOLDERS], - () => client.myAccount.getBookmarkFolders, + () => client.myAccount.getBookmarkFolders(), { enabled: features.bookmarkFolders, schema: bookmarkFolderSchema }, ); diff --git a/src/api/hooks/statuses/useDeleteBookmarkFolder.ts b/src/api/hooks/statuses/useDeleteBookmarkFolder.ts index 2ec9d36d7..f5058438b 100644 --- a/src/api/hooks/statuses/useDeleteBookmarkFolder.ts +++ b/src/api/hooks/statuses/useDeleteBookmarkFolder.ts @@ -2,12 +2,12 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useDeleteEntity } from 'soapbox/entity-store/hooks'; import { useClient } from 'soapbox/hooks'; -const useDeleteBookmarkFolder = (folderId: string) => { +const useDeleteBookmarkFolder = () => { const client = useClient(); const { deleteEntity, isSubmitting } = useDeleteEntity( Entities.BOOKMARK_FOLDERS, - () => client.myAccount.deleteBookmarkFolder(folderId), + (folderId: string) => client.myAccount.deleteBookmarkFolder(folderId), ); return { diff --git a/src/components/announcements/announcement-content.tsx b/src/components/announcements/announcement-content.tsx index 108b06452..c65325d2f 100644 --- a/src/components/announcements/announcement-content.tsx +++ b/src/components/announcements/announcement-content.tsx @@ -3,10 +3,11 @@ import { useHistory } from 'react-router-dom'; import { getTextDirection } from 'soapbox/utils/rtl'; -import type { Announcement as AnnouncementEntity, Mention as MentionEntity } from 'soapbox/schemas'; +import type { Announcement } from 'soapbox/api/hooks/announcements/useAnnouncements'; +import type { Mention as MentionEntity } from 'soapbox/schemas'; interface IAnnouncementContent { - announcement: AnnouncementEntity; + announcement: Announcement; } const AnnouncementContent: React.FC = ({ announcement }) => { diff --git a/src/components/announcements/announcement.tsx b/src/components/announcements/announcement.tsx index b1467a9cf..759c0bffd 100644 --- a/src/components/announcements/announcement.tsx +++ b/src/components/announcements/announcement.tsx @@ -9,7 +9,7 @@ import AnnouncementContent from './announcement-content'; import ReactionsBar from './reactions-bar'; import type { Map as ImmutableMap } from 'immutable'; -import type { Announcement as AnnouncementEntity } from 'soapbox/schemas'; +import type { Announcement as AnnouncementEntity } from 'soapbox/api/hooks/announcements/useAnnouncements'; interface IAnnouncement { announcement: AnnouncementEntity; diff --git a/src/components/announcements/announcements-panel.tsx b/src/components/announcements/announcements-panel.tsx index cc3676330..2e3619e62 100644 --- a/src/components/announcements/announcements-panel.tsx +++ b/src/components/announcements/announcements-panel.tsx @@ -31,7 +31,7 @@ const AnnouncementsPanel = () => { }> - {announcements!.map((announcement) => ( + {announcements.map((announcement) => ( ({ isOpen: false, @@ -69,7 +71,7 @@ const ChatProvider: React.FC = ({ children }) => { }; interface IChatContext { - chat: IChat | null; + chat: Chat | null; isOpen: boolean; isUsingMainChatPage?: boolean; toggleChatPane(): void; diff --git a/src/entity-store/hooks/useBatchedEntities.ts b/src/entity-store/hooks/useBatchedEntities.ts index 7c4889989..3ba102e80 100644 --- a/src/entity-store/hooks/useBatchedEntities.ts +++ b/src/entity-store/hooks/useBatchedEntities.ts @@ -23,7 +23,7 @@ interface UseBatchedEntitiesOpts { const useBatchedEntities = ( expandedPath: ExpandedEntitiesPath, ids: string[], - entityFn: EntityFn, + entityFn: EntityFn, opts: UseBatchedEntitiesOpts = {}, ) => { const getState = useGetState(); diff --git a/src/entity-store/hooks/useEntity.ts b/src/entity-store/hooks/useEntity.ts index 1c03a0ab8..4e6a1ed72 100644 --- a/src/entity-store/hooks/useEntity.ts +++ b/src/entity-store/hooks/useEntity.ts @@ -24,7 +24,7 @@ interface UseEntityOpts { const useEntity = ( path: EntityPath, - entityFn: EntityFn, + entityFn: EntityFn, opts: UseEntityOpts = {}, ) => { const [isFetching, setPromise] = useLoading(true); diff --git a/src/entity-store/utils.ts b/src/entity-store/utils.ts index 2aac06a17..9167a40f4 100644 --- a/src/entity-store/utils.ts +++ b/src/entity-store/utils.ts @@ -37,8 +37,8 @@ const createList = (): EntityList => ({ /** Create an empty entity list state. */ const createListState = (): EntityListState => ({ - next: undefined, - prev: undefined, + next: null, + prev: null, totalCount: 0, error: null, fetched: false, diff --git a/src/features/auth-login/components/registration-form.tsx b/src/features/auth-login/components/registration-form.tsx index 3795b3175..aaf30cb22 100644 --- a/src/features/auth-login/components/registration-form.tsx +++ b/src/features/auth-login/components/registration-form.tsx @@ -13,6 +13,8 @@ import { Checkbox, Form, FormGroup, FormActions, Button, Input, Textarea, Select import CaptchaField from 'soapbox/features/auth-login/components/captcha'; import { useAppDispatch, useSettings, useFeatures, useInstance } from 'soapbox/hooks'; +import type { CreateAccountParams } from 'pl-api'; + const messages = defineMessages({ username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' }, username_hint: { id: 'registration.fields.username_hint', defaultMessage: 'Only letters, numbers, and underscores are allowed.' }, @@ -53,7 +55,13 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const [captchaLoading, setCaptchaLoading] = useState(true); const [submissionLoading, setSubmissionLoading] = useState(false); - const [params, setParams] = useState(ImmutableMap()); + const [params, setParams] = useState({ + username: '', + email: '', + password: '', + agreement: false, + locale: '', + }); const [captchaIdempotencyKey, setCaptchaIdempotencyKey] = useState(uuidv4()); const [usernameUnavailable, setUsernameUnavailable] = useState(false); const [passwordConfirmation, setPasswordConfirmation] = useState(''); @@ -61,39 +69,35 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const controller = useRef(new AbortController()); - const updateParams = (map: any) => { - setParams(params.merge(ImmutableMap(map))); - }; - const onInputChange: React.ChangeEventHandler = e => { - updateParams({ [e.target.name]: e.target.value }); + setParams(params => ({ ...params, [e.target.name]: e.target.value })); }; const onUsernameChange: React.ChangeEventHandler = e => { - updateParams({ username: e.target.value }); + setParams(params => ({ ...params, username: e.target.value })); setUsernameUnavailable(false); controller.current.abort(); controller.current = new AbortController(); - const domain = params.get('domain'); + const domain = params.domain; usernameAvailable(e.target.value, domain ? domains!.find(({ id }) => id === domain)?.domain : undefined); }; const onDomainChange: React.ChangeEventHandler = e => { - updateParams({ domain: e.target.value || null }); + setParams(params => ({ ...params, domain: e.target.value || undefined })); setUsernameUnavailable(false); controller.current.abort(); controller.current = new AbortController(); - const username = params.get('username'); + const username = params.username; if (username) { usernameAvailable(username, domains!.find(({ id }) => id === e.target.value)?.domain); } }; const onCheckboxChange: React.ChangeEventHandler = e => { - updateParams({ [e.target.name]: e.target.checked }); + setParams(params => ({ ...params, [e.target.name]: e.target.checked })); }; const onPasswordChange: React.ChangeEventHandler = e => { @@ -106,7 +110,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { }; const onPasswordConfirmChange: React.ChangeEventHandler = e => { - const password = params.get('password', ''); + const password = params.password || ''; const passwordConfirmation = e.target.value; setPasswordConfirmation(passwordConfirmation); @@ -120,7 +124,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { }; const onBirthdayChange = (birthday: string) => { - updateParams({ birthday }); + setParams(params => ({ ...params, birthday })); }; const launchModal = () => { @@ -129,7 +133,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { {params.get('email')} }} + values={{ email: {params.email} }} />

} {needsApproval &&

= ({ inviteToken }) => { } }; - const passwordsMatch = () => params.get('password', '') === passwordConfirmation; + const passwordsMatch = () => params.password === passwordConfirmation; const usernameAvailable = useCallback(debounce((username, domain?: string) => { if (!supportsAccountLookup) return; @@ -186,19 +190,18 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { return; } - const normalParams = params.withMutations(params => { - // Locale for confirmation email - params.set('locale', locale); + const normalParams = { + ...params, + locale, + }; - // Pleroma invites - if (inviteToken) { - params.set('token', inviteToken); - } - }); + if (inviteToken) { + params.token = inviteToken; + } setSubmissionLoading(true); - dispatch(register(normalParams.toJS())) + dispatch(register(normalParams)) .then(postRegisterAction) .catch(() => { setSubmissionLoading(false); @@ -212,10 +215,11 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const onFetchCaptcha = (captcha: ImmutableMap) => { setCaptchaLoading(false); - updateParams({ + setParams(params => ({ + ...params, captcha_token: captcha.get('token'), captcha_answer_data: captcha.get('answer_data'), - }); + })); }; const onFetchCaptchaFail = () => { @@ -224,7 +228,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const refreshCaptcha = () => { setCaptchaIdempotencyKey(uuidv4()); - updateParams({ captcha_solution: '' }); + setParams(params => ({ ...params, captcha_solution: '' })); }; const isLoading = captchaLoading || submissionLoading; @@ -247,7 +251,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { pattern='^[a-zA-Z\d_-]+' icon={require('@tabler/icons/outline/at.svg')} onChange={onUsernameChange} - value={params.get('username', '')} + value={params.username} required /> @@ -256,7 +260,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => {