chore: refactor the global state and improve signer (#56)

* refactor

* update

* .

* rustfmt

* .

* .

* .

* .

* .

* add document

* .

* add logout

* handle error

* chore: update gpui

* adjust timeout
This commit is contained in:
reya
2025-06-07 14:52:21 +07:00
committed by GitHub
parent 50beaebd2c
commit e687204361
73 changed files with 1871 additions and 1504 deletions

View File

@@ -14,7 +14,6 @@ theme = { path = "../theme" }
common = { path = "../common" }
global = { path = "../global" }
chats = { path = "../chats" }
account = { path = "../account" }
auto_update = { path = "../auto_update" }
gpui.workspace = true

View File

@@ -1,29 +1,27 @@
use std::sync::Arc;
use account::Account;
use anyhow::Error;
use chats::{ChatRegistry, RoomEmitter};
use global::{
constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH},
get_client,
};
use global::constants::{DEFAULT_MODAL_WIDTH, DEFAULT_SIDEBAR_WIDTH};
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
div, impl_internal_actions, px, App, AppContext, Axis, Context, Entity, InteractiveElement,
IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
};
use nostr_sdk::prelude::*;
use nostr_connect::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeMode};
use ui::{
button::{Button, ButtonVariants},
dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem},
ContextModal, IconName, Root, Sizable, TitleBar,
};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView;
use ui::dock_area::{DockArea, DockItem};
use ui::{ContextModal, IconName, Root, Sizable, TitleBar};
use crate::views::chat::{self, Chat};
use crate::views::{
chat::{self, Chat},
compose, login, new_account, onboarding, profile, relays, sidebar, welcome,
compose, login, new_account, onboarding, profile, relays, sidebar, startup, welcome,
};
impl_internal_actions!(dock, [ToggleModal]);
@@ -63,16 +61,16 @@ pub struct ToggleModal {
}
pub struct ChatSpace {
titlebar: bool,
dock: Entity<DockArea>,
titlebar: bool,
#[allow(unused)]
subscriptions: SmallVec<[Subscription; 3]>,
subscriptions: SmallVec<[Subscription; 2]>,
}
impl ChatSpace {
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let dock = cx.new(|cx| {
let panel = Arc::new(onboarding::init(window, 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
@@ -81,46 +79,39 @@ impl ChatSpace {
});
cx.new(|cx| {
let account = Account::global(cx);
let chats = ChatRegistry::global(cx);
let mut subscriptions = smallvec![];
subscriptions.push(cx.observe_in(
&account,
window,
|this: &mut ChatSpace, account, window, cx| {
if account.read(cx).profile.is_some() {
this.open_chats(window, cx);
} else {
this.open_onboarding(window, cx);
}
},
));
subscriptions.push(cx.subscribe_in(
&chats,
window,
|this, _state, event, window, cx| {
if let RoomEmitter::Open(room) = event {
if let Some(room) = room.upgrade() {
this.dock.update(cx, |this, cx| {
let panel = chat::init(room, window, cx);
this.add_panel(panel, DockPlacement::Center, window, cx);
});
} else {
window
.push_notification("Failed to open room. Please retry later.", cx);
}
}
},
));
subscriptions.push(cx.observe_new::<Chat>(|this, window, cx| {
// Automatically load messages when chat panel opens
subscriptions.push(cx.observe_new::<Chat>(|this: &mut Chat, window, cx| {
if let Some(window) = window {
this.load_messages(window, cx);
}
}));
// Subscribe to open chat room requests
subscriptions.push(cx.subscribe_in(
&chats,
window,
|this: &mut ChatSpace, _state, event, window, cx| {
if let RoomEmitter::Open(room) = event {
if let Some(room) = room.upgrade() {
this.dock.update(cx, |this, cx| {
let panel = chat::init(room, window, cx);
let placement = DockPlacement::Center;
this.add_panel(panel, placement, window, cx);
});
} else {
window.push_notification(
"Failed to open room. Please try again later.",
cx,
);
}
}
},
));
Self {
dock,
subscriptions,
@@ -129,12 +120,10 @@ impl ChatSpace {
})
}
fn show_titlebar(&mut self, cx: &mut Context<Self>) {
self.titlebar = true;
cx.notify();
}
pub fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Disable the titlebar
self.titlebar(false, cx);
fn open_onboarding(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let panel = Arc::new(onboarding::init(window, cx));
let center = DockItem::panel(panel);
@@ -144,8 +133,9 @@ impl ChatSpace {
});
}
fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.show_titlebar(cx);
pub fn open_chats(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// Enable the titlebar
self.titlebar(true, cx);
let weak_dock = self.dock.downgrade();
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
@@ -191,20 +181,28 @@ impl ChatSpace {
});
}
fn titlebar(&mut self, status: bool, cx: &mut Context<Self>) {
self.titlebar = status;
cx.notify();
}
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 signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let is_exist = shared_state()
.client
.database()
.query(filter)
.await?
.first()
.is_some();
let exist = client.database().query(filter).await?.first().is_some();
Ok(exist)
Ok(is_exist)
})
}
@@ -298,11 +296,12 @@ impl Render for ChatSpace {
.flex()
.items_center()
.justify_end()
.gap_2()
.gap_1p5()
.px_2()
.child(
Button::new("appearance")
.xsmall()
.tooltip("Change the app's appearance")
.small()
.ghost()
.map(|this| {
if cx.theme().mode.is_dark() {
@@ -326,6 +325,26 @@ impl Render for ChatSpace {
);
}
})),
)
.child(
Button::new("settings")
.tooltip("Open settings")
.small()
.ghost()
.icon(IconName::Settings),
)
.child(
Button::new("logout")
.tooltip("Log out")
.small()
.ghost()
.icon(IconName::Logout)
.on_click(cx.listener(move |_, _, _window, cx| {
cx.background_spawn(async move {
shared_state().unset_signer().await;
})
.detach();
})),
),
),
)

View File

@@ -1,17 +1,14 @@
use anyhow::{anyhow, Error};
use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use asset::Assets;
use auto_update::AutoUpdater;
use chats::ChatRegistry;
use futures::{select, FutureExt};
#[cfg(not(target_os = "linux"))]
use global::constants::APP_NAME;
use global::{
constants::{
ALL_MESSAGES_SUB_ID, APP_ID, APP_PUBKEY, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_SUB_ID, SEARCH_RELAYS,
},
get_client, init_global_state, profiles,
};
use global::constants::{APP_ID, KEYRING_BUNKER, KEYRING_USER_PATH};
use global::{shared_state, NostrSignal};
use gpui::{
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
WindowBounds, WindowKind, WindowOptions,
@@ -20,13 +17,7 @@ use gpui::{
use gpui::{point, SharedString, TitlebarOptions};
#[cfg(target_os = "linux")]
use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::{
async_utility::task::spawn, nips::nip01::Coordinate, pool::prelude::ReqExitPolicy, Client,
Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, Metadata, PublicKey, RelayMessage,
RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag,
};
use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
use nostr_connect::prelude::*;
use theme::Theme;
use ui::Root;
@@ -36,226 +27,27 @@ pub(crate) mod views;
actions!(coop, [Quit]);
#[derive(Debug)]
enum Signal {
/// Receive event
Event(Event),
/// Receive eose
Eose,
/// Receive app updates
AppUpdates(Event),
}
fn main() {
// Initialize logging
tracing_subscriber::fmt::init();
// Initialize global state
init_global_state();
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048);
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(500);
let client = get_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
// Spawn a task to establish relay connections
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
for relay in BOOTSTRAP_RELAYS.into_iter() {
if let Err(e) = client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
for relay in SEARCH_RELAYS.into_iter() {
if let Err(e) = client.add_relay(relay).await {
log::error!("Failed to add relay {}: {}", relay, e);
}
}
// Establish connection to bootstrap relays
client.connect().await;
log::info!("Connected to bootstrap relays");
log::info!("Subscribing to app updates...");
let coordinate = Coordinate {
kind: Kind::Custom(32267),
public_key: PublicKey::from_hex(APP_PUBKEY).expect("App Pubkey is invalid"),
identifier: APP_ID.into(),
};
let filter = Filter::new()
.kind(Kind::ReleaseArtifactSet)
.coordinate(&coordinate)
.limit(1);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to subscribe for app updates: {}", e);
}
// Initialize the Global State and process events in a separate thread.
// Must be run under async utility runtime
nostr_sdk::async_utility::task::spawn(async move {
shared_state().start().await;
});
// Spawn a task to handle metadata batching
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
let mut batch: HashSet<PublicKey> = HashSet::new();
loop {
let mut timeout =
Box::pin(Timer::after(Duration::from_millis(METADATA_BATCH_TIMEOUT)).fuse());
select! {
pubkeys = batch_rx.recv().fuse() => {
match pubkeys {
Ok(keys) => {
batch.extend(keys);
if batch.len() >= METADATA_BATCH_LIMIT {
sync_metadata(mem::take(&mut batch), client, opts).await;
}
}
Err(_) => break,
}
}
_ = timeout => {
if !batch.is_empty() {
sync_metadata(mem::take(&mut batch), client, opts).await;
}
}
}
}
});
// Spawn a task to handle relay pool notification
// NOTE: Use `async_utility` instead of `smol-rs`
spawn(async move {
let keys = Keys::generate();
let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
let mut notifications = client.notifications();
let mut processed_events: HashSet<EventId> = HashSet::new();
while let Ok(notification) = notifications.recv().await {
if let RelayPoolNotification::Message { message, .. } = notification {
match message {
RelayMessage::Event {
event,
subscription_id,
} => {
if processed_events.contains(&event.id) {
continue;
}
processed_events.insert(event.id);
match event.kind {
Kind::GiftWrap => {
let event = match get_unwrapped(event.id).await {
Ok(event) => event,
Err(_) => match client.unwrap_gift_wrap(&event).await {
Ok(unwrap) => match unwrap.rumor.sign_with_keys(&keys) {
Ok(unwrapped) => {
set_unwrapped(event.id, &unwrapped, &keys)
.await
.ok();
unwrapped
}
Err(_) => continue,
},
Err(_) => continue,
},
};
let mut pubkeys = vec![];
pubkeys.extend(event.tags.public_keys());
pubkeys.push(event.pubkey);
// Send all pubkeys to the batch to sync metadata
batch_tx.send(pubkeys).await.ok();
// Save the event to the database, use for query directly.
client.database().save_event(&event).await.ok();
// Send this event to the GPUI
if new_id == *subscription_id {
event_tx.send(Signal::Event(event)).await.ok();
}
}
Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).ok();
profiles()
.write()
.await
.entry(event.pubkey)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
}
Kind::ContactList => {
if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await {
if public_key == event.pubkey {
let pubkeys = event
.tags
.public_keys()
.copied()
.collect::<Vec<_>>();
batch_tx.send(pubkeys).await.ok();
}
}
}
}
Kind::ReleaseArtifactSet => {
let filter = Filter::new()
.ids(event.tags.event_ids().copied())
.kind(Kind::FileMetadata);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to subscribe for file metadata: {}", e);
} else {
event_tx
.send(Signal::AppUpdates(event.into_owned()))
.await
.ok();
}
}
_ => {}
}
}
RelayMessage::EndOfStoredEvents(subscription_id) => {
if all_id == *subscription_id {
event_tx.send(Signal::Eose).await.ok();
}
}
_ => {}
}
}
}
});
// Initialize application
// Initialize the Application
let app = Application::new()
.with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
app.run(move |cx| {
// Bring the app to the foreground
cx.activate(true);
// Register the `quit` function
cx.on_action(quit);
// Register the `quit` function with CMD+Q
// Register the `quit` function with CMD+Q (macOS only)
#[cfg(target_os = "macos")]
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
// Set menu items
@@ -297,37 +89,87 @@ fn main() {
// Root Entity
cx.new(|cx| {
cx.activate(true);
// Initialize components
ui::init(cx);
// Initialize auto update
auto_update::init(cx);
// Initialize chat state
chats::init(cx);
// Initialize account state
account::init(cx);
// Initialize chatspace (or workspace)
let chatspace = chatspace::init(window, cx);
let async_chatspace = chatspace.downgrade();
let async_chatspace_clone = async_chatspace.clone();
// Read user's credential
let read_credential = cx.read_credentials(KEYRING_USER_PATH);
cx.spawn_in(window, async move |_, cx| {
if let Ok(Some((user, secret))) = read_credential.await {
cx.update(|window, cx| {
if let Ok(signer) = extract_credential(&user, secret) {
cx.background_spawn(async move {
if let Err(e) = shared_state().set_signer(signer).await {
log::error!("Signer error: {}", e);
}
})
.detach();
} else {
async_chatspace
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
}
})
.ok();
} else {
cx.update(|window, cx| {
async_chatspace
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
})
.ok();
}
})
.detach();
// Spawn a task to handle events from nostr channel
cx.spawn_in(window, async move |_, cx| {
while let Ok(signal) = event_rx.recv().await {
while let Ok(signal) = shared_state().global_receiver.recv().await {
cx.update(|window, cx| {
let chats = ChatRegistry::global(cx);
let auto_updater = AutoUpdater::global(cx);
match signal {
Signal::Eose => {
NostrSignal::SignerUpdated => {
async_chatspace_clone
.update(cx, |this, cx| {
this.open_chats(window, cx);
})
.ok();
}
NostrSignal::SignerUnset => {
async_chatspace_clone
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
}
NostrSignal::Eose => {
chats.update(cx, |this, cx| {
this.load_rooms(window, cx);
});
}
Signal::Event(event) => {
NostrSignal::Event(event) => {
chats.update(cx, |this, cx| {
this.event_to_message(event, window, cx);
});
}
Signal::AppUpdates(event) => {
NostrSignal::AppUpdate(event) => {
auto_updater.update(cx, |this, cx| {
this.update(event, cx);
});
@@ -339,62 +181,26 @@ fn main() {
})
.detach();
Root::new(chatspace::init(window, cx).into(), window, cx)
Root::new(chatspace.into(), window, cx)
})
})
.expect("Failed to open window. Please restart the application.");
});
}
async fn set_unwrapped(root: EventId, event: &Event, keys: &Keys) -> Result<(), Error> {
let client = get_client();
let event = EventBuilder::new(Kind::Custom(9001), event.as_json())
.tags(vec![Tag::event(root)])
.sign(keys) // keys must be random generated
.await?;
fn extract_credential(user: &str, secret: Vec<u8>) -> Result<impl NostrSigner, Error> {
if user == KEYRING_BUNKER {
let value = String::from_utf8(secret)?;
let uri = NostrConnectURI::parse(value)?;
let client_keys = shared_state().client_signer.clone();
let signer = NostrConnect::new(uri, client_keys, Duration::from_secs(300), None)?;
client.database().save_event(&event).await?;
Ok(())
}
async fn get_unwrapped(gift_wrap: EventId) -> Result<Event, Error> {
let client = get_client();
let filter = Filter::new()
.kind(Kind::Custom(9001))
.event(gift_wrap)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
let parsed = Event::from_json(event.content)?;
Ok(parsed)
Ok(signer.into_nostr_signer())
} else {
Err(anyhow!("Event not found"))
}
}
let secret_key = SecretKey::from_slice(&secret)?;
let keys = Keys::new(secret_key);
async fn sync_metadata(
buffer: HashSet<PublicKey>,
client: &Client,
opts: SubscribeAutoCloseOptions,
) {
let kinds = vec![
Kind::Metadata,
Kind::ContactList,
Kind::InboxRelays,
Kind::UserStatus,
];
let filter = Filter::new()
.authors(buffer.iter().cloned())
.limit(buffer.len() * kinds.len())
.kinds(kinds);
if let Err(e) = client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await
{
log::error!("Failed to sync metadata: {e}");
Ok(keys.into_nostr_signer())
}
}

View File

@@ -1,19 +1,21 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use account::Account;
use async_utility::task::spawn;
use chats::{
message::Message,
room::{Room, RoomKind, SendError},
};
use common::{nip96_upload, profile::RenderProfile};
use global::get_client;
use chats::message::Message;
use chats::room::{Room, RoomKind, SendError};
use common::nip96_upload;
use common::profile::RenderProfile;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, impl_internal_actions, list, prelude::FluentBuilder, px, red, relative, rems, svg,
white, AnyElement, App, AppContext, ClipboardItem, Context, Div, Element, Empty, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment,
ListState, ObjectFit, ParentElement, PathPromptOptions, Render, RetainAllImageCache,
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
div, img, impl_internal_actions, list, px, red, relative, rems, svg, white, AnyElement, App,
AppContext, ClipboardItem, Context, Div, Element, Empty, Entity, EventEmitter, Flatten,
FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListState, ObjectFit,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
@@ -21,15 +23,15 @@ use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::fs;
use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::emoji_picker::EmojiPicker;
use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification;
use ui::popup_menu::PopupMenu;
use ui::text::RichText;
use ui::{
avatar::Avatar,
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
emoji_picker::EmojiPicker,
input::{InputEvent, InputState, TextInput},
notification::Notification,
popup_menu::PopupMenu,
text::RichText,
v_flex, ContextModal, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
};
@@ -217,7 +219,7 @@ impl Chat {
// TODO: find a better way to prevent duplicate messages during optimistic updates
fn prevent_duplicate_message(&self, new_msg: &Message, cx: &Context<Self>) -> bool {
let Some(current_user) = Account::get_global(cx).profile_ref() else {
let Some(account) = shared_state().identity() else {
return false;
};
@@ -225,7 +227,7 @@ impl Chat {
return false;
};
if current_user.public_key() != author.public_key() {
if account.public_key() != author.public_key() {
return false;
}
@@ -238,7 +240,7 @@ impl Chat {
m.borrow()
.author
.as_ref()
.is_some_and(|p| p.public_key() == current_user.public_key())
.is_some_and(|p| p.public_key() == account.public_key())
})
.any(|existing| {
let existing = existing.borrow();
@@ -383,12 +385,11 @@ impl Chat {
};
if let Ok(file_data) = fs::read(path).await {
let client = get_client();
let (tx, rx) = oneshot::channel::<Option<Url>>();
// Spawn task via async utility instead of GPUI context
spawn(async move {
let url = match nip96_upload(client, file_data).await {
let url = match nip96_upload(&shared_state().client, file_data).await {
Ok(url) => Some(url),
Err(e) => {
log::error!("Upload error: {e}");

View File

@@ -1,28 +1,25 @@
use std::{
collections::{BTreeSet, HashSet},
time::Duration,
};
use std::collections::{BTreeSet, HashSet};
use std::time::Duration;
use anyhow::Error;
use chats::{room::Room, ChatRegistry};
use chats::room::Room;
use chats::ChatRegistry;
use common::profile::RenderProfile;
use global::get_client;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextAlign,
Window,
div, img, impl_internal_actions, px, red, relative, uniform_list, App, AppContext, Context,
Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, TextAlign, Window,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
use smol::Timer;
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::{InputEvent, InputState, TextInput},
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
};
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{ContextModal, Disableable, Icon, IconName, Sizable, StyledExt};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Compose> {
cx.new(|cx| Compose::new(window, cx))
@@ -71,10 +68,13 @@ impl Compose {
cx.spawn(async move |this, cx| {
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
let profiles = shared_state()
.client
.database()
.contacts(public_key)
.await?;
Ok(profiles)
});
@@ -133,8 +133,7 @@ impl Compose {
let tags = Tags::from_list(tag_list);
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
// [IMPORTANT]
@@ -173,7 +172,6 @@ impl Compose {
}
fn add(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client();
let content = self.user_input.read(cx).value().to_string();
// Show loading spinner
@@ -184,7 +182,8 @@ impl Compose {
let profile = nip05::profile(&content, None).await?;
let public_key = profile.public_key;
let metadata = client
let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
@@ -199,7 +198,8 @@ impl Compose {
};
cx.background_spawn(async move {
let metadata = client
let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();

View File

@@ -1,24 +1,27 @@
use std::{sync::Arc, time::Duration};
use std::sync::Arc;
use std::time::Duration;
use account::Account;
use common::string_to_qr;
use global::get_client_keys;
use global::constants::{APP_NAME, KEYRING_BUNKER, KEYRING_USER_PATH};
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, prelude::FluentBuilder, red, relative, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString,
Styled, Subscription, Window,
div, img, red, relative, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
};
use nostr_connect::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputEvent, InputState, TextInput},
notification::Notification,
popup_menu::PopupMenu,
ContextModal, Disableable, Sizable, StyledExt,
};
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};
const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
const NOSTR_CONNECT_TIMEOUT: u64 = 300;
#[derive(Debug, Clone)]
struct CoopAuthUrlHandler;
@@ -37,23 +40,20 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
}
pub struct Login {
// Inputs
key_input: Entity<InputState>,
relay_input: Entity<InputState>,
connection_string: Entity<NostrConnectURI>,
qr_image: Entity<Option<Arc<Image>>>,
// Signer that created by Connection String
active_signer: Entity<Option<NostrConnect>>,
// Error for the key input
error: Entity<Option<SharedString>>,
is_logging_in: bool,
// Nostr Connect
qr: Entity<Option<Arc<Image>>>,
connect_relay: Entity<InputState>,
connect_client: Entity<Option<NostrConnectURI>>,
// Keep track of all signers created by nostr connect
signers: SmallVec<[NostrConnect; 3]>,
// Panel
name: SharedString,
closable: bool,
zoomable: bool,
focus_handle: FocusHandle,
#[allow(unused)]
subscriptions: SmallVec<[Subscription; 4]>,
subscriptions: SmallVec<[Subscription; 5]>,
}
impl Login {
@@ -62,106 +62,159 @@ impl Login {
}
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let connect_client: Entity<Option<NostrConnectURI>> = cx.new(|_| None);
let error = cx.new(|_| None);
let qr = cx.new(|_| None);
// 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 connect_relay =
cx.new(|cx| InputState::new(window, cx).default_value("wss://relay.nsec.app"));
let signers = smallvec![];
let relay_input =
cx.new(|cx| InputState::new(window, cx).default_value(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 = shared_state().client_signer.clone();
NostrConnectURI::client(client_keys.public_key(), vec![relay], APP_NAME)
});
// Convert the Connection String into QR Image
let qr_image = cx.new(|_| None);
let async_qr_image = qr_image.downgrade();
// Keep track of the signer that created by Connection String
let active_signer = cx.new(|_| None);
let async_active_signer = active_signer.downgrade();
let error = cx.new(|_| None);
let mut subscriptions = smallvec![];
subscriptions.push(cx.subscribe_in(
&key_input,
window,
move |this, _, event, window, cx| {
subscriptions.push(
cx.subscribe_in(&key_input, window, |this, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.login(window, cx);
}
}),
);
subscriptions.push(
cx.subscribe_in(&relay_input, window, |this, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.change_relay(window, cx);
}
}),
);
subscriptions.push(cx.observe_new::<NostrConnectURI>(
move |connection_string, _window, cx| {
if let Ok(mut signer) = NostrConnect::new(
connection_string.to_owned(),
shared_state().client_signer.clone(),
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None,
) {
// Automatically open remote signer's webpage when received auth url
signer.auth_url_handler(CoopAuthUrlHandler);
async_active_signer
.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
})
.ok();
}
// Update the QR Image with the new connection string
async_qr_image
.update(cx, |this, cx| {
*this = string_to_qr(&connection_string.to_string());
cx.notify();
})
.ok();
},
));
subscriptions.push(cx.subscribe_in(
&connect_relay,
subscriptions.push(cx.observe_in(
&connection_string,
window,
move |this, _, event, window, cx| {
if let InputEvent::PressEnter { .. } = event {
this.change_relay(window, cx);
|this, entity, _window, cx| {
let connection_string = entity.read(cx).clone();
let client_keys = shared_state().client_signer.clone();
// Update the QR Image with the new connection string
this.qr_image.update(cx, |this, cx| {
*this = string_to_qr(&connection_string.to_string());
cx.notify();
});
if let Ok(mut signer) = NostrConnect::new(
connection_string,
client_keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
None,
) {
// Automatically open remote signer's webpage when received auth url
signer.auth_url_handler(CoopAuthUrlHandler);
this.active_signer.update(cx, |this, cx| {
*this = Some(signer);
cx.notify();
});
}
},
));
subscriptions.push(
cx.observe_in(&connect_client, window, |this, uri, window, cx| {
let keys = get_client_keys().to_owned();
cx.observe_in(&active_signer, window, |_this, entity, window, cx| {
if let Some(signer) = entity.read(cx).clone() {
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
if let Some(uri) = uri.read(cx).clone() {
if let Ok(qr) = string_to_qr(uri.to_string().as_str()) {
this.qr.update(cx, |this, cx| {
*this = Some(qr);
cx.notify();
});
}
cx.background_spawn(async move {
if let Ok(bunker_uri) = signer.bunker_uri().await {
tx.send(Some(bunker_uri)).ok();
// Shutdown all previous nostr connect clients
for client in std::mem::take(&mut this.signers).into_iter() {
cx.background_spawn(async move {
client.shutdown().await;
})
.detach();
}
// Create a new nostr connect client
match NostrConnect::new(uri, keys, Duration::from_secs(200), None) {
Ok(mut signer) => {
// Handle auth url
signer.auth_url_handler(CoopAuthUrlHandler);
// Store this signer for further clean up
this.signers.push(signer.clone());
Account::global(cx).update(cx, |this, cx| {
this.login(signer, window, cx);
});
if let Err(e) = shared_state().set_signer(signer).await {
log::error!("{}", e);
}
} else {
tx.send(None).ok();
}
Err(e) => {
window.push_notification(Notification::error(e.to_string()), cx);
})
.detach();
cx.spawn_in(window, async move |this, cx| {
if let Ok(Some(uri)) = rx.await {
this.update(cx, |this, cx| {
this.save_bunker(&uri, cx);
})
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(
Notification::error("Connection failed"),
cx,
);
})
.ok();
}
}
})
.detach();
}
}),
);
cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(300))
.await;
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.change_relay(window, cx);
})
.ok();
})
.ok();
})
.detach();
Self {
key_input,
connect_relay,
connect_client,
subscriptions,
signers,
error,
qr,
is_logging_in: false,
name: "Login".into(),
closable: true,
zoomable: true,
focus_handle: cx.focus_handle(),
is_logging_in: false,
key_input,
relay_input,
connection_string,
qr_image,
error,
active_signer,
subscriptions,
}
}
@@ -169,63 +222,169 @@ impl Login {
if self.is_logging_in {
return;
};
self.set_logging_in(true, cx);
let client_keys = shared_state().client_signer.clone();
let content = self.key_input.read(cx).value();
let account = Account::global(cx);
if content.starts_with("nsec1") {
match SecretKey::parse(content.as_ref()) {
Ok(secret) => {
let keys = Keys::new(secret);
let Ok(keys) = SecretKey::parse(content.as_ref()).map(Keys::new) else {
self.set_error("Secret key is not valid", cx);
return;
};
account.update(cx, |this, cx| {
this.login(keys, window, cx);
});
// Active signer is no longer needed
self.shutdown_active_signer(cx);
// Save these keys to the OS storage for further logins
self.save_keys(&keys, cx);
// Set signer with this keys in the background
cx.background_spawn(async move {
if let Err(e) = shared_state().set_signer(keys).await {
log::error!("{}", e);
}
})
.detach();
} else if content.starts_with("bunker://") {
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
self.set_error("Bunker URL is not valid", cx);
return;
};
// Active signer is no longer needed
self.shutdown_active_signer(cx);
match NostrConnect::new(
uri.clone(),
client_keys,
Duration::from_secs(NOSTR_CONNECT_TIMEOUT / 2),
None,
) {
Ok(signer) => {
let (tx, rx) = oneshot::channel::<Option<NostrConnectURI>>();
// Set signer with this remote signer in the background
cx.background_spawn(async move {
if let Ok(bunker_uri) = signer.bunker_uri().await {
tx.send(Some(bunker_uri)).ok();
if let Err(e) = shared_state().set_signer(signer).await {
log::error!("{}", e);
}
} else {
tx.send(None).ok();
}
})
.detach();
// Handle error
cx.spawn_in(window, async move |this, cx| {
if let Ok(Some(uri)) = rx.await {
this.update(cx, |this, cx| {
this.save_bunker(&uri, cx);
})
.ok();
} else {
this.update(cx, |this, cx| {
this.set_error(
"Connection to the Remote Signer failed or timed out",
cx,
);
})
.ok();
}
})
.detach();
}
Err(e) => {
self.set_error(e.to_string(), cx);
}
}
} else if content.starts_with("bunker://") {
let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else {
self.set_error("Bunker URL is not valid".to_owned(), cx);
return;
};
self.connect_client.update(cx, |this, cx| {
*this = Some(uri);
cx.notify();
});
} else {
self.set_error("You must provide a valid Private Key or Bunker.".into(), cx);
self.set_error("You must provide a valid Private Key or Bunker.", cx);
};
}
fn change_relay(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Ok(relay_url) =
RelayUrl::parse(self.connect_relay.read(cx).value().to_string().as_str())
let Ok(relay_url) = RelayUrl::parse(self.relay_input.read(cx).value().to_string().as_str())
else {
window.push_notification(Notification::error("Relay URL is not valid."), cx);
return;
};
let client_pubkey = get_client_keys().public_key();
let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop");
let client_keys = shared_state().client_signer.clone();
let uri = NostrConnectURI::client(client_keys.public_key(), vec![relay_url], "Coop");
self.connect_client.update(cx, |this, cx| {
*this = Some(uri);
self.connection_string.update(cx, |this, cx| {
*this = uri;
cx.notify();
});
}
fn set_error(&mut self, message: String, cx: &mut Context<Self>) {
fn save_keys(&self, keys: &Keys, cx: &mut Context<Self>) {
let save_credential = cx.write_credentials(
KEYRING_USER_PATH,
keys.public_key().to_hex().as_str(),
keys.secret_key().as_secret_bytes(),
);
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save keys: {}", e)
}
})
.detach();
}
fn save_bunker(&self, uri: &NostrConnectURI, cx: &mut Context<Self>) {
let mut value = uri.to_string();
// Remove the secret param if it exists
if let Some(secret) = uri.secret() {
value = value.replace(secret, "");
}
let save_credential =
cx.write_credentials(KEYRING_USER_PATH, KEYRING_BUNKER, value.as_bytes());
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save the Bunker URI: {}", e)
}
})
.detach();
}
fn shutdown_active_signer(&self, cx: &Context<Self>) {
if let Some(signer) = self.active_signer.read(cx).clone() {
cx.background_spawn(async move {
signer.shutdown().await;
})
.detach();
}
}
fn set_error(&mut self, message: impl Into<SharedString>, cx: &mut Context<Self>) {
self.set_logging_in(false, cx);
self.error.update(cx, |this, cx| {
*this = Some(SharedString::new(message));
*this = Some(message.into());
cx.notify();
});
// Clear the error message after 3 secs
cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(3)).await;
this.update(cx, |this, cx| {
this.error.update(cx, |this, cx| {
*this = None;
cx.notify();
});
})
.ok();
})
.detach();
}
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
@@ -243,14 +402,6 @@ impl Panel for Login {
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)
}
@@ -365,9 +516,10 @@ impl Render for Login {
.child("Use Nostr Connect apps to scan the code"),
),
)
.when_some(self.qr.read(cx).clone(), |this, qr| {
.when_some(self.qr_image.read(cx).clone(), |this, qr| {
this.child(
div()
.id("")
.mb_2()
.p_2()
.size_72()
@@ -384,7 +536,27 @@ impl Render for Login {
.border_color(cx.theme().border)
})
.bg(cx.theme().background)
.child(img(qr).h_64()),
.child(img(qr).h_64())
.on_click(cx.listener(move |this, _, window, cx| {
#[cfg(any(
target_os = "linux",
target_os = "freebsd"
))]
cx.write_to_clipboard(ClipboardItem::new_string(
this.connection_string.read(cx).to_string(),
));
#[cfg(any(
target_os = "macos",
target_os = "windows"
))]
cx.write_to_clipboard(ClipboardItem::new_string(
this.connection_string.read(cx).to_string(),
));
window.push_notification(
"Connection String has been copied",
cx,
);
})),
)
})
.child(
@@ -394,7 +566,7 @@ impl Render for Login {
.items_center()
.justify_center()
.gap_1()
.child(TextInput::new(&self.connect_relay).xsmall())
.child(TextInput::new(&self.relay_input).xsmall())
.child(
Button::new("change")
.label("Change")

View File

@@ -6,5 +6,6 @@ pub mod onboarding;
pub mod profile;
pub mod relays;
pub mod sidebar;
pub mod startup;
pub mod subject;
pub mod welcome;

View File

@@ -1,24 +1,21 @@
use std::str::FromStr;
use account::Account;
use async_utility::task::spawn;
use common::nip96_upload;
use global::get_client;
use global::constants::KEYRING_USER_PATH;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,
EventEmitter, Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions,
Render, SharedString, Styled, Window,
div, img, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, Flatten,
FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString,
Styled, Window,
};
use nostr_sdk::prelude::*;
use smol::fs;
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputState, TextInput},
popup_menu::PopupMenu,
Disableable, Icon, IconName, Sizable, StyledExt,
};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::popup_menu::PopupMenu;
use ui::{Disableable, Icon, IconName, Sizable, StyledExt};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<NewAccount> {
NewAccount::new(window, cx)
@@ -44,8 +41,10 @@ impl NewAccount {
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let avatar_input =
cx.new(|cx| InputState::new(window, cx).placeholder("https://example.com/avatar.jpg"));
let bio_input = cx.new(|cx| {
InputState::new(window, cx)
.multi_line()
@@ -65,22 +64,33 @@ impl NewAccount {
}
}
fn submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn submit(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
self.set_submitting(true, cx);
let avatar = self.avatar_input.read(cx).value().to_string();
let name = self.name_input.read(cx).value().to_string();
let bio = self.bio_input.read(cx).value().to_string();
let keys = Keys::generate();
let mut metadata = Metadata::new().display_name(name).about(bio);
if let Ok(url) = Url::from_str(&avatar) {
if let Ok(url) = Url::parse(&avatar) {
metadata = metadata.picture(url);
};
Account::global(cx).update(cx, |this, cx| {
this.new_account(metadata, window, cx);
});
let save_credential = cx.write_credentials(
KEYRING_USER_PATH,
keys.public_key().to_hex().as_str(),
keys.secret_key().as_secret_bytes(),
);
cx.background_spawn(async move {
if let Err(e) = save_credential.await {
log::error!("Failed to save keys: {}", e)
};
shared_state().new_account(keys, metadata).await;
})
.detach();
}
fn upload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -109,11 +119,10 @@ impl NewAccount {
};
if let Ok(file_data) = fs::read(path).await {
let client = get_client();
let (tx, rx) = oneshot::channel::<Url>();
spawn(async move {
if let Ok(url) = nip96_upload(client, file_data).await {
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
_ = tx.send(url);
}
});

View File

@@ -3,12 +3,10 @@ use gpui::{
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
Icon, IconName, StyledExt,
};
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::popup_menu::PopupMenu;
use ui::{Icon, IconName, StyledExt};
use crate::chatspace;

View File

@@ -1,19 +1,20 @@
use std::str::FromStr;
use std::time::Duration;
use async_utility::task::spawn;
use common::nip96_upload;
use global::get_client;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, prelude::FluentBuilder, App, AppContext, Context, Entity, Flatten, IntoElement,
ParentElement, PathPromptOptions, Render, Styled, Task, Window,
div, img, App, AppContext, Context, Entity, Flatten, IntoElement, ParentElement,
PathPromptOptions, Render, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use smol::fs;
use std::{str::FromStr, time::Duration};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::{InputState, TextInput},
ContextModal, Disableable, IconName, Sizable,
};
use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput};
use ui::{ContextModal, Disableable, IconName, Sizable};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Profile> {
Profile::new(window, cx)
@@ -54,10 +55,10 @@ impl Profile {
};
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
let metadata = client
let metadata = shared_state()
.client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?;
@@ -122,8 +123,7 @@ impl Profile {
let (tx, rx) = oneshot::channel::<Url>();
spawn(async move {
let client = get_client();
if let Ok(url) = nip96_upload(client, file_data).await {
if let Ok(url) = nip96_upload(&shared_state().client, file_data).await {
_ = tx.send(url);
}
});
@@ -189,9 +189,7 @@ impl Profile {
}
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = get_client();
_ = client.set_metadata(&new_metadata).await?;
let _ = shared_state().client.set_metadata(&new_metadata).await?;
Ok(())
});

View File

@@ -1,18 +1,17 @@
use anyhow::Error;
use global::{constants::NEW_MESSAGE_SUB_ID, get_client};
use global::constants::NEW_MESSAGE_SUB_ID;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, prelude::FluentBuilder, px, uniform_list, App, AppContext, Context, Entity, FocusHandle,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign,
UniformList, Window,
div, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement,
IntoElement, ParentElement, Render, Styled, Subscription, Task, TextAlign, UniformList, Window,
};
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::{InputEvent, InputState, TextInput},
ContextModal, Disableable, IconName, Sizable,
};
use ui::button::{Button, ButtonVariants};
use ui::input::{InputEvent, InputState, TextInput};
use ui::{ContextModal, Disableable, IconName, Sizable};
const MIN_HEIGHT: f32 = 200.0;
const MESSAGE: &str = "In order to receive messages from others, you need to setup at least one Messaging Relay. You can use the recommend relays or add more.";
@@ -36,16 +35,20 @@ impl Relays {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("wss://example.com"));
let relays = cx.new(|cx| {
let task: Task<Result<Vec<RelayUrl>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
if let Some(event) = client.database().query(filter).await?.first_owned() {
if let Some(event) = shared_state()
.client
.database()
.query(filter)
.await?
.first_owned()
{
let relays = event
.tags
.filter(TagKind::Relay)
@@ -108,18 +111,23 @@ impl Relays {
let relays = self.relays.read(cx).clone();
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let signer = shared_state().client.signer().await?;
let public_key = signer.get_public_key().await?;
// If user didn't have any NIP-65 relays, add default ones
if client.database().relay_list(public_key).await?.is_empty() {
if shared_state()
.client
.database()
.relay_list(public_key)
.await?
.is_empty()
{
let builder = EventBuilder::relay_list(vec![
(RelayUrl::parse("wss://relay.damus.io/").unwrap(), None),
(RelayUrl::parse("wss://relay.primal.net/").unwrap(), None),
]);
if let Err(e) = client.send_event_builder(builder).await {
if let Err(e) = shared_state().client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
}
}
@@ -130,21 +138,22 @@ impl Relays {
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let output = client.send_event_builder(builder).await?;
let output = shared_state().client.send_event_builder(builder).await?;
// Connect to messaging relays
for relay in relays.into_iter() {
_ = client.add_relay(&relay).await;
_ = client.connect_relay(&relay).await;
_ = shared_state().client.add_relay(&relay).await;
_ = shared_state().client.connect_relay(&relay).await;
}
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
// Close old subscription
client.unsubscribe(&sub_id).await;
shared_state().client.unsubscribe(&sub_id).await;
// Subscribe to new messages
if let Err(e) = client
if let Err(e) = shared_state()
.client
.subscribe_with_id(
sub_id,
Filter::new()

View File

@@ -1,11 +1,13 @@
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, img, prelude::FluentBuilder, rems, App, ClickEvent, Div, InteractiveElement, IntoElement,
ParentElement as _, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
div, img, rems, App, ClickEvent, Div, InteractiveElement, IntoElement, ParentElement as _,
RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
};
use theme::ActiveTheme;
use ui::{avatar::Avatar, StyledExt};
use ui::avatar::Avatar;
use ui::StyledExt;
#[derive(IntoElement)]
pub struct DisplayRoom {

View File

@@ -1,33 +1,32 @@
use std::{collections::BTreeSet, ops::Range, time::Duration};
use std::collections::BTreeSet;
use std::ops::Range;
use std::time::Duration;
use account::Account;
use async_utility::task::spawn;
use chats::{
room::{Room, RoomKind},
ChatRegistry, RoomEmitter,
};
use common::{debounced_delay::DebouncedDelay, profile::RenderProfile};
use chats::room::{Room, RoomKind};
use chats::{ChatRegistry, RoomEmitter};
use common::debounced_delay::DebouncedDelay;
use common::profile::RenderProfile;
use element::DisplayRoom;
use global::{constants::SEARCH_RELAYS, get_client};
use global::constants::SEARCH_RELAYS;
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, prelude::FluentBuilder, rems, uniform_list, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache,
SharedString, Styled, Subscription, Task, Window,
div, rems, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString,
Styled, Subscription, Task, Window,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
avatar::Avatar,
button::{Button, ButtonRounded, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputEvent, InputState, TextInput},
popup_menu::{PopupMenu, PopupMenuExt},
skeleton::Skeleton,
ContextModal, IconName, Selectable, Sizable, StyledExt,
};
use ui::avatar::Avatar;
use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::{PopupMenu, PopupMenuExt};
use ui::skeleton::Skeleton;
use ui::{ContextModal, IconName, Selectable, Sizable, StyledExt};
use crate::chatspace::{ModalKind, ToggleModal};
@@ -142,14 +141,13 @@ impl Sidebar {
let query = self.find_input.read(cx).value().clone();
cx.background_spawn(async move {
let client = get_client();
let filter = Filter::new()
.kind(Kind::Metadata)
.search(query.to_lowercase())
.limit(FIND_LIMIT);
let events = client
let events = shared_state()
.client
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
.await?
.into_iter()
@@ -160,8 +158,11 @@ impl Sidebar {
let (tx, rx) = smol::channel::bounded::<Room>(10);
spawn(async move {
let client = get_client();
let signer = client.signer().await.expect("signer is required");
let signer = shared_state()
.client
.signer()
.await
.expect("signer is required");
let public_key = signer.get_public_key().await.expect("error");
for event in events.into_iter() {
@@ -492,9 +493,10 @@ impl Render for Sidebar {
.flex_col()
.gap_3()
// Account
.when_some(Account::get_global(cx).profile_ref(), |this, profile| {
this.child(self.render_account(profile, cx))
})
.when_some(
shared_state().identity.read_blocking().as_ref(),
|this, profile| this.child(self.render_account(profile, cx)),
)
// Search Input
.child(
div().px_3().w_full().h_7().flex_none().child(

View File

@@ -0,0 +1,88 @@
use gpui::{
div, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::button::Button;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::popup_menu::PopupMenu;
use ui::Sizable;
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: "Welcome".into(),
focus_handle: cx.focus_handle(),
})
}
}
impl Panel for Startup {
fn panel_id(&self) -> SharedString {
self.name.clone()
}
fn title(&self, _cx: &App) -> AnyElement {
"Startup".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 {
div()
.size_full()
.flex()
.items_center()
.justify_center()
.child(
div()
.flex()
.flex_col()
.items_center()
.gap_6()
.child(
svg()
.path("brand/coop.svg")
.size_12()
.text_color(cx.theme().elevated_surface_background),
)
.child(
div()
.flex()
.items_center()
.gap_1p5()
.text_xs()
.text_center()
.text_color(cx.theme().text_muted)
.child("Connection in progress")
.child(Indicator::new().small()),
),
)
}
}

View File

@@ -4,11 +4,9 @@ use gpui::{
ParentElement, Render, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonVariants},
input::{InputState, TextInput},
ContextModal, Sizable,
};
use ui::button::{Button, ButtonVariants};
use ui::input::{InputState, TextInput};
use ui::{ContextModal, Sizable};
pub fn init(
id: u64,

View File

@@ -3,12 +3,10 @@ use gpui::{
IntoElement, ParentElement, Render, SharedString, Styled, Window,
};
use theme::ActiveTheme;
use ui::{
button::Button,
dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu,
StyledExt,
};
use ui::button::Button;
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::popup_menu::PopupMenu;
use ui::StyledExt;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Welcome> {
Welcome::new(window, cx)