Redesign for the v1 stable release #3

Merged
reya merged 30 commits from v1-redesign into master 2026-02-04 01:43:24 +00:00
8 changed files with 63 additions and 492 deletions
Showing only changes of commit a87111e138 - Show all commits

58
Cargo.lock generated
View File

@@ -1180,7 +1180,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1619,7 +1619,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2542,7 +2542,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.2.2" version = "0.2.2"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2644,7 +2644,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2655,7 +2655,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -2877,7 +2877,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compression", "async-compression",
@@ -2902,7 +2902,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2997,9 +2997,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.64" version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@@ -3663,7 +3663,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen", "bindgen",
@@ -4264,9 +4264,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]] [[package]]
name = "oneshot" name = "oneshot"
version = "0.1.12" version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ce66197e99546da6c6d991285f605192e794ceae69686c17163844a7bf8fcc2" checksum = "269bca4c2591a28585d6bf10d9ed0332b7d76900a1b02bec41bdc3a2cdcda107"
[[package]] [[package]]
name = "oo7" name = "oo7"
@@ -4502,7 +4502,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "perf" name = "perf"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"collections", "collections",
"serde", "serde",
@@ -5137,7 +5137,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
] ]
@@ -5236,7 +5236,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5290,7 +5290,7 @@ dependencies = [
[[package]] [[package]]
name = "rope" name = "rope"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5569,7 +5569,7 @@ dependencies = [
[[package]] [[package]]
name = "scheduler" name = "scheduler"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"async-task", "async-task",
"backtrace", "backtrace",
@@ -5942,9 +5942,9 @@ dependencies = [
[[package]] [[package]]
name = "siphasher" name = "siphasher"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
@@ -6161,7 +6161,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -7102,7 +7102,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7140,7 +7140,7 @@ dependencies = [
[[package]] [[package]]
name = "util_macros" name = "util_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"perf", "perf",
"quote", "quote",
@@ -8521,18 +8521,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.33" version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" checksum = "fdea86ddd5568519879b8187e1cf04e24fce28f7fe046ceecbce472ff19a2572"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.33" version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" checksum = "0c15e1b46eff7c6c91195752e0eeed8ef040e391cdece7c25376957d5f15df22"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -8616,7 +8616,7 @@ dependencies = [
[[package]] [[package]]
name = "zlog" name = "zlog"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -8633,7 +8633,7 @@ checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
[[package]] [[package]]
name = "ztracing" name = "ztracing"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
@@ -8644,7 +8644,7 @@ dependencies = [
[[package]] [[package]]
name = "ztracing_macro" name = "ztracing_macro"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#aceff52f8a93a31009d6f5284253f0b334a043dc" source = "git+https://github.com/zed-industries/zed#1870425b2b262e3f28c90931782aba435afd5b99"
[[package]] [[package]]
name = "zune-core" name = "zune-core"

View File

@@ -1,2 +1,3 @@
pub mod compose; pub mod compose;
pub mod profile;
pub mod screening; pub mod screening;

View File

@@ -9,7 +9,7 @@ use gpui::{
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry}; use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -17,13 +17,13 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt}; use ui::{h_flex, v_flex, Icon, IconName, Sizable, StyledExt};
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfileViewer> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfileDialog> {
cx.new(|cx| ProfileViewer::new(public_key, window, cx)) cx.new(|cx| ProfileDialog::new(public_key, window, cx))
} }
#[derive(Debug)] #[derive(Debug)]
pub struct ProfileViewer { pub struct ProfileDialog {
profile: Person, public_key: PublicKey,
/// Follow status /// Follow status
followed: bool, followed: bool,
@@ -38,13 +38,13 @@ pub struct ProfileViewer {
_tasks: SmallVec<[Task<()>; 1]>, _tasks: SmallVec<[Task<()>; 1]>,
} }
impl ProfileViewer { impl ProfileDialog {
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&target, cx); let profile = persons.read(cx).get(&public_key, cx);
let mut tasks = smallvec![]; let mut tasks = smallvec![];
@@ -53,12 +53,12 @@ impl ProfileViewer {
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
let contact_list = client.database().contacts_public_keys(public_key).await?; let contact_list = client.database().contacts_public_keys(public_key).await?;
Ok(contact_list.contains(&target)) Ok(contact_list.contains(&public_key))
}); });
let verify_nip05 = if let Some(address) = profile.metadata().nip05 { let verify_nip05 = if let Some(address) = profile.metadata().nip05 {
Some(Tokio::spawn(cx, async move { Some(Tokio::spawn(cx, async move {
nip05_verify(target, &address).await.unwrap_or(false) nip05_verify(public_key, &address).await.unwrap_or(false)
})) }))
} else { } else {
None None
@@ -90,7 +90,7 @@ impl ProfileViewer {
); );
Self { Self {
profile, public_key,
followed: false, followed: false,
verified: false, verified: false,
copied: false, copied: false,
@@ -98,12 +98,18 @@ impl ProfileViewer {
} }
} }
fn address(&self, _cx: &Context<Self>) -> Option<String> { fn address(&self, cx: &Context<Self>) -> Option<String> {
self.profile.metadata().nip05 let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&self.public_key, cx);
profile.metadata().nip05
} }
fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn copy_pubkey(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok(bech32) = self.profile.public_key().to_bech32(); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&self.public_key, cx);
let Ok(bech32) = profile.public_key().to_bech32();
let item = ClipboardItem::new_string(bech32); let item = ClipboardItem::new_string(bech32);
cx.write_to_clipboard(item); cx.write_to_clipboard(item);
@@ -132,9 +138,11 @@ impl ProfileViewer {
} }
} }
impl Render for ProfileViewer { 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 bech32 = shorten_pubkey(self.profile.public_key(), 16); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&self.public_key, cx);
let bech32 = shorten_pubkey(profile.public_key(), 16);
let shared_bech32 = SharedString::from(bech32); let shared_bech32 = SharedString::from(bech32);
v_flex() v_flex()
@@ -146,14 +154,14 @@ impl Render for ProfileViewer {
.items_center() .items_center()
.justify_center() .justify_center()
.text_center() .text_center()
.child(Avatar::new(self.profile.avatar()).size(rems(4.))) .child(Avatar::new(profile.avatar()).size(rems(4.)))
.child( .child(
v_flex() v_flex()
.child( .child(
div() div()
.font_semibold() .font_semibold()
.line_height(relative(1.25)) .line_height(relative(1.25))
.child(self.profile.name()), .child(profile.name()),
) )
.when_some(self.address(cx), |this, address| { .when_some(self.address(cx), |this, address| {
this.child( this.child(
@@ -208,7 +216,7 @@ impl Render for ProfileViewer {
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.bg(cx.theme().elevated_surface_background) .bg(cx.theme().elevated_surface_background)
.child( .child(
self.profile profile
.metadata() .metadata()
.about .about
.map(SharedString::from) .map(SharedString::from)

View File

@@ -15,7 +15,6 @@ mod actions;
mod dialogs; mod dialogs;
mod panels; mod panels;
mod sidebar; mod sidebar;
mod user;
mod workspace; mod workspace;
fn main() { fn main() {

View File

@@ -140,7 +140,7 @@ impl Render for GreeterPanel {
.gap_2() .gap_2()
.when(relay_list_state == RelayState::NotSet, |this| { .when(relay_list_state == RelayState::NotSet, |this| {
this.child( this.child(
Button::new("connect") Button::new("relaylist")
.icon(Icon::new(IconName::Door)) .icon(Icon::new(IconName::Door))
.label("Set up relay list") .label("Set up relay list")
.ghost() .ghost()

View File

@@ -47,7 +47,7 @@ pub struct ImportPanel {
impl ImportPanel { impl ImportPanel {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let key_input = cx.new(|cx| InputState::new(window, cx)); let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
let error = cx.new(|_| None); let error = cx.new(|_| None);

View File

@@ -1,388 +0,0 @@
use std::str::FromStr;
use std::time::Duration;
use anyhow::{anyhow, Error};
use common::{nip96_upload, shorten_pubkey};
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, App, AppContext, ClipboardItem, Context, Entity, IntoElement, ParentElement,
PathPromptOptions, Render, SharedString, Styled, Task, Window,
};
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::Person;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use state::NostrRegistry;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput};
use ui::{h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
pub mod viewer;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<UserProfile> {
cx.new(|cx| UserProfile::new(window, cx))
}
#[derive(Debug)]
pub struct UserProfile {
/// User profile
profile: Option<Profile>,
/// 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<InputState>,
/// User's website url text input
website_input: Entity<InputState>,
/// Uploading state
uploading: bool,
/// Copied states
copied: bool,
/// Async operations
_tasks: SmallVec<[Task<()>; 1]>,
}
impl UserProfile {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg"));
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
// Use multi-line input for bio
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
.auto_grow(3, 8)
.placeholder("A short introduce about you.")
});
let get_profile = Self::get_profile(cx);
let mut tasks = smallvec![];
tasks.push(
// Get metadata in the background
cx.spawn_in(window, async move |this, cx| {
if let Ok(profile) = get_profile.await {
this.update_in(cx, |this, window, cx| {
this.set_profile(profile, window, cx);
})
.ok();
}
}),
);
Self {
profile: None,
name_input,
avatar_input,
bio_input,
website_input,
uploading: false,
copied: false,
_tasks: tasks,
}
}
fn get_profile(cx: &App) -> Task<Result<Profile, Error>> {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let metadata = client
.database()
.metadata(public_key)
.await?
.unwrap_or_default();
Ok(Profile::new(public_key, metadata))
})
}
fn set_profile(&mut self, profile: Profile, window: &mut Window, cx: &mut Context<Self>) {
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);
}
});
self.profile = Some(profile);
cx.notify();
}
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(
// Reset the copied state after a delay
cx.spawn_in(window, async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(2)).await;
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.set_copied(false, window, cx);
})
.ok();
})
.ok();
}),
);
}
}
fn 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>) {
self.uploading(true, cx);
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get the user's configured NIP96 server
let nip96_server = AppSettings::get_file_server(cx);
// Open native file dialog
let paths = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: false,
multiple: false,
prompt: None,
});
let task = Tokio::spawn(cx, async move {
match paths.await {
Ok(Ok(Some(mut paths))) => {
if let Some(path) = paths.pop() {
let file = fs::read(path).await?;
let url = nip96_upload(&client, &nip96_server, file).await?;
Ok(url)
} else {
Err(anyhow!("Path not found"))
}
}
_ => Err(anyhow!("Error")),
}
});
cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update_in(cx, |this, window, cx| {
match result {
Ok(Ok(url)) => {
this.avatar_input.update(cx, |this, cx| {
this.set_value(url.to_string(), window, cx);
});
}
Ok(Err(e)) => {
window.push_notification(e.to_string(), cx);
}
Err(e) => {
log::warn!("Failed to upload avatar: {e}");
}
};
this.uploading(false, cx);
})
.expect("Entity has been released");
})
.detach();
}
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Person, Error>> {
let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string();
let website = self.website_input.read(cx).value().to_string();
// Get the current profile metadata
let old_metadata = self
.profile
.as_ref()
.map(|profile| profile.metadata())
.unwrap_or_default();
// Construct the new metadata
let mut new_metadata = old_metadata.display_name(name).about(bio);
if let Ok(url) = Url::from_str(&avatar) {
new_metadata = new_metadata.picture(url);
};
if let Ok(url) = Url::from_str(&website) {
new_metadata = new_metadata.website(url);
}
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().await?;
// Sign the new metadata event
let event = EventBuilder::metadata(&new_metadata).sign(&signer).await?;
// Send event to user's write relayss
client.send_event_to(urls, &event).await?;
// Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Person::new(event.pubkey, metadata);
Ok(profile)
})
}
}
impl Render for UserProfile {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.gap_3()
.child(
v_flex()
.relative()
.w_full()
.h_32()
.items_center()
.justify_center()
.gap_2()
.bg(cx.theme().surface_background)
.rounded(cx.theme().radius)
.map(|this| {
let picture = self.avatar_input.read(cx).value();
let source = if picture.is_empty() {
"brand/avatar.png"
} else {
picture.as_str()
};
this.child(img(source).rounded_full().size_10().flex_shrink_0())
})
.child(
Button::new("upload")
.icon(IconName::Upload)
.label("Change")
.ghost()
.small()
.disabled(self.uploading)
.on_click(cx.listener(move |this, _, window, cx| {
this.upload(window, cx);
})),
),
)
.child(
v_flex()
.gap_1()
.text_sm()
.child(SharedString::from("Name:"))
.child(TextInput::new(&self.name_input).small()),
)
.child(
v_flex()
.gap_1()
.text_sm()
.child(SharedString::from("Bio:"))
.child(TextInput::new(&self.bio_input).small()),
)
.child(
v_flex()
.gap_1()
.text_sm()
.child(SharedString::from("Website:"))
.child(TextInput::new(&self.website_input).small()),
)
.when_some(self.profile.as_ref(), |this, profile| {
let public_key = profile.public_key();
let display = SharedString::from(shorten_pubkey(profile.public_key(), 8));
this.child(div().my_1().h_px().w_full().bg(cx.theme().border))
.child(
v_flex()
.gap_1()
.child(
div()
.text_xs()
.text_color(cx.theme().text_placeholder)
.font_semibold()
.child(SharedString::from("Public Key:")),
)
.child(
h_flex()
.gap_2()
.w_full()
.h_12()
.justify_center()
.bg(cx.theme().surface_background)
.rounded(cx.theme().radius)
.text_sm()
.child(display)
.child(
Button::new("copy")
.icon({
if self.copied {
IconName::CheckCircle
} else {
IconName::Copy
}
})
.xsmall()
.ghost()
.on_click(cx.listener(move |this, _e, window, cx| {
this.copy(
public_key.to_bech32().unwrap(),
window,
cx,
);
})),
),
),
)
})
}
}

