feat: add relay group creation #62

Merged
reya merged 4 commits from group-creation into master 2026-10-10 14:11:19 +00:00
Showing only changes of commit ea95756637 - Show all commits
+92 -7
View File
@@ -2,19 +2,21 @@ use anyhow::Error;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
Anchor, App, AppContext, Context, Div, Entity, InteractiveElement as _, IntoElement, Anchor, App, AppContext, Context, Div, Entity, InteractiveElement as _, IntoElement,
ParentElement, Render, SharedString, StatefulInteractiveElement as _, Styled, Subscription, ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
Task, WeakEntity, Window, div, px, Styled, Subscription, Task, WeakEntity, Window, div, px,
}; };
use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry}; use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::NostrRegistry; use settings::AppSettings;
use state::{NostrRegistry, upload};
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius}; use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::dock::{DockArea, DockPlacement, PanelHandle}; use ui::dock::{DockArea, DockPlacement, PanelHandle};
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState}; use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::switch::Switch; use ui::switch::Switch;
use ui::{Disableable, StyledExt, WindowExtension, h_flex, v_flex}; use ui::{Disableable, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
const SUGGESTED_RELAY: &str = "wss://relay.ditto.pub"; const SUGGESTED_RELAY: &str = "wss://relay.ditto.pub";
const PRIVATE_DESC: &str = "Only members can read group messages"; const PRIVATE_DESC: &str = "Only members can read group messages";
@@ -47,6 +49,10 @@ struct NewGroup {
selected_relay: Option<RelayUrl>, selected_relay: Option<RelayUrl>,
/// Whether the optional custom group id input is shown. /// Whether the optional custom group id input is shown.
show_custom_id: bool, show_custom_id: bool,
/// The uploaded group picture, ready to be published with the metadata.
picture: Option<SharedString>,
/// Whether a picture upload is in flight.
uploading: bool,
private: bool, private: bool,
closed: bool, closed: bool,
restricted: bool, restricted: bool,
@@ -126,6 +132,8 @@ impl NewGroup {
relays, relays,
selected_relay, selected_relay,
show_custom_id: false, show_custom_id: false,
picture: None,
uploading: false,
private: false, private: false,
closed: false, closed: false,
restricted: false, restricted: false,
@@ -206,7 +214,7 @@ impl NewGroup {
} }
fn create(&mut self, cx: &mut Context<Self>) { fn create(&mut self, cx: &mut Context<Self>) {
if self.creating { if self.creating || self.uploading {
return; return;
} }
@@ -244,6 +252,7 @@ impl NewGroup {
let options = GroupCreateOptions { let options = GroupCreateOptions {
name, name,
about: (!about.is_empty()).then_some(about), about: (!about.is_empty()).then_some(about),
picture: self.picture.as_ref().map(|picture| picture.to_string()),
private: self.private, private: self.private,
closed: self.closed, closed: self.closed,
restricted: self.restricted, restricted: self.restricted,
@@ -282,6 +291,50 @@ impl NewGroup {
cx.notify(); cx.notify();
} }
fn set_uploading(&mut self, uploading: bool, cx: &mut Context<Self>) {
self.uploading = uploading;
cx.notify();
}
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let server = AppSettings::get_file_server(cx);
let path = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: false,
multiple: false,
prompt: None,
});
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
// Selecting no file means the prompt was cancelled
let Some(path) = path.await??.and_then(|mut paths| paths.pop()) else {
return Ok(());
};
this.update(cx, |this, cx| {
this.set_uploading(true, cx);
})?;
match upload(server, path, cx).await {
Ok(url) => {
this.update(cx, |this, cx| {
this.picture = Some(url.to_string().into());
this.set_uploading(false, cx);
})?;
}
Err(error) => {
this.update(cx, |this, cx| {
this.set_uploading(false, cx);
this.set_error(friendly_error(&error.to_string()), cx);
})?;
}
}
Ok(())
}));
}
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) { fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
self.error = Some(message.into()); self.error = Some(message.into());
cx.notify(); cx.notify();
@@ -450,13 +503,24 @@ impl Render for NewGroup {
.selected_relay .selected_relay
.as_ref() .as_ref()
.map(relay_label) .map(relay_label)
.unwrap_or_else(|| "Custom relay...".into()); .unwrap_or_else(|| "Custom relay…".into());
let web_url = self let web_url = self
.effective_relay(cx) .effective_relay(cx)
.ok() .ok()
.map(|relay| relay_web_url(&relay)); .map(|relay| relay_web_url(&relay));
// The avatar fallback is seeded with the id, falling back to the name.
let id = self.id_input.read(cx).value().trim().to_lowercase();
let name = self.name_input.read(cx).value().trim().to_owned();
let seed: String = if !id.is_empty() {
id
} else if !name.is_empty() {
name
} else {
"group".to_owned()
};
v_flex() v_flex()
.gap_3() .gap_3()
.child( .child(
@@ -467,6 +531,27 @@ impl Render for NewGroup {
"Give your new group a name and description. You can always change these later.", "Give your new group a name and description. You can always change these later.",
), ),
) )
.child(
h_flex()
.items_center()
.gap_3()
.child(Avatar::new(self.picture.clone()).seed(seed).large())
.child(
Button::new("upload-picture")
.label(if self.picture.is_some() {
"Change picture"
} else {
"Add a picture"
})
.xsmall()
.ghost()
.disabled(self.uploading)
.loading(self.uploading)
.on_click(cx.listener(|this, _event, window, cx| {
this.upload(window, cx);
})),
),
)
.child(field("Group Name", Input::new(&self.name_input), cx)) .child(field("Group Name", Input::new(&self.name_input), cx))
.when(self.show_custom_id, |this| { .when(self.show_custom_id, |this| {
this.child( this.child(
@@ -520,7 +605,7 @@ impl Render for NewGroup {
.primary() .primary()
.font_semibold() .font_semibold()
.loading(self.creating) .loading(self.creating)
.disabled(self.creating) .disabled(self.creating || self.uploading)
.on_click(cx.listener(|this, _event, _window, cx| this.create(cx))), .on_click(cx.listener(|this, _event, _window, cx| this.create(cx))),
), ),
) )