Refactor server reducer into TypeScript (#39089)

This commit is contained in:
Eugen Rochko
2026-05-20 16:06:38 +02:00
committed by GitHub
parent f5b57e8ba7
commit 076c8ec51e
26 changed files with 390 additions and 304 deletions

View File

@@ -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,

View File

@@ -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,
});

View File

@@ -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(),
);

View File

@@ -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,

View File

@@ -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<ApiPrivacyPolicyJSON>('v1/instance/privacy_policy');
export const apiGetInstance = () =>
apiRequestGet<ApiInstanceJSON>('v2/instance');
export const apiGetExtendedDescription = () =>
apiRequestGet<ApiExtendedDescriptionJSON>('v1/instance/extended_description');
export const apiGetTranslationLanguages = () =>
apiRequestGet<ApiTranslationLanguagesJSON>(
'v1/instance/translation_languages',
);
export const apiGetDomainBlocks = () =>
apiRequestGet<ApiDomainBlockJSON[]>('v1/instance/domain_blocks');

View File

@@ -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<string, ApiBaseRuleJSON>;
}
export interface ApiExtendedDescriptionJSON {
updated_at: string;
content: string;
}
export interface ApiDomainBlockJSON {
domain: string;
digest: string;
severity: string;
comment: string;
}
export type ApiTranslationLanguagesJSON = Record<string, string[]>;
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<string, string>;
};
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;
};
}

View File

