Refactor server reducer into TypeScript (#39089)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
34
app/javascript/mastodon/actions/server.ts
Normal file
34
app/javascript/mastodon/actions/server.ts
Normal 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(),
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<
|
||||
|
||||
21
app/javascript/mastodon/models/server.ts
Normal file
21
app/javascript/mastodon/models/server.ts
Normal 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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
127
app/javascript/mastodon/reducers/server.ts
Normal file
127
app/javascript/mastodon/reducers/server.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user