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