feat: Out-of-Box Experience (#12)

* refactor app view

* feat: onboarding

* add back buttons in onboarding
This commit is contained in:
reya
2025-03-25 12:34:39 +07:00
committed by GitHub
parent e15cbcc22c
commit 00cf7792e5
34 changed files with 1680 additions and 1920 deletions

17
crates/account/Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "account"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
ui = { path = "../ui" }
common = { path = "../common" }
global = { path = "../global" }
gpui.workspace = true
nostr-sdk.workspace = true
anyhow.workspace = true
smol.workspace = true
smallvec.workspace = true
log.workspace = true

166
crates/account/src/lib.rs Normal file
View File

@@ -0,0 +1,166 @@
use std::time::Duration;
use anyhow::Error;
use common::profile::NostrProfile;
use global::{
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
get_client,
};
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
use nostr_sdk::prelude::*;
use ui::{notification::Notification, ContextModal};
struct GlobalAccount(Entity<Account>);
impl Global for GlobalAccount {}
pub fn init(cx: &mut App) {
Account::set_global(cx.new(|_| Account { profile: None }), cx);
}
#[derive(Debug, Clone)]
pub struct Account {
pub profile: Option<NostrProfile>,
}
impl Account {
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalAccount>().0.clone()
}
pub fn set_global(account: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalAccount(account));
}
pub fn login<S>(&mut self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
{
let task: Task<Result<NostrProfile, Error>> = cx.background_spawn(async move {
let client = get_client();
// Use user's signer for main signer
_ = client.set_signer(signer).await;
// Verify nostr signer and get public key
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// Fetch user's metadata
let metadata = client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata))
});
cx.spawn_in(window, |this, mut cx| async move {
match task.await {
Ok(profile) => {
cx.update(|_, cx| {
this.update(cx, |this, cx| {
this.profile = Some(profile);
this.subscribe(cx);
cx.notify();
})
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(Notification::error(e.to_string()), cx)
})
.ok();
}
}
})
.detach();
}
pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let keys = Keys::generate();
let task: Task<Result<NostrProfile, Error>> = cx.background_spawn(async move {
let public_key = keys.public_key();
// Update signer
client.set_signer(keys).await;
// Set metadata
client.set_metadata(&metadata).await?;
Ok(NostrProfile::new(public_key, metadata))
});
cx.spawn_in(window, |this, mut cx| async move {
if let Ok(profile) = task.await {
cx.update(|_, cx| {
this.update(cx, |this, cx| {
this.profile = Some(profile);
this.subscribe(cx);
cx.notify();
})
})
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(Notification::error("Failed to create account."), cx)
})
.ok();
}
})
.detach();
}
pub fn subscribe(&self, cx: &Context<Self>) {
let Some(profile) = self.profile.as_ref() else {
return;
};
let client = get_client();
let user = profile.public_key;
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
// Create a contact list filter
let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1);
// Create a user's data filter
let data = Filter::new()
.author(user)
.since(Timestamp::now())
.kinds(vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::RelayList,
]);
// Create a filter for getting all gift wrapped events send to current user
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user);
// Create a filter to continuously receive new messages.
let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Only subscribe to the latest contact list
client.subscribe(contacts, Some(opts)).await?;
// Continuously receive new user's data since now
client.subscribe(data, None).await?;
let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
client.subscribe_with_id(sub_id, msg, Some(opts)).await?;
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
client.subscribe_with_id(sub_id, new_msg, None).await?;
Ok(())
});
cx.spawn(|_, _| async move {
if let Err(e) = task.await {
log::error!("Error: {}", e);
}
})
.detach();
}
}