From 82731e633d6f964e2fe08623ec1703437b50c271 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 11 Oct 2024 15:12:30 -0500 Subject: [PATCH] Make instance stop aggressively refreshing, replace isStandalone with instance.isNotFound --- src/actions/auth.ts | 6 +- src/api/hooks/instance/useInstanceV1.ts | 10 +-- src/api/hooks/instance/useInstanceV2.ts | 10 +-- .../auth-login/components/login-page.tsx | 9 ++- src/features/ui/components/navbar.tsx | 7 +- src/features/ui/index.tsx | 10 +-- src/hooks/useInstance.ts | 26 ++++++- src/init/soapbox-load.tsx | 4 +- src/init/soapbox-mount.tsx | 4 +- src/main.tsx | 1 - src/precheck.ts | 13 ---- src/reducers/instance.ts | 76 +------------------ src/utils/state.ts | 10 --- 13 files changed, 52 insertions(+), 134 deletions(-) delete mode 100644 src/precheck.ts diff --git a/src/actions/auth.ts b/src/actions/auth.ts index bd5dd726b..f84e1df40 100644 --- a/src/actions/auth.ts +++ b/src/actions/auth.ts @@ -24,7 +24,6 @@ import { getLoggedInAccount, parseBaseURL } from 'soapbox/utils/auth'; import sourceCode from 'soapbox/utils/code'; import { normalizeUsername } from 'soapbox/utils/input'; import { getScopes } from 'soapbox/utils/scopes'; -import { isStandalone } from 'soapbox/utils/state'; import api, { baseClient } from '../api'; @@ -201,11 +200,10 @@ export const logIn = (username: string, password: string) => export const deleteSession = () => (dispatch: AppDispatch, getState: () => RootState) => api(getState).delete('/api/sign_out'); -export const logOut = () => +export const logOut = (refresh?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); const account = getLoggedInAccount(state); - const standalone = isStandalone(state); if (!account) return dispatch(noOp); @@ -229,7 +227,7 @@ export const logOut = () => localStorage.removeItem('soapbox:external:baseurl'); localStorage.removeItem('soapbox:external:scopes'); - dispatch({ type: AUTH_LOGGED_OUT, account, standalone }); + dispatch({ type: AUTH_LOGGED_OUT, account, standalone: refresh }); toast.success(messages.loggedOut); }); diff --git a/src/api/hooks/instance/useInstanceV1.ts b/src/api/hooks/instance/useInstanceV1.ts index 07d44c4a2..368256de8 100644 --- a/src/api/hooks/instance/useInstanceV1.ts +++ b/src/api/hooks/instance/useInstanceV1.ts @@ -1,20 +1,18 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks'; import { InstanceV1, instanceV1Schema } from 'soapbox/schemas/instance'; -interface Opts { +interface Opts extends Pick, 'enabled' | 'retry' | 'retryOnMount'> { /** 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(opts: Opts = {}) { const api = useApi(); - const { baseUrl, enabled } = opts; + const { baseUrl } = opts; const { data: instance, ...rest } = useQuery({ queryKey: ['instance', baseUrl ?? api.baseUrl, 'v1'], @@ -23,7 +21,7 @@ export function useInstanceV1(opts: Opts = {}) { const data = await response.json(); return instanceV1Schema.parse(data); }, - enabled, + ...opts, }); return { instance, ...rest }; diff --git a/src/api/hooks/instance/useInstanceV2.ts b/src/api/hooks/instance/useInstanceV2.ts index 933a3e4f3..3cc7f12c2 100644 --- a/src/api/hooks/instance/useInstanceV2.ts +++ b/src/api/hooks/instance/useInstanceV2.ts @@ -1,20 +1,18 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, UseQueryOptions } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks'; import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance'; -interface Opts { +interface Opts extends Pick, 'enabled' | 'retry' | 'retryOnMount'> { /** 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(opts: Opts = {}) { const api = useApi(); - const { baseUrl, enabled } = opts; + const { baseUrl } = opts; const { data: instance, ...rest } = useQuery({ queryKey: ['instance', baseUrl ?? api.baseUrl, 'v2'], @@ -23,7 +21,7 @@ export function useInstanceV2(opts: Opts = {}) { const data = await response.json(); return instanceV2Schema.parse(data); }, - enabled, + ...opts, }); return { instance, ...rest }; diff --git a/src/features/auth-login/components/login-page.tsx b/src/features/auth-login/components/login-page.tsx index 9ceec4a55..50dc1ac83 100644 --- a/src/features/auth-login/components/login-page.tsx +++ b/src/features/auth-login/components/login-page.tsx @@ -6,9 +6,8 @@ import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth'; import { fetchInstance } from 'soapbox/actions/instance'; import { closeModal, openModal } from 'soapbox/actions/modals'; 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 { isStandalone } from 'soapbox/utils/state'; import ConsumersList from './consumers-list'; import LoginForm from './login-form'; @@ -20,7 +19,7 @@ const LoginPage = () => { const dispatch = useAppDispatch(); const me = useAppSelector((state) => state.me); - const standalone = useAppSelector((state) => isStandalone(state)); + const instance = useInstance(); const { nostrSignup } = useFeatures(); const token = new URLSearchParams(window.location.search).get('token'); @@ -68,7 +67,9 @@ const LoginPage = () => { return ; } - if (standalone) return ; + if (instance.isNotFound) { + return ; + } if (shouldRedirect) { const redirectUri = getRedirectUrl(); diff --git a/src/features/ui/components/navbar.tsx b/src/features/ui/components/navbar.tsx index 9af98cd8f..627236d6e 100644 --- a/src/features/ui/components/navbar.tsx +++ b/src/features/ui/components/navbar.tsx @@ -10,10 +10,9 @@ import { openSidebar } from 'soapbox/actions/sidebar'; import SiteLogo from 'soapbox/components/site-logo'; import { Avatar, Button, Counter, Form, HStack, IconButton, Input, Tooltip } from 'soapbox/components/ui'; 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 { useSettingsNotifications } from 'soapbox/hooks/useSettingsNotifications'; -import { isStandalone } from 'soapbox/utils/state'; import ProfileDropdown from './profile-dropdown'; @@ -31,7 +30,7 @@ const Navbar = () => { const dispatch = useAppDispatch(); const intl = useIntl(); const features = useFeatures(); - const standalone = useAppSelector(isStandalone); + const instance = useInstance(); const { isOpen } = useRegistrationStatus(); const { account } = useOwnAccount(); const node = useRef(null); @@ -121,7 +120,7 @@ const Navbar = () => { )} - {!standalone && ( + {instance.isSuccess && ( {account ? (
diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index 2fd5fac92..711f14289 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -35,7 +35,6 @@ import RemoteInstancePage from 'soapbox/pages/remote-instance-page'; import SearchPage from 'soapbox/pages/search-page'; import StatusPage from 'soapbox/pages/status-page'; import { getVapidKey } from 'soapbox/utils/auth'; -import { isStandalone } from 'soapbox/utils/state'; import BackgroundShapes from './components/background-shapes'; import FloatingActionButton from './components/floating-action-button'; @@ -157,11 +156,10 @@ interface ISwitchingColumnsArea { } const SwitchingColumnsArea: React.FC = ({ children }) => { - const { instance } = useInstance(); + const { instance, isNotFound } = useInstance(); const features = useFeatures(); const { search } = useLocation(); const { isLoggedIn } = useLoggedIn(); - const standalone = useAppSelector(isStandalone); const { authenticatedProfile, cryptoAddresses } = useSoapboxConfig(); const hasCrypto = cryptoAddresses.size > 0; @@ -173,7 +171,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => // Ex: use /login instead of /auth, but redirect /auth to /login return ( - {standalone && } + {isNotFound && } @@ -388,11 +386,11 @@ const UI: React.FC = ({ children }) => { const node = useRef(null); const me = useAppSelector(state => state.me); const { account } = useOwnAccount(); + const instance = useInstance(); const features = useFeatures(); const vapidKey = useAppSelector(state => getVapidKey(state)); const dropdownMenuIsOpen = useAppSelector(state => state.dropdown_menu.isOpen); - const standalone = useAppSelector(isStandalone); const { isDragging } = useDraggedFiles(node); @@ -503,7 +501,7 @@ const UI: React.FC = ({ children }) => { - {!standalone && } + {instance.isSuccess && } diff --git a/src/hooks/useInstance.ts b/src/hooks/useInstance.ts index 6e7edc21d..d23ac36bf 100644 --- a/src/hooks/useInstance.ts +++ b/src/hooks/useInstance.ts @@ -1,15 +1,32 @@ +import { UseQueryOptions } from '@tanstack/react-query'; import { useEffect, useMemo } from 'react'; +import { HTTPError } from 'soapbox/api/HTTPError'; import { useInstanceV1 } from 'soapbox/api/hooks/instance/useInstanceV1'; import { useInstanceV2 } from 'soapbox/api/hooks/instance/useInstanceV2'; import { instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance'; import { useAppDispatch } from './useAppDispatch'; +interface Opts extends Pick, 'enabled' | 'retryOnMount'> { + /** The base URL of the instance. */ + baseUrl?: string; +} + /** Get the Instance for the current backend. */ -export function useInstance() { - const v2 = useInstanceV2(); - const v1 = useInstanceV1({ enabled: v2.isError }); +export function useInstance(opts: Opts = {}) { + const { baseUrl, retryOnMount = false } = opts; + + 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(() => { if (v2.instance) { @@ -22,6 +39,7 @@ export function useInstance() { }, [v2.instance, v1.instance]); 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 const dispatch = useAppDispatch(); @@ -32,5 +50,5 @@ export function useInstance() { }); }, [instance]); - return { ...props, instance }; + return { ...props, instance, isNotFound }; } diff --git a/src/init/soapbox-load.tsx b/src/init/soapbox-load.tsx index f15814a07..97c0a7efc 100644 --- a/src/init/soapbox-load.tsx +++ b/src/init/soapbox-load.tsx @@ -68,14 +68,14 @@ const SoapboxLoad: React.FC = ({ children }) => { // Load initial data from the API useEffect(() => { - if (instance.isSuccess) { + if (!instance.isLoading) { dispatch(loadInitial()).then(() => { setIsLoaded(true); }).catch(() => { setIsLoaded(true); }); } - }, [instance.isSuccess]); + }, [instance.isLoading]); // intl is part of loading. // It's important nothing in here depends on intl. diff --git a/src/init/soapbox-mount.tsx b/src/init/soapbox-mount.tsx index d91c71c3e..71942395c 100644 --- a/src/init/soapbox-mount.tsx +++ b/src/init/soapbox-mount.tsx @@ -35,7 +35,7 @@ const SoapboxMount = () => { 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 showOnboarding = account && needsOnboarding; @@ -48,7 +48,7 @@ const SoapboxMount = () => { if (showOnboarding) { dispatch(openModal('ONBOARDING_FLOW')); } - + const { redirectRootNoLogin, gdpr } = soapboxConfig; // @ts-ignore: I don't actually know what these should be, lol diff --git a/src/main.tsx b/src/main.tsx index 6be862eae..78555853e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -23,7 +23,6 @@ import './styles/i18n/javanese.css'; import './styles/application.scss'; import './styles/tailwind.css'; -import './precheck'; import ready from './ready'; import { registerSW, lockSW } from './utils/sw'; diff --git a/src/precheck.ts b/src/precheck.ts deleted file mode 100644 index 03fea80a1..000000000 --- a/src/precheck.ts +++ /dev/null @@ -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; diff --git a/src/reducers/instance.ts b/src/reducers/instance.ts index ea44aaee5..ad16cbfe8 100644 --- a/src/reducers/instance.ts +++ b/src/reducers/instance.ts @@ -2,33 +2,15 @@ import { produce } from 'immer'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin'; -import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload'; -import { InstanceV1, instanceV1Schema, InstanceV2, instanceV2Schema, upgradeInstance } from 'soapbox/schemas/instance'; -import KVStore from 'soapbox/storage/kv-store'; +import { InstanceV2, instanceV2Schema } from 'soapbox/schemas/instance'; import { ConfigDB } from 'soapbox/utils/config-db'; -import { - fetchInstance, - fetchInstanceV2, -} from '../actions/instance'; +import { fetchInstanceV2 } from '../actions/instance'; import type { AnyAction } from 'redux'; 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, path: string) => { - const instance = action.data[path]; - return instance ? importInstanceV1(state, instance) : state; -}; - const getConfigValue = (instanceConfig: ImmutableMap, key: string) => { const v = instanceConfig .find(value => value.getIn(['tuple', 0]) === key); @@ -61,60 +43,10 @@ const importConfigs = (state: InstanceV2, configs: ImmutableList) => { }); }; -const handleAuthFetch = (state: InstanceV1 | 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) => { - if (error.response?.status === 401) { - return handleAuthFetch(state); - } else { - return state; - } -}; - -export default function instance(state = initialState, action: AnyAction) { +export default function instance(state = initialState, action: AnyAction): InstanceV2 { 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: - persistInstanceV2(action.payload); - return importInstanceV2(state, action.payload.instance); - case fetchInstance.rejected.type: - return handleInstanceFetchFail(state, action.error); + return action.payload.instance; case ADMIN_CONFIG_UPDATE_REQUEST: case ADMIN_CONFIG_UPDATE_SUCCESS: return importConfigs(state, ImmutableList(fromJS(action.configs))); diff --git a/src/utils/state.ts b/src/utils/state.ts index f2cf27660..27dbcf33b 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -5,7 +5,6 @@ import { getSoapboxConfig } from 'soapbox/actions/soapbox'; import * as BuildConfig from 'soapbox/build-config'; -import { isPrerendered } from 'soapbox/precheck'; import { selectOwnAccount } from 'soapbox/selectors'; import { isURL } from 'soapbox/utils/auth'; @@ -21,15 +20,6 @@ export const federationRestrictionsDisclosed = (state: RootState): boolean => { 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 => { try { return new URL(url).origin;