46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -1242,7 +1242,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1674,7 +1674,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2614,7 +2614,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2713,7 +2713,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2724,7 +2724,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"gpui",
|
"gpui",
|
||||||
@@ -2953,7 +2953,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -2979,7 +2979,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -3775,7 +3775,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.71.1",
|
"bindgen 0.71.1",
|
||||||
@@ -4025,7 +4025,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -4049,7 +4049,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4061,7 +4061,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -4072,7 +4072,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-gossip"
|
name = "nostr-gossip"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nostr",
|
"nostr",
|
||||||
]
|
]
|
||||||
@@ -4080,7 +4080,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"flume",
|
"flume",
|
||||||
@@ -4094,7 +4094,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -4111,7 +4111,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.44.1"
|
version = "0.44.1"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#a7c768a8c1e6ecb3853ab17090766dbc779a5da9"
|
source = "git+https://github.com/rust-nostr/nostr#aa01f9fb8cd1ddbcc66e2b4ef58eeedbc0d54f56"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -4628,7 +4628,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5254,7 +5254,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -5352,7 +5352,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -5406,7 +5406,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "rope"
|
name = "rope"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5872,7 +5872,7 @@ checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "semantic_version"
|
name = "semantic_version"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6327,7 +6327,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -7295,7 +7295,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -7331,7 +7331,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#a39ba03bccff4eaf7e4fbc6496b2f4ee6faf7bb6"
|
source = "git+https://github.com/zed-industries/zed#1e2f15a3d70258ab366e9ac9309605749d5b0a27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"perf",
|
"perf",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -5,9 +5,23 @@ use key_store::{KeyItem, KeyStore};
|
|||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
|
|
||||||
actions!(coop, [KeyringPopup, DarkMode, Settings, Logout, Quit]);
|
// Sidebar actions
|
||||||
actions!(sidebar, [Reload, RelayStatus]);
|
actions!(sidebar, [Reload, RelayStatus]);
|
||||||
|
|
||||||
|
// User actions
|
||||||
|
actions!(
|
||||||
|
coop,
|
||||||
|
[
|
||||||
|
KeyringPopup,
|
||||||
|
DarkMode,
|
||||||
|
ViewProfile,
|
||||||
|
ViewRelays,
|
||||||
|
Settings,
|
||||||
|
Logout,
|
||||||
|
Quit
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct CoopAuthUrlHandler;
|
pub struct CoopAuthUrlHandler;
|
||||||
|
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ use ui::popover::{Popover, PopoverContent};
|
|||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::popup_menu::PopupMenuExt;
|
||||||
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings};
|
use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings, ViewProfile, ViewRelays};
|
||||||
|
use crate::user::viewer;
|
||||||
use crate::views::compose::compose_button;
|
use crate::views::compose::compose_button;
|
||||||
use crate::views::{onboarding, preferences, sidebar, startup, user_profile, welcome};
|
use crate::views::{onboarding, preferences, setup_relay, sidebar, startup, welcome};
|
||||||
use crate::{login, new_identity};
|
use crate::{login, new_identity, user};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||||
cx.new(|cx| ChatSpace::new(window, cx))
|
cx.new(|cx| ChatSpace::new(window, cx))
|
||||||
@@ -228,11 +229,82 @@ impl ChatSpace {
|
|||||||
window.open_modal(cx, move |modal, _window, _cx| {
|
window.open_modal(cx, move |modal, _window, _cx| {
|
||||||
modal
|
modal
|
||||||
.title(shared_t!("common.preferences"))
|
.title(shared_t!("common.preferences"))
|
||||||
.width(px(580.))
|
.width(px(520.))
|
||||||
.child(view.clone())
|
.child(view.clone())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(profile) => {
|
||||||
|
persons.update(cx, |this, cx| {
|
||||||
|
this.insert_or_update_person(profile, 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_relays(&mut self, _ev: &ViewRelays, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let view = setup_relay::init(window, cx);
|
||||||
|
let entity = view.downgrade();
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
let entity = entity.clone();
|
||||||
|
|
||||||
|
this.confirm()
|
||||||
|
.title(shared_t!("relays.modal"))
|
||||||
|
.child(view.clone())
|
||||||
|
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
||||||
|
.on_ok(move |_, window, cx| {
|
||||||
|
entity
|
||||||
|
.update(cx, |this, cx| {
|
||||||
|
this.set_relays(window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
// false to keep the modal open
|
||||||
|
false
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_dark_mode(&mut self, _ev: &DarkMode, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if cx.theme().mode.is_dark() {
|
if cx.theme().mode.is_dark() {
|
||||||
Theme::change(ThemeMode::Light, Some(window), cx);
|
Theme::change(ThemeMode::Light, Some(window), cx);
|
||||||
@@ -247,17 +319,22 @@ impl ChatSpace {
|
|||||||
|
|
||||||
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 profile = user_profile::init(public_key, window, cx);
|
let view = viewer::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()
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.overlay_closable(true)
|
.overlay_closable(true)
|
||||||
.child(profile.clone())
|
.child(view.clone())
|
||||||
.button_props(ModalButtonProps::default().ok_text(t!("profile.njump")))
|
.button_props(ModalButtonProps::default().ok_text("View on njump.me"))
|
||||||
.on_ok(move |_, _window, cx| {
|
.on_ok(move |_, _window, cx| {
|
||||||
let Ok(bech32) = public_key.to_bech32();
|
let bech32 = public_key.to_bech32().unwrap();
|
||||||
cx.open_url(&format!("https://njump.me/{bech32}"));
|
let url = format!("https://njump.me/{bech32}");
|
||||||
|
|
||||||
|
// Open the URL in the default browser
|
||||||
|
cx.open_url(&url);
|
||||||
|
|
||||||
|
// false to keep the modal open
|
||||||
false
|
false
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
@@ -467,14 +544,14 @@ impl ChatSpace {
|
|||||||
.popup_menu(move |this, _window, _cx| {
|
.popup_menu(move |this, _window, _cx| {
|
||||||
this.label(profile.display_name())
|
this.label(profile.display_name())
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
t!("user.dark_mode"),
|
"Profile",
|
||||||
IconName::Sun,
|
IconName::EmojiFill,
|
||||||
Box::new(DarkMode),
|
Box::new(ViewProfile),
|
||||||
)
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
t!("user.settings"),
|
"Messaging Relays",
|
||||||
IconName::Settings,
|
IconName::Server,
|
||||||
Box::new(Settings),
|
Box::new(ViewRelays),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.label(SharedString::from("Keyring Service"))
|
.label(SharedString::from("Keyring Service"))
|
||||||
@@ -486,7 +563,17 @@ impl ChatSpace {
|
|||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
t!("user.sign_out"),
|
"Dark Mode",
|
||||||
|
IconName::Sun,
|
||||||
|
Box::new(DarkMode),
|
||||||
|
)
|
||||||
|
.menu_with_icon(
|
||||||
|
"Settings",
|
||||||
|
IconName::Settings,
|
||||||
|
Box::new(Settings),
|
||||||
|
)
|
||||||
|
.menu_with_icon(
|
||||||
|
"Sign Out",
|
||||||
IconName::Logout,
|
IconName::Logout,
|
||||||
Box::new(Logout),
|
Box::new(Logout),
|
||||||
)
|
)
|
||||||
@@ -556,6 +643,8 @@ impl Render for ChatSpace {
|
|||||||
div()
|
div()
|
||||||
.id(SharedString::from("chatspace"))
|
.id(SharedString::from("chatspace"))
|
||||||
.on_action(cx.listener(Self::on_settings))
|
.on_action(cx.listener(Self::on_settings))
|
||||||
|
.on_action(cx.listener(Self::on_profile))
|
||||||
|
.on_action(cx.listener(Self::on_relays))
|
||||||
.on_action(cx.listener(Self::on_dark_mode))
|
.on_action(cx.listener(Self::on_dark_mode))
|
||||||
.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))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ mod actions;
|
|||||||
mod chatspace;
|
mod chatspace;
|
||||||
mod login;
|
mod login;
|
||||||
mod new_identity;
|
mod new_identity;
|
||||||
|
mod user;
|
||||||
mod views;
|
mod views;
|
||||||
|
|
||||||
i18n::init!();
|
i18n::init!();
|
||||||
|
|||||||
@@ -244,20 +244,18 @@ impl NewAccount {
|
|||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(url)) => {
|
Ok(Ok(url)) => {
|
||||||
this.uploading(false, cx);
|
|
||||||
this.avatar_input.update(cx, |this, cx| {
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
this.set_value(url.to_string(), window, cx);
|
this.set_value(url.to_string(), window, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
window.push_notification(e.to_string(), cx);
|
window.push_notification(e.to_string(), cx);
|
||||||
this.uploading(false, cx);
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to upload avatar: {e}");
|
log::warn!("Failed to upload avatar: {e}");
|
||||||
this.uploading(false, cx);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.uploading(false, cx);
|
||||||
})
|
})
|
||||||
.expect("Entity has been released");
|
.expect("Entity has been released");
|
||||||
})
|
})
|
||||||
|
|||||||
393
crates/coop/src/user/mod.rs
Normal file
393
crates/coop/src/user/mod.rs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
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, Flatten, IntoElement, ParentElement,
|
||||||
|
PathPromptOptions, Render, SharedString, Styled, Task, Window,
|
||||||
|
};
|
||||||
|
use gpui_tokio::Tokio;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
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, ContextModal, Disableable, IconName, Sizable, StyledExt};
|
||||||
|
|
||||||
|
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_media_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 Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
|
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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => Err(anyhow!("User cancelled")),
|
||||||
|
Err(e) => Err(anyhow!("File dialog error: {e}")),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
|
||||||
|
|
||||||
|
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<Profile, 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 gossip = nostr.read(cx).gossip();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let gossip = gossip.read().await;
|
||||||
|
let write_relays = gossip.inbox_relays(&public_key);
|
||||||
|
|
||||||
|
// Ensure connections to the write relays
|
||||||
|
gossip.ensure_connections(&client, &write_relays).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(write_relays, &event).await?;
|
||||||
|
|
||||||
|
// Return the updated profile
|
||||||
|
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
||||||
|
let profile = Profile::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::CheckCircleFill
|
||||||
|
} else {
|
||||||
|
IconName::Copy
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.xsmall()
|
||||||
|
.ghost()
|
||||||
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
|
this.copy(
|
||||||
|
public_key.to_bech32().unwrap(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use common::{nip05_verify, RenderedProfile};
|
use common::{nip05_verify, shorten_pubkey, RenderedProfile};
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
|
||||||
ParentElement, Render, SharedString, Styled, Task, Window,
|
ParentElement, Render, SharedString, Styled, Task, Window,
|
||||||
};
|
};
|
||||||
use gpui_tokio::Tokio;
|
use gpui_tokio::Tokio;
|
||||||
use i18n::{shared_t, t};
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
@@ -18,19 +17,28 @@ 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<UserProfile> {
|
pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfileViewer> {
|
||||||
cx.new(|cx| UserProfile::new(public_key, window, cx))
|
cx.new(|cx| ProfileViewer::new(public_key, window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserProfile {
|
#[derive(Debug)]
|
||||||
|
pub struct ProfileViewer {
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
|
|
||||||
|
/// Follow status
|
||||||
followed: bool,
|
followed: bool,
|
||||||
|
|
||||||
|
/// Verification status
|
||||||
verified: bool,
|
verified: bool,
|
||||||
|
|
||||||
|
/// Copy status
|
||||||
copied: bool,
|
copied: bool,
|
||||||
|
|
||||||
|
/// Async operations
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
_tasks: SmallVec<[Task<()>; 1]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserProfile {
|
impl ProfileViewer {
|
||||||
pub fn new(target: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn new(target: 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();
|
||||||
@@ -106,27 +114,28 @@ impl UserProfile {
|
|||||||
self.copied = status;
|
self.copied = status;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
// Reset the copied state after a delay
|
|
||||||
if status {
|
if status {
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
self._tasks.push(
|
||||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
// Reset the copied state after a delay
|
||||||
cx.update(|window, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
this.update(cx, |this, cx| {
|
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||||
this.set_copied(false, window, cx);
|
cx.update(|window, cx| {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_copied(false, window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
}),
|
||||||
.ok();
|
);
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for UserProfile {
|
impl Render for ProfileViewer {
|
||||||
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 proxy = AppSettings::get_proxy_user_avatars(cx);
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let bech32 = self.profile.public_key().to_bech32().unwrap();
|
let bech32 = shorten_pubkey(self.profile.public_key(), 16);
|
||||||
let shared_bech32 = SharedString::from(bech32);
|
let shared_bech32 = SharedString::from(bech32);
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -180,33 +189,55 @@ impl Render for UserProfile {
|
|||||||
.bg(cx.theme().elevated_surface_background)
|
.bg(cx.theme().elevated_surface_background)
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.child(shared_t!("profile.unknown")),
|
.child(SharedString::from("Unknown contact")),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.text_sm()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from("Bio:")),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.p_2()
|
||||||
|
.min_h_16()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().elevated_surface_background)
|
||||||
|
.child(
|
||||||
|
self.profile
|
||||||
|
.metadata()
|
||||||
|
.about
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or(SharedString::from("No bio.")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.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(SharedString::from("Public Key:")),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1()
|
.gap_2()
|
||||||
.child(
|
.w_full()
|
||||||
div()
|
.h_12()
|
||||||
.p_2()
|
.justify_center()
|
||||||
.h_7()
|
.bg(cx.theme().surface_background)
|
||||||
.rounded_md()
|
.rounded(cx.theme().radius)
|
||||||
.bg(cx.theme().elevated_surface_background)
|
.text_sm()
|
||||||
.truncate()
|
.child(shared_bech32)
|
||||||
.text_ellipsis()
|
|
||||||
.line_clamp(1)
|
|
||||||
.line_height(relative(1.))
|
|
||||||
.child(shared_bech32),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("copy")
|
Button::new("copy")
|
||||||
.icon({
|
.icon({
|
||||||
@@ -216,35 +247,13 @@ impl Render for UserProfile {
|
|||||||
IconName::Copy
|
IconName::Copy
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.cta()
|
.xsmall()
|
||||||
.ghost_alt()
|
.ghost()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
this.copy_pubkey(window, cx);
|
this.copy_pubkey(window, cx);
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(shared_t!("profile.label_bio")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.p_2()
|
|
||||||
.rounded_md()
|
|
||||||
.bg(cx.theme().elevated_surface_background)
|
|
||||||
.child(
|
|
||||||
self.profile
|
|
||||||
.metadata()
|
|
||||||
.about
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or(shared_t!("profile.no_bio")),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use anyhow::Error;
|
|
||||||
use common::nip96_upload;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
|
|
||||||
PathPromptOptions, Render, SharedString, Styled, Task, Window,
|
|
||||||
};
|
|
||||||
use i18n::{shared_t, t};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use settings::AppSettings;
|
|
||||||
use smol::fs;
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::{InputState, TextInput};
|
|
||||||
use ui::{v_flex, Disableable, IconName, Sizable};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<EditProfile> {
|
|
||||||
EditProfile::new(window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct EditProfile {
|
|
||||||
profile: Option<Metadata>,
|
|
||||||
name_input: Entity<InputState>,
|
|
||||||
avatar_input: Entity<InputState>,
|
|
||||||
bio_input: Entity<InputState>,
|
|
||||||
website_input: Entity<InputState>,
|
|
||||||
is_loading: bool,
|
|
||||||
is_submitting: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EditProfile {
|
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let name_input =
|
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder(t!("profile.placeholder_name")));
|
|
||||||
|
|
||||||
let avatar_input =
|
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
|
|
||||||
|
|
||||||
let website_input =
|
|
||||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://your-website.com"));
|
|
||||||
|
|
||||||
let bio_input = cx.new(|cx| {
|
|
||||||
InputState::new(window, cx)
|
|
||||||
.multi_line()
|
|
||||||
.placeholder(t!("profile.placeholder_bio"))
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.new(|cx| {
|
|
||||||
let this = Self {
|
|
||||||
name_input,
|
|
||||||
avatar_input,
|
|
||||||
bio_input,
|
|
||||||
website_input,
|
|
||||||
profile: None,
|
|
||||||
is_loading: false,
|
|
||||||
is_submitting: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let metadata = client
|
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(metadata)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Ok(Some(metadata)) = task.await {
|
|
||||||
this.update_in(cx, |this: &mut EditProfile, window, cx| {
|
|
||||||
this.avatar_input.update(cx, |this, cx| {
|
|
||||||
if let Some(avatar) = metadata.picture.as_ref() {
|
|
||||||
this.set_value(avatar, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.bio_input.update(cx, |this, cx| {
|
|
||||||
if let Some(bio) = metadata.about.as_ref() {
|
|
||||||
this.set_value(bio, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.name_input.update(cx, |this, cx| {
|
|
||||||
if let Some(display_name) = metadata.display_name.as_ref() {
|
|
||||||
this.set_value(display_name, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.website_input.update(cx, |this, cx| {
|
|
||||||
if let Some(website) = metadata.website.as_ref() {
|
|
||||||
this.set_value(website, window, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.profile = Some(metadata);
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
this
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let client = nostr.read(cx).client();
|
|
||||||
|
|
||||||
let nip96 = AppSettings::get_media_server(cx);
|
|
||||||
let avatar_input = self.avatar_input.downgrade();
|
|
||||||
|
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
|
||||||
files: true,
|
|
||||||
directories: false,
|
|
||||||
multiple: false,
|
|
||||||
prompt: None,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show loading spinner
|
|
||||||
self.set_loading(true, cx);
|
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
|
||||||
Ok(Some(mut paths)) => {
|
|
||||||
let path = paths.pop().unwrap();
|
|
||||||
|
|
||||||
if let Ok(file_data) = fs::read(path).await {
|
|
||||||
let (tx, rx) = oneshot::channel::<Url>();
|
|
||||||
|
|
||||||
nostr_sdk::async_utility::task::spawn(async move {
|
|
||||||
if let Ok(url) = nip96_upload(&client, &nip96, file_data).await {
|
|
||||||
_ = tx.send(url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(url) = rx.await {
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
// Stop loading spinner
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Set avatar input
|
|
||||||
avatar_input
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.set_value(url.to_string(), window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
// Stop loading spinner
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_loading(false, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, 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();
|
|
||||||
|
|
||||||
let old_metadata = if let Some(metadata) = self.profile.as_ref() {
|
|
||||||
metadata.clone()
|
|
||||||
} else {
|
|
||||||
Metadata::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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 gossip = nostr.read(cx).gossip();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
let gossip = gossip.read().await;
|
|
||||||
let write_relays = gossip.inbox_relays(&public_key);
|
|
||||||
|
|
||||||
// Ensure connections to the write relays
|
|
||||||
gossip.ensure_connections(&client, &write_relays).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(write_relays, &event).await?;
|
|
||||||
|
|
||||||
// Return the updated profile
|
|
||||||
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
|
|
||||||
let profile = Profile::new(event.pubkey, metadata);
|
|
||||||
|
|
||||||
Ok(profile)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.is_loading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for EditProfile {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.h_32()
|
|
||||||
.bg(cx.theme().surface_background)
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_2()
|
|
||||||
.map(|this| {
|
|
||||||
let picture = self.avatar_input.read(cx).value();
|
|
||||||
if picture.is_empty() {
|
|
||||||
this.child(
|
|
||||||
img("brand/avatar.png")
|
|
||||||
.rounded_full()
|
|
||||||
.size_10()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.child(
|
|
||||||
img(picture.clone())
|
|
||||||
.rounded_full()
|
|
||||||
.size_10()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
Button::new("upload")
|
|
||||||
.icon(IconName::Upload)
|
|
||||||
.label(t!("common.change"))
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.disabled(self.is_loading || self.is_submitting)
|
|
||||||
.loading(self.is_loading)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.upload(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(shared_t!("profile.label_name"))
|
|
||||||
.child(TextInput::new(&self.name_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(shared_t!("profile.label_website"))
|
|
||||||
.child(TextInput::new(&self.website_input).small()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.gap_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(shared_t!("profile.label_bio"))
|
|
||||||
.child(TextInput::new(&self.bio_input).small()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod edit_profile;
|
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod setup_relay;
|
pub mod setup_relay;
|
||||||
pub mod sidebar;
|
pub mod sidebar;
|
||||||
pub mod startup;
|
pub mod startup;
|
||||||
pub mod user_profile;
|
|
||||||
pub mod welcome;
|
pub mod welcome;
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
use account::Account;
|
|
||||||
use common::RenderedProfile;
|
|
||||||
use gpui::http_client::Url;
|
use gpui::http_client::Url;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
div, px, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString,
|
||||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
Styled, Window,
|
||||||
};
|
};
|
||||||
use i18n::{shared_t, t};
|
use i18n::{shared_t, t};
|
||||||
use person::PersonRegistry;
|
|
||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::input::{InputState, TextInput};
|
use ui::input::{InputState, TextInput};
|
||||||
use ui::modal::ModalButtonProps;
|
|
||||||
use ui::switch::Switch;
|
use ui::switch::Switch;
|
||||||
use ui::{h_flex, v_flex, ContextModal, IconName, Sizable, Size, StyledExt};
|
use ui::{h_flex, v_flex, IconName, Sizable, Size, StyledExt};
|
||||||
|
|
||||||
use crate::views::{edit_profile, setup_relay};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Preferences> {
|
||||||
cx.new(|cx| Preferences::new(window, cx))
|
cx.new(|cx| Preferences::new(window, cx))
|
||||||
@@ -37,73 +30,6 @@ impl Preferences {
|
|||||||
|
|
||||||
Self { media_input }
|
Self { media_input }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_edit_profile(&self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let view = edit_profile::init(window, cx);
|
|
||||||
let weak_view = view.downgrade();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, _window, _cx| {
|
|
||||||
let weak_view = weak_view.clone();
|
|
||||||
|
|
||||||
modal
|
|
||||||
.confirm()
|
|
||||||
.title(shared_t!("profile.title"))
|
|
||||||
.child(view.clone())
|
|
||||||
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
|
||||||
.on_ok(move |_, window, cx| {
|
|
||||||
weak_view
|
|
||||||
.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(profile) => {
|
|
||||||
persons.update(cx, |this, cx| {
|
|
||||||
this.insert_or_update_person(profile, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
// true to close the modal
|
|
||||||
true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn open_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let view = setup_relay::init(window, cx);
|
|
||||||
let weak_view = view.downgrade();
|
|
||||||
|
|
||||||
window.open_modal(cx, move |this, _window, _cx| {
|
|
||||||
let weak_view = weak_view.clone();
|
|
||||||
|
|
||||||
this.confirm()
|
|
||||||
.title(shared_t!("relays.modal"))
|
|
||||||
.child(view.clone())
|
|
||||||
.button_props(ModalButtonProps::default().ok_text(t!("common.update")))
|
|
||||||
.on_ok(move |_, window, cx| {
|
|
||||||
weak_view
|
|
||||||
.update(cx, |this, cx| {
|
|
||||||
this.set_relays(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
// true to close the modal
|
|
||||||
false
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for Preferences {
|
impl Render for Preferences {
|
||||||
@@ -115,68 +41,9 @@ impl Render for Preferences {
|
|||||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||||
let hide = AppSettings::get_hide_user_avatars(cx);
|
let hide = AppSettings::get_hide_user_avatars(cx);
|
||||||
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let account = Account::global(cx);
|
|
||||||
let public_key = account.read(cx).public_key();
|
|
||||||
let profile = persons.read(cx).get_person(&public_key, cx);
|
|
||||||
|
|
||||||
let input_state = self.media_input.downgrade();
|
let input_state = self.media_input.downgrade();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.pb_2()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.font_semibold()
|
|
||||||
.child(shared_t!("preferences.account_header")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.id("user")
|
|
||||||
.gap_2()
|
|
||||||
.child(Avatar::new(profile.avatar(proxy)).size(rems(2.4)))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_1()
|
|
||||||
.text_sm()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.3))
|
|
||||||
.child(profile.display_name()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.line_height(relative(1.3))
|
|
||||||
.child(shared_t!("preferences.account_btn")),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.open_edit_profile(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("relays")
|
|
||||||
.label("Messaging Relays")
|
|
||||||
.xsmall()
|
|
||||||
.ghost_alt()
|
|
||||||
.rounded()
|
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
|
||||||
this.open_relays(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.py_2()
|
.py_2()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ impl Startup {
|
|||||||
Self {
|
Self {
|
||||||
credential,
|
credential,
|
||||||
loading: false,
|
loading: false,
|
||||||
name: "Continue".into(),
|
name: "Onboarding".into(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
image_cache: RetainAllImageCache::new(cx),
|
image_cache: RetainAllImageCache::new(cx),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
|
|||||||
Reference in New Issue
Block a user