Make instance stop aggressively refreshing, replace isStandalone with instance.isNotFound

This commit is contained in:
Alex Gleason 2024-10-11 15:12:30 -05:00
parent 9d1ee47166
commit 82731e633d
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
13 changed files with 52 additions and 134 deletions

View File

@ -24,7 +24,6 @@ import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth';
import sourceCode from 'soapbox/utils/code'; import sourceCode from 'soapbox/utils/code';
import { normalizeUsername } from 'soapbox/utils/input'; import { normalizeUsername } from 'soapbox/utils/input';
import { getScopes } from 'soapbox/utils/scopes'; import { getScopes } from 'soapbox/utils/scopes';
import { isStandalone } from 'soapbox/utils/state';
import api, { baseClient } from '../api'; import api, { baseClient } from '../api';
@ -201,11 +200,10 @@ export const logIn = (username: string, password: string) =>
export const deleteSession = () => export const deleteSession = () =>
(dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out'); (dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out');
export const logOut = () => export const logOut = (refresh?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const account = getLoggedInAccount(state); const account = getLoggedInAccount(state);
const standalone = isStandalone(state);
if (!account) return dispatch(noOp); if (!account) return dispatch(noOp);
@ -229,7 +227,7 @@ export const logOut = () =>
localStorage.removeItem('soapbox:external:baseurl'); localStorage.removeItem('soapbox:external:baseurl');
localStorage.removeItem('soapbox:external:scopes'); localStorage.removeItem('soapbox:external:scopes');
dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); dispatch({ type: AUTH_LOGGED_OUT, account, standalone: refresh });
toast.success(messages.loggedOut); toast.success(messages.loggedOut);
}); });

View File

