refactor: Client keys and Identity (#61)
* . * . * . * . * refactor client keys * . * . * refactor * . * . * . * update new account
This commit is contained in:
@@ -10,11 +10,13 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
ui = { path = "../ui" }
|
||||
identity = { path = "../identity" }
|
||||
theme = { path = "../theme" }
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
chats = { path = "../chats" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
gpui.workspace = true
|
||||
@@ -35,5 +37,4 @@ smol.workspace = true
|
||||
futures.workspace = true
|
||||
oneshot.workspace = true
|
||||
|
||||
webbrowser = "1.0.4"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
|
||||
@@ -2,13 +2,15 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::{ChatRegistry, RoomEmitter};
|
||||
use client_keys::ClientKeys;
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, IntoElement,
|
||||
div, impl_internal_actions, px, relative, App, AppContext, Axis, Context, Entity, IntoElement,
|
||||
ParentElement, Render, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -17,7 +19,8 @@ use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::dock::DockPlacement;
|
||||
use ui::dock_area::panel::PanelView;
|
||||
use ui::dock_area::{DockArea, DockItem};
|
||||
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{ContextModal, IconName, Root, Sizable, StyledExt, TitleBar};
|
||||
|
||||
use crate::views::chat::{self, Chat};
|
||||
use crate::views::{login, new_account, onboarding, preferences, sidebar, startup, welcome};
|
||||
@@ -62,7 +65,7 @@ pub struct ChatSpace {
|
||||
dock: Entity<DockArea>,
|
||||
titlebar: bool,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
subscriptions: SmallVec<[Subscription; 4]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
@@ -78,8 +81,81 @@ impl ChatSpace {
|
||||
|
||||
cx.new(|cx| {
|
||||
let chats = ChatRegistry::global(cx);
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let identity = Identity::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
// Observe the client keys and show an alert modal if they fail to initialize
|
||||
subscriptions.push(cx.observe_in(
|
||||
&client_keys,
|
||||
window,
|
||||
|_this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_keys() {
|
||||
window.open_modal(cx, |this, _window, cx| {
|
||||
const DESCRIPTION: &str =
|
||||
"Allow Coop to read the client keys stored in Keychain to continue";
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text("Create New Keys")
|
||||
.ok_text("Allow"),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_10()
|
||||
.w_full()
|
||||
.h_40()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Warning"),
|
||||
)
|
||||
.child(div().line_height(relative(1.4)).child(DESCRIPTION)),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
this.new_keys(cx);
|
||||
});
|
||||
// true: Close modal
|
||||
true
|
||||
})
|
||||
.on_ok(|_, window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
// true: Close modal
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Observe the identity and show onboarding if it fails to initialize
|
||||
subscriptions.push(cx.observe_in(
|
||||
&identity,
|
||||
window,
|
||||
|this: &mut Self, state, window, cx| {
|
||||
if !state.read(cx).has_profile() {
|
||||
this.open_onboarding(window, cx);
|
||||
} else {
|
||||
this.open_chats(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Automatically load messages when chat panel opens
|
||||
subscriptions.push(cx.observe_new::<Chat>(|this: &mut Chat, window, cx| {
|
||||
if let Some(window) = window {
|
||||
@@ -91,7 +167,7 @@ impl ChatSpace {
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&chats,
|
||||
window,
|
||||
|this: &mut ChatSpace, _state, event, window, cx| {
|
||||
|this: &mut Self, _state, event, window, cx| {
|
||||
if let RoomEmitter::Open(room) = event {
|
||||
if let Some(room) = room.upgrade() {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
@@ -223,11 +299,10 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
fn logout(&self, _window: &mut Window, cx: &mut App) {
|
||||
cx.background_spawn(async move {
|
||||
shared_state().unset_signer().await;
|
||||
})
|
||||
.detach();
|
||||
fn logout(&self, window: &mut Window, cx: &mut App) {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use asset::Assets;
|
||||
use auto_update::AutoUpdater;
|
||||
use chats::ChatRegistry;
|
||||
use global::constants::APP_ID;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use global::constants::APP_NAME;
|
||||
use global::constants::{APP_ID, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::{shared_state, NostrSignal};
|
||||
use gpui::{
|
||||
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
@@ -17,7 +15,6 @@ use gpui::{
|
||||
use gpui::{point, SharedString, TitlebarOptions};
|
||||
#[cfg(target_os = "linux")]
|
||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||
use nostr_connect::prelude::*;
|
||||
use theme::Theme;
|
||||
use ui::Root;
|
||||
|
||||
@@ -94,6 +91,10 @@ fn main() {
|
||||
ui::init(cx);
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
// Initialize client keys
|
||||
client_keys::init(cx);
|
||||
// Initialize identity
|
||||
identity::init(window, cx);
|
||||
// Initialize auto update
|
||||
auto_update::init(cx);
|
||||
// Initialize chat state
|
||||
@@ -102,42 +103,6 @@ fn main() {
|
||||
// Initialize chatspace (or workspace)
|
||||
let chatspace = chatspace::init(window, cx);
|
||||
let async_chatspace = chatspace.downgrade();
|
||||
let async_chatspace_clone = async_chatspace.clone();
|
||||
|
||||
// Read user's credential
|
||||
let read_credential = cx.read_credentials(KEYRING_USER_PATH);
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if let Ok(Some((user, secret))) = read_credential.await {
|
||||
cx.update(|window, cx| {
|
||||
if let Ok(signer) = extract_credential(&user, secret) {
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("Signer error: {}", e);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Spawn a task to handle events from nostr channel
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
@@ -148,14 +113,14 @@ fn main() {
|
||||
|
||||
match signal {
|
||||
NostrSignal::SignerUpdated => {
|
||||
async_chatspace_clone
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_chats(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
NostrSignal::SignerUnset => {
|
||||
async_chatspace_clone
|
||||
async_chatspace
|
||||
.update(cx, |this, cx| {
|
||||
this.open_onboarding(window, cx);
|
||||
})
|
||||
@@ -190,22 +155,6 @@ fn main() {
|
||||
});
|
||||
}
|
||||
|
||||
fn extract_credential(user: &str, secret: Vec<u8>) -> Result<impl NostrSigner, Error> {
|
||||
if user == KEYRING_BUNKER {
|
||||
let value = String::from_utf8(secret)?;
|
||||
let uri = NostrConnectURI::parse(value)?;
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let signer = NostrConnect::new(uri, client_keys, Duration::from_secs(300), None)?;
|
||||
|
||||
Ok(signer.into_nostr_signer())
|
||||
} else {
|
||||
let secret_key = SecretKey::from_slice(&secret)?;
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
Ok(keys.into_nostr_signer())
|
||||
}
|
||||
}
|
||||
|
||||
fn quit(_: &Quit, cx: &mut App) {
|
||||
log::info!("Gracefully quitting the application . . .");
|
||||
cx.quit();
|
||||
|
||||
@@ -17,6 +17,7 @@ use gpui::{
|
||||
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::Deserialize;
|
||||
@@ -220,7 +221,7 @@ impl Chat {
|
||||
|
||||
// TODO: find a better way to prevent duplicate messages during optimistic updates
|
||||
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
|
||||
let Some(account) = shared_state().identity() else {
|
||||
let Some(account) = Identity::get_global(cx).profile() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -372,7 +373,7 @@ impl Chat {
|
||||
|
||||
self.uploading(true, cx);
|
||||
|
||||
let nip96 = AppSettings::get_global(cx).settings().media_server.clone();
|
||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
@@ -546,8 +547,8 @@ impl Chat {
|
||||
return div().id(ix);
|
||||
};
|
||||
|
||||
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||
let hide_avatar = AppSettings::get_global(cx).settings().hide_user_avatars;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||
|
||||
let message = message.borrow();
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ impl Render for Compose {
|
||||
const DESCRIPTION: &str =
|
||||
"Start a conversation with someone using their npub or NIP-05 (like foo@bar.com).";
|
||||
|
||||
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
|
||||
let label: SharedString = if self.selected.read(cx).len() > 1 {
|
||||
"Create Group DM".into()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use client_keys::ClientKeys;
|
||||
use common::handle_auth::CoopAuthUrlHandler;
|
||||
use common::string_to_qr;
|
||||
use global::constants::{APP_NAME, KEYRING_BUNKER, KEYRING_USER_PATH};
|
||||
use global::shared_state;
|
||||
use global::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, red, relative, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
@@ -20,21 +22,6 @@ use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{ContextModal, Disableable, Sizable, StyledExt};
|
||||
|
||||
const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
||||
const NOSTR_CONNECT_TIMEOUT: u64 = 300;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CoopAuthUrlHandler;
|
||||
|
||||
impl AuthUrlHandler for CoopAuthUrlHandler {
|
||||
fn on_auth_url(&self, auth_url: Url) -> BoxedFuture<Result<()>> {
|
||||
Box::pin(async move {
|
||||
webbrowser::open(auth_url.as_str())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
Login::new(window, cx)
|
||||
}
|
||||
@@ -72,9 +59,9 @@ impl Login {
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
// Direct connection initiated by the client
|
||||
let connection_string = cx.new(|_cx| {
|
||||
let connection_string = cx.new(|cx| {
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
|
||||
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
|
||||
});
|
||||
@@ -90,6 +77,7 @@ impl Login {
|
||||
let error = cx.new(|_| None);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
// Subscribe to key input events and process login when the user presses enter
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&key_input, window, |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
@@ -98,6 +86,7 @@ impl Login {
|
||||
}),
|
||||
);
|
||||
|
||||
// Subscribe to relay input events and change relay when the user presses enter
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&relay_input, window, |this, _, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
@@ -106,41 +95,38 @@ impl Login {
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.observe_new::<NostrConnectURI>(
|
||||
move |connection_string, _window, cx| {
|
||||
if let Ok(mut signer) = NostrConnect::new(
|
||||
connection_string.to_owned(),
|
||||
shared_state().client_signer.clone(),
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
) {
|
||||
// Automatically open remote signer's webpage when received auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
// Observe the Connect URI that changes when the relay is changed
|
||||
subscriptions.push(cx.observe_new::<NostrConnectURI>(move |uri, _window, cx| {
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
|
||||
async_active_signer
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
if let Ok(mut signer) = NostrConnect::new(uri.to_owned(), client_keys, timeout, None) {
|
||||
// Automatically open auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
// Update the QR Image with the new connection string
|
||||
async_qr_image
|
||||
async_active_signer
|
||||
.update(cx, |this, cx| {
|
||||
*this = string_to_qr(&connection_string.to_string());
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
// Update the QR Image with the new connection string
|
||||
async_qr_image
|
||||
.update(cx, |this, cx| {
|
||||
*this = string_to_qr(&uri.to_string());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
|
||||
subscriptions.push(cx.observe_in(
|
||||
&connection_string,
|
||||
window,
|
||||
|this, entity, _window, cx| {
|
||||
let connection_string = entity.read(cx).clone();
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
|
||||
// Update the QR Image with the new connection string
|
||||
this.qr_image.update(cx, |this, cx| {
|
||||
@@ -148,58 +134,35 @@ impl Login {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
if let Ok(mut signer) = NostrConnect::new(
|
||||
match NostrConnect::new(
|
||||
connection_string,
|
||||
client_keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
) {
|
||||
// Automatically open remote signer's webpage when received auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
Ok(mut signer) => {
|
||||
// Automatically open auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
this.active_signer.update(cx, |this, cx| {
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
});
|
||||
this.active_signer.update(cx, |this, cx| {
|
||||
*this = Some(signer);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
log::error!("Failed to create Nostr Connect")
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
subscriptions.push(
|
||||
cx.observe_in(&active_signer, window, |_this, entity, window, cx| {
|
||||
if let Some(signer) = entity.read(cx).clone() {
|
||||
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some(bunker_uri)).ok();
|
||||
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(uri)) = rx.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.save_bunker(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(
|
||||
Notification::error("Connection failed"),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.observe_in(&active_signer, window, |this, entity, window, cx| {
|
||||
if let Some(mut signer) = entity.read(cx).clone() {
|
||||
// Automatically open auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
// Wait for connection from remote signer
|
||||
this.wait_for_connection(signer, window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -222,90 +185,212 @@ impl Login {
|
||||
if self.is_logging_in {
|
||||
return;
|
||||
};
|
||||
// Prevent duplicate login requests
|
||||
self.set_logging_in(true, cx);
|
||||
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let content = self.key_input.read(cx).value();
|
||||
|
||||
if content.starts_with("nsec1") {
|
||||
let Ok(keys) = SecretKey::parse(content.as_ref()).map(Keys::new) else {
|
||||
self.set_error("Secret key is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
// Save these keys to the OS storage for further logins
|
||||
self.save_keys(&keys, cx);
|
||||
|
||||
// Set signer with this keys in the background
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = shared_state().set_signer(keys).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
} else if content.starts_with("bunker://") {
|
||||
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
|
||||
self.set_error("Bunker URL is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
match NostrConnect::new(
|
||||
uri.clone(),
|
||||
client_keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2),
|
||||
None,
|
||||
) {
|
||||
Ok(signer) => {
|
||||
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
|
||||
|
||||
// Set signer with this remote signer in the background
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some(bunker_uri)).ok();
|
||||
|
||||
if let Err(e) = shared_state().set_signer(signer).await {
|
||||
log::error!("{}", e);
|
||||
}
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
// Handle error
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some(uri)) = rx.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.save_bunker(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(
|
||||
"Connection to the Remote Signer failed or timed out",
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
Err(e) => {
|
||||
self.set_error(e.to_string(), cx);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.set_error("You must provide a valid Private Key or Bunker.", cx);
|
||||
// Content can be secret key or bunker://
|
||||
match self.key_input.read(cx).value().to_string() {
|
||||
s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx),
|
||||
s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx),
|
||||
s if s.starts_with("bunker://") => self.login_with_bunker(&s, window, cx),
|
||||
_ => self.set_error("You must provide a valid Private Key or Bunker.", cx),
|
||||
};
|
||||
}
|
||||
|
||||
fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let current_view = cx.entity().downgrade();
|
||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let weak_input = weak_input.clone();
|
||||
let view_cancel = current_view.clone();
|
||||
let view_ok = current_view.clone();
|
||||
|
||||
let label: SharedString = if content.starts_with("nsec1") {
|
||||
"Set password to encrypt your key *".into()
|
||||
} else {
|
||||
"Password to decrypt your key *".into()
|
||||
};
|
||||
|
||||
let description: Option<SharedString> = if content.starts_with("ncryptsec1") {
|
||||
Some("Coop will only stored the encrypted version".into())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, _window, cx| {
|
||||
view_cancel
|
||||
.update(cx, |this, cx| {
|
||||
this.set_error("Password is required", cx);
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
let value = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
view_ok
|
||||
.update(cx, |this, cx| {
|
||||
if let Some(password) = value {
|
||||
this.login_with_keys(password.to_string(), window, cx);
|
||||
} else {
|
||||
this.set_error("Password is required", cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.pt_4()
|
||||
.px_4()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(label)
|
||||
.child(TextInput::new(&pwd_input).small())
|
||||
.when_some(description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(description),
|
||||
)
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.key_input.read(cx).value().to_string();
|
||||
let secret_key = if value.starts_with("nsec1") {
|
||||
SecretKey::parse(&value).ok()
|
||||
} else if value.starts_with("ncryptsec1") {
|
||||
EncryptedSecretKey::from_bech32(&value)
|
||||
.map(|enc| enc.decrypt(&password).ok())
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(secret_key) = secret_key {
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.write_keys(&keys, password, cx);
|
||||
this.set_signer(keys, window, cx);
|
||||
});
|
||||
} else {
|
||||
self.set_error("Secret Key is invalid", cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(uri) = NostrConnectURI::parse(content) else {
|
||||
self.set_error("Bunker URL is not valid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2);
|
||||
|
||||
let Ok(mut signer) = NostrConnect::new(uri, client_keys, timeout, None) else {
|
||||
self.set_error("Failed to create remote signer", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Active signer is no longer needed
|
||||
self.shutdown_active_signer(cx);
|
||||
|
||||
// Automatically open auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
let (tx, rx) = oneshot::channel::<Option<(NostrConnect, NostrConnectURI)>>();
|
||||
|
||||
// Verify remote signer connection
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some((signer, bunker_uri))).ok();
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some((signer, uri))) = rx.await {
|
||||
cx.update(|window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.write_bunker(&uri, cx);
|
||||
this.set_signer(signer, window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
let msg = "Connection to the Remote Signer failed or timed out";
|
||||
this.set_error(msg, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn wait_for_connection(
|
||||
&mut self,
|
||||
signer: NostrConnect,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let (tx, rx) = oneshot::channel::<Option<(NostrConnectURI, NostrConnect)>>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(bunker_uri) = signer.bunker_uri().await {
|
||||
tx.send(Some((bunker_uri, signer))).ok();
|
||||
} else {
|
||||
tx.send(None).ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(Some((uri, signer))) = rx.await {
|
||||
cx.update(|window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.write_bunker(&uri, cx);
|
||||
this.set_signer(signer, window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(Notification::error("Connection failed"), cx);
|
||||
// Refresh the active signer
|
||||
this.update(cx, |this, cx| {
|
||||
this.change_relay(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn change_relay(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(relay_url) = RelayUrl::parse(self.relay_input.read(cx).value().to_string().as_str())
|
||||
else {
|
||||
@@ -313,7 +398,7 @@ impl Login {
|
||||
return;
|
||||
};
|
||||
|
||||
let client_keys = shared_state().client_signer.clone();
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
|
||||
|
||||
self.connection_string.update(cx, |this, cx| {
|
||||
@@ -322,40 +407,6 @@ impl Login {
|
||||
});
|
||||
}
|
||||
|
||||
fn save_keys(&self, keys: &Keys, cx: &mut Context<Self>) {
|
||||
let save_credential = cx.write_credentials(
|
||||
KEYRING_USER_PATH,
|
||||
keys.public_key().to_hex().as_str(),
|
||||
keys.secret_key().as_secret_bytes(),
|
||||
);
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save keys: {}", e)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn save_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
||||
let mut value = uri.to_string();
|
||||
|
||||
// Remove the secret param if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
value = value.replace(secret, "");
|
||||
}
|
||||
|
||||
let save_credential =
|
||||
cx.write_credentials(KEYRING_USER_PATH, KEYRING_BUNKER, value.as_bytes());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save the Bunker URI: {}", e)
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn shutdown_active_signer(&self, cx: &Context<Self>) {
|
||||
if let Some(signer) = self.active_signer.read(cx).clone() {
|
||||
cx.background_spawn(async move {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use async_utility::task::spawn;
|
||||
use common::nip96_upload;
|
||||
use global::constants::KEYRING_USER_PATH;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -8,6 +7,7 @@ use gpui::{
|
||||
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
|
||||
Styled, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
@@ -16,7 +16,7 @@ use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
NewAccount::new(window, cx)
|
||||
@@ -65,37 +65,71 @@ impl NewAccount {
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_submitting(true, cx);
|
||||
|
||||
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 keys = Keys::generate();
|
||||
let mut metadata = Metadata::new().display_name(name).about(bio);
|
||||
|
||||
if let Ok(url) = Url::parse(&avatar) {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
let save_credential = cx.write_credentials(
|
||||
KEYRING_USER_PATH,
|
||||
keys.public_key().to_hex().as_str(),
|
||||
keys.secret_key().as_secret_bytes(),
|
||||
);
|
||||
let current_view = cx.entity().downgrade();
|
||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Err(e) = save_credential.await {
|
||||
log::error!("Failed to save keys: {}", e)
|
||||
};
|
||||
shared_state().new_account(keys, metadata).await;
|
||||
})
|
||||
.detach();
|
||||
window.open_modal(cx, move |this, _window, _cx| {
|
||||
let metadata = metadata.clone();
|
||||
let weak_input = weak_input.clone();
|
||||
let view_cancel = current_view.clone();
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, window, cx| {
|
||||
view_cancel
|
||||
.update(cx, |_this, cx| {
|
||||
window.push_notification("Password is invalid", cx)
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, _window, cx| {
|
||||
let metadata = metadata.clone();
|
||||
let value = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
if let Some(password) = value {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.new_identity(Keys::generate(), password.to_string(), metadata, cx);
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.pt_4()
|
||||
.px_4()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child("Set password to encrypt your key *")
|
||||
.child(TextInput::new(&pwd_input).small()),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nip96 = AppSettings::get_global(cx).settings().media_server.clone();
|
||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
use anyhow::anyhow;
|
||||
use common::profile::RenderProfile;
|
||||
use global::constants::ACCOUNT_D;
|
||||
use global::shared_state;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Icon, IconName, StyledExt};
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
@@ -16,6 +29,8 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
|
||||
pub struct Onboarding {
|
||||
name: SharedString,
|
||||
local_account: Entity<Option<Profile>>,
|
||||
loading: bool,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -26,14 +41,73 @@ impl Onboarding {
|
||||
cx.new(|cx| Self::view(window, cx))
|
||||
}
|
||||
|
||||
fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let local_account = cx.new(|_| None);
|
||||
|
||||
let task = cx.background_spawn(async move {
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_D)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
.first_owned()
|
||||
{
|
||||
let public_key = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.copied()
|
||||
.collect_vec()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let metadata = shared_state()
|
||||
.client
|
||||
.database()
|
||||
.metadata(public_key)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let profile = Profile::new(public_key, metadata);
|
||||
|
||||
Ok(profile)
|
||||
} else {
|
||||
Err(anyhow!("Not found"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(profile) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.local_account.update(cx, |this, cx| {
|
||||
*this = Some(profile);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
local_account,
|
||||
name: "Onboarding".into(),
|
||||
loading: false,
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Onboarding {
|
||||
@@ -71,6 +145,9 @@ impl Render for Onboarding {
|
||||
const TITLE: &str = "Welcome to Coop!";
|
||||
const SUBTITLE: &str = "Secure Communication on Nostr.";
|
||||
|
||||
let auto_login = AppSettings::get_global(cx).settings.auto_login;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
|
||||
div()
|
||||
.py_4()
|
||||
.size_full()
|
||||
@@ -78,9 +155,9 @@ impl Render for Onboarding {
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.mb_10()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
@@ -104,31 +181,121 @@ impl Render for Onboarding {
|
||||
.child(div().text_color(cx.theme().text_muted).child(SUBTITLE)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("continue_btn")
|
||||
.icon(Icon::new(IconName::ArrowRight))
|
||||
.label("Start Messaging")
|
||||
.primary()
|
||||
.reverse()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::new_account(window, cx);
|
||||
})),
|
||||
.map(|this| {
|
||||
if let Some(profile) = self.local_account.read(cx).as_ref() {
|
||||
this.relative()
|
||||
.child(
|
||||
div()
|
||||
.id("account")
|
||||
.mb_3()
|
||||
.h_10()
|
||||
.w_72()
|
||||
.bg(cx.theme().element_background)
|
||||
.text_color(cx.theme().element_foreground)
|
||||
.rounded_lg()
|
||||
.text_sm()
|
||||
.map(|this| {
|
||||
if self.loading {
|
||||
this.child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Indicator::new().small()),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child("Continue as")
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(
|
||||
Avatar::new(
|
||||
profile.render_avatar(proxy),
|
||||
)
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.child(profile.render_name()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.set_loading(true, cx);
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("auto_login")
|
||||
.label("Automatically log in next time")
|
||||
.checked(auto_login)
|
||||
.on_click(|_, _window, cx| {
|
||||
AppSettings::global(cx).update(cx, |this, cx| {
|
||||
this.settings.auto_login = !this.settings.auto_login;
|
||||
cx.notify();
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_4().right_4().child(
|
||||
Button::new("unload")
|
||||
.icon(IconName::Logout)
|
||||
.label("Logout")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(self.loading)
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
}),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.w_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("continue_btn")
|
||||
.icon(Icon::new(IconName::ArrowRight))
|
||||
.label("Start Messaging")
|
||||
.primary()
|
||||
.reverse()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::new_account(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label("Already have an account? Log in.")
|
||||
.ghost()
|
||||
.underline()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label("Already have an account? Log in.")
|
||||
.ghost()
|
||||
.underline()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use common::profile::RenderProfile;
|
||||
use global::{
|
||||
constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER},
|
||||
shared_state,
|
||||
};
|
||||
use global::constants::{DEFAULT_MODAL_WIDTH, NIP96_SERVER};
|
||||
use gpui::{
|
||||
div, http_client::Url, prelude::FluentBuilder, px, relative, rems, App, AppContext, Context,
|
||||
Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
@@ -33,7 +31,7 @@ impl Preferences {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let media_server = AppSettings::get_global(cx)
|
||||
.settings()
|
||||
.settings
|
||||
.media_server
|
||||
.to_string();
|
||||
|
||||
@@ -84,7 +82,7 @@ impl Render for Preferences {
|
||||
"Use wsrv.nl to resize and downscale avatar pictures (saves ~50MB of data)";
|
||||
|
||||
let input_state = self.media_input.downgrade();
|
||||
let settings = AppSettings::get_global(cx).settings();
|
||||
let settings = AppSettings::get_global(cx).settings.as_ref();
|
||||
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
@@ -106,7 +104,7 @@ impl Render for Preferences {
|
||||
.font_semibold()
|
||||
.child("Account"),
|
||||
)
|
||||
.when_some(shared_state().identity(), |this, profile| {
|
||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
|
||||
@@ -105,7 +105,7 @@ impl Profile {
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nip96 = AppSettings::get_global(cx).settings().media_server.clone();
|
||||
let nip96 = AppSettings::get_global(cx).settings.media_server.clone();
|
||||
let avatar_input = self.avatar_input.downgrade();
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
|
||||
@@ -60,7 +60,7 @@ impl DisplayRoom {
|
||||
impl RenderOnce for DisplayRoom {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
let hide_avatar = AppSettings::get_global(cx).settings().hide_user_avatars;
|
||||
let hide_avatar = AppSettings::get_global(cx).settings.hide_user_avatars;
|
||||
|
||||
self.base
|
||||
.id(self.ix)
|
||||
|
||||
@@ -17,6 +17,7 @@ use gpui::{
|
||||
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||
Window,
|
||||
};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
@@ -70,7 +71,7 @@ impl Sidebar {
|
||||
let indicator = cx.new(|_| None);
|
||||
let local_result = cx.new(|_| None);
|
||||
let global_result = cx.new(|_| None);
|
||||
let trusted_only = AppSettings::get_global(cx).settings().only_show_trusted;
|
||||
let trusted_only = AppSettings::get_global(cx).settings.only_show_trusted;
|
||||
|
||||
let find_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("Find or start a conversation"));
|
||||
@@ -349,7 +350,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn render_account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
|
||||
let proxy = AppSettings::get_global(cx).settings().proxy_user_avatars;
|
||||
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
|
||||
|
||||
div()
|
||||
.px_3()
|
||||
@@ -491,10 +492,9 @@ impl Render for Sidebar {
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
// Account
|
||||
.when_some(
|
||||
shared_state().identity.read_blocking().as_ref(),
|
||||
|this, profile| this.child(self.render_account(profile, cx)),
|
||||
)
|
||||
.when_some(Identity::get_global(cx).profile(), |this, profile| {
|
||||
this.child(self.render_account(&profile, cx))
|
||||
})
|
||||
// Search Input
|
||||
.child(
|
||||
div().px_3().w_full().h_7().flex_none().child(
|
||||
|
||||
@@ -65,6 +65,8 @@ impl Render for Startup {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
@@ -74,13 +76,10 @@ impl Render for Startup {
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Connection in progress")
|
||||
.justify_center()
|
||||
.child(Indicator::new().small()),
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user