feat: add relay group creation #62
@@ -2,19 +2,21 @@ use anyhow::Error;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, App, AppContext, Context, Div, Entity, InteractiveElement as _, IntoElement,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement as _, Styled, Subscription,
|
||||
Task, WeakEntity, Window, div, px,
|
||||
ParentElement, PathPromptOptions, Render, SharedString, StatefulInteractiveElement as _,
|
||||
Styled, Subscription, Task, WeakEntity, Window, div, px,
|
||||
};
|
||||
use nip29::{GroupCreateOptions, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use state::NostrRegistry;
|
||||
use settings::AppSettings;
|
||||
use state::{NostrRegistry, upload};
|
||||
use theme::{ActiveTheme, TextStyle, Typeset as _, control_radius};
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
|
||||
use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
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 PRIVATE_DESC: &str = "Only members can read group messages";
|
||||
@@ -47,6 +49,10 @@ struct NewGroup {
|
||||
selected_relay: Option<RelayUrl>,
|
||||
/// Whether the optional custom group id input is shown.
|
||||
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,
|
||||
closed: bool,
|
||||
restricted: bool,
|
||||
@@ -126,6 +132,8 @@ impl NewGroup {
|
||||
relays,
|
||||
selected_relay,
|
||||
show_custom_id: false,
|
||||
picture: None,
|
||||
uploading: false,
|
||||
private: false,
|
||||
closed: false,
|
||||
restricted: false,
|
||||
@@ -206,7 +214,7 @@ impl NewGroup {
|
||||
}
|
||||
|
||||
fn create(&mut self, cx: &mut Context<Self>) {
|
||||
if self.creating {
|
||||
if self.creating || self.uploading {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,6 +252,7 @@ impl NewGroup {
|
||||
let options = GroupCreateOptions {
|
||||
name,
|
||||
about: (!about.is_empty()).then_some(about),
|
||||
picture: self.picture.as_ref().map(|picture| picture.to_string()),
|
||||
private: self.private,
|
||||
closed: self.closed,
|
||||
restricted: self.restricted,
|
||||
@@ -282,6 +291,50 @@ impl NewGroup {
|
||||
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>) {
|
||||
self.error = Some(message.into());
|
||||
cx.notify();
|
||||
@@ -450,13 +503,24 @@ impl Render for NewGroup {
|
||||
.selected_relay
|
||||
.as_ref()
|
||||
.map(relay_label)
|
||||
.unwrap_or_else(|| "Custom relay...".into());
|
||||
.unwrap_or_else(|| "Custom relay…".into());
|
||||
|
||||
let web_url = self
|
||||
.effective_relay(cx)
|
||||
.ok()
|
||||
.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()
|
||||
.gap_3()
|
||||
.child(
|
||||
@@ -467,6 +531,27 @@ impl Render for NewGroup {
|
||||
"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))
|
||||
.when(self.show_custom_id, |this| {
|
||||
this.child(
|
||||
@@ -520,7 +605,7 @@ impl Render for NewGroup {
|
||||
.primary()
|
||||
.font_semibold()
|
||||
.loading(self.creating)
|
||||
.disabled(self.creating)
|
||||
.disabled(self.creating || self.uploading)
|
||||
.on_click(cx.listener(|this, _event, _window, cx| this.create(cx))),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user