mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
docs: simplify the reference and tighten the verifier (#2736)
This commit is contained in:
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);
|
||||
|
||||
Reference in new issue
Block a user