From 97aa9e31856edb9d8abcdcb33c472027f5588890 Mon Sep 17 00:00:00 2001 From: Wes Date: Sun, 9 Aug 2026 10:05:47 -0600 Subject: [PATCH] fix(desktop): preserve Welcome banner dismissal (#5406) ## Summary - remove the complete Welcome guidance surface when dismissal reaches `hidden` - preserve dismissal across the private and starter Welcome channels for the active identity - assert the starter channel's actual `welcome-everyone` title on re-entry ## Why PR #5330 introduced two deterministic Desktop E2E failures: - the inner banner unmounted, but `welcome-composer-guidance-layer` remained - the re-entry test expected case-sensitive `Welcome` while navigating to `welcome-everyone` The state hook also scoped completion to channel IDs while `ChannelPane` remounts during navigation. The Welcome guidance is one experience spanning both Welcome channels, so completion now survives that remount while remaining identity-scoped. ## Validation At `b577eb42edffe889f63566f2457eacea720f3593`: - `pnpm -C desktop typecheck` - focused Biome check for all four changed files - E2E build - both `welcome-everywhere banner` integration tests repeated three times: **6/6 passed** - mandatory pre-push desktop check, typecheck, and full desktop unit suite: **4,535 passed** - `git diff --check` Signed-off-by: Wes Co-authored-by: Carl --- .../src/features/channels/ui/ChannelPane.tsx | 6 ++- .../channels/ui/WelcomeComposerBanner.tsx | 4 ++ .../channels/ui/useWelcomeComposerBanner.ts | 39 +++++++++++++------ desktop/tests/e2e/onboarding.spec.ts | 4 +- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 13790d779..410b05a2c 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -216,7 +216,11 @@ export const ChannelPane = React.memo(function ChannelPane({ bannerState: welcomeComposerBannerState, completeBanner: completeWelcomeComposerBanner, dismissBanner: handleDismissWelcomeBanner, - } = useWelcomeComposerBanner(activeChannelId, isActiveWelcomeChannel); + } = useWelcomeComposerBanner( + activeChannelId, + isActiveWelcomeChannel, + currentPubkey ?? null, + ); const isEditInThread = editTarget != null && threadHeadMessage != null && diff --git a/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx b/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx index f050dd4b9..9a8db082e 100644 --- a/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx +++ b/desktop/src/features/channels/ui/WelcomeComposerBanner.tsx @@ -443,6 +443,10 @@ export function WelcomeComposerGuidanceLayer({ settingUp, state, }: WelcomeComposerGuidanceLayerProps) { + if (state === "hidden") { + return null; + } + return (
(); + /** * Manages the Welcome-channel composer hint banner's state machine. * - * Tracks which channels have been completed within the session so the banner - * stays hidden on re-entry. Exposes three transitions: + * Remembers completion across the Welcome experience per identity for this app + * session, so the hint stays hidden while moving between the private and + * starter Welcome channels without leaking dismissal to another identity. * - `completeBanner`: agent-mention path — plays the "Nice work." success * animation before auto-dismissing. * - `dismissBanner`: manual X-button path — immediately begins the slide-down @@ -21,12 +24,12 @@ import { export function useWelcomeComposerBanner( activeChannelId: string | null, isActiveWelcomeChannel: boolean, + identityPubkey: string | null, ): { bannerState: WelcomeComposerBannerState; completeBanner: () => void; dismissBanner: () => void; } { - const completedChannelIdsRef = React.useRef(new Set()); const dismissTimerRef = React.useRef(null); const hideTimerRef = React.useRef(null); const [bannerState, setBannerState] = @@ -48,15 +51,15 @@ export function useWelcomeComposerBanner( React.useEffect(() => { clearTimers(); if ( - activeChannelId && isActiveWelcomeChannel && - completedChannelIdsRef.current.has(activeChannelId) + identityPubkey && + completedWelcomeComposerIdentityPubkeys.has(identityPubkey) ) { setBannerState("hidden"); return; } setBannerState("prompt"); - }, [activeChannelId, clearTimers, isActiveWelcomeChannel]); + }, [clearTimers, identityPubkey, isActiveWelcomeChannel]); const scheduleHide = React.useCallback(() => { hideTimerRef.current = window.setTimeout( @@ -70,30 +73,42 @@ export function useWelcomeComposerBanner( }, []); const completeBanner = React.useCallback(() => { - if (!activeChannelId || !isActiveWelcomeChannel) { + if (!activeChannelId || !isActiveWelcomeChannel || !identityPubkey) { return; } clearTimers(); - completedChannelIdsRef.current.add(activeChannelId); + completedWelcomeComposerIdentityPubkeys.add(identityPubkey); setBannerState("complete"); dismissTimerRef.current = window.setTimeout(() => { setBannerState("dismissing"); dismissTimerRef.current = null; scheduleHide(); }, WELCOME_PERSONA_ROTATION_MS + WELCOME_COMPOSER_BANNER_SUCCESS_SETTLE_MS); - }, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]); + }, [ + activeChannelId, + clearTimers, + identityPubkey, + isActiveWelcomeChannel, + scheduleHide, + ]); const dismissBanner = React.useCallback(() => { - if (!activeChannelId || !isActiveWelcomeChannel) { + if (!activeChannelId || !isActiveWelcomeChannel || !identityPubkey) { return; } clearTimers(); - completedChannelIdsRef.current.add(activeChannelId); + completedWelcomeComposerIdentityPubkeys.add(identityPubkey); setBannerState("dismissing"); scheduleHide(); - }, [activeChannelId, clearTimers, isActiveWelcomeChannel, scheduleHide]); + }, [ + activeChannelId, + clearTimers, + identityPubkey, + isActiveWelcomeChannel, + scheduleHide, + ]); return { bannerState, completeBanner, dismissBanner }; } diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 5e0e6a4fa..403edbcda 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -3218,7 +3218,9 @@ test("welcome-everywhere banner: dismiss persists after channel re-entry", async // Return — banner must stay hidden. await page.getByTestId("channel-welcome-everyone").click(); - await expect(page.getByTestId("chat-title")).toContainText("Welcome"); + await expect(page.getByTestId("chat-title")).toContainText( + "welcome-everyone", + ); await expect(banner).toHaveCount(0); });