chore: rework login and identity (#129)
* . * redesign onboarding screen * . * add signer proxy * . * . * . * . * fix proxy * clean up * fix new account
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use auto_update::AutoUpdater;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::DEFAULT_SIDEBAR_WIDTH;
|
||||
use global::constants::{ACCOUNT_IDENTIFIER, DEFAULT_SIDEBAR_WIDTH};
|
||||
use global::{global_channel, nostr_client, NostrSignal};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
actions, div, px, rems, Action, App, AppContext, Axis, Context, Entity, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
||||
Subscription, Window,
|
||||
actions, div, px, rems, Action, App, AppContext, AsyncWindowContext, Axis, Context, Entity,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
@@ -31,18 +33,18 @@ use ui::indicator::Indicator;
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::tooltip::Tooltip;
|
||||
use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
|
||||
|
||||
use crate::views::compose::compose_button;
|
||||
use crate::views::screening::Screening;
|
||||
use crate::views::user_profile::UserProfile;
|
||||
use crate::views::{
|
||||
backup_keys, chat, login, messaging_relays, new_account, onboarding, preferences, sidebar,
|
||||
startup, user_profile, welcome,
|
||||
account, chat, login, messaging_relays, new_account, onboarding, preferences, sidebar,
|
||||
user_profile, welcome,
|
||||
};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||
ChatSpace::new(window, cx)
|
||||
cx.new(|cx| ChatSpace::new(window, cx))
|
||||
}
|
||||
|
||||
pub fn login(window: &mut Window, cx: &mut App) {
|
||||
@@ -85,118 +87,189 @@ pub struct ToggleModal {
|
||||
pub struct ChatSpace {
|
||||
title_bar: Entity<TitleBar>,
|
||||
dock: Entity<DockArea>,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 5]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
}
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let registry = Registry::global(cx);
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
|
||||
let title_bar = cx.new(|_| TitleBar::new());
|
||||
let dock = cx.new(|cx| {
|
||||
let panel = Arc::new(startup::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
let mut dock = DockArea::new(window, cx);
|
||||
// Initialize the dock area with the center panel
|
||||
dock.set_center(center, window, cx);
|
||||
dock
|
||||
});
|
||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||
|
||||
cx.new(|cx| {
|
||||
let registry = Registry::global(cx);
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let identity = Identity::global(cx);
|
||||
let mut subscriptions = smallvec![];
|
||||
let mut subscriptions = smallvec![];
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// 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() {
|
||||
this.render_client_keys_modal(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// 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_signer() {
|
||||
this.set_onboarding_panels(window, cx);
|
||||
} else {
|
||||
this.set_chat_panels(window, cx);
|
||||
}
|
||||
},
|
||||
));
|
||||
cx.observe_in(&client_keys, window, |this, state, window, cx| {
|
||||
if !state.read(cx).has_keys() {
|
||||
this.render_client_keys_modal(window, cx);
|
||||
} else {
|
||||
this.load_local_account(window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Automatically run load function when UserProfile is created
|
||||
subscriptions.push(cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||
cx.observe_new::<UserProfile>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.load(window, cx);
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Automatically run load function when Screening is created
|
||||
subscriptions.push(cx.observe_new::<Screening>(|this, window, cx| {
|
||||
cx.observe_new::<Screening>(|this, window, cx| {
|
||||
if let Some(window) = window {
|
||||
this.load(window, cx);
|
||||
}
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe to open chat room requests
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
®istry,
|
||||
window,
|
||||
|this: &mut Self, _state, event, window, cx| {
|
||||
match event {
|
||||
RegistrySignal::Open(room) => {
|
||||
if let Some(room) = room.upgrade() {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
let panel = chat::init(room, window, cx);
|
||||
|
||||
// Load messages when the panel is created
|
||||
panel.update(cx, |this, cx| {
|
||||
this.load_messages(window, cx);
|
||||
});
|
||||
|
||||
// Add the panel to the center dock (tabs)
|
||||
this.add_panel(
|
||||
Arc::new(panel),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(t!("common.room_error"), cx);
|
||||
}
|
||||
}
|
||||
RegistrySignal::Close(..) => {
|
||||
cx.subscribe_in(®istry, window, |this, _e, event, window, cx| {
|
||||
match event {
|
||||
RegistrySignal::Open(room) => {
|
||||
if let Some(room) = room.upgrade() {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.focus_tab_panel(window, cx);
|
||||
let panel = chat::init(room, window, cx);
|
||||
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
window.close_all_modals(cx);
|
||||
// Load messages when the panel is created
|
||||
panel.update(cx, |this, cx| {
|
||||
this.load_messages(window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
Self {
|
||||
dock,
|
||||
title_bar,
|
||||
subscriptions,
|
||||
}
|
||||
})
|
||||
// Add the panel to the center dock (tabs)
|
||||
this.add_panel(Arc::new(panel), DockPlacement::Center, window, cx);
|
||||
});
|
||||
} else {
|
||||
window.push_notification(t!("common.room_error"), cx);
|
||||
}
|
||||
}
|
||||
RegistrySignal::Close(..) => {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.focus_tab_panel(window, cx);
|
||||
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
window.close_all_modals(cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
tasks.push(
|
||||
// Continuously handle signals from the Nostr channel
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
ChatSpace::handle_signal(this, cx).await
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
dock,
|
||||
title_bar,
|
||||
_subscriptions: subscriptions,
|
||||
_tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_onboarding_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
async fn handle_signal(e: WeakEntity<ChatSpace>, cx: &mut AsyncWindowContext) {
|
||||
let channel = global_channel();
|
||||
let mut is_open_proxy_modal = false;
|
||||
|
||||
while let Ok(signal) = channel.1.recv().await {
|
||||
cx.update(|window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
|
||||
match signal {
|
||||
NostrSignal::SignerSet(public_key) => {
|
||||
window.close_modal(cx);
|
||||
|
||||
// Setup the default layout for current workspace
|
||||
e.update(cx, |this, cx| {
|
||||
this.set_default_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Initialize identity
|
||||
identity::init(public_key, window, cx);
|
||||
|
||||
// Load all chat rooms
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
});
|
||||
}
|
||||
NostrSignal::SignerUnset => {
|
||||
e.update(cx, |this, cx| {
|
||||
this.set_onboarding_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
NostrSignal::ProxyDown => {
|
||||
if !is_open_proxy_modal {
|
||||
e.update(cx, |this, cx| {
|
||||
this.render_proxy_modal(window, cx);
|
||||
})
|
||||
.ok();
|
||||
is_open_proxy_modal = true;
|
||||
}
|
||||
}
|
||||
// Load chat rooms and stop the loading status
|
||||
NostrSignal::Finish => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.set_loading(false, cx);
|
||||
// Send a signal to refresh all opened rooms' messages
|
||||
if let Some(ids) = ChatSpace::all_panels(window, cx) {
|
||||
this.refresh_rooms(ids, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Load chat rooms without setting as finished
|
||||
NostrSignal::PartialFinish => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
// Send a signal to refresh all opened rooms' messages
|
||||
if let Some(ids) = ChatSpace::all_panels(window, cx) {
|
||||
this.refresh_rooms(ids, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add the new metadata to the registry or update the existing one
|
||||
NostrSignal::Metadata(event) => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.insert_or_update_person(event, cx);
|
||||
});
|
||||
}
|
||||
// Convert the gift wrapped message to a message
|
||||
NostrSignal::GiftWrap(event) => {
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
registry.update(cx, |this, cx| {
|
||||
this.event_to_message(identity, event, window, cx);
|
||||
});
|
||||
}
|
||||
NostrSignal::DmRelaysFound => {
|
||||
//
|
||||
}
|
||||
NostrSignal::Notice(_msg) => {
|
||||
// window.push_notification(msg, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_onboarding_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let panel = Arc::new(onboarding::init(window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
@@ -206,48 +279,93 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn set_chat_panels(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx);
|
||||
pub fn set_account_layout(
|
||||
&mut self,
|
||||
secret: String,
|
||||
profile: Profile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let panel = Arc::new(account::init(secret, profile, window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.reset(window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn set_default_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let weak_dock = self.dock.downgrade();
|
||||
|
||||
// The left panel will render sidebar
|
||||
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||
let sidebar = Arc::new(sidebar::init(window, cx));
|
||||
let center = Arc::new(welcome::init(window, cx));
|
||||
|
||||
// The center panel will render chat rooms (as tabs)
|
||||
let left = DockItem::panel(sidebar);
|
||||
let center = DockItem::split_with_sizes(
|
||||
Axis::Vertical,
|
||||
vec![DockItem::tabs(
|
||||
vec![Arc::new(welcome::init(window, cx))],
|
||||
None,
|
||||
&weak_dock,
|
||||
window,
|
||||
cx,
|
||||
)],
|
||||
vec![DockItem::tabs(vec![center], None, &weak_dock, window, cx)],
|
||||
vec![None],
|
||||
&weak_dock,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
// Update dock
|
||||
self.dock.update(cx, |this, cx| {
|
||||
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
// Load all chat rooms from the database
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
let metadata = client
|
||||
.database()
|
||||
.metadata(event.pubkey)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok((event.content, Profile::new(event.pubkey, metadata)))
|
||||
} else {
|
||||
Err(anyhow!("Empty"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok((secret, profile)) = task.await {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_account_layout(secret, profile, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_onboarding_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let view = preferences::init(window, cx);
|
||||
let title = SharedString::new(t!("common.preferences"));
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
modal
|
||||
.title(title.clone())
|
||||
.title(shared_t!("common.preferences"))
|
||||
.width(px(480.))
|
||||
.child(view.clone())
|
||||
});
|
||||
@@ -261,17 +379,30 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let registry = Registry::global(cx);
|
||||
let identity = Identity::global(cx);
|
||||
|
||||
registry.update(cx, |this, cx| {
|
||||
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
Identity::remove_global(cx);
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.reset(cx);
|
||||
});
|
||||
|
||||
identity.update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
});
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let channel = global_channel();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER);
|
||||
|
||||
// Delete account
|
||||
client.database().delete(filter).await.ok();
|
||||
|
||||
// Reset the nostr client
|
||||
client.reset().await;
|
||||
|
||||
// Notify the channel about the signer being unset
|
||||
channel.0.send(NostrSignal::SignerUnset).await.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_open_profile(&mut self, ev: &OpenProfile, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -292,10 +423,33 @@ impl ChatSpace {
|
||||
});
|
||||
}
|
||||
|
||||
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let title = SharedString::new(t!("startup.client_keys_warning"));
|
||||
let desc = SharedString::new(t!("startup.client_keys_desc"));
|
||||
fn render_proxy_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||
window.open_modal(cx, |this, _window, _cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.alert()
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.open_browser")))
|
||||
.title(shared_t!("proxy.label"))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.gap_1()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_sm()
|
||||
.child(shared_t!("proxy.description")),
|
||||
)
|
||||
.on_ok(move |_e, _window, cx| {
|
||||
cx.open_url("http://localhost:7400");
|
||||
false
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
@@ -321,9 +475,9 @@ impl ChatSpace {
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(title.clone()),
|
||||
.child(shared_t!("startup.client_keys_warning")),
|
||||
)
|
||||
.child(desc.clone()),
|
||||
.child(shared_t!("startup.client_keys_desc")),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
@@ -379,8 +533,7 @@ impl ChatSpace {
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
let need_backup = Identity::read_global(cx).need_backup();
|
||||
let has_dm_relays = Identity::read_global(cx).has_dm_relays();
|
||||
let nip17_relays = Identity::read_global(cx).nip17_relays();
|
||||
|
||||
let updating = AutoUpdater::read_global(cx).status.is_updating();
|
||||
let updated = AutoUpdater::read_global(cx).status.is_updated();
|
||||
@@ -415,12 +568,9 @@ impl ChatSpace {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(has_dm_relays, |this, status| {
|
||||
.when_some(nip17_relays, |this, status| {
|
||||
this.when(!status, |this| this.child(messaging_relays::relay_button()))
|
||||
})
|
||||
.when_some(need_backup, |this, keys| {
|
||||
this.child(backup_keys::backup_button(keys.to_owned()))
|
||||
})
|
||||
.child(
|
||||
Button::new("user")
|
||||
.small()
|
||||
@@ -437,24 +587,6 @@ impl ChatSpace {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
chatspace.update(cx, |this, cx| {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn all_panels(window: &mut Window, cx: &mut App) -> Option<Vec<u64>> {
|
||||
let Some(Some(root)) = window.root::<Root>() else {
|
||||
return None;
|
||||
@@ -476,15 +608,35 @@ impl ChatSpace {
|
||||
|
||||
Some(ids)
|
||||
}
|
||||
|
||||
pub(crate) fn set_center_panel<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
let panel = Arc::new(panel);
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
chatspace.update(cx, |this, cx| {
|
||||
this.dock.update(cx, |this, cx| {
|
||||
this.set_center(center, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ChatSpace {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let modal_layer = Root::render_modal_layer(window, cx);
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
let logged_in = Identity::has_global(cx);
|
||||
|
||||
// Only render titlebar element if user is logged in
|
||||
if let Some(identity) = Identity::read_global(cx).public_key() {
|
||||
// Only render titlebar child elements if user is logged in
|
||||
if logged_in {
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let profile = Registry::read_global(cx).get_person(&identity, cx);
|
||||
|
||||
let left_side = self
|
||||
|
||||
@@ -9,22 +9,18 @@ use global::constants::{
|
||||
APP_ID, APP_NAME, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT,
|
||||
SEARCH_RELAYS, WAIT_FOR_FINISH,
|
||||
};
|
||||
use global::{nostr_client, processed_events, starting_time, NostrSignal};
|
||||
use global::{global_channel, nostr_client, processed_events, starting_time, NostrSignal};
|
||||
use gpui::{
|
||||
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
|
||||
SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
|
||||
WindowKind, WindowOptions,
|
||||
};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::Registry;
|
||||
use smol::channel::{self, Sender};
|
||||
use theme::Theme;
|
||||
use ui::Root;
|
||||
|
||||
use crate::chatspace::ChatSpace;
|
||||
|
||||
pub(crate) mod chatspace;
|
||||
pub(crate) mod views;
|
||||
|
||||
@@ -40,17 +36,15 @@ fn main() {
|
||||
let client = nostr_client();
|
||||
|
||||
// Initialize the starting time
|
||||
let _ = starting_time();
|
||||
let _starting_time = starting_time();
|
||||
|
||||
// Initialize the Application
|
||||
let app = Application::new()
|
||||
.with_assets(Assets)
|
||||
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
|
||||
|
||||
let (signal_tx, signal_rx) = channel::bounded::<NostrSignal>(2048);
|
||||
let (mta_tx, mta_rx) = channel::bounded::<PublicKey>(1024);
|
||||
let (pubkey_tx, pubkey_rx) = channel::bounded::<PublicKey>(1024);
|
||||
let (event_tx, event_rx) = channel::bounded::<Event>(2048);
|
||||
let signal_tx_clone = signal_tx.clone();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
@@ -62,12 +56,33 @@ fn main() {
|
||||
// Handle Nostr notifications.
|
||||
//
|
||||
// Send the redefined signal back to GPUI via channel.
|
||||
if let Err(e) = handle_nostr_notifications(&signal_tx_clone, &event_tx).await {
|
||||
if let Err(e) = handle_nostr_notifications(&event_tx).await {
|
||||
log::error!("Failed to handle Nostr notifications: {e}");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
let channel = global_channel();
|
||||
|
||||
loop {
|
||||
if let Ok(signer) = client.signer().await {
|
||||
if let Ok(public_key) = signer.get_public_key().await {
|
||||
// Notify the app that the signer has been set.
|
||||
_ = channel.0.send(NostrSignal::SignerSet(public_key)).await;
|
||||
|
||||
// Get the NIP-65 relays for the public key.
|
||||
get_nip65_relays(public_key).await.ok();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
smol::Timer::after(Duration::from_secs(1)).await;
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
|
||||
@@ -85,7 +100,7 @@ fn main() {
|
||||
let duration = smol::Timer::after(duration);
|
||||
|
||||
let recv = || async {
|
||||
if let Ok(public_key) = mta_rx.recv().await {
|
||||
if let Ok(public_key) = pubkey_rx.recv().await {
|
||||
BatchEvent::NewKeys(public_key)
|
||||
} else {
|
||||
BatchEvent::Closed
|
||||
@@ -126,6 +141,7 @@ fn main() {
|
||||
|
||||
app.background_executor()
|
||||
.spawn(async move {
|
||||
let channel = global_channel();
|
||||
let mut counter = 0;
|
||||
|
||||
loop {
|
||||
@@ -149,7 +165,7 @@ fn main() {
|
||||
|
||||
match smol::future::or(recv(), timeout()).await {
|
||||
Some(event) => {
|
||||
let cached = unwrap_gift(&event, &signal_tx, &mta_tx).await;
|
||||
let cached = unwrap_gift(&event, &pubkey_tx).await;
|
||||
|
||||
// Increment the total messages counter if message is not from cache
|
||||
if !cached {
|
||||
@@ -158,14 +174,14 @@ fn main() {
|
||||
|
||||
// Send partial finish signal to GPUI
|
||||
if counter >= 20 {
|
||||
signal_tx.send(NostrSignal::PartialFinish).await.ok();
|
||||
channel.0.send(NostrSignal::PartialFinish).await.ok();
|
||||
// Reset counter
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Notify the UI that the processing is finished
|
||||
signal_tx.send(NostrSignal::Finish).await.ok();
|
||||
channel.0.send(NostrSignal::Finish).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,77 +242,22 @@ fn main() {
|
||||
cx.activate(true);
|
||||
// Initialize the tokio runtime
|
||||
gpui_tokio::init(cx);
|
||||
|
||||
// Initialize components
|
||||
ui::init(cx);
|
||||
// Initialize app registry
|
||||
registry::init(cx);
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
|
||||
// Initialize client keys
|
||||
client_keys::init(cx);
|
||||
// Initialize identity
|
||||
identity::init(window, cx);
|
||||
|
||||
// Initialize app registry
|
||||
registry::init(cx);
|
||||
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
|
||||
// Initialize auto update
|
||||
auto_update::init(cx);
|
||||
|
||||
// Spawn a task to handle events from nostr channel
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
while let Ok(signal) = signal_rx.recv().await {
|
||||
cx.update(|window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
let identity = Identity::global(cx);
|
||||
|
||||
match signal {
|
||||
// Load chat rooms and stop the loading status
|
||||
NostrSignal::Finish => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.set_loading(false, cx);
|
||||
// Send a signal to refresh all opened rooms' messages
|
||||
if let Some(ids) = ChatSpace::all_panels(window, cx) {
|
||||
this.refresh_rooms(ids, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Load chat rooms without setting as finished
|
||||
NostrSignal::PartialFinish => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
// Send a signal to refresh all opened rooms' messages
|
||||
if let Some(ids) = ChatSpace::all_panels(window, cx) {
|
||||
this.refresh_rooms(ids, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add the new metadata to the registry or update the existing one
|
||||
NostrSignal::Metadata(event) => {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.insert_or_update_person(event, cx);
|
||||
});
|
||||
}
|
||||
// Convert the gift wrapped message to a message
|
||||
NostrSignal::GiftWrap(event) => {
|
||||
if let Some(public_key) = identity.read(cx).public_key() {
|
||||
registry.update(cx, |this, cx| {
|
||||
this.event_to_message(public_key, event, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
NostrSignal::DmRelaysFound => {
|
||||
identity.update(cx, |this, cx| {
|
||||
this.set_has_dm_relays(cx);
|
||||
});
|
||||
}
|
||||
NostrSignal::Notice(_msg) => {
|
||||
// window.push_notification(msg, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Root::new(chatspace::init(window, cx).into(), window, cx)
|
||||
})
|
||||
})
|
||||
@@ -352,11 +313,9 @@ async fn connect(client: &Client) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_nostr_notifications(
|
||||
signal_tx: &Sender<NostrSignal>,
|
||||
event_tx: &Sender<Event>,
|
||||
) -> Result<(), Error> {
|
||||
async fn handle_nostr_notifications(event_tx: &Sender<Event>) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let channel = global_channel();
|
||||
let auto_close = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let mut notifications = client.notifications();
|
||||
|
||||
@@ -379,10 +338,8 @@ async fn handle_nostr_notifications(
|
||||
// Get metadata for event's pubkey that matches the current user's pubkey
|
||||
if let Ok(true) = is_from_current_user(&event).await {
|
||||
let sub_id = SubscriptionId::new("metadata");
|
||||
let filter = Filter::new()
|
||||
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::InboxRelays])
|
||||
.author(event.pubkey)
|
||||
.limit(10);
|
||||
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::InboxRelays];
|
||||
let filter = Filter::new().kinds(kinds).author(event.pubkey).limit(10);
|
||||
|
||||
client
|
||||
.subscribe_with_id(sub_id, filter, Some(auto_close))
|
||||
@@ -416,7 +373,7 @@ async fn handle_nostr_notifications(
|
||||
let sub_id = SubscriptionId::new("gift-wrap");
|
||||
|
||||
// Notify the UI that the current user has set up the DM relays
|
||||
signal_tx.send(NostrSignal::DmRelaysFound).await.ok();
|
||||
channel.0.send(NostrSignal::DmRelaysFound).await.ok();
|
||||
|
||||
if client
|
||||
.subscribe_with_id_to(relays.clone(), sub_id, filter, None)
|
||||
@@ -442,7 +399,8 @@ async fn handle_nostr_notifications(
|
||||
}
|
||||
}
|
||||
Kind::Metadata => {
|
||||
signal_tx
|
||||
channel
|
||||
.0
|
||||
.send(NostrSignal::Metadata(event.into_owned()))
|
||||
.await
|
||||
.ok();
|
||||
@@ -457,6 +415,21 @@ async fn handle_nostr_notifications(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_nip65_relays(public_key: PublicKey) -> Result<(), Error> {
|
||||
let client = nostr_client();
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let sub_id = SubscriptionId::new("nip65-relays");
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::RelayList)
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
client.subscribe_with_id(sub_id, filter, Some(opts)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn is_from_current_user(event: &Event) -> Result<bool, Error> {
|
||||
let client = nostr_client();
|
||||
let signer = client.signer().await?;
|
||||
@@ -522,12 +495,9 @@ async fn get_unwrapped(root: EventId) -> Result<Event, Error> {
|
||||
}
|
||||
|
||||
/// Unwraps a gift-wrapped event and processes its contents.
|
||||
async fn unwrap_gift(
|
||||
gift: &Event,
|
||||
signal_tx: &Sender<NostrSignal>,
|
||||
mta_tx: &Sender<PublicKey>,
|
||||
) -> bool {
|
||||
async fn unwrap_gift(gift: &Event, pubkey_tx: &Sender<PublicKey>) -> bool {
|
||||
let client = nostr_client();
|
||||
let channel = global_channel();
|
||||
let mut is_cached = false;
|
||||
|
||||
let event = match get_unwrapped(gift.id).await {
|
||||
@@ -561,12 +531,12 @@ async fn unwrap_gift(
|
||||
|
||||
// Send all pubkeys to the metadata batch to sync data
|
||||
for public_key in event.all_pubkeys() {
|
||||
mta_tx.send(public_key).await.ok();
|
||||
pubkey_tx.send(public_key).await.ok();
|
||||
}
|
||||
|
||||
// Send a notify to GPUI if this is a new message
|
||||
if starting_time() <= &event.created_at {
|
||||
signal_tx.send(NostrSignal::GiftWrap(event)).await.ok();
|
||||
channel.0.send(NostrSignal::GiftWrap(event)).await.ok();
|
||||
}
|
||||
|
||||
is_cached
|
||||
|
||||
398
crates/coop/src/views/account.rs
Normal file
398
crates/coop/src/views/account.rs
Normal file
@@ -0,0 +1,398 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::DisplayProfile;
|
||||
use common::handle_auth::CoopAuthUrlHandler;
|
||||
use global::constants::ACCOUNT_IDENTIFIER;
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Task, WeakEntity, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{h_flex, v_flex, ContextModal, Disableable, Sizable, StyledExt};
|
||||
|
||||
pub fn init(
|
||||
secret: String,
|
||||
profile: Profile,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Account> {
|
||||
Account::new(secret, profile, window, cx)
|
||||
}
|
||||
|
||||
pub struct Account {
|
||||
profile: Profile,
|
||||
stored_secret: String,
|
||||
is_bunker: bool,
|
||||
is_extension: bool,
|
||||
loading: bool,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
fn new(secret: String, profile: Profile, _window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let is_bunker = secret.starts_with("bunker://");
|
||||
let is_extension = secret.starts_with("extension");
|
||||
|
||||
cx.new(|cx| Self {
|
||||
profile,
|
||||
is_bunker,
|
||||
is_extension,
|
||||
stored_secret: secret,
|
||||
loading: false,
|
||||
name: "Account".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
|
||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_loading(true, cx);
|
||||
|
||||
if self.is_bunker {
|
||||
if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) {
|
||||
self.nostr_connect(uri, window, cx);
|
||||
}
|
||||
} else if self.is_extension {
|
||||
self.proxy(window, cx);
|
||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
|
||||
self.keys(enc, window, cx);
|
||||
} else {
|
||||
window.push_notification("Cannot continue with current account", cx);
|
||||
self.set_loading(false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn nostr_connect(&mut self, uri: NostrConnectURI, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let app_keys = client_keys.read(cx).keys();
|
||||
|
||||
let secs = 30;
|
||||
let timeout = Duration::from_secs(secs);
|
||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
||||
|
||||
// Handle auth url with the default browser
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
// Handle connection
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
let client = nostr_client();
|
||||
|
||||
match signer.bunker_uri().await {
|
||||
Ok(_) => {
|
||||
// Set the client's signer with the current nostr connect instance
|
||||
client.set_signer(signer).await;
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn proxy(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
Identity::start_browser_proxy(cx);
|
||||
}
|
||||
|
||||
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
|
||||
let error: Entity<Option<SharedString>> = cx.new(|_| None);
|
||||
let weak_error = error.downgrade();
|
||||
|
||||
let entity = cx.weak_entity();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let entity = entity.clone();
|
||||
let entity_clone = entity.clone();
|
||||
let weak_input = weak_input.clone();
|
||||
let weak_error = weak_error.clone();
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, _window, cx| {
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
let weak_error = weak_error.clone();
|
||||
let password = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
entity_clone
|
||||
.update(cx, |this, cx| {
|
||||
this.verify_keys(enc, password, weak_error, window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// false to keep the modal open
|
||||
false
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(shared_t!("login.password_to_decrypt"))
|
||||
.child(TextInput::new(&pwd_input).small())
|
||||
.when_some(error.read(cx).as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn verify_keys(
|
||||
&mut self,
|
||||
enc: EncryptedSecretKey,
|
||||
password: Option<SharedString>,
|
||||
error: WeakEntity<Option<SharedString>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(password) = password else {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some("Password is required".into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
|
||||
if password.is_empty() {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some("Password cannot be empty".into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
let task: Task<Result<SecretKey, Error>> = cx.background_spawn(async move {
|
||||
let secret = enc.decrypt(&password)?;
|
||||
Ok(secret)
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
match task.await {
|
||||
Ok(secret) => {
|
||||
cx.update(|window, cx| {
|
||||
window.close_all_modals(cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let client = nostr_client();
|
||||
let keys = Keys::new(secret);
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
client.set_signer(keys).await
|
||||
}
|
||||
Err(e) => {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some(e.to_string().into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn logout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER);
|
||||
|
||||
// Delete account
|
||||
client.database().delete(filter).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|_window, cx| {
|
||||
cx.restart();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.loading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Account {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Account {}
|
||||
|
||||
impl Focusable for Account {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Account {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.relative()
|
||||
.size_full()
|
||||
.gap_10()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
v_flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_16()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(shared_t!("welcome.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("welcome.subtitle")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id("account")
|
||||
.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(shared_t!("onboarding.choose_account"))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Avatar::new(self.profile.avatar_url(true))
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.font_semibold()
|
||||
.child(self.profile.display_name()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
.hover(|this| this.bg(cx.theme().element_hover))
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.login(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("logout")
|
||||
.label(t!("user.sign_out"))
|
||||
.ghost()
|
||||
.disabled(self.loading)
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.logout(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,56 +5,14 @@ use dirs::document_dir;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Window,
|
||||
SharedString, Styled, Task, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_sdk::prelude::*;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::{divider, h_flex, v_flex, ContextModal, Disableable, IconName, Sizable};
|
||||
|
||||
pub fn backup_button(keys: Keys) -> impl IntoElement {
|
||||
div().child(
|
||||
Button::new("backup")
|
||||
.icon(IconName::Info)
|
||||
.label(t!("new_account.backup_label"))
|
||||
.danger()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(move |_, window, cx| {
|
||||
let title = SharedString::new(t!("new_account.backup_label"));
|
||||
let keys = keys.clone();
|
||||
let view = cx.new(|cx| BackupKeys::new(&keys, window, cx));
|
||||
let weak_view = view.downgrade();
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
|
||||
modal
|
||||
.confirm()
|
||||
.title(title.clone())
|
||||
.child(view.clone())
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("new_account.backup_skip"))
|
||||
.ok_text(t!("new_account.backup_download")),
|
||||
)
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
this.download(window, cx);
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
false
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable};
|
||||
|
||||
pub struct BackupKeys {
|
||||
password: Entity<InputState>,
|
||||
@@ -92,6 +50,42 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn password(&self, cx: &Context<Self>) -> String {
|
||||
self.password.read(cx).value().to_string()
|
||||
}
|
||||
|
||||
pub fn backup(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Task<()>> {
|
||||
let document_dir = document_dir().expect("Failed to get document directory");
|
||||
let password = self.password.read(cx).value().to_string();
|
||||
|
||||
if password.is_empty() {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return None;
|
||||
};
|
||||
|
||||
let path = cx.prompt_for_new_path(&document_dir, Some("My Nostr Account"));
|
||||
let nsec = self.secret_input.read(cx).value().to_string();
|
||||
|
||||
Some(cx.spawn_in(window, async move |this, cx| {
|
||||
match Flatten::flatten(path.await.map_err(|e| e.into())) {
|
||||
Ok(Ok(Some(path))) => {
|
||||
cx.update(|window, cx| {
|
||||
if let Err(e) = fs::write(&path, nsec) {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(e.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {
|
||||
log::error!("Failed to save backup keys");
|
||||
}
|
||||
};
|
||||
}))
|
||||
}
|
||||
|
||||
fn copy_secret(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let item = ClipboardItem::new_string(self.secret_input.read(cx).value().to_string());
|
||||
cx.write_to_clipboard(item);
|
||||
@@ -140,48 +134,6 @@ impl BackupKeys {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn download(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let document_dir = document_dir().expect("Failed to get document directory");
|
||||
let password = self.password.read(cx).value().to_string();
|
||||
|
||||
if password.is_empty() {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let path = cx.prompt_for_new_path(&document_dir, None);
|
||||
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())) {
|
||||
Ok(Ok(Some(path))) => {
|
||||
cx.update(|window, cx| {
|
||||
match fs::write(&path, nsec) {
|
||||
Ok(_) => {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.clear_need_backup(password, cx);
|
||||
});
|
||||
// Close the current modal
|
||||
window.close_modal(cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(e.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
_ => {
|
||||
log::error!("Failed to save backup keys");
|
||||
}
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BackupKeys {
|
||||
|
||||
@@ -223,11 +223,6 @@ impl Chat {
|
||||
|
||||
/// Send a message to all members of the chat
|
||||
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Return if user is not logged in
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the message which includes all attachments
|
||||
let content = self.input_content(cx);
|
||||
|
||||
@@ -254,6 +249,7 @@ impl Chat {
|
||||
|
||||
// Get the current room entity
|
||||
let room = self.room.read(cx);
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
|
||||
// Create a temporary message for optimistic update
|
||||
let temp_message = room.create_temp_message(identity, &content, replies.as_ref());
|
||||
|
||||
@@ -1,38 +1,30 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::TextUtils;
|
||||
use common::handle_auth::CoopAuthUrlHandler;
|
||||
use global::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use global::constants::ACCOUNT_IDENTIFIER;
|
||||
use global::nostr_client;
|
||||
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,
|
||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{ContextModal, Disableable, Sizable, StyledExt};
|
||||
use ui::{v_flex, ContextModal, Disableable, Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
Login::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
key_input: Entity<InputState>,
|
||||
relay_input: Entity<InputState>,
|
||||
connection_string: Entity<NostrConnectURI>,
|
||||
qr_image: Entity<Option<Arc<Image>>>,
|
||||
// Error for the key input
|
||||
input: Entity<InputState>,
|
||||
error: Entity<Option<SharedString>>,
|
||||
countdown: Entity<Option<u64>>,
|
||||
logging_in: bool,
|
||||
@@ -40,7 +32,7 @@ pub struct Login {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 3]>,
|
||||
subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
@@ -49,110 +41,29 @@ impl Login {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
// nsec or bunker_uri (NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md)
|
||||
let key_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
|
||||
let relay_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.default_value(NOSTR_CONNECT_RELAY)
|
||||
.placeholder(NOSTR_CONNECT_RELAY)
|
||||
});
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
// Direct connection initiated by the client
|
||||
let connection_string = cx.new(|cx| {
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
|
||||
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
|
||||
});
|
||||
|
||||
let qr_image = cx.new(|_| None);
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
let error = cx.new(|_| None);
|
||||
let countdown = 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| {
|
||||
cx.subscribe_in(&input, window, |this, _e, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.login(window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// 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 {
|
||||
this.change_relay(window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Observe changes to the Nostr Connect URI and wait for a connection
|
||||
subscriptions.push(cx.observe_in(
|
||||
&connection_string,
|
||||
window,
|
||||
|this, entity, window, cx| {
|
||||
let connection_string = entity.read(cx).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| {
|
||||
*this = connection_string.to_string().to_qr();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
match NostrConnect::new(
|
||||
connection_string,
|
||||
client_keys,
|
||||
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||
None,
|
||||
) {
|
||||
Ok(mut signer) => {
|
||||
// Automatically open auth url
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
// Wait for connection in the background
|
||||
this.wait_for_connection(signer, window, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).title("Nostr Connect"),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
// Create a Nostr Connect URI and QR Code 800ms after opening the login screen
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(800))
|
||||
.await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.connection_string.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
input,
|
||||
error,
|
||||
countdown,
|
||||
subscriptions,
|
||||
name: "Login".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
logging_in: false,
|
||||
countdown,
|
||||
key_input,
|
||||
relay_input,
|
||||
connection_string,
|
||||
qr_image,
|
||||
error,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,17 +71,18 @@ impl Login {
|
||||
if self.logging_in {
|
||||
return;
|
||||
};
|
||||
|
||||
// Prevent duplicate login requests
|
||||
self.set_logging_in(true, cx);
|
||||
|
||||
// Disable the input
|
||||
self.key_input.update(cx, |this, cx| {
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_loading(true, cx);
|
||||
this.set_disabled(true, cx);
|
||||
});
|
||||
|
||||
// Content can be secret key or bunker://
|
||||
match self.key_input.read(cx).value().to_string() {
|
||||
match self.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),
|
||||
@@ -314,7 +226,8 @@ impl Login {
|
||||
}
|
||||
|
||||
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 value = self.input.read(cx).value().to_string();
|
||||
|
||||
let secret_key = if value.starts_with("nsec1") {
|
||||
SecretKey::parse(&value).ok()
|
||||
} else if value.starts_with("ncryptsec1") {
|
||||
@@ -328,10 +241,15 @@ impl Login {
|
||||
if let Some(secret_key) = secret_key {
|
||||
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);
|
||||
});
|
||||
// Encrypt and save user secret key to disk
|
||||
self.write_keys_to_disk(&keys, password, cx);
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
self.set_error(t!("login.key_invalid"), window, cx);
|
||||
}
|
||||
@@ -343,16 +261,19 @@ impl Login {
|
||||
return;
|
||||
};
|
||||
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 8);
|
||||
// .unwrap() is fine here because there's no error handling for bunker uri
|
||||
let mut signer = NostrConnect::new(uri, client_keys, timeout, None).unwrap();
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let app_keys = client_keys.read(cx).keys();
|
||||
|
||||
let secs = 30;
|
||||
let timeout = Duration::from_secs(secs);
|
||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
||||
|
||||
// Handle auth url with the default browser
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
// Start countdown
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
for i in (0..=NOSTR_CONNECT_TIMEOUT / 8).rev() {
|
||||
for i in (0..=secs).rev() {
|
||||
if i == 0 {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_countdown(None, cx);
|
||||
@@ -371,26 +292,28 @@ impl Login {
|
||||
|
||||
// Handle connection
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let client = nostr_client();
|
||||
|
||||
match signer.bunker_uri().await {
|
||||
Ok(bunker_uri) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(t!("login.logging_in"), cx);
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.write_bunker(&bunker_uri, cx);
|
||||
this.set_signer(signer, window, cx);
|
||||
});
|
||||
Ok(uri) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.write_uri_to_disk(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Set the client's signer with the current nostr connect instance
|
||||
client.set_signer(signer).await;
|
||||
}
|
||||
Err(error) => {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
// Force reset the client keys without notify UI
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
log::info!("Timeout occurred. Reset client keys");
|
||||
this.set_error(error.to_string(), window, cx);
|
||||
// Force reset the client keys
|
||||
//
|
||||
// This step is necessary to ensure that user can retry the connection
|
||||
client_keys.update(cx, |this, cx| {
|
||||
this.force_new_keys(cx);
|
||||
});
|
||||
this.set_error(error.to_string(), window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -401,55 +324,68 @@ impl Login {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn wait_for_connection(
|
||||
&mut self,
|
||||
signer: NostrConnect,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match signer.bunker_uri().await {
|
||||
Ok(uri) => {
|
||||
cx.update(|window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.write_bunker(&uri, cx);
|
||||
this.set_signer(signer, window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
// Only send notifications on the login screen
|
||||
this.update(cx, |_, cx| {
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).title("Nostr Connect"),
|
||||
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 {
|
||||
window.push_notification(Notification::error(t!("relays.invalid")), cx);
|
||||
fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
||||
let Some(public_key) = uri.remote_signer_public_key().cloned() else {
|
||||
log::error!("Remote Signer's public key not found");
|
||||
return;
|
||||
};
|
||||
|
||||
let client_keys = ClientKeys::get_global(cx).keys();
|
||||
let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
|
||||
let mut value = uri.to_string();
|
||||
|
||||
self.connection_string.update(cx, |this, cx| {
|
||||
*this = uri;
|
||||
cx.notify();
|
||||
});
|
||||
// Clear the secret param if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
value = value.replace(secret, "");
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||
let keys = keys.to_owned();
|
||||
let public_key = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let client = nostr_client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_error(
|
||||
@@ -471,7 +407,7 @@ impl Login {
|
||||
});
|
||||
|
||||
// Re enable the input
|
||||
self.key_input.update(cx, |this, cx| {
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
this.set_loading(false, cx);
|
||||
this.set_disabled(false, cx);
|
||||
@@ -533,162 +469,64 @@ impl Focusable for Login {
|
||||
|
||||
impl Render for Login {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
v_flex()
|
||||
.relative()
|
||||
.flex()
|
||||
.size_full()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
v_flex()
|
||||
.w_96()
|
||||
.gap_10()
|
||||
.child(
|
||||
div()
|
||||
.w_80()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_8()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(shared_t!("login.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.key_description")),
|
||||
),
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(shared_t!("login.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(TextInput::new(&self.key_input))
|
||||
.child(
|
||||
Button::new("login")
|
||||
.label(t!("common.continue"))
|
||||
.primary()
|
||||
.loading(self.logging_in)
|
||||
.disabled(self.logging_in)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.login(window, cx);
|
||||
})),
|
||||
)
|
||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.approve_message", i = i)),
|
||||
)
|
||||
})
|
||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(red())
|
||||
.child(error),
|
||||
)
|
||||
}),
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.key_description")),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().p_1().child(
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded(cx.theme().radius)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_3()
|
||||
.text_center()
|
||||
.child(
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(TextInput::new(&self.input))
|
||||
.child(
|
||||
Button::new("login")
|
||||
.label(t!("common.continue"))
|
||||
.primary()
|
||||
.loading(self.logging_in)
|
||||
.disabled(self.logging_in)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.login(window, cx);
|
||||
})),
|
||||
)
|
||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.text_color(cx.theme().text)
|
||||
.child(shared_t!("login.nostr_connect")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.scan_qr")),
|
||||
),
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("login.approve_message", i = i)),
|
||||
)
|
||||
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.rounded_2xl()
|
||||
.shadow_md()
|
||||
.when(cx.theme().mode.is_dark(), |this| {
|
||||
this.shadow_none()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
})
|
||||
.bg(cx.theme().background)
|
||||
.child(img(qr).h_64())
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
this.connection_string.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
})
|
||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1()
|
||||
.child(TextInput::new(&self.relay_input).xsmall())
|
||||
.child(
|
||||
Button::new("change")
|
||||
.label(t!("common.change"))
|
||||
.ghost()
|
||||
.xsmall()
|
||||
.on_click(cx.listener(
|
||||
move |this, _, window, cx| {
|
||||
this.change_relay(window, cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod account;
|
||||
pub mod backup_keys;
|
||||
pub mod chat;
|
||||
pub mod compose;
|
||||
@@ -9,6 +10,5 @@ pub mod onboarding;
|
||||
pub mod preferences;
|
||||
pub mod screening;
|
||||
pub mod sidebar;
|
||||
pub mod startup;
|
||||
pub mod user_profile;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use anyhow::anyhow;
|
||||
use common::nip96::nip96_upload;
|
||||
use global::constants::ACCOUNT_IDENTIFIER;
|
||||
use global::nostr_client;
|
||||
use gpui::{
|
||||
div, relative, rems, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity,
|
||||
@@ -7,8 +8,7 @@ use gpui::{
|
||||
Render, SharedString, Styled, WeakEntity, Window,
|
||||
};
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
@@ -17,9 +17,12 @@ use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::modal::ModalButtonProps;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{divider, v_flex, ContextModal, Disableable, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::views::backup_keys::BackupKeys;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
NewAccount::new(window, cx)
|
||||
}
|
||||
@@ -27,12 +30,11 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
|
||||
pub struct NewAccount {
|
||||
name_input: Entity<InputState>,
|
||||
avatar_input: Entity<InputState>,
|
||||
is_uploading: bool,
|
||||
is_submitting: bool,
|
||||
temp_keys: Entity<Keys>,
|
||||
uploading: bool,
|
||||
submitting: bool,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -42,43 +44,120 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.placeholder(SharedString::new(t!("profile.placeholder_name")))
|
||||
});
|
||||
|
||||
let avatar_input =
|
||||
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.png"));
|
||||
let temp_keys = cx.new(|_| Keys::generate());
|
||||
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
|
||||
let avatar_input = cx.new(|cx| InputState::new(window, cx));
|
||||
|
||||
Self {
|
||||
name_input,
|
||||
avatar_input,
|
||||
is_uploading: false,
|
||||
is_submitting: false,
|
||||
temp_keys,
|
||||
uploading: false,
|
||||
submitting: false,
|
||||
name: "New Account".into(),
|
||||
closable: true,
|
||||
zoomable: true,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn create(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.submitting(true, cx);
|
||||
|
||||
let identity = Identity::global(cx);
|
||||
let keys = self.temp_keys.read(cx).clone();
|
||||
let view = cx.new(|cx| BackupKeys::new(&keys, window, cx));
|
||||
let weak_view = view.downgrade();
|
||||
let current_view = cx.entity().downgrade();
|
||||
|
||||
window.open_modal(cx, move |modal, _window, _cx| {
|
||||
let weak_view = weak_view.clone();
|
||||
let current_view = current_view.clone();
|
||||
modal
|
||||
.alert()
|
||||
.title(shared_t!("new_account.backup_label"))
|
||||
.child(view.clone())
|
||||
.button_props(
|
||||
ModalButtonProps::default().ok_text(t!("new_account.backup_download")),
|
||||
)
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
let password = this.password(cx);
|
||||
let current_view = current_view.clone();
|
||||
|
||||
if let Some(task) = this.backup(window, cx) {
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
task.await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
current_view
|
||||
.update(cx, |this, cx| {
|
||||
this.set_signer(password, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
// true to close the modal
|
||||
false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_signer(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.close_modal(cx);
|
||||
|
||||
let keys = self.temp_keys.read(cx).clone();
|
||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||
let name = self.name_input.read(cx).value().to_string();
|
||||
|
||||
// Build metadata
|
||||
let mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
||||
|
||||
if let Ok(url) = Url::parse(&avatar) {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
identity.update(cx, |this, cx| {
|
||||
this.new_identity(metadata, window, cx);
|
||||
});
|
||||
// Encrypt and save user secret key to disk
|
||||
self.write_keys_to_disk(&keys, password, cx);
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
client.set_signer(keys).await;
|
||||
client.set_metadata(&metadata).await.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||
let keys = keys.to_owned();
|
||||
let public_key = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let client = nostr_client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -150,12 +229,12 @@ impl NewAccount {
|
||||
}
|
||||
|
||||
fn submitting(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_submitting = status;
|
||||
self.submitting = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn uploading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.is_uploading = status;
|
||||
self.uploading = status;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -169,14 +248,6 @@ impl Panel for NewAccount {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
@@ -208,7 +279,7 @@ impl Render for NewAccount {
|
||||
.text_center()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("new_account.title"))),
|
||||
.child(shared_t!("new_account.title")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -218,17 +289,13 @@ impl Render for NewAccount {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("new_account.name")))
|
||||
.child(shared_t!("new_account.name"))
|
||||
.child(TextInput::new(&self.name_input).small()),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child(SharedString::new(t!("new_account.avatar"))),
|
||||
)
|
||||
.child(div().text_sm().child(shared_t!("new_account.avatar")))
|
||||
.child(
|
||||
v_flex()
|
||||
.p_1()
|
||||
@@ -252,8 +319,8 @@ impl Render for NewAccount {
|
||||
.ghost()
|
||||
.small()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.disabled(self.is_submitting)
|
||||
.loading(self.is_uploading)
|
||||
.disabled(self.submitting || self.uploading)
|
||||
.loading(self.uploading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.upload(window, cx);
|
||||
})),
|
||||
@@ -263,12 +330,12 @@ impl Render for NewAccount {
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
Button::new("submit")
|
||||
.label(SharedString::new(t!("common.continue")))
|
||||
.label(t!("common.continue"))
|
||||
.primary()
|
||||
.loading(self.is_submitting)
|
||||
.disabled(self.is_submitting || self.is_uploading)
|
||||
.loading(self.submitting)
|
||||
.disabled(self.submitting || self.uploading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.submit(window, cx);
|
||||
this.create(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
use anyhow::anyhow;
|
||||
use common::display::DisplayProfile;
|
||||
use global::constants::ACCOUNT_D;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::TextUtils;
|
||||
use global::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use global::nostr_client;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||
StatefulInteractiveElement, Styled, Window,
|
||||
div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use i18n::{shared_t, t};
|
||||
use identity::Identity;
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use nostr_connect::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonRounded, ButtonVariants};
|
||||
use ui::checkbox::Checkbox;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
|
||||
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
|
||||
@@ -28,13 +27,47 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NostrConnectApp {
|
||||
Nsec(String),
|
||||
Amber(String),
|
||||
Aegis(String),
|
||||
}
|
||||
|
||||
impl NostrConnectApp {
|
||||
pub fn all() -> Vec<Self> {
|
||||
vec![
|
||||
NostrConnectApp::Nsec("https://nsec.app".to_string()),
|
||||
NostrConnectApp::Amber("https://github.com/greenart7c3/Amber".to_string()),
|
||||
NostrConnectApp::Aegis("https://github.com/ZharlieW/Aegis".to_string()),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &str {
|
||||
match self {
|
||||
Self::Nsec(url) | Self::Amber(url) | Self::Aegis(url) => url,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> String {
|
||||
match self {
|
||||
NostrConnectApp::Nsec(_) => "nsec.app (Desktop)".into(),
|
||||
NostrConnectApp::Amber(_) => "Amber (Android)".into(),
|
||||
NostrConnectApp::Aegis(_) => "Aegis (iOS)".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Onboarding {
|
||||
nostr_connect_uri: Entity<NostrConnectURI>,
|
||||
nostr_connect: Entity<Option<NostrConnect>>,
|
||||
qr_code: Entity<Option<Arc<Image>>>,
|
||||
connecting: bool,
|
||||
// Panel
|
||||
name: SharedString,
|
||||
local_account: Entity<Option<Profile>>,
|
||||
loading: bool,
|
||||
closable: bool,
|
||||
zoomable: bool,
|
||||
focus_handle: FocusHandle,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
@@ -43,63 +76,176 @@ impl Onboarding {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let local_account = cx.new(|_| None);
|
||||
let nostr_connect = cx.new(|_| None);
|
||||
let qr_code = 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) = nostr_client().database().query(filter).await?.first_owned() {
|
||||
let public_key = event
|
||||
.tags
|
||||
.public_keys()
|
||||
.copied()
|
||||
.collect_vec()
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap();
|
||||
|
||||
let metadata = nostr_client()
|
||||
.database()
|
||||
.metadata(public_key)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Profile::new(public_key, metadata))
|
||||
} else {
|
||||
Err(anyhow!("Not found"))
|
||||
}
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
// Direct connection initiated by the client
|
||||
let nostr_connect_uri = cx.new(|cx| {
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let app_keys = ClientKeys::read_global(cx).keys();
|
||||
NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME)
|
||||
});
|
||||
|
||||
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();
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
// Clean up when the current view is released
|
||||
subscriptions.push(cx.on_release_in(window, |this, window, cx| {
|
||||
this.shutdown_nostr_connect(window, cx);
|
||||
}));
|
||||
|
||||
// Set Nostr Connect after the view is initialized
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.set_connect(window, cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
local_account,
|
||||
nostr_connect,
|
||||
nostr_connect_uri,
|
||||
qr_code,
|
||||
subscriptions,
|
||||
connecting: false,
|
||||
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;
|
||||
fn set_connecting(&mut self, cx: &mut Context<Self>) {
|
||||
self.connecting = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let uri = self.nostr_connect_uri.read(cx).clone();
|
||||
let app_keys = ClientKeys::read_global(cx).keys();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
|
||||
self.qr_code.update(cx, |this, cx| {
|
||||
*this = uri.to_string().to_qr();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.nostr_connect.update(cx, |this, cx| {
|
||||
*this = NostrConnect::new(uri, app_keys, timeout, None).ok();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let client = nostr_client();
|
||||
let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone());
|
||||
|
||||
if let Ok(Some(signer)) = connect {
|
||||
match signer.bunker_uri().await {
|
||||
Ok(uri) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_connecting(cx);
|
||||
this.write_uri_to_disk(&uri, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// Set the client's signer with the current nostr connect instance
|
||||
client.set_signer(signer).await;
|
||||
}
|
||||
Err(e) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_proxy(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
Identity::start_browser_proxy(cx);
|
||||
}
|
||||
|
||||
fn write_uri_to_disk(&mut self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
|
||||
let Some(public_key) = uri.remote_signer_public_key().cloned() else {
|
||||
log::error!("Remote Signer's public key not found");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut value = uri.to_string();
|
||||
|
||||
// Clear the secret param if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
value = value.replace(secret, "");
|
||||
}
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = nostr_client();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn copy_uri(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
self.nostr_connect_uri.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
}
|
||||
|
||||
fn shutdown_nostr_connect(&mut self, _window: &mut Window, cx: &mut App) {
|
||||
if !self.connecting {
|
||||
if let Some(signer) = self.nostr_connect.read(cx).clone() {
|
||||
cx.background_spawn(async move {
|
||||
log::info!("Shutting down Nostr Connect");
|
||||
signer.shutdown().await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_apps(&self, cx: &Context<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
let all_apps = NostrConnectApp::all();
|
||||
let mut items = Vec::with_capacity(all_apps.len());
|
||||
|
||||
for (ix, item) in all_apps.into_iter().enumerate() {
|
||||
items.push(self.render_app(ix, item.as_str(), item.url(), cx));
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn render_app<T>(&self, ix: usize, label: T, url: &str, cx: &Context<Self>) -> impl IntoElement
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
div()
|
||||
.id(ix)
|
||||
.flex_1()
|
||||
.rounded_md()
|
||||
.py_0p5()
|
||||
.px_2()
|
||||
.bg(cx.theme().ghost_element_background_alt)
|
||||
.child(label.into())
|
||||
.on_click({
|
||||
let url = url.to_owned();
|
||||
move |_e, _window, cx| {
|
||||
cx.open_url(&url);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Onboarding {
|
||||
@@ -111,14 +257,6 @@ impl Panel for Onboarding {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn closable(&self, _cx: &App) -> bool {
|
||||
self.closable
|
||||
}
|
||||
|
||||
fn zoomable(&self, _cx: &App) -> bool {
|
||||
self.zoomable
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
@@ -134,159 +272,167 @@ impl Focusable for Onboarding {
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let auto_login = AppSettings::get_auto_login(cx);
|
||||
let proxy = AppSettings::get_proxy_user_avatars(cx);
|
||||
|
||||
div()
|
||||
.py_4()
|
||||
h_flex()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.mb_10()
|
||||
.flex()
|
||||
.flex_col()
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.gap_10()
|
||||
.items_center()
|
||||
.gap_4()
|
||||
.justify_center()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_16()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_center()
|
||||
v_flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(SharedString::new(t!("welcome.title"))),
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_16()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("welcome.subtitle"))),
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.text_xl()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(shared_t!("welcome.title")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("welcome.subtitle")),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.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(SharedString::new(t!(
|
||||
"onboarding.choose_account"
|
||||
)))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.font_semibold()
|
||||
.child(
|
||||
Avatar::new(profile.avatar_url(proxy))
|
||||
.size(rems(1.5)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.pb_px()
|
||||
.child(profile.display_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(SharedString::new(t!("onboarding.auto_login")))
|
||||
.checked(auto_login)
|
||||
.on_click(move |_, _window, cx| {
|
||||
AppSettings::update_auto_login(!auto_login, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div().w_24().absolute().bottom_2().right_2().child(
|
||||
Button::new("logout")
|
||||
.icon(IconName::Logout)
|
||||
.label(SharedString::new(t!("user.sign_out")))
|
||||
.danger()
|
||||
.xsmall()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.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(
|
||||
v_flex()
|
||||
.w_80()
|
||||
.gap_3()
|
||||
.child(
|
||||
Button::new("continue_btn")
|
||||
.icon(Icon::new(IconName::ArrowRight))
|
||||
.label(SharedString::new(t!("onboarding.start_messaging")))
|
||||
.label(shared_t!("onboarding.start_messaging"))
|
||||
.primary()
|
||||
.large()
|
||||
.bold()
|
||||
.reverse()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::new_account(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label(SharedString::new(t!("onboarding.already_have_account")))
|
||||
.ghost()
|
||||
.underline()
|
||||
h_flex()
|
||||
.my_1()
|
||||
.gap_1()
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("onboarding.divider")),
|
||||
)
|
||||
.child(divider(cx)),
|
||||
)
|
||||
.child(
|
||||
Button::new("key")
|
||||
.label(t!("onboarding.key_login"))
|
||||
.ghost_alt()
|
||||
.on_click(cx.listener(move |_, _, window, cx| {
|
||||
chatspace::login(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("ext")
|
||||
.label(t!("onboarding.ext_login"))
|
||||
.ghost_alt()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.set_proxy(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("onboarding.ext_login_note")),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
})
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.relative()
|
||||
.p_2()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.rounded_2xl()
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.bg(cx.theme().surface_background)
|
||||
.rounded_2xl()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_5()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when_some(self.qr_code.read(cx).as_ref(), |this, qr| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.child(
|
||||
img(qr.clone())
|
||||
.size(px(256.))
|
||||
.rounded_xl()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().element_active),
|
||||
)
|
||||
.on_click(cx.listener(
|
||||
move |this, _e, window, cx| {
|
||||
this.copy_uri(window, cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.3))
|
||||
.child(shared_t!("onboarding.nostr_connect")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("onboarding.scan_qr")),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.justify_center()
|
||||
.children(self.render_apps(cx)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use common::display::DisplayProfile;
|
||||
use gpui::http_client::Url;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, px, relative, rems, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
@@ -116,9 +115,8 @@ impl Preferences {
|
||||
impl Render for Preferences {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let input_state = self.media_input.downgrade();
|
||||
let profile = Identity::read_global(cx)
|
||||
.public_key()
|
||||
.map(|pk| Registry::read_global(cx).get_person(&pk, cx));
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let profile = Registry::read_global(cx).get_person(&identity, cx);
|
||||
|
||||
let backup_messages = AppSettings::get_backup_messages(cx);
|
||||
let screening = AppSettings::get_screening(cx);
|
||||
@@ -138,58 +136,56 @@ impl Render for Preferences {
|
||||
.font_semibold()
|
||||
.child(SharedString::new(t!("preferences.account_header"))),
|
||||
)
|
||||
.when_some(profile, |this, profile| {
|
||||
this.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.id("current-user")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Avatar::new(profile.avatar_url(proxy_avatar))
|
||||
.size(rems(2.4)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.line_height(relative(1.3))
|
||||
.font_semibold()
|
||||
.child(profile.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.line_height(relative(1.3))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!(
|
||||
"preferences.see_your_profile"
|
||||
))),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_edit_profile(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("relays")
|
||||
.label("Messaging Relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_relays(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.child(
|
||||
div()
|
||||
.id("current-user")
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Avatar::new(profile.avatar_url(proxy_avatar))
|
||||
.size(rems(2.4)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.line_height(relative(1.3))
|
||||
.font_semibold()
|
||||
.child(profile.display_name()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.line_height(relative(1.3))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!(
|
||||
"preferences.see_your_profile"
|
||||
))),
|
||||
),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_edit_profile(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("relays")
|
||||
.label("Messaging Relays")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.open_relays(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
|
||||
@@ -40,10 +40,7 @@ impl Screening {
|
||||
}
|
||||
|
||||
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Skip if user isn't logged in
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
};
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let public_key = self.public_key;
|
||||
|
||||
let check_trust_score: Task<(bool, usize, bool)> = cx.background_spawn(async move {
|
||||
|
||||
@@ -211,13 +211,7 @@ impl Sidebar {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
// User is not logged in. Stop searching
|
||||
self.set_finding(false, window, cx);
|
||||
self.set_cancel_handle(None, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let query = query.to_owned();
|
||||
let query_cloned = query.clone();
|
||||
|
||||
@@ -277,13 +271,7 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
fn search_by_nip05(&mut self, query: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
// User is not logged in. Stop searching
|
||||
self.set_finding(false, window, cx);
|
||||
self.set_cancel_handle(None, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let address = query.to_owned();
|
||||
|
||||
let task = Tokio::spawn(cx, async move {
|
||||
@@ -337,12 +325,7 @@ impl Sidebar {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
// User is not logged in. Stop searching
|
||||
self.set_finding(false, window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let task: Task<Result<Room, Error>> = cx.background_spawn(async move {
|
||||
// Create a gift wrap event to represent as room
|
||||
Self::create_temp_room(identity, public_key).await
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Window,
|
||||
};
|
||||
use i18n::t;
|
||||
use identity::Identity;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{Sizable, StyledExt};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||
Startup::new(window, cx)
|
||||
}
|
||||
|
||||
pub struct Startup {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl Startup {
|
||||
fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self {
|
||||
name: "Startup".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Startup {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &App) -> AnyElement {
|
||||
self.name.clone().into_any_element()
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
menu.track_focus(&self.focus_handle)
|
||||
}
|
||||
|
||||
fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec<Button> {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for Startup {}
|
||||
|
||||
impl Focusable for Startup {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Startup {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let identity = Identity::global(cx);
|
||||
let logging_in = identity.read(cx).logging_in();
|
||||
|
||||
div()
|
||||
.relative()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_12()
|
||||
.text_color(cx.theme().elevated_surface_background),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_24()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.when(logging_in, |this| {
|
||||
this.child(
|
||||
div().text_sm().text_color(cx.theme().text).child(
|
||||
SharedString::new(t!("startup.auto_login_in_progress")),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(Indicator::new().small()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().absolute().bottom_3().right_3().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(SharedString::new(t!("startup.stuck"))),
|
||||
)
|
||||
.child(
|
||||
Button::new("reset")
|
||||
.label(SharedString::new(t!("startup.reset")))
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(|_, window, cx| {
|
||||
Identity::global(cx).update(cx, |this, cx| {
|
||||
this.unload(window, cx);
|
||||
// Restart application
|
||||
cx.restart();
|
||||
});
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,7 @@ impl UserProfile {
|
||||
}
|
||||
|
||||
pub fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// Skip if user isn't logged in
|
||||
let Some(identity) = Identity::read_global(cx).public_key() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let identity = Identity::read_global(cx).public_key();
|
||||
let public_key = self.public_key;
|
||||
|
||||
let check_follow: Task<bool> = cx.background_spawn(async move {
|
||||
|
||||
Reference in New Issue
Block a user