fix(app): show cached auth splash art without a fade-in flash (#1907)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:43:26 +02:00
1 parent 79cf57abe4
commit 5eb7822691
4 files changed
+28 -51

No files matched your search

@@ -41,7 +41,7 @@ const AuthLayoutContent = observer(function AuthLayoutContent({children}: {child
const registerFormDraftsRef = useRef<Map<string, AuthRegisterFormDraft>>(new Map()); const registerFormDraftsRef = useRef<Map<string, AuthRegisterFormDraft>>(new Map());
const scrollerRef = useRef<ScrollerHandle>(null); const scrollerRef = useRef<ScrollerHandle>(null);
const location = useLocation(); const location = useLocation();
const {patternReady, splashLoaded, splashDimensions} = useAuthBackground(splashUrl, foodPatternUrl); const {patternReady, splashDimensions} = useAuthBackground(splashUrl, foodPatternUrl);
const handleSetSplashUrl = useCallback( const handleSetSplashUrl = useCallback(
(url: string | null) => { (url: string | null) => {
if (splashUrlRef.current === url) return; if (splashUrlRef.current === url) return;
@@ -196,7 +196,6 @@ const AuthLayoutContent = observer(function AuthLayoutContent({children}: {child
> >
<AuthBackground <AuthBackground
splashUrl={splashUrl} splashUrl={splashUrl}
splashLoaded={splashLoaded}
splashDimensions={splashDimensions} splashDimensions={splashDimensions}
splashScale={splashScale} splashScale={splashScale}
patternReady={patternReady} patternReady={patternReady}
@@ -1,10 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import Accessibility from '@app/features/accessibility/state/Accessibility';
import styles from '@app/features/app/components/layout/AuthLayout.module.css'; import styles from '@app/features/app/components/layout/AuthLayout.module.css';
import {GuildSplashCardAlignment} from '@fluxer/constants/src/GuildConstants'; import {GuildSplashCardAlignment} from '@fluxer/constants/src/GuildConstants';
import type {ValueOf} from '@fluxer/constants/src/ValueOf'; import type {ValueOf} from '@fluxer/constants/src/ValueOf';
import {motion} from 'framer-motion';
import type React from 'react'; import type React from 'react';
const getSplashAlignmentStyles = (alignment: ValueOf<typeof GuildSplashCardAlignment>) => { const getSplashAlignmentStyles = (alignment: ValueOf<typeof GuildSplashCardAlignment>) => {
@@ -20,7 +18,6 @@ const getSplashAlignmentStyles = (alignment: ValueOf<typeof GuildSplashCardAlign
export interface AuthBackgroundProps { export interface AuthBackgroundProps {
splashUrl: string | null; splashUrl: string | null;
splashLoaded: boolean;
splashDimensions?: {width: number; height: number} | null; splashDimensions?: {width: number; height: number} | null;
splashScale?: number | null; splashScale?: number | null;
patternReady: boolean; patternReady: boolean;
@@ -32,7 +29,6 @@ export interface AuthBackgroundProps {
export const AuthBackground: React.FC<AuthBackgroundProps> = ({ export const AuthBackground: React.FC<AuthBackgroundProps> = ({
splashUrl, splashUrl,
splashLoaded,
splashDimensions, splashDimensions,
splashScale, splashScale,
patternReady, patternReady,
@@ -47,13 +43,7 @@ export const AuthBackground: React.FC<AuthBackgroundProps> = ({
if (useFullCover) { if (useFullCover) {
return ( return (
<div className={className} data-flx="auth.flow.auth-background.div"> <div className={className} data-flx="auth.flow.auth-background.div">
<motion.div <div style={{position: 'absolute', inset: 0}} data-flx="auth.flow.auth-background.div--2">
initial={{opacity: 0}}
animate={{opacity: splashLoaded ? 1 : 0}}
transition={{duration: Accessibility.useReducedMotion ? 0 : 0.5, ease: 'easeInOut'}}
style={{position: 'absolute', inset: 0}}
data-flx="auth.flow.auth-background.div--2"
>
<img <img
src={splashUrl} src={splashUrl}
alt="" alt=""
@@ -68,17 +58,14 @@ export const AuthBackground: React.FC<AuthBackgroundProps> = ({
data-flx="auth.flow.auth-background.img" data-flx="auth.flow.auth-background.img"
/> />
<div className={styles.splashOverlay} data-flx="auth.flow.auth-background.splash-overlay" /> <div className={styles.splashOverlay} data-flx="auth.flow.auth-background.splash-overlay" />
</motion.div> </div>
</div> </div>
); );
} }
return ( return (
<div className={styles.rightSplit} data-flx="auth.flow.auth-background.right-split"> <div className={styles.rightSplit} data-flx="auth.flow.auth-background.right-split">
<motion.div <div
className={styles.splashImage} className={styles.splashImage}
initial={{opacity: 0}}
animate={{opacity: splashLoaded ? 1 : 0}}
transition={{duration: Accessibility.useReducedMotion ? 0 : 0.5, ease: 'easeInOut'}}
style={{ style={{
width: splashDimensions.width, width: splashDimensions.width,
height: splashDimensions.height, height: splashDimensions.height,
@@ -104,7 +91,7 @@ export const AuthBackground: React.FC<AuthBackgroundProps> = ({
data-flx="auth.flow.auth-background.img--2" data-flx="auth.flow.auth-background.img--2"
/> />
<div className={styles.splashOverlay} data-flx="auth.flow.auth-background.splash-overlay--2" /> <div className={styles.splashOverlay} data-flx="auth.flow.auth-background.splash-overlay--2" />
</motion.div> </div>
</div> </div>
); );
} }
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
interface ImageDimensions { interface ImageDimensions {
@@ -12,62 +13,53 @@ interface PatternImageLoaderResult {
} }
interface SplashImageLoaderResult { interface SplashImageLoaderResult {
loaded: boolean;
dimensions: ImageDimensions | null; dimensions: ImageDimensions | null;
} }
interface AuthBackgroundResult { interface AuthBackgroundResult {
patternReady: boolean; patternReady: boolean;
splashLoaded: boolean;
splashDimensions: ImageDimensions | null; splashDimensions: ImageDimensions | null;
} }
export function usePatternImageLoader(patternUrl: string): PatternImageLoaderResult { export function usePatternImageLoader(patternUrl: string): PatternImageLoaderResult {
const [patternReady, setPatternReady] = useState(false); const [patternReady, setPatternReady] = useState(() => ImageCacheUtils.hasImage(patternUrl));
useEffect(() => { useEffect(() => {
const img = new Image(); if (ImageCacheUtils.hasImage(patternUrl)) {
const handleLoad = () => setPatternReady(true); setPatternReady(true);
img.addEventListener('load', handleLoad, {once: true}); return;
img.src = patternUrl; }
return () => img.removeEventListener('load', handleLoad); setPatternReady(false);
return ImageCacheUtils.loadImage(patternUrl, () => setPatternReady(true));
}, [patternUrl]); }, [patternUrl]);
return {patternReady}; return {patternReady};
} }
export function useSplashImageLoader(imageUrl: string | null): SplashImageLoaderResult { export function useSplashImageLoader(imageUrl: string | null): SplashImageLoaderResult {
const [loaded, setLoaded] = useState(false); const [dimensions, setDimensions] = useState<ImageDimensions | null>(
const [dimensions, setDimensions] = useState<ImageDimensions | null>(null); () => ImageCacheUtils.getImageSize(imageUrl) ?? null,
);
useEffect(() => { useEffect(() => {
if (!imageUrl) { const cached = ImageCacheUtils.getImageSize(imageUrl);
setLoaded(false); if (cached) {
setDimensions(null); setDimensions(cached);
return; return;
} }
let isMounted = true; setDimensions(null);
const img = new Image(); if (!imageUrl) {
const handleLoad = () => { return;
if (!isMounted) return; }
setLoaded(true); return ImageCacheUtils.loadImage(imageUrl, () => {
setDimensions({ setDimensions(ImageCacheUtils.getImageSize(imageUrl) ?? null);
width: img.naturalWidth,
height: img.naturalHeight,
}); });
};
img.addEventListener('load', handleLoad, {once: true});
img.src = imageUrl;
return () => {
isMounted = false;
};
}, [imageUrl]); }, [imageUrl]);
return {loaded, dimensions}; return {dimensions};
} }
export function useAuthBackground(splashUrl: string | null, patternUrl: string): AuthBackgroundResult { export function useAuthBackground(splashUrl: string | null, patternUrl: string): AuthBackgroundResult {
const {patternReady} = usePatternImageLoader(patternUrl); const {patternReady} = usePatternImageLoader(patternUrl);
const {loaded: splashLoaded, dimensions: splashDimensions} = useSplashImageLoader(splashUrl); const {dimensions: splashDimensions} = useSplashImageLoader(splashUrl);
return { return {
patternReady, patternReady,
splashLoaded,
splashDimensions, splashDimensions,
}; };
} }
@@ -61,7 +61,7 @@ export const InvitePagePreviewModal: React.FC<InvitePagePreviewModalProps> = obs
} }
return null; return null;
}, [previewSplashUrl, guild?.id, guild?.splash]); }, [previewSplashUrl, guild?.id, guild?.splash]);
const {patternReady, splashLoaded, splashDimensions} = useAuthBackground(splashUrl, foodPatternUrl); const {patternReady, splashDimensions} = useAuthBackground(splashUrl, foodPatternUrl);
const shouldShowSplash = Boolean(splashUrl && splashDimensions); const shouldShowSplash = Boolean(splashUrl && splashDimensions);
const handleClose = useCallback(() => { const handleClose = useCallback(() => {
ModalCommands.pop(); ModalCommands.pop();
@@ -122,7 +122,6 @@ export const InvitePagePreviewModal: React.FC<InvitePagePreviewModalProps> = obs
<AuthBackground <AuthBackground
className={clsx(styles.background, !shouldShowSplash && authLayoutStyles.patternHost)} className={clsx(styles.background, !shouldShowSplash && authLayoutStyles.patternHost)}
splashUrl={splashUrl} splashUrl={splashUrl}
splashLoaded={splashLoaded}
splashDimensions={splashDimensions} splashDimensions={splashDimensions}
patternReady={patternReady} patternReady={patternReady}
patternImageUrl={foodPatternUrl} patternImageUrl={foodPatternUrl}