feat(experiments): gate local communities

Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh
2026-08-10 20:20:42 -04:00
committed by Brother Darryl
parent 9d95f8758e
commit a9898cd973
6 changed files with 155 additions and 31 deletions
@@ -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<AddCommunityMode>("choose");
@@ -220,6 +222,7 @@ export function AddCommunityDialog({
hasLocalCommunity={hasLocalCommunity}
onChooseHosted={() => setMode("create-hosted")}
onChooseLocal={startLocalCommunity}
showLocalOption={localCommunitiesEnabled}
variant="dialog"
/>
) : (
@@ -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({
</span>
</button>
</Card>
<Card asChild className={ONBOARDING_OPTION_CLASS} variant="textured">
<button
data-testid="community-choice-local"
onClick={onChooseLocal}
type="button"
>
<span className="block text-sm font-medium">{localLabel}</span>
<span className="mt-1 block text-xs leading-5 text-foreground/70">
{LOCAL_DESCRIPTION}
</span>
</button>
</Card>
{showLocalOption ? (
<Card asChild className={ONBOARDING_OPTION_CLASS} variant="textured">
<button
data-testid="community-choice-local"
onClick={onChooseLocal}
type="button"
>
<span className="block text-sm font-medium">{localLabel}</span>
<span className="mt-1 block text-xs leading-5 text-foreground/70">
{LOCAL_DESCRIPTION}
</span>
</button>
</Card>
) : null}
</div>
);
}
@@ -81,25 +85,27 @@ export function CommunityCreationChoice({
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/60" />
</button>
<button
className={DIALOG_OPTION_CLASS}
data-testid="community-choice-local"
onClick={onChooseLocal}
type="button"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Laptop className="h-4 w-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-foreground">
{localLabel}
{showLocalOption ? (
<button
className={DIALOG_OPTION_CLASS}
data-testid="community-choice-local"
onClick={onChooseLocal}
type="button"
>
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Laptop className="h-4 w-4" />
</span>
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
{LOCAL_DESCRIPTION}
<span className="min-w-0 flex-1">
<span className="block text-sm font-medium text-foreground">
{localLabel}
</span>
<span className="mt-0.5 block text-xs leading-5 text-muted-foreground">
{LOCAL_DESCRIPTION}
</span>
</span>
</span>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/60" />
</button>
<ChevronRight className="h-4 w-4 shrink-0 text-muted-foreground/60" />
</button>
) : null}
</div>
);
}
@@ -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({
<CommunityCreationChoice
onChooseHosted={() => setIsHostedSignInOpen(true)}
onChooseLocal={startLocalCommunity}
showLocalOption={localCommunitiesEnabled}
variant="onboarding"
/>
</div>
+60
View File
@@ -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,
}) => {
+48 -2
View File
@@ -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 <body> 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("/");
+6
View File
@@ -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"]
}
]
}