From ea95756637820949afe2fef62519c6924c795043 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sat, 10 Oct 2026 09:50:56 +0700 Subject: [PATCH] add group picture upload to new group dialog --- crates/workspace/src/dialogs/new_group.rs | 99 +++++++++++++++++++++-- 1 file changed, 92 insertions(+), 7 deletions(-) diff --git a/crates/workspace/src/dialogs/new_group.rs b/crates/workspace/src/dialogs/new_group.rs index bd2a856d..4b8703c8 100644 --- a/crates/workspace/src/dialogs/new_group.rs +++ b/crates/workspace/src/dialogs/new_group.rs @@ -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, /// 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, + /// 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) { - 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.uploading = uploading; + cx.notify(); + } + + fn upload(&mut self, window: &mut Window, cx: &mut Context) { + 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, cx: &mut Context) { 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))), ), )