From b47880885f87bf6066b339b86d63d255b3701a45 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Thu, 23 Jul 2026 08:44:03 +0200 Subject: [PATCH] fix(desktop): make starter channels best-effort for community joiners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joining an established community failed onboarding with 'starter channels created but metadata not yet available' whenever the community's starter channels (general / welcome-everyone) no longer matched the pristine spec — renamed, made private (their kind:39000 metadata is invisible to a non-member joiner), archived, or converted to a forum. Every new member hit the error deterministically on the Meet-your-starter-team screen. Starter-channel setup is a real requirement only for the community founder creating them; for a joiner, a customized starter set is normal community evolution, not a failure. Downgrade both layers to best-effort: - ensure_starter_channels (Rust) logs and proceeds instead of erroring when the full starter set is not observable; membership joins still happen for whatever starter channels do match. - ensureStarterChannels (TS) returns nullable generalChannel/welcomeChannel instead of throwing when the ensured list lacks a full match. No callers consumed those fields outside tests. The private Welcome channel flow is unchanged and was already independent of this failure. Co-authored-by: Thomas Petersen Signed-off-by: Thomas Petersen --- desktop/src-tauri/src/commands/channels.rs | 15 +++++- .../src/features/onboarding/welcome.test.mjs | 25 ++++++++++ desktop/src/features/onboarding/welcome.ts | 47 ++++++++++--------- 3 files changed, 63 insertions(+), 24 deletions(-) diff --git a/desktop/src-tauri/src/commands/channels.rs b/desktop/src-tauri/src/commands/channels.rs index 59c80c480..814849b32 100644 --- a/desktop/src-tauri/src/commands/channels.rs +++ b/desktop/src-tauri/src/commands/channels.rs @@ -674,8 +674,21 @@ pub async fn ensure_starter_channels( existing_channels = get_channels(state.clone()).await?; } + // Best-effort from here: a spec that still has no match is normal + // community evolution, not a failure. In an established community the + // starter channels may have been renamed, made private (their kind:39000 + // is then invisible to a non-member joiner), archived, or converted to a + // forum — none of which should block a new member's onboarding. + // The freshly-created case needs no rescue either: the relay grants the + // creator owner membership at create time, so a slow kind:39000 only + // delays sidebar visibility. `ensure_starter_channel_memberships` skips + // unmatched specs, joining only the starter channels that are actually + // observable. if !has_all_starter_channels(&existing_channels) { - return Err("starter channels created but metadata not yet available".to_string()); + eprintln!( + "buzz-desktop: ensure_starter_channels: proceeding without full starter set \ + (metadata missing or channels customized)" + ); } ensure_starter_channel_memberships(&state, &creator_keys, &mut existing_channels).await?; diff --git a/desktop/src/features/onboarding/welcome.test.mjs b/desktop/src/features/onboarding/welcome.test.mjs index e0a57a458..b7c7df039 100644 --- a/desktop/src/features/onboarding/welcome.test.mjs +++ b/desktop/src/features/onboarding/welcome.test.mjs @@ -332,6 +332,31 @@ test("ensureStarterChannels resumes when one starter channel is missing", async assert.equal(ensureCalls, 1); }); +test("ensureStarterChannels succeeds without a full starter set (customized community)", async () => { + // An established community renamed/privatized its starter channels: the + // ensure command returns whatever this member can see, and a partial (or + // empty) starter match must not throw — joiner onboarding is best-effort. + const renamedGeneral = makeChannel({ + id: "general-channel", + name: "town-square", + visibility: "open", + }); + let ensureCalls = 0; + + const result = await ensureStarterChannels({ + getChannels: async () => [renamedGeneral], + ensureStarterChannels: async () => { + ensureCalls += 1; + return [renamedGeneral]; + }, + }); + + assert.equal(result.generalChannel, null); + assert.equal(result.welcomeChannel, null); + assert.deepEqual(result.channels, [renamedGeneral]); + assert.equal(ensureCalls, 1); +}); + test("isWelcomeExperienceChannel matches legacy Welcome and starter welcome-everyone", () => { assert.equal(isWelcomeExperienceChannel(makeChannel()), true); assert.equal( diff --git a/desktop/src/features/onboarding/welcome.ts b/desktop/src/features/onboarding/welcome.ts index 359c8341b..2818f1d3b 100644 --- a/desktop/src/features/onboarding/welcome.ts +++ b/desktop/src/features/onboarding/welcome.ts @@ -41,8 +41,13 @@ type StarterChannelsClient = { export type StarterChannelsResult = { channels: Channel[]; - generalChannel: Channel; - welcomeChannel: Channel; + /** + * Null when the community's starter channel no longer matches the spec + * (renamed, private, archived, or forum). Starter channels are best-effort + * for joiners of established communities — a missing match is not an error. + */ + generalChannel: Channel | null; + welcomeChannel: Channel | null; }; type WelcomeChannelOptions = { @@ -86,21 +91,17 @@ function findStarterChannel(channels: Channel[], name: string) { export function findStarterChannels( channels: Channel[], -): Omit | null { - const generalChannel = findStarterChannel( - channels, - STARTER_GENERAL_CHANNEL_NAME, - ); - const welcomeChannel = findStarterChannel( - channels, - STARTER_WELCOME_CHANNEL_NAME, - ); +): Omit { + return { + generalChannel: findStarterChannel(channels, STARTER_GENERAL_CHANNEL_NAME), + welcomeChannel: findStarterChannel(channels, STARTER_WELCOME_CHANNEL_NAME), + }; +} - if (!generalChannel || !welcomeChannel) { - return null; - } - - return { generalChannel, welcomeChannel }; +function hasAllStarterChannels( + starters: Omit, +) { + return starters.generalChannel !== null && starters.welcomeChannel !== null; } function hasOnlyCurrentOrAllowedMembers( @@ -262,22 +263,22 @@ export async function ensureStarterChannels( ): Promise { const existingChannels = await client.getChannels(); const existingStarters = findStarterChannels(existingChannels); - if (existingStarters) { + if (hasAllStarterChannels(existingStarters)) { return { channels: existingChannels, ...existingStarters, }; } + // Best-effort: in an established community the starter channels may have + // been renamed, made private, archived, or converted — the ensure command + // creates whatever is genuinely missing and returns what this member can + // see. A partial (or empty) starter set is fine; it must not fail a + // joiner's onboarding. const ensuredChannels = await client.ensureStarterChannels(); - const ensuredStarters = findStarterChannels(ensuredChannels); - if (!ensuredStarters) { - throw new Error("Starter channels were not available after setup"); - } - return { channels: ensuredChannels, - ...ensuredStarters, + ...findStarterChannels(ensuredChannels), }; }