Skip to content

Commit 8120c11

Browse files
JSKittyclaude
andcommitted
fix(concord): open a channel we can actually read
The channel summary carried only id and name, so the UI could not tell a readable channel from a locked one and simply took the first. Landing in a private channel we hold no key for renders empty and refuses every send, which reads as the community being broken rather than as one channel being locked. Summaries now carry `private` + `readable`, and navigation prefers a readable channel — falling back to the first when nothing is readable, so a community can never become unopenable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 8910c3d commit 8120c11

2 files changed

Lines changed: 21 additions & 4 deletions

File tree

src-tauri/src/commands/community.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ pub struct CommunitySummary {
9898
pub struct ChannelSummary {
9999
pub channel_id: String,
100100
pub name: String,
101+
/// CORD-03 private channel. Always false for v1, which has none.
102+
pub private: bool,
103+
/// Whether we hold this channel's key. A private channel we are not (yet)
104+
/// granted reads false — the UI must not default to one, since it renders
105+
/// empty and refuses every send.
106+
pub readable: bool,
101107
}
102108

103109
fn summarize(community: &vector_core::community::Community) -> CommunitySummary {
@@ -119,7 +125,7 @@ fn summarize(community: &vector_core::community::Community) -> CommunitySummary
119125
channels: community
120126
.channels
121127
.iter()
122-
.map(|c| ChannelSummary { channel_id: c.id.to_hex(), name: c.name.clone() })
128+
.map(|c| ChannelSummary { channel_id: c.id.to_hex(), name: c.name.clone(), private: false, readable: true })
123129
.collect(),
124130
owner_npub,
125131
dissolved: community.dissolved,
@@ -147,6 +153,8 @@ fn summarize_v2(c: &vector_core::community::v2::community::CommunityV2) -> Commu
147153
.map(|ch| ChannelSummary {
148154
channel_id: vector_core::simd::hex::bytes_to_hex_32(&ch.id.0),
149155
name: ch.name.clone(),
156+
private: ch.private,
157+
readable: !ch.private || ch.key.is_some(),
150158
})
151159
.collect(),
152160
owner_npub: owner.and_then(|pk| pk.to_bech32().ok()),

src/main.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,6 +2090,8 @@ async function surfaceCommunitySummary(summary) {
20902090
if (!summary) return null;
20912091
let firstChannel = null;
20922092
let firstSync = null;
2093+
let fallbackChannel = null;
2094+
let fallbackSync = null;
20932095
for (const ch of summary.channels || []) {
20942096
const chat = getOrCreateChat(ch.channel_id, 'Community');
20952097
chat.metadata = chat.metadata || {};
@@ -2118,17 +2120,24 @@ async function surfaceCommunitySummary(summary) {
21182120
// The page-1 sync pulls existing history (e.g. the owner's welcome message) so the
21192121
// channel isn't empty on open. Backend anti-stampede dedups a later open.
21202122
const p = invoke('sync_community_channel', { channelId: ch.channel_id, beforeMs: null }).catch(() => {});
2121-
if (!firstChannel) { firstChannel = ch.channel_id; firstSync = p; }
2123+
// Navigate to a channel we can actually READ. A private channel we hold no
2124+
// key for renders empty and refuses every send, so landing there reads as
2125+
// the community being broken. Falls back to the first channel when nothing
2126+
// is readable, so a community never becomes unopenable.
2127+
if (ch.readable !== false && !firstChannel) { firstChannel = ch.channel_id; firstSync = p; }
2128+
if (!fallbackChannel) { fallbackChannel = ch.channel_id; fallbackSync = p; }
21222129
}
21232130
loadCommunityRoles(summary.community_id);
21242131
resolveCommunityAvatars();
21252132
renderChatlist();
21262133
// If a warmed preload was promoted on Accept, the chat is ALREADY populated (its messages were
21272134
// emitted by the backend), so open immediately — the first sync trues it up in the background.
21282135
// Only await the sync when NOT preloaded (a cold join would otherwise open to an empty chat).
2129-
if (firstSync && !summary.preloaded) await firstSync;
2136+
const openChannel = firstChannel || fallbackChannel;
2137+
const openSync = firstChannel ? firstSync : fallbackSync;
2138+
if (openSync && !summary.preloaded) await openSync;
21302139
renderChatlist();
2131-
return firstChannel;
2140+
return openChannel;
21322141
}
21332142

21342143
/**

0 commit comments

Comments
 (0)