update sidebar
This commit is contained in:
@@ -46,6 +46,8 @@ setting_accessors! {
|
||||
pub nip4e: bool,
|
||||
pub trusted_relays: Vec<String>,
|
||||
pub file_server: Url,
|
||||
pub pinned_rooms: Vec<u64>,
|
||||
pub expanded_sections: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Signer kind
|
||||
@@ -130,6 +132,14 @@ pub struct Settings {
|
||||
|
||||
/// Server for blossom media attachments
|
||||
pub file_server: Url,
|
||||
|
||||
/// Pinned sidebar room ids, in pin order
|
||||
#[serde(default)]
|
||||
pub pinned_rooms: Vec<u64>,
|
||||
|
||||
/// Expanded sidebar tree sections; `None` means the default sections
|
||||
#[serde(default)]
|
||||
pub expanded_sections: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -142,6 +152,8 @@ impl Default for Settings {
|
||||
nip4e: false,
|
||||
trusted_relays: vec![],
|
||||
file_server: Url::parse(DEFAULT_FILE_SERVER).unwrap(),
|
||||
pinned_rooms: vec![],
|
||||
expanded_sections: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,6 +183,13 @@ impl AppSettings {
|
||||
cx.global::<GlobalAppSettings>().0.clone()
|
||||
}
|
||||
|
||||
/// The underlying settings entity, which notifies whenever any field changes.
|
||||
/// Settings load asynchronously, so observers can watch it to pick up values
|
||||
/// that arrive after construction.
|
||||
pub fn entity(&self) -> &Entity<Settings> {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Set the global settings instance
|
||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||
cx.set_global(GlobalAppSettings(state));
|
||||
|
||||
@@ -12,6 +12,7 @@ use gpui::{
|
||||
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
||||
};
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
@@ -49,17 +50,17 @@ pub struct Sidebar {
|
||||
pinned_rooms: Vec<u64>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let settings = AppSettings::global(cx).read(cx).entity().clone();
|
||||
let chat = ChatRegistry::global(cx);
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Subscribe for registry new events
|
||||
cx.subscribe_in(&chat, window, move |this, _s, event, _window, cx| {
|
||||
if event == &ChatEvent::Ping {
|
||||
this.new_requests = true;
|
||||
@@ -68,12 +69,16 @@ impl Sidebar {
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.observe(&settings, move |this, _settings, cx| {
|
||||
this.restore_state(cx);
|
||||
}));
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
new_requests: false,
|
||||
expanded: BTreeSet::from([TreeSection::Community, TreeSection::Messages]),
|
||||
pinned_rooms: Vec::new(),
|
||||
expanded: load_expanded(cx),
|
||||
pinned_rooms: AppSettings::get_pinned_rooms(cx),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -87,6 +92,7 @@ impl Sidebar {
|
||||
self.new_requests = false;
|
||||
}
|
||||
|
||||
self.save_expanded(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -94,16 +100,43 @@ impl Sidebar {
|
||||
self.expanded.contains(§ion)
|
||||
}
|
||||
|
||||
fn restore_state(&mut self, cx: &mut Context<Self>) {
|
||||
let pinned_rooms = AppSettings::get_pinned_rooms(cx);
|
||||
let expanded = load_expanded(cx);
|
||||
|
||||
if self.pinned_rooms == pinned_rooms && self.expanded == expanded {
|
||||
return;
|
||||
}
|
||||
|
||||
self.pinned_rooms = pinned_rooms;
|
||||
self.expanded = expanded;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn save_expanded(&self, cx: &mut App) {
|
||||
let keys = self
|
||||
.expanded
|
||||
.iter()
|
||||
.map(|section| section.key().to_string())
|
||||
.collect();
|
||||
AppSettings::update_expanded_sections(Some(keys), cx);
|
||||
}
|
||||
|
||||
fn pin_room(&mut self, room_id: u64, cx: &mut Context<Self>) {
|
||||
if !self.pinned_rooms.contains(&room_id) {
|
||||
self.pinned_rooms.push(room_id);
|
||||
}
|
||||
self.expanded.insert(TreeSection::Pins);
|
||||
|
||||
AppSettings::update_pinned_rooms(self.pinned_rooms.clone(), cx);
|
||||
self.save_expanded(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn unpin_room(&mut self, room_id: u64, cx: &mut Context<Self>) {
|
||||
self.pinned_rooms.retain(|id| *id != room_id);
|
||||
|
||||
AppSettings::update_pinned_rooms(self.pinned_rooms.clone(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -443,6 +476,16 @@ fn nav_item(id: &'static str, icon: IconName, label: &'static str, command: Comm
|
||||
})
|
||||
}
|
||||
|
||||
fn load_expanded(cx: &App) -> BTreeSet<TreeSection> {
|
||||
let Some(keys) = AppSettings::get_expanded_sections(cx) else {
|
||||
return BTreeSet::from([TreeSection::Community, TreeSection::Messages]);
|
||||
};
|
||||
|
||||
keys.iter()
|
||||
.filter_map(|key| TreeSection::from_key(key.as_str()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
fn panel_id(&self) -> SharedString {
|
||||
"Sidebar".into()
|
||||
|
||||
@@ -9,7 +9,6 @@ use gpui::{
|
||||
use theme::ActiveTheme;
|
||||
use ui::{Icon, IconName, Sizable, StyledExt, h_flex};
|
||||
|
||||
/// Collapsible tree sections; declaration order is render order.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum TreeSection {
|
||||
Pins,
|
||||
@@ -34,9 +33,27 @@ impl TreeSection {
|
||||
Self::Messages => IconName::Message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pins => "pins",
|
||||
Self::Requests => "requests",
|
||||
Self::Community => "community",
|
||||
Self::Messages => "messages",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_key(key: &str) -> Option<Self> {
|
||||
match key {
|
||||
"pins" => Some(Self::Pins),
|
||||
"requests" => Some(Self::Requests),
|
||||
"community" => Some(Self::Community),
|
||||
"messages" => Some(Self::Messages),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One rendered tree row, in flattened order.
|
||||
pub enum SidebarRow {
|
||||
Section {
|
||||
section: TreeSection,
|
||||
@@ -57,12 +74,10 @@ pub enum SidebarRow {
|
||||
},
|
||||
}
|
||||
|
||||
/// A community shown under the Community section.
|
||||
pub struct CommunityEntry {
|
||||
pub name: &'static str,
|
||||
}
|
||||
|
||||
/// Communities to show until the Concord backend is wired up.
|
||||
pub fn dummy_communities() -> &'static [CommunityEntry] {
|
||||
// TODO(concord): replace with ConcordRegistry communities, see docs/concord-usage.md.
|
||||
&[
|
||||
@@ -75,7 +90,6 @@ pub fn dummy_communities() -> &'static [CommunityEntry] {
|
||||
]
|
||||
}
|
||||
|
||||
/// Presentation differences between the rows [`TreeRow`] draws.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TreeRowKind {
|
||||
Section,
|
||||
@@ -83,7 +97,6 @@ pub enum TreeRowKind {
|
||||
Hint,
|
||||
}
|
||||
|
||||
/// Folder/file row. One element for section headers, community rows and hints.
|
||||
#[derive(IntoElement)]
|
||||
pub struct TreeRow {
|
||||
id: ElementId,
|
||||
@@ -95,7 +108,6 @@ pub struct TreeRow {
|
||||
label: SharedString,
|
||||
count: Option<usize>,
|
||||
dot: bool,
|
||||
selected: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||
}
|
||||
@@ -116,7 +128,6 @@ impl TreeRow {
|
||||
label: label.into(),
|
||||
count: None,
|
||||
dot: false,
|
||||
selected: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
@@ -151,11 +162,6 @@ impl TreeRow {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
@@ -196,9 +202,6 @@ impl RenderOnce for TreeRow {
|
||||
.font_normal()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
})
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().ghost_element_selected)
|
||||
})
|
||||
.when_some(self.caret, |this, caret| {
|
||||
this.child(Icon::new(caret).xsmall().text_color(cx.theme().icon_muted))
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Sidebar tree redesign
|
||||
|
||||
Status: steps 1-7 implemented. Search lives in `panels/search.rs`; the sidebar
|
||||
Status: steps 1-9 implemented. Search lives in `panels/search.rs`; the sidebar
|
||||
renders the nav rail, the flattened tree, per-row pin/unpin menus, and the
|
||||
Community section from placeholder data (`TODO(concord)`). Remaining: optional
|
||||
step 8 (persistence), step 9 (remove the unused `TreeRow::selected` and run the
|
||||
final cleanup).
|
||||
Community section from placeholder data (`TODO(concord)`). Pins and expanded
|
||||
sections persist through `settings::Settings`. `cargo check`, `cargo clippy
|
||||
--workspace --all-targets` and `rustfmt --check` on the changed files are clean.
|
||||
Remaining: the §15 manual QA checklist (needs the running app).
|
||||
|
||||
Scope: `crates/workspace/src/sidebar` (`mod.rs`, `entry.rs`, new `tree.rs`),
|
||||
new panel shells in `crates/workspace/src/panels/`, and the `Command` wiring in
|
||||
@@ -108,6 +109,20 @@ pinned_rooms: Vec<u64>, // room ids in pin order
|
||||
Defaults: `expanded = {Community, Messages}` (Requests intentionally absent;
|
||||
Pins only matters when non-empty and starts expanded).
|
||||
|
||||
Both fields persist through `settings::Settings`:
|
||||
|
||||
```rust
|
||||
#[serde(default)] pinned_rooms: Vec<u64>,
|
||||
#[serde(default)] expanded_sections: Option<Vec<String>>,
|
||||
```
|
||||
|
||||
`expanded_sections` is an `Option` so that an empty list (the user collapsed
|
||||
everything) is distinguishable from the field never having been written, which
|
||||
keeps the `{Community, Messages}` default. `TreeSection::key()`/`from_key()` map
|
||||
the sections to their stable string keys. Because settings load asynchronously,
|
||||
`Sidebar` observes the settings entity and re-reads both fields in
|
||||
`restore_state` instead of trusting the constructor's read.
|
||||
|
||||
New methods:
|
||||
|
||||
```rust
|
||||
@@ -116,6 +131,7 @@ fn is_expanded(&self, section: TreeSection) -> bool;
|
||||
fn pin_room(&mut self, room_id: u64, cx: &mut Context<Self>);
|
||||
fn unpin_room(&mut self, room_id: u64, cx: &mut Context<Self>);
|
||||
fn is_pinned(&self, room_id: u64) -> bool;
|
||||
fn restore_state(&mut self, cx: &mut Context<Self>); // step 8
|
||||
fn tree_rows(&self, cx: &App) -> Vec<SidebarRow>; // see §5
|
||||
```
|
||||
|
||||
@@ -349,14 +365,34 @@ unused until step 5 consumes them. Run the checks in §15 after each step.
|
||||
flattening and rendering landed with step 5 (`SidebarRow::Community` ->
|
||||
`TreeRow`), so this step added the missing hint branch and confirmed the §10
|
||||
placeholder names.
|
||||
- [ ] **Step 8 (optional) — persistence.** Add
|
||||
`#[serde(default)] pinned_rooms: Vec<u64>` (and optionally
|
||||
`expanded_sections: Vec<String>`) to `settings::Settings`, register accessors
|
||||
in `setting_accessors!`, and load/save from `Sidebar`. The `#[serde(default)]`
|
||||
attribute is required: `Settings` has no defaults today, so a new field
|
||||
without it breaks parsing of existing `.settings` files.
|
||||
- [ ] **Step 9 — cleanup.** `cargo fmt`, remove dead imports/helpers, run
|
||||
clippy.
|
||||
- [x] **Step 8 — persistence.** `settings::Settings` gained
|
||||
`#[serde(default)] pinned_rooms: Vec<u64>` and
|
||||
`#[serde(default)] expanded_sections: Option<Vec<String>>`, both registered in
|
||||
`setting_accessors!` (so `AppSettings::get_*`/`update_*` exist). The
|
||||
`#[serde(default)]` attribute is required: `Settings` has no serde defaults, so
|
||||
a new field without it breaks parsing of existing `.settings` files. `Sidebar::new`
|
||||
loads both (falling back to the default sections when the setting is `None`),
|
||||
and `toggle_section`/`pin_room`/`unpin_room` write back through
|
||||
`AppSettings::update_*`; the settings observer already saves on every change, so
|
||||
no explicit file I/O was added. `expanded_sections` is `Option` so that
|
||||
collapsing every folder does not silently revert to the default on restart.
|
||||
Stale pinned ids are still skipped at flatten time rather than pruned on load.
|
||||
|
||||
Settings load asynchronously (a deferred, background file read), so the
|
||||
constructor's read always sees defaults on a cold start. To pick up the loaded
|
||||
values, `AppSettings::entity()` now exposes the inner `Entity<Settings>` (it
|
||||
notifies on every field change) and `Sidebar` observes it, re-reading through
|
||||
`restore_state` and re-rendering only when the values actually differ. Without
|
||||
this the sidebar would render with empty pins until the next unrelated change.
|
||||
The observation is on the inner entity because `AppSettings` itself never
|
||||
notifies its own observers.
|
||||
- [x] **Step 9 — cleanup.** Removed `TreeRow::selected` (the field, the builder
|
||||
method, and the `ghost_element_selected` render branch) — it was the only dead
|
||||
code left after step 5. No other unused imports or helpers remained.
|
||||
`cargo clippy --workspace --all-targets` reports zero warnings. Formatting is
|
||||
checked per file with `rustfmt +nightly --check`; `cargo fmt --all` is **not**
|
||||
run, because the repo's committed formatting does not match the installed
|
||||
nightly rustfmt (many pre-existing diffs in unrelated files).
|
||||
|
||||
## 13. Files touched
|
||||
|
||||
@@ -370,7 +406,7 @@ unused until step 5 consumes them. Run the checks in §15 after each step.
|
||||
| `crates/workspace/src/lib.rs` | `Command` variants + `on_command` arms |
|
||||
| `crates/ui/src/icon.rs` | New icon variants |
|
||||
| `assets/icons/{folder,compass,message}.svg` | New assets |
|
||||
| `crates/settings/src/lib.rs` | Optional step 8 only |
|
||||
| `crates/settings/src/lib.rs` | Step 8: `pinned_rooms`, `expanded_sections`, accessors, `entity()` |
|
||||
|
||||
## 14. Edge cases
|
||||
|
||||
@@ -394,8 +430,10 @@ unused until step 5 consumes them. Run the checks in §15 after each step.
|
||||
|
||||
## 15. Validation
|
||||
|
||||
- `cargo fmt --check` (workspace `rustfmt.toml`).
|
||||
- `cargo check -p workspace` and `cargo clippy -p workspace --all-targets`.
|
||||
- `rustfmt +nightly --check` on the changed files (not `cargo fmt --all`: the
|
||||
repo's committed formatting does not match the installed nightly rustfmt, so a
|
||||
workspace-wide check reports many pre-existing diffs).
|
||||
- `cargo check --workspace` and `cargo clippy --workspace --all-targets`.
|
||||
- Manual QA checklist:
|
||||
- Inbox/Browse/Search each open their panel; clicking the same nav item again
|
||||
focuses the existing panel instead of duplicating it;
|
||||
@@ -409,6 +447,8 @@ unused until step 5 consumes them. Run the checks in §15 after each step.
|
||||
room; clicking a pinned row opens it;
|
||||
- Messages lists ongoing rooms and still opens the screening modal for
|
||||
non-ongoing rooms;
|
||||
- pins and expanded/collapsed folders survive an app restart (collapsing every
|
||||
folder also survives, rather than reverting to the default sections);
|
||||
- empty states at 0 ongoing and 0 requests.
|
||||
- There is no GPUI test infrastructure in the repo (no `#[gpui::test]`
|
||||
anywhere), so tests are limited to pure helpers (`TreeSection` defaults, pin
|
||||
@@ -417,7 +457,8 @@ unused until step 5 consumes them. Run the checks in §15 after each step.
|
||||
|
||||
## 16. Open questions
|
||||
|
||||
1. **Persistence.** Persist pins and folder state, or keep them session-local?
|
||||
1. **Persistence.** Resolved in step 8: pins and expanded sections persist in
|
||||
`settings::Settings`.
|
||||
2. **Row density.** `h_8` vs the current `h_9`; `SIDEBAR_WIDTH` stays 240px for
|
||||
now, one indent level fits.
|
||||
3. **Community entries.** Preferred dummy names/branding before the real
|
||||
|
||||
Reference in New Issue
Block a user