perf: code-split and defer the notification bell
This commit is contained in:
parent
7040a90630
commit
7bbe73e903
3 changed files with 83 additions and 5 deletions
|
|
@ -59,7 +59,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
|||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { NewsletterOverlay } from '@/components/NewsletterOverlay';
|
||||
import { NotificationBell } from '@/components/NotificationBell';
|
||||
import { NotificationBellLazy } from '@/components/NotificationBellLazy';
|
||||
import { OsNotificationBridge } from '@/components/OsNotificationBridge';
|
||||
|
||||
// Cashu balance widget is lazy — see components/wallet/CashuBalance.tsx for why
|
||||
|
|
@ -781,7 +781,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
</Button>
|
||||
)}
|
||||
{/* Notifications (crew invites & membership requests) */}
|
||||
{user && <NotificationBell />}
|
||||
{user && <NotificationBellLazy />}
|
||||
{/* Offline Indicator */}
|
||||
<OfflineIndicator />
|
||||
{/* Language Toggle */}
|
||||
|
|
@ -825,7 +825,7 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
{/* Notifications (crew invites & membership requests) */}
|
||||
{user && <NotificationBell />}
|
||||
{user && <NotificationBellLazy />}
|
||||
{/* User menu when logged in / Login button when not.
|
||||
IMPORTANT: LoginArea hosts the SignupDialog. The signup flow logs
|
||||
the user in at the *first* step (to publish their profile), then
|
||||
|
|
|
|||
|
|
@ -209,9 +209,15 @@ function EmptyState({ label }: { label: string }) {
|
|||
* Unread state is tracked by a per-account "last read" timestamp; opening the
|
||||
* bell (or hitting "mark all read") clears the badge. Only renders when logged in.
|
||||
*/
|
||||
export function NotificationBell({ className }: { className?: string }) {
|
||||
export function NotificationBell({
|
||||
className,
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
className?: string;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const { user, invites, listedCrews, joinRequests, collectiveRequests, isLoading: crewLoading } =
|
||||
useCrewNotifications();
|
||||
const { notifications: activity, isLoading: activityLoading } = useActivityNotifications();
|
||||
|
|
|
|||
72
src/components/NotificationBellLazy.tsx
Normal file
72
src/components/NotificationBellLazy.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { Suspense, useEffect, useState } from 'react';
|
||||
import { Bell } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTranslation } from '@/contexts/language';
|
||||
import { lazyWithRetry } from '@/lib/lazyWithRetry';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type BellProps = { className?: string; defaultOpen?: boolean };
|
||||
|
||||
// Keep the whole notification center (and its five data sources) out of the
|
||||
// entry bundle. It loads after first paint on idle, or immediately if the user
|
||||
// taps the bell. The placeholder is the same footprint as the real button.
|
||||
const LazyBell = lazyWithRetry(() =>
|
||||
import('@/components/NotificationBell').then((m) => ({
|
||||
default: m.NotificationBell as unknown as React.ComponentType<object>,
|
||||
})),
|
||||
) as React.LazyExoticComponent<React.ComponentType<BellProps>>;
|
||||
|
||||
function scheduleIdle(callback: () => void): () => void {
|
||||
const win = window as Window & {
|
||||
requestIdleCallback?: (cb: () => void, opts?: { timeout: number }) => number;
|
||||
cancelIdleCallback?: (id: number) => void;
|
||||
};
|
||||
if (win.requestIdleCallback) {
|
||||
const id = win.requestIdleCallback(callback, { timeout: 1800 });
|
||||
return () => win.cancelIdleCallback?.(id);
|
||||
}
|
||||
const id = window.setTimeout(callback, 1200);
|
||||
return () => window.clearTimeout(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deferred notification bell. The bell icon is present immediately; the data
|
||||
* hooks and lazy chunk arrive once the map/app has had its first paint.
|
||||
*/
|
||||
export function NotificationBellLazy({ className }: { className?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [armed, setArmed] = useState(false);
|
||||
const [openOnMount, setOpenOnMount] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (armed) return;
|
||||
return scheduleIdle(() => setArmed(true));
|
||||
}, [armed]);
|
||||
|
||||
const placeholder = (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'group relative text-gray-300 transition-colors hover:text-[#ccff00] hover:bg-[#ccff00]/10',
|
||||
className,
|
||||
)}
|
||||
title={t('socialUi.bell.notifications')}
|
||||
aria-label={t('socialUi.bell.notifications')}
|
||||
onClick={() => {
|
||||
setOpenOnMount(true);
|
||||
setArmed(true);
|
||||
}}
|
||||
>
|
||||
<Bell className="h-5 w-5" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (!armed) return placeholder;
|
||||
|
||||
return (
|
||||
<Suspense fallback={placeholder}>
|
||||
<LazyBell className={className} defaultOpen={openOnMount} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue