Convert instance to use zod
This commit is contained in:
parent
970ad24de9
commit
3b630ed8fb
|
@ -1,11 +1,11 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildRelationship } from 'soapbox/jest/factory';
|
||||
import { buildInstance, buildRelationship } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { ListRecord, ReducerRecord } from 'soapbox/reducers/user-lists';
|
||||
|
||||
import { normalizeAccount, normalizeInstance } from '../../normalizers';
|
||||
import { normalizeAccount } from '../../normalizers';
|
||||
import {
|
||||
authorizeFollowRequest,
|
||||
blockAccount,
|
||||
|
@ -190,13 +190,13 @@ describe('fetchAccountByUsername()', () => {
|
|||
describe('when "accountByUsername" feature is enabled', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('instance', normalizeInstance({
|
||||
.set('instance', buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)',
|
||||
pleroma: ImmutableMap({
|
||||
metadata: ImmutableMap({
|
||||
pleroma: {
|
||||
metadata: {
|
||||
features: [],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
|
@ -253,13 +253,13 @@ describe('fetchAccountByUsername()', () => {
|
|||
describe('when "accountLookup" feature is enabled', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('instance', normalizeInstance({
|
||||
.set('instance', buildInstance({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
pleroma: ImmutableMap({
|
||||
metadata: ImmutableMap({
|
||||
pleroma: {
|
||||
metadata: {
|
||||
features: [],
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}))
|
||||
.set('me', '123');
|
||||
store = mockStore(state);
|
||||
|
|
|
@ -2,8 +2,9 @@ import { List as ImmutableList } from 'immutable';
|
|||
|
||||
import { fetchAnnouncements, dismissAnnouncement, addReaction, removeReaction } from 'soapbox/actions/announcements';
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildInstance } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { normalizeAnnouncement, normalizeInstance } from 'soapbox/normalizers';
|
||||
import { normalizeAnnouncement } from 'soapbox/normalizers';
|
||||
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -13,7 +14,7 @@ describe('fetchAnnouncements()', () => {
|
|||
describe('with a successful API request', () => {
|
||||
it('should fetch announcements from the API', async() => {
|
||||
const state = rootState
|
||||
.set('instance', normalizeInstance({ version: '3.5.3' }));
|
||||
.set('instance', buildInstance({ version: '3.5.3' }));
|
||||
const store = mockStore(state);
|
||||
|
||||
__stub((mock) => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
|
||||
import { buildInstance } from 'soapbox/jest/factory';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { InstanceRecord } from 'soapbox/normalizers';
|
||||
import { ReducerCompose } from 'soapbox/reducers/compose';
|
||||
|
||||
import { uploadCompose, submitCompose } from '../compose';
|
||||
|
@ -14,15 +14,15 @@ describe('uploadCompose()', () => {
|
|||
let files: FileList, store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const instance = InstanceRecord({
|
||||
configuration: ImmutableMap({
|
||||
statuses: ImmutableMap({
|
||||
const instance = buildInstance({
|
||||
configuration: {
|
||||
statuses: {
|
||||
max_media_attachments: 4,
|
||||
}),
|
||||
media_attachments: ImmutableMap({
|
||||
},
|
||||
media_attachments: {
|
||||
image_size_limit: 10,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const state = rootState
|
||||
|
@ -60,15 +60,15 @@ describe('uploadCompose()', () => {
|
|||
let files: FileList, store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const instance = InstanceRecord({
|
||||
configuration: ImmutableMap({
|
||||
statuses: ImmutableMap({
|
||||
const instance = buildInstance({
|
||||
configuration: {
|
||||
statuses: {
|
||||
max_media_attachments: 4,
|
||||
}),
|
||||
media_attachments: ImmutableMap({
|
||||
},
|
||||
media_attachments: {
|
||||
video_size_limit: 10,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const state = rootState
|
||||
|
|
|
@ -393,7 +393,7 @@ const submitComposeFail = (composeId: string, error: AxiosError) => ({
|
|||
const uploadCompose = (composeId: string, files: FileList, intl: IntlShape) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const attachmentLimit = getState().instance.configuration.getIn(['statuses', 'max_media_attachments']) as number;
|
||||
const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments;
|
||||
|
||||
const media = getState().compose.get(composeId)?.media_attachments;
|
||||
const progress = new Array(files.length).fill(0);
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import { createApp } from 'soapbox/actions/apps';
|
||||
import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { normalizeInstance } from 'soapbox/normalizers';
|
||||
import { instanceSchema, type Instance } from 'soapbox/schemas';
|
||||
import { parseBaseURL } from 'soapbox/utils/auth';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { getQuirks } from 'soapbox/utils/quirks';
|
||||
|
@ -18,17 +18,16 @@ import { getInstanceScopes } from 'soapbox/utils/scopes';
|
|||
import { baseClient } from '../api';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
|
||||
const fetchExternalInstance = (baseURL?: string) => {
|
||||
return baseClient(null, baseURL)
|
||||
.get('/api/v1/instance')
|
||||
.then(({ data: instance }) => normalizeInstance(instance))
|
||||
.then(({ data: instance }) => instanceSchema.parse(instance))
|
||||
.catch(error => {
|
||||
if (error.response?.status === 401) {
|
||||
// Authenticated fetch is enabled.
|
||||
// Continue with a limited featureset.
|
||||
return normalizeInstance({});
|
||||
return instanceSchema.parse({});
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
|
|
@ -65,9 +65,9 @@ const uploadFile = (
|
|||
) =>
|
||||
async (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
const maxImageSize = getState().instance.configuration.getIn(['media_attachments', 'image_size_limit']) as number | undefined;
|
||||
const maxVideoSize = getState().instance.configuration.getIn(['media_attachments', 'video_size_limit']) as number | undefined;
|
||||
const maxVideoDuration = getState().instance.configuration.getIn(['media_attachments', 'video_duration_limit']) as number | undefined;
|
||||
const maxImageSize = getState().instance.configuration.media_attachments.image_size_limit;
|
||||
const maxVideoSize = getState().instance.configuration.media_attachments.video_size_limit;
|
||||
const maxVideoDuration = getState().instance.configuration.media_attachments.video_duration_limit;
|
||||
|
||||
const isImage = file.type.match(/image.*/);
|
||||
const isVideo = file.type.match(/video.*/);
|
||||
|
|
|
@ -4,19 +4,21 @@ import ConfigDB from 'soapbox/utils/config-db';
|
|||
|
||||
import { fetchConfig, updateConfig } from './admin';
|
||||
|
||||
import type { MRFSimple } from 'soapbox/schemas/pleroma';
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
import type { Policy } from 'soapbox/utils/config-db';
|
||||
|
||||
const simplePolicyMerge = (simplePolicy: Policy, host: string, restrictions: ImmutableMap<string, any>) => {
|
||||
return simplePolicy.map((hosts, key) => {
|
||||
const simplePolicyMerge = (simplePolicy: MRFSimple, host: string, restrictions: ImmutableMap<string, any>) => {
|
||||
const entries = Object.entries(simplePolicy).map(([key, hosts]) => {
|
||||
const isRestricted = restrictions.get(key);
|
||||
|
||||
if (isRestricted) {
|
||||
return ImmutableSet(hosts).add(host);
|
||||
return [key, ImmutableSet(hosts).add(host).toJS()];
|
||||
} else {
|
||||
return ImmutableSet(hosts).delete(host);
|
||||
return [key, ImmutableSet(hosts).delete(host).toJS()];
|
||||
}
|
||||
});
|
||||
|
||||
return Object.fromEntries(entries);
|
||||
};
|
||||
|
||||
const updateMrf = (host: string, restrictions: ImmutableMap<string, any>) =>
|
||||
|
|
|
@ -6,10 +6,10 @@ import { connectRequestSchema } from 'soapbox/schemas/nostr';
|
|||
import { jsonSchema } from 'soapbox/schemas/utils';
|
||||
|
||||
function useSignerStream() {
|
||||
const { nostr } = useInstance();
|
||||
const instance = useInstance();
|
||||
|
||||
const relayUrl = nostr.get('relay') as string | undefined;
|
||||
const pubkey = nostr.get('pubkey') as string | undefined;
|
||||
const relayUrl = instance.nostr?.relay;
|
||||
const pubkey = instance.nostr?.pubkey;
|
||||
|
||||
useEffect(() => {
|
||||
let relay: Relay | undefined;
|
||||
|
|
|
@ -14,7 +14,7 @@ function useTimelineStream(...args: Parameters<typeof connectTimelineStream>) {
|
|||
const stream = useRef<(() => void) | null>(null);
|
||||
|
||||
const accessToken = useAppSelector(getAccessToken);
|
||||
const streamingUrl = instance.urls.get('streaming_api');
|
||||
const streamingUrl = instance.urls?.streaming_api;
|
||||
|
||||
const connect = () => {
|
||||
if (enabled && streamingUrl && !stream.current) {
|
||||
|
|
|
@ -26,7 +26,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
const instance = useInstance();
|
||||
|
||||
const supportsBirthdays = features.birthdays;
|
||||
const minAge = instance.pleroma.getIn(['metadata', 'birthday_min_age']) as number;
|
||||
const minAge = instance.pleroma.metadata.birthday_min_age;
|
||||
|
||||
const maxDate = useMemo(() => {
|
||||
if (!supportsBirthdays) return null;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -22,11 +21,12 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const allowUnauthenticated = instance.pleroma.getIn(['metadata', 'translation', 'allow_unauthenticated'], false);
|
||||
const allowRemote = instance.pleroma.getIn(['metadata', 'translation', 'allow_remote'], true);
|
||||
|
||||
const sourceLanguages = instance.pleroma.getIn(['metadata', 'translation', 'source_languages']) as ImmutableList<string>;
|
||||
const targetLanguages = instance.pleroma.getIn(['metadata', 'translation', 'target_languages']) as ImmutableList<string>;
|
||||
const {
|
||||
allow_remote: allowRemote,
|
||||
allow_unauthenticated: allowUnauthenticated,
|
||||
source_languages: sourceLanguages,
|
||||
target_languages: targetLanguages,
|
||||
} = instance.pleroma.metadata.translation;
|
||||
|
||||
const renderTranslate = (me || allowUnauthenticated) && (allowRemote || isLocal(status.account as Account)) && ['public', 'unlisted'].includes(status.visibility) && status.contentHtml.length > 0 && status.language !== null && intl.locale !== status.language;
|
||||
|
||||
|
|
|
@ -4,10 +4,9 @@ import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
|||
import { updateConfig } from 'soapbox/actions/admin';
|
||||
import { RadioGroup, RadioItem } from 'soapbox/components/radio';
|
||||
import { useAppDispatch, useInstance } from 'soapbox/hooks';
|
||||
import { Instance } from 'soapbox/schemas';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
|
||||
type RegistrationMode = 'open' | 'approval' | 'closed';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -41,11 +41,13 @@ const Dashboard: React.FC = () => {
|
|||
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
const userCount = instance.stats.get('user_count');
|
||||
const statusCount = instance.stats.get('status_count');
|
||||
const domainCount = instance.stats.get('domain_count');
|
||||
const {
|
||||
user_count: userCount,
|
||||
status_count: statusCount,
|
||||
domain_count: domainCount,
|
||||
} = instance.stats;
|
||||
|
||||
const mau = instance.pleroma.getIn(['stats', 'mau']) as number | undefined;
|
||||
const mau = instance.pleroma.stats.mau;
|
||||
const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined;
|
||||
|
||||
if (!account) return null;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -13,9 +12,9 @@ interface IConsumersList {
|
|||
/** Displays OAuth consumers to log in with. */
|
||||
const ConsumersList: React.FC<IConsumersList> = () => {
|
||||
const instance = useInstance();
|
||||
const providers = ImmutableList<string>(instance.pleroma.get('oauth_consumer_strategies'));
|
||||
const providers = instance.pleroma.oauth_consumer_strategies;
|
||||
|
||||
if (providers.size > 0) {
|
||||
if (providers.length > 0) {
|
||||
return (
|
||||
<Card className='bg-gray-50 p-4 dark:bg-primary-800 sm:rounded-xl'>
|
||||
<Text size='xs' theme='muted'>
|
||||
|
|
|
@ -46,11 +46,11 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
const instance = useInstance();
|
||||
|
||||
const locale = settings.get('locale');
|
||||
const needsConfirmation = !!instance.pleroma.getIn(['metadata', 'account_activation_required']);
|
||||
const needsConfirmation = instance.pleroma.metadata.account_activation_required;
|
||||
const needsApproval = instance.approval_required;
|
||||
const supportsEmailList = features.emailList;
|
||||
const supportsAccountLookup = features.accountLookup;
|
||||
const birthdayRequired = instance.pleroma.getIn(['metadata', 'birthday_required']);
|
||||
const birthdayRequired = instance.pleroma.metadata.birthday_required;
|
||||
|
||||
const [captchaLoading, setCaptchaLoading] = useState(true);
|
||||
const [submissionLoading, setSubmissionLoading] = useState(false);
|
||||
|
|
|
@ -3,8 +3,8 @@ import React from 'react';
|
|||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
|
||||
import { ChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
import { normalizeChatMessage, normalizeInstance } from 'soapbox/normalizers';
|
||||
import { buildAccount, buildInstance } from 'soapbox/jest/factory';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { ChatMessage } from 'soapbox/types/entities';
|
||||
|
||||
import { __stub } from '../../../../api';
|
||||
|
@ -70,7 +70,7 @@ Object.assign(navigator, {
|
|||
|
||||
const store = rootState
|
||||
.set('me', '1')
|
||||
.set('instance', normalizeInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' }));
|
||||
.set('instance', buildInstance({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)' }));
|
||||
|
||||
const renderComponentWithChatContext = () => render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>
|
||||
|
|
|
@ -76,8 +76,8 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
|
|||
|
||||
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
|
||||
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
|
||||
const maxCharacterCount = useAppSelector((state) => state.instance.getIn(['configuration', 'chats', 'max_characters']) as number);
|
||||
const attachmentLimit = useAppSelector(state => state.instance.configuration.getIn(['chats', 'max_media_attachments']) as number);
|
||||
const maxCharacterCount = useAppSelector((state) => state.instance.configuration.chats.max_characters);
|
||||
const attachmentLimit = useAppSelector(state => state.instance.configuration.chats.max_media_attachments);
|
||||
|
||||
const [suggestions, setSuggestions] = useState<Suggestion>(initialSuggestionState);
|
||||
const isSuggestionsAvailable = suggestions.list.length > 0;
|
||||
|
|
|
@ -53,7 +53,7 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
|
|||
const dispatch = useAppDispatch();
|
||||
|
||||
const { createChatMessage, acceptChat } = useChatActions(chat.id);
|
||||
const attachmentLimit = useAppSelector(state => state.instance.configuration.getIn(['chats', 'max_media_attachments']) as number);
|
||||
const attachmentLimit = useAppSelector(state => state.instance.configuration.chats.max_media_attachments);
|
||||
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
||||
|
|
|
@ -77,7 +77,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
|
||||
const compose = useCompose(id);
|
||||
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
|
||||
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
|
||||
const maxTootChars = configuration.statuses.max_characters;
|
||||
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
|
||||
const features = useFeatures();
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { useAppDispatch, useCompose, useInstance } from 'soapbox/hooks';
|
|||
|
||||
import DurationSelector from './duration-selector';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -115,13 +114,14 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
|||
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
const pollLimits = configuration.get('polls') as ImmutableMap<string, number>;
|
||||
const options = compose.poll?.options;
|
||||
const expiresIn = compose.poll?.expires_in;
|
||||
const isMultiple = compose.poll?.multiple;
|
||||
|
||||
const maxOptions = pollLimits.get('max_options') as number;
|
||||
const maxOptionChars = pollLimits.get('max_characters_per_option') as number;
|
||||
const {
|
||||
max_options: maxOptions,
|
||||
max_characters_per_option: maxOptionChars,
|
||||
} = configuration.polls;
|
||||
|
||||
const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index));
|
||||
const onChangeOption = (index: number, title: string) => dispatch(changePollOption(composeId, index, title));
|
||||
|
|
|
@ -4,14 +4,12 @@ import { defineMessages, IntlShape, useIntl } from 'react-intl';
|
|||
import { IconButton } from 'soapbox/components/ui';
|
||||
import { useInstance } from 'soapbox/hooks';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add media attachment' },
|
||||
});
|
||||
|
||||
export const onlyImages = (types: ImmutableList<string>) => {
|
||||
return Boolean(types && types.every(type => type.startsWith('image/')));
|
||||
export const onlyImages = (types: string[] | undefined): boolean => {
|
||||
return types?.every((type) => type.startsWith('image/')) ?? false;
|
||||
};
|
||||
|
||||
export interface IUploadButton {
|
||||
|
@ -38,7 +36,7 @@ const UploadButton: React.FC<IUploadButton> = ({
|
|||
const { configuration } = useInstance();
|
||||
|
||||
const fileElement = useRef<HTMLInputElement>(null);
|
||||
const attachmentTypes = configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>;
|
||||
const attachmentTypes = configuration.media_attachments.supported_mime_types;
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files?.length) {
|
||||
|
@ -78,7 +76,7 @@ const UploadButton: React.FC<IUploadButton> = ({
|
|||
ref={fileElement}
|
||||
type='file'
|
||||
multiple
|
||||
accept={attachmentTypes && attachmentTypes.toArray().join(',')}
|
||||
accept={attachmentTypes?.join(',')}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
|
|
|
@ -8,7 +8,7 @@ import { useDraggedFiles } from 'soapbox/hooks';
|
|||
interface IMediaInput {
|
||||
className?: string
|
||||
src: string | undefined
|
||||
accept: string
|
||||
accept?: string
|
||||
onChange: (files: FileList | null) => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IMediaInput {
|
||||
src: string | undefined
|
||||
accept: string
|
||||
accept?: string
|
||||
onChange: (files: FileList | null) => void
|
||||
onClear?: () => void
|
||||
disabled?: boolean
|
||||
|
|
|
@ -25,7 +25,6 @@ import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
|
|||
import AvatarPicker from './components/avatar-picker';
|
||||
import HeaderPicker from './components/header-picker';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import type { Account } from 'soapbox/schemas';
|
||||
|
||||
|
@ -183,11 +182,12 @@ const EditProfile: React.FC = () => {
|
|||
|
||||
const { account } = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const maxFields = instance.pleroma.getIn(['metadata', 'fields_limits', 'max_fields'], 4) as number;
|
||||
const maxFields = instance.pleroma.metadata.fields_limits.max_fields;
|
||||
|
||||
const attachmentTypes = useAppSelector(
|
||||
state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>,
|
||||
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
||||
state => state.instance.configuration.media_attachments.supported_mime_types)
|
||||
?.filter(type => type.startsWith('image/'))
|
||||
.join(',');
|
||||
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<AccountCredentials>({});
|
||||
|
|
|
@ -111,7 +111,7 @@ const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance
|
|||
if (!instance || !remoteInstance) return null;
|
||||
|
||||
const host = remoteInstance.get('host');
|
||||
const siteTitle = instance.get('title');
|
||||
const siteTitle = instance.title;
|
||||
|
||||
if (remoteInstance.getIn(['federation', 'reject']) === true) {
|
||||
return (
|
||||
|
|
|
@ -13,8 +13,6 @@ import HeaderPicker from '../edit-profile/components/header-picker';
|
|||
|
||||
import GroupTagsField from './components/group-tags-field';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url;
|
||||
const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url;
|
||||
|
||||
|
@ -48,12 +46,12 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
|
|||
const displayName = useTextField(group?.display_name);
|
||||
const note = useTextField(group?.note_plain);
|
||||
|
||||
const maxName = Number(instance.configuration.getIn(['groups', 'max_characters_name']));
|
||||
const maxNote = Number(instance.configuration.getIn(['groups', 'max_characters_description']));
|
||||
const maxName = Number(instance.configuration.groups.max_characters_name);
|
||||
const maxNote = Number(instance.configuration.groups.max_characters_description);
|
||||
|
||||
const attachmentTypes = useAppSelector(
|
||||
state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>,
|
||||
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
||||
const attachmentTypes = useAppSelector(state => state.instance.configuration.media_attachments.supported_mime_types)
|
||||
?.filter((type) => type.startsWith('image/'))
|
||||
.join(',');
|
||||
|
||||
async function handleSubmit() {
|
||||
setIsSubmitting(true);
|
||||
|
|
|
@ -23,7 +23,7 @@ const Migration = () => {
|
|||
const dispatch = useAppDispatch();
|
||||
const instance = useInstance();
|
||||
|
||||
const cooldownPeriod = instance.pleroma.getIn(['metadata', 'migration_cooldown_period']) as number | undefined;
|
||||
const cooldownPeriod = instance.pleroma.metadata.migration_cooldown_period;
|
||||
|
||||
const [targetAccount, setTargetAccount] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
|
|
@ -5,8 +5,6 @@ import Icon from 'soapbox/components/icon';
|
|||
import { HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
interface IUploadButton {
|
||||
disabled?: boolean
|
||||
onSelectFile: (files: FileList) => void
|
||||
|
@ -14,7 +12,8 @@ interface IUploadButton {
|
|||
|
||||
const UploadButton: React.FC<IUploadButton> = ({ disabled, onSelectFile }) => {
|
||||
const fileElement = useRef<HTMLInputElement>(null);
|
||||
const attachmentTypes = useAppSelector(state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>)?.filter(type => type.startsWith('image/'));
|
||||
const attachmentTypes = useAppSelector(state => state.instance.configuration.media_attachments.supported_mime_types)
|
||||
?.filter((type) => type.startsWith('image/'));
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files?.length) {
|
||||
|
@ -40,7 +39,7 @@ const UploadButton: React.FC<IUploadButton> = ({ disabled, onSelectFile }) => {
|
|||
<input
|
||||
ref={fileElement}
|
||||
type='file'
|
||||
accept={attachmentTypes && attachmentTypes.toArray().join(',')}
|
||||
accept={attachmentTypes?.join(',')}
|
||||
onChange={handleChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
|
|
|
@ -10,8 +10,6 @@ import { useAppSelector, useDebounce, useInstance } from 'soapbox/hooks';
|
|||
import { usePreview } from 'soapbox/hooks/forms';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
||||
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
||||
|
@ -40,9 +38,9 @@ const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
|||
const avatarSrc = usePreview(params.avatar);
|
||||
const headerSrc = usePreview(params.header);
|
||||
|
||||
const attachmentTypes = useAppSelector(
|
||||
state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>,
|
||||
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
||||
const attachmentTypes = useAppSelector(state => state.instance.configuration.media_attachments.supported_mime_types)
|
||||
?.filter((type) => type.startsWith('image/'))
|
||||
.join(',');
|
||||
|
||||
const handleTextChange = (property: keyof CreateGroupParams): React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> => {
|
||||
return (e) => {
|
||||
|
@ -107,7 +105,7 @@ const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
|||
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
|
||||
value={displayName}
|
||||
onChange={handleTextChange('display_name')}
|
||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
|
||||
maxLength={Number(instance.configuration.groups.max_characters_name)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -119,7 +117,7 @@ const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
|||
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
|
||||
value={note}
|
||||
onChange={handleTextChange('note')}
|
||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
|
||||
maxLength={Number(instance.configuration.groups.max_characters_description)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
type GroupTag,
|
||||
type Relationship,
|
||||
type Status,
|
||||
Instance,
|
||||
instanceSchema,
|
||||
} from 'soapbox/schemas';
|
||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||
|
||||
|
@ -71,6 +73,10 @@ function buildGroupMember(
|
|||
}, props));
|
||||
}
|
||||
|
||||
function buildInstance(props: PartialDeep<Instance> = {}) {
|
||||
return instanceSchema.parse(props);
|
||||
}
|
||||
|
||||
function buildRelationship(props: PartialDeep<Relationship> = {}): Relationship {
|
||||
return relationshipSchema.parse(Object.assign({
|
||||
id: uuidv4(),
|
||||
|
@ -91,6 +97,7 @@ export {
|
|||
buildGroupMember,
|
||||
buildGroupRelationship,
|
||||
buildGroupTag,
|
||||
buildInstance,
|
||||
buildRelationship,
|
||||
buildStatus,
|
||||
};
|
|
@ -1,5 +1,3 @@
|
|||
import { Record } from 'immutable';
|
||||
|
||||
import { ADMIN_CONFIG_UPDATE_REQUEST } from 'soapbox/actions/admin';
|
||||
import { rememberInstance } from 'soapbox/actions/instance';
|
||||
|
||||
|
@ -30,8 +28,7 @@ describe('instance reducer', () => {
|
|||
version: '0.0.0',
|
||||
};
|
||||
|
||||
expect(Record.isRecord(result)).toBe(true);
|
||||
expect(result.toJS()).toMatchObject(expected);
|
||||
expect(result).toMatchObject(expected);
|
||||
});
|
||||
|
||||
describe('rememberInstance.fulfilled', () => {
|
||||
|
@ -58,7 +55,7 @@ describe('instance reducer', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(result.toJS()).toMatchObject(expected);
|
||||
expect(result).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it('normalizes Mastodon instance with retained configuration', () => {
|
||||
|
@ -92,7 +89,7 @@ describe('instance reducer', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(result.toJS()).toMatchObject(expected);
|
||||
expect(result).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it('normalizes Mastodon 3.0.0 instance with default configuration', () => {
|
||||
|
@ -118,7 +115,7 @@ describe('instance reducer', () => {
|
|||
},
|
||||
};
|
||||
|
||||
expect(result.toJS()).toMatchObject(expected);
|
||||
expect(result).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,48 +1,28 @@
|
|||
import { produce } from 'immer';
|
||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin';
|
||||
import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
||||
import { normalizeInstance } from 'soapbox/normalizers/instance';
|
||||
import { type Instance, instanceSchema } from 'soapbox/schemas';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import { ConfigDB } from 'soapbox/utils/config-db';
|
||||
|
||||
import {
|
||||
rememberInstance,
|
||||
fetchInstance,
|
||||
fetchNodeinfo,
|
||||
} from '../actions/instance';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const initialState = normalizeInstance(ImmutableMap());
|
||||
const initialState: Instance = instanceSchema.parse({});
|
||||
|
||||
const nodeinfoToInstance = (nodeinfo: ImmutableMap<string, any>) => {
|
||||
// Match Pleroma's develop branch
|
||||
return normalizeInstance(ImmutableMap({
|
||||
pleroma: ImmutableMap({
|
||||
metadata: ImmutableMap({
|
||||
account_activation_required: nodeinfo.getIn(['metadata', 'accountActivationRequired']),
|
||||
features: nodeinfo.getIn(['metadata', 'features']),
|
||||
federation: nodeinfo.getIn(['metadata', 'federation']),
|
||||
fields_limits: ImmutableMap({
|
||||
max_fields: nodeinfo.getIn(['metadata', 'fieldsLimits', 'maxFields']),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
const importInstance = (_state: typeof initialState, instance: ImmutableMap<string, any>) => {
|
||||
return normalizeInstance(instance);
|
||||
};
|
||||
|
||||
const importNodeinfo = (state: typeof initialState, nodeinfo: ImmutableMap<string, any>) => {
|
||||
return nodeinfoToInstance(nodeinfo).mergeDeep(state);
|
||||
const importInstance = (_state: typeof initialState, instance: unknown) => {
|
||||
return instanceSchema.parse(instance);
|
||||
};
|
||||
|
||||
const preloadImport = (state: typeof initialState, action: Record<string, any>, path: string) => {
|
||||
const instance = action.data[path];
|
||||
return instance ? importInstance(state, ImmutableMap(fromJS(instance))) : state;
|
||||
return instance ? importInstance(state, instance) : state;
|
||||
};
|
||||
|
||||
const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string) => {
|
||||
|
@ -59,28 +39,29 @@ const importConfigs = (state: typeof initialState, configs: ImmutableList<any>)
|
|||
|
||||
if (!config && !simplePolicy) return state;
|
||||
|
||||
return state.withMutations(state => {
|
||||
return produce(state, (draft) => {
|
||||
if (config) {
|
||||
const value = config.get('value', ImmutableList());
|
||||
const registrationsOpen = getConfigValue(value, ':registrations_open');
|
||||
const approvalRequired = getConfigValue(value, ':account_approval_required');
|
||||
const registrationsOpen = getConfigValue(value, ':registrations_open') as boolean | undefined;
|
||||
const approvalRequired = getConfigValue(value, ':account_approval_required') as boolean | undefined;
|
||||
|
||||
state.update('registrations', c => typeof registrationsOpen === 'boolean' ? registrationsOpen : c);
|
||||
state.update('approval_required', c => typeof approvalRequired === 'boolean' ? approvalRequired : c);
|
||||
draft.registrations = registrationsOpen ?? draft.registrations;
|
||||
draft.approval_required = approvalRequired ?? draft.approval_required;
|
||||
}
|
||||
|
||||
if (simplePolicy) {
|
||||
state.setIn(['pleroma', 'metadata', 'federation', 'mrf_simple'], simplePolicy);
|
||||
draft.pleroma.metadata.federation.mrf_simple = simplePolicy;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAuthFetch = (state: typeof initialState) => {
|
||||
// Authenticated fetch is enabled, so make the instance appear censored
|
||||
return state.mergeWith((o, n) => o || n, {
|
||||
title: '██████',
|
||||
description: '████████████',
|
||||
});
|
||||
return {
|
||||
...state,
|
||||
title: state.title || '██████',
|
||||
description: state.description || '████████████',
|
||||
};
|
||||
};
|
||||
|
||||
const getHost = (instance: { uri: string }) => {
|
||||
|
@ -116,14 +97,12 @@ export default function instance(state = initialState, action: AnyAction) {
|
|||
case PLEROMA_PRELOAD_IMPORT:
|
||||
return preloadImport(state, action, '/api/v1/instance');
|
||||
case rememberInstance.fulfilled.type:
|
||||
return importInstance(state, ImmutableMap(fromJS(action.payload)));
|
||||
return importInstance(state, action.payload);
|
||||
case fetchInstance.fulfilled.type:
|
||||
persistInstance(action.payload);
|
||||
return importInstance(state, ImmutableMap(fromJS(action.payload)));
|
||||
return importInstance(state, action.payload);
|
||||
case fetchInstance.rejected.type:
|
||||
return handleInstanceFetchFail(state, action.error);
|
||||
case fetchNodeinfo.fulfilled.type:
|
||||
return importNodeinfo(state, ImmutableMap(fromJS(action.payload)));
|
||||
case ADMIN_CONFIG_UPDATE_REQUEST:
|
||||
case ADMIN_CONFIG_UPDATE_SUCCESS:
|
||||
return importConfigs(state, ImmutableList(fromJS(action.configs)));
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { isBlurhashValid } from 'blurhash';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { mimeSchema } from './utils';
|
||||
|
||||
const blurhashSchema = z.string().superRefine((value, ctx) => {
|
||||
const r = isBlurhashValid(value);
|
||||
|
||||
|
@ -17,7 +19,7 @@ const baseAttachmentSchema = z.object({
|
|||
description: z.string().catch(''),
|
||||
id: z.string(),
|
||||
pleroma: z.object({
|
||||
mime_type: z.string().regex(/^\w+\/[-+.\w]+$/),
|
||||
mime_type: mimeSchema,
|
||||
}).optional().catch(undefined),
|
||||
preview_url: z.string().url().catch(''),
|
||||
remote_url: z.string().url().nullable().catch(null),
|
||||
|
|
|
@ -8,6 +8,7 @@ export { groupSchema, type Group } from './group';
|
|||
export { groupMemberSchema, type GroupMember } from './group-member';
|
||||
export { groupRelationshipSchema, type GroupRelationship } from './group-relationship';
|
||||
export { groupTagSchema, type GroupTag } from './group-tag';
|
||||
export { instanceSchema, type Instance } from './instance';
|
||||
export { mentionSchema, type Mention } from './mention';
|
||||
export { notificationSchema, type Notification } from './notification';
|
||||
export { patronUserSchema, type PatronUser } from './patron';
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/* eslint sort-keys: "error" */
|
||||
import z from 'zod';
|
||||
|
||||
import { accountSchema } from './account';
|
||||
import { mrfSimpleSchema } from './pleroma';
|
||||
import { coerceObject, mimeSchema } from './utils';
|
||||
|
||||
const configurationSchema = coerceObject({
|
||||
chats: coerceObject({
|
||||
max_characters: z.number().catch(5000),
|
||||
max_media_attachments: z.number().catch(1),
|
||||
}),
|
||||
groups: coerceObject({
|
||||
max_characters_description: z.number().catch(160),
|
||||
max_characters_name: z.number().catch(50),
|
||||
}),
|
||||
media_attachments: coerceObject({
|
||||
image_matrix_limit: z.number().optional().catch(undefined),
|
||||
image_size_limit: z.number().optional().catch(undefined),
|
||||
supported_mime_types: mimeSchema.array().optional().catch(undefined),
|
||||
video_duration_limit: z.number().optional().catch(undefined),
|
||||
video_frame_rate_limit: z.number().optional().catch(undefined),
|
||||
video_matrix_limit: z.number().optional().catch(undefined),
|
||||
video_size_limit: z.number().optional().catch(undefined),
|
||||
}),
|
||||
polls: coerceObject({
|
||||
max_characters_per_option: z.number().catch(25),
|
||||
max_expiration: z.number().catch(2629746),
|
||||
max_options: z.number().catch(4),
|
||||
min_expiration: z.number().catch(300),
|
||||
}),
|
||||
statuses: coerceObject({
|
||||
max_characters: z.number().catch(500),
|
||||
max_media_attachments: z.number().catch(4),
|
||||
}),
|
||||
});
|
||||
|
||||
const nostrSchema = coerceObject({
|
||||
pubkey: z.string(),
|
||||
relay: z.string().url(),
|
||||
});
|
||||
|
||||
const pleromaSchema = coerceObject({
|
||||
metadata: coerceObject({
|
||||
account_activation_required: z.boolean().catch(false),
|
||||
birthday_min_age: z.number().catch(0),
|
||||
birthday_required: z.boolean().catch(false),
|
||||
features: z.string().array().catch([]),
|
||||
federation: coerceObject({
|
||||
enabled: z.boolean().catch(true), // Assume true unless explicitly false
|
||||
mrf_policies: z.string().array().optional().catch(undefined),
|
||||
mrf_simple: mrfSimpleSchema,
|
||||
}),
|
||||
fields_limits: z.any(),
|
||||
migration_cooldown_period: z.number().optional().catch(undefined),
|
||||
translation: coerceObject({
|
||||
allow_remote: z.boolean().catch(true),
|
||||
allow_unauthenticated: z.boolean().catch(false),
|
||||
source_languages: z.string().array().optional().catch(undefined),
|
||||
target_languages: z.string().array().optional().catch(undefined),
|
||||
}),
|
||||
}),
|
||||
oauth_consumer_strategies: z.string().array().catch([]),
|
||||
stats: coerceObject({
|
||||
mau: z.number().optional().catch(undefined),
|
||||
}),
|
||||
vapid_public_key: z.string().catch(''),
|
||||
});
|
||||
|
||||
const statsSchema = coerceObject({
|
||||
domain_count: z.number().catch(0),
|
||||
status_count: z.number().catch(0),
|
||||
user_count: z.number().catch(0),
|
||||
});
|
||||
|
||||
const urlsSchema = coerceObject({
|
||||
streaming_api: z.string().url().optional().catch(undefined),
|
||||
});
|
||||
|
||||
const usageSchema = coerceObject({
|
||||
users: coerceObject({
|
||||
active_month: z.number().catch(0),
|
||||
}),
|
||||
});
|
||||
|
||||
const instanceSchema = coerceObject({
|
||||
approval_required: z.boolean().catch(false),
|
||||
configuration: configurationSchema,
|
||||
contact_account: accountSchema.optional().catch(undefined),
|
||||
description: z.string().catch(''),
|
||||
description_limit: z.number().catch(1500),
|
||||
email: z.string().email().catch(''),
|
||||
feature_quote: z.boolean().catch(false),
|
||||
fedibird_capabilities: z.array(z.string()).catch([]),
|
||||
languages: z.string().array().catch([]),
|
||||
nostr: nostrSchema.optional().catch(undefined),
|
||||
pleroma: pleromaSchema,
|
||||
registrations: z.boolean().catch(false),
|
||||
rules: z.any(),
|
||||
short_description: z.string().catch(''),
|
||||
stats: statsSchema,
|
||||
thumbnail: z.string().catch(''),
|
||||
title: z.string().catch(''),
|
||||
urls: urlsSchema,
|
||||
usage: usageSchema,
|
||||
version: z.string().catch(''),
|
||||
});
|
||||
|
||||
type Instance = z.infer<typeof instanceSchema>;
|
||||
|
||||
export { instanceSchema, Instance };
|
|
@ -0,0 +1,20 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
import { coerceObject } from './utils';
|
||||
|
||||
const mrfSimpleSchema = coerceObject({
|
||||
accept: z.string().array().catch([]),
|
||||
avatar_removal: z.string().array().catch([]),
|
||||
banner_removal: z.string().array().catch([]),
|
||||
federated_timeline_removal: z.string().array().catch([]),
|
||||
followers_only: z.string().array().catch([]),
|
||||
media_nsfw: z.string().array().catch([]),
|
||||
media_removal: z.string().array().catch([]),
|
||||
reject: z.string().array().catch([]),
|
||||
reject_deletes: z.string().array().catch([]),
|
||||
report_removal: z.string().array().catch([]),
|
||||
});
|
||||
|
||||
type MRFSimple = z.infer<typeof mrfSimpleSchema>;
|
||||
|
||||
export { mrfSimpleSchema, type MRFSimple };
|
|
@ -39,4 +39,12 @@ const jsonSchema = z.string().transform((value, ctx) => {
|
|||
}
|
||||
});
|
||||
|
||||
export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema, jsonSchema };
|
||||
/** MIME schema, eg `image/png`. */
|
||||
const mimeSchema = z.string().regex(/^\w+\/[-+.\w]+$/);
|
||||
|
||||
/** zod schema to force the value into an object, if it isn't already. */
|
||||
function coerceObject<T extends z.ZodRawShape>(shape: T) {
|
||||
return z.object({}).passthrough().catch({}).pipe(z.object(shape));
|
||||
}
|
||||
|
||||
export { filteredArray, makeCustomEmojiMap, emojiSchema, contentSchema, dateSchema, jsonSchema, mimeSchema, coerceObject };
|
|
@ -8,6 +8,7 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { Entities } from 'soapbox/entity-store/entities';
|
||||
import { type MRFSimple } from 'soapbox/schemas/pleroma';
|
||||
import { getDomain } from 'soapbox/utils/accounts';
|
||||
import { validId } from 'soapbox/utils/auth';
|
||||
import ConfigDB from 'soapbox/utils/config-db';
|
||||
|
@ -283,9 +284,12 @@ export const makeGetOtherAccounts = () => {
|
|||
|
||||
const getSimplePolicy = createSelector([
|
||||
(state: RootState) => state.admin.configs,
|
||||
(state: RootState) => state.instance.pleroma.getIn(['metadata', 'federation', 'mrf_simple'], ImmutableMap()) as ImmutableMap<string, any>,
|
||||
], (configs, instancePolicy: ImmutableMap<string, any>) => {
|
||||
return instancePolicy.merge(ConfigDB.toSimplePolicy(configs));
|
||||
(state: RootState) => state.instance.pleroma.metadata.federation.mrf_simple,
|
||||
], (configs, instancePolicy) => {
|
||||
return {
|
||||
...instancePolicy,
|
||||
...ConfigDB.toSimplePolicy(configs),
|
||||
};
|
||||
});
|
||||
|
||||
const getRemoteInstanceFavicon = (state: RootState, host: string) => {
|
||||
|
@ -294,15 +298,24 @@ const getRemoteInstanceFavicon = (state: RootState, host: string) => {
|
|||
return account?.pleroma?.favicon;
|
||||
};
|
||||
|
||||
const getRemoteInstanceFederation = (state: RootState, host: string) => (
|
||||
getSimplePolicy(state)
|
||||
.map(hosts => hosts.includes(host))
|
||||
);
|
||||
type HostFederation = {
|
||||
[key in keyof MRFSimple]: boolean;
|
||||
};
|
||||
|
||||
const getRemoteInstanceFederation = (state: RootState, host: string): HostFederation => {
|
||||
const simplePolicy = getSimplePolicy(state);
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(simplePolicy).map(([key, hosts]) => [key, hosts.includes(host)]),
|
||||
) as HostFederation;
|
||||
};
|
||||
|
||||
|
||||
export const makeGetHosts = () => {
|
||||
return createSelector([getSimplePolicy], (simplePolicy) => {
|
||||
return simplePolicy
|
||||
.deleteAll(['accept', 'reject_deletes', 'report_removal'])
|
||||
const { accept, reject_deletes, report_removal, ...rest } = simplePolicy;
|
||||
|
||||
return Object.values(rest)
|
||||
.reduce((acc, hosts) => acc.union(hosts), ImmutableOrderedSet())
|
||||
.sort();
|
||||
});
|
||||
|
|
|
@ -20,7 +20,7 @@ export function connectStream(
|
|||
callbacks: (dispatch: AppDispatch, getState: () => RootState) => ConnectStreamCallbacks,
|
||||
) {
|
||||
return (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const streamingAPIBaseURL = getState().instance.urls.get('streaming_api');
|
||||
const streamingAPIBaseURL = getState().instance.urls.streaming_api;
|
||||
const accessToken = getAccessToken(getState());
|
||||
const { onConnect, onDisconnect, onReceive } = callbacks(dispatch, getState);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { InstanceRecord } from 'soapbox/normalizers';
|
||||
import { buildInstance } from 'soapbox/jest/factory';
|
||||
|
||||
import {
|
||||
parseVersion,
|
||||
|
@ -77,7 +77,7 @@ describe('parseVersion', () => {
|
|||
describe('getFeatures', () => {
|
||||
describe('emojiReacts', () => {
|
||||
it('is true for Pleroma 2.0+', () => {
|
||||
const instance = InstanceRecord({
|
||||
const instance = buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 2.0.5-6-ga36eb5ea-plerasstodon+dev)',
|
||||
});
|
||||
const features = getFeatures(instance);
|
||||
|
@ -85,7 +85,7 @@ describe('getFeatures', () => {
|
|||
});
|
||||
|
||||
it('is false for Pleroma < 2.0', () => {
|
||||
const instance = InstanceRecord({
|
||||
const instance = buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 1.1.50-42-g3d9ac6ae-develop)',
|
||||
});
|
||||
const features = getFeatures(instance);
|
||||
|
@ -93,7 +93,7 @@ describe('getFeatures', () => {
|
|||
});
|
||||
|
||||
it('is false for Mastodon', () => {
|
||||
const instance = InstanceRecord({ version: '3.1.4' });
|
||||
const instance = buildInstance({ version: '3.1.4' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.emojiReacts).toBe(false);
|
||||
});
|
||||
|
@ -101,19 +101,19 @@ describe('getFeatures', () => {
|
|||
|
||||
describe('suggestions', () => {
|
||||
it('is true for Mastodon 2.4.3+', () => {
|
||||
const instance = InstanceRecord({ version: '2.4.3' });
|
||||
const instance = buildInstance({ version: '2.4.3' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.suggestions).toBe(true);
|
||||
});
|
||||
|
||||
it('is false for Mastodon < 2.4.3', () => {
|
||||
const instance = InstanceRecord({ version: '2.4.2' });
|
||||
const instance = buildInstance({ version: '2.4.2' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.suggestions).toBe(false);
|
||||
});
|
||||
|
||||
it('is false for Pleroma', () => {
|
||||
const instance = InstanceRecord({
|
||||
const instance = buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 1.1.50-42-g3d9ac6ae-develop)',
|
||||
});
|
||||
const features = getFeatures(instance);
|
||||
|
@ -123,19 +123,19 @@ describe('getFeatures', () => {
|
|||
|
||||
describe('trends', () => {
|
||||
it('is true for Mastodon 3.0.0+', () => {
|
||||
const instance = InstanceRecord({ version: '3.0.0' });
|
||||
const instance = buildInstance({ version: '3.0.0' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.trends).toBe(true);
|
||||
});
|
||||
|
||||
it('is false for Mastodon < 3.0.0', () => {
|
||||
const instance = InstanceRecord({ version: '2.4.3' });
|
||||
const instance = buildInstance({ version: '2.4.3' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.trends).toBe(false);
|
||||
});
|
||||
|
||||
it('is false for Pleroma', () => {
|
||||
const instance = InstanceRecord({
|
||||
const instance = buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 1.1.50-42-g3d9ac6ae-develop)',
|
||||
});
|
||||
const features = getFeatures(instance);
|
||||
|
@ -145,13 +145,13 @@ describe('getFeatures', () => {
|
|||
|
||||
describe('focalPoint', () => {
|
||||
it('is true for Mastodon 2.3.0+', () => {
|
||||
const instance = InstanceRecord({ version: '2.3.0' });
|
||||
const instance = buildInstance({ version: '2.3.0' });
|
||||
const features = getFeatures(instance);
|
||||
expect(features.focalPoint).toBe(true);
|
||||
});
|
||||
|
||||
it('is false for Pleroma', () => {
|
||||
const instance = InstanceRecord({
|
||||
const instance = buildInstance({
|
||||
version: '2.7.2 (compatible; Pleroma 1.1.50-42-g3d9ac6ae-develop)',
|
||||
});
|
||||
const features = getFeatures(instance);
|
||||
|
|
|
@ -64,6 +64,6 @@ export const getAuthUserUrl = (state: RootState) => {
|
|||
|
||||
/** Get the VAPID public key. */
|
||||
export const getVapidKey = (state: RootState) =>
|
||||
(state.auth.app.vapid_key || state.instance.pleroma.get('vapid_public_key')) as string;
|
||||
state.auth.app.vapid_key || state.instance.pleroma.vapid_public_key;
|
||||
|
||||
export const getMeUrl = (state: RootState) => selectOwnAccount(state)?.url;
|
|
@ -6,6 +6,8 @@ import {
|
|||
} from 'immutable';
|
||||
import trimStart from 'lodash/trimStart';
|
||||
|
||||
import { type MRFSimple, mrfSimpleSchema } from 'soapbox/schemas/pleroma';
|
||||
|
||||
export type Config = ImmutableMap<string, any>;
|
||||
export type Policy = ImmutableMap<string, any>;
|
||||
|
||||
|
@ -19,7 +21,7 @@ const find = (
|
|||
);
|
||||
};
|
||||
|
||||
const toSimplePolicy = (configs: ImmutableList<Config>): Policy => {
|
||||
const toSimplePolicy = (configs: ImmutableList<Config>): MRFSimple => {
|
||||
const config = find(configs, ':pleroma', ':mrf_simple');
|
||||
|
||||
const reducer = (acc: ImmutableMap<string, any>, curr: ImmutableMap<string, any>) => {
|
||||
|
@ -30,9 +32,10 @@ const toSimplePolicy = (configs: ImmutableList<Config>): Policy => {
|
|||
|
||||
if (config?.get) {
|
||||
const value = config.get('value', ImmutableList());
|
||||
return value.reduce(reducer, ImmutableMap());
|
||||
const result = value.reduce(reducer, ImmutableMap());
|
||||
return mrfSimpleSchema.parse(result.toJS());
|
||||
} else {
|
||||
return ImmutableMap();
|
||||
return mrfSimpleSchema.parse({});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint sort-keys: "error" */
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import { createSelector } from 'reselect';
|
||||
import semverCoerce from 'semver/functions/coerce';
|
||||
import gte from 'semver/functions/gte';
|
||||
|
@ -7,8 +6,7 @@ import lt from 'semver/functions/lt';
|
|||
import semverParse from 'semver/functions/parse';
|
||||
|
||||
import { custom } from 'soapbox/custom';
|
||||
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
import { type Instance } from 'soapbox/schemas';
|
||||
|
||||
/** Import custom overrides, if exists */
|
||||
const overrides = custom('features');
|
||||
|
@ -100,8 +98,7 @@ export const UNRELEASED = 'unreleased';
|
|||
/** Parse features for the given instance */
|
||||
const getInstanceFeatures = (instance: Instance) => {
|
||||
const v = parseVersion(instance.version);
|
||||
const features = instance.pleroma.getIn(['metadata', 'features'], ImmutableList()) as ImmutableList<string>;
|
||||
const federation = instance.pleroma.getIn(['metadata', 'federation'], ImmutableMap()) as ImmutableMap<string, any>;
|
||||
const { features, federation } = instance.pleroma.metadata;
|
||||
|
||||
return {
|
||||
/**
|
||||
|
@ -457,7 +454,7 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
]),
|
||||
|
||||
/** Whether the instance federates. */
|
||||
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
|
||||
federating: federation.enabled,
|
||||
|
||||
/**
|
||||
* Can edit and manage timeline filters (aka "muted words").
|
||||
|
|
|
@ -3,8 +3,8 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import { parseVersion, PLEROMA, MITRA } from './features';
|
||||
|
||||
import type { Instance } from 'soapbox/schemas';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
|
||||
/** For solving bugs between API implementations. */
|
||||
export const getQuirks = createSelector([
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
|
||||
import { PLEROMA, parseVersion } from './features';
|
||||
|
||||
import type { Instance } from 'soapbox/schemas';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
import type { Instance } from 'soapbox/types/entities';
|
||||
|
||||
/**
|
||||
* Get the OAuth scopes to use for login & signup.
|
||||
|
@ -20,9 +20,7 @@ const getInstanceScopes = (instance: Instance) => {
|
|||
};
|
||||
|
||||
/** Convenience function to get scopes from instance in store. */
|
||||
const getScopes = (state: RootState) => {
|
||||
return getInstanceScopes(state.instance);
|
||||
};
|
||||
const getScopes = (state: RootState) => getInstanceScopes(state.instance);
|
||||
|
||||
export {
|
||||
getInstanceScopes,
|
||||
|
|
|
@ -18,7 +18,7 @@ export const displayFqn = (state: RootState): boolean => {
|
|||
|
||||
/** Whether the instance exposes instance blocks through the API. */
|
||||
export const federationRestrictionsDisclosed = (state: RootState): boolean => {
|
||||
return state.instance.pleroma.hasIn(['metadata', 'federation', 'mrf_policies']);
|
||||
return !!state.instance.pleroma.metadata.federation.mrf_policies;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in New Issue