docs: simplify the reference and tighten the verifier (#2736)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 17:38:50 +02:00
1 parent 6af33c7188
commit 7375ac9d80
111 files changed
+1616 -2430

No files matched your search

@@ -1,83 +1,54 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {observeResize} from '@app/features/platform/utils/SharedResizeObserver';
import {useEffect, useState} from 'react';
type Callback = (el: Element) => void;
function observeElement(el: Element, cb: Callback): () => void {
if (typeof ResizeObserver === 'undefined') return () => {};
return observeResize(el, () => cb(el));
}
import {type RefObject, useEffect, useState} from 'react';
type OverflowAxis = 'horizontal' | 'vertical' | 'both';
function getElementOverflowState(el: HTMLElement, axis: OverflowAxis): boolean {
const {scrollWidth, clientWidth, scrollHeight, clientHeight} = el;
const horizontalOverflowing = scrollWidth - clientWidth > 1;
const verticalOverflowing = scrollHeight - clientHeight > 1;
return axis === 'horizontal'
? horizontalOverflowing
: axis === 'vertical'
? verticalOverflowing
: horizontalOverflowing || verticalOverflowing;
return (
(axis !== 'vertical' && el.scrollWidth - el.clientWidth > 1) ||
(axis !== 'horizontal' && el.scrollHeight - el.clientHeight > 1)
);
}
export function useElementOverflow(element: HTMLElement | null, axis: OverflowAxis = 'horizontal'): boolean {
function useOverflow(target: HTMLElement | RefObject<HTMLElement | null> | null, axis: OverflowAxis): boolean {
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
if (!element) {
const element = target && 'current' in target ? target.current : target;
const ownerWindow = element?.ownerDocument.defaultView;
if (!element || !ownerWindow) {
setIsOverflowing(false);
return;
}
let rafId: number | null = null;
let disposed = false;
const checkOverflow = () => {
rafId = null;
const overflowing = getElementOverflowState(element, axis);
setIsOverflowing((prev) => (prev === overflowing ? prev : overflowing));
};
const scheduleCheckOverflow = () => {
if (rafId != null) return;
rafId = requestAnimationFrame(checkOverflow);
if (disposed || rafId != null) return;
rafId = ownerWindow.requestAnimationFrame(checkOverflow);
};
scheduleCheckOverflow();
const unobserve = observeElement(element, scheduleCheckOverflow);
const unobserve = typeof ResizeObserver === 'undefined' ? undefined : observeResize(element, scheduleCheckOverflow);
return () => {
disposed = true;
if (rafId != null) {
cancelAnimationFrame(rafId);
ownerWindow.cancelAnimationFrame(rafId);
}
unobserve();
unobserve?.();
};
}, [element, axis]);
}, [target, axis]);
return isOverflowing;
}
export function useTextOverflow(ref: React.RefObject<HTMLElement | null>, axis: OverflowAxis = 'horizontal'): boolean {
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) {
setIsOverflowing(false);
return;
}
let rafId: number | null = null;
const checkOverflow = () => {
rafId = null;
const overflowing = getElementOverflowState(el, axis);
setIsOverflowing((prev) => (prev === overflowing ? prev : overflowing));
};
const scheduleCheckOverflow = () => {
if (rafId != null) return;
rafId = requestAnimationFrame(checkOverflow);
};
scheduleCheckOverflow();
const unobserve = observeElement(el, scheduleCheckOverflow);
return () => {
if (rafId != null) {
cancelAnimationFrame(rafId);
}
unobserve();
};
}, [ref, axis]);
return isOverflowing;
export function useElementOverflow(element: HTMLElement | null, axis: OverflowAxis = 'horizontal'): boolean {
return useOverflow(element, axis);
}
export function useTextOverflow(ref: RefObject<HTMLElement | null>, axis: OverflowAxis = 'horizontal'): boolean {
return useOverflow(ref, axis);
}
@@ -9,7 +9,7 @@ import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
import type {CustomStatus} from '@app/features/user/state/CustomStatus';
import {getCustomStatusText, isCustomStatusExpired, normalizeCustomStatus} from '@app/features/user/state/CustomStatus';
import clsx from 'clsx';
import {type ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import {type ReactNode, useEffect, useRef, useState} from 'react';
interface CompactMemberCustomStatusProps {
className?: string;
@@ -97,13 +97,7 @@ export function CompactMemberCustomStatus({
const timer = window.setTimeout(() => setExpiryTick((tick) => tick + 1), delay);
return () => window.clearTimeout(timer);
}, [status]);
const normalized = useMemo(() => {
const nextStatus = normalizeCustomStatus(status);
if (!nextStatus || isCustomStatusExpired(nextStatus)) {
return null;
}
return nextStatus;
}, [status]);
const normalized = normalizeCustomStatus(status);
const fullText = normalized ? getCustomStatusText(normalized) : null;
const isOverflowing = useTextOverflow(containerRef, {content: fullText, measureTextRange: true});
if (!normalized) {
@@ -150,7 +150,7 @@ export const AddConnectionModal = observer(({defaultType}: AddConnectionModalPro
if (type === ConnectionTypes.BLUESKY) {
identifier = identifier.replace(/^https?:\/\/bsky\.app\/profile\//i, '').replace(/^@/, '');
}
if (UserConnection.hasConnectionByTypeAndName(type, identifier)) {
if (type === ConnectionTypes.DOMAIN && UserConnection.hasConnectionByTypeAndName(type, identifier)) {
initiateForm.setError('identifier', {
type: 'validate',
message: i18n._(YOU_ALREADY_HAVE_THIS_CONNECTION_DESCRIPTOR),
@@ -37,17 +37,17 @@ const defaultNavigator: Navigator = {
};
let inProgress = false;
let pendingTarget: string | null = null;
function computeBasePath(url: string): string | null {
const pathname = new URL(url, window.location.origin).pathname;
if (Routes.isDMRoute(pathname) && pathname !== Routes.ME) {
return Routes.ME;
}
if (Routes.isGuildChannelRoute(pathname) && pathname.split('/').length === 4) {
if (Routes.isGuildChannelRoute(pathname)) {
const parts = pathname.split('/');
const guildId = parts[2];
return Routes.guildChannel(guildId);
if (parts.length === 4) {
return Routes.guildChannel(parts[2]);
}
}
return null;
}
@@ -55,11 +55,10 @@ function computeBasePath(url: string): string | null {
export function navigateToWithMobileHistory(url: string, isMobile: boolean, nav: Navigator = defaultNavigator): void {
if (!isMobile) {
inProgress = false;
pendingTarget = null;
nav.replace(url);
return;
}
if (inProgress && (pendingTarget === url || pendingTarget !== null)) {
if (inProgress) {
return;
}
const current = nav.getPath();
@@ -74,22 +73,11 @@ export function navigateToWithMobileHistory(url: string, isMobile: boolean, nav:
nav.replace(url);
return;
}
if (current === base) {
inProgress = true;
pendingTarget = url;
nav.replaceSilent(base);
nav.push(url);
inProgress = false;
pendingTarget = null;
return;
}
inProgress = true;
pendingTarget = url;
try {
nav.replaceSilent(base);
nav.push(url);
} finally {
inProgress = false;
pendingTarget = null;
}
}
@@ -1,54 +1,68 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
type Callback = (entry: ResizeObserverEntry) => void;
type ResizeCallback = (entry: ResizeObserverEntry) => void;
const callbacks = new WeakMap<Element, Set<Callback>>();
const callbacks = new Map<Element, Map<ResizeCallback, number>>();
let nativeObserver: ResizeObserver | null = null;
let observedElementCount = 0;
function getObserver(): ResizeObserver {
if (nativeObserver) return nativeObserver;
nativeObserver = new ResizeObserver((entries) => {
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
if (nativeObserver !== observer) return;
const handlers = callbacks.get(entry.target);
if (!handlers) continue;
for (const cb of handlers) {
for (const callback of [...handlers.keys()]) {
if (callbacks.get(entry.target) !== handlers) break;
if (!handlers.has(callback)) continue;
try {
cb(entry);
callback(entry);
} catch (error) {
console.error('SharedResizeObserver callback threw:', error);
}
}
}
});
return nativeObserver;
nativeObserver = observer;
return observer;
}
export function observeResize(element: Element, callback: Callback): () => void {
export function observeResize(element: Element, callback: ResizeCallback): () => void {
const observer = getObserver();
let handlers = callbacks.get(element);
if (!handlers) {
handlers = new Set();
callbacks.set(element, handlers);
observedElementCount++;
getObserver().observe(element);
}
handlers.add(callback);
return () => unobserveResize(element, callback);
}
export function unobserveResize(element: Element, callback: Callback): void {
const handlers = callbacks.get(element);
if (!handlers) return;
handlers.delete(callback);
if (handlers.size === 0) {
callbacks.delete(element);
nativeObserver?.unobserve?.(element);
observedElementCount = Math.max(0, observedElementCount - 1);
if (observedElementCount === 0) {
nativeObserver?.disconnect();
nativeObserver = null;
try {
observer.observe(element);
} catch (error) {
if (callbacks.size === 0) {
nativeObserver = null;
observer.disconnect();
}
throw error;
}
handlers = new Map();
callbacks.set(element, handlers);
}
handlers.set(callback, (handlers.get(callback) ?? 0) + 1);
let released = false;
return () => {
if (released) return;
released = true;
const count = handlers.get(callback);
if (!count) throw new Error('Resize subscription has no active callback');
if (count > 1) {
handlers.set(callback, count - 1);
return;
}
handlers.delete(callback);
if (handlers.size > 0) return;
callbacks.delete(element);
if (callbacks.size === 0) {
nativeObserver = null;
observer.disconnect();
} else {
observer.unobserve(element);
}
};
}
@@ -11,12 +11,6 @@ interface UseTextOverflowOptions {
const OVERFLOW_EPSILON_PX = 1;
type FontFaceSetLike = {
ready?: Promise<unknown>;
addEventListener?: (type: 'loadingdone', listener: () => void) => void;
removeEventListener?: (type: 'loadingdone', listener: () => void) => void;
};
function isMeaningfullyGreater(measuredSize: number, availableSize: number): boolean {
return measuredSize - availableSize > OVERFLOW_EPSILON_PX;
}
@@ -94,7 +88,8 @@ export function useTextOverflow(
const [isOverflowing, setIsOverflowing] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element || !content) {
const ownerWindow = element?.ownerDocument.defaultView;
if (!element || !ownerWindow || !content) {
setIsOverflowing(false);
return;
}
@@ -112,7 +107,7 @@ export function useTextOverflow(
updateOverflowing(true);
return;
}
if (measureTextRange && typeof document !== 'undefined') {
if (measureTextRange) {
const availableWidth = getAvailableInlineSize(element);
if (isMeaningfullyGreater(measureRangeInlineSize(element), availableWidth)) {
updateOverflowing(true);
@@ -128,14 +123,14 @@ export function useTextOverflow(
if (disposed || frameId != null) {
return;
}
frameId = window.requestAnimationFrame(checkOverflow);
frameId = ownerWindow.requestAnimationFrame(checkOverflow);
};
scheduleOverflowCheck();
const unobserveResize =
typeof ResizeObserver !== 'undefined' ? observeResize(element, scheduleOverflowCheck) : undefined;
let mutationObserver: MutationObserver | null = null;
if (typeof MutationObserver !== 'undefined') {
mutationObserver = new MutationObserver(scheduleOverflowCheck);
if (typeof ownerWindow.MutationObserver !== 'undefined') {
mutationObserver = new ownerWindow.MutationObserver(scheduleOverflowCheck);
mutationObserver.observe(element, {
attributes: true,
attributeFilter: ['alt', 'class', 'src', 'style'],
@@ -145,14 +140,13 @@ export function useTextOverflow(
});
}
element.addEventListener('load', scheduleOverflowCheck, true);
const fontSet =
typeof document !== 'undefined' ? (document as Document & {fonts?: FontFaceSetLike}).fonts : undefined;
const fontSet = element.ownerDocument.fonts;
fontSet?.ready?.then(scheduleOverflowCheck);
fontSet?.addEventListener?.('loadingdone', scheduleOverflowCheck);
return () => {
disposed = true;
if (frameId != null) {
window.cancelAnimationFrame(frameId);
ownerWindow.cancelAnimationFrame(frameId);
}
unobserveResize?.();
mutationObserver?.disconnect();
@@ -26,6 +26,7 @@ import UserSettings from '@app/features/user/state/UserSettings';
import * as LocaleUtils from '@app/features/user/utils/LocaleUtils';
import {TimeFormatTypes} from '@fluxer/constants/src/UserConstants';
import {getFormattedTime} from '@fluxer/date_utils/src/DateFormatting';
import {localeUses12Hour} from '@fluxer/date_utils/src/DateHourCycle';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
import {clsx} from 'clsx';
@@ -293,29 +294,6 @@ const LanguageTab = observer(() => {
const appLocale = UserSettings.getLocale();
const browserLocale = navigator.language;
const effectiveLocale = Accessibility.useBrowserLocaleForTimeFormat ? browserLocale : appLocale;
const localeUses12Hour = (locale: string): boolean => {
const lang = locale.toLowerCase();
const twelveHourLocales = [
'en-us',
'en-ca',
'en-au',
'en-nz',
'en-ph',
'en-in',
'en-pk',
'en-bd',
'en-za',
'es-mx',
'es-co',
'ar',
'hi',
'bn',
'ur',
'fil',
'tl',
];
return twelveHourLocales.some((l) => lang.startsWith(l));
};
const uses12Hour = localeUses12Hour(effectiveLocale);
const sampleDate = new Date(2025, 0, 1, 14, 30, 0);
const format = getFormattedTime(sampleDate, effectiveLocale, uses12Hour);