chore: refactor account and fixes

This commit is contained in:
2025-04-21 15:18:02 +07:00
parent a30f2dcc8a
commit 87f038248c
10 changed files with 270 additions and 184 deletions

View File

@@ -31,6 +31,7 @@ impl Account {
cx.set_global(GlobalAccount(account));
}
/// Login to the account using the given signer.
pub fn login<S>(&mut self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where
S: NostrSigner + 'static,
@@ -56,12 +57,15 @@ impl Account {
cx.spawn_in(window, async move |this, cx| match task.await {
Ok(profile) => {
cx.update(|_, cx| {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile = Some(profile);
this.subscribe(cx);
cx.notify();
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
}
@@ -75,28 +79,80 @@ impl Account {
.detach();
}
/// Create a new account with the given metadata.
pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
const DEFAULT_NIP_65_RELAYS: [&str; 4] = [
"wss://relay.damus.io",
"wss://relay.primal.net",
"wss://relay.nostr.net",
"wss://nos.lol",
];
const DEFAULT_MESSAGING_RELAYS: [&str; 2] =
["wss://auth.nostr1.com", "wss://relay.0xchat.com"];
let keys = Keys::generate();
let public_key = keys.public_key();
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let public_key = keys.public_key();
let client = get_client();
// Update signer
client.set_signer(keys).await;
// Set metadata
client.set_metadata(&metadata).await?;
// Create relay list
let tags: Vec<Tag> = DEFAULT_NIP_65_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay_metadata(url, None))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::RelayList, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
};
// Create messaging relay list
let tags: Vec<Tag> = DEFAULT_MESSAGING_RELAYS
.into_iter()
.filter_map(|url| {
if let Ok(url) = RelayUrl::parse(url) {
Some(Tag::relay(url))
} else {
None
}
})
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send messaging relay list event: {}", e);
};
Ok(Profile::new(public_key, metadata))
});
cx.spawn_in(window, async move |this, cx| {
if let Ok(profile) = task.await {
cx.update(|_, cx| {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.profile = Some(profile);
this.subscribe(cx);
cx.notify();
this.profile(profile, cx);
cx.defer_in(window, |this, _, cx| {
this.subscribe(cx);
});
})
.ok();
})
.ok();
} else {
@@ -109,12 +165,18 @@ impl Account {
.detach();
}
pub fn subscribe(&self, cx: &Context<Self>) {
/// Sets the profile for the account.
pub fn profile(&mut self, profile: Profile, cx: &mut Context<Self>) {
self.profile = Some(profile);
cx.notify();
}
/// Subscribes to the current account's metadata.
pub fn subscribe(&self, cx: &mut 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);
@@ -145,6 +207,7 @@ impl Account {
let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0);
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client();
client.subscribe(metadata, Some(opts)).await?;
client.subscribe(data, None).await?;

View File

@@ -337,13 +337,7 @@ impl Room {
.author(*pubkey)
.limit(1);
let is_ready = client
.database()
.query(filter)
.await
.ok()
.and_then(|events| events.first_owned())
.is_some();
let is_ready = client.database().query(filter).await?.first().is_some();
result.push((*pubkey, is_ready));
}

View File

@@ -1,8 +1,11 @@
use account::Account;
use anyhow::Error;
use global::get_client;
use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Window,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use std::sync::Arc;
@@ -13,7 +16,7 @@ use ui::{
ContextModal, IconName, Root, Sizable, TitleBar,
};
use crate::views::{chat, compose, contacts, profile, relays, welcome};
use crate::views::{chat, compose, contacts, login, new_account, profile, relays, welcome};
use crate::views::{onboarding, sidebar};
const MODAL_WIDTH: f32 = 420.;
@@ -25,6 +28,16 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
ChatSpace::new(window, cx)
}
pub fn login(window: &mut Window, cx: &mut App) {
let panel = login::init(window, cx);
ChatSpace::set_center_panel(panel, window, cx);
}
pub fn new_account(window: &mut Window, cx: &mut App) {
let panel = new_account::init(window, cx);
ChatSpace::set_center_panel(panel, window, cx);
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub enum PanelKind {
Room(u64),
@@ -37,6 +50,7 @@ pub enum ModalKind {
Compose,
Contact,
Relay,
SetupRelay,
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
@@ -65,10 +79,17 @@ pub struct ChatSpace {
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let account = Account::global(cx);
let dock = cx.new(|cx| DockArea::new(window, cx));
let dock = cx.new(|cx| {
let panel = Arc::new(onboarding::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
});
cx.new(|cx| {
let account = Account::global(cx);
let mut subscriptions = smallvec![];
subscriptions.push(cx.observe_in(
@@ -83,35 +104,17 @@ impl ChatSpace {
},
));
let mut this = Self {
Self {
dock,
subscriptions,
titlebar: false,
};
if Account::global(cx).read(cx).profile.is_some() {
this.open_chats(window, cx);
} else {
this.open_onboarding(window, cx);
}
this
})
}
pub fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
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);
});
});
}
}
fn show_titlebar(&mut self, cx: &mut Context<Self>) {
self.titlebar = true;
cx.notify();
}
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -147,31 +150,44 @@ impl ChatSpace {
this.set_left_dock(left, Some(px(SIDEBAR_WIDTH)), true, window, cx);
this.set_center(center, window, cx);
});
}
fn show_titlebar(&mut self, cx: &mut Context<Self>) {
self.titlebar = true;
cx.notify();
}
cx.defer_in(window, |this, window, cx| {
let verify_messaging_relays = this.verify_messaging_relays(cx);
fn render_appearance_btn(&self, cx: &mut Context<Self>) -> impl IntoElement {
Button::new("appearance")
.xsmall()
.ghost()
.map(|this| {
if cx.theme().appearance.is_dark() {
this.icon(IconName::Sun)
} else {
this.icon(IconName::Moon)
cx.spawn_in(window, async move |_, cx| {
if let Ok(status) = verify_messaging_relays.await {
if !status {
cx.update(|window, cx| {
window.dispatch_action(
Box::new(ToggleModal {
modal: ModalKind::SetupRelay,
}),
cx,
);
})
.ok();
}
}
})
.on_click(cx.listener(|_, _, window, cx| {
if cx.theme().appearance.is_dark() {
Theme::change(Appearance::Light, Some(window), cx);
} else {
Theme::change(Appearance::Dark, Some(window), cx);
}
}))
.detach();
});
}
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let exist = client.database().query(filter).await?.first().is_some();
Ok(exist)
})
}
fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
@@ -235,8 +251,32 @@ impl ChatSpace {
.child(relays.clone())
});
}
ModalKind::SetupRelay => {
let relays = relays::init(window, cx);
window.open_modal(cx, move |this, _, _| {
this.width(px(MODAL_WIDTH))
.title("Your Messaging Relays are not configured")
.child(relays.clone())
});
}
};
}
fn set_center_panel<P: PanelView>(panel: P, window: &mut Window, cx: &mut App) {
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 {
@@ -266,7 +306,33 @@ impl Render for ChatSpace {
.justify_end()
.gap_2()
.px_2()
.child(self.render_appearance_btn(cx)),
.child(
Button::new("appearance")
.xsmall()
.ghost()
.map(|this| {
if cx.theme().appearance.is_dark() {
this.icon(IconName::Sun)
} else {
this.icon(IconName::Moon)
}
})
.on_click(cx.listener(|_, _, window, cx| {
if cx.theme().appearance.is_dark() {
Theme::change(
Appearance::Light,
Some(window),
cx,
);
} else {
Theme::change(
Appearance::Dark,
Some(window),
cx,
);
}
})),
),
),
)
})

