feat: rewrite the nip-4e implementation (#1)

Make NIP-4e a core feature, not an optional feature.

Note:
- The UI is broken and needs to be updated in a separate PR.

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-01-13 16:00:08 +08:00
parent bb455871e5
commit 75c3783522
50 changed files with 2818 additions and 3458 deletions

View File

@@ -33,14 +33,12 @@ title_bar = { path = "../title_bar" }
theme = { path = "../theme" }
common = { path = "../common" }
state = { path = "../state" }
device = { path = "../device" }
key_store = { path = "../key_store" }
chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" }
settings = { path = "../settings" }
auto_update = { path = "../auto_update" }
account = { path = "../account" }
encryption = { path = "../encryption" }
encryption_ui = { path = "../encryption_ui" }
person = { path = "../person" }
relay_auth = { path = "../relay_auth" }

View File

@@ -83,8 +83,7 @@ pub fn reset(cx: &mut App) {
cx.update(|cx| {
cx.restart();
})
.ok();
});
})
.detach();
}

View File

@@ -1,12 +1,9 @@
use std::sync::Arc;
use account::Account;
use auto_update::{AutoUpdateStatus, AutoUpdater};
use chat::{ChatEvent, ChatRegistry};
use chat_ui::{CopyPublicKey, OpenPublicKey};
use common::{RenderedProfile, DEFAULT_SIDEBAR_WIDTH};
use encryption::Encryption;
use encryption_ui::EncryptionPanel;
use common::DEFAULT_SIDEBAR_WIDTH;
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
@@ -17,8 +14,8 @@ use key_store::{Credential, KeyItem, KeyStore};
use nostr_connect::prelude::*;
use person::PersonRegistry;
use relay_auth::RelayAuth;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
use title_bar::TitleBar;
use ui::avatar::Avatar;
@@ -27,7 +24,6 @@ use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::modal::ModalButtonProps;
use ui::popover::{Popover, PopoverContent};
use ui::popup_menu::PopupMenuExt;
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
@@ -61,9 +57,6 @@ pub struct ChatSpace {
/// App's Dock Area
dock: Entity<DockArea>,
/// App's Encryption Panel
encryption_panel: Entity<EncryptionPanel>,
/// Determines if the chat space is ready to use
ready: bool,
@@ -73,13 +66,14 @@ pub struct ChatSpace {
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let keystore = KeyStore::global(cx);
let account = Account::global(cx);
let title_bar = cx.new(|_| TitleBar::new());
let dock = cx.new(|cx| DockArea::new(window, cx));
let encryption_panel = encryption_ui::init(window, cx);
let identity = nostr.read(cx).identity();
let mut subscriptions = smallvec![];
@@ -92,8 +86,8 @@ impl ChatSpace {
subscriptions.push(
// Observe account entity changes
cx.observe_in(&account, window, move |this, state, window, cx| {
if !this.ready && state.read(cx).has_account() {
cx.observe_in(&identity, window, move |this, state, window, cx| {
if !this.ready && state.read(cx).has_public_key() {
this.set_default_layout(window, cx);
// Load all chat room in the database if available
@@ -141,15 +135,20 @@ impl ChatSpace {
ChatEvent::OpenRoom(id) => {
if let Some(room) = chat.read(cx).room(id, cx) {
this.dock.update(cx, |this, cx| {
let panel = chat_ui::init(room, window, cx);
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
this.add_panel(
Arc::new(chat_ui::init(room, window, cx)),
DockPlacement::Center,
window,
cx,
);
});
}
}
ChatEvent::CloseRoom(..) => {
this.dock.update(cx, |this, cx| {
// Force focus to the tab panel
this.focus_tab_panel(window, cx);
// Dispatch the close panel action
cx.defer_in(window, |_, window, cx| {
window.dispatch_action(Box::new(ClosePanel), cx);
window.close_all_modals(cx);
@@ -175,7 +174,6 @@ impl ChatSpace {
Self {
dock,
title_bar,
encryption_panel,
ready: false,
_subscriptions: subscriptions,
}
@@ -258,9 +256,9 @@ impl ChatSpace {
this.update_in(cx, |_, window, cx| {
match result {
Ok(profile) => {
Ok(person) => {
persons.update(cx, |this, cx| {
this.insert_or_update_person(profile, cx);
this.insert(person, cx);
// Close the edit profile modal
window.close_all_modals(cx);
});
@@ -447,11 +445,11 @@ impl ChatSpace {
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
let account = Account::global(cx);
let nostr = NostrRegistry::global(cx);
let chat = ChatRegistry::global(cx);
let status = chat.read(cx).loading;
let status = chat.read(cx).loading();
if !account.read(cx).has_account() {
if !nostr.read(cx).identity().read(cx).has_public_key() {
return div();
}
@@ -477,12 +475,13 @@ impl ChatSpace {
}
fn titlebar_right(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let auto_update = AutoUpdater::global(cx);
let account = Account::global(cx);
let relay_auth = RelayAuth::global(cx);
let pending_requests = relay_auth.read(cx).pending_requests(cx);
let encryption_panel = self.encryption_panel.downgrade();
let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
h_flex()
.gap_2()
@@ -542,15 +541,9 @@ impl ChatSpace {
}),
)
})
.when(account.read(cx).has_account(), |this| {
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
.when_some(identity.read(cx).public_key, |this, public_key| {
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let encryption = Encryption::global(cx);
let has_encryption = encryption.read(cx).has_encryption(cx);
let profile = persons.read(cx).get(&public_key, cx);
let keystore = KeyStore::global(cx);
let is_using_file_keystore = keystore.read(cx).is_using_file_keystore();
@@ -562,82 +555,38 @@ impl ChatSpace {
};
this.child(
h_flex()
.gap_1()
.child(
Popover::new("encryption")
.trigger(
Button::new("encryption-trigger")
.tooltip("Manage Encryption Key")
.icon(IconName::Encryption)
.rounded()
.small()
.cta()
.map(|this| match has_encryption {
true => this.ghost_alt(),
false => this.warning(),
}),
Button::new("user")
.small()
.reverse()
.transparent()
.icon(IconName::CaretDown)
.child(Avatar::new(profile.avatar()).size(rems(1.45)))
.popup_menu(move |this, _window, _cx| {
this.label(profile.name())
.menu_with_icon(
"Profile",
IconName::EmojiFill,
Box::new(ViewProfile),
)
.content(move |window, cx| {
let encryption_panel = encryption_panel.clone();
cx.new(|cx| {
PopoverContent::new(window, cx, move |_window, _cx| {
if let Some(view) = encryption_panel.upgrade() {
view.clone().into_any_element()
} else {
div().into_any_element()
}
})
})
}),
)
.child(
Button::new("user")
.small()
.reverse()
.transparent()
.icon(IconName::CaretDown)
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.45)))
.popup_menu(move |this, _window, _cx| {
this.label(profile.display_name())
.menu_with_icon(
"Profile",
IconName::EmojiFill,
Box::new(ViewProfile),
)
.menu_with_icon(
"Messaging Relays",
IconName::Server,
Box::new(ViewRelays),
)
.separator()
.label(SharedString::from("Keyring Service"))
.menu_with_icon_and_disabled(
keyring_label.clone(),
IconName::Encryption,
Box::new(KeyringPopup),
!is_using_file_keystore,
)
.separator()
.menu_with_icon(
"Dark Mode",
IconName::Sun,
Box::new(DarkMode),
)
.menu_with_icon("Themes", IconName::Moon, Box::new(Themes))
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Settings),
)
.menu_with_icon(
"Sign Out",
IconName::Logout,
Box::new(Logout),
)
}),
),
.menu_with_icon(
"Messaging Relays",
IconName::Server,
Box::new(ViewRelays),
)
.separator()
.label(SharedString::from("Keyring Service"))
.menu_with_icon_and_disabled(
keyring_label.clone(),
IconName::Encryption,
Box::new(KeyringPopup),
!is_using_file_keystore,
)
.separator()
.menu_with_icon("Dark Mode", IconName::Sun, Box::new(DarkMode))
.menu_with_icon("Themes", IconName::Moon, Box::new(Themes))
.menu_with_icon("Settings", IconName::Settings, Box::new(Settings))
.menu_with_icon("Sign Out", IconName::Logout, Box::new(Logout))
}),
)
})
}

View File

@@ -210,12 +210,10 @@ impl Login {
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
cx.background_spawn(async move {
client.set_signer(signer).await;
})
.detach();
nostr.update(cx, |this, cx| {
this.set_signer(signer, cx);
});
}
pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
@@ -260,10 +258,6 @@ impl Login {
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
let keystore = KeyStore::global(cx).read(cx).backend();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let username = keys.public_key().to_hex();
let secret = keys.secret_key().to_secret_hex().into_bytes();
@@ -281,11 +275,14 @@ impl Login {
.ok();
}
// Update the signer
cx.background_spawn(async move {
client.set_signer(keys).await;
this.update(cx, |_this, cx| {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.set_signer(keys, cx);
});
})
.detach();
.ok();
})
.detach();
}

View File

@@ -73,7 +73,6 @@ fn main() {
// Bring the app to the foreground
cx.activate(true);
// Root Entity
cx.new(|cx| {
// Initialize the tokio runtime
gpui_tokio::init(cx);
@@ -90,27 +89,27 @@ fn main() {
// Initialize the nostr client
state::init(cx);
// Initialize person registry
person::init(cx);
// Initialize device signer
//
// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md
device::init(cx);
// Initialize settings
settings::init(cx);
// Initialize account state
account::init(cx);
// Initialize encryption state
encryption::init(cx);
// Initialize relay auth registry
relay_auth::init(window, cx);
// Initialize app registry
chat::init(cx);
// Initialize relay auth registry
relay_auth::init(window, cx);
// Initialize person registry
person::init(cx);
// Initialize auto update
auto_update::init(cx);
// Root Entity
Root::new(chatspace::init(window, cx).into(), window, cx)
})
})

View File

@@ -3,8 +3,8 @@ use std::time::Duration;
use anyhow::{anyhow, Error};
use common::home_dir;
use gpui::{
div, App, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement,
Render, SharedString, Styled, Task, Window,
div, App, AppContext, ClipboardItem, Context, Entity, IntoElement, ParentElement, Render,
SharedString, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
@@ -60,7 +60,7 @@ impl Backup {
let nsec = self.secret_input.read(cx).value().to_string();
cx.spawn_in(window, async move |this, cx| {
match Flatten::flatten(path.await.map_err(|e| e.into())) {
match path.await {
Ok(Ok(Some(path))) => {
if let Err(e) = smol::fs::write(&path, nsec).await {
this.update_in(cx, |this, window, cx| {

View File

@@ -1,9 +1,8 @@
use anyhow::{anyhow, Error};
use common::{default_nip17_relays, default_nip65_relays, nip96_upload, BOOTSTRAP_RELAYS};
use gpui::{
rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten, FocusHandle,
Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task,
Window,
rems, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, Window,
};
use gpui_tokio::Tokio;
use key_store::{KeyItem, KeyStore};
@@ -221,8 +220,8 @@ impl NewAccount {
});
let task = Tokio::spawn(cx, async move {
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => {
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?;
@@ -232,13 +231,12 @@ impl NewAccount {
Err(anyhow!("Path not found"))
}
}
Ok(None) => Err(anyhow!("User cancelled")),
Err(e) => Err(anyhow!("File dialog error: {e}")),
_ => Err(anyhow!("Error")),
}
});
cx.spawn_in(window, async move |this, cx| {
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
let result = task.await;
this.update_in(cx, |this, window, cx| {
match result {

View File

@@ -1,20 +1,18 @@
use std::ops::Range;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
use common::{DebouncedDelay, RenderedTimestamp, TextUtils, BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
deferred, div, relative, uniform_list, App, AppContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
RetainAllImageCache, SharedString, Styled, Subscription, Task, Window,
};
use gpui_tokio::Tokio;
use list_item::RoomListItem;
use nostr_sdk::prelude::*;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::{NostrRegistry, GIFTWRAP_SUBSCRIPTION};
use theme::ActiveTheme;
@@ -35,6 +33,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
cx.new(|cx| Sidebar::new(window, cx))
}
/// Sidebar.
pub struct Sidebar {
name: SharedString,
@@ -50,73 +49,75 @@ pub struct Sidebar {
/// Async search operation
search_task: Option<Task<()>>,
/// Search input state
find_input: Entity<InputState>,
/// Debounced delay for search input
find_debouncer: DebouncedDelay<Self>,
/// Whether searching is in progress
finding: bool,
indicator: Entity<Option<RoomKind>>,
/// New request flag
new_request: bool,
/// Current chat room filter
active_filter: Entity<RoomKind>,
/// Event subscriptions
_subscriptions: SmallVec<[Subscription; 3]>,
_subscriptions: SmallVec<[Subscription; 2]>,
}
impl Sidebar {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let active_filter = cx.new(|_| RoomKind::Ongoing);
let indicator = cx.new(|_| None);
let search_results = cx.new(|_| None);
let find_input =
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
// Define the find input state
let find_input = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Find or start a conversation")
.clean_on_escape()
});
// Get the chat registry
let chat = ChatRegistry::global(cx);
let mut subscriptions = smallvec![];
subscriptions.push(
// Clear the image cache when sidebar is closed
cx.on_release_in(window, move |this, window, cx| {
this.image_cache.update(cx, |this, cx| {
this.clear(window, cx);
})
}),
);
subscriptions.push(
// Subscribe for registry new events
cx.subscribe_in(&chat, window, move |this, _, event, _window, cx| {
if let ChatEvent::NewChatRequest(kind) = event {
this.indicator.update(cx, |this, cx| {
*this = Some(kind.to_owned());
cx.notify();
});
}
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
if event == &ChatEvent::Ping {
this.new_request = true;
cx.notify();
};
}),
);
subscriptions.push(
// Subscribe for find input events
cx.subscribe_in(&find_input, window, |this, state, event, window, cx| {
let delay = Duration::from_millis(FIND_DELAY);
match event {
InputEvent::PressEnter { .. } => {
this.search(window, cx);
}
InputEvent::Change => {
// Clear the result when input is empty
if state.read(cx).value().is_empty() {
// Clear the result when input is empty
this.clear(window, cx);
} else {
// Run debounced search
this.find_debouncer.fire_new(
Duration::from_millis(FIND_DELAY),
window,
cx,
|this, window, cx| this.debounced_search(window, cx),
);
this.find_debouncer
.fire_new(delay, window, cx, |this, window, cx| {
this.debounced_search(window, cx)
});
}
}
_ => {}
}
};
}),
);
@@ -126,7 +127,7 @@ impl Sidebar {
image_cache: RetainAllImageCache::new(cx),
find_debouncer: DebouncedDelay::new(),
finding: false,
indicator,
new_request: false,
active_filter,
find_input,
search_results,
@@ -199,11 +200,9 @@ impl Sidebar {
}
fn search_by_nip50(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let query = query.to_owned();
@@ -387,13 +386,13 @@ impl Sidebar {
}
fn set_finding(&mut self, status: bool, _window: &mut Window, cx: &mut Context<Self>) {
self.finding = status;
// Disable the input to prevent duplicate requests
self.find_input.update(cx, |this, cx| {
this.set_disabled(status, cx);
this.set_loading(status, cx);
});
// Set the finding status
self.finding = status;
cx.notify();
}
@@ -415,47 +414,46 @@ impl Sidebar {
}
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
self.indicator.update(cx, |this, cx| {
*this = None;
cx.notify();
});
self.active_filter.update(cx, |this, cx| {
*this = kind;
cx.notify();
});
self.new_request = false;
cx.notify();
}
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
fn open(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let room = if let Some(room) = chat.read(cx).room(&id, cx) {
room
} else {
let Some(result) = self.search_results.read(cx).as_ref() else {
window.push_notification("Failed to open room. Please try again later.", cx);
return;
};
let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() else {
window.push_notification("Failed to open room. Please try again later.", cx);
return;
};
// Clear all search results
self.clear(window, cx);
room
};
chat.update(cx, |this, cx| {
this.push_room(room, cx);
});
match chat.read(cx).room(&id, cx) {
Some(room) => {
chat.update(cx, |this, cx| {
this.emit_room(room, cx);
});
}
None => {
if let Some(room) = self
.search_results
.read(cx)
.as_ref()
.and_then(|results| results.iter().find(|this| this.read(cx).id == id))
.map(|this| this.downgrade())
{
chat.update(cx, |this, cx| {
this.emit_room(room, cx);
});
// Clear all search results
self.clear(window, cx);
}
}
}
}
fn on_reload(&mut self, _ev: &Reload, window: &mut Window, cx: &mut Context<Self>) {
ChatRegistry::global(cx).update(cx, |this, cx| {
this.get_rooms(cx);
});
window.push_notification("Refreshed", cx);
window.push_notification("Reload", cx);
}
fn on_manage(&mut self, _ev: &RelayStatus, window: &mut Window, cx: &mut Context<Self>) {
@@ -541,7 +539,6 @@ impl Sidebar {
range: Range<usize>,
cx: &Context<Self>,
) -> Vec<impl IntoElement> {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let mut items = Vec::with_capacity(range.end - range.start);
for ix in range {
@@ -556,7 +553,7 @@ impl Sidebar {
let handler = cx.listener({
move |this, _, window, cx| {
this.open_room(room_id, window, cx);
this.open(room_id, window, cx);
}
});
@@ -564,7 +561,7 @@ impl Sidebar {
RoomListItem::new(ix)
.room_id(room_id)
.name(this.display_name(cx))
.avatar(this.display_image(proxy, cx))
.avatar(this.display_image(cx))
.public_key(member.public_key())
.kind(this.kind)
.created_at(this.created_at.to_ago())
@@ -580,10 +577,6 @@ impl Panel for Sidebar {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
self.name.clone().into_any_element()
}
}
impl EventEmitter<PanelEvent> for Sidebar {}
@@ -597,7 +590,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading;
let loading = chat.read(cx).loading();
// Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.search_results.read(cx).as_ref() {
@@ -675,13 +668,6 @@ impl Render for Sidebar {
Button::new("all")
.label("All")
.tooltip("All ongoing conversations")
.when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind == &RoomKind::Ongoing, |this| {
this.child(
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
})
.small()
.cta()
.bold()
@@ -696,12 +682,10 @@ impl Render for Sidebar {
Button::new("requests")
.label("Requests")
.tooltip("Incoming new conversations")
.when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind != &RoomKind::Ongoing, |this| {
this.child(
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
.when(self.new_request, |this| {
this.child(
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
.small()
.cta()

View File

@@ -5,11 +5,12 @@ 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,
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;
@@ -193,8 +194,8 @@ impl UserProfile {
});
let task = Tokio::spawn(cx, async move {
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
Ok(Some(mut paths)) => {
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?;
@@ -204,13 +205,12 @@ impl UserProfile {
Err(anyhow!("Path not found"))
}
}
Ok(None) => Err(anyhow!("User cancelled")),
Err(e) => Err(anyhow!("File dialog error: {e}")),
_ => Err(anyhow!("Error")),
}
});
cx.spawn_in(window, async move |this, cx| {
let result = Flatten::flatten(task.await.map_err(|e| e.into()));
let result = task.await;
this.update_in(cx, |this, window, cx| {
match result {
@@ -233,7 +233,7 @@ impl UserProfile {
.detach();
}
pub fn set_metadata(&mut self, cx: &mut Context<Self>) -> Task<Result<Profile, Error>> {
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();
@@ -259,27 +259,22 @@ impl UserProfile {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
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?;
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?;
client.send_event_to(urls, &event).await?;
// Return the updated profile
let metadata = Metadata::from_json(&event.content).unwrap_or_default();
let profile = Profile::new(event.pubkey, metadata);
let profile = Person::new(event.pubkey, metadata);
Ok(profile)
})

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use common::{nip05_verify, shorten_pubkey, RenderedProfile};
use common::{nip05_verify, shorten_pubkey};
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, rems, App, AppContext, ClipboardItem, Context, Entity, IntoElement,
@@ -8,8 +8,7 @@ use gpui::{
};
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use person::{Person, PersonRegistry};
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::ActiveTheme;
@@ -23,7 +22,7 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
#[derive(Debug)]
pub struct ProfileViewer {
profile: Profile,
profile: Person,
/// Follow status
followed: bool,
@@ -44,7 +43,7 @@ impl ProfileViewer {
let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&target, cx);
let profile = persons.read(cx).get(&target, cx);
let mut tasks = smallvec![];
@@ -134,7 +133,6 @@ impl ProfileViewer {
impl Render for ProfileViewer {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let bech32 = shorten_pubkey(self.profile.public_key(), 16);
let shared_bech32 = SharedString::from(bech32);
@@ -147,14 +145,14 @@ impl Render for ProfileViewer {
.items_center()
.justify_center()
.text_center()
.child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.)))
.child(Avatar::new(self.profile.avatar()).size(rems(4.)))
.child(
v_flex()
.child(
div()
.font_semibold()
.line_height(relative(1.25))
.child(self.profile.display_name()),
.child(self.profile.name()),
)
.when_some(self.address(cx), |this, address| {
this.child(

View File

@@ -1,10 +1,9 @@
use std::ops::Range;
use std::time::Duration;
use account::Account;
use anyhow::{anyhow, Error};
use chat::{ChatRegistry, Room};
use common::{nip05_profile, RenderedProfile, TextUtils, BOOTSTRAP_RELAYS};
use common::{nip05_profile, TextUtils, BOOTSTRAP_RELAYS};
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, relative, rems, uniform_list, App, AppContext, Context, Entity, InteractiveElement,
@@ -14,7 +13,6 @@ use gpui::{
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::ActiveTheme;
@@ -312,9 +310,8 @@ impl Compose {
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx);
let account = Account::global(cx);
let public_key = account.read(cx).public_key();
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let receivers: Vec<PublicKey> = self.selected(cx);
let subject_input = self.title_input.read(cx).value();
@@ -326,7 +323,8 @@ impl Compose {
};
chat.update(cx, |this, cx| {
this.push_room(cx.new(|_| Room::new(subject, public_key, receivers)), cx);
let room = cx.new(|_| Room::new(subject, public_key, receivers));
this.emit_room(room.downgrade(), cx);
});
window.close_modal(cx);
@@ -360,7 +358,6 @@ impl Compose {
}
fn list_items(&self, range: Range<usize>, cx: &Context<Self>) -> Vec<impl IntoElement> {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let persons = PersonRegistry::global(cx);
let mut items = Vec::with_capacity(self.contacts.read(cx).len());
@@ -370,7 +367,7 @@ impl Compose {
};
let public_key = contact.public_key;
let profile = persons.read(cx).get_person(&public_key, cx);
let profile = persons.read(cx).get(&public_key, cx);
items.push(
h_flex()
@@ -384,8 +381,8 @@ impl Compose {
h_flex()
.gap_1p5()
.text_sm()
.child(Avatar::new(profile.avatar(proxy)).size(rems(1.75)))
.child(profile.display_name()),
.child(Avatar::new(profile.avatar()).size(rems(1.75)))
.child(profile.name()),
)
.when(contact.selected, |this| {
this.child(

View File

@@ -8,8 +8,7 @@ use gpui::{
};
use gpui_tokio::Tokio;
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use person::{Person, PersonRegistry};
use smallvec::{smallvec, SmallVec};
use state::NostrRegistry;
use theme::ActiveTheme;
@@ -23,7 +22,7 @@ pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<
}
pub struct Screening {
profile: Profile,
profile: Person,
verified: bool,
followed: bool,
last_active: Option<Timestamp>,
@@ -37,7 +36,7 @@ impl Screening {
let client = nostr.read(cx).client();
let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get_person(&public_key, cx);
let profile = persons.read(cx).get(&public_key, cx);
let mut tasks = smallvec![];
@@ -225,7 +224,6 @@ impl Screening {
impl Render for Screening {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx);
let shorten_pubkey = shorten_pubkey(self.profile.public_key(), 8);
let total_mutuals = self.mutual_contacts.len();
let last_active = self.last_active.map(|_| true);
@@ -238,12 +236,12 @@ impl Render for Screening {
.items_center()
.justify_center()
.text_center()
.child(Avatar::new(self.profile.avatar(proxy)).size(rems(4.)))
.child(Avatar::new(self.profile.avatar()).size(rems(4.)))
.child(
div()
.font_semibold()
.line_height(relative(1.25))
.child(self.profile.display_name()),
.child(self.profile.name()),
),
)
.child(

View File

@@ -158,17 +158,14 @@ impl SetupRelay {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let gossip = nostr.read(cx).gossip();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let relays = self.relays.clone();
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
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;
let tags: Vec<Tag> = relays
.iter()
@@ -181,7 +178,7 @@ impl SetupRelay {
.await?;
// Set messaging relays
client.send_event_to(write_relays, &event).await?;
client.send_event_to(urls, &event).await?;
// Connect to messaging relays
for relay in relays.iter() {

View File

@@ -1,6 +1,6 @@
use std::time::Duration;
use common::{RenderedProfile, BUNKER_TIMEOUT};
use common::BUNKER_TIMEOUT;
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
@@ -29,11 +29,16 @@ pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity<Startu
/// Startup
#[derive(Debug)]
pub struct Startup {
credential: Credential,
loading: bool,
name: SharedString,
focus_handle: FocusHandle,
/// Local user credentials
credential: Credential,
/// Whether the loadng is in progress
loading: bool,
/// Image cache
image_cache: Entity<RetainAllImageCache>,
/// Event subscriptions
@@ -164,15 +169,12 @@ impl Startup {
}
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let keys = Keys::new(secret);
let nostr = NostrRegistry::global(cx);
// Update the signer
cx.background_spawn(async move {
client.set_signer(keys).await;
nostr.update(cx, |this, cx| {
this.set_signer(keys, cx);
})
.detach();
}
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
@@ -203,9 +205,7 @@ impl Render for Startup {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let persons = PersonRegistry::global(cx);
let bunker = self.credential.secret().starts_with("bunker://");
let profile = persons
.read(cx)
.get_person(&self.credential.public_key(), cx);
let profile = persons.read(cx).get(&self.credential.public_key(), cx);
v_flex()
.image_cache(self.image_cache.clone())
@@ -266,8 +266,8 @@ impl Render for Startup {
)
})
.when(!self.loading, |this| {
let avatar = profile.avatar(true);
let name = profile.display_name();
let avatar = profile.avatar();
let name = profile.name();
this.child(
h_flex()