Skip to content
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
2 changes: 2 additions & 0 deletions internal/server/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type Customize struct {
DisableKillCount bool `json:"disableKillCount" yaml:"disableKillCount"`
HeaderTitle string `json:"headerTitle" yaml:"headerTitle"`
HeaderSubtitle string `json:"headerSubtitle" yaml:"headerSubtitle"`
PageTitle string `json:"pageTitle" yaml:"pageTitle"`
CSSOverrides map[string]string `json:"cssOverrides,omitempty" yaml:"cssOverrides"`
}

Expand Down Expand Up @@ -108,6 +109,7 @@ func NewSetting() (setting Setting, err error) {
viper.SetDefault("customize.disableKillCount", false)
viper.SetDefault("customize.headerTitle", "")
viper.SetDefault("customize.headerSubtitle", "")
viper.SetDefault("customize.pageTitle", "")
viper.SetDefault("conversion.enabled", false)
viper.SetDefault("conversion.interval", "5m")
viper.SetDefault("conversion.batchSize", 1)
Expand Down
1 change: 1 addition & 0 deletions setting.json.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"enabled": false,
"headerSubtitle": "",
"headerTitle": "",
"pageTitle": "",
"websiteLogo": "",
"websiteLogoSize": "32px",
"websiteURL": "",
Expand Down
1 change: 1 addition & 0 deletions ui/src/data/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface CustomizeConfig {
disableKillCount?: boolean;
headerTitle?: string;
headerSubtitle?: string;
pageTitle?: string;
cssOverrides?: Record<string, string>;
}

Expand Down
99 changes: 99 additions & 0 deletions ui/src/hooks/__tests__/useCustomize.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,105 @@ describe("useCustomize", () => {
expect(parsed.websiteURL).toBeUndefined();
});

it("applies pageTitle to document.title and restores on unmount", async () => {
const originalTitle = document.title;
document.title = "OCAP2";

mockGetCustomize.mockResolvedValue({
enabled: true,
pageTitle: "Custom Server Title",
});

const { unmount } = render(() => (
<CustomizeProvider>
<div>test</div>
</CustomizeProvider>
));

await vi.waitFor(() => {
expect(document.title).toBe("Custom Server Title");
});

unmount();
expect(document.title).toBe("OCAP2");

document.title = originalTitle;
});

it("does not override document.title when pageTitle is empty", async () => {
const originalTitle = document.title;
document.title = "OCAP2";

mockGetCustomize.mockResolvedValue({
enabled: true,
pageTitle: "",
});

const { unmount } = render(() => (
<CustomizeProvider>
<div>test</div>
</CustomizeProvider>
));

await new Promise((r) => setTimeout(r, 10));
expect(document.title).toBe("OCAP2");

unmount();
expect(document.title).toBe("OCAP2");

document.title = originalTitle;
});

it("does not mutate document.title if unmounted before API resolves", async () => {
const originalTitle = document.title;
document.title = "OCAP2";

let resolveCustomize: (value: CustomizeConfig) => void = () => {};
mockGetCustomize.mockReturnValue(
new Promise<CustomizeConfig>((resolve) => {
resolveCustomize = resolve;
}),
);

const { unmount } = render(() => (
<CustomizeProvider>
<div>test</div>
</CustomizeProvider>
));

unmount();
resolveCustomize({ enabled: true, pageTitle: "Should Not Apply" });

await new Promise((r) => setTimeout(r, 10));
expect(document.title).toBe("OCAP2");

document.title = originalTitle;
});

it("does not apply cssOverrides if unmounted before API resolves", async () => {
let resolveCustomize: (value: CustomizeConfig) => void = () => {};
mockGetCustomize.mockReturnValue(
new Promise<CustomizeConfig>((resolve) => {
resolveCustomize = resolve;
}),
);

const { unmount } = render(() => (
<CustomizeProvider>
<div>test</div>
</CustomizeProvider>
));

unmount();
resolveCustomize({
enabled: true,
cssOverrides: { "--accent-primary": "#ff0000" },
});

await new Promise((r) => setTimeout(r, 10));
expect(document.documentElement.style.getPropertyValue("--accent-primary")).toBe("");
});

it("cleans up applied properties on unmount", async () => {
mockGetCustomize.mockResolvedValue({
enabled: true,
Expand Down
14 changes: 14 additions & 0 deletions ui/src/hooks/useCustomize.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ export function CustomizeProvider(props: {
}): JSX.Element {
const [config, setConfig] = createSignal<CustomizeConfig>({});
let appliedProps: string[] = [];
const originalTitle = document.title;
let titleOverridden = false;
let mounted = true;

onMount(async () => {
try {
const api = new ApiClient();
const data = await api.getCustomize();
if (!mounted) return;
if (!data.enabled) {
// disableKillCount is a privacy toggle, not a branding option, so
// honor it even when customize itself is not enabled.
Expand All @@ -29,6 +33,11 @@ export function CustomizeProvider(props: {
}
setConfig(data);

if (data.pageTitle) {
document.title = data.pageTitle;
titleOverridden = true;
}
Comment thread
fank marked this conversation as resolved.

// Apply CSS variable overrides to :root
if (data.cssOverrides) {
const style = document.documentElement.style;
Expand All @@ -45,11 +54,16 @@ export function CustomizeProvider(props: {
});

onCleanup(() => {
mounted = false;
const style = document.documentElement.style;
for (const prop of appliedProps) {
style.removeProperty(prop);
}
appliedProps = [];
if (titleOverridden) {
document.title = originalTitle;
titleOverridden = false;
}
});

return (
Expand Down
Loading