mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): follow instance branding in tab title and favicon (#3288)
This commit is contained in:
1 parent
39e48458da
commit
6dc698ff5e
2 files changed
+39
-34
No files matched your search
@@ -1,14 +1,9 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {DEFAULT_APP_SHELL_BRANDING} from '@app/features/app/state/AppShellBranding';
|
||||
import {setDocumentTitleProductName} from '@app/features/window/hooks/useFluxerDocumentTitle';
|
||||
import type {InstanceAppPublic} from '@fluxer/instance_bootstrap/src/Types';
|
||||
|
||||
let lastProductName = DEFAULT_APP_SHELL_BRANDING.productName;
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
||||
}
|
||||
|
||||
function brandedLink(rel: string): HTMLLinkElement | null {
|
||||
return document.head.querySelector<HTMLLinkElement>(`link[rel="${rel}"][data-fluxer-branding="true"]`);
|
||||
}
|
||||
@@ -28,6 +23,22 @@ function removeLink(rel: string): void {
|
||||
brandedLink(rel)?.remove();
|
||||
}
|
||||
|
||||
function suspendDefaultLinks(rel: string): void {
|
||||
for (const link of document.head.querySelectorAll<HTMLLinkElement>(
|
||||
`link[rel="${rel}"]:not([data-fluxer-branding])`,
|
||||
)) {
|
||||
link.dataset.fluxerDefaultRel = rel;
|
||||
link.rel = '';
|
||||
}
|
||||
}
|
||||
|
||||
function restoreDefaultLinks(rel: string): void {
|
||||
for (const link of document.head.querySelectorAll<HTMLLinkElement>(`link[data-fluxer-default-rel="${rel}"]`)) {
|
||||
link.rel = rel;
|
||||
delete link.dataset.fluxerDefaultRel;
|
||||
}
|
||||
}
|
||||
|
||||
function brandedMeta(name: string): HTMLMetaElement | null {
|
||||
return document.head.querySelector<HTMLMetaElement>(`meta[name="${name}"][data-fluxer-branding="true"]`);
|
||||
}
|
||||
@@ -47,25 +58,16 @@ function removeMeta(name: string): void {
|
||||
brandedMeta(name)?.remove();
|
||||
}
|
||||
|
||||
function replaceTitleProductName(nextProductName: string): void {
|
||||
if (lastProductName === nextProductName) return;
|
||||
const prefix = new RegExp(`^(\\(\\d+\\)\\s+|\\u2022\\s+)?${escapeRegExp(lastProductName)}(?=$| \\| )`, 'u');
|
||||
if (prefix.test(document.title)) {
|
||||
document.title = document.title.replace(prefix, (_match, notificationPrefix: string | undefined) => {
|
||||
return `${notificationPrefix ?? ''}${nextProductName}`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function applyDocumentBranding(productName: string, faviconUrl: string | null, themeColor: string | null): void {
|
||||
if (typeof document === 'undefined') return;
|
||||
replaceTitleProductName(productName);
|
||||
lastProductName = productName;
|
||||
setDocumentTitleProductName(productName);
|
||||
setMeta('application-name', productName);
|
||||
setMeta('apple-mobile-web-app-title', productName);
|
||||
if (faviconUrl === null) {
|
||||
removeLink('icon');
|
||||
restoreDefaultLinks('icon');
|
||||
} else {
|
||||
suspendDefaultLinks('icon');
|
||||
setLink('icon', faviconUrl);
|
||||
}
|
||||
if (themeColor === null) {
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {getCurrentLocale} from '@app/features/user/utils/LocaleUtils';
|
||||
import {formatNumber} from '@pkgs/number_utils/src/NumberFormatting';
|
||||
import {useEffect, useRef} from 'react';
|
||||
|
||||
const TITLE_PREFIX = PRODUCT_NAME;
|
||||
import {useEffect} from 'react';
|
||||
|
||||
type TitlePart = string | null | undefined;
|
||||
type TitleInput = TitlePart | Array<TitlePart>;
|
||||
@@ -19,7 +17,8 @@ interface BadgeState {
|
||||
hasUnread: boolean;
|
||||
}
|
||||
|
||||
let currentBaseTitle = TITLE_PREFIX;
|
||||
let titleProductName = PRODUCT_NAME;
|
||||
let currentTitleParts: ReadonlyArray<string> = [];
|
||||
let currentBadgeState: BadgeState = {mentionCount: 0, hasUnread: false};
|
||||
|
||||
const normalizeTitleParts = (value?: TitleInput): Array<string> => {
|
||||
@@ -29,11 +28,11 @@ const normalizeTitleParts = (value?: TitleInput): Array<string> => {
|
||||
const parts = Array.isArray(value) ? value : [value];
|
||||
return parts.map((part) => part?.trim()).filter((part): part is string => Boolean(part));
|
||||
};
|
||||
const buildDocumentTitle = (parts: Array<string>): string => {
|
||||
const buildDocumentTitle = (parts: ReadonlyArray<string>): string => {
|
||||
if (!parts.length) {
|
||||
return TITLE_PREFIX;
|
||||
return titleProductName;
|
||||
}
|
||||
return [TITLE_PREFIX, ...parts].join(' | ');
|
||||
return [titleProductName, ...parts].join(' | ');
|
||||
};
|
||||
const applyBadgePrefix = (baseTitle: string, badge: BadgeState): string => {
|
||||
if (badge.mentionCount > 0) {
|
||||
@@ -45,25 +44,29 @@ const applyBadgePrefix = (baseTitle: string, badge: BadgeState): string => {
|
||||
return baseTitle;
|
||||
};
|
||||
const updateDocumentTitle = (): void => {
|
||||
document.title = applyBadgePrefix(currentBaseTitle, currentBadgeState);
|
||||
document.title = applyBadgePrefix(buildDocumentTitle(currentTitleParts), currentBadgeState);
|
||||
};
|
||||
export const setDocumentTitleProductName = (productName: string): void => {
|
||||
if (titleProductName === productName) return;
|
||||
titleProductName = productName;
|
||||
updateDocumentTitle();
|
||||
};
|
||||
export const updateDocumentTitleBadge = (mentionCount: number, hasUnread: boolean): void => {
|
||||
currentBadgeState = {mentionCount, hasUnread};
|
||||
updateDocumentTitle();
|
||||
};
|
||||
export const useFluxerDocumentTitle = (title?: TitleInput, options?: UseDocumentTitleOptions) => {
|
||||
const parts = normalizeTitleParts(title);
|
||||
const fullTitle = buildDocumentTitle(parts);
|
||||
const prevTitleRef = useRef<string | undefined>(undefined);
|
||||
const partsKey = JSON.stringify(normalizeTitleParts(title));
|
||||
const preserveTitleOnUnmount = options?.preserveTitleOnUnmount;
|
||||
useEffect(() => {
|
||||
prevTitleRef.current = currentBaseTitle;
|
||||
currentBaseTitle = fullTitle;
|
||||
const previousParts = currentTitleParts;
|
||||
currentTitleParts = JSON.parse(partsKey) as Array<string>;
|
||||
updateDocumentTitle();
|
||||
return () => {
|
||||
if (!options?.preserveTitleOnUnmount && prevTitleRef.current) {
|
||||
currentBaseTitle = prevTitleRef.current;
|
||||
if (!preserveTitleOnUnmount) {
|
||||
currentTitleParts = previousParts;
|
||||
updateDocumentTitle();
|
||||
}
|
||||
};
|
||||
}, [fullTitle, options?.preserveTitleOnUnmount]);
|
||||
}, [partsKey, preserveTitleOnUnmount]);
|
||||
};
|
||||
Reference in new issue
Block a user