feat: wait for processing to complete (#66)

* wait instead of check eose

* refactor

* refactor

* refactor

* improve extend rooms function

* .
This commit is contained in:
reya
2025-06-23 09:00:56 +07:00
committed by GitHub
parent 1d77fd443e
commit c7e3331eb0
18 changed files with 650 additions and 484 deletions

View File

@@ -151,6 +151,11 @@ impl ChatSpace {
if !state.read(cx).has_profile() {
this.open_onboarding(window, cx);
} else {
// Load all chat rooms from database
ChatRegistry::global(cx).update(cx, |this, cx| {
this.load_rooms(window, cx);
});
// Open chat panels
this.open_chats(window, cx);
}
},
@@ -273,19 +278,14 @@ impl ChatSpace {
fn verify_messaging_relays(&self, cx: &App) -> Task<Result<bool, Error>> {
cx.background_spawn(async move {
let signer = shared_state().client.signer().await?;
let client = shared_state().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 is_exist = shared_state()
.client
.database()
.query(filter)
.await?
.first()
.is_some();
let is_exist = client.database().query(filter).await?.first().is_some();
Ok(is_exist)
})

View File

@@ -3,9 +3,9 @@ use std::sync::Arc;
use asset::Assets;
use auto_update::AutoUpdater;
use chats::ChatRegistry;
use global::constants::APP_ID;
#[cfg(not(target_os = "linux"))]
use global::constants::APP_NAME;
use global::constants::{ALL_MESSAGES_SUB_ID, APP_ID};
use global::{shared_state, NostrSignal};
use gpui::{
actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
@@ -15,6 +15,7 @@ use gpui::{
use gpui::{point, SharedString, TitlebarOptions};
#[cfg(target_os = "linux")]
use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::SubscriptionId;
use theme::Theme;
use ui::Root;
@@ -28,17 +29,19 @@ fn main() {
// Initialize logging
tracing_subscriber::fmt::init();
// 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;
});
// Initialize the Application
let app = Application::new()
.with_assets(Assets)
.with_http_client(Arc::new(reqwest_client::ReqwestClient::new()));
// Initialize the Global State and process events in a separate thread.
app.background_executor()
.spawn(async move {
shared_state().start().await;
})
.detach();
// Run application
app.run(move |cx| {
// Register the `quit` function
cx.on_action(quit);
@@ -100,42 +103,44 @@ fn main() {
// Initialize chat state
chats::init(cx);
// Initialize chatspace (or workspace)
let chatspace = chatspace::init(window, cx);
let async_chatspace = chatspace.downgrade();
// Spawn a task to handle events from nostr channel
cx.spawn_in(window, async move |_, cx| {
while let Ok(signal) = shared_state().global_receiver.recv().await {
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
while let Ok(signal) = shared_state().signal().recv().await {
cx.update(|window, cx| {
let chats = ChatRegistry::global(cx);
let auto_updater = AutoUpdater::global(cx);
match signal {
NostrSignal::SignerUpdated => {
async_chatspace
.update(cx, |this, cx| {
this.open_chats(window, cx);
})
.ok();
}
NostrSignal::SignerUnset => {
async_chatspace
.update(cx, |this, cx| {
this.open_onboarding(window, cx);
})
.ok();
}
NostrSignal::Eose => {
chats.update(cx, |this, cx| {
this.load_rooms(window, cx);
});
}
NostrSignal::Event(event) => {
chats.update(cx, |this, cx| {
this.event_to_message(event, window, cx);
});
}
// Load chat rooms and stop the loading status
NostrSignal::Finish => {
chats.update(cx, |this, cx| {
this.load_rooms(window, cx);
this.set_loading(false, cx);
});
}
// Load chat rooms without setting as finished
NostrSignal::PartialFinish => {
chats.update(cx, |this, cx| {
this.load_rooms(window, cx);
});
}
NostrSignal::Eose(subscription_id) => {
if subscription_id == all_messages_sub_id {
chats.update(cx, |this, cx| {
this.load_rooms(window, cx);
});
}
}
NostrSignal::Notice(_msg) => {
// window.push_notification(msg, cx);
}
NostrSignal::AppUpdate(event) => {
auto_updater.update(cx, |this, cx| {
this.update(event, cx);
@@ -148,7 +153,7 @@ fn main() {
})
.detach();
Root::new(chatspace.into(), window, cx)
Root::new(chatspace::init(window, cx).into(), window, cx)
})
})
.expect("Failed to open window. Please restart the application.");

View File

@@ -3,7 +3,6 @@ use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use async_utility::task::spawn;
use chats::message::Message;
use chats::room::{Room, RoomKind, SendError};
use common::nip96_upload;
@@ -391,8 +390,8 @@ impl Chat {
let (tx, rx) = oneshot::channel::<Option<Url>>();
// Spawn task via async utility instead of GPUI context
spawn(async move {
let url = match nip96_upload(&shared_state().client, nip96, file_data)
nostr_sdk::async_utility::task::spawn(async move {
let url = match nip96_upload(shared_state().client(), nip96, file_data)
.await
{
Ok(url) => Some(url),

View File

@@ -69,13 +69,10 @@ impl Compose {
cx.spawn(async move |this, cx| {
let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let signer = shared_state().client.signer().await?;
let client = shared_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = shared_state()
.client
.database()
.contacts(public_key)
.await?;
let profiles = client.database().contacts(public_key).await?;
Ok(profiles)
});
@@ -134,7 +131,7 @@ impl Compose {
let tags = Tags::from_list(tag_list);
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
let signer = shared_state().client.signer().await?;
let signer = shared_state().client().signer().await?;
let public_key = signer.get_public_key().await?;
// [IMPORTANT]
@@ -184,7 +181,7 @@ impl Compose {
let public_key = profile.public_key;
let metadata = shared_state()
.client
.client()
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();
@@ -200,7 +197,7 @@ impl Compose {
cx.background_spawn(async move {
let metadata = shared_state()
.client
.client()
.fetch_metadata(public_key, Duration::from_secs(2))
.await?
.unwrap_or_default();

View File

@@ -158,11 +158,9 @@ impl Login {
subscriptions.push(
cx.observe_in(&active_signer, window, |this, entity, window, cx| {
if let Some(mut signer) = entity.read(cx).clone() {
// Automatically open auth url
signer.auth_url_handler(CoopAuthUrlHandler);
if let Some(signer) = entity.read(cx).as_ref() {
// Wait for connection from remote signer
this.wait_for_connection(signer, window, cx);
this.wait_for_connection(signer.to_owned(), window, cx);
}
}),
);
@@ -284,11 +282,7 @@ impl Login {
};
if let Some(secret_key) = secret_key {
// Active signer is no longer needed
self.shutdown_active_signer(cx);
let keys = Keys::new(secret_key);
Identity::global(cx).update(cx, |this, cx| {
this.write_keys(&keys, password, cx);
this.set_signer(keys, window, cx);
@@ -312,9 +306,6 @@ impl Login {
return;
};
// Active signer is no longer needed
self.shutdown_active_signer(cx);
// Automatically open auth url
signer.auth_url_handler(CoopAuthUrlHandler);
@@ -359,10 +350,14 @@ impl Login {
let (tx, rx) = oneshot::channel::<Option<(NostrConnectURI, NostrConnect)>>();
cx.background_spawn(async move {
if let Ok(bunker_uri) = signer.bunker_uri().await {
tx.send(Some((bunker_uri, signer))).ok();
} else {
tx.send(None).ok();
match signer.bunker_uri().await {
Ok(bunker_uri) => {
tx.send(Some((bunker_uri, signer))).ok();
}
Err(e) => {
log::error!("Nostr Connect (Client): {e}");
tx.send(None).ok();
}
}
})
.detach();
@@ -378,9 +373,9 @@ impl Login {
.ok();
} else {
cx.update(|window, cx| {
window.push_notification(Notification::error("Connection failed"), cx);
// Refresh the active signer
this.update(cx, |this, cx| {
window.push_notification(Notification::error("Connection failed"), cx);
this.change_relay(window, cx);
})
.ok();
@@ -407,15 +402,6 @@ impl Login {
});
}
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| {

View File

@@ -1,4 +1,3 @@
use async_utility::task::spawn;
use common::nip96_upload;
use global::shared_state;
use gpui::prelude::FluentBuilder;
@@ -157,9 +156,9 @@ impl NewAccount {
if let Ok(file_data) = fs::read(path).await {
let (tx, rx) = oneshot::channel::<Url>();
spawn(async move {
nostr_sdk::async_utility::task::spawn(async move {
if let Ok(url) =
nip96_upload(&shared_state().client, nip96, file_data).await
nip96_upload(shared_state().client(), nip96, file_data).await
{
_ = tx.send(url);
}

View File

@@ -45,18 +45,14 @@ impl Onboarding {
let local_account = cx.new(|_| None);
let task = cx.background_spawn(async move {
let database = shared_state().client().database();
let filter = Filter::new()
.kind(Kind::ApplicationSpecificData)
.identifier(ACCOUNT_D)
.limit(1);
if let Some(event) = shared_state()
.client
.database()
.query(filter)
.await?
.first_owned()
{
if let Some(event) = database.query(filter).await?.first_owned() {
let public_key = event
.tags
.public_keys()
@@ -65,14 +61,7 @@ impl Onboarding {
.first()
.cloned()
.unwrap();
let metadata = shared_state()
.client
.database()
.metadata(public_key)
.await?
.unwrap_or_default();
let metadata = database.metadata(public_key).await?.unwrap_or_default();
let profile = Profile::new(public_key, metadata);
Ok(profile)

View File

@@ -1,7 +1,6 @@
use std::str::FromStr;
use std::time::Duration;
use async_utility::task::spawn;
use common::nip96_upload;
use global::shared_state;
use gpui::prelude::FluentBuilder;
@@ -56,10 +55,10 @@ impl Profile {
};
let task: Task<Result<Option<Metadata>, Error>> = cx.background_spawn(async move {
let signer = shared_state().client.signer().await?;
let client = shared_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let metadata = shared_state()
.client
let metadata = client
.fetch_metadata(public_key, Duration::from_secs(2))
.await?;
@@ -124,9 +123,9 @@ impl Profile {
if let Ok(file_data) = fs::read(path).await {
let (tx, rx) = oneshot::channel::<Url>();
spawn(async move {
nostr_sdk::async_utility::task::spawn(async move {
if let Ok(url) =
nip96_upload(&shared_state().client, nip96, file_data).await
nip96_upload(shared_state().client(), nip96, file_data).await
{
_ = tx.send(url);
}
@@ -193,7 +192,7 @@ impl Profile {
}
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let _ = shared_state().client.set_metadata(&new_metadata).await?;
let _ = shared_state().client().set_metadata(&new_metadata).await?;
Ok(())
});

View File

@@ -35,20 +35,15 @@ 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 signer = shared_state().client.signer().await?;
let client = shared_state().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);
if let Some(event) = shared_state()
.client
.database()
.query(filter)
.await?
.first_owned()
{
if let Some(event) = client.database().query(filter).await?.first_owned() {
let relays = event
.tags
.filter(TagKind::Relay)
@@ -111,23 +106,18 @@ impl Relays {
let relays = self.relays.read(cx).clone();
let task: Task<Result<EventId, Error>> = cx.background_spawn(async move {
let signer = shared_state().client.signer().await?;
let client = shared_state().client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
// If user didn't have any NIP-65 relays, add default ones
if shared_state()
.client
.database()
.relay_list(public_key)
.await?
.is_empty()
{
if 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) = shared_state().client.send_event_builder(builder).await {
if let Err(e) = client.send_event_builder(builder).await {
log::error!("Failed to send relay list event: {}", e);
}
}
@@ -138,22 +128,21 @@ impl Relays {
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let output = shared_state().client.send_event_builder(builder).await?;
let output = client.send_event_builder(builder).await?;
// Connect to messaging relays
for relay in relays.into_iter() {
_ = shared_state().client.add_relay(&relay).await;
_ = shared_state().client.connect_relay(&relay).await;
_ = client.add_relay(&relay).await;
_ = client.connect_relay(&relay).await;
}
let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
// Close old subscription
shared_state().client.unsubscribe(&sub_id).await;
client.unsubscribe(&sub_id).await;
// Subscribe to new messages
if let Err(e) = shared_state()
.client
if let Err(e) = client
.subscribe_with_id(
sub_id,
Filter::new()

View File

@@ -2,7 +2,6 @@ use std::collections::BTreeSet;
use std::ops::Range;
use std::time::Duration;
use async_utility::task::spawn;
use chats::room::{Room, RoomKind};
use chats::{ChatRegistry, RoomEmitter};
use common::debounced_delay::DebouncedDelay;
@@ -12,10 +11,10 @@ use global::constants::{DEFAULT_MODAL_WIDTH, SEARCH_RELAYS};
use global::shared_state;
use gpui::prelude::FluentBuilder;
use gpui::{
div, px, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
Window,
div, px, relative, rems, uniform_list, AnyElement, App, AppContext, ClipboardItem, Context,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
Render, RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription,
Task, Window,
};
use identity::Identity;
use itertools::Itertools;
@@ -26,6 +25,7 @@ use theme::ActiveTheme;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::PopupMenu;
use ui::skeleton::Skeleton;
@@ -145,13 +145,14 @@ impl Sidebar {
let query = self.find_input.read(cx).value().clone();
cx.background_spawn(async move {
let client = shared_state().client();
let filter = Filter::new()
.kind(Kind::Metadata)
.search(query.to_lowercase())
.limit(FIND_LIMIT);
let events = shared_state()
.client
let events = client
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
.await?
.into_iter()
@@ -161,12 +162,8 @@ impl Sidebar {
let mut rooms = BTreeSet::new();
let (tx, rx) = smol::channel::bounded::<Room>(10);
spawn(async move {
let signer = shared_state()
.client
.signer()
.await
.expect("signer is required");
nostr_sdk::async_utility::task::spawn(async move {
let signer = client.signer().await.expect("signer is required");
let public_key = signer.get_public_key().await.expect("error");
for event in events.into_iter() {
@@ -349,7 +346,46 @@ impl Sidebar {
});
}
fn render_account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| {
const BODY_1: &str =
"Coop is downloading all your messages from the messaging relays. \
Depending on your total number of messages, this process may take up to \
15 minutes if you're using Nostr Connect.";
const BODY_2: &str =
"Please be patient - you only need to do this full download once. \
Next time, Coop will only download new messages.";
const DESCRIPTION: &str = "You still can use the app normally \
while messages are processing in the background";
this.child(
div()
.pt_8()
.pb_4()
.px_4()
.flex()
.flex_col()
.gap_2()
.child(
div()
.flex()
.flex_col()
.gap_2()
.text_sm()
.child(BODY_1)
.child(BODY_2),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(DESCRIPTION),
),
)
});
}
fn account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
let proxy = AppSettings::get_global(cx).settings.proxy_user_avatars;
div()
@@ -396,7 +432,7 @@ impl Sidebar {
)
}
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
fn skeletons(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
(0..total).map(|_| {
div()
.h_9()
@@ -406,7 +442,14 @@ impl Sidebar {
.items_center()
.gap_2()
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child(Skeleton::new().w_40().h_4().rounded_sm())
.child(
div()
.flex_1()
.flex()
.justify_between()
.child(Skeleton::new().w_32().h_2p5().rounded_sm())
.child(Skeleton::new().w_6().h_2p5().rounded_sm()),
)
})
}
@@ -473,7 +516,7 @@ impl Focusable for Sidebar {
impl Render for Sidebar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let chats = ChatRegistry::get_global(cx);
// Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx) {
results.to_owned()
} else {
@@ -488,12 +531,13 @@ impl Render for Sidebar {
div()
.image_cache(self.image_cache.clone())
.size_full()
.relative()
.flex()
.flex_col()
.gap_3()
// Account
.when_some(Identity::get_global(cx).profile(), |this, profile| {
this.child(self.render_account(&profile, cx))
this.child(self.account(&profile, cx))
})
// Search Input
.child(
@@ -528,6 +572,7 @@ impl Render for Sidebar {
items
}))
})
// Chat Rooms
.child(
div()
.px_2()
@@ -623,13 +668,14 @@ impl Render for Sidebar {
)
}),
)
.when(chats.wait_for_eose, |this| {
.when(chats.loading, |this| {
this.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_1()
.children(self.render_skeleton(10)),
.children(self.skeletons(1)),
)
})
.child(
@@ -643,5 +689,61 @@ impl Render for Sidebar {
.h_full(),
),
)
.when(chats.loading, |this| {
this.child(
div().absolute().bottom_4().px_4().child(
div()
.p_1()
.w_full()
.rounded_full()
.flex()
.items_center()
.justify_between()
.bg(cx.theme().panel_background)
.shadow_sm()
// Empty div
.child(div().size_6().flex_shrink_0())
// Loading indicator
.child(
div()
.flex_1()
.flex()
.flex_col()
.items_center()
.justify_center()
.text_xs()
.text_center()
.child(
div()
.font_semibold()
.flex()
.items_center()
.gap_1()
.line_height(relative(1.2))
.child(Indicator::new().xsmall())
.child("Retrieving messages..."),
)
.child(
div()
.text_color(cx.theme().text_muted)
.child("This may take some time"),
),
)
// Info button
.child(
Button::new("help")
.icon(IconName::Info)
.tooltip("Why you're seeing this")
.small()
.ghost()
.rounded(ButtonRounded::Full)
.flex_shrink_0()
.on_click(cx.listener(move |this, _, window, cx| {
this.open_loading_modal(window, cx)
})),
),
),
)
})
}
}