mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(experiments): gate local communities
Signed-off-by: npub1z3hmzc9ryehxzedl5wzlvpyvja0d483peaja5zt6pd0209f9x2jspe2dxh <146fb160a3266e6165bfa385f6048c975eda9e21cf65da097a0b5ea7952532a5@buzz.block.builderlab.xyz>
This commit is contained in:
parent
9d95f8758e
commit
a9898cd973
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user