@@ -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 (
<div className='server-banner'>
@@ -50,8 +50,8 @@ class ServerBanner extends PureComponent {
<NavLink to='/about'>
<ServerHeroImage
blurhash={server.getIn(['thumbnail', 'blurhash'])}
src={server.getIn(['thumbnail', 'url'])}
blurhash={server.item?.thumbnail.blurhash}
src={server.item?.thumbnail.url}
alt={intl.formatMessage(messages.aboutThisServer)}
className='server-banner__hero'
/>
@@ -66,14 +66,14 @@ class ServerBanner extends PureComponent {
<br />
<Skeleton width='70%' />
</>
) : server.get('description')}
) : server.item?.description}
</div>
<div className='server-banner__meta'>
<div className='server-banner__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
<Account id={server.item?.contact.account?.id} size={36} minimal />
</div>
<div className='server-banner__meta__column'>
@@ -87,7 +87,7 @@ class ServerBanner extends PureComponent {
</>
) : (
<>
<strong className='server-banner__number'><ShortNumber value={server.getIn(['usage', 'users', 'active_month'])} /></strong>
<strong className='server-banner__number'><ShortNumber value={server.item?.usage.users.active_month} /></strong>
<br />
<span className='server-banner__number-label' title={intl.formatMessage(messages.aboutActiveUsers)}><FormattedMessage id='server_banner.active_users' defaultMessage='active users' /></span>
</>

View File

@@ -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 {

View File

@@ -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<RulesSectionProps> = ({ isLoading = false }) => {
};
const selectRules = (state: RootState) => {
const rules = state.server.getIn([
'server',
'rules',
]) as ImmutableList<Rule> | null;
const rules = state.server.server.item?.rules;
if (!rules) {
return [];
}
return rules.toJS() as Rule[];
return rules;
};
const rulesSelector = createSelector(

View File

@@ -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 (
<Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.title)}>
@@ -84,13 +84,13 @@ class About extends PureComponent {
<div className='about__header'>
<ServerHeroImage
withAltBadge
alt={server.getIn(['thumbnail', 'description']) ?? ''}
blurhash={server.getIn(['thumbnail', 'blurhash'])}
src={server.getIn(['thumbnail', 'url'])}
srcSet={server.getIn(['thumbnail', 'versions'])?.map((value, key) => `${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'
/>
<h1>{isLoading ? <Skeleton width='10ch' /> : server.get('domain')}</h1>
<h1>{isLoading ? <Skeleton width='10ch' /> : server.domain}</h1>
<p><FormattedMessage id='about.powered_by' defaultMessage='Decentralized social media powered by {mastodon}' values={{ mastodon: <a href='https://joinmastodon.org' className='about__mail' target='_blank' rel='noopener'>Mastodon</a> }} /></p>
</div>
@@ -98,7 +98,7 @@ class About extends PureComponent {
<div className='about__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
<Account id={server.item?.contact?.account?.id} size={36} minimal />
</div>
<hr className='about__meta__divider' />
@@ -106,12 +106,12 @@ class About extends PureComponent {
<div className='about__meta__column'>
<h4><FormattedMessage id='about.contact' defaultMessage='Contact:' /></h4>
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.getIn(['contact', 'email'])}`}>{server.getIn(['contact', 'email'])}</a>}
{isLoading ? <Skeleton width='10ch' /> : <a className='about__mail' href={`mailto:${server.item?.contact?.email}`}>{server.item?.contact?.email}</a>}
</div>
</div>
<Section open title={intl.formatMessage(messages.title)}>
{extendedDescription.get('isLoading') ? (
{extendedDescription.isLoading ? (
<>
<Skeleton width='100%' />
<br />
@@ -121,10 +121,10 @@ class About extends PureComponent {
<br />
<Skeleton width='70%' />
</>
) : (extendedDescription.get('content')?.length > 0 ? (
) : (extendedDescription.item?.content?.length > 0 ? (
<div
className='prose'
dangerouslySetInnerHTML={{ __html: extendedDescription.get('content') }}
dangerouslySetInnerHTML={{ __html: extendedDescription.item?.content }}
/>
) : (
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
@@ -134,26 +134,26 @@ class About extends PureComponent {
<RulesSection />
<Section title={intl.formatMessage(messages.blocks)} onOpen={this.handleDomainBlocksOpen}>
{domainBlocks.get('isLoading') ? (
{domainBlocks.isLoading ? (
<>
<Skeleton width='100%' />
<br />
<Skeleton width='70%' />
</>
) : (domainBlocks.get('isAvailable') ? (
) : (domainBlocks.isAvailable ? (
<>
<p><FormattedMessage id='about.domain_blocks.preamble' defaultMessage='Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.' /></p>
{domainBlocks.get('items').size > 0 && (
{domainBlocks.items.length > 0 && (
<div className='about__domain-blocks'>
{domainBlocks.get('items').map(block => (
<div className='about__domain-blocks__domain' key={block.get('domain')}>
{domainBlocks.items.map(block => (
<div className='about__domain-blocks__domain' key={block.domain}>
<div className='about__domain-blocks__domain__header'>
<h6><span title={`SHA-256: ${block.get('digest')}`}>{block.get('domain')}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.get('severity')].explanation)}>{intl.formatMessage(severityMessages[block.get('severity')].title)}</span>
<h6><span title={`SHA-256: ${block.digest}`}>{block.domain}</span></h6>
<span className='about__domain-blocks__domain__type' title={intl.formatMessage(severityMessages[block.severity].explanation)}>{intl.formatMessage(severityMessages[block.severity].title)}</span>
</div>
<p>{(block.get('comment') || '').length > 0 ? block.get('comment') : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
<p>{(block.comment ?? '').length > 0 ? block.comment : <FormattedMessage id='about.domain_blocks.no_reason_available' defaultMessage='Reason not available' />}</p>
</div>
))}
</div>

View File

@@ -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 ?? [],

View File

@@ -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(

View File

@@ -36,13 +36,7 @@ export const BioModal: FC<BaseConfirmationModalProps> = ({ 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();

View File

@@ -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<string, number>
| 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,
}),
);

View File

@@ -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<HTMLTextAreaElement> = useCallback(

View File

@@ -33,12 +33,7 @@ export const NameModal: FC<BaseConfirmationModalProps> = ({ 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 ?? '');

View File

@@ -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;

View File

@@ -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 = <DisplayName account={account} variant='simple' />;

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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<

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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<string, string[]>;
};
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;
});
});

View File

@@ -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');
}