diff --git a/src/actions/blocks.ts b/src/actions/blocks.ts index 58641b5c8..b672e778f 100644 --- a/src/actions/blocks.ts +++ b/src/actions/blocks.ts @@ -1,5 +1,4 @@ import { isLoggedIn } from 'soapbox/utils/auth'; -import { getNextLinkName } from 'soapbox/utils/quirks'; import api, { getLinks } from '../api'; @@ -18,14 +17,13 @@ const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; const fetchBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; - const nextLinkName = getNextLinkName(getState); dispatch(fetchBlocksRequest()); return api(getState) .get('/api/v1/blocks') .then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); @@ -54,7 +52,6 @@ function fetchBlocksFail(error: unknown) { const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; - const nextLinkName = getNextLinkName(getState); const url = getState().user_lists.blocks.next; @@ -67,7 +64,7 @@ const expandBlocks = () => (dispatch: AppDispatch, getState: () => RootState) => return api(getState) .get(url) .then(response => { - const next = getLinks(response).refs.find(link => link.rel === nextLinkName); + const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedAccounts(response.data)); dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); dispatch(fetchRelationships(response.data.map((item: any) => item.id)) as any); diff --git a/src/actions/external-auth.ts b/src/actions/external-auth.ts index 45edb8ba5..45d288bc5 100644 --- a/src/actions/external-auth.ts +++ b/src/actions/external-auth.ts @@ -9,10 +9,9 @@ import { createApp } from 'soapbox/actions/apps'; import { authLoggedIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { obtainOAuthToken } from 'soapbox/actions/oauth'; -import { instanceSchema, type Instance } from 'soapbox/schemas'; +import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance'; import { parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; -import { getQuirks } from 'soapbox/utils/quirks'; import { getInstanceScopes } from 'soapbox/utils/scopes'; import { baseClient } from '../api'; @@ -22,36 +21,33 @@ import type { AppDispatch, RootState } from 'soapbox/store'; const fetchExternalInstance = (baseURL?: string) => { return baseClient(null, baseURL) .get('/api/v1/instance') - .then(({ data: instance }) => instanceSchema.parse(instance)) + .then(({ data: instance }) => instanceV1Schema.parse(instance)) .catch(error => { if (error.response?.status === 401) { // Authenticated fetch is enabled. // Continue with a limited featureset. - return instanceSchema.parse({}); + return instanceV1Schema.parse({}); } else { throw error; } }); }; -const createExternalApp = (instance: Instance, baseURL?: string) => +const createExternalApp = (instance: InstanceV1, baseURL?: string) => (dispatch: AppDispatch, _getState: () => RootState) => { - // Mitra: skip creating the auth app - if (getQuirks(instance).noApps) return new Promise(f => f({})); - const params = { client_name: sourceCode.displayName, redirect_uris: `${window.location.origin}/login/external`, website: sourceCode.homepage, - scopes: getInstanceScopes(instance), + scopes: getInstanceScopes(instance.version), }; return dispatch(createApp(params, baseURL)); }; -const externalAuthorize = (instance: Instance, baseURL: string) => +const externalAuthorize = (instance: InstanceV1, baseURL: string) => (dispatch: AppDispatch, _getState: () => RootState) => { - const scopes = getInstanceScopes(instance); + const scopes = getInstanceScopes(instance.version); return dispatch(createExternalApp(instance, baseURL)).then((app) => { const { client_id, redirect_uri } = app as Record; diff --git a/src/actions/instance.ts b/src/actions/instance.ts index 510fb052c..dd17af6ef 100644 --- a/src/actions/instance.ts +++ b/src/actions/instance.ts @@ -1,6 +1,6 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema, instanceV2Schema } from 'soapbox/schemas/instance'; import { RootState } from 'soapbox/store'; import { getAuthUserUrl, getMeUrl } from 'soapbox/utils/auth'; import { getFeatures } from 'soapbox/utils/features'; @@ -28,7 +28,7 @@ export const fetchInstance = createAsyncThunk { try { const { data } = await api(getState).get('/api/v1/instance'); - const instance = instanceSchema.parse(data); + const instance = instanceV1Schema.parse(data); const features = getFeatures(instance); if (features.instanceV2) { @@ -46,7 +46,8 @@ export const fetchInstanceV2 = createAsyncThunk { try { - const { data: instance } = await api(getState).get('/api/v2/instance'); + const { data } = await api(getState).get('/api/v2/instance'); + const instance = instanceV2Schema.parse(data); return { instance, host }; } catch (e) { return rejectWithValue(e); diff --git a/src/api/hooks/groups/useGroups.test.ts b/src/api/hooks/groups/useGroups.test.ts index 65a699969..c29d6eb09 100644 --- a/src/api/hooks/groups/useGroups.test.ts +++ b/src/api/hooks/groups/useGroups.test.ts @@ -1,13 +1,13 @@ import { __stub } from 'soapbox/api'; import { buildGroup } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import { useGroups } from './useGroups'; const group = buildGroup({ id: '1', display_name: 'soapbox' }); const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', }), }; diff --git a/src/api/hooks/groups/usePendingGroups.test.ts b/src/api/hooks/groups/usePendingGroups.test.ts index 33190ae0b..a590360c3 100644 --- a/src/api/hooks/groups/usePendingGroups.test.ts +++ b/src/api/hooks/groups/usePendingGroups.test.ts @@ -2,14 +2,14 @@ import { __stub } from 'soapbox/api'; import { Entities } from 'soapbox/entity-store/entities'; import { buildAccount, buildGroup } from 'soapbox/jest/factory'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import { usePendingGroups } from './usePendingGroups'; const id = '1'; const group = buildGroup({ id, display_name: 'soapbox' }); const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', }), me: '1', diff --git a/src/api/hooks/instance/useInstanceV1.ts b/src/api/hooks/instance/useInstanceV1.ts index b15e7eed9..07d44c4a2 100644 --- a/src/api/hooks/instance/useInstanceV1.ts +++ b/src/api/hooks/instance/useInstanceV1.ts @@ -3,17 +3,27 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks'; import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance'; +interface Opts { + /** The base URL of the instance. */ + baseUrl?: string; + /** Whether to fetch the instance from the API. */ + enabled?: boolean; +} + /** Get the Instance for the current backend. */ -export function useInstanceV1() { +export function useInstanceV1(opts: Opts = {}) { const api = useApi(); + const { baseUrl, enabled } = opts; + const { data: instance, ...rest } = useQuery({ - queryKey: ['instance', api.baseUrl, 'v1'], + queryKey: ['instance', baseUrl ?? api.baseUrl, 'v1'], queryFn: async () => { const response = await api.get('/api/v1/instance'); const data = await response.json(); return instanceV1Schema.parse(data); }, + enabled, }); return { instance, ...rest }; diff --git a/src/api/hooks/instance/useInstanceV2.ts b/src/api/hooks/instance/useInstanceV2.ts index 91eaaeead..933a3e4f3 100644 --- a/src/api/hooks/instance/useInstanceV2.ts +++ b/src/api/hooks/instance/useInstanceV2.ts @@ -3,17 +3,27 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks'; import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance'; +interface Opts { + /** The base URL of the instance. */ + baseUrl?: string; + /** Whether to fetch the instance from the API. */ + enabled?: boolean; +} + /** Get the Instance for the current backend. */ -export function useInstanceV2() { +export function useInstanceV2(opts: Opts = {}) { const api = useApi(); + const { baseUrl, enabled } = opts; + const { data: instance, ...rest } = useQuery({ - queryKey: ['instance', api.baseUrl, 'v2'], + queryKey: ['instance', baseUrl ?? api.baseUrl, 'v2'], queryFn: async () => { const response = await api.get('/api/v2/instance'); const data = await response.json(); return instanceV2Schema.parse(data); }, + enabled, }); return { instance, ...rest }; diff --git a/src/features/admin/components/registration-mode-picker.tsx b/src/features/admin/components/registration-mode-picker.tsx index d7762dfe4..8cd6959bf 100644 --- a/src/features/admin/components/registration-mode-picker.tsx +++ b/src/features/admin/components/registration-mode-picker.tsx @@ -4,7 +4,7 @@ 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 { InstanceV2 } from 'soapbox/schemas/instance'; import toast from 'soapbox/toast'; type RegistrationMode = 'open' | 'approval' | 'closed'; @@ -27,7 +27,7 @@ const generateConfig = (mode: RegistrationMode) => { }]; }; -const modeFromInstance = ({ registrations }: Instance): RegistrationMode => { +const modeFromInstance = ({ registrations }: InstanceV2): RegistrationMode => { if (registrations.approval_required && registrations.enabled) return 'approval'; return registrations.enabled ? 'open' : 'closed'; }; diff --git a/src/features/admin/tabs/dashboard.tsx b/src/features/admin/tabs/dashboard.tsx index bfba473ad..8a8b65d0b 100644 --- a/src/features/admin/tabs/dashboard.tsx +++ b/src/features/admin/tabs/dashboard.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list'; +import { useInstanceV1 } from 'soapbox/api/hooks/instance/useInstanceV1'; import List, { ListItem } from 'soapbox/components/list'; import { CardTitle, Icon, IconButton, Stack } from 'soapbox/components/ui'; -import { useAppDispatch, useOwnAccount, useFeatures, useInstance } from 'soapbox/hooks'; +import { useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks'; import sourceCode from 'soapbox/utils/code'; import { download } from 'soapbox/utils/download'; import { parseVersion } from 'soapbox/utils/features'; @@ -14,7 +15,7 @@ import RegistrationModePicker from '../components/registration-mode-picker'; const Dashboard: React.FC = () => { const dispatch = useAppDispatch(); - const { instance } = useInstance(); + const { instance } = useInstanceV1(); const features = useFeatures(); const { account } = useOwnAccount(); @@ -39,15 +40,15 @@ const Dashboard: React.FC = () => { e.preventDefault(); }; - const v = parseVersion(instance.version); + const v = parseVersion(instance?.version ?? '0.0.0'); const { user_count: userCount, status_count: statusCount, domain_count: domainCount, - } = instance.stats; + } = instance?.stats ?? {}; - const mau = instance.pleroma.stats.mau; + const mau = instance?.pleroma.stats.mau; const retention = (userCount && mau) ? Math.round(mau / userCount * 100) : undefined; if (!account) return null; diff --git a/src/features/auth-login/components/login-form.test.tsx b/src/features/auth-login/components/login-form.test.tsx index 2cecaa059..cef110b44 100644 --- a/src/features/auth-login/components/login-form.test.tsx +++ b/src/features/auth-login/components/login-form.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { fireEvent, render, screen } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import LoginForm from './login-form'; @@ -9,7 +9,7 @@ describe('', () => { it('renders for Pleroma', () => { const mockFn = vi.fn(); const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '2.7.2 (compatible; Pleroma 2.3.0)', }), }; @@ -22,7 +22,7 @@ describe('', () => { it('renders for Mastodon', () => { const mockFn = vi.fn(); const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '3.0.0', }), }; diff --git a/src/features/auth-login/components/login-page.test.tsx b/src/features/auth-login/components/login-page.test.tsx index fc308ff4f..dfae49747 100644 --- a/src/features/auth-login/components/login-page.test.tsx +++ b/src/features/auth-login/components/login-page.test.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { render, screen } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import LoginPage from './login-page'; describe('', () => { it('renders correctly on load', () => { const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '2.7.2 (compatible; Pleroma 2.3.0)', }), }; diff --git a/src/features/groups/components/discover/search/search.test.tsx b/src/features/groups/components/discover/search/search.test.tsx index 5c25cc5a3..0ae0f8cd8 100644 --- a/src/features/groups/components/discover/search/search.test.tsx +++ b/src/features/groups/components/discover/search/search.test.tsx @@ -3,12 +3,12 @@ import React from 'react'; import { __stub } from 'soapbox/api'; import { buildGroup } from 'soapbox/jest/factory'; import { render, screen, waitFor } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import Search from './search'; const store = { - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', }), }; diff --git a/src/features/groups/discover.test.tsx b/src/features/groups/discover.test.tsx index 880b17c1c..349999e7a 100644 --- a/src/features/groups/discover.test.tsx +++ b/src/features/groups/discover.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { buildAccount } from 'soapbox/jest/factory'; import { render, screen, waitFor } from 'soapbox/jest/test-helpers'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import Discover from './discover'; @@ -32,9 +32,8 @@ const store: any = { }, }), }, - instance: instanceSchema.parse({ + instance: instanceV1Schema.parse({ version: '3.4.1 (compatible; TruthSocial 1.0.0)', - software: 'TRUTHSOCIAL', }), }; diff --git a/src/hooks/useInstance.ts b/src/hooks/useInstance.ts index 62635553c..a49c0ac17 100644 --- a/src/hooks/useInstance.ts +++ b/src/hooks/useInstance.ts @@ -1,7 +1,22 @@ -import { useAppSelector } from './useAppSelector'; +import { useMemo } from 'react'; + +import { useInstanceV1 } from 'soapbox/api/hooks/instance/useInstanceV1'; +import { useInstanceV2 } from 'soapbox/api/hooks/instance/useInstanceV2'; +import { instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance'; /** Get the Instance for the current backend. */ -export const useInstance = () => { - const instance = useAppSelector((state) => state.instance); - return { instance }; -}; +export function useInstance() { + const v2 = useInstanceV2(); + const v1 = useInstanceV1({ enabled: v2.isError }); + + const upgradedV1 = useMemo(() => { + if (v1.instance) { + return upgradeInstance(v1.instance); + } + }, [v1.instance]); + + const instance = v2.instance ?? upgradedV1 ?? instanceV2Schema.parse({}); + const props = v2.isError ? v1 : v2; + + return { ...props, instance }; +} diff --git a/src/jest/factory.ts b/src/jest/factory.ts index 145da76b2..a2818c0c8 100644 --- a/src/jest/factory.ts +++ b/src/jest/factory.ts @@ -15,10 +15,9 @@ import { type GroupTag, type Relationship, type Status, - Instance, - instanceSchema, } from 'soapbox/schemas'; import { GroupRoles } from 'soapbox/schemas/group-member'; +import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance'; import type { PartialDeep } from 'type-fest'; @@ -71,8 +70,8 @@ function buildGroupMember( }, props)); } -function buildInstance(props: PartialDeep = {}) { - return instanceSchema.parse(props); +function buildInstance(props: PartialDeep = {}) { + return instanceV2Schema.parse(props); } function buildRelationship(props: PartialDeep = {}): Relationship { diff --git a/src/jest/mock-stores.tsx b/src/jest/mock-stores.tsx index c3ba64aac..638a7d4a9 100644 --- a/src/jest/mock-stores.tsx +++ b/src/jest/mock-stores.tsx @@ -1,13 +1,13 @@ import alexJson from 'soapbox/__fixtures__/pleroma-account.json'; -import { instanceSchema } from 'soapbox/schemas'; +import { instanceV1Schema } from 'soapbox/schemas/instance'; import { buildAccount } from './factory'; /** Store with registrations open. */ -const storeOpen = { instance: instanceSchema.parse({ registrations: true }) }; +const storeOpen = { instance: instanceV1Schema.parse({ registrations: true }) }; /** Store with registrations closed. */ -const storeClosed = { instance: instanceSchema.parse({ registrations: false }) }; +const storeClosed = { instance: instanceV1Schema.parse({ registrations: false }) }; /** Store with a logged-in user. */ const storeLoggedIn = { diff --git a/src/reducers/instance.test.ts b/src/reducers/instance.test.ts index 63b5cbebb..1f89d8c52 100644 --- a/src/reducers/instance.test.ts +++ b/src/reducers/instance.test.ts @@ -44,6 +44,7 @@ describe('instance reducer', () => { expect(state.registrations).toBe(false); // After importing the configs, registration will be open + // @ts-ignore don't know why the type is not working const result = reducer(state, action); expect(result.registrations).toBe(true); }); diff --git a/src/reducers/instance.ts b/src/reducers/instance.ts index 3425ff37f..ea44aaee5 100644 --- a/src/reducers/instance.ts +++ b/src/reducers/instance.ts @@ -3,7 +3,7 @@ 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 { type Instance, instanceSchema } from 'soapbox/schemas'; +import { InstanceV1, instanceV1Schema, InstanceV2, instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance'; import KVStore from 'soapbox/storage/kv-store'; import { ConfigDB } from 'soapbox/utils/config-db'; @@ -13,22 +13,20 @@ import { } from '../actions/instance'; import type { AnyAction } from 'redux'; -import type { APIEntity } from 'soapbox/types/entities'; -const initialState: Instance = instanceSchema.parse({}); +const initialState: InstanceV2 = instanceV2Schema.parse({}); -const importInstance = (_state: Instance, instance: APIEntity): Instance => { - return instanceSchema.parse(instance); +const importInstanceV1 = (_state: InstanceV2, instance: InstanceV1): InstanceV2 => { + return upgradeInstance(instanceV1Schema.parse(instance)); }; -const importInstanceV2 = (state: Instance, data: APIEntity): Instance => { - const instance = instanceSchema.parse(data); - return { ...instance, stats: state.stats }; +const importInstanceV2 = (_state: InstanceV2, data: InstanceV2): InstanceV2 => { + return instanceV2Schema.parse(data); }; -const preloadImport = (state: Instance, action: Record, path: string) => { +const preloadImport = (state: InstanceV2, action: Record, path: string) => { const instance = action.data[path]; - return instance ? importInstance(state, instance) : state; + return instance ? importInstanceV1(state, instance) : state; }; const getConfigValue = (instanceConfig: ImmutableMap, key: string) => { @@ -38,7 +36,7 @@ const getConfigValue = (instanceConfig: ImmutableMap, key: string) return v ? v.getIn(['tuple', 1]) : undefined; }; -const importConfigs = (state: Instance, configs: ImmutableList) => { +const importConfigs = (state: InstanceV2, configs: ImmutableList) => { // FIXME: This is pretty hacked together. Need to make a cleaner map. const config = ConfigDB.find(configs, ':pleroma', ':instance'); const simplePolicy = ConfigDB.toSimplePolicy(configs); @@ -63,7 +61,7 @@ const importConfigs = (state: Instance, configs: ImmutableList) => { }); }; -const handleAuthFetch = (state: Instance) => { +const handleAuthFetch = (state: InstanceV1 | InstanceV2) => { // Authenticated fetch is enabled, so make the instance appear censored return { ...state, @@ -97,7 +95,7 @@ const persistInstanceV2 = ({ instance }: { instance: { domain: string } }, host: } }; -const handleInstanceFetchFail = (state: Instance, error: Record) => { +const handleInstanceFetchFail = (state: InstanceV2, error: Record) => { if (error.response?.status === 401) { return handleAuthFetch(state); } else { @@ -111,7 +109,7 @@ export default function instance(state = initialState, action: AnyAction) { return preloadImport(state, action, '/api/v1/instance'); case fetchInstance.fulfilled.type: persistInstance(action.payload); - return importInstance(state, action.payload.instance); + return importInstanceV1(state, action.payload.instance); case fetchInstanceV2.fulfilled.type: persistInstanceV2(action.payload); return importInstanceV2(state, action.payload.instance); diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 3e8d00f7f..253baff7a 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -12,7 +12,6 @@ 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 { moderationLogEntrySchema, type ModerationLogEntry } from './moderation-log-entry'; export { notificationSchema, type Notification } from './notification'; diff --git a/src/schemas/instance.test.ts b/src/schemas/instance.test.ts index a47e1adad..fd1b6a37d 100644 --- a/src/schemas/instance.test.ts +++ b/src/schemas/instance.test.ts @@ -1,6 +1,6 @@ -import { instanceSchema } from './instance'; +import { instanceV1Schema } from './instance'; -describe('instanceSchema.parse()', () => { +describe('instanceV1Schema.parse()', () => { it('normalizes an empty Map', () => { const expected = { configuration: { @@ -67,7 +67,7 @@ describe('instanceSchema.parse()', () => { version: '0.0.0', }; - const result = instanceSchema.parse({}); + const result = instanceV1Schema.parse({}); expect(result).toMatchObject(expected); }); @@ -89,7 +89,7 @@ describe('instanceSchema.parse()', () => { }, }; - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result).toMatchObject(expected); }); @@ -119,7 +119,7 @@ describe('instanceSchema.parse()', () => { }, }; - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result).toMatchObject(expected); }); @@ -141,13 +141,13 @@ describe('instanceSchema.parse()', () => { }, }; - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result).toMatchObject(expected); }); it('normalizes Fedibird instance', () => { const instance = require('soapbox/__fixtures__/fedibird-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); // Sets description_limit expect(result.pleroma.metadata.description_limit).toEqual(1500); @@ -158,7 +158,7 @@ describe('instanceSchema.parse()', () => { it('normalizes Mitra instance', () => { const instance = require('soapbox/__fixtures__/mitra-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); // Adds configuration and description_limit expect(result.configuration).toBeTruthy(); @@ -167,7 +167,7 @@ describe('instanceSchema.parse()', () => { it('normalizes GoToSocial instance', () => { const instance = require('soapbox/__fixtures__/gotosocial-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); // Normalizes max_toot_chars expect(result.configuration.statuses.max_characters).toEqual(5000); @@ -180,7 +180,7 @@ describe('instanceSchema.parse()', () => { it('normalizes Friendica instance', () => { const instance = require('soapbox/__fixtures__/friendica-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); // Normalizes max_toot_chars expect(result.configuration.statuses.max_characters).toEqual(200000); @@ -193,20 +193,20 @@ describe('instanceSchema.parse()', () => { it('normalizes a Mastodon RC version', () => { const instance = require('soapbox/__fixtures__/mastodon-instance-rc.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result.version).toEqual('3.5.0-rc1'); }); it('normalizes Pixelfed instance', () => { const instance = require('soapbox/__fixtures__/pixelfed-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result.title).toBe('pixelfed'); }); it('renames Akkoma to Pleroma', () => { const instance = require('soapbox/__fixtures__/akkoma-instance.json'); - const result = instanceSchema.parse(instance); + const result = instanceV1Schema.parse(instance); expect(result.version).toEqual('2.7.2 (compatible; Pleroma 2.4.50+akkoma)'); diff --git a/src/schemas/instance.ts b/src/schemas/instance.ts index 786a54000..90eaa906a 100644 --- a/src/schemas/instance.ts +++ b/src/schemas/instance.ts @@ -27,8 +27,8 @@ const versionSchema = z.string().catch('0.0.0').transform((version) => { const configurationSchema = coerceObject({ accounts: coerceObject({ - max_featured_tags: z.number().optional().catch(undefined), - max_pinned_statuses: z.number().optional().catch(undefined), + max_featured_tags: z.number().catch(Infinity), + max_pinned_statuses: z.number().catch(Infinity), }), chats: coerceObject({ max_characters: z.number().catch(Infinity), @@ -48,18 +48,18 @@ const configurationSchema = coerceObject({ video_size_limit: z.number().optional().catch(undefined), }), polls: coerceObject({ - max_characters_per_option: z.number().optional().catch(undefined), - max_expiration: z.number().optional().catch(undefined), - max_options: z.number().optional().catch(undefined), - min_expiration: z.number().optional().catch(undefined), + max_characters_per_option: z.number().catch(Infinity), + max_expiration: z.number().catch(Infinity), + max_options: z.number().catch(Infinity), + min_expiration: z.number().catch(Infinity), }), reactions: coerceObject({ max_reactions: z.number().catch(0), }), statuses: coerceObject({ characters_reserved_per_url: z.number().optional().catch(undefined), - max_characters: z.number().optional().catch(undefined), - max_media_attachments: z.number().optional().catch(undefined), + max_characters: z.number().catch(Infinity), + max_media_attachments: z.number().catch(Infinity), }), translation: coerceObject({ diff --git a/src/utils/features.ts b/src/utils/features.ts index 2bbc3914b..d30905225 100644 --- a/src/utils/features.ts +++ b/src/utils/features.ts @@ -6,7 +6,7 @@ import lt from 'semver/functions/lt'; import semverParse from 'semver/functions/parse'; import { custom } from 'soapbox/custom'; -import { type Instance } from 'soapbox/schemas'; +import { InstanceV1, InstanceV2 } from 'soapbox/schemas/instance'; /** Import custom overrides, if exists */ const overrides = custom('features'); @@ -102,7 +102,7 @@ export const REBASED = 'soapbox'; export const UNRELEASED = 'unreleased'; /** Parse features for the given instance */ -const getInstanceFeatures = (instance: Instance) => { +const getInstanceFeatures = (instance: InstanceV1 | InstanceV2) => { const v = parseVersion(instance.version); const { features, federation } = instance.pleroma.metadata; @@ -912,7 +912,7 @@ const getInstanceFeatures = (instance: Instance) => { v.software === FRIENDICA && gte(v.version, '2023.3.0'), v.software === PLEROMA && [REBASED, AKKOMA].includes(v.build!) && gte(v.version, '2.4.50'), features.includes('quote_posting'), - instance.feature_quote === true, + 'feature_quote' in instance && instance.feature_quote === true, ]), /** @@ -1087,7 +1087,7 @@ export type Features = ReturnType; /** Detect backend features to conditionally render elements */ export const getFeatures = createSelector([ - (instance: Instance) => instance, + (instance: InstanceV1 | InstanceV2) => instance, ], (instance): Features => { const features = getInstanceFeatures(instance); return Object.assign(features, overrides) as Features; diff --git a/src/utils/quirks.ts b/src/utils/quirks.ts deleted file mode 100644 index 9024c67be..000000000 --- a/src/utils/quirks.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint sort-keys: "error" */ -import { createSelector } from 'reselect'; - -import { parseVersion, PLEROMA, MITRA } from './features'; - -import type { Instance } from 'soapbox/schemas'; -import type { RootState } from 'soapbox/store'; - -/** For solving bugs between API implementations. */ -export const getQuirks = createSelector([ - (instance: Instance) => parseVersion(instance.version), -], (v) => { - return { - /** - * The `next` and `prev` Link headers are backwards for blocks and mutes. - * @see GET /api/v1/blocks - * @see GET /api/v1/mutes - */ - invertedPagination: v.software === PLEROMA, - - /** - * Apps are not supported by the API, and should not be created during login or registration. - * @see POST /api/v1/apps - * @see POST /oauth/token - */ - noApps: v.software === MITRA, - }; -}); - -/** Shortcut for inverted pagination quirk. */ -export const getNextLinkName = (getState: () => RootState) => - getQuirks(getState().instance).invertedPagination ? 'prev' : 'next'; diff --git a/src/utils/scopes.ts b/src/utils/scopes.ts index 9a6175952..27712c0c3 100644 --- a/src/utils/scopes.ts +++ b/src/utils/scopes.ts @@ -1,15 +1,14 @@ import { PLEROMA, parseVersion } from './features'; -import type { Instance } from 'soapbox/schemas'; import type { RootState } from 'soapbox/store'; /** * Get the OAuth scopes to use for login & signup. * Mastodon will refuse scopes it doesn't know, so care is needed. */ -const getInstanceScopes = (instance: Instance) => { - const v = parseVersion(instance.version); +const getInstanceScopes = (version: string) => { + const v = parseVersion(version); switch (v.software) { case PLEROMA: @@ -20,7 +19,7 @@ const getInstanceScopes = (instance: Instance) => { }; /** Convenience function to get scopes from instance in store. */ -const getScopes = (state: RootState) => getInstanceScopes(state.instance); +const getScopes = (state: RootState) => getInstanceScopes(state.instance.version); export { getInstanceScopes,