diff --git a/Cargo.lock b/Cargo.lock index eb662aad..f75466b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9343,6 +9343,7 @@ dependencies = [ "chat", "chat_ui", "common", + "community", "device", "gpui-pre", "instant", diff --git a/crates/community/src/community.rs b/crates/community/src/community.rs index 4dc6628d..4750f83b 100644 --- a/crates/community/src/community.rs +++ b/crates/community/src/community.rs @@ -70,6 +70,13 @@ impl Community { &self.state } + pub fn name(&self) -> String { + match &self.control.community { + Some(metadata) => metadata.name.clone(), + None => self.state.id.to_hex(), + } + } + pub fn control(&self) -> &ControlFold { &self.control } diff --git a/crates/community/src/lib.rs b/crates/community/src/lib.rs index 17068a31..483e194c 100644 --- a/crates/community/src/lib.rs +++ b/crates/community/src/lib.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use anyhow::Result; use concord::CommunityId; use concord::cord01::KIND_WRAP; -use concord::cord02::CommunityMetadata; +pub use concord::cord02::CommunityMetadata; use concord::store::CommunityState; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task}; use nostr_sdk::prelude::*; @@ -204,7 +204,9 @@ impl CommunityRegistry { self.observers .push(cx.observe(&community, |this, _community, cx| { this.sync_subscriptions(cx); + cx.notify(); })); + self.index.insert(id, community.clone()); self.communities.push(community); } @@ -230,9 +232,10 @@ impl CommunityRegistry { /// Re-subscribe every community whose held planes moved. fn sync_subscriptions(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); for community in self.communities.clone() { + let client = nostr.read(cx).client(); + let (id, key, state) = { let community = community.read(cx); ( @@ -257,9 +260,9 @@ impl CommunityRegistry { let subscription = sync::subscription_id(&id); let filter = sync::subscription_filter(&planes); let relays = key.relays().to_vec(); + self.synced.insert(id, key); - let client = client.clone(); self.tasks.push(cx.spawn(async move |this, cx| { if let Err(error) = subscribe(&client, &subscription, &relays, filter).await { this.update(cx, |_this, cx| { @@ -325,6 +328,7 @@ async fn subscribe( relays: &[RelayUrl], filter: Filter, ) -> Result<()> { + log::info!("community {id}: subscribing to {relays:?}"); client.unsubscribe(id).await?; for url in relays { diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 6a75fb25..66aa4d90 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -12,6 +12,7 @@ state = { path = "../state" } device = { path = "../device" } chat = { path = "../chat" } chat_ui = { path = "../chat_ui" } +community = { path = "../community" } settings = { path = "../settings" } person = { path = "../person" } auto_update = { path = "../auto_update" } diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 01069320..37ad3216 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -5,9 +5,10 @@ use std::rc::Rc; use auto_update::AutoUpdater; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::TimestampExt; +use community::{CommunityEvent, CommunityMetadata, CommunityRegistry}; use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, + AnyElement, App, AppContext, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, UniformListScrollHandle, Window, div, px, retain_all, uniform_list, }; @@ -20,12 +21,13 @@ use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; use ui::indicator::Indicator; +use ui::input::{Input, InputState}; use ui::menu::{ContextMenu, DropdownMenu, PopupMenuItem}; use ui::nav_item::NavItem; use ui::scroll::Scrollbar; use ui::{ - Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, h_flex, title_bar_drag_handlers, - v_flex, + Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex, + title_bar_drag_handlers, v_flex, }; use crate::Command; @@ -34,7 +36,7 @@ mod entry; mod tree; pub(crate) use entry::RoomEntry; -use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection, dummy_communities}; +use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection}; /// Sidebar. pub struct Sidebar { @@ -58,6 +60,7 @@ impl Sidebar { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let settings = AppSettings::global(cx).read(cx).entity().clone(); let chat = ChatRegistry::global(cx); + let communities = CommunityRegistry::global(cx); let mut subscriptions = smallvec![]; @@ -74,6 +77,14 @@ impl Sidebar { this.restore_state(cx); })); + subscriptions.push( + cx.subscribe(&communities, |_this, _communities, event, _cx| { + if let CommunityEvent::Error(error) = event { + log::error!("community: {error}"); + } + }), + ); + Self { focus_handle: cx.focus_handle(), scroll_handle: UniformListScrollHandle::new(), @@ -145,6 +156,36 @@ impl Sidebar { self.pinned_rooms.contains(&room_id) } + fn new_community(&mut self, window: &mut Window, cx: &mut Context) { + let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Community name")); + + window.open_modal(cx, move |this, _window, _cx| { + let name_input = name_input.clone(); + + this.width(px(380.)) + .confirm() + .title("New community") + .child(Input::new(&name_input)) + .on_ok(move |_event, _window, cx| { + let name = name_input.read(cx).value().trim().to_owned(); + + if name.is_empty() { + return false; + } + + let metadata = CommunityMetadata { + name, + ..CommunityMetadata::default() + }; + + CommunityRegistry::global(cx) + .update(cx, |registry, cx| registry.create(metadata, cx)); + + true + }) + }); + } + fn tree_rows(&self, cx: &App) -> Vec { let chat = ChatRegistry::global(cx); let chat = chat.read(cx); @@ -197,7 +238,9 @@ impl Sidebar { } } - let communities = dummy_communities(); + let registry = CommunityRegistry::global(cx); + let communities = registry.read(cx).communities(); + rows.push(SidebarRow::Section { section: TreeSection::Community, count: communities.len(), @@ -213,9 +256,15 @@ impl Sidebar { rows.extend( communities .iter() - .map(|entry| SidebarRow::Community { entry, depth: 1 }), + .cloned() + .map(|community| SidebarRow::Community { + community, + depth: 1, + }), ); } + + rows.push(SidebarRow::NewCommunity { depth: 1 }); } let messages = chat.rooms(&RoomKind::Ongoing, cx); @@ -345,13 +394,28 @@ impl Sidebar { ) .into_any_element() } - SidebarRow::Community { entry, depth } => TreeRow::new( + SidebarRow::Community { community, depth } => { + let community = community.read(cx); + + TreeRow::new( + ElementId::NamedInteger("tree-row".into(), index as u64), + TreeRowKind::Community, + community.name(), + ) + .depth(*depth) + .avatar(community.id().to_hex()) + .into_any_element() + } + SidebarRow::NewCommunity { depth } => TreeRow::new( ElementId::NamedInteger("tree-row".into(), index as u64), - TreeRowKind::Community, - entry.name, + TreeRowKind::Hint, + "New community", ) .depth(*depth) - .avatar(entry.name) + .icon(IconName::Plus) + .on_click(cx.listener(|this, _event, window, cx| { + this.new_community(window, cx); + })) .into_any_element(), SidebarRow::Hint { text, depth } => TreeRow::new( ElementId::NamedInteger("tree-row".into(), index as u64), diff --git a/crates/workspace/src/sidebar/tree.rs b/crates/workspace/src/sidebar/tree.rs index 04a9d11a..6a891265 100644 --- a/crates/workspace/src/sidebar/tree.rs +++ b/crates/workspace/src/sidebar/tree.rs @@ -1,6 +1,7 @@ use std::rc::Rc; use chat::Room; +use community::Community; use gpui::prelude::FluentBuilder; use gpui::{ App, ClickEvent, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce, @@ -66,7 +67,10 @@ pub enum SidebarRow { pinned: bool, }, Community { - entry: &'static CommunityEntry, + community: Entity, + depth: u8, + }, + NewCommunity { depth: u8, }, Hint { @@ -75,22 +79,6 @@ pub enum SidebarRow { }, } -pub struct CommunityEntry { - pub name: &'static str, -} - -pub fn dummy_communities() -> &'static [CommunityEntry] { - // TODO(concord): replace with CommunityRegistry communities, see docs/concord-usage.md. - &[ - CommunityEntry { - name: "Coop Contributors", - }, - CommunityEntry { - name: "Nostr Design", - }, - ] -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TreeRowKind { Section, diff --git a/docs/concord-simplification-plan.md b/docs/concord-simplification-plan.md index f38ec941..6d056bd2 100644 --- a/docs/concord-simplification-plan.md +++ b/docs/concord-simplification-plan.md @@ -282,6 +282,33 @@ the base64 record). `cargo clippy -p concord --all-targets` and arity, `set_pin_list`'s missing `.await`, the GPUI `init` signature and registry names, and the "Not wired up yet" registry bullet. +### Phase 5 — sidebar calls `create` — DONE + +The last blocker was that nothing invoked `CommunityRegistry::create`; the +running app logged `community load: 0 state document(s) found` and `subscribe` +never ran. The sidebar now: + +1. Renders `CommunityRegistry::communities()` instead of the hardcoded + `dummy_communities()`. `SidebarRow::Community` carries an `Entity`, + labelled with `Community::name()` (control-fold metadata, falling back to the + community id until the first fold). +2. Adds a "New community" row to the Community section that opens a name prompt + and calls `CommunityRegistry::create` with default metadata. Relays stay empty, + so the subscription resolves through `ReqTarget::auto` against the pool's + relays rather than a manual target that `add_relay` might not have connected. +3. Observes the registry, so a `track` or fold re-render reaches the list, and + subscribes to `CommunityEvent::Error`, which is now logged + (`log::error!("community: {error}")`) instead of vanishing. A `cx.notify()` in + the registry's per-community observer propagates the fold that fills in the + name. + +Validation: `cargo check -p workspace -p community --all-targets`, +`cargo test -p community` (1 passed), `cargo clippy -p workspace -p community +--all-targets`, and `cargo fmt -p workspace -p community --check` are clean. + +Still local-only: the genesis is persisted but not published to relays, so a +second account cannot discover the community yet. + --- ## 3. Retained-by-decision surface (reference only) diff --git a/docs/concord-usage.md b/docs/concord-usage.md index 7010dc65..95790af5 100644 --- a/docs/concord-usage.md +++ b/docs/concord-usage.md @@ -542,9 +542,10 @@ client.subscribe(filter).with_id(sub_id).await?; `crates/community`.** `concord` has no subscriptions, no `init`, and no `Entity`; `community::CommunityRegistry` owns one `Entity` per state document, subscribes when a community's plane set changes, and - re-folds on an inbound wrap. Nothing observes `CommunityEvent` yet, and - `CommunityRegistry::create` persists the genesis locally without publishing it - to the metadata's relays. + re-folds on an inbound wrap. The sidebar observes the registry, logs + `CommunityEvent::Error` through `log::error!`, and its "New community" row opens + a name prompt that calls `CommunityRegistry::create`. `create` still persists + the genesis locally without publishing it to the metadata's relays. - **Account-key writers take any signer, not `&Keys`.** `genesis`, `ControlWriter`, the guestbook and chat `seal_rumor`s, the `list` builders, and the `cord05` invite writers (`build_direct_invite` / `unwrap_direct_invite`,