feat: PWA install prompt, brand-colored channel icons, image load fix, semantic connection indicator

- Fix image display bug: remove loading='lazy' + use opacity instead of hidden (#66)
- PWA: add id/scope to manifest, fix SW cache name, add install prompt hook + button (#64)
- Colorize channel icons with brand colors (Discord blurple, Telegram blue, etc.) (#68)
- Use semantic green for connection indicator instead of theme accent (#70)
- Replace Sparkles icon with PinchChat logo in sidebar (#65)
- Replace decorative dots in sidebar footer with GitHub link (#71)
This commit is contained in:
Nicolas Varrot
2026-02-14 13:58:18 +00:00
parent cf6512043c
commit 143e9486c2
8 changed files with 193 additions and 35 deletions

View File

@@ -0,0 +1,52 @@
import { useState, useEffect, useCallback, useRef } from 'react';
interface BeforeInstallPromptEvent extends Event {
prompt(): Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
// Check standalone mode outside of React to avoid setState-in-effect
const isStandalone = typeof window !== 'undefined' && window.matchMedia('(display-mode: standalone)').matches;
export function usePwaInstall() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [isInstalled, setIsInstalled] = useState(isStandalone);
const installedRef = useRef(isStandalone);
useEffect(() => {
if (installedRef.current) return;
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e as BeforeInstallPromptEvent);
};
const installedHandler = () => {
installedRef.current = true;
setIsInstalled(true);
setDeferredPrompt(null);
};
window.addEventListener('beforeinstallprompt', handler);
window.addEventListener('appinstalled', installedHandler);
return () => {
window.removeEventListener('beforeinstallprompt', handler);
window.removeEventListener('appinstalled', installedHandler);
};
}, []);
const install = useCallback(async () => {
if (!deferredPrompt) return false;
await deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
setDeferredPrompt(null);
return outcome === 'accepted';
}, [deferredPrompt]);
return {
canInstall: !!deferredPrompt && !isInstalled,
isInstalled,
install,
};
}