@ -1,20 +1,18 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks'; import { useApi } from 'soapbox/hooks';
import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance'; import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance';
interface Opts { interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retry' | 'retryOnMount'> {
/** The base URL of the instance. */ /** The base URL of the instance. */
baseUrl?: string; baseUrl?: string;
/** Whether to fetch the instance from the API. */
enabled?: boolean;
} }
/** Get the Instance for the current backend. */ /** Get the Instance for the current backend. */
export function useInstanceV1(opts: Opts = {}) { export function useInstanceV1(opts: Opts = {}) {
const api = useApi(); const api = useApi();
const { baseUrl, enabled } = opts; const { baseUrl } = opts;
const { data: instance, ...rest } = useQuery<InstanceV1>({ const { data: instance, ...rest } = useQuery<InstanceV1>({
queryKey: ['instance', baseUrl ?? api.baseUrl, 'v1'], queryKey: ['instance', baseUrl ?? api.baseUrl, 'v1'],
@ -23,7 +21,7 @@ export function useInstanceV1(opts: Opts = {}) {
const data = await response.json(); const data = await response.json();
return instanceV1Schema.parse(data); return instanceV1Schema.parse(data);
}, },
enabled, ...opts,
}); });
return { instance, ...rest }; return { instance, ...rest };

View File

@ -1,20 +1,18 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery, UseQueryOptions } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks'; import { useApi } from 'soapbox/hooks';
import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance'; import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance';
interface Opts { interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retry' | 'retryOnMount'> {
/** The base URL of the instance. */ /** The base URL of the instance. */
baseUrl?: string; baseUrl?: string;
/** Whether to fetch the instance from the API. */
enabled?: boolean;
} }
/** Get the Instance for the current backend. */ /** Get the Instance for the current backend. */
export function useInstanceV2(opts: Opts = {}) { export function useInstanceV2(opts: Opts = {}) {
const api = useApi(); const api = useApi();
const { baseUrl, enabled } = opts; const { baseUrl } = opts;
const { data: instance, ...rest } = useQuery<InstanceV2>({ const { data: instance, ...rest } = useQuery<InstanceV2>({
queryKey: ['instance', baseUrl ?? api.baseUrl, 'v2'], queryKey: ['instance', baseUrl ?? api.baseUrl, 'v2'],
@ -23,7 +21,7 @@ export function useInstanceV2(opts: Opts = {}) {
const data = await response.json(); const data = await response.json();
return instanceV2Schema.parse(data); return instanceV2Schema.parse(data);
}, },
enabled, ...opts,
}); });
return { instance, ...rest }; return { instance, ...rest };

View File

@ -6,9 +6,8 @@ import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
import { fetchInstance } from 'soapbox/actions/instance'; import { fetchInstance } from 'soapbox/actions/instance';
import { closeModal, openModal } from 'soapbox/actions/modals'; import { closeModal, openModal } from 'soapbox/actions/modals';
import { BigCard } from 'soapbox/components/big-card'; import { BigCard } from 'soapbox/components/big-card';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
import { getRedirectUrl } from 'soapbox/utils/redirect'; import { getRedirectUrl } from 'soapbox/utils/redirect';
import { isStandalone } from 'soapbox/utils/state';
import ConsumersList from './consumers-list'; import ConsumersList from './consumers-list';
import LoginForm from './login-form'; import LoginForm from './login-form';
@ -20,7 +19,7 @@ const LoginPage = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const me = useAppSelector((state) => state.me); const me = useAppSelector((state) => state.me);
const standalone = useAppSelector((state) => isStandalone(state)); const instance = useInstance();
const { nostrSignup } = useFeatures(); const { nostrSignup } = useFeatures();
const token = new URLSearchParams(window.location.search).get('token'); const token = new URLSearchParams(window.location.search).get('token');
@ -68,7 +67,9 @@ const LoginPage = () => {
return <Redirect to='/' />; return <Redirect to='/' />;
} }
if (standalone) return <Redirect to='/login/external' />; if (instance.isNotFound) {
return <Redirect to='/login/external' />;
}
if (shouldRedirect) { if (shouldRedirect) {
const redirectUri = getRedirectUrl(); const redirectUri = getRedirectUrl();

View File

@ -10,10 +10,9 @@ import { openSidebar } from 'soapbox/actions/sidebar';
import SiteLogo from 'soapbox/components/site-logo'; import SiteLogo from 'soapbox/components/site-logo';
import { Avatar, Button, Counter, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui'; import { Avatar, Button, Counter, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui';
import Search from 'soapbox/features/compose/components/search'; import Search from 'soapbox/features/compose/components/search';
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks'; import { useAppDispatch, useFeatures, useInstance, useOwnAccount, useRegistrationStatus } from 'soapbox/hooks';
import { useIsMobile } from 'soapbox/hooks/useIsMobile'; import { useIsMobile } from 'soapbox/hooks/useIsMobile';
import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications'; import { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications';
import { isStandalone } from 'soapbox/utils/state';
import ProfileDropdown from './profile-dropdown'; import ProfileDropdown from './profile-dropdown';
@ -31,7 +30,7 @@ const Navbar = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl(); const intl = useIntl();
const features = useFeatures(); const features = useFeatures();
const standalone = useAppSelector(isStandalone); const instance = useInstance();
const { isOpen } = useRegistrationStatus(); const { isOpen } = useRegistrationStatus();
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const node = useRef(null); const node = useRef(null);
@ -121,7 +120,7 @@ const Navbar = () => {
)} )}
</HStack> </HStack>
{!standalone && ( {instance.isSuccess && (
<HStack space={3} alignItems='center' className='absolute inset-y-0 right-0 pr-2 lg:static lg:inset-auto lg:ml-6 lg:pr-0'> <HStack space={3} alignItems='center' className='absolute inset-y-0 right-0 pr-2 lg:static lg:inset-auto lg:ml-6 lg:pr-0'>
{account ? ( {account ? (
<div className='relative hidden items-center lg:flex'> <div className='relative hidden items-center lg:flex'>

View File

@ -35,7 +35,6 @@ import RemoteInstancePage from 'soapbox/pages/remote-instance-page';
import SearchPage from 'soapbox/pages/search-page'; import SearchPage from 'soapbox/pages/search-page';
import StatusPage from 'soapbox/pages/status-page'; import StatusPage from 'soapbox/pages/status-page';
import { getVapidKey } from 'soapbox/utils/auth'; import { getVapidKey } from 'soapbox/utils/auth';
import { isStandalone } from 'soapbox/utils/state';
import BackgroundShapes from './components/background-shapes'; import BackgroundShapes from './components/background-shapes';
import FloatingActionButton from './components/floating-action-button'; import FloatingActionButton from './components/floating-action-button';
@ -157,11 +156,10 @@ interface ISwitchingColumnsArea {
} }
const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) => { const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) => {
const { instance } = useInstance(); const { instance, isNotFound } = useInstance();
const features = useFeatures(); const features = useFeatures();
const { search } = useLocation(); const { search } = useLocation();
const { isLoggedIn } = useLoggedIn(); const { isLoggedIn } = useLoggedIn();
const standalone = useAppSelector(isStandalone);
const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig(); const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig();
const hasCrypto = cryptoAddresses.size > 0; const hasCrypto = cryptoAddresses.size > 0;
@ -173,7 +171,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
// Ex: use /login instead of /auth, but redirect /auth to /login // Ex: use /login instead of /auth, but redirect /auth to /login
return ( return (
<Switch> <Switch>
{standalone && <Redirect from='/' to='/login/external' exact />} {isNotFound && <Redirect from='/' to='/login/external' exact />}
<WrappedRoute path='/email-confirmation' page={EmptyPage} component={EmailConfirmation} publicRoute exact /> <WrappedRoute path='/email-confirmation' page={EmptyPage} component={EmailConfirmation} publicRoute exact />
<WrappedRoute path='/logout' page={EmptyPage} component={LogoutPage} publicRoute exact /> <WrappedRoute path='/logout' page={EmptyPage} component={LogoutPage} publicRoute exact />
@ -388,11 +386,11 @@ const UI: React.FC<IUI> = ({ children }) => {
const node = useRef<HTMLDivElement | null>(null); const node = useRef<HTMLDivElement | null>(null);
const me = useAppSelector(state => state.me); const me = useAppSelector(state => state.me);
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const instance = useInstance();
const features = useFeatures(); const features = useFeatures();
const vapidKey = useAppSelector(state => getVapidKey(state)); const vapidKey = useAppSelector(state => getVapidKey(state));
const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen); const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen);
const standalone = useAppSelector(isStandalone);
const { isDragging } = useDraggedFiles(node); const { isDragging } = useDraggedFiles(node);
@ -503,7 +501,7 @@ const UI: React.FC<IUI> = ({ children }) => {
<Layout> <Layout>
<Layout.Sidebar> <Layout.Sidebar>
{!standalone && <SidebarNavigation />} {instance.isSuccess && <SidebarNavigation />}
</Layout.Sidebar> </Layout.Sidebar>
<SwitchingColumnsArea> <SwitchingColumnsArea>

View File

@ -1,15 +1,32 @@
import { UseQueryOptions } from '@tanstack/react-query';
import { useEffect, useMemo } from 'react'; import { useEffect, useMemo } from 'react';
import { HTTPError } from 'soapbox/api/HTTPError';
import { useInstanceV1 } from 'soapbox/api/hooks/instance/useInstanceV1'; import { useInstanceV1 } from 'soapbox/api/hooks/instance/useInstanceV1';
import { useInstanceV2 } from 'soapbox/api/hooks/instance/useInstanceV2'; import { useInstanceV2 } from 'soapbox/api/hooks/instance/useInstanceV2';
import { instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance'; import { instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance';
import { useAppDispatch } from './useAppDispatch'; import { useAppDispatch } from './useAppDispatch';
interface Opts extends Pick<UseQueryOptions<unknown>, 'enabled' | 'retryOnMount'> {
/** The base URL of the instance. */
baseUrl?: string;
}
/** Get the Instance for the current backend. */ /** Get the Instance for the current backend. */
export function useInstance() { export function useInstance(opts: Opts = {}) {
const v2 = useInstanceV2(); const { baseUrl, retryOnMount = false } = opts;
const v1 = useInstanceV1({ enabled: v2.isError });
function retry(failureCount: number, error: Error): boolean {
if (error instanceof HTTPError && error.response.status === 404) {
return false;
} else {
return failureCount < 3;
}
}
const v2 = useInstanceV2({ baseUrl, retry, retryOnMount });
const v1 = useInstanceV1({ baseUrl, retry, retryOnMount, enabled: v2.isError });
const instance = useMemo(() => { const instance = useMemo(() => {
if (v2.instance) { if (v2.instance) {
@ -22,6 +39,7 @@ export function useInstance() {
}, [v2.instance, v1.instance]); }, [v2.instance, v1.instance]);
const props = v2.isError ? v1 : v2; const props = v2.isError ? v1 : v2;
const isNotFound = props.error instanceof HTTPError && props.error.response.status === 404;
// HACK: store the instance in Redux for legacy code // HACK: store the instance in Redux for legacy code
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -32,5 +50,5 @@ export function useInstance() {
}); });
}, [instance]); }, [instance]);
return { ...props, instance }; return { ...props, instance, isNotFound };
} }

View File

@ -68,14 +68,14 @@ const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
// Load initial data from the API // Load initial data from the API
useEffect(() => { useEffect(() => {
if (instance.isSuccess) { if (!instance.isLoading) {
dispatch(loadInitial()).then(() => { dispatch(loadInitial()).then(() => {
setIsLoaded(true); setIsLoaded(true);
}).catch(() => { }).catch(() => {
setIsLoaded(true); setIsLoaded(true);
}); });
} }
}, [instance.isSuccess]); }, [instance.isLoading]);
// intl is part of loading. // intl is part of loading.
// It's important nothing in here depends on intl. // It's important nothing in here depends on intl.

View File

@ -35,7 +35,7 @@ const SoapboxMount = () => {
const soapboxConfig = useSoapboxConfig(); const soapboxConfig = useSoapboxConfig();
const showCaptcha = account && account?.source?.ditto.captcha_solved === false; const showCaptcha = account?.source?.ditto.captcha_solved === false;
const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding); const needsOnboarding = useAppSelector(state => state.onboarding.needsOnboarding);
const showOnboarding = account && needsOnboarding; const showOnboarding = account && needsOnboarding;

View File

@ -23,7 +23,6 @@ import './styles/i18n/javanese.css';
import './styles/application.scss'; import './styles/application.scss';
import './styles/tailwind.css'; import './styles/tailwind.css';
import './precheck';
import ready from './ready'; import ready from './ready';
import { registerSW, lockSW } from './utils/sw'; import { registerSW, lockSW } from './utils/sw';

View File

@ -1,13 +0,0 @@
/**
* Precheck: information about the site before anything renders.
* @module soapbox/precheck
*/
/** Whether pre-rendered data exists in Pleroma's format. */
const hasPrerenderPleroma = Boolean(document.getElementById('initial-results'));
/** Whether pre-rendered data exists in Mastodon's format. */
const hasPrerenderMastodon = Boolean(document.getElementById('initial-state'));
/** Whether initial data was loaded into the page by server-side-rendering (SSR). */
export const isPrerendered = hasPrerenderPleroma || hasPrerenderMastodon;

View File

@ -2,33 +2,15 @@ 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 { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance';
import { InstanceV1, instanceV1Schema, InstanceV2, instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance';
import KVStore from 'soapbox/storage/kv-store';
import { ConfigDB } from 'soapbox/utils/config-db'; import { ConfigDB } from 'soapbox/utils/config-db';
import { import { fetchInstanceV2 } from '../actions/instance';
fetchInstance,
fetchInstanceV2,
} from '../actions/instance';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
const initialState: InstanceV2 = instanceV2Schema.parse({}); const initialState: InstanceV2 = instanceV2Schema.parse({});
const importInstanceV1 = (_state: InstanceV2, instance: InstanceV1): InstanceV2 => {
return upgradeInstance(instanceV1Schema.parse(instance));
};
const importInstanceV2 = (_state: InstanceV2, data: InstanceV2): InstanceV2 => {
return instanceV2Schema.parse(data);
};
const preloadImport = (state: InstanceV2, action: Record<string, any>, path: string) => {
const instance = action.data[path];
return instance ? importInstanceV1(state, instance) : state;
};
const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string) => { const getConfigValue = (instanceConfig: ImmutableMap<string, any>, key: string) => {
const v = instanceConfig const v = instanceConfig
.find(value => value.getIn(['tuple', 0]) === key); .find(value => value.getIn(['tuple', 0]) === key);
@ -61,60 +43,10 @@ const importConfigs = (state: InstanceV2, configs: ImmutableList<any>) => {
}); });
}; };
const handleAuthFetch = (state: InstanceV1 | InstanceV2) => { export default function instance(state = initialState, action: AnyAction): InstanceV2 {
// Authenticated fetch is enabled, so make the instance appear censored
return {
...state,
title: state.title || '██████',
description: state.description || '████████████',
};
};
const getHost = (instance: { uri?: string; domain?: string }) => {
const domain = instance.uri || instance.domain as string;
try {
return new URL(domain).host;
} catch {
try {
return new URL(`https://${domain}`).host;
} catch {
return null;
}
}
};
const persistInstance = ({ instance }: { instance: { uri: string } }, host: string | null = getHost(instance)) => {
if (host) {
KVStore.setItem(`instance:${host}`, instance).catch(console.error);
}
};
const persistInstanceV2 = ({ instance }: { instance: { domain: string } }, host: string | null = getHost(instance)) => {
if (host) {
KVStore.setItem(`instanceV2:${host}`, instance).catch(console.error);
}
};
const handleInstanceFetchFail = (state: InstanceV2, error: Record<string, any>) => {
if (error.response?.status === 401) {
return handleAuthFetch(state);
} else {
return state;
}
};
export default function instance(state = initialState, action: AnyAction) {
switch (action.type) { switch (action.type) {
case PLEROMA_PRELOAD_IMPORT:
return preloadImport(state, action, '/api/v1/instance');
case fetchInstance.fulfilled.type:
persistInstance(action.payload);
return importInstanceV1(state, action.payload.instance);
case fetchInstanceV2.fulfilled.type: case fetchInstanceV2.fulfilled.type:
persistInstanceV2(action.payload); return action.payload.instance;
return importInstanceV2(state, action.payload.instance);
case fetchInstance.rejected.type:
return handleInstanceFetchFail(state, action.error);
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)));

View File

@ -5,7 +5,6 @@
import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import * as BuildConfig from 'soapbox/build-config'; import * as BuildConfig from 'soapbox/build-config';
import { isPrerendered } from 'soapbox/precheck';
import { selectOwnAccount } from 'soapbox/selectors'; import { selectOwnAccount } from 'soapbox/selectors';
import { isURL } from 'soapbox/utils/auth'; import { isURL } from 'soapbox/utils/auth';
@ -21,15 +20,6 @@ export const federationRestrictionsDisclosed = (state: RootState): boolean => {
return !!state.instance.pleroma.metadata.federation.mrf_policies; return !!state.instance.pleroma.metadata.federation.mrf_policies;
}; };
/**
* Determine whether Soapbox is running in standalone mode.
* Standalone mode runs separately from any backend and can login anywhere.
*/
export const isStandalone = (state: RootState): boolean => {
const instanceFetchFailed = state.meta.instance_fetch_failed;
return isURL(BuildConfig.BACKEND_URL) ? false : (!isPrerendered && instanceFetchFailed);
};
const getHost = (url: any): string => { const getHost = (url: any): string => {
try { try {
return new URL(url).origin; return new URL(url).origin;