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"
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]]
name = "futf"
version = "0.1.5"
@@ -5225,20 +5216,21 @@ dependencies = [
[[package]]
name = "notify"
version = "8.2.0"
version = "8.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
checksum = "82f812702fbc75d35416d3f884e04925be7e9498f4eceea2e24ba34559284674"
dependencies = [
"bitflags 2.13.2",
"fsevent-sys",
"inotify",
"kqueue",
"libc",
"log",
"mio",
"notify-types",
"objc2-core-foundation",
"objc2-core-services",
"walkdir",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -5565,6 +5557,16 @@ dependencies = [
"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]]
name = "objc2-core-text"
version = "0.3.2"
@@ -9728,15 +9730,6 @@ dependencies = [
"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]]
name = "windows-sys"
version = "0.61.2"
@@ -10022,6 +10015,7 @@ dependencies = [
"nostr-connect",
"nostr-sdk",
"person",
"rand 0.10.3",
"serde",
"settings",
"smallvec",
+2
View File
@@ -47,6 +47,7 @@ impl Global for GlobalChatRegistry {}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChatEvent {
Ping,
Loaded,
InboxRelayNotFound,
Error(String),
}
@@ -402,6 +403,7 @@ impl ChatRegistry {
if eose && tracking.swap(false, Ordering::AcqRel) {
this.update(cx, |this, cx| {
cx.emit(ChatEvent::Loaded);
this.get_rooms(cx);
})?;
}
+8 -1
View File
@@ -232,6 +232,7 @@ impl WebOfTrust {
.idle_timeout(Duration::from_secs(SYNC_IDLE_TIMEOUT));
self.syncing = true;
cx.notify();
self.refresh_task = Some(cx.spawn(async move |this, cx| {
// Scope the reconcile to the follow set known so far
@@ -265,12 +266,13 @@ impl WebOfTrust {
this.snapshot = Some(snapshot);
cx.emit(WoTEvent);
cx.notify();
if grew {
this.sync(cx);
}
}
cx.notify();
})
.ok();
}));
@@ -314,6 +316,11 @@ impl WebOfTrust {
.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
fn has_data(&self) -> bool {
self.snapshot
+1
View File
@@ -27,4 +27,5 @@ nostr-connect.workspace = true
anyhow.workspace = true
serde.workspace = true
log.workspace = true
rand.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, _cx: &mut Context<Self>) -> Div {
pub(super) fn render_footer(&self, loading: bool, _cx: &mut Context<Self>) -> Div {
v_flex()
.p_2()
.flex_shrink_0()
.gap_1()
.items_start()
.justify_start()
.child(
Nav::new("sidebar-settings")
.when(!loading, |this| {
this.child(
Nav::new("settings")
.prefix(Icon::new(IconName::Settings).small())
.center_prefix(true)
.label("Settings")
@@ -132,6 +132,7 @@ impl Sidebar {
window.dispatch_action(Box::new(Command::ShowSettings), cx)
}),
)
})
}
#[cfg(not(target_arch = "wasm32"))]
+151 -40
View File
@@ -5,20 +5,22 @@ use chat::{ChatEvent, ChatRegistry};
use common::DebouncedDelay;
use gpui::prelude::FluentBuilder;
use gpui::{
App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled, StyledImage, Subscription,
Task, WeakEntity, Window, div, img, retain_all,
App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Styled,
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::input::{InputEvent, InputState};
use gpui_kit::component::notification::Notification;
use gpui_kit::component::spinner::Spinner;
use gpui_kit::component::{ActiveTheme, Sizable, WindowExt, h_flex, v_flex};
use gpui_kit::component::progress::ProgressCircle;
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
use instant::Duration;
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
use nostr_sdk::prelude::*;
use person::{Directory, PersonRegistry};
pub(crate) use rows::open_room;
use search::SearchResult;
use smallvec::{SmallVec, smallvec};
use state::{FIND_DELAY, NostrRegistry, StateEvent};
use ui::title_bar_drag_handlers;
@@ -33,8 +35,8 @@ mod onboarding;
mod rows;
mod search;
pub(crate) use rows::open_room;
use search::SearchResult;
const PROGRESS_TICK_MS: u64 = 500;
const PROGRESS_CAP: f32 = 95.0;
pub struct Sidebar {
focus_handle: FocusHandle,
@@ -88,6 +90,11 @@ pub struct Sidebar {
/// The async global profile search.
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]>,
_subscriptions: SmallVec<[Subscription; 4]>,
}
@@ -97,7 +104,6 @@ impl Sidebar {
let chat = ChatRegistry::global(cx);
let groups = GroupsRegistry::global(cx);
let nostr = NostrRegistry::global(cx);
let wot = PersonRegistry::global(cx).read(cx).wot();
let find_input = cx.new(|cx| {
InputState::new(window, cx)
@@ -105,20 +111,22 @@ impl Sidebar {
.clean_on_escape()
});
let mut tasks = smallvec![];
let mut subscriptions = smallvec![];
subscriptions.push(
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
if event == &ChatEvent::Ping {
cx.subscribe_in(&chat, window, move |this, _, event, _, cx| {
match event {
ChatEvent::Ping => {
this.new_requests = true;
cx.notify();
};
}),
);
subscriptions.push(
cx.subscribe_in(&wot, window, |_this, _wot, _event, _window, cx| {
}
ChatEvent::Loaded => {
this.chat_progress = 0.0;
cx.notify();
}
_ => {}
};
}),
);
@@ -126,7 +134,7 @@ impl Sidebar {
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
match event {
GroupsEvent::Updated => {
// TODO: find a better way?
// TODO: we must not trigger re-render directly like this
cx.notify();
}
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 {
focus_handle: cx.focus_handle(),
dock,
@@ -223,7 +241,9 @@ impl Sidebar {
global_finding: false,
global_results: None,
global_task: None,
tasks: smallvec![],
chat_progress: 0.0,
sync_progress: 0.0,
tasks,
_subscriptions: subscriptions,
}
}
@@ -251,6 +271,80 @@ impl Sidebar {
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 {
@@ -290,6 +384,9 @@ impl Render for Sidebar {
let chat = ChatRegistry::global(cx);
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 searching = self.search_open && !self.find_input.read(cx).value().is_empty();
let picking_contacts = self.contacts_open && !self.requests_open;
@@ -343,32 +440,46 @@ impl Render for Sidebar {
}),
)
.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(
div()
v_flex()
.absolute()
.bottom_4()
.bottom_0()
.left_0()
.h_9()
.w_full()
.px_8()
.child(
h_flex()
.gap_2()
.w_full()
.h_9()
.justify_center()
.bg(cx.theme().background.opacity(0.85))
.when(cx.theme().shadow, |this| this.shadow_md())
.rounded_full()
.text_xs()
.font_semibold()
.text_color(cx.theme().muted_foreground)
.child(Spinner::new().small().color(cx.theme().primary))
.child("Getting messages..."),
),
.bg(cx.theme().sidebar)
.text_color(cx.theme().sidebar_foreground)
.children(chat_presence.should_render().then(|| {
self.render_loading_pill(
"chat-sync",
"Syncing messages",
self.chat_progress,
chat_presence.progress,
cx,
)
}))
.children(wot_presence.should_render().then(|| {
self.render_loading_pill(
"wot-sync",
"Syncing contacts",
self.sync_progress,
wot_presence.progress,
cx,
)
})),
)
})
.into_any_element()