feat: audit codebase (#72)
Reviewed-on: #72
This commit was merged in pull request #72.
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use auto_update::AutoUpdater;
|
||||
use auto_update::{AutoUpdater, UpdateDisplay};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui::AnyElement;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Anchor, App, Context, Div, InteractiveElement, IntoElement, ParentElement, SharedString,
|
||||
Stateful, Styled, Window, div, px,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui::{AnyElement, Entity};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use gpui_kit::component::Disableable;
|
||||
@@ -35,7 +35,7 @@ impl Sidebar {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Stateful<Div> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let updater = AutoUpdater::try_global(cx);
|
||||
let updater_display = self.updater_display.read(cx).clone();
|
||||
let profile = self.directory.read(cx).person(current_user, cx);
|
||||
let avatar = profile.avatar();
|
||||
let encryption_key = profile
|
||||
@@ -78,8 +78,8 @@ impl Sidebar {
|
||||
let actions = h_flex().gap_1();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let actions = actions.when_some(updater, |this, updater| {
|
||||
this.child(self.render_updater(updater, cx))
|
||||
let actions = actions.when_some(updater_display, |this, display| {
|
||||
this.child(self.render_updater(display))
|
||||
});
|
||||
|
||||
actions
|
||||
@@ -233,10 +233,12 @@ impl Sidebar {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
|
||||
let status = updater.read(cx).status();
|
||||
let up_to_date = updater.read(cx).up_to_date();
|
||||
let staged = updater.read(cx).staged();
|
||||
fn render_updater(&self, display: UpdateDisplay) -> AnyElement {
|
||||
let UpdateDisplay {
|
||||
up_to_date,
|
||||
staged,
|
||||
status,
|
||||
} = display;
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
@@ -257,10 +259,12 @@ impl Sidebar {
|
||||
.tooltip("Quit and relaunch into the installed update")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |_, _window, cx| {
|
||||
updater.update(cx, |this, cx| {
|
||||
this.restart(cx);
|
||||
});
|
||||
.on_click(|_, _window, cx| {
|
||||
if let Some(updater) = AutoUpdater::try_global(cx) {
|
||||
updater.update(cx, |this, cx| {
|
||||
this.restart(cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -173,14 +173,20 @@ impl Sidebar {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The chat requests awaiting a response.
|
||||
/// The chat requests awaiting a response, followed by the trash entry when
|
||||
/// it holds messages that could not be decrypted.
|
||||
pub(super) fn requests_rows(&self, cx: &App) -> Vec<SidebarRow> {
|
||||
ChatRegistry::global(cx)
|
||||
.read(cx)
|
||||
let chat = ChatRegistry::global(cx).read(cx);
|
||||
let mut rows: Vec<SidebarRow> = chat
|
||||
.rooms(&RoomKind::Request, cx)
|
||||
.into_iter()
|
||||
.map(|room| SidebarRow::Room { room })
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
if chat.trashed_count(cx) > 0 {
|
||||
rows.push(SidebarRow::Trash);
|
||||
}
|
||||
rows
|
||||
}
|
||||
|
||||
/// The overlay with the create button and selection count.
|
||||
|
||||
@@ -34,11 +34,44 @@ impl ListSection {
|
||||
}
|
||||
}
|
||||
|
||||
/// Cheap snapshot of the cached rows' inputs, compared on every render; covers
|
||||
/// inputs that never notify the sidebar (pin edits, room kind promotions, and
|
||||
/// the visibility flags mutated across the sidebar's modules).
|
||||
#[derive(Default, PartialEq)]
|
||||
pub(super) struct RowsSignature {
|
||||
contacts_open: bool,
|
||||
requests_open: bool,
|
||||
contacts_loaded: bool,
|
||||
pins_open: bool,
|
||||
groups_open: bool,
|
||||
chats_open: bool,
|
||||
pinned_rooms: Vec<u64>,
|
||||
pinned_groups: Vec<String>,
|
||||
/// Kinds of the rooms held by the cache; a request can turn ongoing with
|
||||
/// no event or registry notify the sidebar can hear
|
||||
room_kinds: Vec<(u64, RoomKind)>,
|
||||
/// Size of the undecryptable-messages trash, which no notify reaches
|
||||
trashed: usize,
|
||||
}
|
||||
|
||||
/// Read the id and kind of every room row held by the given cache.
|
||||
fn room_signature(rows: &[SidebarRow], cx: &App) -> Vec<(u64, RoomKind)> {
|
||||
rows.iter()
|
||||
.filter_map(|row| match row {
|
||||
SidebarRow::Room { room } => {
|
||||
let room = room.read(cx);
|
||||
Some((room.id, room.kind))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
pub(super) fn render_list(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let requests_open = self.requests_open;
|
||||
let contacts_open = self.contacts_open && !requests_open;
|
||||
let rows = Rc::new(self.rows_for(cx));
|
||||
let rows = self.cached_rows(cx);
|
||||
// Whether to show the create button for the contact picker
|
||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||
|
||||
@@ -50,9 +83,10 @@ impl Sidebar {
|
||||
("sidebar-list", None)
|
||||
};
|
||||
|
||||
let request_count = ChatRegistry::global(cx)
|
||||
.read(cx)
|
||||
.count(&RoomKind::Request, cx);
|
||||
let request_count = {
|
||||
let chat = ChatRegistry::global(cx).read(cx);
|
||||
chat.count(&RoomKind::Request, cx) + chat.trashed_count(cx)
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
@@ -175,6 +209,32 @@ impl Sidebar {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// Rebuild the rows only when a dirty source fired or the snapshot changed.
|
||||
fn cached_rows(&mut self, cx: &mut Context<Self>) -> Rc<Vec<SidebarRow>> {
|
||||
let mut signature = RowsSignature {
|
||||
contacts_open: self.contacts_open,
|
||||
requests_open: self.requests_open,
|
||||
contacts_loaded: self.contacts.is_some(),
|
||||
pins_open: self.pins_open,
|
||||
groups_open: self.is_groups_open(cx),
|
||||
chats_open: self.is_chats_open(cx),
|
||||
pinned_rooms: AppSettings::get_pinned_rooms(cx),
|
||||
pinned_groups: AppSettings::get_pinned_groups(cx),
|
||||
room_kinds: room_signature(&self.rows, cx),
|
||||
trashed: ChatRegistry::global(cx).read(cx).trashed_count(cx),
|
||||
};
|
||||
|
||||
if self.rows_dirty || self.rows_signature != signature {
|
||||
self.rows = Rc::new(self.rows_for(cx));
|
||||
self.rows_dirty = false;
|
||||
// Describe the rebuilt cache, so the next render compares apples to apples
|
||||
signature.room_kinds = room_signature(&self.rows, cx);
|
||||
}
|
||||
|
||||
self.rows_signature = signature;
|
||||
Rc::clone(&self.rows)
|
||||
}
|
||||
|
||||
fn rows_for(&self, cx: &App) -> Vec<SidebarRow> {
|
||||
if self.contacts_open {
|
||||
return self.contacts_rows();
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Error;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use auto_update::{AutoUpdater, UpdateDisplay};
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::DebouncedDelay;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -16,12 +19,13 @@ use gpui_kit::component::notification::Notification;
|
||||
use gpui_kit::component::progress::ProgressCircle;
|
||||
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
|
||||
use instant::Duration;
|
||||
use list::RowsSignature;
|
||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{Directory, PersonRegistry};
|
||||
use rows::SidebarRow;
|
||||
pub(crate) use rows::open_room;
|
||||
use search::SearchResult;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::{FIND_DELAY, NostrRegistry, StateEvent};
|
||||
use ui::title_bar_drag_handlers;
|
||||
@@ -44,7 +48,6 @@ pub struct Sidebar {
|
||||
dock: WeakEntity<DockArea>,
|
||||
/// Random background shown behind the signed-out screen.
|
||||
banner: SharedString,
|
||||
|
||||
/// The group whose information the sidebar is browsing, if any.
|
||||
group: Option<WeakEntity<Group>>,
|
||||
/// Whether the sidebar is showing the group's information instead of its list.
|
||||
@@ -53,14 +56,12 @@ pub struct Sidebar {
|
||||
admins_open: bool,
|
||||
/// Whether the group view's members section is expanded.
|
||||
members_open: bool,
|
||||
|
||||
/// Whether the main list's pinned section is expanded.
|
||||
pins_open: bool,
|
||||
/// Whether the user expanded or collapsed the main list's groups section.
|
||||
groups_open: Option<bool>,
|
||||
/// Same as `groups_open` for the chats section.
|
||||
chats_open: Option<bool>,
|
||||
|
||||
/// Whether the main list is showing contacts to start a chat with.
|
||||
contacts_open: bool,
|
||||
/// The signed-in user's contacts, loaded when the contact picker opens.
|
||||
@@ -69,11 +70,9 @@ pub struct Sidebar {
|
||||
directory: Entity<Directory>,
|
||||
/// Contacts selected in the contact picker.
|
||||
selected_contacts: HashSet<PublicKey>,
|
||||
|
||||
/// Whether the main list is showing chat requests instead of chats and groups.
|
||||
requests_open: bool,
|
||||
new_requests: bool,
|
||||
|
||||
/// Whether the search bar is shown below the header.
|
||||
search_open: bool,
|
||||
/// Whether searches also cover global (relay) profiles.
|
||||
@@ -90,12 +89,19 @@ pub struct Sidebar {
|
||||
global_results: Option<Vec<PublicKey>>,
|
||||
/// 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,
|
||||
|
||||
/// Cached main list rows; rebuilt only when invalidation is signaled
|
||||
rows: Rc<Vec<SidebarRow>>,
|
||||
/// Snapshot of the cached rows' inputs, compared on every render
|
||||
rows_signature: RowsSignature,
|
||||
/// Whether the rows changed through a source the snapshot cannot see
|
||||
rows_dirty: bool,
|
||||
/// The updater facts the header renders, mirrored so updater churn cannot invalidate the cached sidebar
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
updater_display: Entity<Option<UpdateDisplay>>,
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
}
|
||||
@@ -105,7 +111,6 @@ impl Sidebar {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let groups = GroupsRegistry::global(cx);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let settings = AppSettings::global(cx);
|
||||
let directory = Directory::new(cx);
|
||||
|
||||
let find_input = cx.new(|cx| {
|
||||
@@ -122,10 +127,12 @@ impl Sidebar {
|
||||
match event {
|
||||
ChatEvent::Ping => {
|
||||
this.new_requests = true;
|
||||
this.rows_dirty = true;
|
||||
cx.notify();
|
||||
}
|
||||
ChatEvent::Loaded => {
|
||||
this.chat_progress = 0.0;
|
||||
this.rows_dirty = true;
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
@@ -134,9 +141,10 @@ impl Sidebar {
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
||||
cx.subscribe_in(&groups, window, |this, _, event, window, cx| {
|
||||
match event {
|
||||
GroupsEvent::Updated => {
|
||||
this.rows_dirty = true;
|
||||
// TODO: we must not trigger re-render directly like this
|
||||
cx.notify();
|
||||
}
|
||||
@@ -149,11 +157,24 @@ impl Sidebar {
|
||||
} => {
|
||||
window.push_notification(Notification::warning(warning.clone()), cx);
|
||||
}
|
||||
// New groups can arrive without `Updated` and without a
|
||||
// registry notify, so only the event itself can dirty the cache
|
||||
GroupsEvent::Synced | GroupsEvent::Created { .. } => {
|
||||
this.rows_dirty = true;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(cx.observe(&chat, |this, _, _| {
|
||||
this.rows_dirty = true;
|
||||
}));
|
||||
|
||||
subscriptions.push(cx.observe(&groups, |this, _, _| {
|
||||
this.rows_dirty = true;
|
||||
}));
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(
|
||||
&nostr,
|
||||
@@ -209,10 +230,23 @@ impl Sidebar {
|
||||
},
|
||||
));
|
||||
|
||||
// The user header reads settings and the tracked profiles on render, so
|
||||
// the encryption button reacts to toggles and announcement arrivals
|
||||
subscriptions.push(cx.observe(&settings, |_, _, cx| cx.notify()));
|
||||
subscriptions.push(cx.observe(&directory, |_, _, cx| cx.notify()));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let updater_display =
|
||||
cx.new(|cx| AutoUpdater::try_global(cx).map(|updater| updater.read(cx).display()));
|
||||
|
||||
// Mirror only the header's rendered facts; updater churn must not invalidate the whole cached sidebar
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if let Some(updater) = AutoUpdater::try_global(cx) {
|
||||
subscriptions.push(cx.observe(&updater, |this, updater, cx| {
|
||||
let display = updater.read(cx).display();
|
||||
this.updater_display.update(cx, |state, cx| {
|
||||
if state.as_ref() != Some(&display) {
|
||||
*state = Some(display);
|
||||
cx.notify();
|
||||
}
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
tasks.push(cx.spawn(async move |this, cx| {
|
||||
let timer = Duration::from_millis(PROGRESS_TICK_MS);
|
||||
@@ -251,6 +285,11 @@ impl Sidebar {
|
||||
global_task: None,
|
||||
chat_progress: 0.0,
|
||||
sync_progress: 0.0,
|
||||
rows: Rc::default(),
|
||||
rows_signature: RowsSignature::default(),
|
||||
rows_dirty: true,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
updater_display,
|
||||
tasks,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use chat::{Room, RoomKind};
|
||||
use chat::{ChatRegistry, Room, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
};
|
||||
use gpui_kit::base::StyledExt as _;
|
||||
use gpui_kit::component::dialog::DialogButtonProps;
|
||||
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::dock::{DockArea, DockPlacement, PanelHandle};
|
||||
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
||||
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex};
|
||||
use nip29::Group;
|
||||
@@ -30,9 +30,12 @@ pub(crate) fn open_room(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let panel = chat_ui::init(room.downgrade(), window, cx);
|
||||
let room_id = panel.read(cx).room_id().clone();
|
||||
|
||||
crate::dock::add_panel_to(
|
||||
dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
PanelHandle::new(panel),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
@@ -41,8 +44,11 @@ pub(crate) fn open_room(
|
||||
if room.read(cx).kind != RoomKind::Ongoing && AppSettings::get_screening(cx) {
|
||||
let peer = room.read(cx).display_member(cx).public_key();
|
||||
let view = screening::init(peer, window, cx);
|
||||
let dock = dock.clone();
|
||||
|
||||
window.open_alert_dialog(cx, move |alert, _window, _cx| {
|
||||
let dock = dock.clone();
|
||||
let room_id = room_id.clone();
|
||||
alert
|
||||
.confirm()
|
||||
.child(view.clone())
|
||||
@@ -52,7 +58,11 @@ pub(crate) fn open_room(
|
||||
.ok_text("Response"),
|
||||
)
|
||||
.on_cancel(move |_event, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
if let Err(error) = dock.update(cx, |area, cx| {
|
||||
crate::dock::close_room_panel(area, &room_id, window, cx);
|
||||
}) {
|
||||
log::warn!("Failed to close the request room's tab: {error}");
|
||||
}
|
||||
true
|
||||
})
|
||||
});
|
||||
@@ -78,6 +88,8 @@ pub(super) enum SidebarRow {
|
||||
Message {
|
||||
hit: MessageHit,
|
||||
},
|
||||
/// Entry point to the trash panel, shown under requests
|
||||
Trash,
|
||||
/// A non-interactive label, such as a search results section.
|
||||
Heading(&'static str),
|
||||
Empty(&'static str),
|
||||
@@ -95,10 +107,7 @@ impl Sidebar {
|
||||
rows.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(offset, row)| {
|
||||
let index = range.start + offset;
|
||||
|
||||
.map(|row| {
|
||||
let element = match row {
|
||||
SidebarRow::Section(section) => {
|
||||
let section = *section;
|
||||
@@ -171,7 +180,7 @@ impl Sidebar {
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
|
||||
let nav = Nav::new(index)
|
||||
let nav = Nav::new(id)
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
@@ -210,7 +219,7 @@ impl Sidebar {
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.id(id)
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
@@ -241,7 +250,7 @@ impl Sidebar {
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
@@ -265,7 +274,7 @@ impl Sidebar {
|
||||
let person = self.directory.read(cx).person(public_key, cx);
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(index)
|
||||
Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
@@ -298,7 +307,7 @@ impl Sidebar {
|
||||
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
||||
.map(SharedString::from);
|
||||
|
||||
let nav = Nav::new(index)
|
||||
let nav = Nav::new(tag.clone())
|
||||
.label(name)
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
@@ -318,8 +327,11 @@ impl Sidebar {
|
||||
)
|
||||
})
|
||||
.on_click(move |_event, window, cx| {
|
||||
crate::dock::add_panel_to(
|
||||
let group_key = group.read(cx).key().clone();
|
||||
|
||||
crate::dock::add_group_panel_to(
|
||||
&dock,
|
||||
group_key,
|
||||
PanelHandle::new(nip29_ui::init(group.clone(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
@@ -334,7 +346,7 @@ impl Sidebar {
|
||||
});
|
||||
|
||||
div()
|
||||
.id(index)
|
||||
.id(tag.clone())
|
||||
.w_full()
|
||||
.child(nav)
|
||||
.context_menu(move |menu, _window, _cx| {
|
||||
@@ -370,8 +382,15 @@ impl Sidebar {
|
||||
let dock = self.dock.clone();
|
||||
let room = hit.room.clone();
|
||||
let content = hit.content.clone();
|
||||
// Several hits can share a room, so the id digests the whole hit
|
||||
let hit_id = common::stable_id_u64(&[
|
||||
hit.room.read(cx).id.to_string().as_str(),
|
||||
hit.author.to_string().as_str(),
|
||||
hit.created_at.as_secs().to_string().as_str(),
|
||||
hit.content.as_ref(),
|
||||
]);
|
||||
|
||||
Nav::new(index)
|
||||
Nav::new(hit_id)
|
||||
.label(content)
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
@@ -394,6 +413,34 @@ impl Sidebar {
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Trash => {
|
||||
let count = ChatRegistry::global(cx).read(cx).trashed_count(cx);
|
||||
let dock = self.dock.clone();
|
||||
|
||||
Nav::new("sidebar-trash")
|
||||
.label("Trash")
|
||||
.text_sm()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(Avatar::new(None).seed("trash").small())
|
||||
})
|
||||
.suffix(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(count.to_string())),
|
||||
)
|
||||
.on_click(move |_event, window, cx| {
|
||||
let panel = chat_ui::trash::init(window, cx);
|
||||
crate::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(panel),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
};
|
||||
// Keep a small gap between rows so selected rows read as pills.
|
||||
div().pb_0p5().child(element).into_any_element()
|
||||
|
||||
Reference in New Issue
Block a user