View File

@@ -27,7 +27,7 @@ use std::{collections::HashSet, mem, sync::Arc, time::Duration};
use ui::{theme::Theme, Root};
pub(crate) mod asset;
pub(crate) mod chat_space;
pub(crate) mod chatspace;
pub(crate) mod views;
actions!(coop, [Quit]);
@@ -84,7 +84,7 @@ fn main() {
};
let filter = Filter::new()
.kind(Kind::ArticlesCurationSet)
.kind(Kind::ReleaseArtifactSet)
.coordinate(&coordinate)
.limit(1);
@@ -334,7 +334,7 @@ fn main() {
})
.detach();
Root::new(chat_space::init(window, cx).into(), window, cx)
Root::new(chatspace::init(window, cx).into(), window, cx)
})
})
.expect("Failed to open window. Please restart the application.");

View File

@@ -178,6 +178,7 @@ impl Login {
}
} else if content.starts_with("bunker://") {
let keys = get_client_keys().to_owned();
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
self.set_error_message("Bunker URL is not valid".to_owned(), cx);
self.set_logging_in(false, cx);
@@ -196,8 +197,8 @@ impl Login {
}
}
} else {
self.set_logging_in(false, cx);
window.push_notification(Notification::error(INPUT_INVALID), cx);
self.set_logging_in(false, cx);
};
}

View File

