-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathapp.rs
More file actions
88 lines (79 loc) · 3.88 KB
/
Copy pathapp.rs
File metadata and controls
88 lines (79 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//! Root component: provides global state and the top-level state machine.
//!
//! Two states, handled by a root conditional (NOT routing — see CLAUDE.md §5):
//! - Disconnected -> `ConnectionManager` (full screen, no studio chrome)
//! - Connected -> `Studio`
//!
//! Global state is exposed as fine-grained signals via context. The backend
//! seam is a `dyn Backend` behind an `Rc`, so swapping the mock for a
//! real NodeDB client later is a one-line change here.
use std::rc::Rc;
use dioxus::prelude::*;
use crate::modals::ModalHost;
use crate::models::notification::Notification;
use crate::services::async_state::AsyncState;
use crate::services::backend::Backend;
use crate::services::connection_service::MockConnectionService;
use crate::state::connection::ActiveConnection;
use crate::state::connections_registry::SavedConnection;
use crate::state::preferences::Preferences;
use crate::state::ui::ModalKind;
use crate::views::connection_manager::ConnectionManager;
use crate::views::studio_shell::Studio;
const STYLES: Asset = asset!("/assets/styles.css");
// Dioxus components are PascalCase by convention; `App` is the root component.
#[allow(non_snake_case)]
pub fn App() -> Element {
// The single seam to the outside world. Provided as a trait object so a
// real client can replace the mock without touching consumers.
let service: Rc<dyn Backend> = Rc::new(MockConnectionService::ready());
// The real-client stub's instantiability + object-safety behind the seam is
// proven by `nodedb_service::tests::stub_is_object_safe_behind_rc`, so it is
// not constructed here on every render.
use_context_provider(|| service.clone());
use_context_provider(|| Signal::new(None::<ActiveConnection>));
let mut registry = use_context_provider(|| Signal::new(Vec::<SavedConnection>::new()));
// Single source of truth for notifications: the bell badge AND the popover
// read this one store; user actions (mark-all-read / click) mutate it.
let mut notifications =
use_context_provider(|| Signal::new(AsyncState::<Vec<Notification>>::Loading));
use_context_provider(|| Signal::new(Preferences::default()));
// Modal state is provided here (not in Studio) because Preferences is
// reachable while disconnected and via Cmd+, in either state.
use_context_provider(|| Signal::new(None::<ModalKind>));
// Seed the registry + notification feed asynchronously, at the seam. The
// mock resolves instantly; the real client awaits the network. The guard
// rule: clone the Rc before the async block and `.set()` the signals only
// AFTER the await resolves — never hold a read/write guard across `.await`.
// The Resource handle is shared via context so a view (the notification
// popover's Retry) can reload the feed.
let reload_feed = {
let svc = service.clone();
use_resource(move || {
let svc = svc.clone();
async move {
// Reset to Loading at the start of each (re)load so a Retry shows
// the spinner. `.set()` holds no guard across the awaits below.
notifications.set(AsyncState::Loading);
if let Ok(conns) = svc.list_connections().await {
registry.set(conns);
}
// Map the seam result straight into the store: Ok(empty)->Empty,
// Ok(data)->Loaded, Err->Error.
notifications.set(AsyncState::from_value(Some(svc.notifications().await)));
}
})
};
use_context_provider(|| reload_feed);
let active = use_context::<Signal<Option<ActiveConnection>>>();
rsx! {
document::Stylesheet { href: STYLES }
if active.read().is_some() {
Studio {}
} else {
ConnectionManager {}
}
// Overlays everything; renders nothing when no modal is open.
ModalHost {}
}
}