Prune dead code and key elements by stable ids
This commit is contained in:
Generated
-8
@@ -381,7 +381,6 @@ dependencies = [
|
|||||||
name = "assets"
|
name = "assets"
|
||||||
version = "2.0.0-alpha3"
|
version = "2.0.0-alpha3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"gpui-kit",
|
"gpui-kit",
|
||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
@@ -1448,7 +1447,6 @@ dependencies = [
|
|||||||
"rand 0.10.3",
|
"rand 0.10.3",
|
||||||
"settings",
|
"settings",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
|
||||||
"state",
|
"state",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1821,7 +1819,6 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"state",
|
"state",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ui",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1849,7 +1846,6 @@ dependencies = [
|
|||||||
"settings",
|
"settings",
|
||||||
"state",
|
"state",
|
||||||
"tracing-wasm",
|
"tracing-wasm",
|
||||||
"ui",
|
|
||||||
"universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)",
|
"universal-time 0.3.1 (git+https://github.com/shadowylab/universal-time)",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
@@ -6052,7 +6048,6 @@ dependencies = [
|
|||||||
"gpui-pre",
|
"gpui-pre",
|
||||||
"log",
|
"log",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"person",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"state",
|
"state",
|
||||||
@@ -6119,7 +6114,6 @@ dependencies = [
|
|||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"state",
|
"state",
|
||||||
"tokio",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10348,8 +10342,6 @@ dependencies = [
|
|||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"person",
|
"person",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"uuid",
|
|
||||||
"web-time",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ publish.workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
rust-embed.workspace = true
|
rust-embed.workspace = true
|
||||||
gpui-kit.workspace = true
|
gpui-kit.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,3 @@ flume.workspace = true
|
|||||||
|
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy-matcher = "0.3.7"
|
fuzzy-matcher = "0.3.7"
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
||||||
smol.workspace = true
|
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ impl ChatPanel {
|
|||||||
})
|
})
|
||||||
.unwrap_or(("Unknown".into(), "Message...".into()));
|
.unwrap_or(("Unknown".into(), "Message...".into()));
|
||||||
|
|
||||||
// Define input state
|
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
TextareaState::new(window, cx)
|
TextareaState::new(window, cx)
|
||||||
.placeholder(format!("Message {}", name))
|
.placeholder(format!("Message {}", name))
|
||||||
@@ -153,7 +152,6 @@ impl ChatPanel {
|
|||||||
.clean_on_escape()
|
.clean_on_escape()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define dictation state
|
|
||||||
let speech = cx.new(|cx| {
|
let speech = cx.new(|cx| {
|
||||||
let state = SpeechState::new(cx)
|
let state = SpeechState::new(cx)
|
||||||
.recognizer(speech::recognizer())
|
.recognizer(speech::recognizer())
|
||||||
@@ -166,11 +164,9 @@ impl ChatPanel {
|
|||||||
state
|
state
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define subject input state
|
|
||||||
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
let subject_input = cx.new(|cx| InputState::new(window, cx).placeholder("New subject..."));
|
||||||
let subject_bar = cx.new(|_cx| false);
|
let subject_bar = cx.new(|_cx| false);
|
||||||
|
|
||||||
// Define subscriptions
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
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| {
|
cx.defer_in(window, |this, window, cx| {
|
||||||
this.connect(window, cx);
|
this.connect(window, cx);
|
||||||
this.subscribe_room_events(window, cx);
|
this.subscribe_room_events(window, cx);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ futures.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
state = { path = "../state" }
|
state = { path = "../state" }
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
person = { path = "../person" }
|
|
||||||
chat = { path = "../chat" }
|
chat = { path = "../chat" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
|||||||
@@ -1065,7 +1065,8 @@ impl GroupPanel {
|
|||||||
cx,
|
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
|
let admin = nostr
|
||||||
@@ -1077,7 +1078,6 @@ impl GroupPanel {
|
|||||||
let pinned = group.pins().pins().contains(&Pin::Event(message.id));
|
let pinned = group.pins().pins().contains(&Pin::Event(message.id));
|
||||||
|
|
||||||
message::render(
|
message::render(
|
||||||
ix,
|
|
||||||
message,
|
message,
|
||||||
content,
|
content,
|
||||||
show_author,
|
show_author,
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ pub(crate) struct Actions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render(
|
pub(crate) fn render(
|
||||||
ix: usize,
|
|
||||||
message: &Message,
|
message: &Message,
|
||||||
content: AnyElement,
|
content: AnyElement,
|
||||||
show_author: bool,
|
show_author: bool,
|
||||||
@@ -60,7 +59,8 @@ pub(crate) fn render(
|
|||||||
nostr.read(cx).current_user(),
|
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)
|
.show_author(show_author)
|
||||||
.hide_avatar(hide_avatar)
|
.hide_avatar(hide_avatar)
|
||||||
.avatar(
|
.avatar(
|
||||||
@@ -337,7 +337,7 @@ fn replies(
|
|||||||
|
|
||||||
let mut items = Vec::with_capacity(targets.len());
|
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 context = entity.read_with(cx, |group, cx| {
|
||||||
let message = group.message(*id)?;
|
let message = group.message(*id)?;
|
||||||
let author = persons.read(cx).person(&message.author, cx);
|
let author = persons.read(cx).person(&message.author, cx);
|
||||||
@@ -356,7 +356,7 @@ fn replies(
|
|||||||
let panel = panel.clone();
|
let panel = panel.clone();
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
ReplyPreview::new(ix, author, preview)
|
ReplyPreview::new(SharedString::from(format!("reply-{id}")), author, preview)
|
||||||
.on_click(move |_event, _window, cx| {
|
.on_click(move |_event, _window, cx| {
|
||||||
if let Err(error) = panel.update(cx, |panel, cx| {
|
if let Err(error) = panel.update(cx, |panel, cx| {
|
||||||
panel.scroll_to(&id, cx);
|
panel.scroll_to(&id, cx);
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ serde_json.workspace = true
|
|||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
hkdf.workspace = true
|
hkdf.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
tokio = { version = "1", default-features = false, features = [ "rt" ] }
|
|
||||||
|
|
||||||
cdk.workspace = true
|
cdk.workspace = true
|
||||||
cdk-redb.workspace = true
|
cdk-redb.workspace = true
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ impl Global for GlobalWalletRegistry {}
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum WalletEvent {
|
pub enum WalletEvent {
|
||||||
Updated,
|
Updated,
|
||||||
TokenCreated(String),
|
|
||||||
Error(String),
|
Error(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,36 +202,6 @@ impl WalletRegistry {
|
|||||||
self.emit_updated(cx);
|
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.
|
/// Send a NIP-61 nutzap to a peer, optionally nutzapping one of their events.
|
||||||
pub fn send_nutzap(
|
pub fn send_nutzap(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -947,16 +916,6 @@ impl WalletRegistry {
|
|||||||
self.emit_updated(cx);
|
self.emit_updated(cx);
|
||||||
log::info!("minted {amount} sats from quote {quote_id}");
|
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 } => {
|
ops::Outcome::NutzapSent { update, amount } => {
|
||||||
self.apply_token_update(update);
|
self.apply_token_update(update);
|
||||||
self.emit_updated(cx);
|
self.emit_updated(cx);
|
||||||
|
|||||||
+2
-79
@@ -8,8 +8,8 @@ use cdk::amount::SplitTarget;
|
|||||||
use cdk::mint_url::MintUrl;
|
use cdk::mint_url::MintUrl;
|
||||||
use cdk::nuts::nut00::KnownMethod;
|
use cdk::nuts::nut00::KnownMethod;
|
||||||
use cdk::nuts::{
|
use cdk::nuts::{
|
||||||
CurrencyUnit, Id, KeySetInfo, MeltQuoteState, MintQuoteState, PaymentMethod, Proof, Proofs,
|
CurrencyUnit, Id, KeySetInfo, MintQuoteState, PaymentMethod, Proof, Proofs, SecretKey,
|
||||||
SecretKey, SpendingConditions, State,
|
SpendingConditions, State,
|
||||||
};
|
};
|
||||||
use cdk::secret::Secret;
|
use cdk::secret::Secret;
|
||||||
use cdk::wallet::types::ProofInfo;
|
use cdk::wallet::types::ProofInfo;
|
||||||
@@ -35,14 +35,6 @@ pub(crate) enum Job {
|
|||||||
mint: MintUrl,
|
mint: MintUrl,
|
||||||
quote_id: String,
|
quote_id: String,
|
||||||
},
|
},
|
||||||
Melt {
|
|
||||||
mint: MintUrl,
|
|
||||||
invoice: String,
|
|
||||||
},
|
|
||||||
SendToken {
|
|
||||||
mint: MintUrl,
|
|
||||||
amount: u64,
|
|
||||||
},
|
|
||||||
SendNutzap {
|
SendNutzap {
|
||||||
recipient: PublicKey,
|
recipient: PublicKey,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
@@ -117,67 +109,6 @@ impl Job {
|
|||||||
amount,
|
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("e.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 {
|
Job::SendNutzap {
|
||||||
recipient,
|
recipient,
|
||||||
amount,
|
amount,
|
||||||
@@ -330,14 +261,6 @@ pub(crate) enum Outcome {
|
|||||||
update: TokenUpdate,
|
update: TokenUpdate,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
},
|
},
|
||||||
Melted {
|
|
||||||
update: TokenUpdate,
|
|
||||||
amount: u64,
|
|
||||||
},
|
|
||||||
Sent {
|
|
||||||
update: TokenUpdate,
|
|
||||||
token: String,
|
|
||||||
},
|
|
||||||
NutzapSent {
|
NutzapSent {
|
||||||
update: TokenUpdate,
|
update: TokenUpdate,
|
||||||
amount: u64,
|
amount: u64,
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ impl WalletPanel {
|
|||||||
WalletEvent::Error(error) => {
|
WalletEvent::Error(error) => {
|
||||||
window.push_notification(Notification::error(error.clone()), cx);
|
window.push_notification(Notification::error(error.clone()), cx);
|
||||||
}
|
}
|
||||||
WalletEvent::TokenCreated(_) => {}
|
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
use std::hash::{Hash, Hasher};
|
|
||||||
|
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::Announcement;
|
use state::Announcement;
|
||||||
@@ -20,32 +17,6 @@ pub struct Person {
|
|||||||
messaging_relays: Vec<RelayUrl>,
|
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 {
|
impl Person {
|
||||||
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
|
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|||||||
@@ -376,12 +376,6 @@ impl DownloadedFile {
|
|||||||
LocalFile::Url(_) => Ok(()),
|
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
|
/// Download and decrypt a file attachment to the user's device
|
||||||
|
|||||||
@@ -91,8 +91,6 @@ pub struct NostrRegistry {
|
|||||||
signer: UniversalSigner,
|
signer: UniversalSigner,
|
||||||
/// Current user's public key
|
/// Current user's public key
|
||||||
current_user: Option<PublicKey>,
|
current_user: Option<PublicKey>,
|
||||||
/// Whether the stored credential needs a passphrase to unlock
|
|
||||||
passphrase_required: bool,
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
}
|
}
|
||||||
@@ -175,7 +173,6 @@ impl NostrRegistry {
|
|||||||
client,
|
client,
|
||||||
signer,
|
signer,
|
||||||
current_user: None,
|
current_user: None,
|
||||||
passphrase_required: false,
|
|
||||||
tasks: vec![],
|
tasks: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +206,6 @@ impl NostrRegistry {
|
|||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.signer.swap_inner(new_signer);
|
this.signer.swap_inner(new_signer);
|
||||||
this.current_user = Some(public_key);
|
this.current_user = Some(public_key);
|
||||||
this.passphrase_required = false;
|
|
||||||
|
|
||||||
cx.emit(StateEvent::SignerChanged);
|
cx.emit(StateEvent::SignerChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -291,8 +287,7 @@ impl NostrRegistry {
|
|||||||
this.set_signer(signer, cx);
|
this.set_signer(signer, cx);
|
||||||
})?;
|
})?;
|
||||||
} else if content.starts_with("ncryptsec1") {
|
} else if content.starts_with("ncryptsec1") {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |_this, cx| {
|
||||||
this.passphrase_required = true;
|
|
||||||
cx.emit(StateEvent::PassphraseRequired);
|
cx.emit(StateEvent::PassphraseRequired);
|
||||||
})?;
|
})?;
|
||||||
} else if content == "proxy" {
|
} else if content == "proxy" {
|
||||||
@@ -492,9 +487,8 @@ impl NostrRegistry {
|
|||||||
/// Sign out and remove the stored credential
|
/// Sign out and remove the stored credential
|
||||||
pub fn sign_out(&mut self, cx: &mut Context<Self>) {
|
pub fn sign_out(&mut self, cx: &mut Context<Self>) {
|
||||||
self.current_user = None;
|
self.current_user = None;
|
||||||
self.passphrase_required = false;
|
|
||||||
|
|
||||||
credentials::delete(cx).detach_and_log_err(cx);
|
credentials::delete(cx).detach_and_log_err(cx);
|
||||||
|
|
||||||
cx.emit(StateEvent::SignerChanged);
|
cx.emit(StateEvent::SignerChanged);
|
||||||
cx.emit(StateEvent::NoSigner);
|
cx.emit(StateEvent::NoSigner);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -519,7 +513,6 @@ impl NostrRegistry {
|
|||||||
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
|
credentials::write(cx, "extension", b"extension").detach_and_log_err(cx);
|
||||||
this.signer.swap_inner(signer);
|
this.signer.swap_inner(signer);
|
||||||
this.current_user = Some(public_key);
|
this.current_user = Some(public_key);
|
||||||
this.passphrase_required = false;
|
|
||||||
|
|
||||||
cx.emit(StateEvent::SignerChanged);
|
cx.emit(StateEvent::SignerChanged);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
@@ -12,8 +12,5 @@ gpui.workspace = true
|
|||||||
gpui-base.workspace = true
|
gpui-base.workspace = true
|
||||||
gpui-kit.workspace = true
|
gpui-kit.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
web-time = "1.1"
|
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
||||||
uuid = "1.10"
|
|
||||||
|
|||||||
+41
-7
@@ -1,3 +1,6 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{LazyLock, Mutex};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, Div, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
|
App, Div, Hsla, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
|
||||||
SharedString, StyleRefinement, Styled, Window, div, px,
|
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.
|
/// Seed for an avatar that has neither a picture nor a seed of its own.
|
||||||
const FALLBACK_SEED: &str = "coop";
|
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.
|
/// An avatar for a user or other entity.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Avatar {
|
pub struct Avatar {
|
||||||
@@ -93,7 +100,10 @@ impl RenderOnce for Avatar {
|
|||||||
.child(AvatarImage::new(url).size_full().rounded_full())
|
.child(AvatarImage::new(url).size_full().rounded_full())
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
None => PixelAvatar {
|
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,
|
side,
|
||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
}
|
}
|
||||||
@@ -114,7 +124,7 @@ impl RenderOnce for Avatar {
|
|||||||
/// A deterministic, offline pixel-art avatar.
|
/// A deterministic, offline pixel-art avatar.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
struct PixelAvatar {
|
struct PixelAvatar {
|
||||||
seed: u64,
|
seed: SharedString,
|
||||||
side: Pixels,
|
side: Pixels,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
}
|
}
|
||||||
@@ -128,11 +138,11 @@ impl Styled for PixelAvatar {
|
|||||||
impl RenderOnce for PixelAvatar {
|
impl RenderOnce for PixelAvatar {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let pattern = pattern(self.seed);
|
let AvatarPattern { hash, grid } = cached_pattern(&self.seed);
|
||||||
let mut cells = Vec::new();
|
let mut cells = Vec::new();
|
||||||
|
|
||||||
let main = Hsla {
|
let main = Hsla {
|
||||||
h: self.seed as f32 / u64::MAX as f32,
|
h: hash as f32 / u64::MAX as f32,
|
||||||
s: 0.6,
|
s: 0.6,
|
||||||
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
l: if theme.is_dark() { 0.6 } else { 0.45 },
|
||||||
a: 1.,
|
a: 1.,
|
||||||
@@ -152,7 +162,7 @@ impl RenderOnce for PixelAvatar {
|
|||||||
|
|
||||||
for row in 0..GRID_SIZE {
|
for row in 0..GRID_SIZE {
|
||||||
for col 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 {
|
if value != 0 {
|
||||||
let color = if value == 2 { shade } else { main };
|
let color = if value == 2 { shade } else { main };
|
||||||
cells.push(
|
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`.
|
/// The rendered side length of an avatar at `size`.
|
||||||
fn side_length(size: Size) -> Pixels {
|
fn side_length(size: Size) -> Pixels {
|
||||||
match size {
|
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 pattern = [0u8; GRID_SIZE * GRID_SIZE];
|
||||||
let mut filled = 0usize;
|
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_rows = GRID_SIZE - 2 * MARGIN;
|
||||||
let art_columns = GRID_SIZE / 2 - MARGIN;
|
let art_columns = GRID_SIZE / 2 - MARGIN;
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
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.
|
/// 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.
|
/// 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 {
|
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.
|
/// A message's text revealed word by word, paced by the estimated speech duration.
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct RevealText {
|
pub struct RevealText {
|
||||||
text: SharedString,
|
words: Vec<SharedString>,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RevealText {
|
impl RevealText {
|
||||||
|
/// Create text that reveals its words over `duration`.
|
||||||
pub fn new(text: impl Into<SharedString>, duration: Duration) -> Self {
|
pub fn new(text: impl Into<SharedString>, duration: Duration) -> Self {
|
||||||
Self {
|
Self {
|
||||||
text: text.into(),
|
words: text
|
||||||
|
.into()
|
||||||
|
.split_whitespace()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.collect(),
|
||||||
duration,
|
duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -773,11 +778,7 @@ impl RevealText {
|
|||||||
|
|
||||||
impl RenderOnce for RevealText {
|
impl RenderOnce for RevealText {
|
||||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
let words: Vec<SharedString> = self
|
let words = self.words;
|
||||||
.text
|
|
||||||
.split_whitespace()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.collect();
|
|
||||||
let total = words.len().max(1) as f32;
|
let total = words.len().max(1) as f32;
|
||||||
|
|
||||||
div().flex().flex_wrap().gap_x_1().with_animation(
|
div().flex().flex_wrap().gap_x_1().with_animation(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use person::Directory;
|
|||||||
|
|
||||||
/// A span of the source content, by byte range, that renders as `text` instead.
|
/// A span of the source content, by byte range, that renders as `text` instead.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct InlineReplacement {
|
pub(crate) struct InlineReplacement {
|
||||||
pub range: Range<usize>,
|
pub range: Range<usize>,
|
||||||
pub text: SharedString,
|
pub text: SharedString,
|
||||||
/// When set, the span renders as this image (NIP-30 custom emoji)
|
/// When set, the span renders as this image (NIP-30 custom emoji)
|
||||||
@@ -44,7 +44,7 @@ pub struct RenderedText {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 text = String::with_capacity(content.len());
|
||||||
let mut last = 0;
|
let mut last = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
|
|||||||
cx.new(|cx| Workspace::new(window, cx))
|
cx.new(|cx| Workspace::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DeviceNotifcation;
|
struct DeviceNotification;
|
||||||
struct MsgRelayNotification;
|
struct MsgRelayNotification;
|
||||||
|
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
@@ -141,7 +141,7 @@ impl Workspace {
|
|||||||
one shown there before approving.";
|
one shown there before approving.";
|
||||||
|
|
||||||
let note = Notification::new()
|
let note = Notification::new()
|
||||||
.id::<DeviceNotifcation>()
|
.id::<DeviceNotification>()
|
||||||
.autohide(false)
|
.autohide(false)
|
||||||
.title(format!("Wait for approval · {code}"))
|
.title(format!("Wait for approval · {code}"))
|
||||||
.message(MSG)
|
.message(MSG)
|
||||||
@@ -155,7 +155,7 @@ impl Workspace {
|
|||||||
from the settings.";
|
from the settings.";
|
||||||
|
|
||||||
let note = Notification::new()
|
let note = Notification::new()
|
||||||
.id::<DeviceNotifcation>()
|
.id::<DeviceNotification>()
|
||||||
.message(MSG)
|
.message(MSG)
|
||||||
.with_type(NotificationType::Info)
|
.with_type(NotificationType::Info)
|
||||||
.action(|_this, _window, _cx| {
|
.action(|_this, _window, _cx| {
|
||||||
@@ -165,7 +165,7 @@ impl Workspace {
|
|||||||
device.update(cx, |this, cx| {
|
device.update(cx, |this, cx| {
|
||||||
this.get_announcement(cx);
|
this.get_announcement(cx);
|
||||||
});
|
});
|
||||||
window.remove_notification::<DeviceNotifcation>(cx);
|
window.remove_notification::<DeviceNotification>(cx);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@@ -174,7 +174,7 @@ impl Workspace {
|
|||||||
}
|
}
|
||||||
DeviceEvent::Set => {
|
DeviceEvent::Set => {
|
||||||
let note = Notification::new()
|
let note = Notification::new()
|
||||||
.id::<DeviceNotifcation>()
|
.id::<DeviceNotification>()
|
||||||
.message("Encryption Key has been set")
|
.message("Encryption Key has been set")
|
||||||
.with_type(NotificationType::Success);
|
.with_type(NotificationType::Success);
|
||||||
|
|
||||||
@@ -231,14 +231,6 @@ impl Workspace {
|
|||||||
::nip60::WalletEvent::Error(error) => {
|
::nip60::WalletEvent::Error(error) => {
|
||||||
window.push_notification(Notification::error(error.clone()), cx);
|
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 => {}
|
::nip60::WalletEvent::Updated => {}
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -235,13 +235,14 @@ impl ContactListPanel {
|
|||||||
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
fn render_list_items(&mut self, cx: &mut Context<Self>) -> Vec<impl IntoElement> {
|
||||||
let mut items = Vec::new();
|
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 public_key = contact.public_key;
|
||||||
|
let hex = public_key.to_hex();
|
||||||
let profile = self.directory.read(cx).person(&public_key, cx);
|
let profile = self.directory.read(cx).person(&public_key, cx);
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(ix)
|
.id(hex.clone())
|
||||||
.group("")
|
.group("")
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -262,7 +263,7 @@ impl ContactListPanel {
|
|||||||
.child(profile.name()),
|
.child(profile.name()),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("remove_{ix}")
|
Button::new(format!("remove-{hex}"))
|
||||||
.icon(IconName::X)
|
.icon(IconName::X)
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
|
|||||||
@@ -213,17 +213,17 @@ impl GreeterPanel {
|
|||||||
} else {
|
} else {
|
||||||
requests
|
requests
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|(room, preview)| {
|
||||||
.map(|(index, (room, preview))| {
|
|
||||||
let room = room.clone();
|
let room = room.clone();
|
||||||
let dock = dock.clone();
|
let dock = dock.clone();
|
||||||
|
let room_id = room.read(cx).id;
|
||||||
let name = room.read(cx).display_name(cx);
|
let name = room.read(cx).display_name(cx);
|
||||||
let seed = room.read(cx).display_image_seed(cx);
|
let seed = room.read(cx).display_image_seed(cx);
|
||||||
let created_at = room.read(cx).created_at.to_ago();
|
let created_at = room.read(cx).created_at.to_ago();
|
||||||
let preview = preview.clone();
|
let preview = preview.clone();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(format!("request-{index}"))
|
.id(format!("request-{room_id}"))
|
||||||
.group("")
|
.group("")
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_9()
|
.h_9()
|
||||||
@@ -280,7 +280,7 @@ impl GreeterPanel {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(format!("response-{index}"))
|
Button::new(format!("response-{room_id}"))
|
||||||
.label("Response")
|
.label("Response")
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
@@ -332,17 +332,17 @@ impl GreeterPanel {
|
|||||||
let rows: Vec<AnyElement> = self
|
let rows: Vec<AnyElement> = self
|
||||||
.follow_ups
|
.follow_ups
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.map(|(room, preview)| {
|
||||||
.map(|(index, (room, preview))| {
|
|
||||||
let room = room.clone();
|
let room = room.clone();
|
||||||
let dock = dock.clone();
|
let dock = dock.clone();
|
||||||
|
let room_id = room.read(cx).id;
|
||||||
let name = room.read(cx).display_name(cx);
|
let name = room.read(cx).display_name(cx);
|
||||||
let seed = room.read(cx).display_image_seed(cx);
|
let seed = room.read(cx).display_image_seed(cx);
|
||||||
let created_at = room.read(cx).created_at.to_ago();
|
let created_at = room.read(cx).created_at.to_ago();
|
||||||
let preview = preview.clone();
|
let preview = preview.clone();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(format!("follow-up-{index}"))
|
.id(format!("follow-up-{room_id}"))
|
||||||
.group("")
|
.group("")
|
||||||
.w_full()
|
.w_full()
|
||||||
.h_9()
|
.h_9()
|
||||||
@@ -399,7 +399,7 @@ impl GreeterPanel {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(format!("reply-{index}"))
|
Button::new(format!("reply-{room_id}"))
|
||||||
.label("Reply")
|
.label("Reply")
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.ghost()
|
.ghost()
|
||||||
|
|||||||
@@ -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 {
|
impl Sidebar {
|
||||||
pub(super) fn render_list(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
pub(super) fn render_list(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let requests_open = self.requests_open;
|
let requests_open = self.requests_open;
|
||||||
let contacts_open = self.contacts_open && !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
|
// Whether to show the create button for the contact picker
|
||||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||||
|
|
||||||
@@ -175,6 +206,31 @@ impl Sidebar {
|
|||||||
.into_any_element()
|
.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> {
|
fn rows_for(&self, cx: &App) -> Vec<SidebarRow> {
|
||||||
if self.contacts_open {
|
if self.contacts_open {
|
||||||
return self.contacts_rows();
|
return self.contacts_rows();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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::progress::ProgressCircle;
|
||||||
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
|
use gpui_kit::component::{ActiveTheme, WindowExt, h_flex, v_flex};
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
|
use list::RowsSignature;
|
||||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use person::{Directory, PersonRegistry};
|
use person::{Directory, PersonRegistry};
|
||||||
|
use rows::SidebarRow;
|
||||||
pub(crate) use rows::open_room;
|
pub(crate) use rows::open_room;
|
||||||
use search::SearchResult;
|
use search::SearchResult;
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
@@ -90,6 +93,12 @@ pub struct Sidebar {
|
|||||||
chat_progress: f32,
|
chat_progress: f32,
|
||||||
/// Simulated web of trust sync progress
|
/// Simulated web of trust sync progress
|
||||||
sync_progress: f32,
|
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
|
/// The updater facts the header renders, mirrored so updater churn cannot invalidate the cached sidebar
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
updater_display: Entity<Option<UpdateDisplay>>,
|
updater_display: Entity<Option<UpdateDisplay>>,
|
||||||
@@ -118,10 +127,12 @@ impl Sidebar {
|
|||||||
match event {
|
match event {
|
||||||
ChatEvent::Ping => {
|
ChatEvent::Ping => {
|
||||||
this.new_requests = true;
|
this.new_requests = true;
|
||||||
|
this.rows_dirty = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
ChatEvent::Loaded => {
|
ChatEvent::Loaded => {
|
||||||
this.chat_progress = 0.0;
|
this.chat_progress = 0.0;
|
||||||
|
this.rows_dirty = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -130,9 +141,10 @@ impl Sidebar {
|
|||||||
);
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
cx.subscribe_in(&groups, window, |_this, _, event, window, cx| {
|
cx.subscribe_in(&groups, window, |this, _, event, window, cx| {
|
||||||
match event {
|
match event {
|
||||||
GroupsEvent::Updated => {
|
GroupsEvent::Updated => {
|
||||||
|
this.rows_dirty = true;
|
||||||
// TODO: we must not trigger re-render directly like this
|
// TODO: we must not trigger re-render directly like this
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@@ -145,11 +157,24 @@ impl Sidebar {
|
|||||||
} => {
|
} => {
|
||||||
window.push_notification(Notification::warning(warning.clone()), cx);
|
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(
|
subscriptions.push(
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&nostr,
|
&nostr,
|
||||||
@@ -260,6 +285,9 @@ impl Sidebar {
|
|||||||
global_task: None,
|
global_task: None,
|
||||||
chat_progress: 0.0,
|
chat_progress: 0.0,
|
||||||
sync_progress: 0.0,
|
sync_progress: 0.0,
|
||||||
|
rows: Rc::default(),
|
||||||
|
rows_signature: RowsSignature::default(),
|
||||||
|
rows_dirty: true,
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
updater_display,
|
updater_display,
|
||||||
tasks,
|
tasks,
|
||||||
|
|||||||
@@ -105,10 +105,7 @@ impl Sidebar {
|
|||||||
rows.get(range.clone())
|
rows.get(range.clone())
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.enumerate()
|
.map(|row| {
|
||||||
.map(|(offset, row)| {
|
|
||||||
let index = range.start + offset;
|
|
||||||
|
|
||||||
let element = match row {
|
let element = match row {
|
||||||
SidebarRow::Section(section) => {
|
SidebarRow::Section(section) => {
|
||||||
let section = *section;
|
let section = *section;
|
||||||
@@ -181,7 +178,7 @@ impl Sidebar {
|
|||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let room = room.clone();
|
let room = room.clone();
|
||||||
|
|
||||||
let nav = Nav::new(index)
|
let nav = Nav::new(id)
|
||||||
.label(name)
|
.label(name)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
@@ -220,7 +217,7 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(index)
|
.id(id)
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(nav)
|
.child(nav)
|
||||||
.context_menu(move |menu, _window, _cx| {
|
.context_menu(move |menu, _window, _cx| {
|
||||||
@@ -251,7 +248,7 @@ impl Sidebar {
|
|||||||
let sidebar = cx.entity().downgrade();
|
let sidebar = cx.entity().downgrade();
|
||||||
let public_key = public_key.to_owned();
|
let public_key = public_key.to_owned();
|
||||||
|
|
||||||
Nav::new(index)
|
Nav::new(public_key.to_hex())
|
||||||
.label(person.name())
|
.label(person.name())
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
@@ -275,7 +272,7 @@ impl Sidebar {
|
|||||||
let person = self.directory.read(cx).person(public_key, cx);
|
let person = self.directory.read(cx).person(public_key, cx);
|
||||||
let public_key = public_key.to_owned();
|
let public_key = public_key.to_owned();
|
||||||
|
|
||||||
Nav::new(index)
|
Nav::new(public_key.to_hex())
|
||||||
.label(person.name())
|
.label(person.name())
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
@@ -308,7 +305,7 @@ impl Sidebar {
|
|||||||
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
||||||
.map(SharedString::from);
|
.map(SharedString::from);
|
||||||
|
|
||||||
let nav = Nav::new(index)
|
let nav = Nav::new(tag.clone())
|
||||||
.label(name)
|
.label(name)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
@@ -347,7 +344,7 @@ impl Sidebar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(index)
|
.id(tag.clone())
|
||||||
.w_full()
|
.w_full()
|
||||||
.child(nav)
|
.child(nav)
|
||||||
.context_menu(move |menu, _window, _cx| {
|
.context_menu(move |menu, _window, _cx| {
|
||||||
@@ -383,8 +380,15 @@ impl Sidebar {
|
|||||||
let dock = self.dock.clone();
|
let dock = self.dock.clone();
|
||||||
let room = hit.room.clone();
|
let room = hit.room.clone();
|
||||||
let content = hit.content.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)
|
.label(content)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_medium()
|
.font_medium()
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ info_plist_path = "resources/Info.plist"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
workspace = { path = "../crates/workspace" }
|
workspace = { path = "../crates/workspace" }
|
||||||
ui = { path = "../crates/ui" }
|
|
||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
nip4e = { path = "../crates/nip4e" }
|
nip4e = { path = "../crates/nip4e" }
|
||||||
nip60 = { path = "../crates/nip60" }
|
nip60 = { path = "../crates/nip60" }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Date: 2026-11-12 · Revision audited: `8fa79aef` (master) · Scope: entire works
|
|||||||
> **Remediation status (2026-11-12, later the same day):** all ten P0 items from §8 have been
|
> **Remediation status (2026-11-12, later the same day):** all ten P0 items from §8 have been
|
||||||
> implemented and validated (workspace clippy clean, `cargo fmt --check` clean, 35 tests passing).
|
> implemented and validated (workspace clippy clean, `cargo fmt --check` clean, 35 tests passing).
|
||||||
> See §9 for the per-fix log. The P1 list (committed as `fd802a88` base) has also been processed —
|
> See §9 for the per-fix log. The P1 list (committed as `fd802a88` base) has also been processed —
|
||||||
> see §10.
|
> see §10. P2 hygiene is done too (S1/S2 excluded by request) — see §11.
|
||||||
|
|
||||||
## 1. Scope and Method
|
## 1. Scope and Method
|
||||||
|
|
||||||
@@ -994,3 +994,66 @@ clean, `cargo test --workspace` 38 passed / 0 failed. Diff: 25 files, +1072/−2
|
|||||||
the field; NIP-05 shows "could not be verified" on network failure instead of "does not
|
the field; NIP-05 shows "could not be verified" on network failure instead of "does not
|
||||||
match"; undecryptable messages and room load/connect failures produce visible notifications.
|
match"; undecryptable messages and room load/connect failures produce visible notifications.
|
||||||
- First message-send/receive after the room-id change republishes `r` tags under the new ids.
|
- First message-send/receive after the room-id change republishes `r` tags under the new ids.
|
||||||
|
|
||||||
|
## 11. P2 Remediation Log (2026-11-12)
|
||||||
|
|
||||||
|
P2 hygiene processed with S1 (task-handle containers) and S2 (silent error discards) excluded by
|
||||||
|
request. Validation after the full batch: `cargo clippy --workspace --all-targets` zero warnings,
|
||||||
|
`cargo fmt --all --check` clean, `cargo test --workspace` 38 passed / 0 failed.
|
||||||
|
|
||||||
|
1. **Domain-keyed element ids (S6)** — sidebar rows use `Room.id` / group `cache_tag` / pubkey
|
||||||
|
hex / compound message-hit digests instead of list indexes (`rows.rs`, `nav.rs` untouched);
|
||||||
|
contact-list rows and remove buttons key on pubkey hex; greeter sections key on
|
||||||
|
`request-/response-/follow-up-/reply-{room_id}`; nip29_ui content `TextView`, `MessageRow`,
|
||||||
|
and `ReplyPreview` key on event-id hex with a `reply-` namespace (traced into gpui-base:
|
||||||
|
`TextViewState`'s key is the whole element-id stack, so the row container id had to change
|
||||||
|
too — this also kills the prepend-triggered markdown re-parse jank). Uniqueness arguments per
|
||||||
|
list are in the work log; `chat_ui`'s identical index-keyed row pattern remains as a noted
|
||||||
|
follow-up (audit S3).
|
||||||
|
2. **Unused dependencies removed** (each grep-verified before removal): `smol` (chat), `person`
|
||||||
|
(nip29), `tokio` (nip60 — `gpui_tokio` keeps a superset), `ui` (desktop + web — plain path
|
||||||
|
deps, no feature unification needed; ui stays in the graph via chat_ui/nip29_ui/nip4e/
|
||||||
|
nip60_ui/workspace), `anyhow` (assets), `web-time` + `uuid` (ui). Cargo.lock rewritten by
|
||||||
|
cargo only.
|
||||||
|
3. **Dead code removed** (each grep-verified): `state::passphrase_required` field + writes (UI
|
||||||
|
uses `StateEvent::PassphraseRequired`); wasm-only `DownloadedFile::save`; `Person`'s unused
|
||||||
|
`PartialEq/Eq/PartialOrd/Ord/Hash` impls (+ their imports); nip60's unreachable
|
||||||
|
`melt()`/`send_token()` API with `Job::Melt`/`Job::SendToken`, `Outcome::Melted`/`Sent`,
|
||||||
|
`WalletEvent::TokenCreated`, its emission, and workspace's misleading "copy it from the wallet
|
||||||
|
panel" notification arm (plus the matching no-op arm in nip60_ui's exhaustive match).
|
||||||
|
`dock::find_panel` dead machinery and `skin.rs`'s write-once flag were already removed in P1.
|
||||||
|
4. **Avatar memoization** (`ui/src/avatar.rs`): `AvatarPattern { hash, grid: [u8; 64] }` cached
|
||||||
|
per seed in a `LazyLock<Mutex<HashMap<String, AvatarPattern>>>`; repeat renders skip FNV-1a +
|
||||||
|
xorshift and only rebuild the divs; theme colors stay computed at paint time so output is
|
||||||
|
bit-identical. The per-frame fallback-seed `String` is gone (`SharedString::new_static`).
|
||||||
|
5. **Sidebar row-model caching** (`sidebar/mod.rs`, `list.rs`): rows cached as
|
||||||
|
`Rc<Vec<SidebarRow>>` behind two invalidation inputs — a `rows_dirty` flag set by chat/groups
|
||||||
|
events and two new entity observes (catching `track_room`/`sort`/`reset`, which notify the
|
||||||
|
registries without emitting events), and a cheap `RowsSignature` snapshot (catching room-kind
|
||||||
|
promotion Request→Ongoing, which notifies only the Room entity, and pin/unpin, which mutates
|
||||||
|
`AppSettings`' inner entity and notifies nothing the sidebar observes — both verified real
|
||||||
|
hazards a flag-only design would have missed). Progress ticks, typing, and hover are verified
|
||||||
|
cache-neutral; `find_input` verified as not a `rows_for` input. `group_view` member rendering
|
||||||
|
left uncached deliberately: `GroupEvent::Updated` is over-emitted and not provably complete,
|
||||||
|
so no safe cache exists without a membership-version counter.
|
||||||
|
6. **Rename** `DeviceNotifcation` → `DeviceNotification` (workspace/src/lib.rs, 5 sites). Note:
|
||||||
|
nip4e defines its own correctly-spelled marker type, so the two crates' notification ids were
|
||||||
|
already distinct; unifying them (so removals cross-dismiss) is a deliberate behavior change
|
||||||
|
left for later.
|
||||||
|
7. **ui API tightening + RevealText**: `InlineReplacement`, `RenderedText::new`, and
|
||||||
|
`RUN_WINDOW_SECS` → `pub(crate)` (verified unused externally); `RevealText::new` doc added;
|
||||||
|
word-split moved from every animation frame into `new`.
|
||||||
|
8. **Summary-comment cleanup**: the five cited chat_ui "Define …" comments removed; all
|
||||||
|
why-comments (including the O(1)-lookup one) kept. Remaining candidates flagged
|
||||||
|
(`// Get room id and name`, `// Define attachments and replies_to entities`) left untouched
|
||||||
|
per scope.
|
||||||
|
|
||||||
|
### P2 notes
|
||||||
|
|
||||||
|
- nip60's send/melt removal means there is currently no UI to melt to a lightning invoice or
|
||||||
|
export a token string; both were unreachable before. Re-add together with their UI in a future
|
||||||
|
feature change.
|
||||||
|
- Element ids: two known residuals documented (inbox.rs contact p-tag dedupe;
|
||||||
|
`HashSet<Contact>` equality includes relay/alias, so the same pubkey with a different relay
|
||||||
|
could render twice in the contact picker).
|
||||||
|
- S1/S2 (task-handle containers, `.ok()`/`let _ =` sweeps) remain open by request.
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
assets = { path = "../crates/assets" }
|
assets = { path = "../crates/assets" }
|
||||||
workspace = { path = "../crates/workspace" }
|
workspace = { path = "../crates/workspace" }
|
||||||
ui = { path = "../crates/ui" }
|
|
||||||
state = { path = "../crates/state" }
|
state = { path = "../crates/state" }
|
||||||
nip4e = { path = "../crates/nip4e" }
|
nip4e = { path = "../crates/nip4e" }
|
||||||
chat = { path = "../crates/chat" }
|
chat = { path = "../crates/chat" }
|
||||||
|
|||||||
Reference in New Issue
Block a user