@@ -3,7 +3,7 @@ use async_utility::task::spawn;
use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{
div, img, prelude::FluentBuilder, px, relative, AnyElement, App, AppContext, Context, Entity,
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
Render, SharedString, Styled, Window,
};
@@ -19,9 +19,6 @@ use ui::{
Disableable, Icon, IconName, Sizable, Size, StyledExt,
};
const STEAM_ID_DESCRIPTION: &str =
"Steam ID is used to get your currently playing game and update your status.";
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
NewAccount::new(window, cx)
}
@@ -30,7 +27,6 @@ pub struct NewAccount {
name_input: Entity<TextInput>,
avatar_input: Entity<TextInput>,
bio_input: Entity<TextInput>,
steam_input: Entity<TextInput>,
is_uploading: bool,
is_submitting: bool,
// Panel
@@ -59,12 +55,6 @@ impl NewAccount {
.placeholder("https://example.com/avatar.jpg")
});
let steam_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::Small)
.placeholder("76561199810385277")
});
let bio_input = cx.new(|cx| {
TextInput::new(window, cx)
.text_size(Size::Small)
@@ -75,7 +65,6 @@ impl NewAccount {
Self {
name_input,
avatar_input,
steam_input,
bio_input,
is_uploading: false,
is_submitting: false,
@@ -92,12 +81,8 @@ impl NewAccount {
let avatar = self.avatar_input.read(cx).text().to_string();
let name = self.name_input.read(cx).text().to_string();
let bio = self.bio_input.read(cx).text().to_string();
let steam = self.steam_input.read(cx).text().to_string();
let mut metadata = Metadata::new()
.display_name(name)
.about(bio)
.custom_field("steam", steam);
let mut metadata = Metadata::new().display_name(name).about(bio);
if let Ok(url) = Url::from_str(&avatar) {
metadata = metadata.picture(url);
@@ -126,6 +111,7 @@ impl NewAccount {
this.update(cx, |this, cx| {
this.set_uploading(false, cx);
})
.ok();
})
.ok();
@@ -299,21 +285,6 @@ impl Render for NewAccount {
.child("Bio:")
.child(self.bio_input.clone()),
)
.child(
div()
.flex()
.flex_col()
.gap_1()
.text_sm()
.child("Steam ID:")
.child(self.steam_input.clone())
.child(
div()
.text_size(px(10.))
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
.child(STEAM_ID_DESCRIPTION),
),
)
.child(
div()
.my_2()

View File

@@ -10,13 +10,7 @@ use ui::{
Icon, IconName, StyledExt,
};
use crate::chat_space::ChatSpace;
use super::{login, new_account};
const LOGO_URL: &str = "brand/coop.svg";
const TITLE: &str = "Welcome to Coop!";
const SUBTITLE: &str = "Secure Communication on Nostr.";
use crate::chatspace;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
Onboarding::new(window, cx)
@@ -42,16 +36,6 @@ impl Onboarding {
focus_handle: cx.focus_handle(),
}
}
fn open_new_account(&self, window: &mut Window, cx: &mut Context<Self>) {
let new_account = new_account::init(window, cx);
ChatSpace::set_center_panel(new_account, window, cx);
}
fn open_login(&self, window: &mut Window, cx: &mut Context<Self>) {
let login = login::init(window, cx);
ChatSpace::set_center_panel(login, window, cx);
}
}
impl Panel for Onboarding {
@@ -74,10 +58,6 @@ impl Panel for Onboarding {
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 Onboarding {}
@@ -90,6 +70,9 @@ impl Focusable for Onboarding {
impl Render for Onboarding {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
const TITLE: &str = "Welcome to Coop!";
const SUBTITLE: &str = "Secure Communication on Nostr.";
div()
.py_4()
.size_full()
@@ -106,7 +89,7 @@ impl Render for Onboarding {
.gap_4()
.child(
svg()
.path(LOGO_URL)
.path("brand/coop.svg")
.size_16()
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
)
@@ -139,8 +122,8 @@ impl Render for Onboarding {
.label("Start Messaging")
.primary()
.reverse()
.on_click(cx.listener(move |this, _, window, cx| {
this.open_new_account(window, cx);
.on_click(cx.listener(move |_, _, window, cx| {
chatspace::new_account(window, cx);
})),
)
.child(
@@ -148,8 +131,8 @@ impl Render for Onboarding {
.label("Already have an account? Log in.")
.ghost()
.underline()
.on_click(cx.listener(move |this, _, window, cx| {
this.open_login(window, cx);
.on_click(cx.listener(move |_, _, window, cx| {
chatspace::login(window, cx);
})),
),
)

View File

@@ -1,4 +1,4 @@
use anyhow::{anyhow, Error};
use anyhow::Error;
use global::{constants::NEW_MESSAGE_SUB_ID, get_client};
use gpui::{
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
@@ -41,11 +41,6 @@ impl Relays {
});
let relays = cx.new(|cx| {
let relays = vec![
RelayUrl::parse("wss://auth.nostr1.com").unwrap(),
RelayUrl::parse("wss://relay.0xchat.com").unwrap(),
];
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
@@ -59,16 +54,18 @@ impl Relays {
if let Some(event) = client.database().query(filter).await?.first_owned() {
let relays = event
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|t| match t {
TagStandard::Relay(url) => Some(url.to_owned()),
_ => None,
})
.filter(TagKind::Relay)
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
.collect::<Vec<_>>();
Ok(relays)
} else {
Err(anyhow!("Messaging Relays not found."))
let relays = vec![
RelayUrl::parse("wss://auth.nostr1.com")?,
RelayUrl::parse("wss://relay.0xchat.com")?,
];
Ok(relays)
}
});
@@ -86,7 +83,7 @@ impl Relays {
})
.detach();
relays
vec![]
});
cx.new(|cx| {

View File

@@ -25,7 +25,7 @@ use ui::{
IconName, Sizable, StyledExt,
};
use crate::chat_space::{AddPanel, ModalKind, PanelKind, ToggleModal};
use crate::chatspace::{AddPanel, ModalKind, PanelKind, ToggleModal};
mod button;
mod folder;