diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index f487545444..06335f9fbf 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -99,7 +99,7 @@ export const ensureComposeIsVisible = (getState) => { export function setComposeToStatus(status, text, spoiler_text) { return (dispatch, getState) => { - const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); + const maxOptions = getState().server.server.item?.configuration.polls.max_options; dispatch({ type: COMPOSE_SET_STATUS, diff --git a/app/javascript/mastodon/actions/server.js b/app/javascript/mastodon/actions/server.js deleted file mode 100644 index c291eb772a..0000000000 --- a/app/javascript/mastodon/actions/server.js +++ /dev/null @@ -1,139 +0,0 @@ -import api from '../api'; - -import { importFetchedAccount } from './importer'; - -export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST'; -export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS'; -export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL'; - -export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST'; -export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS'; -export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL'; - -export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST'; -export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS'; -export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL'; - -export const SERVER_DOMAIN_BLOCKS_FETCH_REQUEST = 'SERVER_DOMAIN_BLOCKS_FETCH_REQUEST'; -export const SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS = 'SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS'; -export const SERVER_DOMAIN_BLOCKS_FETCH_FAIL = 'SERVER_DOMAIN_BLOCKS_FETCH_FAIL'; - -export const fetchServer = () => (dispatch, getState) => { - if (getState().getIn(['server', 'server', 'isLoading'])) { - return; - } - - dispatch(fetchServerRequest()); - - api() - .get('/api/v2/instance').then(({ data }) => { - // Only import the account if it doesn't already exist, - // because the API is cached even for logged in users. - const account = data.contact.account; - if (account) { - const existingAccount = getState().getIn(['accounts', account.id]); - if (!existingAccount) { - dispatch(importFetchedAccount(account)); - } - } - dispatch(fetchServerSuccess(data)); - }).catch(err => dispatch(fetchServerFail(err))); -}; - -const fetchServerRequest = () => ({ - type: SERVER_FETCH_REQUEST, -}); - -const fetchServerSuccess = server => ({ - type: SERVER_FETCH_SUCCESS, - server, -}); - -const fetchServerFail = error => ({ - type: SERVER_FETCH_FAIL, - error, -}); - -export const fetchServerTranslationLanguages = () => (dispatch) => { - dispatch(fetchServerTranslationLanguagesRequest()); - - api() - .get('/api/v1/instance/translation_languages').then(({ data }) => { - dispatch(fetchServerTranslationLanguagesSuccess(data)); - }).catch(err => dispatch(fetchServerTranslationLanguagesFail(err))); -}; - -const fetchServerTranslationLanguagesRequest = () => ({ - type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST, -}); - -const fetchServerTranslationLanguagesSuccess = translationLanguages => ({ - type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS, - translationLanguages, -}); - -const fetchServerTranslationLanguagesFail = error => ({ - type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL, - error, -}); - -export const fetchExtendedDescription = () => (dispatch, getState) => { - if (getState().getIn(['server', 'extendedDescription', 'isLoading'])) { - return; - } - - dispatch(fetchExtendedDescriptionRequest()); - - api() - .get('/api/v1/instance/extended_description') - .then(({ data }) => dispatch(fetchExtendedDescriptionSuccess(data))) - .catch(err => dispatch(fetchExtendedDescriptionFail(err))); -}; - -const fetchExtendedDescriptionRequest = () => ({ - type: EXTENDED_DESCRIPTION_REQUEST, -}); - -const fetchExtendedDescriptionSuccess = description => ({ - type: EXTENDED_DESCRIPTION_SUCCESS, - description, -}); - -const fetchExtendedDescriptionFail = error => ({ - type: EXTENDED_DESCRIPTION_FAIL, - error, -}); - -export const fetchDomainBlocks = () => (dispatch, getState) => { - if (getState().getIn(['server', 'domainBlocks', 'isLoading'])) { - return; - } - - dispatch(fetchDomainBlocksRequest()); - - api() - .get('/api/v1/instance/domain_blocks') - .then(({ data }) => dispatch(fetchDomainBlocksSuccess(true, data))) - .catch(err => { - if (err.response.status === 404) { - dispatch(fetchDomainBlocksSuccess(false, [])); - } else { - dispatch(fetchDomainBlocksFail(err)); - } - }); -}; - -const fetchDomainBlocksRequest = () => ({ - type: SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, -}); - -const fetchDomainBlocksSuccess = (isAvailable, blocks) => ({ - type: SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, - isAvailable, - blocks, -}); - -const fetchDomainBlocksFail = error => ({ - type: SERVER_DOMAIN_BLOCKS_FETCH_FAIL, - error, -}); diff --git a/app/javascript/mastodon/actions/server.ts b/app/javascript/mastodon/actions/server.ts new file mode 100644 index 0000000000..4e4795c123 --- /dev/null +++ b/app/javascript/mastodon/actions/server.ts @@ -0,0 +1,34 @@ +import { + apiGetInstance, + apiGetExtendedDescription, + apiGetDomainBlocks, + apiGetTranslationLanguages, +} from 'mastodon/api/instance'; +import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; + +import { importFetchedAccount } from './importer'; + +export const fetchServer = createDataLoadingThunk( + 'server/fetch', + () => apiGetInstance(), + (instance, { dispatch }) => { + if (instance.contact.account) { + dispatch(importFetchedAccount(instance.contact.account)); + } + }, +); + +export const fetchExtendedDescription = createDataLoadingThunk( + 'server/extended_description', + () => apiGetExtendedDescription(), +); + +export const fetchServerTranslationLanguages = createDataLoadingThunk( + 'server/translation_languages', + () => apiGetTranslationLanguages(), +); + +export const fetchDomainBlocks = createDataLoadingThunk( + 'server/domain_blocks', + () => apiGetDomainBlocks(), +); diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index d714ed3849..109d676bfb 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -111,7 +111,7 @@ export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) { export function redraft(status, raw_text, quoted_status_id = null) { return (dispatch, getState) => { - const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']); + const maxOptions = getState().server.server.item?.configuration.polls.max_options; dispatch({ type: REDRAFT, diff --git a/app/javascript/mastodon/api/instance.ts b/app/javascript/mastodon/api/instance.ts index 764e8daab2..b3f04f12d1 100644 --- a/app/javascript/mastodon/api/instance.ts +++ b/app/javascript/mastodon/api/instance.ts @@ -2,6 +2,10 @@ import { apiRequestGet } from 'mastodon/api'; import type { ApiTermsOfServiceJSON, ApiPrivacyPolicyJSON, + ApiInstanceJSON, + ApiExtendedDescriptionJSON, + ApiTranslationLanguagesJSON, + ApiDomainBlockJSON, } from 'mastodon/api_types/instance'; export const apiGetTermsOfService = (version?: string) => @@ -13,3 +17,17 @@ export const apiGetTermsOfService = (version?: string) => export const apiGetPrivacyPolicy = () => apiRequestGet('v1/instance/privacy_policy'); + +export const apiGetInstance = () => + apiRequestGet('v2/instance'); + +export const apiGetExtendedDescription = () => + apiRequestGet('v1/instance/extended_description'); + +export const apiGetTranslationLanguages = () => + apiRequestGet( + 'v1/instance/translation_languages', + ); + +export const apiGetDomainBlocks = () => + apiRequestGet('v1/instance/domain_blocks'); diff --git a/app/javascript/mastodon/api_types/instance.ts b/app/javascript/mastodon/api_types/instance.ts index 3a29684b70..361dade4c7 100644 --- a/app/javascript/mastodon/api_types/instance.ts +++ b/app/javascript/mastodon/api_types/instance.ts @@ -1,3 +1,5 @@ +import type { ApiAccountJSON } from './accounts'; + export interface ApiTermsOfServiceJSON { effective_date: string; effective: boolean; @@ -9,3 +11,136 @@ export interface ApiPrivacyPolicyJSON { updated_at: string; content: string; } + +interface ApiBaseRuleJSON { + text: string; + hint: string; +} + +export interface ApiRuleJSON { + id: string; + text: string; + hint: string; + translations?: Record; +} + +export interface ApiExtendedDescriptionJSON { + updated_at: string; + content: string; +} + +export interface ApiDomainBlockJSON { + domain: string; + digest: string; + severity: string; + comment: string; +} + +export type ApiTranslationLanguagesJSON = Record; + +export interface ApiInstanceJSON { + domain: string; + title: string; + version: string; + source_url: string; + description: string; + languages: string[]; + usage: { + users: { + active_month: number; + }; + }; + thumbnail: { + url: string; + blurhash?: string; + description: string; + versions?: Record; + }; + contact: { + email: string | null; + account: ApiAccountJSON | null; + }; + api_versions: { + mastodon: number; + }; + registrations: { + enabled: boolean; + approval_required: boolean; + reason_required: boolean | null; + message: string | null; + min_age: string | null; + url: string | null; + }; + rules: ApiRuleJSON[]; + configuration: { + urls: { + streaming: string; + status: string | null; + about: string; + privacy_policy: string | null; + terms_of_service: string | null; + }; + + vapid: { + public_key: string; + }; + + accounts: { + max_display_name_length: number; + max_note_length: number; + max_avatar_description_length: number; + max_header_description_length: number; + max_featured_tags: number; + max_pinned_statuses: number; + max_profile_fields: number; + profile_field_name_limit: number; + profile_field_value_limit: number; + }; + + statuses: { + max_characters: number; + max_media_attachments: number; + characters_reserved_per_url: number; + }; + + media_attachments: { + description_limit: number; + image_matrix_limit: number; + image_size_limit: number; + supported_mime_types: string[]; + video_frame_rate_limit: number; + video_matrix_limit: number; + video_size_limit: number; + }; + + polls: { + max_options: number; + max_characters_per_option: number; + min_expiration: number; + max_expiration: number; + }; + + translation: { + enabled: boolean; + }; + + timeline_access: { + live_feeds: { + local: string; + remote: string; + }; + + hashtag_feeds: { + local: string; + remote: string; + }; + + trending_link_feeds: { + local: string; + remote: string; + }; + }; + + limited_federation: boolean; + }; +} diff --git a/app/javascript/mastodon/components/server_banner.jsx b/app/javascript/mastodon/components/server_banner.jsx index 939a92b281..b5484a1a37 100644 --- a/app/javascript/mastodon/components/server_banner.jsx +++ b/app/javascript/mastodon/components/server_banner.jsx @@ -22,7 +22,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - server: state.getIn(['server', 'server']), + server: state.server.server, }); class ServerBanner extends PureComponent { @@ -40,7 +40,7 @@ class ServerBanner extends PureComponent { render () { const { server, intl } = this.props; - const isLoading = server.get('isLoading'); + const isLoading = server.isLoading; return (
@@ -50,8 +50,8 @@ class ServerBanner extends PureComponent { @@ -66,14 +66,14 @@ class ServerBanner extends PureComponent {
- ) : server.get('description')} + ) : server.item?.description}

- +
@@ -87,7 +87,7 @@ class ServerBanner extends PureComponent { ) : ( <> - +
diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 920bdbf14e..2b61499d65 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -69,7 +69,7 @@ class TranslateButton extends PureComponent { } const mapStateToProps = state => ({ - languages: state.getIn(['server', 'translationLanguages', 'items']), + languages: state.server.translationLanguages.items, }); class StatusContent extends PureComponent { diff --git a/app/javascript/mastodon/features/about/components/rules.tsx b/app/javascript/mastodon/features/about/components/rules.tsx index 52339329fe..e8bde723ca 100644 --- a/app/javascript/mastodon/features/about/components/rules.tsx +++ b/app/javascript/mastodon/features/about/components/rules.tsx @@ -5,7 +5,6 @@ import type { IntlShape } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { createSelector } from '@reduxjs/toolkit'; -import type { List as ImmutableList } from 'immutable'; import type { SelectItem } from '@/mastodon/components/dropdown_selector'; import { Select } from '@/mastodon/components/form_fields'; @@ -123,14 +122,13 @@ export const RulesSection: FC = ({ isLoading = false }) => { }; const selectRules = (state: RootState) => { - const rules = state.server.getIn([ - 'server', - 'rules', - ]) as ImmutableList | null; + const rules = state.server.server.item?.rules; + if (!rules) { return []; } - return rules.toJS() as Rule[]; + + return rules; }; const rulesSelector = createSelector( diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 9814032cac..4c1ee5254a 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -41,10 +41,10 @@ const severityMessages = { }; const mapStateToProps = state => ({ - server: state.getIn(['server', 'server']), + server: state.server.server, locale: state.getIn(['meta', 'locale']), - extendedDescription: state.getIn(['server', 'extendedDescription']), - domainBlocks: state.getIn(['server', 'domainBlocks']), + extendedDescription: state.server.extendedDescription, + domainBlocks: state.server.domainBlocks, }); class About extends PureComponent { @@ -76,7 +76,7 @@ class About extends PureComponent { render () { const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props; - const isLoading = server.get('isLoading'); + const isLoading = server.isLoading; return ( @@ -84,13 +84,13 @@ class About extends PureComponent {
`${value} ${key.replace('@', '')}`).join(', ')} + alt={server.item?.thumbnail.description ?? ''} + blurhash={server.item?.thumbnail.blurhash} + src={server.item?.thumbnail.url} + srcSet={Object.keys(server.item?.thumbnail.versions ?? {}).map((key) => `${server.item?.thumbnail.versions && server.item.thumbnail.versions[key]} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> -

{isLoading ? : server.get('domain')}

+

{isLoading ? : server.domain}

Mastodon }} />

@@ -98,7 +98,7 @@ class About extends PureComponent {

- +

@@ -106,12 +106,12 @@ class About extends PureComponent {
- {extendedDescription.get('isLoading') ? ( + {extendedDescription.isLoading ? ( <>
@@ -121,10 +121,10 @@ class About extends PureComponent {
- ) : (extendedDescription.get('content')?.length > 0 ? ( + ) : (extendedDescription.item?.content?.length > 0 ? (
) : (

@@ -134,26 +134,26 @@ class About extends PureComponent {
- {domainBlocks.get('isLoading') ? ( + {domainBlocks.isLoading ? ( <>
- ) : (domainBlocks.get('isAvailable') ? ( + ) : (domainBlocks.isAvailable ? ( <>

- {domainBlocks.get('items').size > 0 && ( + {domainBlocks.items.length > 0 && (
- {domainBlocks.get('items').map(block => ( -
+ {domainBlocks.items.map(block => ( +
-
{block.get('domain')}
- {intl.formatMessage(severityMessages[block.get('severity')].title)} +
{block.domain}
+ {intl.formatMessage(severityMessages[block.severity].title)}
-

{(block.get('comment') || '').length > 0 ? block.get('comment') : }

+

{(block.comment ?? '').length > 0 ? block.comment : }

))}
diff --git a/app/javascript/mastodon/features/account_edit/featured_tags.tsx b/app/javascript/mastodon/features/account_edit/featured_tags.tsx index 5fc602208d..fedfde62e3 100644 --- a/app/javascript/mastodon/features/account_edit/featured_tags.tsx +++ b/app/javascript/mastodon/features/account_edit/featured_tags.tsx @@ -37,10 +37,7 @@ const selectTags = createAppSelector( [ (state) => state.profileEdit, (state) => - state.server.getIn( - ['server', 'accounts', 'max_featured_tags'], - 10, - ) as number, + state.server.server.item?.configuration.accounts.max_featured_tags ?? 0, ], (profileEdit, maxTags) => ({ tags: profileEdit.profile?.featuredTags ?? [], diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index 0199c3efa4..d00473043e 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -133,12 +133,7 @@ export const AccountEdit: FC = () => { const maxFieldCount = useAppSelector( (state) => - (state.server.getIn([ - 'server', - 'configuration', - 'accounts', - 'max_profile_fields', - ]) as number | undefined) ?? 4, + state.server.server.item?.configuration.accounts.max_profile_fields ?? 4, ); const handleOpenModal = useCallback( diff --git a/app/javascript/mastodon/features/account_edit/modals/bio_modal.tsx b/app/javascript/mastodon/features/account_edit/modals/bio_modal.tsx index ff4febeebb..8d286dedce 100644 --- a/app/javascript/mastodon/features/account_edit/modals/bio_modal.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/bio_modal.tsx @@ -36,13 +36,7 @@ export const BioModal: FC = ({ onClose }) => { ); const [newBio, setNewBio] = useState(bio ?? ''); const maxLength = useAppSelector( - (state) => - state.server.getIn([ - 'server', - 'configuration', - 'accounts', - 'max_note_length', - ]) as number | undefined, + (state) => state.server.server.item?.configuration.accounts.max_note_length, ); const dispatch = useAppDispatch(); diff --git a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx index 98324cf4dd..9fc26ac525 100644 --- a/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/fields_modals.tsx @@ -9,8 +9,6 @@ import type { FC, FocusEventHandler } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import type { Map as ImmutableMap } from 'immutable'; - import { closeModal } from '@/mastodon/actions/modal'; import { Button } from '@/mastodon/components/button'; import type { FieldStatus } from '@/mastodon/components/form_fields'; @@ -97,15 +95,10 @@ const messages = defineMessages({ // We have two different values- the hard limit set by the server, // and the soft limit for mobile display. const selectFieldLimits = createAppSelector( - [ - (state) => - state.server.getIn(['server', 'configuration', 'accounts']) as - | ImmutableMap - | undefined, - ], + [(state) => state.server.server.item?.configuration.accounts], (accounts) => ({ - nameLimit: accounts?.get('profile_field_name_limit'), - valueLimit: accounts?.get('profile_field_value_limit'), + nameLimit: accounts?.profile_field_name_limit, + valueLimit: accounts?.profile_field_value_limit, }), ); diff --git a/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx b/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx index d9d014f792..61f5d5ffdd 100644 --- a/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/image_alt.tsx @@ -84,15 +84,8 @@ export const ImageAltTextField: FC<{ }> = ({ imageSrc, altText, onChange, hideTip }) => { const altLimit = useAppSelector( (state) => - state.server.getIn( - [ - 'server', - 'configuration', - 'accounts', - 'max_header_description_length', - ], - 150, - ) as number, + state.server.server.item?.configuration.accounts + .max_header_description_length ?? 0, ); const handleChange: ChangeEventHandler = useCallback( diff --git a/app/javascript/mastodon/features/account_edit/modals/name_modal.tsx b/app/javascript/mastodon/features/account_edit/modals/name_modal.tsx index 07779a02a7..de42977140 100644 --- a/app/javascript/mastodon/features/account_edit/modals/name_modal.tsx +++ b/app/javascript/mastodon/features/account_edit/modals/name_modal.tsx @@ -33,12 +33,7 @@ export const NameModal: FC = ({ onClose }) => { ); const maxLength = useAppSelector( (state) => - state.server.getIn([ - 'server', - 'configuration', - 'accounts', - 'max_display_name_length', - ]) as number | undefined, + state.server.server.item?.configuration.accounts.max_display_name_length, ); const [newName, setNewName] = useState(displayName ?? ''); diff --git a/app/javascript/mastodon/features/account_timeline/modals/join_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/join_modal.tsx index e5401fbebe..40e2b82bca 100644 --- a/app/javascript/mastodon/features/account_timeline/modals/join_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/modals/join_modal.tsx @@ -31,7 +31,7 @@ const selectServerName = createAppSelector( [ (state) => state.accounts, (_, accountId: string) => accountId, - (state) => state.server.getIn(['server', 'domain']) as string | undefined, + (state) => state.server.server.item?.domain, ], (accounts, accountId, serverDomain) => { const acct = accounts.getIn([accountId, 'acct']) as string | undefined; diff --git a/app/javascript/mastodon/features/interaction_modal/index.tsx b/app/javascript/mastodon/features/interaction_modal/index.tsx index a4ba40c069..ef1d3e1111 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.tsx +++ b/app/javascript/mastodon/features/interaction_modal/index.tsx @@ -419,9 +419,7 @@ const InteractionModal: React.FC<{ }> = ({ accountId, url, intent }) => { const dispatch = useAppDispatch(); const signupUrl = useAppSelector( - (state) => - (state.server.getIn(['server', 'registrations', 'url'], null) || - '/auth/sign_up') as string, + (state) => state.server.server.item?.registrations.url ?? '/auth/sign_up', ); const account = useAppSelector((state) => state.accounts.get(accountId)); const name = ; diff --git a/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx b/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx index ebcb532bb4..f9cf4cb28f 100644 --- a/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx +++ b/app/javascript/mastodon/features/navigation_panel/components/sign_in_banner.tsx @@ -20,10 +20,7 @@ export const SignInBanner: React.FC = () => { let signupButton: React.ReactNode; const signupUrl = useAppSelector( - (state) => - (state.server.getIn(['server', 'registrations', 'url'], null) as - | string - | null) ?? '/auth/sign_up', + (state) => state.server.server.item?.registrations.url ?? '/auth/sign_up', ); if (sso_redirect) { diff --git a/app/javascript/mastodon/features/ui/components/navigation_bar.tsx b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx index def1835c3e..ec45e395b2 100644 --- a/app/javascript/mastodon/features/ui/components/navigation_bar.tsx +++ b/app/javascript/mastodon/features/ui/components/navigation_bar.tsx @@ -84,10 +84,7 @@ const NotificationsButton = () => { const LoginOrSignUp: React.FC = () => { const dispatch = useAppDispatch(); const signupUrl = useAppSelector( - (state) => - (state.server.getIn(['server', 'registrations', 'url'], null) as - | string - | null) ?? '/auth/sign_up', + (state) => state.server.server.item?.registrations.url ?? '/auth/sign_up', ); const openClosedRegistrationsModal = useCallback(() => { @@ -95,7 +92,7 @@ const LoginOrSignUp: React.FC = () => { }, [dispatch]); useEffect(() => { - dispatch(fetchServer()); + void dispatch(fetchServer()); }, [dispatch]); if (sso_redirect) { diff --git a/app/javascript/mastodon/features/ui/components/report_collection_modal.tsx b/app/javascript/mastodon/features/ui/components/report_collection_modal.tsx index 945d6f0534..6e9492226d 100644 --- a/app/javascript/mastodon/features/ui/components/report_collection_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/report_collection_modal.tsx @@ -52,7 +52,7 @@ export const ReportCollectionModal: React.FC<{ const account = useAccount(account_id); useEffect(() => { - dispatch(fetchServer()); + void dispatch(fetchServer()); }, [dispatch]); const [submitState, setSubmitState] = useState< diff --git a/app/javascript/mastodon/models/server.ts b/app/javascript/mastodon/models/server.ts new file mode 100644 index 0000000000..0a01a56962 --- /dev/null +++ b/app/javascript/mastodon/models/server.ts @@ -0,0 +1,21 @@ +import type { + ApiInstanceJSON, + ApiExtendedDescriptionJSON, + ApiDomainBlockJSON, +} from 'mastodon/api_types/instance'; + +export type Server = ApiInstanceJSON; + +export const createServerFromServerJSON = (obj: ApiInstanceJSON): Server => obj; + +export type ExtendedDescription = ApiExtendedDescriptionJSON; + +export const createExtendedDescriptionFromServerJSON = ( + obj: ApiExtendedDescriptionJSON, +): ExtendedDescription => obj; + +export type DomainBlock = ApiDomainBlockJSON; + +export const createDomainBlockFromServerJSON = ( + obj: ApiDomainBlockJSON, +): DomainBlock => obj; diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index c3bc5792ac..2d2c8fbab5 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -30,7 +30,7 @@ import { pollsReducer } from './polls'; import push_notifications from './push_notifications'; import { relationshipsReducer } from './relationships'; import { searchReducer } from './search'; -import server from './server'; +import { serverReducer } from './server'; import settings from './settings'; import { sliceReducers } from './slices'; import status_lists from './status_lists'; @@ -58,7 +58,7 @@ const reducers = { relationships: relationshipsReducer, settings, push_notifications, - server, + server: serverReducer, contexts: contextsReducer, compose: composeReducer, search: searchReducer, diff --git a/app/javascript/mastodon/reducers/server.js b/app/javascript/mastodon/reducers/server.js deleted file mode 100644 index 2bbf0f9a30..0000000000 --- a/app/javascript/mastodon/reducers/server.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; - -import { - SERVER_FETCH_REQUEST, - SERVER_FETCH_SUCCESS, - SERVER_FETCH_FAIL, - SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST, - SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS, - SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL, - EXTENDED_DESCRIPTION_REQUEST, - EXTENDED_DESCRIPTION_SUCCESS, - EXTENDED_DESCRIPTION_FAIL, - SERVER_DOMAIN_BLOCKS_FETCH_REQUEST, - SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS, - SERVER_DOMAIN_BLOCKS_FETCH_FAIL, -} from 'mastodon/actions/server'; - -const initialState = ImmutableMap({ - server: ImmutableMap({ - isLoading: false, - }), - - extendedDescription: ImmutableMap({ - isLoading: false, - }), - - domainBlocks: ImmutableMap({ - isLoading: false, - isAvailable: true, - items: ImmutableList(), - }), -}); - -export default function server(state = initialState, action) { - switch (action.type) { - case SERVER_FETCH_REQUEST: - return state.setIn(['server', 'isLoading'], true); - case SERVER_FETCH_SUCCESS: - return state.set('server', fromJS(action.server)).setIn(['server', 'isLoading'], false); - case SERVER_FETCH_FAIL: - return state.setIn(['server', 'isLoading'], false); - case SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST: - return state.setIn(['translationLanguages', 'isLoading'], true); - case SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS: - return state.setIn(['translationLanguages', 'items'], fromJS(action.translationLanguages)).setIn(['translationLanguages', 'isLoading'], false); - case SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL: - return state.setIn(['translationLanguages', 'isLoading'], false); - case EXTENDED_DESCRIPTION_REQUEST: - return state.setIn(['extendedDescription', 'isLoading'], true); - case EXTENDED_DESCRIPTION_SUCCESS: - return state.set('extendedDescription', fromJS(action.description)).setIn(['extendedDescription', 'isLoading'], false); - case EXTENDED_DESCRIPTION_FAIL: - return state.setIn(['extendedDescription', 'isLoading'], false); - case SERVER_DOMAIN_BLOCKS_FETCH_REQUEST: - return state.setIn(['domainBlocks', 'isLoading'], true); - case SERVER_DOMAIN_BLOCKS_FETCH_SUCCESS: - return state.setIn(['domainBlocks', 'items'], fromJS(action.blocks)).setIn(['domainBlocks', 'isLoading'], false).setIn(['domainBlocks', 'isAvailable'], action.isAvailable); - case SERVER_DOMAIN_BLOCKS_FETCH_FAIL: - return state.setIn(['domainBlocks', 'isLoading'], false); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/server.ts b/app/javascript/mastodon/reducers/server.ts new file mode 100644 index 0000000000..92cec39af0 --- /dev/null +++ b/app/javascript/mastodon/reducers/server.ts @@ -0,0 +1,127 @@ +import { createReducer } from '@reduxjs/toolkit'; + +import { + fetchServer, + fetchServerTranslationLanguages, + fetchExtendedDescription, + fetchDomainBlocks, +} from 'mastodon/actions/server'; +import type { + Server, + ExtendedDescription, + DomainBlock, +} from 'mastodon/models/server'; +import { + createServerFromServerJSON, + createExtendedDescriptionFromServerJSON, + createDomainBlockFromServerJSON, +} from 'mastodon/models/server'; + +interface State { + server: { + isLoading: boolean; + item?: Server; + }; + + extendedDescription: { + isLoading: boolean; + item?: ExtendedDescription; + }; + + translationLanguages: { + isLoading: boolean; + item?: Record; + }; + + domainBlocks: { + isLoading: boolean; + isAvailable: boolean; + items: DomainBlock[]; + }; +} + +const initialState: State = { + server: { + isLoading: false, + item: undefined, + }, + + extendedDescription: { + isLoading: false, + item: undefined, + }, + + translationLanguages: { + isLoading: false, + item: undefined, + }, + + domainBlocks: { + isLoading: false, + isAvailable: true, + items: [], + }, +}; + +export const serverReducer = createReducer(initialState, (builder) => { + builder.addCase(fetchServer.pending, (state) => { + state.server.isLoading = true; + }); + + builder.addCase(fetchServer.fulfilled, (state, action) => { + state.server.item = createServerFromServerJSON(action.payload); + state.server.isLoading = false; + }); + + builder.addCase(fetchServer.rejected, (state) => { + state.server.isLoading = false; + }); + + builder.addCase(fetchExtendedDescription.pending, (state) => { + state.extendedDescription.isLoading = true; + }); + + builder.addCase(fetchExtendedDescription.fulfilled, (state, action) => { + state.extendedDescription.item = createExtendedDescriptionFromServerJSON( + action.payload, + ); + state.extendedDescription.isLoading = false; + }); + + builder.addCase(fetchExtendedDescription.rejected, (state) => { + state.extendedDescription.isLoading = false; + }); + + builder.addCase(fetchServerTranslationLanguages.pending, (state) => { + state.translationLanguages.isLoading = true; + }); + + builder.addCase( + fetchServerTranslationLanguages.fulfilled, + (state, action) => { + state.translationLanguages.item = action.payload; + state.translationLanguages.isLoading = false; + }, + ); + + builder.addCase(fetchServerTranslationLanguages.rejected, (state) => { + state.translationLanguages.isLoading = false; + }); + + builder.addCase(fetchDomainBlocks.pending, (state) => { + state.domainBlocks.isLoading = true; + }); + + builder.addCase(fetchDomainBlocks.fulfilled, (state, action) => { + state.domainBlocks.items = action.payload.map((obj) => + createDomainBlockFromServerJSON(obj), + ); + state.domainBlocks.isLoading = false; + state.domainBlocks.isAvailable = true; + }); + + builder.addCase(fetchDomainBlocks.rejected, (state) => { + state.domainBlocks.isLoading = false; + state.domainBlocks.isAvailable = false; + }); +}); diff --git a/app/javascript/mastodon/reducers/slices/profile_edit.ts b/app/javascript/mastodon/reducers/slices/profile_edit.ts index 2437352fbd..bc463ac1bd 100644 --- a/app/javascript/mastodon/reducers/slices/profile_edit.ts +++ b/app/javascript/mastodon/reducers/slices/profile_edit.ts @@ -282,12 +282,8 @@ export const updateField = createAppAsyncThunk( throw new Error('Profile fields not found'); } - const maxFields = getState().server.getIn([ - 'server', - 'configuration', - 'accounts', - 'max_fields', - ]) as number | undefined; + const maxFields = + getState().server.server.item?.configuration.accounts.max_profile_fields; if (maxFields && fields.length >= maxFields && !arg.id) { throw new Error('Maximum number of profile fields reached'); }