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

Follow system color scheme #210

Merged
merged 3 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/@types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ export interface SharingEntry {
seq: number;
event: any;
}

declare global {
interface Window {
/** Injected by hkbus.app app webview
* @since Dec 2024
*/
systemColorSchemeCallbacks?: Function[];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Specify function types explicitly instead of using Function

Using Function as a type is discouraged because it lacks type safety and can lead to bugs. It's better to define the function type explicitly to ensure type safety.

Apply this diff to fix the type definition:

     /** Injected by hkbus.app app webview
      * @since Dec 2024
      */
-    systemColorSchemeCallbacks?: Function[];
+    systemColorSchemeCallbacks?: Array<() => void>;

This change defines systemColorSchemeCallbacks as an array of functions that take no arguments and return void, which aligns with the expected callback signature used in your code.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
systemColorSchemeCallbacks?: Function[];
systemColorSchemeCallbacks?: Array<() => void>;
🧰 Tools
🪛 Biome (1.9.4)

[error] 36-36: Don't use 'Function' as a type.

Prefer explicitly define the function shape. This type accepts any function-like value, which can be a common source of bugs.

(lint/complexity/noBannedTypes)

🪛 eslint

[error] 36-36: Don't use Function as a type. The Function type accepts any function-like value.
It provides no type safety when calling the function, which can be a common source of bugs.
It also accepts things like class declarations, which will throw at runtime as they will not be called with new.
If you are expecting the function to accept certain arguments, you should explicitly define the function shape.

(@typescript-eslint/ban-types)

/** Injected by hkbus.app app webview
* @since Dec 2024
* @see https://reactnative.dev/docs/usecolorscheme
*/
systemColorScheme?: { value: "light" | "dark" | null };
}
}
61 changes: 52 additions & 9 deletions src/context/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -593,19 +593,62 @@ export const AppContextProvider = ({ children }: AppContextProviderProps) => {
);
}, []);

const [colorMode, setColorMode] = useState<"light" | "dark">("light");
useEffect(() => {
const calculateColorMode = useCallback(() => {
if (state._colorMode === "light" || state._colorMode === "dark") {
setColorMode(state._colorMode);
return state._colorMode;
} else {
if (
window.systemColorScheme?.value &&
["light", "dark"].includes(window.systemColorScheme?.value)
) {
return window.systemColorScheme?.value;
}
return window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
}
}, [state._colorMode]);
const [colorMode, setColorMode] = useState<"light" | "dark">(
calculateColorMode()
);
useEffect(() => {
setColorMode(calculateColorMode());

if (state._colorMode === "system") {
const themeListener = () => setColorMode(calculateColorMode());

const mql = window.matchMedia("(prefers-color-scheme: light)");
setColorMode(mql.matches ? "light" : "dark");
const themeListener = (e: MediaQueryListEvent) =>
setColorMode(e.matches ? "light" : "dark");
mql?.addEventListener("change", themeListener);
return () => mql?.removeEventListener("change", themeListener);
mql.addEventListener("change", themeListener);
if (
window.systemColorSchemeCallbacks &&
Array.isArray(window.systemColorSchemeCallbacks)
) {
window.systemColorSchemeCallbacks.push(themeListener);
}

return () => {
mql.removeEventListener("change", themeListener);
if (
Comment on lines +621 to +631
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure compatibility with older browsers for media query listeners

The MediaQueryList interface's addEventListener and removeEventListener methods are not supported in some older browsers. To maintain broader compatibility, consider using addListener and removeListener or include a fallback.

Here is how you can modify the code to support both methods:

      // Add event listener for theme changes
-     mql.addEventListener("change", themeListener);
+     if (mql.addEventListener) {
+       mql.addEventListener("change", themeListener);
+     } else {
+       mql.addListener(themeListener);
+     }

And in the cleanup function:

      // Remove event listener
-     mql.removeEventListener("change", themeListener);
+     if (mql.removeEventListener) {
+       mql.removeEventListener("change", themeListener);
+     } else {
+       mql.removeListener(themeListener);
+     }

This ensures compatibility with browsers that only support the older addListener and removeListener methods.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mql.addEventListener("change", themeListener);
if (
window.systemColorSchemeCallbacks &&
Array.isArray(window.systemColorSchemeCallbacks)
) {
window.systemColorSchemeCallbacks.push(themeListener);
}
return () => {
mql.removeEventListener("change", themeListener);
if (
if (mql.addEventListener) {
mql.addEventListener("change", themeListener);
} else {
mql.addListener(themeListener);
}
if (
window.systemColorSchemeCallbacks &&
Array.isArray(window.systemColorSchemeCallbacks)
) {
window.systemColorSchemeCallbacks.push(themeListener);
}
return () => {
if (mql.removeEventListener) {
mql.removeEventListener("change", themeListener);
} else {
mql.removeListener(themeListener);
}
if (

window.systemColorSchemeCallbacks &&
Array.isArray(window.systemColorSchemeCallbacks)
) {
window.systemColorSchemeCallbacks =
window.systemColorSchemeCallbacks.filter(
(cb) => cb !== themeListener
);
}
};
}
}, [state._colorMode, setColorMode]);
}, [state._colorMode, calculateColorMode]);

useEffect(() => {
window.ReactNativeWebView?.postMessage(
JSON.stringify({
type: "color-mode",
value: colorMode,
})
);
}, [colorMode]);

const importAppState = useCallback((appState: AppState) => {
setStateRaw(appState);
Expand Down
Loading