View File

@@ -11,17 +11,16 @@ use gpui::{
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
}; };
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeRegistry}; use theme::{ActiveTheme, Theme, ThemeRegistry};
use ui::button::{Button, ButtonVariants}; use ui::button::{Button, ButtonVariants};
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension}; use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
use crate::actions::{reset, KeyringPopup, Logout, Themes, ViewProfile}; use crate::actions::{reset, KeyringPopup, Logout, Themes};
use crate::dialogs::profile;
use crate::panels::greeter; use crate::panels::greeter;
use crate::user::viewer; use crate::sidebar;
use crate::{sidebar, user};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> { pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx)) cx.new(|cx| Workspace::new(window, cx))
@@ -150,53 +149,6 @@ impl Workspace {
}); });
} }
fn on_profile(&mut self, _ev: &ViewProfile, window: &mut Window, cx: &mut Context<Self>) {
let view = user::init(window, cx);
let entity = view.downgrade();
window.open_modal(cx, move |modal, _window, _cx| {
let entity = entity.clone();
modal
.title("Profile")
.confirm()
.child(view.clone())
.button_props(ModalButtonProps::default().ok_text("Update"))
.on_ok(move |_, window, cx| {
entity
.update(cx, |this, cx| {
let persons = PersonRegistry::global(cx);
let set_metadata = this.set_metadata(cx);
cx.spawn_in(window, async move |this, cx| {
let result = set_metadata.await;
this.update_in(cx, |_, window, cx| {
match result {
Ok(person) => {
persons.update(cx, |this, cx| {
this.insert(person, cx);
// Close the edit profile modal
window.close_all_modals(cx);
});
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
};
})
.ok();
})
.detach();
})
.ok();
// false to keep the modal open
false
})
});
}
fn on_themes(&mut self, _ev: &Themes, window: &mut Window, cx: &mut Context<Self>) { fn on_themes(&mut self, _ev: &Themes, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
let registry = ThemeRegistry::global(cx); let registry = ThemeRegistry::global(cx);
@@ -255,7 +207,7 @@ impl Workspace {
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) { fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
let public_key = ev.0; let public_key = ev.0;
let view = viewer::init(public_key, window, cx); let view = profile::init(public_key, window, cx);
window.open_modal(cx, move |this, _window, _cx| { window.open_modal(cx, move |this, _window, _cx| {
this.alert() this.alert()
@@ -319,7 +271,6 @@ impl Render for Workspace {
div() div()
.id(SharedString::from("workspace")) .id(SharedString::from("workspace"))
.on_action(cx.listener(Self::on_profile))
.on_action(cx.listener(Self::on_themes)) .on_action(cx.listener(Self::on_themes))
.on_action(cx.listener(Self::on_sign_out)) .on_action(cx.listener(Self::on_sign_out))
.on_action(cx.listener(Self::on_open_pubkey)) .on_action(cx.listener(Self::on_open_pubkey))