diff --git a/src/server/utils/create-ssr-html.tsx b/src/server/utils/create-ssr-html.tsx index 1655d3c8..c47e1dae 100644 --- a/src/server/utils/create-ssr-html.tsx +++ b/src/server/utils/create-ssr-html.tsx @@ -79,6 +79,12 @@ export async function createSsrHtml( + ${lazyScripts} diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx index bcdf14f5..970c3a4d 100644 --- a/src/shared/components/app/app.tsx +++ b/src/shared/components/app/app.tsx @@ -1,5 +1,4 @@ import { isAnonymousPath, isAuthPath, setIsoData } from "@utils/app"; -import { dataBsTheme } from "@utils/browser"; import { Component, RefObject, createRef, linkEvent } from "inferno"; import { Provider } from "inferno-i18next-dess"; import { Route, Switch } from "inferno-router"; @@ -14,7 +13,6 @@ import { Navbar } from "./navbar"; import "./styles.scss"; import { Theme } from "./theme"; import AnonymousGuard from "../common/anonymous-guard"; -import { CodeTheme } from "./code-theme"; export class App extends Component { private isoData: IsoDataOptionalSite = setIsoData(this.context); @@ -36,11 +34,7 @@ export class App extends Component { return ( <> -
+
{siteView && ( - <> - - - + )}
diff --git a/src/shared/components/app/code-theme.tsx b/src/shared/components/app/code-theme.tsx index 9cc016ef..c454ed56 100644 --- a/src/shared/components/app/code-theme.tsx +++ b/src/shared/components/app/code-theme.tsx @@ -1,18 +1,34 @@ import { dataBsTheme } from "@utils/browser"; import { Component } from "inferno"; import { Helmet } from "inferno-helmet"; -import { UserService } from "../../services"; interface CodeThemeProps { - defaultTheme: string; + theme: string; } export class CodeTheme extends Component { render() { - const user = UserService.Instance.myUserInfo; - const userTheme = user?.local_user_view.local_user.theme; - const theme = - user && userTheme !== "browser" ? userTheme : this.props.defaultTheme; + const { theme } = this.props; + const hasTheme = theme !== "browser" && theme !== "browser-compact"; + + if (!hasTheme) { + return ( + + + + + ); + } return ( diff --git a/src/shared/components/app/theme.tsx b/src/shared/components/app/theme.tsx index 07c4f8c7..b577e435 100644 --- a/src/shared/components/app/theme.tsx +++ b/src/shared/components/app/theme.tsx @@ -1,76 +1,168 @@ import { Component } from "inferno"; import { Helmet } from "inferno-helmet"; import { UserService } from "../../services"; +import { dataBsTheme, isBrowser } from "@utils/browser"; +import { CodeTheme } from "./code-theme"; interface Props { defaultTheme: string; } -export class Theme extends Component { - render() { - const user = UserService.Instance.myUserInfo; - const hasTheme = user?.local_user_view.local_user.theme !== "browser"; +interface State { + themeOverride?: string; + graceTheme?: string; +} - if (user && hasTheme) { +export class Theme extends Component { + private lightQuery?: MediaQueryList; + constructor(props, context) { + super(props, context); + if (isBrowser()) { + window.addEventListener("refresh-theme", this.eventListener); + window.addEventListener("set-theme-override", this.eventListener); + this.lightQuery = window.matchMedia("(prefers-color-scheme: light)"); + this.lightQuery.addEventListener("change", this.eventListener); + } + } + + private graceTimer; + private eventListener = e => { + if (e.type === "refresh-theme" || e.type === "change") { + this.forceUpdate(); + } else if (e.type === "set-theme-override") { + if (e.detail?.theme) { + this.setState({ + themeOverride: e.detail.theme, + graceTheme: this.state?.themeOverride ?? this.currentTheme(), + }); + // Keep both themes enabled for one second. Avoids unstyled flashes. + clearTimeout(this.graceTimer); + this.graceTimer = setTimeout(() => { + this.setState({ graceTheme: undefined }); + }, 1000); + } else { + this.setState({ themeOverride: undefined, graceTheme: undefined }); + } + } + }; + + componentWillUnmount(): void { + if (isBrowser()) { + window.removeEventListener("refresh-theme", this.eventListener); + this.lightQuery?.removeEventListener("change", this.eventListener); + } + } + + currentTheme(): string { + const user = UserService.Instance.myUserInfo; + const userTheme = user?.local_user_view.local_user.theme; + return userTheme ?? "browser"; + } + + render() { + if (this.state?.themeOverride) { + if (!this.state.graceTheme) { + return this.renderTheme(this.state.themeOverride); + } + // Render both themes to prevent rendering without theme. + return [ + this.renderTheme(this.state.graceTheme ?? this.currentTheme()), + this.renderTheme(this.state.themeOverride), + ]; + } + + return this.renderTheme(this.currentTheme()); + } + + renderTheme(theme: string) { + const hasTheme = theme !== "browser" && theme !== "browser-compact"; + + const detectedBsTheme = {}; + if (this.lightQuery) { + detectedBsTheme["data-bs-theme"] = this.lightQuery.matches + ? "light" + : "dark"; + } + + if (theme && hasTheme) { return ( - - - + <> + + + + + ); } else if ( this.props.defaultTheme !== "browser" && this.props.defaultTheme !== "browser-compact" ) { return ( - - - + <> + + + + + ); - } else if (this.props.defaultTheme === "browser-compact") { + } else if ( + this.props.defaultTheme === "browser-compact" || + theme === "browser-compact" + ) { return ( - - - - + <> + + + + + + ); } else { return ( - - - - + <> + + + + + + ); } } diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx index 1d2a69e2..48de62a2 100644 --- a/src/shared/components/home/admin-settings.tsx +++ b/src/shared/components/home/admin-settings.tsx @@ -102,6 +102,9 @@ export class AdminSettings extends Component { async componentDidMount() { if (!this.state.isIsomorphic) { await this.fetchData(); + } else { + const themeList = await fetchThemeList(); + this.setState({ themeList }); } } diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx index 8beba844..d0c1becf 100644 --- a/src/shared/components/home/login.tsx +++ b/src/shared/components/home/login.tsx @@ -1,5 +1,5 @@ import { setIsoData } from "@utils/app"; -import { isBrowser, updateDataBsTheme } from "@utils/browser"; +import { isBrowser, refreshTheme } from "@utils/browser"; import { getQueryParams } from "@utils/helpers"; import { Component, linkEvent } from "inferno"; import { RouteComponentProps } from "inferno-router/dist/Route"; @@ -47,7 +47,7 @@ async function handleLoginSuccess(i: Login, loginRes: LoginResponse) { if (site.state === "success") { UserService.Instance.myUserInfo = site.data.my_user; - updateDataBsTheme(site.data); + refreshTheme(); } const { prev } = getLoginQueryParams(); diff --git a/src/shared/components/person/settings.tsx b/src/shared/components/person/settings.tsx index 23ed4c8a..2760958a 100644 --- a/src/shared/components/person/settings.tsx +++ b/src/shared/components/person/settings.tsx @@ -7,7 +7,6 @@ import { myAuth, personToChoice, setIsoData, - setTheme, showLocal, updateCommunityBlock, updateInstanceBlock, @@ -67,7 +66,7 @@ import { PersonListing } from "./person-listing"; import { InitialFetchRequest } from "../../interfaces"; import TotpModal from "../common/totp-modal"; import { LoadingEllipses } from "../common/loading-ellipses"; -import { updateDataBsTheme } from "../../utils/browser"; +import { refreshTheme, setThemeOverride } from "../../utils/browser"; import { getHttpBaseInternal } from "../../utils/env"; type SettingsData = RouteDataResponse<{ @@ -342,6 +341,7 @@ export class Settings extends Component { componentWillUnmount(): void { // In case `interface_language` change wasn't saved. loadUserLanguage(); + setThemeOverride(undefined); } static async fetchInitialData({ @@ -1453,7 +1453,7 @@ export class Settings extends Component { handleThemeChange(i: Settings, event: any) { i.setState(s => ((s.saveUserSettingsForm.theme = event.target.value), s)); - setTheme(event.target.value, true); + setThemeOverride(event.target.value); } handleInterfaceLangChange(i: Settings, event: any) { @@ -1571,6 +1571,7 @@ export class Settings extends Component { window.scrollTo(0, 0); } + setThemeOverride(undefined); i.setState({ saveRes }); } @@ -1665,7 +1666,7 @@ export class Settings extends Component { } = siteRes.data.my_user!.local_user_view; UserService.Instance.myUserInfo = siteRes.data.my_user; - updateDataBsTheme(siteRes.data); + refreshTheme(); i.setState(prev => ({ ...prev, diff --git a/src/shared/utils/app/index.ts b/src/shared/utils/app/index.ts index 870fa7fb..484bae02 100644 --- a/src/shared/utils/app/index.ts +++ b/src/shared/utils/app/index.ts @@ -44,7 +44,6 @@ import postToCommentSortType from "./post-to-comment-sort-type"; import searchCommentTree from "./search-comment-tree"; import selectableLanguages from "./selectable-languages"; import setIsoData from "./set-iso-data"; -import setTheme from "./set-theme"; import setupDateFns from "./setup-date-fns"; import showAvatars from "./show-avatars"; import showLocal from "./show-local"; @@ -103,7 +102,6 @@ export { searchCommentTree, selectableLanguages, setIsoData, - setTheme, setupDateFns, showAvatars, showLocal, diff --git a/src/shared/utils/app/initialize-site.ts b/src/shared/utils/app/initialize-site.ts index b9fa1f09..d6c6511e 100644 --- a/src/shared/utils/app/initialize-site.ts +++ b/src/shared/utils/app/initialize-site.ts @@ -1,11 +1,9 @@ import { GetSiteResponse } from "lemmy-js-client"; import { setupEmojiDataModel, setupMarkdown } from "../../markdown"; import { UserService } from "../../services"; -import { updateDataBsTheme } from "@utils/browser"; export default function initializeSite(site?: GetSiteResponse) { UserService.Instance.myUserInfo = site?.my_user; - updateDataBsTheme(site); if (site) { setupEmojiDataModel(site.custom_emojis ?? []); } diff --git a/src/shared/utils/app/set-theme.ts b/src/shared/utils/app/set-theme.ts deleted file mode 100644 index 348e0ba6..00000000 --- a/src/shared/utils/app/set-theme.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { fetchThemeList } from "@utils/app"; -import { dataBsTheme, isBrowser, loadCss } from "@utils/browser"; - -export default async function setTheme(theme: string, forceReload = false) { - if (!isBrowser()) { - return; - } - if (theme === "browser" && !forceReload) { - return; - } - // This is only run on a force reload - if (theme === "browser") { - theme = "darkly"; - } - - const themeList = await fetchThemeList(); - - // Unload all the other themes - for (var i = 0; i < themeList.length; i++) { - const styleSheet = document.getElementById(themeList[i]); - if (styleSheet) { - styleSheet.setAttribute("disabled", "disabled"); - } - } - - document - .getElementById("default-light") - ?.setAttribute("disabled", "disabled"); - document.getElementById("default-dark")?.setAttribute("disabled", "disabled"); - - // Load the theme dynamically - const cssLoc = `/css/themes/${theme}.css`; - - loadCss(theme, cssLoc); - document - .getElementById("app") - ?.setAttribute("data-bs-theme", dataBsTheme(theme)); - document.getElementById(theme)?.removeAttribute("disabled"); -} diff --git a/src/shared/utils/browser/index.ts b/src/shared/utils/browser/index.ts index 8aac65aa..98c2bba8 100644 --- a/src/shared/utils/browser/index.ts +++ b/src/shared/utils/browser/index.ts @@ -3,13 +3,13 @@ import clearAuthCookie from "./clear-auth-cookie"; import dataBsTheme from "./data-bs-theme"; import isBrowser from "./is-browser"; import isDark from "./is-dark"; -import loadCss from "./load-css"; import platform from "./platform"; +import refreshTheme from "./refresh-theme"; import restoreScrollPosition from "./restore-scroll-position"; import saveScrollPosition from "./save-scroll-position"; import setAuthCookie from "./set-auth-cookie"; +import setThemeOverride from "./set-theme-override"; import share from "./share"; -import updateDataBsTheme from "./update-data-bs-theme"; export { canShare, @@ -17,11 +17,11 @@ export { dataBsTheme, isBrowser, isDark, - loadCss, platform, + refreshTheme, restoreScrollPosition, saveScrollPosition, setAuthCookie, + setThemeOverride, share, - updateDataBsTheme, }; diff --git a/src/shared/utils/browser/load-css.ts b/src/shared/utils/browser/load-css.ts deleted file mode 100644 index 4b4b86e3..00000000 --- a/src/shared/utils/browser/load-css.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default function loadCss(id: string, loc: string) { - if (!document.getElementById(id)) { - var head = document.getElementsByTagName("head")[0]; - var link = document.createElement("link"); - link.id = id; - link.rel = "stylesheet"; - link.type = "text/css"; - link.href = loc; - link.media = "all"; - head.appendChild(link); - } -} diff --git a/src/shared/utils/browser/refresh-theme.tsx b/src/shared/utils/browser/refresh-theme.tsx new file mode 100644 index 00000000..b7ef2c18 --- /dev/null +++ b/src/shared/utils/browser/refresh-theme.tsx @@ -0,0 +1,7 @@ +import { isBrowser } from "."; + +export default function refreshTheme() { + if (isBrowser()) { + window.dispatchEvent(new CustomEvent("refresh-theme")); + } +} diff --git a/src/shared/utils/browser/set-theme-override.ts b/src/shared/utils/browser/set-theme-override.ts new file mode 100644 index 00000000..a5f76fb0 --- /dev/null +++ b/src/shared/utils/browser/set-theme-override.ts @@ -0,0 +1,10 @@ +import { isBrowser } from "@utils/browser"; + +export default async function setThemeOverride(theme?: string) { + if (!isBrowser()) { + return; + } + window.dispatchEvent( + new CustomEvent("set-theme-override", { detail: { theme } }), + ); +} diff --git a/src/shared/utils/browser/update-data-bs-theme.ts b/src/shared/utils/browser/update-data-bs-theme.ts deleted file mode 100644 index a277572b..00000000 --- a/src/shared/utils/browser/update-data-bs-theme.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GetSiteResponse } from "lemmy-js-client"; -import isBrowser from "./is-browser"; -import dataBsTheme from "./data-bs-theme"; - -export default function updateDataBsTheme(siteRes?: GetSiteResponse) { - if (isBrowser()) { - document - .getElementById("app") - ?.setAttribute("data-bs-theme", dataBsTheme(siteRes)); - } -}