From ac50e5eebc4300dacb6bef6b76e63e6569f12adc Mon Sep 17 00:00:00 2001 From: Brad Dunbar Date: Tue, 30 Sep 2025 07:14:58 -0400 Subject: [PATCH] Convert mastodon/initial_state to TypeScript (#36274) --- .../mastodon/containers/compose_container.jsx | 2 +- .../mastodon/containers/mastodon.jsx | 2 +- .../mastodon/features/emoji/index.ts | 2 +- .../features/standalone/status/index.tsx | 2 +- app/javascript/mastodon/features/ui/index.jsx | 2 +- .../mastodon/features/ui/util/focusUtils.ts | 2 +- app/javascript/mastodon/initial_state.js | 145 ------------------ app/javascript/mastodon/initial_state.ts | 141 +++++++++++++++++ app/javascript/mastodon/utils/environment.ts | 2 +- 9 files changed, 148 insertions(+), 152 deletions(-) delete mode 100644 app/javascript/mastodon/initial_state.js create mode 100644 app/javascript/mastodon/initial_state.ts diff --git a/app/javascript/mastodon/containers/compose_container.jsx b/app/javascript/mastodon/containers/compose_container.jsx index a2513cc552d..3e6d20c74c4 100644 --- a/app/javascript/mastodon/containers/compose_container.jsx +++ b/app/javascript/mastodon/containers/compose_container.jsx @@ -5,7 +5,7 @@ import { fetchServer } from 'mastodon/actions/server'; import { hydrateStore } from 'mastodon/actions/store'; import { Router } from 'mastodon/components/router'; import Compose from 'mastodon/features/standalone/compose'; -import initialState from 'mastodon/initial_state'; +import { initialState } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index 086a7681c40..ee861366a50 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -13,7 +13,7 @@ import ErrorBoundary from 'mastodon/components/error_boundary'; import { Router } from 'mastodon/components/router'; import UI from 'mastodon/features/ui'; import { IdentityContext, createIdentityContext } from 'mastodon/identity_context'; -import initialState, { title as siteTitle } from 'mastodon/initial_state'; +import { initialState, title as siteTitle } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { store } from 'mastodon/store'; import { isProduction } from 'mastodon/utils/environment'; diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 99c16fe361c..d128da6b536 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,4 +1,4 @@ -import initialState from '@/mastodon/initial_state'; +import { initialState } from '@/mastodon/initial_state'; import { loadWorker } from '@/mastodon/utils/workers'; import { toSupportedLocale } from './locale'; diff --git a/app/javascript/mastodon/features/standalone/status/index.tsx b/app/javascript/mastodon/features/standalone/status/index.tsx index a7850eae1cc..a53d6e6b237 100644 --- a/app/javascript/mastodon/features/standalone/status/index.tsx +++ b/app/javascript/mastodon/features/standalone/status/index.tsx @@ -11,7 +11,7 @@ import { hydrateStore } from 'mastodon/actions/store'; import { Router } from 'mastodon/components/router'; import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; import { useRenderSignal } from 'mastodon/hooks/useRenderSignal'; -import initialState from 'mastodon/initial_state'; +import { initialState } from 'mastodon/initial_state'; import { IntlProvider } from 'mastodon/locales'; import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; import { store, useAppSelector, useAppDispatch } from 'mastodon/store'; diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index efec38caf4b..04c7f33dfd5 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -27,7 +27,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; +import { initialState, me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; diff --git a/app/javascript/mastodon/features/ui/util/focusUtils.ts b/app/javascript/mastodon/features/ui/util/focusUtils.ts index 9bcd3f89439..e46ede3553f 100644 --- a/app/javascript/mastodon/features/ui/util/focusUtils.ts +++ b/app/javascript/mastodon/features/ui/util/focusUtils.ts @@ -1,4 +1,4 @@ -import initialState from '@/mastodon/initial_state'; +import { initialState } from '@/mastodon/initial_state'; interface FocusColumnOptions { index?: number; diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js deleted file mode 100644 index 4a078b86f5b..00000000000 --- a/app/javascript/mastodon/initial_state.js +++ /dev/null @@ -1,145 +0,0 @@ -// @ts-check - -/** - * @typedef {[code: string, name: string, localName: string]} InitialStateLanguage - */ - -/** - * @typedef InitialStateMeta - * @property {string} access_token - * @property {boolean=} advanced_layout - * @property {boolean} auto_play_gif - * @property {boolean} activity_api_enabled - * @property {string} admin - * @property {boolean=} boost_modal - * @property {boolean=} delete_modal - * @property {boolean=} missing_alt_text_modal - * @property {boolean=} disable_swiping - * @property {boolean=} disable_hover_cards - * @property {string=} disabled_account_id - * @property {string} display_media - * @property {string} domain - * @property {boolean=} expand_spoilers - * @property {boolean} limited_federation_mode - * @property {string} locale - * @property {string | null} mascot - * @property {string=} me - * @property {string=} moved_to_account_id - * @property {string=} owner - * @property {boolean} profile_directory - * @property {boolean} registrations_open - * @property {boolean} reduce_motion - * @property {string} repository - * @property {boolean} search_enabled - * @property {boolean} trends_enabled - * @property {boolean} single_user_mode - * @property {string} source_url - * @property {string} streaming_api_base_url - * @property {boolean} timeline_preview - * @property {string} title - * @property {boolean} show_trends - * @property {boolean} trends_as_landing_page - * @property {boolean} use_blurhash - * @property {boolean=} use_pending_items - * @property {string} version - * @property {string} sso_redirect - * @property {string} status_page_url - * @property {boolean} terms_of_service_enabled - * @property {string?} emoji_style - */ - -/** - * @typedef Role - * @property {string} id - * @property {string} name - * @property {string} permissions - * @property {string} color - * @property {boolean} highlighted - */ - -/** - * @typedef InitialState - * @property {Record} accounts - * @property {InitialStateLanguage[]} languages - * @property {boolean=} critical_updates_pending - * @property {InitialStateMeta} meta - * @property {Role?} role - * @property {string[]} features - */ - -const element = document.getElementById('initial-state'); -/** @type {InitialState | undefined} */ -const initialState = element?.textContent && JSON.parse(element.textContent); - -/** @type {string} */ -const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? ''; -/** @type {boolean} */ -export const hasMultiColumnPath = initialPath === '/' - || initialPath === '/getting-started' - || initialPath === '/home' - || initialPath.startsWith('/deck'); - -/** - * @template {keyof InitialStateMeta} K - * @param {K} prop - * @returns {InitialStateMeta[K] | undefined} - */ -const getMeta = (prop) => initialState?.meta && initialState.meta[prop]; - -export const activityApiEnabled = getMeta('activity_api_enabled'); -export const autoPlayGif = getMeta('auto_play_gif'); -export const boostModal = getMeta('boost_modal'); -export const deleteModal = getMeta('delete_modal'); -export const missingAltTextModal = getMeta('missing_alt_text_modal'); -export const disableSwiping = getMeta('disable_swiping'); -export const disableHoverCards = getMeta('disable_hover_cards'); -export const disabledAccountId = getMeta('disabled_account_id'); -export const displayMedia = getMeta('display_media'); -export const domain = getMeta('domain'); -export const emojiStyle = getMeta('emoji_style') || 'auto'; -export const expandSpoilers = getMeta('expand_spoilers'); -export const forceSingleColumn = !getMeta('advanced_layout'); -export const limitedFederationMode = getMeta('limited_federation_mode'); -export const mascot = getMeta('mascot'); -export const me = getMeta('me'); -export const movedToAccountId = getMeta('moved_to_account_id'); -export const owner = getMeta('owner'); -export const profile_directory = getMeta('profile_directory'); -export const reduceMotion = getMeta('reduce_motion'); -export const registrationsOpen = getMeta('registrations_open'); -export const repository = getMeta('repository'); -export const searchEnabled = getMeta('search_enabled'); -export const trendsEnabled = getMeta('trends_enabled'); -export const showTrends = getMeta('show_trends'); -export const singleUserMode = getMeta('single_user_mode'); -export const source_url = getMeta('source_url'); -export const timelinePreview = getMeta('timeline_preview'); -export const title = getMeta('title'); -export const trendsAsLanding = getMeta('trends_as_landing_page'); -export const useBlurhash = getMeta('use_blurhash'); -export const usePendingItems = getMeta('use_pending_items'); -export const version = getMeta('version'); -export const criticalUpdatesPending = initialState?.critical_updates_pending; -export const statusPageUrl = getMeta('status_page_url'); -export const sso_redirect = getMeta('sso_redirect'); -export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); - -const displayNames = Intl.DisplayNames && new Intl.DisplayNames(getMeta('locale'), { - type: 'language', - fallback: 'none', - languageDisplay: 'standard', -}); - -export const languages = initialState?.languages?.map(lang => { - // zh-YUE is not a valid CLDR unicode_language_id - return [lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) || lang[1], lang[2]]; -}); - -/** - * @returns {string | undefined} - */ -export function getAccessToken() { - return getMeta('access_token'); -} - -export default initialState; diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts new file mode 100644 index 00000000000..b6d7d55483f --- /dev/null +++ b/app/javascript/mastodon/initial_state.ts @@ -0,0 +1,141 @@ +import type { ApiAccountJSON } from './api_types/accounts'; + +type InitialStateLanguage = [code: string, name: string, localName: string]; + +interface InitialStateMeta { + access_token: string; + advanced_layout?: boolean; + auto_play_gif: boolean; + activity_api_enabled: boolean; + admin: string; + boost_modal?: boolean; + delete_modal?: boolean; + missing_alt_text_modal?: boolean; + disable_swiping?: boolean; + disable_hover_cards?: boolean; + disabled_account_id?: string; + display_media: string; + domain: string; + expand_spoilers?: boolean; + limited_federation_mode: boolean; + locale: string; + mascot: string | null; + me?: string; + moved_to_account_id?: string; + owner?: string; + profile_directory: boolean; + registrations_open: boolean; + reduce_motion: boolean; + repository: string; + search_enabled: boolean; + trends_enabled: boolean; + single_user_mode: boolean; + source_url: string; + streaming_api_base_url: string; + timeline_preview: boolean; + title: string; + show_trends: boolean; + trends_as_landing_page: boolean; + use_blurhash: boolean; + use_pending_items?: boolean; + version: string; + sso_redirect: string; + status_page_url: string; + terms_of_service_enabled: boolean; + emoji_style?: string; +} + +interface Role { + id: string; + name: string; + permissions: string; + color: string; + highlighted: boolean; +} + +export interface InitialState { + accounts: Record; + languages: InitialStateLanguage[]; + critical_updates_pending?: boolean; + meta: InitialStateMeta; + role?: Role; + features: string[]; +} + +const element = document.getElementById('initial-state'); +export const initialState: InitialState | undefined = element?.textContent + ? (JSON.parse(element.textContent) as InitialState) + : undefined; + +const initialPath: string = + document + .querySelector('head meta[name=initialPath]') + ?.getAttribute('content') ?? ''; +export const hasMultiColumnPath: boolean = + initialPath === '/' || + initialPath === '/getting-started' || + initialPath === '/home' || + initialPath.startsWith('/deck'); + +function getMeta( + prop: K, +): InitialStateMeta[K] | undefined { + return initialState?.meta[prop]; +} + +export const activityApiEnabled = getMeta('activity_api_enabled'); +export const autoPlayGif = getMeta('auto_play_gif'); +export const boostModal = getMeta('boost_modal'); +export const deleteModal = getMeta('delete_modal'); +export const missingAltTextModal = getMeta('missing_alt_text_modal'); +export const disableSwiping = getMeta('disable_swiping'); +export const disableHoverCards = getMeta('disable_hover_cards'); +export const disabledAccountId = getMeta('disabled_account_id'); +export const displayMedia = getMeta('display_media'); +export const domain = getMeta('domain'); +export const emojiStyle = getMeta('emoji_style') ?? 'auto'; +export const expandSpoilers = getMeta('expand_spoilers'); +export const forceSingleColumn = !getMeta('advanced_layout'); +export const limitedFederationMode = getMeta('limited_federation_mode'); +export const mascot = getMeta('mascot'); +export const me = getMeta('me'); +export const movedToAccountId = getMeta('moved_to_account_id'); +export const owner = getMeta('owner'); +export const profile_directory = getMeta('profile_directory'); +export const reduceMotion = getMeta('reduce_motion'); +export const registrationsOpen = getMeta('registrations_open'); +export const repository = getMeta('repository'); +export const searchEnabled = getMeta('search_enabled'); +export const trendsEnabled = getMeta('trends_enabled'); +export const showTrends = getMeta('show_trends'); +export const singleUserMode = getMeta('single_user_mode'); +export const source_url = getMeta('source_url'); +export const timelinePreview = getMeta('timeline_preview'); +export const title = getMeta('title'); +export const trendsAsLanding = getMeta('trends_as_landing_page'); +export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); +export const version = getMeta('version'); +export const criticalUpdatesPending = initialState?.critical_updates_pending; +export const statusPageUrl = getMeta('status_page_url'); +export const sso_redirect = getMeta('sso_redirect'); +export const termsOfServiceEnabled = getMeta('terms_of_service_enabled'); + +const displayNames = new Intl.DisplayNames(getMeta('locale'), { + type: 'language', + fallback: 'none', + languageDisplay: 'standard', +}); + +export const languages = initialState?.languages.map((lang) => { + // zh-YUE is not a valid CLDR unicode_language_id + return [ + lang[0], + displayNames.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1], + lang[2], + ]; +}); + +export function getAccessToken(): string | undefined { + return getMeta('access_token'); +} diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 2d544417e3d..c666e2c94d7 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -1,4 +1,4 @@ -import initialState from '../initial_state'; +import { initialState } from '../initial_state'; export function isDevelopment() { if (typeof process !== 'undefined')