Refactor server reducer into TypeScript (#39089)

This commit is contained in:
Eugen Rochko authored and GitHub committed 2026-05-20 14:06:38 +00:00
1 parent f5b57e8ba7
commit 076c8ec51e
26 files changed
+390 -304

No files matched your search

@@ -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(
@@ -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>
@@ -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 ?? [],
@@ -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(
@@ -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();
@@ -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,
}),
);
@@ -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(
@@ -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 ?? '');
@@ -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;
@@ -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' />;
@@ -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) {
@@ -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) {
@@ -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<