From a9898cd973c8a276515dd65dadd7b21afcc89d26 Mon Sep 17 00:00:00 2001 From: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> Date: Mon, 3 Aug 2026 14:03:15 -0400 Subject: [PATCH] feat(experiments): gate local communities Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz> --- .../communities/ui/AddCommunityDialog.tsx | 3 + .../ui/CommunityCreationChoice.tsx | 64 ++++++++++--------- .../features/communities/ui/WelcomeSetup.tsx | 3 + desktop/tests/e2e/onboarding.spec.ts | 60 +++++++++++++++++ desktop/tests/e2e/sidebar.spec.ts | 50 ++++++++++++++- preview-features.json | 6 ++ 6 files changed, 155 insertions(+), 31 deletions(-) diff --git a/desktop/src/features/communities/ui/AddCommunityDialog.tsx b/desktop/src/features/communities/ui/AddCommunityDialog.tsx index 108ba4c35..b636aaf31 100644 --- a/desktop/src/features/communities/ui/AddCommunityDialog.tsx +++ b/desktop/src/features/communities/ui/AddCommunityDialog.tsx @@ -11,6 +11,7 @@ import { CommunityCreationChoice } from "@/features/communities/ui/CommunityCrea import { HostedCommunityCreateFlow } from "@/features/communities/ui/HostedCommunityCreateFlow"; import { useCommunityOnboarding } from "@/features/onboarding/communityOnboarding"; import { InviteRedeemForm } from "@/features/onboarding/ui/InviteRedeemForm"; +import { useFeatureEnabled } from "@/shared/features"; import { Dialog, DialogContent, @@ -39,6 +40,7 @@ export function AddCommunityDialog({ onOpenChange, }: AddCommunityDialogProps) { const communityOnboarding = useCommunityOnboarding(); + const localCommunitiesEnabled = useFeatureEnabled("localCommunities"); const { communities } = useCommunities(); const hasLocalCommunity = communities.some((community) => community.local); const [mode, setMode] = React.useState("choose"); @@ -220,6 +222,7 @@ export function AddCommunityDialog({ hasLocalCommunity={hasLocalCommunity} onChooseHosted={() => setMode("create-hosted")} onChooseLocal={startLocalCommunity} + showLocalOption={localCommunitiesEnabled} variant="dialog" /> ) : ( diff --git a/desktop/src/features/communities/ui/CommunityCreationChoice.tsx b/desktop/src/features/communities/ui/CommunityCreationChoice.tsx index 307db6958..e640c398e 100644 --- a/desktop/src/features/communities/ui/CommunityCreationChoice.tsx +++ b/desktop/src/features/communities/ui/CommunityCreationChoice.tsx @@ -16,6 +16,7 @@ type CommunityCreationChoiceProps = { hasLocalCommunity?: boolean; onChooseHosted: () => void; onChooseLocal: () => void; + showLocalOption: boolean; variant: "onboarding" | "dialog"; }; @@ -23,6 +24,7 @@ export function CommunityCreationChoice({ hasLocalCommunity = false, onChooseHosted, onChooseLocal, + showLocalOption, variant, }: CommunityCreationChoiceProps) { const localLabel = hasLocalCommunity @@ -44,18 +46,20 @@ export function CommunityCreationChoice({ - - - + {showLocalOption ? ( + + + + ) : null} ); } @@ -81,25 +85,27 @@ export function CommunityCreationChoice({ - + + + ) : null} ); } diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index e309b2767..3b00fa823 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -19,6 +19,7 @@ import { OnboardingSlideTransition, } from "@/features/onboarding/ui/OnboardingSlideTransition"; import { useIdentityQuery } from "@/shared/api/hooks"; +import { useFeatureEnabled } from "@/shared/features"; import { writeTextToClipboard } from "@/shared/lib/clipboard"; import { pubkeyToNpub } from "@/shared/lib/nostrUtils"; import { useSystemColorScheme } from "@/shared/theme/useSystemColorScheme"; @@ -58,6 +59,7 @@ export function WelcomeSetup({ const [isHostedSignInOpen, setIsHostedSignInOpen] = React.useState(false); const [copiedNpub, setCopiedNpub] = React.useState(false); const communityOnboarding = useCommunityOnboarding(); + const localCommunitiesEnabled = useFeatureEnabled("localCommunities"); const identityQuery = useIdentityQuery(); const systemColorScheme = useSystemColorScheme(); const npub = identityQuery.data?.pubkey @@ -215,6 +217,7 @@ export function WelcomeSetup({ setIsHostedSignInOpen(true)} onChooseLocal={startLocalCommunity} + showLocalOption={localCommunitiesEnabled} variant="onboarding" /> diff --git a/desktop/tests/e2e/onboarding.spec.ts b/desktop/tests/e2e/onboarding.spec.ts index 3f5733b2e..d652675c0 100644 --- a/desktop/tests/e2e/onboarding.spec.ts +++ b/desktop/tests/e2e/onboarding.spec.ts @@ -3,6 +3,7 @@ import { expect, test, type Page } from "@playwright/test"; import { nsecEncode } from "nostr-tools/nip19"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; +import { FEATURE_OVERRIDES_STORAGE_KEY } from "../helpers/features"; import { installFakeCamera } from "../helpers/fakeCamera"; import { E2E_IDENTITY_OVERRIDE_STORAGE_KEY, @@ -80,6 +81,18 @@ const FIRST_RUN_ALICE = { username: "", }; +async function setLocalCommunitiesFeature(page: Page, enabled: boolean) { + await page.addInitScript( + ({ key, enabled }) => { + window.localStorage.setItem( + key, + JSON.stringify({ localCommunities: enabled }), + ); + }, + { key: FEATURE_OVERRIDES_STORAGE_KEY, enabled }, + ); +} + async function seedOnboardingCompletion(page: Page, pubkey: string) { await page.addInitScript( ({ storageKey }) => { @@ -1064,6 +1077,53 @@ test("non-local default auto-connects when the release flag is enabled", async ( }); }); +test("first-community hides the local choice by default", async ({ page }) => { + await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); + await page.addInitScript((pubkey) => { + window.localStorage.setItem( + `buzz-machine-onboarding-complete.v2:${pubkey}`, + "true", + ); + }, BLANK_TYLER_IDENTITY.pubkey); + await installMockBridge(page, undefined, { + relayWsUrl: "ws://localhost:3000", + seedPreviewFeatures: false, + skipOnboardingSeed: true, + skipCommunitySeed: true, + }); + await setLocalCommunitiesFeature(page, false); + await page.goto("/"); + + await page.getByTestId("community-choice-create").click(); + await expect(page.getByTestId("community-create-hosted")).toBeVisible(); + await expect(page.getByTestId("community-choice-local")).toHaveCount(0); +}); + +test("first-community shows the local choice when its preview is enabled", async ({ + page, +}) => { + await seedActiveIdentity(page, BLANK_TYLER_IDENTITY); + await setLocalCommunitiesFeature(page, true); + await page.addInitScript((pubkey) => { + window.localStorage.setItem( + `buzz-machine-onboarding-complete.v2:${pubkey}`, + "true", + ); + }, BLANK_TYLER_IDENTITY.pubkey); + await installMockBridge(page, undefined, { + relayWsUrl: "ws://localhost:3000", + seedPreviewFeatures: false, + skipOnboardingSeed: true, + skipCommunitySeed: true, + }); + await page.goto("/"); + + await page.getByTestId("community-choice-create").click(); + await expect(page.getByTestId("community-choice-local")).toContainText( + "Keep it on this device", + ); +}); + test("first-community choices route join, create, owner, and member intents", async ({ page, }) => { diff --git a/desktop/tests/e2e/sidebar.spec.ts b/desktop/tests/e2e/sidebar.spec.ts index be97ba4e0..b11ef5ed5 100644 --- a/desktop/tests/e2e/sidebar.spec.ts +++ b/desktop/tests/e2e/sidebar.spec.ts @@ -1,6 +1,7 @@ import { expect, test, type Page } from "@playwright/test"; import { installMockBridge } from "../helpers/bridge"; +import { FEATURE_OVERRIDES_STORAGE_KEY } from "../helpers/features"; import { openSettings } from "../helpers/settings"; const SIDEBAR_WIDTH_STORAGE_KEY = "buzz-sidebar-width"; @@ -39,6 +40,18 @@ async function openAddCommunityDialog(page: Page) { await page.getByRole("menuitem", { name: "Add a community" }).click(); } +async function setLocalCommunitiesFeature(page: Page, enabled: boolean) { + await page.addInitScript( + ({ key, enabled }) => { + window.localStorage.setItem( + key, + JSON.stringify({ localCommunities: enabled }), + ); + }, + { key: FEATURE_OVERRIDES_STORAGE_KEY, enabled }, + ); +} + // Regression guard for the "Leave channel" lockup: with two bundled copies of // @radix-ui/react-dismissable-layer, opening a modal AlertDialog from a modal // Radix ContextMenu left `pointer-events: none` stuck on after the @@ -74,6 +87,33 @@ async function dragSidebarRail(page: Page, deltaX: number) { await page.mouse.up(); } +test("add community hides the local choice by default", async ({ page }) => { + await installMockBridge(page, {}, { seedPreviewFeatures: false }); + await setLocalCommunitiesFeature(page, false); + await page.goto("/"); + + await openAddCommunityDialog(page); + await page.getByTestId("add-community-create").click(); + + await expect(page.getByTestId("community-create-hosted")).toBeVisible(); + await expect(page.getByTestId("community-choice-local")).toHaveCount(0); +}); + +test("add community shows the local choice when its preview is enabled", async ({ + page, +}) => { + await setLocalCommunitiesFeature(page, true); + await installMockBridge(page, {}, { seedPreviewFeatures: false }); + await page.goto("/"); + + await openAddCommunityDialog(page); + await page.getByTestId("add-community-create").click(); + + await expect(page.getByTestId("community-choice-local")).toContainText( + "Keep it on this device", + ); +}); + test("add community starts with create and join choices", async ({ page }) => { await installMockBridge(page, {}); await page.goto("/"); @@ -118,7 +158,12 @@ test("add community starts with create and join choices", async ({ page }) => { test("add community can start an on-this-device community", async ({ page, }) => { - await installMockBridge(page, { applyCommunityDelayMs: 1_000 }); + await setLocalCommunitiesFeature(page, true); + await installMockBridge( + page, + { applyCommunityDelayMs: 1_000 }, + { seedPreviewFeatures: false }, + ); await page.goto("/"); await openAddCommunityDialog(page); @@ -174,10 +219,11 @@ test("create choice offers to open an existing on-this-device community", async ); window.localStorage.setItem("buzz-active-community-id", "hosted"); }); + await setLocalCommunitiesFeature(page, true); await installMockBridge( page, { applyCommunityDelayMs: 1_000 }, - { skipCommunitySeed: true }, + { seedPreviewFeatures: false, skipCommunitySeed: true }, ); await page.goto("/"); diff --git a/preview-features.json b/preview-features.json index 388f1c39b..de991d93c 100644 --- a/preview-features.json +++ b/preview-features.json @@ -30,6 +30,12 @@ "name": "Agent-managed profiles", "description": "Let agents manage their own relay name and avatar instead of restoring the desktop copy", "platforms": ["desktop"] + }, + { + "id": "localCommunities", + "name": "Local communities", + "description": "Create a community that lives entirely on this device (local relay)", + "platforms": ["desktop"] } ] }