From a9d14063877aae89d9a2a4b6c631c6c56c126b95 Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
<52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Date: Wed, 29 Jul 2026 14:06:52 -0700
Subject: [PATCH 1/2] feat(desktop): leave communities through relay
Co-authored-by: Taylor Ho
Signed-off-by: Taylor Ho
---
desktop/src/app/AppShell.tsx | 6 +-
.../app/useCommunityNavigationTransitions.ts | 21 +++-
.../communities/leaveCommunity.test.mjs | 118 ++++++++++++++++++
.../features/communities/leaveCommunity.ts | 59 +++++++++
.../resolveCommunityRemoval.test.mjs | 28 +++++
.../communities/ui/CommunitySwitcher.tsx | 4 +-
.../communities/ui/EditCommunityDialog.tsx | 48 +++++--
.../features/communities/useCommunities.tsx | 68 ++++++----
.../src/features/sidebar/ui/AppSidebar.tsx | 2 +-
.../src/features/sidebar/ui/CommunityRail.tsx | 4 +-
.../sidebar/ui/SidebarProfileCard.tsx | 2 +-
desktop/src/testing/e2eBridge.ts | 5 +
desktop/tests/e2e/community-rail.spec.ts | 35 +++++-
13 files changed, 353 insertions(+), 47 deletions(-)
create mode 100644 desktop/src/features/communities/leaveCommunity.test.mjs
create mode 100644 desktop/src/features/communities/leaveCommunity.ts
create mode 100644 desktop/src/features/communities/resolveCommunityRemoval.test.mjs
diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx
index abbbf29610..025bec9970 100644
--- a/desktop/src/app/AppShell.tsx
+++ b/desktop/src/app/AppShell.tsx
@@ -761,7 +761,7 @@ export function AppShell() {
communitiesHook.activeCommunity?.id ?? null
}
onAddCommunity={addCommunityDialog.openDialog}
- onRemoveCommunity={(id) => void handleRemoveCommunity(id)}
+ onRemoveCommunity={handleRemoveCommunity}
onReorderCommunities={communitiesHook.reorderCommunities}
onSwitchCommunity={handleSwitchCommunity}
onUpdateCommunity={communitiesHook.updateCommunity}
@@ -854,9 +854,7 @@ export function AppShell() {
onOpenAddCommunity={addCommunityDialog.openDialog}
onSendFeedback={() => setIsSendFeedbackOpen(true)}
onUpdateCommunity={communitiesHook.updateCommunity}
- onRemoveCommunity={(id) =>
- void handleRemoveCommunity(id)
- }
+ onRemoveCommunity={handleRemoveCommunity}
onSwitchCommunity={handleSwitchCommunity}
onCreateAgent={() => requestOpenCreateAgent()}
selfPresenceStatus={presenceSession.currentStatus}
diff --git a/desktop/src/app/useCommunityNavigationTransitions.ts b/desktop/src/app/useCommunityNavigationTransitions.ts
index 88acb30ad0..3e123c50c3 100644
--- a/desktop/src/app/useCommunityNavigationTransitions.ts
+++ b/desktop/src/app/useCommunityNavigationTransitions.ts
@@ -13,6 +13,7 @@ import {
saveCommunityDestination,
} from "@/features/communities/communityNavigationStorage";
import type { useCommunities } from "@/features/communities/useCommunities";
+import { leaveCommunity } from "@/features/communities/leaveCommunity";
type Communities = ReturnType;
type ShellRoute = ReturnType;
@@ -71,6 +72,19 @@ export function useCommunityNavigationTransitions({
const removeCommunity = React.useCallback(
async (id: string) => {
+ const target = communities.communities.find(
+ (community) => community.id === id,
+ );
+ if (!target) return;
+
+ // Do not touch local state until this relay has explicitly accepted the
+ // signed NIP-43 leave request. Rejections and timeouts bubble back to the
+ // dialog so the person can retry without losing their community config.
+ await leaveCommunity(
+ target.relayUrl,
+ communities.activeCommunity?.relayUrl,
+ );
+
if (id !== communities.activeCommunity?.id) {
communities.removeCommunity(id);
return;
@@ -78,7 +92,12 @@ export function useCommunityNavigationTransitions({
const fallback = communities.communities.find(
(community) => community.id !== id,
);
- if (!fallback) return;
+
+ if (!fallback) {
+ await goHome({ replace: true });
+ communities.removeCommunity(id);
+ return;
+ }
await runCommunityViewTransition(async () => {
saveActiveDestination();
diff --git a/desktop/src/features/communities/leaveCommunity.test.mjs b/desktop/src/features/communities/leaveCommunity.test.mjs
new file mode 100644
index 0000000000..46bbd5e7d2
--- /dev/null
+++ b/desktop/src/features/communities/leaveCommunity.test.mjs
@@ -0,0 +1,118 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { KIND_NIP43_LEAVE_REQUEST, leaveCommunity } from "./leaveCommunity.ts";
+
+const signedEvent = {
+ id: "event-id",
+ pubkey: "a".repeat(64),
+ created_at: 1,
+ kind: KIND_NIP43_LEAVE_REQUEST,
+ tags: [["-"]],
+ content: "",
+ sig: "b".repeat(128),
+};
+
+function dependencies(overrides = {}) {
+ return {
+ sign: async (input) => ({ ...signedEvent, ...input }),
+ publishActive: async () => {},
+ createRelayClient: () => ({
+ publishEvent: async () => {},
+ disconnect() {},
+ }),
+ ...overrides,
+ };
+}
+
+test("signs the protected NIP-43 leave request and awaits active relay acceptance", async () => {
+ let signInput;
+ let published;
+ await leaveCommunity(
+ "wss://active.example",
+ "wss://active.example",
+ dependencies({
+ sign: async (input) => {
+ signInput = input;
+ return signedEvent;
+ },
+ publishActive: async (event) => {
+ published = event;
+ },
+ createRelayClient: () => {
+ throw new Error("inactive client should not be created");
+ },
+ }),
+ );
+
+ assert.deepEqual(signInput, {
+ kind: KIND_NIP43_LEAVE_REQUEST,
+ content: "",
+ tags: [["-"]],
+ });
+ assert.equal(published, signedEvent);
+});
+
+test("targets an inactive community relay and always disconnects", async () => {
+ const calls = [];
+ await leaveCommunity(
+ "wss://inactive.example",
+ "wss://active.example",
+ dependencies({
+ publishActive: async () => {
+ throw new Error("active relay should not be used");
+ },
+ createRelayClient: (relayUrl) => ({
+ publishEvent: async (event) => calls.push(["publish", relayUrl, event]),
+ disconnect: () => calls.push(["disconnect"]),
+ }),
+ }),
+ );
+
+ assert.deepEqual(calls, [
+ ["publish", "wss://inactive.example", signedEvent],
+ ["disconnect"],
+ ]);
+});
+
+test("preserves relay rejection and disconnects without falling through", async () => {
+ const rejection = new Error("invalid: relay owner cannot leave");
+ let disconnected = false;
+
+ await assert.rejects(
+ leaveCommunity(
+ "wss://inactive.example",
+ "wss://active.example",
+ dependencies({
+ createRelayClient: () => ({
+ publishEvent: async () => {
+ throw rejection;
+ },
+ disconnect: () => {
+ disconnected = true;
+ },
+ }),
+ }),
+ ),
+ rejection,
+ );
+ assert.equal(disconnected, true);
+});
+
+test("turns an inactive relay timeout into an actionable leave error", async () => {
+ await assert.rejects(
+ leaveCommunity(
+ "wss://inactive.example",
+ "wss://active.example",
+ dependencies({
+ createRelayClient: () => ({
+ publishEvent: async () => {
+ throw new Error("Timed out publishing to observer relay.");
+ },
+ disconnect() {},
+ }),
+ }),
+ ),
+ /Timed out while leaving the community\. Try again\./,
+ );
+});
diff --git a/desktop/src/features/communities/leaveCommunity.ts b/desktop/src/features/communities/leaveCommunity.ts
new file mode 100644
index 0000000000..c1aca1e73f
--- /dev/null
+++ b/desktop/src/features/communities/leaveCommunity.ts
@@ -0,0 +1,59 @@
+import { relayClient } from "@/shared/api/relayClient";
+import { ReadOnlyRelayClient } from "@/shared/api/readOnlyRelayClient";
+import { signRelayEvent } from "@/shared/api/tauri";
+import type { RelayEvent } from "@/shared/api/types";
+
+export const KIND_NIP43_LEAVE_REQUEST = 28936;
+
+type LeaveCommunityDependencies = {
+ sign: typeof signRelayEvent;
+ publishActive: (event: RelayEvent) => Promise;
+ createRelayClient: (relayUrl: string) => {
+ publishEvent: (event: RelayEvent) => Promise;
+ disconnect: () => void;
+ };
+};
+
+const defaultDependencies: LeaveCommunityDependencies = {
+ sign: signRelayEvent,
+ publishActive: (event) =>
+ relayClient.publishEvent(
+ event,
+ "Timed out while leaving the community. Try again.",
+ "Failed to send the leave request. Check your connection and try again.",
+ ),
+ createRelayClient: (relayUrl) => new ReadOnlyRelayClient(relayUrl),
+};
+
+/** Revoke relay membership and resolve only after the relay accepts the request. */
+export async function leaveCommunity(
+ relayUrl: string,
+ activeRelayUrl: string | undefined,
+ dependencies: LeaveCommunityDependencies = defaultDependencies,
+): Promise {
+ const event = await dependencies.sign({
+ kind: KIND_NIP43_LEAVE_REQUEST,
+ content: "",
+ tags: [["-"]],
+ });
+
+ if (relayUrl === activeRelayUrl) {
+ await dependencies.publishActive(event);
+ return;
+ }
+
+ const client = dependencies.createRelayClient(relayUrl);
+ try {
+ await client.publishEvent(event);
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ error.message.toLowerCase().includes("timed out")
+ ) {
+ throw new Error("Timed out while leaving the community. Try again.");
+ }
+ throw error;
+ } finally {
+ client.disconnect();
+ }
+}
diff --git a/desktop/src/features/communities/resolveCommunityRemoval.test.mjs b/desktop/src/features/communities/resolveCommunityRemoval.test.mjs
new file mode 100644
index 0000000000..6d24ee4db1
--- /dev/null
+++ b/desktop/src/features/communities/resolveCommunityRemoval.test.mjs
@@ -0,0 +1,28 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { resolveCommunityRemoval } from "./useCommunities.tsx";
+
+const alpha = { id: "alpha", name: "Alpha", relayUrl: "wss://alpha" };
+const beta = { id: "beta", name: "Beta", relayUrl: "wss://beta" };
+
+test("removing the final community clears the active community", () => {
+ assert.deepEqual(resolveCommunityRemoval([alpha], "alpha", "alpha"), {
+ communities: [],
+ activeId: null,
+ });
+});
+
+test("removing the active community selects a clean fallback", () => {
+ assert.deepEqual(resolveCommunityRemoval([alpha, beta], "alpha", "alpha"), {
+ communities: [beta],
+ activeId: "beta",
+ });
+});
+
+test("removing an inactive community preserves the active community", () => {
+ assert.deepEqual(resolveCommunityRemoval([alpha, beta], "alpha", "beta"), {
+ communities: [alpha],
+ activeId: "alpha",
+ });
+});
diff --git a/desktop/src/features/communities/ui/CommunitySwitcher.tsx b/desktop/src/features/communities/ui/CommunitySwitcher.tsx
index 985d28fa8e..d4becf1d6c 100644
--- a/desktop/src/features/communities/ui/CommunitySwitcher.tsx
+++ b/desktop/src/features/communities/ui/CommunitySwitcher.tsx
@@ -57,7 +57,7 @@ type CommunitySwitcherProps = {
id: string,
updates: Partial>,
) => void;
- onRemoveCommunity: (id: string) => void;
+ onRemoveCommunity: (id: string) => Promise;
};
export function CommunityEmojiIcon({
@@ -391,7 +391,7 @@ export function CommunitySwitcher({
)}
1}
+ canRemove
onOpenChange={(open) => {
if (!open) setEditingCommunity(null);
}}
diff --git a/desktop/src/features/communities/ui/EditCommunityDialog.tsx b/desktop/src/features/communities/ui/EditCommunityDialog.tsx
index 3b963979bc..de9811ede3 100644
--- a/desktop/src/features/communities/ui/EditCommunityDialog.tsx
+++ b/desktop/src/features/communities/ui/EditCommunityDialog.tsx
@@ -28,7 +28,7 @@ type EditCommunityDialogProps = {
Pick
>,
) => void;
- onRemove?: (id: string) => void;
+ onRemove?: (id: string) => Promise;
canRemove?: boolean;
showIconEditor?: boolean;
};
@@ -47,6 +47,8 @@ export function EditCommunityDialog({
const [token, setToken] = React.useState("");
const [reposDir, setReposDir] = React.useState("");
const [reposDirError, setReposDirError] = React.useState(null);
+ const [leaveError, setLeaveError] = React.useState(null);
+ const [isLeaving, setIsLeaving] = React.useState(false);
const membershipQuery = useMyRelayMembershipLookupQuery();
const activeRole = membershipQuery.data?.membership?.role;
const canEditIcon =
@@ -63,6 +65,8 @@ export function EditCommunityDialog({
setToken(community.token ?? "");
setReposDir(community.reposDir ?? "");
setReposDirError(null);
+ setLeaveError(null);
+ setIsLeaving(false);
}
}, [community, open]);
@@ -122,12 +126,24 @@ export function EditCommunityDialog({
[community, name, relayUrl, token, reposDir, onSave, handleClose],
);
- const handleRemove = React.useCallback(() => {
- if (community && onRemove) {
- onRemove(community.id);
+ const handleRemove = React.useCallback(async () => {
+ if (!community || !onRemove || isLeaving) return;
+
+ setIsLeaving(true);
+ setLeaveError(null);
+ try {
+ await onRemove(community.id);
handleClose();
+ } catch (error) {
+ setLeaveError(
+ error instanceof Error
+ ? error.message
+ : "Could not leave the community. Try again.",
+ );
+ } finally {
+ setIsLeaving(false);
}
- }, [community, onRemove, handleClose]);
+ }, [community, onRemove, isLeaving, handleClose]);
if (!community) {
return null;
@@ -235,25 +251,39 @@ export function EditCommunityDialog({
the default location.
+ {leaveError ? (
+
+ {leaveError}
+
+ ) : null}
{canRemove && onRemove ? (
) : null}
-
diff --git a/desktop/src/features/communities/useCommunities.tsx b/desktop/src/features/communities/useCommunities.tsx
index df019519b6..27088e7add 100644
--- a/desktop/src/features/communities/useCommunities.tsx
+++ b/desktop/src/features/communities/useCommunities.tsx
@@ -112,6 +112,23 @@ export function applyCommunitiesOrder(
return reordered;
}
+export type CommunityRemovalResult = {
+ communities: Community[];
+ activeId: string | null;
+};
+
+export function resolveCommunityRemoval(
+ communities: Community[],
+ activeId: string | null,
+ id: string,
+): CommunityRemovalResult {
+ const next = communities.filter((community) => community.id !== id);
+ return {
+ communities: next,
+ activeId: activeId === id ? (next[0]?.id ?? null) : activeId,
+ };
+}
+
export type UseCommunitiesReturn = {
communities: Community[];
activeCommunity: Community | null;
@@ -205,39 +222,38 @@ function useCommunitiesInternal(): UseCommunitiesReturn {
const removeCommunity = useCallback(
(id: string) => {
- // GC self-profile caches for the removed community's relay. Mirror the
- // updater guard (length > 1) so we only GC when removal will actually
- // proceed. Runs outside the updater — updaters can execute twice under
+ const removed = communitiesRef.current.find(
+ (community) => community.id === id,
+ );
+ if (!removed) return;
+
+ // Relay membership is revoked by the caller before this local cleanup.
+ // Keep side effects outside the updater — updaters can execute twice under
// React StrictMode.
- if (communities.length > 1) {
- const removed = communities.find((w) => w.id === id);
- if (removed) {
- removeSelfProfileCachesForRelay(removed.relayUrl);
- removeChannelSnapshotForRelay(removed.relayUrl);
- removeMessageSnapshotsForRelay(removed.relayUrl);
- clearSavedCommunitySnapshot(id);
- removeCommunityDestination(id);
- }
- }
+ removeSelfProfileCachesForRelay(removed.relayUrl);
+ removeChannelSnapshotForRelay(removed.relayUrl);
+ removeMessageSnapshotsForRelay(removed.relayUrl);
+ clearSavedCommunitySnapshot(id);
+ removeCommunityDestination(id);
setCommunitiesState((prev) => {
- // Never allow removing the last community
- if (prev.length <= 1) {
- return prev;
- }
- const next = prev.filter((w) => w.id !== id);
- saveCommunities(next);
-
- // If removing the active community, switch to first remaining
- if (activeId === id && next.length > 0) {
- saveActiveCommunityId(next[0].id);
- setActiveId(next[0].id);
+ const result = resolveCommunityRemoval(prev, activeId, id);
+ if (result.communities.length === 0) {
+ clearCommunityStorage();
+ setActiveId(null);
+ } else {
+ saveCommunities(result.communities);
+
+ if (result.activeId !== activeId && result.activeId) {
+ saveActiveCommunityId(result.activeId);
+ setActiveId(result.activeId);
+ }
}
- return next;
+ return result.communities;
});
},
- [activeId, communities],
+ [activeId],
);
const switchCommunity = useCallback(
diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx
index 55f467f215..014ed6fd02 100644
--- a/desktop/src/features/sidebar/ui/AppSidebar.tsx
+++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx
@@ -140,7 +140,7 @@ type AppSidebarProps = {
id: string,
updates: Partial
>,
) => void;
- onRemoveCommunity: (id: string) => void;
+ onRemoveCommunity: (id: string) => Promise;
onCreateAgent: () => void;
onSelectAgents: () => void;
onSelectProjects: () => void;
diff --git a/desktop/src/features/sidebar/ui/CommunityRail.tsx b/desktop/src/features/sidebar/ui/CommunityRail.tsx
index 386ee20691..2727f13fb6 100644
--- a/desktop/src/features/sidebar/ui/CommunityRail.tsx
+++ b/desktop/src/features/sidebar/ui/CommunityRail.tsx
@@ -48,7 +48,7 @@ type CommunityRailProps = {
id: string,
updates: Partial>,
) => void;
- onRemoveCommunity: (id: string) => void;
+ onRemoveCommunity: (id: string) => Promise;
onReorderCommunities: (orderedIds: string[]) => void;
};
@@ -423,7 +423,7 @@ export function CommunityRail({
Add community
1}
+ canRemove
onOpenChange={(open) => {
if (!open) setEditingCommunity(null);
}}
diff --git a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
index 5db10509bf..f0b9aa3fc0 100644
--- a/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
+++ b/desktop/src/features/sidebar/ui/SidebarProfileCard.tsx
@@ -22,7 +22,7 @@ type SidebarProfileCardProps = {
isPresencePending?: boolean;
onOpenAddCommunity: () => void;
onOpenSettings: (section?: SettingsSection) => void;
- onRemoveCommunity: (id: string) => void;
+ onRemoveCommunity: (id: string) => Promise;
onSendFeedback?: () => void;
onSetPresenceStatus?: (status: PresenceStatus) => void;
onSetUserStatus: (text: string, emoji: string) => void;
diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts
index 4cfc553df1..686bfa7327 100644
--- a/desktop/src/testing/e2eBridge.ts
+++ b/desktop/src/testing/e2eBridge.ts
@@ -9212,6 +9212,11 @@ function sendToMockSocket(args: {
if (type === "EVENT") {
const event = rest[0] as RelayEvent;
+ if (event.kind === 28936) {
+ sendWsText(socket.handler, ["OK", event.id, true, ""]);
+ return;
+ }
+
if ([9030, 9031, 9032].includes(event.kind)) {
const accepted = updateMockRelayMembershipFromAdminEvent(event);
sendWsText(socket.handler, [
diff --git a/desktop/tests/e2e/community-rail.spec.ts b/desktop/tests/e2e/community-rail.spec.ts
index 0c78108190..6226bcf47a 100644
--- a/desktop/tests/e2e/community-rail.spec.ts
+++ b/desktop/tests/e2e/community-rail.spec.ts
@@ -561,7 +561,7 @@ test.describe("community rail", () => {
.getByTestId(`community-rail-button-${COMMUNITY_A.id}`)
.click({ button: "right" });
await page.getByRole("menuitem", { name: "Community settings" }).click();
- await page.getByRole("button", { name: "Remove Community" }).click();
+ await page.getByRole("button", { name: "Leave Community" }).click();
await expect(page).toHaveURL(randomUrl);
await expect
@@ -601,6 +601,39 @@ test.describe("community rail", () => {
await expect(buttonB).toHaveAttribute("aria-current", "true");
});
+ test("leaving the final community returns to setup without resetting identity", async ({
+ page,
+ }) => {
+ await installMockBridge(page, undefined, { skipCommunitySeed: true });
+ await seedCommunities(page, [COMMUNITY_A], COMMUNITY_A.id);
+ await page.goto("/");
+
+ const identityBefore = await page.evaluate(async () =>
+ window.__TAURI_INTERNALS__.invoke("get_identity"),
+ );
+ await page.getByTestId("sidebar-profile-avatar-button").click();
+ await page.getByTestId("community-switcher").click();
+ await page
+ .getByRole("menu", { name: "Community actions" })
+ .getByRole("menuitem", { name: "Community settings" })
+ .click();
+ await page.getByRole("button", { name: "Leave Community" }).click();
+
+ await expect(page.getByText("Join or create a community")).toBeVisible();
+ await expect
+ .poll(() =>
+ page.evaluate(() => window.localStorage.getItem("buzz-communities")),
+ )
+ .toBeNull();
+ await expect
+ .poll(() =>
+ page.evaluate(async () =>
+ window.__TAURI_INTERNALS__.invoke("get_identity"),
+ ),
+ )
+ .toEqual(identityBefore);
+ });
+
test("hides the rail with a single community", async ({ page }) => {
await installMockBridge(page, undefined, { skipCommunitySeed: true });
await seedCommunities(page, [COMMUNITY_A], COMMUNITY_A.id);
From 4ef7d537532158595877012497539d93c7f3e772 Mon Sep 17 00:00:00 2001
From: npub1223z34hd7vtwc6qj4s7flsxkj644nlre2nthu7lrrmkumhu3xddsrx9r6w
<52a228d6edf316ec6812ac3c9fc0d696ab59fc7954d77e7be31eedcddf91335b@buzz.block.builderlab.xyz>
Date: Thu, 30 Jul 2026 00:47:07 -0700
Subject: [PATCH 2/2] fix(desktop): focus final leave on community discovery
Co-authored-by: Taylor Ho
Signed-off-by: Taylor Ho
---
desktop/src/app/App.tsx | 10 ++++++-
.../features/communities/ui/WelcomeSetup.tsx | 26 ++++++++++---------
desktop/tests/e2e/community-rail.spec.ts | 2 ++
3 files changed, 25 insertions(+), 13 deletions(-)
diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx
index 90b5bf5ebc..ff24112472 100644
--- a/desktop/src/app/App.tsx
+++ b/desktop/src/app/App.tsx
@@ -308,6 +308,12 @@ function CommunityApp({
const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false);
const [resumeFirstCommunityPage, setResumeFirstCommunityPage] =
useState(null);
+ const hasConfiguredCommunityRef = useRef(activeCommunity !== null);
+ if (activeCommunity) {
+ hasConfiguredCommunityRef.current = true;
+ }
+ const isFindingCommunityAfterLeave =
+ activeCommunity === null && hasConfiguredCommunityRef.current;
// Surface nest-related backend events (repos-dir errors, legacy migration)
// as toasts. Mounted before useCommunityInit so the listeners are registered
@@ -501,7 +507,9 @@ function CommunityApp({
appContent = (
);
} else if ("error" in community && community.error) {
diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx
index bde1458222..530933acc2 100644
--- a/desktop/src/features/communities/ui/WelcomeSetup.tsx
+++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx
@@ -27,7 +27,7 @@ type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection;
type WelcomeSetupProps = {
initialPage?: WelcomeSetupPage;
initialTransitionMode?: WelcomeTransitionMode;
- onBack: () => void;
+ onBack?: () => void;
};
const COMMUNITY_OPTION_CARD_CLASS =
@@ -164,17 +164,19 @@ export function WelcomeSetup({
-
-
- Back
-
-
+ {onBack ? (
+
+
+ Back
+
+
+ ) : null}
) : page === "existing" ? (
{
await page.getByRole("button", { name: "Leave Community" }).click();
await expect(page.getByText("Join or create a community")).toBeVisible();
+ await expect(page.getByTestId("welcome-setup-back")).toHaveCount(0);
+ await expect(page.getByTestId("community-choice-join")).toBeVisible();
await expect
.poll(() =>
page.evaluate(() => window.localStorage.getItem("buzz-communities")),