feat: add syncing progress

This commit is contained in:
2026-10-26 09:54:38 +07:00
parent 0824b1825b
commit 4d0e2f5050
6 changed files with 193 additions and 77 deletions
Generated
+16 -22
View File
@@ -2401,15 +2401,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "futf" name = "futf"
version = "0.1.5" version = "0.1.5"
@@ -5225,20 +5216,21 @@ dependencies = [
[[package]] [[package]]
name = "notify" name = "notify"
version = "8.2.0" version = "8.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" checksum = "82f812702fbc75d35416d3f884e04925be7e9498f4eceea2e24ba34559284674"
dependencies = [ dependencies = [
"bitflags 2.13.2", "bitflags 2.13.2",
"fsevent-sys",
"inotify", "inotify",
"kqueue", "kqueue",
"libc", "libc",
"log", "log",
"mio", "mio",
"notify-types", "notify-types",
"objc2-core-foundation",
"objc2-core-services",
"walkdir", "walkdir",
"windows-sys 0.60.2", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@@ -5565,6 +5557,16 @@ dependencies = [
"objc2-foundation 0.3.2", "objc2-foundation 0.3.2",
] ]
[[package]]
name = "objc2-core-services"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "583300ad934cba24ff5292aee751ecc070f7ca6b39a574cc21b7b5e588e06a0b"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "objc2-core-text" name = "objc2-core-text"
version = "0.3.2" version = "0.3.2"
@@ -9728,15 +9730,6 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -10022,6 +10015,7 @@ dependencies = [
"nostr-connect", "nostr-connect",
"nostr-sdk", "nostr-sdk",
"person", "person",
"rand 0.10.3",
"serde", "serde",
"settings", "settings",
"smallvec", "smallvec",
+2
View File
@@ -47,6 +47,7 @@ impl Global for GlobalChatRegistry {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent { pub enum ChatEvent {
Ping, Ping,
Loaded,
InboxRelayNotFound, InboxRelayNotFound,
Error(String), Error(String),
} }
@@ -402,6 +403,7 @@ impl ChatRegistry {
if eose && tracking.swap(false, Ordering::AcqRel) { if eose && tracking.swap(false, Ordering::AcqRel) {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
cx.emit(ChatEvent::Loaded);
this.get_rooms(cx); this.get_rooms(cx);
})?; })?;
} }
+8 -1
View File
@@ -232,6 +232,7 @@ impl WebOfTrust {
.idle_timeout(Duration::from_secs(SYNC_IDLE_TIMEOUT)); .idle_timeout(Duration::from_secs(SYNC_IDLE_TIMEOUT));
self.syncing = true; self.syncing = true;
cx.notify();
self.refresh_task = Some(cx.spawn(async move |this, cx| { self.refresh_task = Some(cx.spawn(async move |this, cx| {
// Scope the reconcile to the follow set known so far // Scope the reconcile to the follow set known so far
@@ -265,12 +266,13 @@ impl WebOfTrust {
this.snapshot = Some(snapshot); this.snapshot = Some(snapshot);
cx.emit(WoTEvent); cx.emit(WoTEvent);
cx.notify();
if grew { if grew {
this.sync(cx); this.sync(cx);
} }
} }
cx.notify();
}) })
.ok(); .ok();
})); }));
@@ -314,6 +316,11 @@ impl WebOfTrust {
.is_some_and(|snapshot| !snapshot.disabled) .is_some_and(|snapshot| !snapshot.disabled)
} }
/// Whether a reconciliation with the relays is in flight
pub fn is_syncing(&self) -> bool {
self.syncing
}
/// Whether the snapshot covers a follow set /// Whether the snapshot covers a follow set
fn has_data(&self) -> bool { fn has_data(&self) -> bool {
self.snapshot self.snapshot
+1
View File
@@ -27,4 +27,5 @@ nostr-connect.workspace = true
anyhow.workspace = true anyhow.workspace = true
serde.workspace = true serde.workspace = true
log.workspace = true log.workspace = true
rand.workspace = true
smallvec.workspace = true smallvec.workspace = true
+5 -4
View File
@@ -114,16 +114,16 @@ impl Sidebar {
}) })
} }
/// The bottom navigation, pinned below the list like signed's sidebar. pub(super) fn render_footer(&self, loading: bool, _cx: &mut Context<Self>) -> Div {
pub(super) fn render_footer(&self, _cx: &mut Context<Self>) -> Div {
v_flex() v_flex()
.p_2() .p_2()
.flex_shrink_0() .flex_shrink_0()
.gap_1() .gap_1()
.items_start() .items_start()
.justify_start() .justify_start()
.child( .when(!loading, |this| {
Nav::new("sidebar-settings") this.child(
Nav::new("settings")
.prefix(Icon::new(IconName::Settings).small()) .prefix(Icon::new(IconName::Settings).small())
.center_prefix(true) .center_prefix(true)
.label("Settings") .label("Settings")
@@ -132,6 +132,7 @@ impl Sidebar {
window.dispatch_action(Box::new(Command::ShowSettings), cx) window.dispatch_action(Box::new(Command::ShowSettings), cx)
}), }),
) )
})
} }
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
+151 -40
View File
@@ -5,20 +5,22 @@ use chat::{ChatEvent, ChatRegistry};
use common::DebouncedDelay; use common::DebouncedDelay;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled,
Task, WeakEntity, Window, div, img, retain_all, StyledImage, Subscription, Task, WeakEntity, Window, div, img, retain_all,
}; };
use gpui_kit::base::StyledExt as _; use gpui_kit::base::{Presence, Transition};
use gpui_kit::component::dock::{DockArea, Panel, PanelEvent}; use gpui_kit::component::dock::{DockArea, Panel, PanelEvent};
use gpui_kit::component::input::{InputEvent, InputState}; use gpui_kit::component::input::{InputEvent, InputState};
use gpui_kit::component::notification::Notification; use gpui_kit::component::notification::Notification;
use gpui_kit::component::spinner::Spinner; use gpui_kit::component::progress::ProgressCircle;
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex}; use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use instant::Duration; use instant::Duration;
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry}; use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry}; use person::{Directory, PersonRegistry};
pub(crate) use rows::open_room;
use search::SearchResult;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent}; use state::{FIND_DELAY, NostrRegistry, StateEvent};
use ui::title_bar_drag_handlers; use ui::title_bar_drag_handlers;
@@ -33,8 +35,8 @@ mod onboarding;
mod rows; mod rows;
mod search; mod search;
pub(crate) use rows::open_room; const PROGRESS_TICK_MS: u64 = 500;
use search::SearchResult; const PROGRESS_CAP: f32 = 95.0;
pub struct Sidebar { pub struct Sidebar {
focus_handle: FocusHandle, focus_handle: FocusHandle,
@@ -88,6 +90,11 @@ pub struct Sidebar {
/// The async global profile search. /// The async global profile search.
global_task: Option<Task<()>>, global_task: Option<Task<()>>,
/// Simulated gift wrap processing progress, shown while the backlog loads
chat_progress: f32,
/// Simulated web of trust sync progress
sync_progress: f32,
tasks: SmallVec<[Task<Result<(), Error>>; 1]>, tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
_subscriptions: SmallVec<[Subscription; 4]>, _subscriptions: SmallVec<[Subscription; 4]>,
} }
@@ -97,7 +104,6 @@ impl Sidebar {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let groups = GroupsRegistry::global(cx); let groups = GroupsRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let wot = PersonRegistry::global(cx).read(cx).wot();
let find_input = cx.new(|cx| { let find_input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
@@ -105,20 +111,22 @@ impl Sidebar {
.clean_on_escape() .clean_on_escape()
}); });
let mut tasks = smallvec![];
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push( subscriptions.push(
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| { cx.subscribe_in(&chat, window, move |this, _, event, _, cx| {
if event == &ChatEvent::Ping { match event {
ChatEvent::Ping => {
this.new_requests = true; this.new_requests = true;
cx.notify(); cx.notify();
}; }
}), ChatEvent::Loaded => {
); this.chat_progress = 0.0;
subscriptions.push(
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
cx.notify(); cx.notify();
}
_ => {}
};
}), }),
); );
@@ -126,7 +134,7 @@ impl Sidebar {
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| { cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
match event { match event {
GroupsEvent::Updated => { GroupsEvent::Updated => {
// TODO: find a better way? // TODO: we must not trigger re-render directly like this
cx.notify(); cx.notify();
} }
GroupsEvent::Error(error) => { GroupsEvent::Error(error) => {
@@ -198,6 +206,16 @@ impl Sidebar {
}, },
)); ));
tasks.push(cx.spawn(async move |this, cx| {
let timer = Duration::from_millis(PROGRESS_TICK_MS);
loop {
cx.background_executor().timer(timer).await;
this.update(cx, |this, cx| {
this.tick_progress(cx);
})?;
}
}));
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
dock, dock,
@@ -223,7 +241,9 @@ impl Sidebar {
global_finding: false, global_finding: false,
global_results: None, global_results: None,
global_task: None, global_task: None,
tasks: smallvec![], chat_progress: 0.0,
sync_progress: 0.0,
tasks,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -251,6 +271,80 @@ impl Sidebar {
self.hide_group(cx); self.hide_group(cx);
} }
} }
/// Advance the simulated loading progress of any active background work.
fn tick_progress(&mut self, cx: &mut Context<Self>) {
let logged_in = NostrRegistry::global(cx).read(cx).current_user().is_some();
let chat = ChatRegistry::global(cx);
let wot = PersonRegistry::global(cx).read(cx).wot();
let chat_loading = logged_in && chat.read(cx).loading();
let syncing = logged_in && wot.read(cx).is_syncing();
let mut changed = false;
if chat_loading {
let next = self.advance_progress(self.chat_progress);
if next > self.chat_progress {
self.chat_progress = next;
changed = true;
}
} else if self.chat_progress != 0.0 {
self.chat_progress = 0.0;
changed = true;
}
if syncing {
let next = self.advance_progress(self.sync_progress);
if next > self.sync_progress {
self.sync_progress = next;
changed = true;
}
} else if self.sync_progress != 0.0 {
self.sync_progress = 0.0;
changed = true;
}
if changed {
cx.notify();
}
}
/// Bump simulated progress by a random step
fn advance_progress(&self, progress: f32) -> f32 {
(progress + rand::random::<f32>() * 8.0 + 1.0).min(PROGRESS_CAP)
}
/// A floating pill showing simulated progress for a background operation.
fn render_loading_pill(
&self,
id: &'static str,
label: &'static str,
progress: f32,
opacity: f32,
cx: &Context<Self>,
) -> Div {
h_flex()
.opacity(opacity)
.h_9()
.w_full()
.px_5()
.gap_2p5()
.border_t_1()
.border_color(cx.theme().border)
.child(
ProgressCircle::new(id)
.value(progress)
.color(cx.theme().primary)
.accessibility_label(label),
)
.child(
div()
.text_sm()
.text_color(cx.theme().sidebar_foreground)
.child(label),
)
}
} }
impl gpui_kit::component::dock::BasePanel for Sidebar { impl gpui_kit::component::dock::BasePanel for Sidebar {
@@ -290,6 +384,9 @@ impl Render for Sidebar {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let loading = chat.read(cx).loading(); let loading = chat.read(cx).loading();
let wot = PersonRegistry::global(cx).read(cx).wot();
let syncing = wot.read(cx).is_syncing();
let group = self.group.clone().and_then(|group| group.upgrade()); let group = self.group.clone().and_then(|group| group.upgrade());
let searching = self.search_open && !self.find_input.read(cx).value().is_empty(); let searching = self.search_open && !self.find_input.read(cx).value().is_empty();
let picking_contacts = self.contacts_open && !self.requests_open; let picking_contacts = self.contacts_open && !self.requests_open;
@@ -343,32 +440,46 @@ impl Render for Sidebar {
}), }),
) )
.when(logged_in && !picking_contacts, |this| { .when(logged_in && !picking_contacts, |this| {
this.child(self.render_footer(cx)) this.child(self.render_footer(loading || syncing, cx))
}) })
.when(loading && logged_in, |this| { .when(logged_in, |this| {
let pill_motion = Transition::new(cx.theme().motion_tokens().duration_normal)
.easing(cx.theme().motion_tokens().easing_move.clone());
let chat_presence = Presence::new("chat-presence", loading)
.transition(pill_motion.clone())
.sample(window, cx);
let wot_presence = Presence::new("wot-presence", syncing)
.transition(pill_motion)
.sample(window, cx);
this.child( this.child(
div() v_flex()
.absolute() .absolute()
.bottom_4() .bottom_0()
.left_0() .left_0()
.h_9()
.w_full() .w_full()
.px_8() .bg(cx.theme().sidebar)
.child( .text_color(cx.theme().sidebar_foreground)
h_flex() .children(chat_presence.should_render().then(|| {
.gap_2() self.render_loading_pill(
.w_full() "chat-sync",
.h_9() "Syncing messages",
.justify_center() self.chat_progress,
.bg(cx.theme().background.opacity(0.85)) chat_presence.progress,
.when(cx.theme().shadow, |this| this.shadow_md()) cx,
.rounded_full() )
.text_xs() }))
.font_semibold() .children(wot_presence.should_render().then(|| {
.text_color(cx.theme().muted_foreground) self.render_loading_pill(
.child(Spinner::new().small().color(cx.theme().primary)) "wot-sync",
.child("Getting messages..."), "Syncing contacts",
), self.sync_progress,
wot_presence.progress,
cx,
)
})),
) )
}) })
.into_any_element() .into_any_element()