chore: simplify chat send reports #55

Merged
reya merged 2 commits from refactor-send-msg into master 2026-09-29 21:53:52 +00:00
8 changed files with 180 additions and 304 deletions
Generated
-2
View File
@@ -1097,8 +1097,6 @@ dependencies = [
"anyhow", "anyhow",
"chat", "chat",
"common", "common",
"flume 0.11.1",
"futures",
"gpui-pre", "gpui-pre",
"itertools 0.13.0", "itertools 0.13.0",
"log", "log",
+16 -25
View File
@@ -14,7 +14,7 @@ use gpui::{
use instant::Duration; use instant::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{SmallVec, smallvec}; use smallvec::{SmallVec, smallvec};
use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP, UniversalSigner}; use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP};
mod message; mod message;
mod room; mod room;
@@ -73,41 +73,29 @@ impl Signal {
pub struct ChatRegistry { pub struct ChatRegistry {
/// Chat rooms /// Chat rooms
rooms: Vec<Entity<Room>>, rooms: Vec<Entity<Room>>,
/// O(1) room lookup by room ID /// O(1) room lookup by room ID
room_index: HashMap<u64, Entity<Room>>, room_index: HashMap<u64, Entity<Room>>,
/// Events that failed to unwrap for any reason /// Events that failed to unwrap for any reason
trash: Entity<BTreeSet<FailedMessage>>, trash: Entity<BTreeSet<FailedMessage>>,
/// Tracking events seen on which relays in the current session /// Tracking events seen on which relays in the current session
seen: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>, seen: Arc<RwLock<HashMap<EventId, HashSet<RelayUrl>>>>,
/// Mapping of unwrapped event ids to their gift wrap event ids /// Mapping of unwrapped event ids to their gift wrap event ids
event_map: Arc<RwLock<HashMap<EventId, EventId>>>, event_map: Arc<RwLock<HashMap<EventId, EventId>>>,
/// True while the initial event backlog is still loading /// True while the initial event backlog is still loading
tracking: Arc<AtomicBool>, tracking: Arc<AtomicBool>,
/// Channel for sending signals to the UI. /// Channel for sending signals to the UI.
signal_tx: flume::Sender<Signal>, signal_tx: flume::Sender<Signal>,
/// Channel for receiving signals from the UI. /// Channel for receiving signals from the UI.
signal_rx: flume::Receiver<Signal>, signal_rx: flume::Receiver<Signal>,
/// Async tasks /// Async tasks
tasks: SmallVec<[Task<Result<(), Error>>; 2]>, tasks: SmallVec<[Task<Result<(), Error>>; 2]>,
/// Notification listener task (cancelled on signer change) /// Notification listener task (cancelled on signer change)
notification_listener: Option<Task<Result<(), Error>>>, notification_listener: Option<Task<Result<(), Error>>>,
/// Signal consumer task (cancelled on signer change) /// Signal consumer task (cancelled on signer change)
signal_consumer: Option<Task<Result<(), Error>>>, signal_consumer: Option<Task<Result<(), Error>>>,
/// Fuzzy matcher for room search (cached; intentionally excluded from Debug) /// Fuzzy matcher for room search (cached; intentionally excluded from Debug)
#[allow(dead_code)] #[allow(dead_code)]
matcher: CachedMatcher, matcher: CachedMatcher,
/// Subscriptions /// Subscriptions
_subscriptions: SmallVec<[Subscription; 2]>, _subscriptions: SmallVec<[Subscription; 2]>,
} }
@@ -144,8 +132,9 @@ impl ChatRegistry {
/// Create a new chat registry instance /// Create a new chat registry instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let entity = cx.entity().downgrade();
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let entity = cx.entity().downgrade();
let (tx, rx) = flume::unbounded::<Signal>(); let (tx, rx) = flume::unbounded::<Signal>();
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
@@ -162,11 +151,9 @@ impl ChatRegistry {
); );
cx.defer(move |cx| { cx.defer(move |cx| {
entity let _ = entity.update(cx, |this, cx| {
.update(cx, |this, cx| {
this.get_rooms(cx); this.get_rooms(cx);
}) });
.ok();
}); });
Self { Self {
@@ -237,6 +224,7 @@ impl ChatRegistry {
if processed_events.len() >= MAX_PROCESSED { if processed_events.len() >= MAX_PROCESSED {
processed_events.clear(); processed_events.clear();
} }
if !processed_events.insert(event.id) { if !processed_events.insert(event.id) {
continue; continue;
} }
@@ -752,11 +740,14 @@ impl ChatRegistry {
} }
/// Unwraps a gift-wrapped event and processes its contents. /// Unwraps a gift-wrapped event and processes its contents.
async fn extract_rumor( async fn extract_rumor<S>(
client: &Client, client: &Client,
signer: &UniversalSigner, signer: &S,
gift_wrap: &Event, gift_wrap: &Event,
) -> Result<UnsignedEvent, Error> { ) -> Result<UnsignedEvent, Error>
where
S: AsyncNip44 + ?Sized,
{
// Try to get cached rumor first // Try to get cached rumor first
if let Ok(rumor) = get_rumor(client, gift_wrap.id).await { if let Ok(rumor) = get_rumor(client, gift_wrap.id).await {
return Ok(rumor); return Ok(rumor);
@@ -783,10 +774,10 @@ async fn extract_rumor(
} }
/// Attempts to unwrap a gift wrap event with a given signer. /// Attempts to unwrap a gift wrap event with a given signer.
async fn try_unwrap_with( async fn try_unwrap_with<S>(signer: &S, gift_wrap: &Event) -> Result<UnwrappedGift, Error>
signer: &UniversalSigner, where
gift_wrap: &Event, S: AsyncNip44 + ?Sized,
) -> Result<UnwrappedGift, Error> { {
// Get the sealed event // Get the sealed event
let seal = signer let seal = signer
.nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content) .nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content)
+120 -161
View File
@@ -10,92 +10,45 @@ use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::{Person, PersonRegistry}; use person::{Person, PersonRegistry};
use settings::{RoomConfig, SignerKind}; use settings::{RoomConfig, SignerKind};
use state::{NostrRegistry, TIMEOUT, UniversalSigner}; use state::{NostrRegistry, TIMEOUT};
use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage}; use crate::{FileAttachment, KIND_FILE_MESSAGE, NewMessage};
const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet."; const NO_DEKEY: &str = "User hasn't set up a decoupled encryption key yet.";
const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available."; const USER_NO_DEKEY: &str = "You haven't set up a decoupled encryption key or it's not available.";
/// The outcome of delivering an encrypted rumor to a single receiver.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SendReport { pub struct SendReport {
/// The receiver the rumor was addressed to.
pub receiver: PublicKey, pub receiver: PublicKey,
pub gift_wrap_id: Option<EventId>, /// The per-relay send output, or the error that prevented the send.
pub error: Option<SharedString>, result: Result<Output<EventId, EventSendStatus>, SharedString>,
pub output: Option<Output<EventId, EventSendStatus>>,
} }
impl SendReport { impl SendReport {
pub fn new(receiver: PublicKey) -> Self { /// The error that prevented the send, if any.
Self { pub fn error(&self) -> Option<&SharedString> {
receiver, self.result.as_ref().err()
gift_wrap_id: None,
error: None,
output: None,
}
} }
/// Set the gift wrap ID. /// The per-relay send output, if the gift wrap was dispatched.
pub fn gift_wrap_id(mut self, gift_wrap_id: EventId) -> Self { pub fn output(&self) -> Option<&Output<EventId, EventSendStatus>> {
self.gift_wrap_id = Some(gift_wrap_id); self.result.as_ref().ok()
self
} }
/// Set the output. /// Whether at least one relay accepted the event.
pub fn output(mut self, output: Output<EventId, EventSendStatus>) -> Self {
self.output = Some(output);
self
}
/// Set the error message.
pub fn error<T>(mut self, error: T) -> Self
where
T: Into<SharedString>,
{
self.error = Some(error.into());
self
}
/// Returns true if the send is pending.
pub fn pending(&self) -> bool {
self.error.is_none()
&& self
.output
.as_ref()
.is_some_and(|o| o.success.is_empty() && o.failed.is_empty())
}
/// Returns true if the send was successful.
pub fn success(&self) -> bool { pub fn success(&self) -> bool {
self.error.is_none() && self.output.as_ref().is_some_and(|o| !o.success.is_empty()) self.output()
.is_some_and(|output| !output.success.is_empty())
} }
/// Returns true if the send failed. /// Whether no relay accepted the event.
pub fn failed(&self) -> bool { pub fn failed(&self) -> bool {
self.error.is_some() && self.output.as_ref().is_some_and(|o| !o.failed.is_empty()) match &self.result {
Err(_) => true,
Ok(output) => output.success.is_empty(),
} }
}
#[derive(Debug, Clone)]
pub enum SendStatus {
Ok {
id: EventId,
relay: RelayUrl,
},
Failed {
id: EventId,
relay: RelayUrl,
message: String,
},
}
impl SendStatus {
pub fn ok(id: EventId, relay: RelayUrl) -> Self {
Self::Ok { id, relay }
}
pub fn failed(id: EventId, relay: RelayUrl, message: String) -> Self {
Self::Failed { id, relay, message }
} }
} }
@@ -520,30 +473,28 @@ impl Room {
} }
/// Select the appropriate signer based on signer kind and available keys. /// Select the appropriate signer based on signer kind and available keys.
fn select_signer( fn select_signer<'a, S>(
signer_kind: &SignerKind, signer_kind: &SignerKind,
has_announcement: bool, has_announcement: bool,
encryption_signer: &Option<UniversalSigner>, encryption_signer: &'a Option<S>,
user_signer: &UniversalSigner, user_signer: &'a S,
) -> UniversalSigner { ) -> &'a S {
match signer_kind { match signer_kind {
SignerKind::Auto => { SignerKind::Auto => {
if has_announcement { if has_announcement {
encryption_signer encryption_signer.as_ref().unwrap_or(user_signer)
.clone()
.unwrap_or_else(|| user_signer.clone())
} else { } else {
user_signer.clone() user_signer
} }
} }
SignerKind::Encryption => encryption_signer SignerKind::Encryption => encryption_signer
.clone() .as_ref()
.expect("encryption signer must be set"), .expect("encryption signer must be set"),
SignerKind::User => user_signer.clone(), SignerKind::User => user_signer,
} }
} }
/// Send rumor event to all members's messaging relays /// Send the rumor to every member's messaging relays.
pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> { pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option<Task<Vec<SendReport>>> {
let config = self.config.clone(); let config = self.config.clone();
@@ -560,7 +511,7 @@ impl Room {
let sender = persons.read(cx).get(&current_user, cx); let sender = persons.read(cx).get(&current_user, cx);
// Get all members (excluding sender) // Get all members (excluding sender)
let members: Vec<Person> = self let receivers: Vec<Person> = self
.members .members
.iter() .iter()
.filter(|public_key| public_key != &&sender.public_key()) .filter(|public_key| public_key != &&sender.public_key())
@@ -569,71 +520,33 @@ impl Room {
Some(cx.background_spawn(async move { Some(cx.background_spawn(async move {
let signer_kind = config.signer_kind(); let signer_kind = config.signer_kind();
let backup = config.backup();
let mut sents = 0; // Deliver to every receiver concurrently
let mut reports = Vec::new(); let sends = receivers.into_iter().map(|receiver| {
deliver(
// Process each member &client,
for member in members {
let announcement = member.announcement();
let public_key = member.public_key();
// Handle encryption signer requirements
if signer_kind.encryption() {
// Receiver didn't set up a decoupled encryption key
if announcement.is_none() {
reports.push(SendReport::new(public_key).error(NO_DEKEY));
continue;
}
// Sender didn't set up a decoupled encryption key
if encryption_signer.is_none() {
reports.push(SendReport::new(sender.public_key()).error(USER_NO_DEKEY));
continue;
}
}
// Determine the signer to use
let signer = Self::select_signer(
signer_kind, signer_kind,
announcement.is_some(), receiver,
&rumor,
&encryption_signer, &encryption_signer,
&user_signer, &user_signer,
); )
});
let mut reports: Vec<SendReport> = futures::future::join_all(sends).await;
// Send the gift wrap event and collect the report // Back up the message to ourselves once at least one receiver got it
match send_gift_wrap(&client, &signer, &member, &rumor, signer_kind).await { if config.backup() && reports.iter().any(SendReport::success) {
Ok(report) => { reports.push(
reports.push(report); deliver(
sents += 1; &client,
}
Err(error) => {
let report = SendReport::new(public_key).error(error.to_string());
reports.push(report);
}
}
}
// Send backup to current user if needed
if backup && sents >= 1 {
let public_key = sender.public_key();
// Determine the signer to use
let signer = Self::select_signer(
signer_kind, signer_kind,
sender.announcement().is_some(), sender,
&rumor,
&encryption_signer, &encryption_signer,
&user_signer, &user_signer,
)
.await,
); );
match send_gift_wrap(&client, &signer, &sender, &rumor, signer_kind).await {
Ok(report) => reports.push(report),
Err(error) => {
let report = SendReport::new(public_key).error(error.to_string());
reports.push(report);
}
}
} }
reports reports
@@ -641,55 +554,101 @@ impl Room {
} }
} }
// Helper function to send a gift-wrapped event /// Deliver a rumor to a single receiver.
async fn send_gift_wrap( async fn deliver<S>(
client: &Client, client: &Client,
signer: &UniversalSigner, signer_kind: &SignerKind,
receiver: Person,
rumor: &UnsignedEvent,
encryption_signer: &Option<S>,
user_signer: &S,
) -> SendReport
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
{
let public_key = receiver.public_key();
let announcement = receiver.announcement();
if signer_kind.encryption() {
// Receiver didn't set up a decoupled encryption key
if announcement.is_none() {
return SendReport {
receiver: public_key,
result: Err(NO_DEKEY.into()),
};
}
// Sender didn't set up a decoupled encryption key
if encryption_signer.is_none() {
return SendReport {
receiver: public_key,
result: Err(USER_NO_DEKEY.into()),
};
}
}
// Determine the signer to use
let signer = Room::select_signer(
signer_kind,
announcement.is_some(),
encryption_signer,
user_signer,
);
match send_gift_wrap(client, signer, &receiver, rumor, signer_kind).await {
Ok(output) => SendReport {
receiver: public_key,
result: Ok(output),
},
Err(error) => SendReport {
receiver: public_key,
result: Err(error.to_string().into()),
},
}
}
/// Build the gift-wrapped event for a rumor and send it.
async fn send_gift_wrap<S>(
client: &Client,
signer: &S,
receiver: &Person, receiver: &Person,
rumor: &UnsignedEvent, rumor: &UnsignedEvent,
config: &SignerKind, config: &SignerKind,
) -> Result<SendReport, Error> { ) -> Result<Output<EventId, EventSendStatus>, Error>
let k_tag = Tag::custom("k", [rumor.kind.to_string()]); where
let mut extra_tags = vec![k_tag]; S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44,
{
let mut extra_tags = vec![Tag::custom("k", [rumor.kind.to_string()])];
// Determine the receiver public key based on the config // Determine the receiver public key based on the config
let receiver = match config { let receiver_key = match config {
SignerKind::Auto => { SignerKind::Auto => match receiver.announcement().as_ref() {
if let Some(announcement) = receiver.announcement().as_ref() { Some(announcement) => {
extra_tags.push(Tag::public_key(receiver.public_key())); extra_tags.push(Tag::public_key(receiver.public_key()));
announcement.public_key() announcement.public_key()
} else {
receiver.public_key()
} }
} None => receiver.public_key(),
SignerKind::Encryption => { },
if let Some(announcement) = receiver.announcement().as_ref() { SignerKind::Encryption => match receiver.announcement().as_ref() {
Some(announcement) => {
extra_tags.push(Tag::public_key(receiver.public_key())); extra_tags.push(Tag::public_key(receiver.public_key()));
announcement.public_key() announcement.public_key()
} else {
return Err(anyhow!("User has no encryption announcement"));
}
} }
None => return Err(anyhow!("User has no encryption announcement")),
},
SignerKind::User => receiver.public_key(), SignerKind::User => receiver.public_key(),
}; };
// Construct the gift wrap event // Construct the gift wrap event
let event = nip59::GiftWrapBuilder::new(receiver, rumor.clone()) let event = nip59::GiftWrapBuilder::new(receiver_key, rumor.clone())
.extra_tags(extra_tags) .extra_tags(extra_tags)
.finalize_async(signer) .finalize_async(signer)
.await?; .await?;
// Send the gift wrap event and collect the report // Send to the receiver's NIP-17 relays.
let report = client client
.send_event(&event) .send_event(&event)
.to_nip17() .to_nip17()
.ack_policy(AckPolicy::none())
.await .await
.map(|output| { .map_err(Into::into)
SendReport::new(receiver)
.gift_wrap_id(event.id)
.output(output)
})?;
Ok(report)
} }
-2
View File
@@ -18,8 +18,6 @@ nostr-sdk.workspace = true
anyhow.workspace = true anyhow.workspace = true
itertools.workspace = true itertools.workspace = true
smallvec.workspace = true smallvec.workspace = true
flume.workspace = true
futures.workspace = true
log.workspace = true log.workspace = true
serde.workspace = true serde.workspace = true
+6 -90
View File
@@ -4,9 +4,8 @@ use std::sync::{Arc, LazyLock, RwLock};
pub use actions::*; pub use actions::*;
use anyhow::Error; use anyhow::Error;
use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus}; use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport};
use common::TimestampExt; use common::TimestampExt;
use futures::lock::Mutex;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
@@ -95,9 +94,6 @@ pub struct ChatPanel {
/// Subject bar visibility /// Subject bar visibility
subject_bar: Entity<bool>, subject_bar: Entity<bool>,
/// Sent message ids
sent_ids: Arc<Mutex<Vec<EventId>>>,
/// Replies to /// Replies to
replies_to: Entity<HashSet<EventId>>, replies_to: Entity<HashSet<EventId>>,
@@ -181,7 +177,6 @@ impl ChatPanel {
// Define all functions that will run after the current cycle // 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(cx); this.connect(cx);
this.handle_notifications(cx);
this.subscribe_room_events(window, cx); this.subscribe_room_events(window, cx);
this.get_messages(window, cx); this.get_messages(window, cx);
}); });
@@ -203,7 +198,6 @@ impl ChatPanel {
decrypted_files: HashMap::new(), decrypted_files: HashMap::new(),
rendered_texts_by_id: BTreeMap::new(), rendered_texts_by_id: BTreeMap::new(),
reports_by_id, reports_by_id,
sent_ids: Arc::new(Mutex::new(Vec::new())),
uploading: false, uploading: false,
subscriptions, subscriptions,
tasks: vec![], tasks: vec![],
@@ -218,78 +212,6 @@ impl ChatPanel {
} }
} }
/// Handle nostr notifications
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
let sent_ids = self.sent_ids.clone();
let reports = self.reports_by_id.clone();
let (tx, rx) = flume::bounded::<Arc<SendStatus>>(256);
self.tasks.push(cx.background_spawn(async move {
let mut notifications = client.notifications();
while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { message, relay_url } = notification
&& let RelayMessage::Ok {
event_id,
status,
message,
} = *message
{
let sent_ids = sent_ids.lock().await;
if sent_ids.contains(&event_id) {
let status = if status {
SendStatus::ok(event_id, relay_url)
} else {
SendStatus::failed(event_id, relay_url, message.into())
};
tx.send_async(Arc::new(status)).await.ok();
}
}
}
Ok(())
}));
self.tasks.push(cx.spawn(async move |this, cx| {
while let Ok(status) = rx.recv_async().await {
{
let mut map = reports.write().unwrap();
let status_id = match &*status {
SendStatus::Ok { id, .. } => *id,
SendStatus::Failed { id, .. } => *id,
};
// Find the matching report and update it (exit early on first match)
'outer: for reports_list in map.values_mut() {
for report in reports_list.iter_mut() {
let Some(output) = report.output.as_mut() else {
continue;
};
if *output.id() != status_id {
continue;
}
match &*status {
SendStatus::Ok { relay, .. } => {
output.success.insert(relay.clone(), EventSendStatus::Sent);
}
SendStatus::Failed { relay, message, .. } => {
output.failed.insert(relay.clone(), message.clone());
}
}
break 'outer;
}
}
}
this.update(cx, |_, cx| cx.notify()).ok();
}
Ok(())
}));
}
/// Subscribe to room events /// Subscribe to room events
fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn subscribe_room_events(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(room) = self.room.upgrade() else { let Some(room) = self.room.upgrade() else {
@@ -513,7 +435,6 @@ impl ChatPanel {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
let id = rumor.id.expect("rumor must have an id"); let id = rumor.id.expect("rumor must have an id");
let sent_ids = self.sent_ids.clone();
// Insert optimistic message and clear input // Insert optimistic message and clear input
if rumor.kind != Kind::Reaction { if rumor.kind != Kind::Reaction {
@@ -528,13 +449,10 @@ impl ChatPanel {
// Spawn a single task to await the send and update reports // Spawn a single task to await the send and update reports
self.tasks.push(cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let outputs = send_task.await; let reports = send_task.await;
let mut sent_ids = sent_ids.lock().await;
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.insert_reports(id, outputs, cx); this.insert_reports(id, reports, cx);
})?; })?;
Ok(()) Ok(())
@@ -1391,9 +1309,7 @@ impl ChatPanel {
fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement { fn render_sent_reports(&self, id: &EventId, cx: &App) -> impl IntoElement {
let reports = self.sent_reports(id); let reports = self.sent_reports(id);
let pending = reports let pending = reports.as_ref().is_some_and(|reports| reports.is_empty());
.as_ref()
.is_some_and(|reports| reports.is_empty() || reports.iter().any(|r| r.pending()));
let success = reports let success = reports
.as_ref() .as_ref()
@@ -1462,7 +1378,7 @@ impl ChatPanel {
.child(name.clone()), .child(name.clone()),
), ),
) )
.when_some(report.error.clone(), |this, error| { .when_some(report.error().cloned(), |this, error| {
this.child( this.child(
h_flex() h_flex()
.flex_wrap() .flex_wrap()
@@ -1477,7 +1393,7 @@ impl ChatPanel {
.child(div().flex_1().w_full().text_center().child(error)), .child(div().flex_1().w_full().text_center().child(error)),
) )
}) })
.when_some(report.output.clone(), |this, output| { .when_some(report.output().cloned(), |this, output| {
this.child( this.child(
v_flex() v_flex()
.gap_2() .gap_2()
+19 -10
View File
@@ -12,7 +12,6 @@ use concord::derive::{GroupKey, channel_rekey_group_key, epoch_key_commitment};
use concord::state::{CommunityState, HeldKey}; use concord::state::{CommunityState, HeldKey};
use concord::{ChannelId, CommunityId, Epoch}; use concord::{ChannelId, CommunityId, Epoch};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::UniversalSigner;
use crate::sync::{self, PlaneKind}; use crate::sync::{self, PlaneKind};
@@ -175,13 +174,16 @@ pub struct ChannelAdoption {
} }
/// Read every rekey wrap the local database holds and adopt what is admissible. /// Read every rekey wrap the local database holds and adopt what is admissible.
pub async fn adopt( pub async fn adopt<S>(
client: &Client, client: &Client,
state: &CommunityState, state: &CommunityState,
roles: &CommunityRoles, roles: &CommunityRoles,
signer: &UniversalSigner, signer: &S,
me: PublicKey, me: PublicKey,
) -> Result<Adoptions> { ) -> Result<Adoptions>
where
S: AsyncNip44 + ?Sized,
{
let watches = watches(state)?; let watches = watches(state)?;
let mut authors: BTreeSet<PublicKey> = BTreeSet::new(); let mut authors: BTreeSet<PublicKey> = BTreeSet::new();
@@ -362,15 +364,18 @@ impl Rewrite {
/// Publish this client's own rotation of one scope to its next epoch. /// Publish this client's own rotation of one scope to its next epoch.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn rotate( pub async fn rotate<S>(
client: &Client, client: &Client,
state: &CommunityState, state: &CommunityState,
roles: &CommunityRoles, roles: &CommunityRoles,
signer: &UniversalSigner, signer: &S,
me: PublicKey, me: PublicKey,
rewrite: &Rewrite, rewrite: &Rewrite,
at: Timestamp, at: Timestamp,
) -> Result<Epoch> { ) -> Result<Epoch>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
if rewrite.excluded.contains(&me) { if rewrite.excluded.contains(&me) {
bail!("a rotation cannot cut off the member who publishes it"); bail!("a rotation cannot cut off the member who publishes it");
} }
@@ -611,18 +616,21 @@ struct Delivery {
/// Walk a scope's rotations forward, one epoch at a time, off the key held. /// Walk a scope's rotations forward, one epoch at a time, off the key held.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
async fn walk( async fn walk<S>(
scope: RekeyScope, scope: RekeyScope,
permissions: &[u64], permissions: &[u64],
mut held_epoch: Epoch, mut held_epoch: Epoch,
mut held_key: [u8; 32], mut held_key: [u8; 32],
state: &CommunityState, state: &CommunityState,
roles: &CommunityRoles, roles: &CommunityRoles,
signer: &UniversalSigner, signer: &S,
me: PublicKey, me: PublicKey,
rotations: &[Rotation], rotations: &[Rotation],
published: &BTreeMap<RotationKey, u64>, published: &BTreeMap<RotationKey, u64>,
) -> Result<Step> { ) -> Result<Step>
where
S: AsyncNip44 + ?Sized,
{
let mut step = Step::default(); let mut step = Step::default();
let mut stepped: Vec<HeldKey> = Vec::new(); let mut stepped: Vec<HeldKey> = Vec::new();
let ceiling = held_epoch.0 + REKEY_LOOKAHEAD; let ceiling = held_epoch.0 + REKEY_LOOKAHEAD;
@@ -794,6 +802,7 @@ mod tests {
}; };
use concord::state::{ChannelKeyRef, HeldRoot}; use concord::state::{ChannelKeyRef, HeldRoot};
use nostr_memory::MemoryDatabase; use nostr_memory::MemoryDatabase;
use state::UniversalSigner;
use super::*; use super::*;
+5 -6
View File
@@ -14,7 +14,6 @@ use concord::state::{CommunityState, HeldKey, HeldRoot, list_entry};
use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32}; use concord::{ChannelId, CommunityId, Epoch, GroupKey, decode_hex_32};
use gpui::AsyncApp; use gpui::AsyncApp;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::UniversalSigner;
use crate::cache::{self, Observed}; use crate::cache::{self, Observed};
use crate::history::{CURSOR_OVERLAP, Window}; use crate::history::{CURSOR_OVERLAP, Window};
@@ -348,11 +347,10 @@ pub async fn subscribe_list(client: &Client, self_pk: PublicKey) -> Result<()> {
/// ///
/// A held membership is dropped only when the List carries a tombstone at least /// A held membership is dropped only when the List carries a tombstone at least
/// as new as it, because absence from the List is never a fact (§8). /// as new as it, because absence from the List is never a fact (§8).
pub async fn load( pub async fn load<S>(client: &Client, signer: &S, self_pk: PublicKey) -> Result<Vec<CommunityState>>
client: &Client, where
signer: &UniversalSigner, S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
self_pk: PublicKey, {
) -> Result<Vec<CommunityState>> {
let list = match load_list(client, signer, self_pk).await? { let list = match load_list(client, signer, self_pk).await? {
Some(list) => list, Some(list) => list,
None => return cache::load_states(client).await, None => return cache::load_states(client).await,
@@ -735,6 +733,7 @@ mod tests {
use std::time::Duration; use std::time::Duration;
use nostr_memory::MemoryDatabase; use nostr_memory::MemoryDatabase;
use state::UniversalSigner;
use super::*; use super::*;
+8 -2
View File
@@ -759,7 +759,10 @@ fn get_or_init_app_keys(cx: &App) -> Task<Result<Keys, Error>> {
} }
/// Encrypt and store device keys in the local database. /// Encrypt and store device keys in the local database.
async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Result<(), Error> { async fn set_keys<S>(client: &Client, signer: &S, secret: &str) -> Result<(), Error>
where
S: AsyncGetPublicKey + AsyncSignEvent + AsyncNip44 + ?Sized,
{
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
let content = signer.nip44_encrypt_async(&public_key, secret).await?; let content = signer.nip44_encrypt_async(&public_key, secret).await?;
@@ -776,7 +779,10 @@ async fn set_keys(client: &Client, signer: &UniversalSigner, secret: &str) -> Re
} }
/// Get device keys from the local database. /// Get device keys from the local database.
async fn get_keys(client: &Client, signer: &UniversalSigner) -> Result<Keys, Error> { async fn get_keys<S>(client: &Client, signer: &S) -> Result<Keys, Error>
where
S: AsyncGetPublicKey + AsyncNip44 + ?Sized,
{
let public_key = signer.get_public_key_async().await?; let public_key = signer.get_public_key_async().await?;
let filter = Filter::new() let filter = Filter::new()