Prune dead code and key elements by stable ids

This commit is contained in:
2026-11-13 04:37:13 +07:00
parent e9f29dc398
commit 8b9c8bd35e
27 changed files with 244 additions and 250 deletions
-1
View File
@@ -6,7 +6,6 @@ publish.workspace = true
[dependencies]
gpui.workspace = true
anyhow.workspace = true
rust-embed.workspace = true
gpui-kit.workspace = true
-3
View File
@@ -24,6 +24,3 @@ flume.workspace = true
futures.workspace = true
fuzzy-matcher = "0.3.7"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
smol.workspace = true
-5
View File
@@ -145,7 +145,6 @@ impl ChatPanel {
})
.unwrap_or(("Unknown".into(), "Message...".into()));
// Define input state
let input = cx.new(|cx| {
TextareaState::new(window, cx)
.placeholder(format!("Message {}", name))
@@ -153,7 +152,6 @@ impl ChatPanel {
.clean_on_escape()
});
// Define dictation state
let speech = cx.new(|cx| {
let state = SpeechState::new(cx)
.recognizer(speech::recognizer())
@@ -166,11 +164,9 @@ impl ChatPanel {
state
});
// Define subject input state
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
let subject_bar = cx.new(|_cx| false);
// Define subscriptions
let mut subscriptions = smallvec![];
subscriptions.push(
@@ -219,7 +215,6 @@ impl ChatPanel {
}),
);
// Define all functions that will run after the current cycle
cx.defer_in(window, |this, window, cx| {
this.connect(window, cx);
this.subscribe_room_events(window, cx);
-1
View File
@@ -11,7 +11,6 @@ futures.workspace = true
serde_json.workspace = true
state = { path = "../state" }
common = { path = "../common" }
person = { path = "../person" }
chat = { path = "../chat" }
gpui.workspace = true
+2 -2
View File
@@ -1065,7 +1065,8 @@ impl GroupPanel {
cx,
)
})
.element(ix.into(), window, cx),
// Key by event id: parse state is kept per element id and indices shift on prepend
.element(SharedString::from(message.id.to_hex()).into(), window, cx),
};
let admin = nostr
@@ -1077,7 +1078,6 @@ impl GroupPanel {
let pinned = group.pins().pins().contains(&Pin::Event(message.id));
message::render(
ix,
message,
content,
show_author,
+4 -4
View File
@@ -35,7 +35,6 @@ pub(crate) struct Actions {
}
pub(crate) fn render(
ix: usize,
message: &Message,
content: AnyElement,
show_author: bool,
@@ -60,7 +59,8 @@ pub(crate) fn render(
nostr.read(cx).current_user(),
);
let row = MessageRow::new(ix)
// Key by event id: indices shift on prepend and would re-key descendants' state
let row = MessageRow::new(SharedString::from(message.id.to_hex()))
.show_author(show_author)
.hide_avatar(hide_avatar)
.avatar(
@@ -337,7 +337,7 @@ fn replies(
let mut items = Vec::with_capacity(targets.len());
for (ix, id) in targets.iter().enumerate() {
for id in targets.iter() {
let context = entity.read_with(cx, |group, cx| {
let message = group.message(*id)?;
let author = persons.read(cx).person(&message.author, cx);
@@ -356,7 +356,7 @@ fn replies(
let panel = panel.clone();
items.push(
ReplyPreview::new(ix, author, preview)
ReplyPreview::new(SharedString::from(format!("reply-{id}")), author, preview)
.on_click(move |_event, _window, cx| {
if let Err(error) = panel.update(cx, |panel, cx| {
panel.scroll_to(&id, cx);
-1
View File
@@ -20,7 +20,6 @@ serde_json.workspace = true
futures.workspace = true
hkdf.workspace = true
sha2.workspace = true
tokio = { version = "1", default-features = false, features = [ "rt" ] }
cdk.workspace = true
cdk-redb.workspace = true
-41
View File
@@ -31,7 +31,6 @@ impl Global for GlobalWalletRegistry {}
#[derive(Debug, Clone)]
pub enum WalletEvent {
Updated,
TokenCreated(String),
Error(String),
}
@@ -203,36 +202,6 @@ impl WalletRegistry {
self.emit_updated(cx);
}
/// Pay a lightning invoice from the wallet.
pub fn melt(&mut self, invoice: String, cx: &mut Context<Self>) {
let Some(mint_url) = self.primary_mint(cx) else {
return;
};
self.schedule_job(
ops::Job::Melt {
mint: mint_url,
invoice,
},
cx,
);
}
/// Send ecash as a token string the recipient can paste anywhere.
pub fn send_token(&mut self, amount: u64, cx: &mut Context<Self>) {
let Some(mint_url) = self.primary_mint(cx) else {
return;
};
self.schedule_job(
ops::Job::SendToken {
mint: mint_url,
amount,
},
cx,
);
}
/// Send a NIP-61 nutzap to a peer, optionally nutzapping one of their events.
pub fn send_nutzap(
&mut self,
@@ -947,16 +916,6 @@ impl WalletRegistry {
self.emit_updated(cx);
log::info!("minted {amount} sats from quote {quote_id}");
}
ops::Outcome::Melted { update, amount } => {
self.apply_token_update(update);
self.emit_updated(cx);
log::info!("melted {amount} sats");
}
ops::Outcome::Sent { update, token } => {
self.apply_token_update(update);
cx.emit(WalletEvent::TokenCreated(token));
self.emit_updated(cx);
}
ops::Outcome::NutzapSent { update, amount } => {
self.apply_token_update(update);
self.emit_updated(cx);
+2 -79
View File
@@ -8,8 +8,8 @@ use cdk::amount::SplitTarget;
use cdk::mint_url::MintUrl;
use cdk::nuts::nut00::KnownMethod;
use cdk::nuts::{
CurrencyUnit, Id, KeySetInfo, MeltQuoteState, MintQuoteState, PaymentMethod, Proof, Proofs,
SecretKey, SpendingConditions, State,
CurrencyUnit, Id, KeySetInfo, MintQuoteState, PaymentMethod, Proof, Proofs, SecretKey,
SpendingConditions, State,
};
use cdk::secret::Secret;
use cdk::wallet::types::ProofInfo;
@@ -35,14 +35,6 @@ pub(crate) enum Job {
mint: MintUrl,
quote_id: String,
},
Melt {
mint: MintUrl,
invoice: String,
},
SendToken {
mint: MintUrl,
amount: u64,
},
SendNutzap {
recipient: PublicKey,
amount: u64,
@@ -117,67 +109,6 @@ impl Job {
amount,
})
}
Job::Melt { mint, invoice } => {
let wallet = ctx.wallet(&mint)?;
let quote = wallet
.melt_quote(
PaymentMethod::Known(KnownMethod::Bolt11),
invoice,
None,
None,
)
.await?;
let finalized = wallet
.prepare_melt(&quote.id, HashMap::new())
.await?
.confirm()
.await?;
if finalized.state() != MeltQuoteState::Paid {
return Err(anyhow!("melt did not settle: {:?}", finalized.state()));
}
let amount = u64::from(finalized.total_amount());
let update = ctx
.sync_token_events(
wallet,
nostr_mint(&mint)?,
&ctx.previous_ids(&mint),
Some(keys::History {
direction: TransactionDirection::Out,
amount,
redeemed: Vec::new(),
sender: None,
}),
)
.await?;
Ok(Outcome::Melted { update, amount })
}
Job::SendToken { mint, amount } => {
let wallet = ctx.wallet(&mint)?;
let prepared = wallet
.prepare_send(Amount::from(amount), SendOptions::default())
.await?;
let token = prepared.confirm(None).await?.to_string();
let update = ctx
.sync_token_events(
wallet,
nostr_mint(&mint)?,
&ctx.previous_ids(&mint),
Some(keys::History {
direction: TransactionDirection::Out,
amount,
redeemed: Vec::new(),
sender: None,
}),
)
.await?;
Ok(Outcome::Sent { update, token })
}
Job::SendNutzap {
recipient,
amount,
@@ -330,14 +261,6 @@ pub(crate) enum Outcome {
update: TokenUpdate,
amount: u64,
},
Melted {
update: TokenUpdate,
amount: u64,
},
Sent {
update: TokenUpdate,
token: String,
},
NutzapSent {
update: TokenUpdate,
amount: u64,
-1
View File
@@ -52,7 +52,6 @@ impl WalletPanel {
WalletEvent::Error(error) => {
window.push_notification(Notification::error(error.clone()), cx);
}
WalletEvent::TokenCreated(_) => {}
},
));
-29
View File
@@ -1,6 +1,3 @@
use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use gpui::SharedString;
use nostr_sdk::prelude::*;
use state::Announcement;
@@ -20,32 +17,6 @@ pub struct Person {
messaging_relays: Vec<RelayUrl>,
}
impl PartialEq for Person {
fn eq(&self, other: &Self) -> bool {
self.public_key == other.public_key
}
}
impl Eq for Person {}
impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Person {
fn cmp(&self, other: &Self) -> Ordering {
self.name().cmp(&other.name())
}
}
impl Hash for Person {
fn hash<H: Hasher>(&self, state: &mut H) {
self.public_key.hash(state)
}
}
impl Person {
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
Self {
-6
View File
@@ -376,12 +376,6 @@ impl DownloadedFile {
LocalFile::Url(_) => Ok(()),
}
}
/// Save the file to the user's downloads (web)
#[cfg(target_arch = "wasm32")]
pub fn save(&self) -> Result<(), Error> {
crate::web_file::download(&self.name, &self.mime, &self.data)
}
}
/// Download and decrypt a file attachment to the user's device
+2 -9
View File
@@ -91,8 +91,6 @@ pub struct NostrRegistry {
signer: UniversalSigner,
/// Current user's public key
current_user: Option<PublicKey>,
/// Whether the stored credential needs a passphrase to unlock
passphrase_required: bool,
/// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>,
}
@@ -175,7 +173,6 @@ impl NostrRegistry {
client,
signer,
current_user: None,
passphrase_required: false,
tasks: vec![],
}
}
@@ -209,7 +206,6 @@ impl NostrRegistry {
this.update(cx, |this, cx| {
this.signer.swap_inner(new_signer);
this.current_user = Some(public_key);
this.passphrase_required = false;
cx.emit(StateEvent::SignerChanged);
cx.notify();
@@ -291,8 +287,7 @@ impl NostrRegistry {
this.set_signer(signer, cx);
})?;
} else if content.starts_with("ncryptsec1") {
this.update(cx, |this, cx| {
this.passphrase_required = true;
this.update(cx, |_this, cx| {
cx.emit(StateEvent::PassphraseRequired);
})?;
} else if content == "proxy" {
@@ -492,9 +487,8 @@ impl NostrRegistry {
/// Sign out and remove the stored credential
pub fn sign_out(&mut self, cx: &mut Context<Self>) {
self.current_user = None;
self.passphrase_required = false;
credentials::delete(cx).detach_and_log_err(cx);
cx.emit(StateEvent::SignerChanged);
cx.emit(StateEvent::NoSigner);
cx.notify();
@@ -519,7 +513,6 @@ impl NostrRegistry {
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
this.signer.swap_inner(signer);
this.current_user = Some(public_key);
this.passphrase_required = false;
cx.emit(StateEvent::SignerChanged);
cx.notify();
-3
View File
@@ -12,8 +12,5 @@ gpui.workspace = true
gpui-base.workspace = true
gpui-kit.workspace = true
nostr-sdk.workspace = true
web-time = "1.1"
smallvec.workspace = true
log.workspace = true
uuid = "1.10"
+41 -7
View File
@@ -1,3 +1,6 @@
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use gpui::{
App, Div, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
SharedString, StyleRefinement, Styled, Window, div, px,
@@ -20,6 +23,10 @@ const MIN_FILLED: usize = 5;
/// Seed for an avatar that has neither a picture nor a seed of its own.
const FALLBACK_SEED: &str = "coop";
/// Memoized seed-derived patterns; PixelAvatar is recreated on every frame.
static PATTERN_CACHE: LazyLock<Mutex<HashMap<String, AvatarPattern>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
/// An avatar for a user or other entity.
#[derive(IntoElement)]
pub struct Avatar {
@@ -93,7 +100,10 @@ impl RenderOnce for Avatar {
.child(AvatarImage::new(url).size_full().rounded_full())
.into_any_element(),
None => PixelAvatar {
seed: fnv1a(self.seed.unwrap_or_else(|| FALLBACK_SEED.into()).as_bytes()),
seed: self
.seed
.map(SharedString::from)
.unwrap_or_else(|| SharedString::new_static(FALLBACK_SEED)),
side,
style: StyleRefinement::default(),
}
@@ -114,7 +124,7 @@ impl RenderOnce for Avatar {
/// A deterministic, offline pixel-art avatar.
#[derive(IntoElement)]
struct PixelAvatar {
seed: u64,
seed: SharedString,
side: Pixels,
style: StyleRefinement,
}
@@ -128,11 +138,11 @@ impl Styled for PixelAvatar {
impl RenderOnce for PixelAvatar {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let pattern = pattern(self.seed);
let AvatarPattern { hash, grid } = cached_pattern(&self.seed);
let mut cells = Vec::new();
let main = Hsla {
h: self.seed as f32 / u64::MAX as f32,
h: hash as f32 / u64::MAX as f32,
s: 0.6,
l: if theme.is_dark() { 0.6 } else { 0.45 },
a: 1.,
@@ -152,7 +162,7 @@ impl RenderOnce for PixelAvatar {
for row in 0..GRID_SIZE {
for col in 0..GRID_SIZE {
let value = pattern[row * GRID_SIZE + col];
let value = grid[row * GRID_SIZE + col];
if value != 0 {
let color = if value == 2 { shade } else { main };
cells.push(
@@ -186,6 +196,32 @@ impl RenderOnce for PixelAvatar {
}
}
/// The seed-derived data a PixelAvatar renders, theme-dependent styling is applied per render.
#[derive(Clone, Copy)]
struct AvatarPattern {
hash: u64,
grid: [u8; GRID_SIZE * GRID_SIZE],
}
/// `seed`'s hash and pixel grid, computing and caching them on first use.
fn cached_pattern(seed: &str) -> AvatarPattern {
let mut cache = PATTERN_CACHE
.lock()
.unwrap_or_else(|error| error.into_inner());
if let Some(cached) = cache.get(seed) {
return *cached;
}
let hash = fnv1a(seed.as_bytes());
let value = AvatarPattern {
hash,
grid: pattern(hash),
};
cache.insert(seed.to_string(), value);
value
}
/// The rendered side length of an avatar at `size`.
fn side_length(size: Size) -> Pixels {
match size {
@@ -202,8 +238,6 @@ fn pattern(seed: u64) -> [u8; GRID_SIZE * GRID_SIZE] {
let mut pattern = [0u8; GRID_SIZE * GRID_SIZE];
let mut filled = 0usize;
// Only the inner rows and the inner left half are candidates; mirroring
// then keeps the art within the same inset, leaving the outer ring empty.
let art_rows = GRID_SIZE - 2 * MARGIN;
let art_columns = GRID_SIZE / 2 - MARGIN;
+9 -8
View File
@@ -25,7 +25,7 @@ type SharedClickListener = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
type DropdownBuilder = Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>;
/// Messages from the same author within this window are grouped into one run.
pub const RUN_WINDOW_SECS: u64 = 300;
pub(crate) const RUN_WINDOW_SECS: u64 = 300;
/// Whether a message starts a new run, given the previous message's author and unix time.
pub fn opens_run<T: PartialEq>(previous: Option<(T, u64)>, current: (T, u64)) -> bool {
@@ -758,14 +758,19 @@ impl RenderOnce for MessageActions {
/// A message's text revealed word by word, paced by the estimated speech duration.
#[derive(IntoElement)]
pub struct RevealText {
text: SharedString,
words: Vec<SharedString>,
duration: Duration,
}
impl RevealText {
/// Create text that reveals its words over `duration`.
pub fn new(text: impl Into<SharedString>, duration: Duration) -> Self {
Self {
text: text.into(),
words: text
.into()
.split_whitespace()
.map(SharedString::from)
.collect(),
duration,
}
}
@@ -773,11 +778,7 @@ impl RevealText {
impl RenderOnce for RevealText {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let words: Vec<SharedString> = self
.text
.split_whitespace()
.map(SharedString::from)
.collect();
let words = self.words;
let total = words.len().max(1) as f32;
div().flex().flex_wrap().gap_x_1().with_animation(
+2 -2
View File
@@ -8,7 +8,7 @@ use person::Directory;
/// A span of the source content, by byte range, that renders as `text` instead.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlineReplacement {
pub(crate) struct InlineReplacement {
pub range: Range<usize>,
pub text: SharedString,
/// When set, the span renders as this image (NIP-30 custom emoji)
@@ -44,7 +44,7 @@ pub struct RenderedText {
}
impl RenderedText {
pub fn new(content: &str, replacements: &[InlineReplacement]) -> Self {
pub(crate) fn new(content: &str, replacements: &[InlineReplacement]) -> Self {
let mut text = String::with_capacity(content.len());
let mut last = 0;
+5 -13
View File
@@ -52,7 +52,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
cx.new(|cx| Workspace::new(window, cx))
}
struct DeviceNotifcation;
struct DeviceNotification;
struct MsgRelayNotification;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
@@ -141,7 +141,7 @@ impl Workspace {
one shown there before approving.";
let note = Notification::new()
.id::<DeviceNotifcation>()
.id::<DeviceNotification>()
.autohide(false)
.title(format!("Wait for approval · {code}"))
.message(MSG)
@@ -155,7 +155,7 @@ impl Workspace {
from the settings.";
let note = Notification::new()
.id::<DeviceNotifcation>()
.id::<DeviceNotification>()
.message(MSG)
.with_type(NotificationType::Info)
.action(|_this, _window, _cx| {
@@ -165,7 +165,7 @@ impl Workspace {
device.update(cx, |this, cx| {
this.get_announcement(cx);
});
window.remove_notification::<DeviceNotifcation>(cx);
window.remove_notification::<DeviceNotification>(cx);
},
)
});
@@ -174,7 +174,7 @@ impl Workspace {
}
DeviceEvent::Set => {
let note = Notification::new()
.id::<DeviceNotifcation>()
.id::<DeviceNotification>()
.message("Encryption Key has been set")
.with_type(NotificationType::Success);
@@ -231,14 +231,6 @@ impl Workspace {
::nip60::WalletEvent::Error(error) => {
window.push_notification(Notification::error(error.clone()), cx);
}
::nip60::WalletEvent::TokenCreated(_) => {
window.push_notification(
Notification::new()
.message("Cashu token created, copy it from the wallet panel")
.with_type(NotificationType::Success),
cx,
);
}
::nip60::WalletEvent::Updated => {}
},
));
+4 -3
View File
@@ -235,13 +235,14 @@ impl ContactListPanel {
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
let mut items = Vec::new();
for (ix, contact) in self.contacts.iter().enumerate() {
for contact in &self.contacts {
let public_key = contact.public_key;
let hex = public_key.to_hex();
let profile = self.directory.read(cx).person(&public_key, cx);
items.push(
h_flex()
.id(ix)
.id(hex.clone())
.group("")
.flex_1()
.w_full()
@@ -262,7 +263,7 @@ impl ContactListPanel {
.child(profile.name()),
)
.child(
Button::new("remove_{ix}")
Button::new(format!("remove-{hex}"))
.icon(IconName::X)
.xsmall()
.ghost()
+8 -8
View File
@@ -213,17 +213,17 @@ impl GreeterPanel {
} else {
requests
.iter()
.enumerate()
.map(|(index, (room, preview))| {
.map(|(room, preview)| {
let room = room.clone();
let dock = dock.clone();
let room_id = room.read(cx).id;
let name = room.read(cx).display_name(cx);
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
h_flex()
.id(format!("request-{index}"))
.id(format!("request-{room_id}"))
.group("")
.w_full()
.h_9()
@@ -280,7 +280,7 @@ impl GreeterPanel {
),
)
.child(
Button::new(format!("response-{index}"))
Button::new(format!("response-{room_id}"))
.label("Response")
.xsmall()
.ghost()
@@ -332,17 +332,17 @@ impl GreeterPanel {
let rows: Vec<AnyElement> = self
.follow_ups
.iter()
.enumerate()
.map(|(index, (room, preview))| {
.map(|(room, preview)| {
let room = room.clone();
let dock = dock.clone();
let room_id = room.read(cx).id;
let name = room.read(cx).display_name(cx);
let seed = room.read(cx).display_image_seed(cx);
let created_at = room.read(cx).created_at.to_ago();
let preview = preview.clone();
h_flex()
.id(format!("follow-up-{index}"))
.id(format!("follow-up-{room_id}"))
.group("")
.w_full()
.h_9()
@@ -399,7 +399,7 @@ impl GreeterPanel {
),
)
.child(
Button::new(format!("reply-{index}"))
Button::new(format!("reply-{room_id}"))
.label("Reply")
.xsmall()
.ghost()
+57 -1
View File
@@ -34,11 +34,42 @@ 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)>,
}
/// 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();
@@ -175,6 +206,31 @@ 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),
};
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();
+29 -1
View File
@@ -1,4 +1,5 @@
use std::collections::HashSet;
use std::rc::Rc;
use anyhow::Error;
#[cfg(not(target_arch = "wasm32"))]
@@ -18,9 +19,11 @@ 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 smallvec::{SmallVec, smallvec};
@@ -90,6 +93,12 @@ pub struct Sidebar {
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>>,
@@ -118,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();
}
_ => {}
@@ -130,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();
}
@@ -145,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,
@@ -260,6 +285,9 @@ 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,
+15 -11
View File
@@ -105,10 +105,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;
@@ -181,7 +178,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| {
@@ -220,7 +217,7 @@ impl Sidebar {
});
div()
.id(index)
.id(id)
.w_full()
.child(nav)
.context_menu(move |menu, _window, _cx| {
@@ -251,7 +248,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| {
@@ -275,7 +272,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| {
@@ -308,7 +305,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| {
@@ -347,7 +344,7 @@ impl Sidebar {
});
div()
.id(index)
.id(tag.clone())
.w_full()
.child(nav)
.context_menu(move |menu, _window, _cx| {
@@ -383,8 +380,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()