add community creation flow
This commit is contained in:
Generated
+1
@@ -9343,6 +9343,7 @@ dependencies = [
|
|||||||
"chat",
|
"chat",
|
||||||
"chat_ui",
|
"chat_ui",
|
||||||
"common",
|
"common",
|
||||||
|
"community",
|
||||||
"device",
|
"device",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"instant",
|
"instant",
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ impl Community {
|
|||||||
&self.state
|
&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 {
|
pub fn control(&self) -> &ControlFold {
|
||||||
&self.control
|
&self.control
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::collections::HashMap;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use concord::CommunityId;
|
use concord::CommunityId;
|
||||||
use concord::cord01::KIND_WRAP;
|
use concord::cord01::KIND_WRAP;
|
||||||
use concord::cord02::CommunityMetadata;
|
pub use concord::cord02::CommunityMetadata;
|
||||||
use concord::store::CommunityState;
|
use concord::store::CommunityState;
|
||||||
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, Subscription, Task};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -204,7 +204,9 @@ impl CommunityRegistry {
|
|||||||
self.observers
|
self.observers
|
||||||
.push(cx.observe(&community, |this, _community, cx| {
|
.push(cx.observe(&community, |this, _community, cx| {
|
||||||
this.sync_subscriptions(cx);
|
this.sync_subscriptions(cx);
|
||||||
|
cx.notify();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
self.index.insert(id, community.clone());
|
self.index.insert(id, community.clone());
|
||||||
self.communities.push(community);
|
self.communities.push(community);
|
||||||
}
|
}
|
||||||
@@ -230,9 +232,10 @@ impl CommunityRegistry {
|
|||||||
/// Re-subscribe every community whose held planes moved.
|
/// Re-subscribe every community whose held planes moved.
|
||||||
fn sync_subscriptions(&mut self, cx: &mut Context<Self>) {
|
fn sync_subscriptions(&mut self, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
for community in self.communities.clone() {
|
for community in self.communities.clone() {
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
|
||||||
let (id, key, state) = {
|
let (id, key, state) = {
|
||||||
let community = community.read(cx);
|
let community = community.read(cx);
|
||||||
(
|
(
|
||||||
@@ -257,9 +260,9 @@ impl CommunityRegistry {
|
|||||||
let subscription = sync::subscription_id(&id);
|
let subscription = sync::subscription_id(&id);
|
||||||
let filter = sync::subscription_filter(&planes);
|
let filter = sync::subscription_filter(&planes);
|
||||||
let relays = key.relays().to_vec();
|
let relays = key.relays().to_vec();
|
||||||
|
|
||||||
self.synced.insert(id, key);
|
self.synced.insert(id, key);
|
||||||
|
|
||||||
let client = client.clone();
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
if let Err(error) = subscribe(&client, &subscription, &relays, filter).await {
|
if let Err(error) = subscribe(&client, &subscription, &relays, filter).await {
|
||||||
this.update(cx, |_this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
@@ -325,6 +328,7 @@ async fn subscribe(
|
|||||||
relays: &[RelayUrl],
|
relays: &[RelayUrl],
|
||||||
filter: Filter,
|
filter: Filter,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
log::info!("community {id}: subscribing to {relays:?}");
|
||||||
client.unsubscribe(id).await?;
|
client.unsubscribe(id).await?;
|
||||||
|
|
||||||
for url in relays {
|
for url in relays {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ state = { path = "../state" }
|
|||||||
device = { path = "../device" }
|
device = { path = "../device" }
|
||||||
chat = { path = "../chat" }
|
chat = { path = "../chat" }
|
||||||
chat_ui = { path = "../chat_ui" }
|
chat_ui = { path = "../chat_ui" }
|
||||||
|
community = { path = "../community" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
person = { path = "../person" }
|
person = { path = "../person" }
|
||||||
auto_update = { path = "../auto_update" }
|
auto_update = { path = "../auto_update" }
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ use std::rc::Rc;
|
|||||||
use auto_update::AutoUpdater;
|
use auto_update::AutoUpdater;
|
||||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
|
use community::{CommunityEvent, CommunityMetadata, CommunityRegistry};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
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,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||||
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
||||||
};
|
};
|
||||||
@@ -20,12 +21,13 @@ use ui::avatar::Avatar;
|
|||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
|
use ui::input::{Input, InputState};
|
||||||
use ui::menu::{ContextMenu, DropdownMenu, PopupMenuItem};
|
use ui::menu::{ContextMenu, DropdownMenu, PopupMenuItem};
|
||||||
use ui::nav_item::NavItem;
|
use ui::nav_item::NavItem;
|
||||||
use ui::scroll::Scrollbar;
|
use ui::scroll::Scrollbar;
|
||||||
use ui::{
|
use ui::{
|
||||||
Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, h_flex, title_bar_drag_handlers,
|
Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex,
|
||||||
v_flex,
|
title_bar_drag_handlers, v_flex,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Command;
|
use crate::Command;
|
||||||
@@ -34,7 +36,7 @@ mod entry;
|
|||||||
mod tree;
|
mod tree;
|
||||||
|
|
||||||
pub(crate) use entry::RoomEntry;
|
pub(crate) use entry::RoomEntry;
|
||||||
use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection, dummy_communities};
|
use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection};
|
||||||
|
|
||||||
/// Sidebar.
|
/// Sidebar.
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
@@ -58,6 +60,7 @@ impl Sidebar {
|
|||||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let settings = AppSettings::global(cx).read(cx).entity().clone();
|
let settings = AppSettings::global(cx).read(cx).entity().clone();
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
|
let communities = CommunityRegistry::global(cx);
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
@@ -74,6 +77,14 @@ impl Sidebar {
|
|||||||
this.restore_state(cx);
|
this.restore_state(cx);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
cx.subscribe(&communities, |_this, _communities, event, _cx| {
|
||||||
|
if let CommunityEvent::Error(error) = event {
|
||||||
|
log::error!("community: {error}");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
@@ -145,6 +156,36 @@ impl Sidebar {
|
|||||||
self.pinned_rooms.contains(&room_id)
|
self.pinned_rooms.contains(&room_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn new_community(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
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<SidebarRow> {
|
fn tree_rows(&self, cx: &App) -> Vec<SidebarRow> {
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let chat = chat.read(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 {
|
rows.push(SidebarRow::Section {
|
||||||
section: TreeSection::Community,
|
section: TreeSection::Community,
|
||||||
count: communities.len(),
|
count: communities.len(),
|
||||||
@@ -213,9 +256,15 @@ impl Sidebar {
|
|||||||
rows.extend(
|
rows.extend(
|
||||||
communities
|
communities
|
||||||
.iter()
|
.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);
|
let messages = chat.rooms(&RoomKind::Ongoing, cx);
|
||||||
@@ -345,13 +394,28 @@ impl Sidebar {
|
|||||||
)
|
)
|
||||||
.into_any_element()
|
.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),
|
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||||
TreeRowKind::Community,
|
TreeRowKind::Hint,
|
||||||
entry.name,
|
"New community",
|
||||||
)
|
)
|
||||||
.depth(*depth)
|
.depth(*depth)
|
||||||
.avatar(entry.name)
|
.icon(IconName::Plus)
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.new_community(window, cx);
|
||||||
|
}))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
SidebarRow::Hint { text, depth } => TreeRow::new(
|
SidebarRow::Hint { text, depth } => TreeRow::new(
|
||||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use chat::Room;
|
use chat::Room;
|
||||||
|
use community::Community;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, ClickEvent, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
App, ClickEvent, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
@@ -66,7 +67,10 @@ pub enum SidebarRow {
|
|||||||
pinned: bool,
|
pinned: bool,
|
||||||
},
|
},
|
||||||
Community {
|
Community {
|
||||||
entry: &'static CommunityEntry,
|
community: Entity<Community>,
|
||||||
|
depth: u8,
|
||||||
|
},
|
||||||
|
NewCommunity {
|
||||||
depth: u8,
|
depth: u8,
|
||||||
},
|
},
|
||||||
Hint {
|
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum TreeRowKind {
|
pub enum TreeRowKind {
|
||||||
Section,
|
Section,
|
||||||
|
|||||||
@@ -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
|
arity, `set_pin_list`'s missing `.await`, the GPUI `init` signature and
|
||||||
registry names, and the "Not wired up yet" registry bullet.
|
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<Community>`,
|
||||||
|
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)
|
## 3. Retained-by-decision surface (reference only)
|
||||||
|
|||||||
@@ -542,9 +542,10 @@ client.subscribe(filter).with_id(sub_id).await?;
|
|||||||
`crates/community`.** `concord` has no subscriptions, no `init`, and no
|
`crates/community`.** `concord` has no subscriptions, no `init`, and no
|
||||||
`Entity<Community>`; `community::CommunityRegistry` owns one `Entity<Community>`
|
`Entity<Community>`; `community::CommunityRegistry` owns one `Entity<Community>`
|
||||||
per state document, subscribes when a community's plane set changes, and
|
per state document, subscribes when a community's plane set changes, and
|
||||||
re-folds on an inbound wrap. Nothing observes `CommunityEvent` yet, and
|
re-folds on an inbound wrap. The sidebar observes the registry, logs
|
||||||
`CommunityRegistry::create` persists the genesis locally without publishing it
|
`CommunityEvent::Error` through `log::error!`, and its "New community" row opens
|
||||||
to the metadata's relays.
|
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`,
|
- **Account-key writers take any signer, not `&Keys`.** `genesis`,
|
||||||
`ControlWriter`, the guestbook and chat `seal_rumor`s, the `list` builders, and
|
`ControlWriter`, the guestbook and chat `seal_rumor`s, the `list` builders, and
|
||||||
the `cord05` invite writers (`build_direct_invite` / `unwrap_direct_invite`,
|
the `cord05` invite writers (`build_direct_invite` / `unwrap_direct_invite`,
|
||||||
|
|||||||
Reference in New Issue
Block a user