Files
coop/docs/group-creation-plan.md
T

6.4 KiB

Group creation flow (nostrord parity)

Plan to bring the group creation flow in line with nostrord (https://github.com/nostrord/nostrord), a NIP-29 client. Analysis of both implementations, then a phased plan.

Current state (coop)

  • Dialog (crates/workspace/src/dialogs/new_group.rs): three plain text inputs (Name, Group id, Relay). Slug is generated from the name when the id is blank. Metadata is GroupMetadata::default() with only the name set. Errors surface as toast notifications only.
  • Protocol (crates/nip29/src/protocol.rs): GroupId::create_group sends kind 9007 with the full metadata in one event. No confirmed-group-id handling and no OK-retry on metadata.
  • Registry (GroupsRegistry::create in crates/nip29/src/lib.rs): ensures and subscribes the group, upserts the list entry, publishes kind 10009, and on publish failure forgets the group and emits GroupsEvent::Error.

Reference (nostrord)

UI (CreateGroupModal.kt)

  • Title "Create a Group" ("Create Channel" for subgroups).
  • Avatar upload row -> picture.
  • Name (required, placeholder #example).
  • Group ID optional, hidden behind a "Set a custom ID" disclosure; filtered to lowercase a-z0-9-_; hint "Leave empty for a random ID. The relay may override your choice."
  • Relay dropdown: user's relays + current relay + suggested NIP-29 relays + "Custom relay..." revealing a URL field. Locked to the parent's relay for subgroups.
  • Description, multi-line (3-5 rows) -> about.
  • ACCESS SETTINGS: switch rows with NIP-29 copy — Private (only members can read), Closed (join requests ignored), Restricted (only members can post), Hidden (not discoverable).
  • YOUR LIST: "Private on my list" — keeps the membership in the NIP-44 encrypted section of kind 10009.
  • Errors: friendly mapping (blocked: -> creation must happen on the relay's website, auth-required -> authorization required, timeouts, ...) plus an "Open relay website ->" link (wss -> https).
  • Submit: spinner while in flight, cancelable; validation (name required, valid relay URL).

Protocol (GroupManager.createGroup)

Two-step flow:

  1. Group ID: custom (trimmed, lowercased) or a random readable id (ReadableId.generate()).
  2. Kind 9007 (create-group) with only the h tag -> await relay OK -> await the relay's kind 39000 to learn the confirmed group id (relays may override the suggested id).
  3. Kind 9002 (edit-metadata) with h + name + access flags (presence-only tags) + about + picture + parent -> await OK (7s timeout, one retry after 1s to cover the admin-grant race). If still rejected, keep the group and warn that the relay did not accept its name.
  4. Bookkeeping: persist membership, discard the self-invite race, set the private-list flag before publishing kind 10009, publish the list, request group messages and metadata.

Existing coop infrastructure to reuse

  • Group::publish already awaits the relay OK and surfaces the rejection reason (crates/nip29/src/group.rs).
  • GroupId::edit_metadata / Group::edit_metadata for kind 9002 (crates/nip29/src/protocol.rs, group.rs).
  • ui::Switch (label + description), PopupMenu dropdown, TextareaState, Avatar (crates/ui).
  • Blossom upload via state::upload with AppSettings::get_file_server.
  • NIP-65 relay-list extraction, as in dialogs/relays.rs (GossipRelays::load).
  • Group panel open flow used by the Browse panel (crates/workspace/src/panels/browse.rs open_panel).

Plan

Phase 1 — Protocol layer (crates/nip29)

  • Adopt the two-step creation in protocol.rs: GroupId::create_group emits kind 9007 with only the h tag; metadata moves to the existing edit_metadata (kind 9002) builder.
  • Add GroupCreateOptions (name, about, picture, private/closed/restricted/ hidden, optional custom id, optional parent) passed to GroupsRegistry::create.
  • Rework GroupsRegistry::create:
    • ID: custom (trim/lowercase/validate) or a random readable id (small port of nostrord's syllable-based ReadableId).
    • ensure() -> publish 9007 via Group::publish (relay OK) -> wait briefly (with timeout) for the relay's kind 39000 to confirm the group id (the group is already subscribed via ensure).
    • Publish 9002 with the full metadata; retry once after 1s; on persistent failure keep the group and emit a warning event (nostrord parity).
    • On 9007 failure: forget() + emit GroupsEvent::Error with the relay's reason.
    • On success: upsert entry + publish_list + emit a new GroupsEvent::Created(GroupKey) so the UI can navigate.

Phase 2 — New dialog (crates/workspace/src/dialogs/new_group.rs)

Rebuild the modal to nostrord's layout and copy:

  • Header + subtitle; name field; "Set a custom ID" disclosure with filtered input.
  • Relay dropdown: user's NIP-65 relays (loaded like GossipRelays::load) + suggested group relay + "Custom relay..." revealing a URL field with validation.
  • Description via TextareaState (3-5 lines).
  • ACCESS SETTINGS: four ui::Switch rows with nostrord's NIP-29 label/description copy.
  • In-flight state on the Create button (spinner, disabled), inline error row with the friendly error mapping + "Open relay website ->" link (wss -> https).
  • Subscribe to GroupsEvent::Created / GroupsEvent::Error while open -> close the dialog and open the new group panel (same flow as browse.rs::open_panel).

Phase 3 — Avatar upload

  • Picture row using ui::Avatar preview + file picker -> state::upload (existing blossom upload), wiring upload errors into the dialog's error row.

Phase 4 — Optional extensions

  • "Private on my list": requires NIP-44 encrypted-section support in GroupList (kind 10009 parse/build/load) — meaningful extra scope; candidate for a follow-up.
  • "Create Channel" subgroup mode: protocol support (parent tag) already exists; needs a trigger point in the group details/subgroups UI and relay-locking in the dialog.

Validation

  • cargo check / cargo clippy after each phase.
  • Manual run of the dialog against a NIP-29 relay.
  • Unit tests for the readable-id and input-filter helpers.

Open questions

  1. Include "Private on my list" now, or defer the encrypted kind 10009 work?
  2. Blank id behavior: nostrord generates a random readable id, coop currently slugs the name. Match nostrord?
  3. Avatar upload in scope for the first pass?
  4. Subgroup / "Create Channel" mode now, or group creation only?