Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do most of the theme handling from the Theme component #2390

Merged
merged 8 commits into from
Mar 14, 2024
6 changes: 6 additions & 0 deletions src/server/utils/create-ssr-html.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ export async function createSsrHtml(
<!DOCTYPE html>
<html ${helmet.htmlAttributes.toString()}>
<head>
<script nonce="${cspNonce}">
if (!document.documentElement.hasAttribute("data-bs-theme")) {
const light = window.matchMedia("(prefers-color-scheme: light)").matches;
document.documentElement.setAttribute("data-bs-theme", light ? "light" : "dark");
}
</script>
${lazyScripts}
<script nonce="${cspNonce}">window.isoData = ${serialize(isoData)}</script>

Expand Down
13 changes: 2 additions & 11 deletions src/shared/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<any, any> {
private isoData: IsoDataOptionalSite = setIsoData(this.context);
Expand All @@ -36,11 +34,7 @@ export class App extends Component<any, any> {
return (
<>
<Provider i18next={I18NextService.i18n}>
<div
id="app"
className="lemmy-site"
data-bs-theme={dataBsTheme(siteRes)}
>
<div id="app" className="lemmy-site">
<button
type="button"
className="btn skip-link bg-light position-absolute start-0 z-3"
Expand All @@ -49,10 +43,7 @@ export class App extends Component<any, any> {
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
</button>
{siteView && (
<>
<Theme defaultTheme={siteView.local_site.default_theme} />
<CodeTheme defaultTheme={siteView.local_site.default_theme} />
</>
<Theme defaultTheme={siteView.local_site.default_theme} />
)}
<Navbar siteRes={siteRes} />
<div className="mt-4 p-0 fl-1">
Expand Down
28 changes: 22 additions & 6 deletions src/shared/components/app/code-theme.tsx
Original file line number Diff line number Diff line change
@@ -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<CodeThemeProps, any> {
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 (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href={`/css/code-themes/atom-one-light.css`}
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href={`/css/code-themes/atom-one-dark.css`}
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
);
}

return (
<Helmet>
Expand Down
194 changes: 143 additions & 51 deletions src/shared/components/app/theme.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> {
render() {
interface State {
themeOverride?: string;
graceTheme?: string;
}

export class Theme extends Component<Props, State> {
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 hasTheme = user?.local_user_view.local_user.theme !== "browser";
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());
}

if (user && hasTheme) {
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 (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${user.local_user_view.local_user.theme}.css`}
/>
</Helmet>
<>
<Helmet htmlAttributes={{ "data-bs-theme": dataBsTheme(theme) }}>
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${theme}.css`}
/>
</Helmet>
<CodeTheme theme={theme} />
</>
);
} else if (
this.props.defaultTheme !== "browser" &&
this.props.defaultTheme !== "browser-compact"
) {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${this.props.defaultTheme}.css`}
/>
</Helmet>
<>
<Helmet
htmlAttributes={{
"data-bs-theme": dataBsTheme(this.props.defaultTheme),
}}
>
<link
rel="stylesheet"
type="text/css"
href={`/css/themes/${this.props.defaultTheme}.css`}
/>
</Helmet>
<CodeTheme theme={this.props.defaultTheme} />
</>
);
} else if (this.props.defaultTheme === "browser-compact") {
} else if (
this.props.defaultTheme === "browser-compact" ||
theme === "browser-compact"
) {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely-compact.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly-compact.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
<>
<Helmet htmlAttributes={detectedBsTheme}>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely-compact.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly-compact.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
<CodeTheme theme="browser-compact" />
</>
);
} else {
return (
<Helmet>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
<>
<Helmet htmlAttributes={detectedBsTheme}>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/litely.css"
id="default-light"
media="(prefers-color-scheme: light)"
/>
<link
rel="stylesheet"
type="text/css"
href="/css/themes/darkly.css"
id="default-dark"
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
/>
</Helmet>
<CodeTheme theme="browser" />
</>
);
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/shared/components/home/admin-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
async componentDidMount() {
if (!this.state.isIsomorphic) {
await this.fetchData();
} else {
const themeList = await fetchThemeList();
this.setState({ themeList });
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/shared/components/home/login.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 5 additions & 4 deletions src/shared/components/person/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
myAuth,
personToChoice,
setIsoData,
setTheme,
showLocal,
updateCommunityBlock,
updateInstanceBlock,
Expand Down Expand Up @@ -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<{
Expand Down Expand Up @@ -342,6 +341,7 @@ export class Settings extends Component<any, SettingsState> {
componentWillUnmount(): void {
// In case `interface_language` change wasn't saved.
loadUserLanguage();
setThemeOverride(undefined);
}

static async fetchInitialData({
Expand Down Expand Up @@ -1453,7 +1453,7 @@ export class Settings extends Component<any, SettingsState> {

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) {
Expand Down Expand Up @@ -1571,6 +1571,7 @@ export class Settings extends Component<any, SettingsState> {
window.scrollTo(0, 0);
}

setThemeOverride(undefined);
i.setState({ saveRes });
}

Expand Down Expand Up @@ -1665,7 +1666,7 @@ export class Settings extends Component<any, SettingsState> {
} = siteRes.data.my_user!.local_user_view;

UserService.Instance.myUserInfo = siteRes.data.my_user;
updateDataBsTheme(siteRes.data);
refreshTheme();

i.setState(prev => ({
...prev,
Expand Down
Loading