chore: move profile panel into the profile dialog
This commit is contained in:
@@ -9,8 +9,6 @@ edition = "2024"
|
|||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# GPUI. The `gpui-pre` family is upstream zed's gpui republished unchanged, so these
|
|
||||||
# aliases keep every `use gpui::..` site as it is while moving off the zed git pin.
|
|
||||||
gpui = { package = "gpui-pre", version = "0.3.5" }
|
gpui = { package = "gpui-pre", version = "0.3.5" }
|
||||||
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
|
gpui_platform = { package = "gpui-pre-platform", version = "0.3.5", features = ["font-kit", "x11", "wayland"] }
|
||||||
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
|
gpui_linux = { package = "gpui-pre-linux", version = "0.3.5" }
|
||||||
@@ -21,8 +19,6 @@ gpui_util = { package = "gpui-pre-util", version = "0.3.5" }
|
|||||||
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
|
sum_tree = { package = "gpui-pre-sum-tree", version = "0.3.5" }
|
||||||
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
|
reqwest_client = { package = "gpui-pre-reqwest-client", version = "0.3.5" }
|
||||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||||
|
|
||||||
# Unstyled behavior, state, and infrastructure from GPUI Kit
|
|
||||||
gpui-base = "0.7.0"
|
gpui-base = "0.7.0"
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
|
|||||||
@@ -934,7 +934,7 @@ impl PopupMenu {
|
|||||||
Icon::empty()
|
Icon::empty()
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(icon.small())
|
Some(icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -990,7 +990,7 @@ impl PopupMenu {
|
|||||||
|
|
||||||
let this = MenuItemElement::new(ix, &group_name)
|
let this = MenuItemElement::new(ix, &group_name)
|
||||||
.relative()
|
.relative()
|
||||||
.text_xs()
|
.text_sm()
|
||||||
.py_0()
|
.py_0()
|
||||||
.px(INNER_PADDING)
|
.px(INNER_PADDING)
|
||||||
.rounded(radius)
|
.rounded(radius)
|
||||||
@@ -1020,7 +1020,7 @@ impl PopupMenu {
|
|||||||
h_flex().cursor_default().items_center().gap_x_1().child(
|
h_flex().cursor_default().items_center().gap_x_1().child(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.text_xs()
|
.text_sm()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.child(label.clone()),
|
.child(label.clone()),
|
||||||
@@ -1063,7 +1063,7 @@ impl PopupMenu {
|
|||||||
})
|
})
|
||||||
.disabled(*disabled)
|
.disabled(*disabled)
|
||||||
.h(item_height)
|
.h(item_height)
|
||||||
.gap_x_1p5()
|
.gap_x_2()
|
||||||
.children(Self::render_icon(
|
.children(Self::render_icon(
|
||||||
has_left_icon,
|
has_left_icon,
|
||||||
is_left_check,
|
is_left_check,
|
||||||
@@ -1084,7 +1084,7 @@ impl PopupMenu {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.gap_1p5()
|
.gap_2()
|
||||||
.child(label.clone())
|
.child(label.clone())
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::Link)
|
Icon::new(IconName::Link)
|
||||||
@@ -1110,7 +1110,7 @@ impl PopupMenu {
|
|||||||
.min_h(item_height)
|
.min_h(item_height)
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_x_1p5()
|
.gap_x_2()
|
||||||
.children(Self::render_icon(
|
.children(Self::render_icon(
|
||||||
has_left_icon,
|
has_left_icon,
|
||||||
false,
|
false,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub(crate) fn resize_handle_appearance() -> ResizeHandleRenderer {
|
|||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
|
.bg(cx.theme().border_variant)
|
||||||
.group_hover("handle", move |this| this.bg(color))
|
.group_hover("handle", move |this| this.bg(color))
|
||||||
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
.when(axis.is_horizontal(), |this| this.h_full().w(HANDLE_SIZE))
|
||||||
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE))
|
.when(axis.is_vertical(), |this| this.w_full().h(HANDLE_SIZE))
|
||||||
|
|||||||
@@ -1,37 +1,90 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
|
use chat::{ChatRegistry, Room, RoomKind};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, ClipboardItem, Context, Div, IntoElement, ParentElement, Render, SharedString,
|
App, AppContext, ClipboardItem, Context, Div, Entity, IntoElement, ParentElement,
|
||||||
Styled, Task, Window, div, px,
|
PathPromptOptions, Render, SharedString, Styled, Task, Window, div, retain_all,
|
||||||
};
|
};
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
use person::{Person, PersonRegistry, shorten_pubkey};
|
||||||
|
use settings::AppSettings;
|
||||||
|
use state::{NostrRegistry, upload};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::dock::DockPlacement;
|
||||||
|
use ui::input::{Input, InputState, Textarea, TextareaState};
|
||||||
|
use ui::notification::Notification;
|
||||||
|
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
|
use crate::Workspace;
|
||||||
|
|
||||||
/// Open a read-only profile for `public_key`.
|
|
||||||
pub fn open(public_key: PublicKey, window: &mut Window, cx: &mut App) {
|
pub fn open(public_key: PublicKey, window: &mut Window, cx: &mut App) {
|
||||||
let profile = cx.new(|_| ProfileDialog::new(public_key));
|
let profile = cx.new(|cx| ProfileDialog::new(public_key, window, cx));
|
||||||
|
|
||||||
window.open_dialog(cx, move |this, _window, _cx| {
|
window.open_dialog(cx, move |this, _window, _cx| {
|
||||||
this.width(px(360.)).title("Profile").child(profile.clone())
|
this.title("Profile").child(profile.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProfileDialog {
|
struct ProfileDialog {
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
|
/// Whether the profile belongs to the signed-in user
|
||||||
|
is_current_user: bool,
|
||||||
|
/// Whether the metadata inputs are shown
|
||||||
|
editing: bool,
|
||||||
|
/// User's name text input
|
||||||
|
name_input: Entity<InputState>,
|
||||||
|
/// User's avatar url text input
|
||||||
|
avatar_input: Entity<InputState>,
|
||||||
|
/// User's bio multi line input
|
||||||
|
bio_input: Entity<TextareaState>,
|
||||||
|
/// User's website url text input
|
||||||
|
website_input: Entity<InputState>,
|
||||||
|
/// Uploading state
|
||||||
|
uploading: bool,
|
||||||
|
/// Copied states
|
||||||
copied: bool,
|
copied: bool,
|
||||||
|
/// Updating state
|
||||||
|
updating: bool,
|
||||||
|
/// Tasks
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileDialog {
|
impl ProfileDialog {
|
||||||
fn new(public_key: PublicKey) -> Self {
|
fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let is_current_user = nostr
|
||||||
|
.read(cx)
|
||||||
|
.current_user()
|
||||||
|
.is_some_and(|me| me == public_key);
|
||||||
|
|
||||||
|
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||||
|
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
|
||||||
|
|
||||||
|
let avatar_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/avatar.jpg"));
|
||||||
|
|
||||||
|
let bio_input = cx.new(|cx| {
|
||||||
|
TextareaState::new(window, cx)
|
||||||
|
.auto_grow(3, 8)
|
||||||
|
.placeholder("A short introduce about you.")
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
|
is_current_user,
|
||||||
|
editing: false,
|
||||||
|
name_input,
|
||||||
|
avatar_input,
|
||||||
|
bio_input,
|
||||||
|
website_input,
|
||||||
|
uploading: false,
|
||||||
copied: false,
|
copied: false,
|
||||||
|
updating: false,
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,6 +94,45 @@ impl ProfileDialog {
|
|||||||
persons.read(cx).get(&self.public_key, cx)
|
persons.read(cx).get(&self.public_key, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let metadata = self.profile(cx).metadata();
|
||||||
|
|
||||||
|
self.avatar_input.update(cx, |this, cx| {
|
||||||
|
if let Some(avatar) = metadata.picture.as_ref() {
|
||||||
|
this.set_value(avatar, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.bio_input.update(cx, |this, cx| {
|
||||||
|
if let Some(bio) = metadata.about.as_ref() {
|
||||||
|
this.set_value(bio, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.name_input.update(cx, |this, cx| {
|
||||||
|
if let Some(display_name) = metadata.display_name.as_ref() {
|
||||||
|
this.set_value(display_name, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.website_input.update(cx, |this, cx| {
|
||||||
|
if let Some(website) = metadata.website.as_ref() {
|
||||||
|
this.set_value(website, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Toggle the metadata inputs, filling them with the saved profile when shown
|
||||||
|
fn toggle_edit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editing = !self.editing;
|
||||||
|
|
||||||
|
if self.editing {
|
||||||
|
self.set_profile(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn copy(&mut self, value: String, window: &mut Window, cx: &mut Context<Self>) {
|
fn copy(&mut self, value: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let item = ClipboardItem::new_string(value);
|
let item = ClipboardItem::new_string(value);
|
||||||
cx.write_to_clipboard(item);
|
cx.write_to_clipboard(item);
|
||||||
@@ -69,63 +161,229 @@ impl ProfileDialog {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.uploading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// Get the user's configured blossom server
|
||||||
|
let server = AppSettings::get_file_server(cx);
|
||||||
|
|
||||||
|
// Ask user for file upload
|
||||||
|
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);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Upload via blossom client
|
||||||
|
match upload(server, path, cx).await {
|
||||||
|
Ok(url) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
|
this.set_value(url.to_string(), window, cx);
|
||||||
|
});
|
||||||
|
this.set_uploading(false, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
this.set_uploading(false, cx);
|
||||||
|
window.push_notification(
|
||||||
|
Notification::error(e.to_string()).autohide(false),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
||||||
|
self.updating = updating;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the metadata for the current user
|
||||||
|
fn publish(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let client = nostr.read(cx).client();
|
||||||
|
let signer = nostr.read(cx).signer();
|
||||||
|
let metadata = metadata.clone();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
// Build and sign the metadata event
|
||||||
|
let event = metadata.finalize_async(&signer).await?;
|
||||||
|
|
||||||
|
// Send event to user's relays
|
||||||
|
client.send_event(&event).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let public_key = self.public_key;
|
||||||
|
let old_metadata = persons.read(cx).get(&public_key, cx).metadata();
|
||||||
|
|
||||||
|
// Extract all new metadata fields
|
||||||
|
let avatar = self.avatar_input.read(cx).value();
|
||||||
|
let name = self.name_input.read(cx).value();
|
||||||
|
let bio = self.bio_input.read(cx).value();
|
||||||
|
let website = self.website_input.read(cx).value();
|
||||||
|
|
||||||
|
// Construct the new metadata
|
||||||
|
let mut new_metadata = old_metadata
|
||||||
|
.display_name(name.as_ref())
|
||||||
|
.name(name.as_ref())
|
||||||
|
.about(bio.as_ref());
|
||||||
|
|
||||||
|
// Verify the avatar URL before adding it
|
||||||
|
if let Ok(url) = Url::from_str(&avatar) {
|
||||||
|
new_metadata = new_metadata.picture(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the website URL before adding it
|
||||||
|
if let Ok(url) = Url::from_str(&website) {
|
||||||
|
new_metadata = new_metadata.website(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the metadata
|
||||||
|
let task = self.publish(&new_metadata, cx);
|
||||||
|
|
||||||
|
// Set the updating state
|
||||||
|
self.set_updating(true, cx);
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
this.update_in(cx, |this, window, cx| {
|
||||||
|
// Update the registry
|
||||||
|
persons.update(cx, |this, cx| {
|
||||||
|
this.insert(Person::new(public_key, new_metadata), cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set_updating(false, cx);
|
||||||
|
this.editing = false;
|
||||||
|
this.set_profile(window, cx);
|
||||||
|
|
||||||
|
window.push_notification("Profile updated successfully", cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update(|window, cx| {
|
||||||
|
window.push_notification(
|
||||||
|
Notification::error(e.to_string()).autohide(false),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open the conversation with this person, creating an ongoing one if needed
|
||||||
|
fn message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let chat = ChatRegistry::global(cx);
|
||||||
|
|
||||||
|
let Some(current_user) = nostr.read(cx).current_user() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = chat
|
||||||
|
.read(cx)
|
||||||
|
.rooms(&RoomKind::Ongoing, cx)
|
||||||
|
.into_iter()
|
||||||
|
.chain(chat.read(cx).rooms(&RoomKind::Request, cx))
|
||||||
|
.find(|room| {
|
||||||
|
let room = room.read(cx);
|
||||||
|
!room.is_group() && room.members().contains(&self.public_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
let room = match existing {
|
||||||
|
Some(room) => room,
|
||||||
|
None => chat.update(cx, |chat, cx| {
|
||||||
|
let room = cx.new(|_| {
|
||||||
|
Room::new(current_user, [self.public_key])
|
||||||
|
.organize(¤t_user)
|
||||||
|
.kind(RoomKind::Ongoing)
|
||||||
|
});
|
||||||
|
chat.track_room(&room, cx);
|
||||||
|
room
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Workspace::add_panel(
|
||||||
|
chat_ui::init(room.downgrade(), window, cx),
|
||||||
|
DockPlacement::Center,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.close_dialog(cx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ProfileDialog {
|
impl Render for ProfileDialog {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let profile = self.profile(cx);
|
let profile = self.profile(cx);
|
||||||
let metadata = profile.metadata();
|
|
||||||
|
|
||||||
// Get the public key as short string
|
|
||||||
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
|
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
|
||||||
|
let avatar_url = self.avatar_input.read(cx).value();
|
||||||
|
|
||||||
let about = metadata
|
let avatar = if self.editing && !avatar_url.is_empty() {
|
||||||
.about
|
Some(avatar_url)
|
||||||
.filter(|about| !about.trim().is_empty())
|
} else {
|
||||||
.map(SharedString::from);
|
profile.avatar()
|
||||||
|
};
|
||||||
let website = metadata
|
|
||||||
.website
|
|
||||||
.filter(|website| !website.trim().is_empty())
|
|
||||||
.map(SharedString::from);
|
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.image_cache(retain_all("profile-dialog"))
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.gap_4()
|
||||||
.h_40()
|
.h_40()
|
||||||
.w_full()
|
.w_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.child(
|
.bg(cx.theme().surface_background)
|
||||||
Avatar::new(profile.avatar())
|
.child(Avatar::new(avatar).seed(profile.avatar_seed()).large())
|
||||||
.seed(profile.avatar_seed())
|
.when(self.editing, |this| {
|
||||||
.large(),
|
this.child(
|
||||||
),
|
Button::new("upload")
|
||||||
|
.icon(IconName::PlusCircle)
|
||||||
|
.label("Add an avatar")
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.rounded()
|
||||||
|
.disabled(self.uploading)
|
||||||
|
.loading(self.uploading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.upload(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(label("Name", cx))
|
|
||||||
.child(div().text_sm().child(profile.name())),
|
|
||||||
)
|
|
||||||
.when_some(about, |this, about| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(label("About", cx))
|
|
||||||
.child(div().text_sm().child(about)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when_some(website, |this, website| {
|
|
||||||
this.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(label("Website", cx))
|
|
||||||
.child(div().text_sm().child(website)),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
v_flex().gap_1p5().child(label("Public Key", cx)).child(
|
v_flex().gap_1p5().child(label("Public Key", cx)).child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -134,9 +392,8 @@ impl Render for ProfileDialog {
|
|||||||
.justify_center()
|
.justify_center()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.bg(cx.theme().secondary_background)
|
.bg(cx.theme().surface_background)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
.child(shorten_pkey)
|
.child(shorten_pkey)
|
||||||
.child(
|
.child(
|
||||||
Button::new("copy")
|
Button::new("copy")
|
||||||
@@ -150,17 +407,131 @@ impl Render for ProfileDialog {
|
|||||||
.xsmall()
|
.xsmall()
|
||||||
.secondary()
|
.secondary()
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
this.copy(this.public_key.to_bech32().unwrap(), window, cx);
|
let pk = this.public_key.to_bech32().unwrap();
|
||||||
|
this.copy(pk, window, cx)
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.when(self.editing, |this| {
|
||||||
|
this.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(label("Name", cx))
|
||||||
|
.child(Input::new(&self.name_input).small()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(label("About", cx))
|
||||||
|
.child(Textarea::new(&self.bio_input).small()),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(label("Website", cx))
|
||||||
|
.child(Input::new(&self.website_input).small()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!self.editing, |this| {
|
||||||
|
let metadata = profile.metadata();
|
||||||
|
|
||||||
|
let name = metadata
|
||||||
|
.display_name
|
||||||
|
.filter(|name| !name.trim().is_empty())
|
||||||
|
.or(metadata.name.filter(|name| !name.trim().is_empty()))
|
||||||
|
.map(SharedString::from);
|
||||||
|
|
||||||
|
let about = metadata
|
||||||
|
.about
|
||||||
|
.filter(|about| !about.trim().is_empty())
|
||||||
|
.map(SharedString::from);
|
||||||
|
|
||||||
|
let website = metadata
|
||||||
|
.website
|
||||||
|
.filter(|website| !website.trim().is_empty())
|
||||||
|
.map(SharedString::from);
|
||||||
|
|
||||||
|
this.child(field("Name", name, "Unknown", cx))
|
||||||
|
.child(field("About", about, "No bio", cx))
|
||||||
|
.child(field("Website", website, "No website", cx))
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.when(self.is_current_user && !self.editing, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("update")
|
||||||
|
.icon(IconName::Edit)
|
||||||
|
.label("Update")
|
||||||
|
.primary()
|
||||||
|
.font_semibold()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.toggle_edit(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!self.is_current_user, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("message")
|
||||||
|
.icon(IconName::Message)
|
||||||
|
.label("Message")
|
||||||
|
.primary()
|
||||||
|
.font_semibold()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.message(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(self.editing, |this| {
|
||||||
|
this.child(
|
||||||
|
Button::new("cancel")
|
||||||
|
.label("Cancel")
|
||||||
|
.ghost()
|
||||||
|
.disabled(self.updating)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.toggle_edit(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("save")
|
||||||
|
.icon(IconName::CheckCircle)
|
||||||
|
.label("Save")
|
||||||
|
.primary()
|
||||||
|
.font_semibold()
|
||||||
|
.loading(self.updating)
|
||||||
|
.disabled(self.updating)
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.save(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn label(label: &'static str, cx: &App) -> Div {
|
fn label(label: &'static str, cx: &App) -> Div {
|
||||||
div()
|
div()
|
||||||
.text_sm()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
|
.font_semibold()
|
||||||
.child(label)
|
.child(label)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn field(
|
||||||
|
text: &'static str,
|
||||||
|
value: Option<SharedString>,
|
||||||
|
placeholder: &'static str,
|
||||||
|
cx: &App,
|
||||||
|
) -> Div {
|
||||||
|
let (content, color) = match value {
|
||||||
|
Some(value) => (value, cx.theme().text),
|
||||||
|
None => (SharedString::from(placeholder), cx.theme().text_muted),
|
||||||
|
};
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(label(text, cx))
|
||||||
|
.child(div().text_sm().text_color(color).child(content))
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,10 +22,9 @@ use ui::notification::{Notification, NotificationKind};
|
|||||||
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
use ui::{IconName, Root, Sizable, WindowExtension, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::dialogs::restore::RestoreEncryption;
|
use crate::dialogs::restore::RestoreEncryption;
|
||||||
use crate::dialogs::{join_group, new_chat, new_group, settings};
|
use crate::dialogs::{join_group, new_chat, new_group, profile, settings};
|
||||||
use crate::panels::{
|
use crate::panels::{
|
||||||
backup, browse, contact_list, greeter, inbox, messaging_relays, profile, relay_list, requests,
|
backup, browse, contact_list, greeter, inbox, messaging_relays, relay_list, requests, search,
|
||||||
search,
|
|
||||||
};
|
};
|
||||||
use crate::sidebar::Sidebar;
|
use crate::sidebar::Sidebar;
|
||||||
|
|
||||||
@@ -284,12 +283,7 @@ impl Workspace {
|
|||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).current_user() {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
self.add_panel_to_dock(
|
profile::open(public_key, window, cx);
|
||||||
profile::init(public_key, window, cx),
|
|
||||||
DockPlacement::Left,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::ShowContactList => {
|
Command::ShowContactList => {
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
use anyhow::Error;
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, svg,
|
IntoElement, ParentElement, Render, SharedString, Styled, Window, div, svg,
|
||||||
};
|
};
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock::{DockPlacement, Panel, PanelEvent};
|
use ui::dock::{Panel, PanelEvent};
|
||||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
use ui::{Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
|
||||||
|
|
||||||
use crate::panels::profile;
|
use crate::Command;
|
||||||
use crate::{Command, Workspace};
|
use crate::dialogs::profile;
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
||||||
cx.new(|cx| GreeterPanel::new(window, cx))
|
cx.new(|cx| GreeterPanel::new(window, cx))
|
||||||
@@ -19,7 +18,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<GreeterPanel> {
|
|||||||
pub struct GreeterPanel {
|
pub struct GreeterPanel {
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GreeterPanel {
|
impl GreeterPanel {
|
||||||
@@ -27,27 +25,14 @@ impl GreeterPanel {
|
|||||||
Self {
|
Self {
|
||||||
name: "Onboarding".into(),
|
name: "Onboarding".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
tasks: vec![],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
if let Some(public_key) = nostr.read(cx).current_user() {
|
if let Some(public_key) = nostr.read(cx).current_user() {
|
||||||
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
profile::open(public_key, window, cx);
|
||||||
cx.update(|window, cx| {
|
|
||||||
Workspace::add_panel(
|
|
||||||
profile::init(public_key, window, cx),
|
|
||||||
DockPlacement::Right,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +136,7 @@ impl Render for GreeterPanel {
|
|||||||
.ghost()
|
.ghost()
|
||||||
.small()
|
.small()
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.add_profile_panel(window, cx)
|
this.open_profile(window, cx)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ pub mod contact_list;
|
|||||||
pub mod greeter;
|
pub mod greeter;
|
||||||
pub mod inbox;
|
pub mod inbox;
|
||||||
pub mod messaging_relays;
|
pub mod messaging_relays;
|
||||||
pub mod profile;
|
|
||||||
pub mod relay_list;
|
pub mod relay_list;
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|||||||
@@ -1,427 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
|
|
||||||
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
|
|
||||||
Window, div, retain_all,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use person::{Person, PersonRegistry, shorten_pubkey};
|
|
||||||
use settings::AppSettings;
|
|
||||||
use state::{NostrRegistry, upload};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock::{Panel, PanelEvent};
|
|
||||||
use ui::input::{Input, InputState, Textarea, TextareaState};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{Disableable, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
|
||||||
|
|
||||||
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
|
|
||||||
cx.new(|cx| ProfilePanel::new(public_key, window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ProfilePanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// User's public key
|
|
||||||
public_key: PublicKey,
|
|
||||||
|
|
||||||
/// User's name text input
|
|
||||||
name_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// User's avatar url text input
|
|
||||||
avatar_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// User's bio multi line input
|
|
||||||
bio_input: Entity<TextareaState>,
|
|
||||||
|
|
||||||
/// User's website url text input
|
|
||||||
website_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Uploading state
|
|
||||||
uploading: bool,
|
|
||||||
|
|
||||||
/// Copied states
|
|
||||||
copied: bool,
|
|
||||||
|
|
||||||
/// Updating state
|
|
||||||
updating: bool,
|
|
||||||
|
|
||||||
/// Tasks
|
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ProfilePanel {
|
|
||||||
fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
|
||||||
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
|
|
||||||
let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg"));
|
|
||||||
|
|
||||||
// Use multi-line input for bio
|
|
||||||
let bio_input = cx.new(|cx| {
|
|
||||||
TextareaState::new(window, cx)
|
|
||||||
.auto_grow(3, 8)
|
|
||||||
.placeholder("A short introduce about you.")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get user's profile and update inputs
|
|
||||||
cx.defer_in(window, move |this, window, cx| {
|
|
||||||
this.set_profile(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Update Profile".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
public_key,
|
|
||||||
name_input,
|
|
||||||
avatar_input,
|
|
||||||
bio_input,
|
|
||||||
website_input,
|
|
||||||
uploading: false,
|
|
||||||
copied: false,
|
|
||||||
updating: false,
|
|
||||||
tasks: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_profile(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let profile = persons.read(cx).get(&self.public_key, cx);
|
|
||||||
let metadata = profile.metadata();
|
|
||||||
|
|
||||||
self.avatar_input.update(cx, |this, cx| {
|
|
||||||
if let Some(avatar) = metadata.picture.as_ref() {
|
|
||||||
this.set_value(avatar, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.bio_input.update(cx, |this, cx| {
|
|
||||||
if let Some(bio) = metadata.about.as_ref() {
|
|
||||||
this.set_value(bio, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.name_input.update(cx, |this, cx| {
|
|
||||||
if let Some(display_name) = metadata.display_name.as_ref() {
|
|
||||||
this.set_value(display_name, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.website_input.update(cx, |this, cx| {
|
|
||||||
if let Some(website) = metadata.website.as_ref() {
|
|
||||||
this.set_value(website, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn copy(&mut self, value: String, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let item = ClipboardItem::new_string(value);
|
|
||||||
cx.write_to_clipboard(item);
|
|
||||||
|
|
||||||
self.set_copied(true, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_copied(&mut self, status: bool, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.copied = status;
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
if status {
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
|
||||||
|
|
||||||
// Reset the copied state after a delay
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_copied(false, window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.uploading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Get the user's configured blossom server
|
|
||||||
let server = AppSettings::get_file_server(cx);
|
|
||||||
|
|
||||||
// Ask user for file upload
|
|
||||||
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);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Upload via blossom client
|
|
||||||
match upload(server, path, cx).await {
|
|
||||||
Ok(url) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.avatar_input.update(cx, |this, cx| {
|
|
||||||
this.set_value(url.to_string(), window, cx);
|
|
||||||
});
|
|
||||||
this.set_uploading(false, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.set_uploading(false, cx);
|
|
||||||
window.push_notification(
|
|
||||||
Notification::error(e.to_string()).autohide(false),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {
|
|
||||||
self.updating = updating;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the metadata for the current user
|
|
||||||
fn publish(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
let signer = nostr.read(cx).signer();
|
|
||||||
let metadata = metadata.clone();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
// Build and sign the metadata event
|
|
||||||
let event = metadata.finalize_async(&signer).await?;
|
|
||||||
|
|
||||||
// Send event to user's relays
|
|
||||||
client.send_event(&event).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let public_key = self.public_key;
|
|
||||||
let old_metadata = persons.read(cx).get(&public_key, cx).metadata();
|
|
||||||
|
|
||||||
// Extract all new metadata fields
|
|
||||||
let avatar = self.avatar_input.read(cx).value();
|
|
||||||
let name = self.name_input.read(cx).value();
|
|
||||||
let bio = self.bio_input.read(cx).value();
|
|
||||||
let website = self.website_input.read(cx).value();
|
|
||||||
|
|
||||||
// Construct the new metadata
|
|
||||||
let mut new_metadata = old_metadata
|
|
||||||
.display_name(name.as_ref())
|
|
||||||
.name(name.as_ref())
|
|
||||||
.about(bio.as_ref());
|
|
||||||
|
|
||||||
// Verify the avatar URL before adding it
|
|
||||||
if let Ok(url) = Url::from_str(&avatar) {
|
|
||||||
new_metadata = new_metadata.picture(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the website URL before adding it
|
|
||||||
if let Ok(url) = Url::from_str(&website) {
|
|
||||||
new_metadata = new_metadata.website(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the metadata
|
|
||||||
let task = self.publish(&new_metadata, cx);
|
|
||||||
|
|
||||||
// Set the updating state
|
|
||||||
self.set_updating(true, cx);
|
|
||||||
|
|
||||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match task.await {
|
|
||||||
Ok(_) => {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
// Update the registry
|
|
||||||
persons.update(cx, |this, cx| {
|
|
||||||
this.insert(Person::new(public_key, new_metadata), cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update current panel
|
|
||||||
this.set_updating(false, cx);
|
|
||||||
this.set_profile(window, cx);
|
|
||||||
|
|
||||||
window.push_notification("Profile updated successfully", cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
window.push_notification(
|
|
||||||
Notification::error(e.to_string()).autohide(false),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ProfilePanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ProfilePanel {}
|
|
||||||
|
|
||||||
impl Focusable for ProfilePanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ProfilePanel {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let avatar_input = self.avatar_input.read(cx).value();
|
|
||||||
|
|
||||||
let picture = (!avatar_input.is_empty()).then_some(avatar_input);
|
|
||||||
|
|
||||||
// Get the public key as short string
|
|
||||||
let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.image_cache(retain_all("profile-panel"))
|
|
||||||
.p_3()
|
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.h_40()
|
|
||||||
.w_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_4()
|
|
||||||
.child(Avatar::new(picture).seed(self.public_key.to_hex()).large())
|
|
||||||
.child(
|
|
||||||
Button::new("upload")
|
|
||||||
.icon(IconName::PlusCircle)
|
|
||||||
.label("Add an avatar")
|
|
||||||
.xsmall()
|
|
||||||
.ghost()
|
|
||||||
.rounded()
|
|
||||||
.disabled(self.uploading)
|
|
||||||
.loading(self.uploading)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.upload(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("What should people call you?")),
|
|
||||||
)
|
|
||||||
.child(Input::new(&self.name_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("A short introduction about you:")),
|
|
||||||
)
|
|
||||||
.child(Textarea::new(&self.bio_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Website:")),
|
|
||||||
)
|
|
||||||
.child(Input::new(&self.website_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1p5()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Public Key:")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.h_8()
|
|
||||||
.w_full()
|
|
||||||
.justify_center()
|
|
||||||
.gap_3()
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.bg(cx.theme().secondary_background)
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().secondary_foreground)
|
|
||||||
.child(shorten_pkey)
|
|
||||||
.child(
|
|
||||||
Button::new("copy")
|
|
||||||
.icon({
|
|
||||||
if self.copied {
|
|
||||||
IconName::CheckCircle
|
|
||||||
} else {
|
|
||||||
IconName::Copy
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.xsmall()
|
|
||||||
.secondary()
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.copy(this.public_key.to_bech32().unwrap(), window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("submit")
|
|
||||||
.icon(IconName::CheckCircle)
|
|
||||||
.label("Update")
|
|
||||||
.primary()
|
|
||||||
.font_semibold()
|
|
||||||
.loading(self.updating)
|
|
||||||
.disabled(self.updating)
|
|
||||||
.on_click(cx.listener(move |this, _ev, window, cx| {
|
|
||||||
this.update(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -278,8 +278,6 @@ impl Render for Sidebar {
|
|||||||
.relative()
|
.relative()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.bg(cx.theme().surface_background)
|
.bg(cx.theme().surface_background)
|
||||||
.border_r_1()
|
|
||||||
.border_color(cx.theme().border_variant)
|
|
||||||
.when_some(current_user.as_ref(), |this, current_user| {
|
.when_some(current_user.as_ref(), |this, current_user| {
|
||||||
this.child(title_bar_drag_handlers(
|
this.child(title_bar_drag_handlers(
|
||||||
self.render_user(current_user, cx),
|
self.render_user(current_user, cx),
|
||||||
|
|||||||
Reference in New Issue
Block a user