From e939bf17adb773c2d32abf3ec9de4095ea774f83 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 2 Dec 2023 10:22:33 +1300 Subject: [PATCH 01/81] Define RelationshipById, RelationshipByAddr --- gossip-lib/src/storage/types/mod.rs | 6 +++ .../storage/types/relationship_by_addr1.rs | 34 ++++++++++++++ .../src/storage/types/relationship_by_id1.rs | 45 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 gossip-lib/src/storage/types/relationship_by_addr1.rs create mode 100644 gossip-lib/src/storage/types/relationship_by_id1.rs diff --git a/gossip-lib/src/storage/types/mod.rs b/gossip-lib/src/storage/types/mod.rs index 61e537123..5de23f4b7 100644 --- a/gossip-lib/src/storage/types/mod.rs +++ b/gossip-lib/src/storage/types/mod.rs @@ -13,6 +13,12 @@ pub use person_relay1::PersonRelay1; mod relationship1; pub use relationship1::Relationship1; +mod relationship_by_addr1; +pub use relationship_by_addr1::RelationshipByAddr1; + +mod relationship_by_id1; +pub use relationship_by_id1::RelationshipById1; + mod relay1; pub use relay1::Relay1; diff --git a/gossip-lib/src/storage/types/relationship_by_addr1.rs b/gossip-lib/src/storage/types/relationship_by_addr1.rs new file mode 100644 index 000000000..2112eff36 --- /dev/null +++ b/gossip-lib/src/storage/types/relationship_by_addr1.rs @@ -0,0 +1,34 @@ +use nostr_types::PublicKey; +use speedy::{Readable, Writable}; + +/// A relationship between events by Address and Id +#[derive(Clone, Debug, PartialEq, Eq, Readable, Writable)] +pub enum RelationshipByAddr1 { + // NIP-01, NIP-10 replies + Reply, + + // NIP-09 Event Deletion + Deletion { by: PublicKey, reason: String }, + + // NIP-51 Lists + ListBookmarks, + + // NIP-51 Lists + Curation, + + // communities + // interests + // emojis + + // NIP-53 + LiveChatMessage, + + // NIP-58 + BadgeAward, + + // NIP-72 Moderated Communities (Reddit-style) + // PostedToCommunity, + + // NIP-89 Recommended Application Handlers + HandlerRecommendation, +} diff --git a/gossip-lib/src/storage/types/relationship_by_id1.rs b/gossip-lib/src/storage/types/relationship_by_id1.rs new file mode 100644 index 000000000..d4b4d8195 --- /dev/null +++ b/gossip-lib/src/storage/types/relationship_by_id1.rs @@ -0,0 +1,45 @@ +use nostr_types::{MilliSatoshi, PublicKey}; +use speedy::{Readable, Writable}; + +/// A relationship between events by Ids +#[derive(Clone, Debug, PartialEq, Eq, Readable, Writable)] +pub enum RelationshipById1 { + // NIP-01, NIP-10 replies + Reply, + + // NIP-03 OpenTimestamps Attestations for Events + Timestamp, + + // NIP-09 Event Deletion + Deletion { by: PublicKey, reason: String }, + + // NIP-25 Reactions + Reaction { by: PublicKey, reaction: String }, + + // NIP-32 Labeling + Labels { label: String, namespace: String }, + + // NIP-51 Lists + ListMutesThread, + + // NIP-51 Lists + ListPins, + + // NIP-51 Lists + ListBookmarks, + + // NIP-51 Lists + Curation, + + // NIP-56 Reporting + Reports(String), + + // NIP-57 Lightning Zaps + ZapReceipt { by: PublicKey, amount: MilliSatoshi }, + + // NIP-72 Moderated Communities (Reddit-style) + // Approves { in_community: EventAddr }, + + // NIP-90 Data Vending Machines + JobResult, +} From 8e42db1c1bbe2fd70081312ecff85e53d8c8707c Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 2 Dec 2023 11:12:29 +1300 Subject: [PATCH 02/81] storage: define two new database: relationships_by_id, relationships_by_addr --- gossip-lib/src/storage/mod.rs | 28 ++++ .../src/storage/relationships_by_addr1.rs | 133 ++++++++++++++++++ .../src/storage/relationships_by_id1.rs | 100 +++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 gossip-lib/src/storage/relationships_by_addr1.rs create mode 100644 gossip-lib/src/storage/relationships_by_id1.rs diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index e58c383ab..953f7cbb5 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -30,6 +30,8 @@ mod person_lists1; mod person_lists2; mod person_relays1; mod relationships1; +mod relationships_by_addr1; +mod relationships_by_id1; mod relays1; mod reprel1; mod unindexed_giftwraps1; @@ -191,6 +193,8 @@ impl Storage { let _ = self.db_person_relays()?; let _ = self.db_relationships()?; let _ = self.db_reprel()?; + let _ = self.db_relationships_by_id()?; + let _ = self.db_relationships_by_addr()?; let _ = self.db_relays()?; let _ = self.db_unindexed_giftwraps()?; let _ = self.db_person_lists()?; @@ -274,6 +278,16 @@ impl Storage { self.db_reprel1() } + #[inline] + pub(crate) fn db_relationships_by_addr(&self) -> Result { + self.db_relationships_by_addr1() + } + + #[inline] + pub(crate) fn db_relationships_by_id(&self) -> Result { + self.db_relationships_by_id1() + } + #[inline] pub(crate) fn db_relays(&self) -> Result { self.db_relays1() @@ -358,6 +372,20 @@ impl Storage { Ok(self.db_reprel()?.len(&txn)?) } + /// The number of records in the relationships_by_addr table + #[inline] + pub fn get_relationships_by_addr_len(&self) -> Result { + let txn = self.env.read_txn()?; + Ok(self.db_relationships_by_addr()?.len(&txn)?) + } + + /// The number of records in the relationships_by_id table + #[inline] + pub fn get_relationships_by_id_len(&self) -> Result { + let txn = self.env.read_txn()?; + Ok(self.db_relationships_by_id()?.len(&txn)?) + } + /// The number of records in the people table #[inline] pub fn get_people_len(&self) -> Result { diff --git a/gossip-lib/src/storage/relationships_by_addr1.rs b/gossip-lib/src/storage/relationships_by_addr1.rs new file mode 100644 index 000000000..c1e8c4f41 --- /dev/null +++ b/gossip-lib/src/storage/relationships_by_addr1.rs @@ -0,0 +1,133 @@ +use crate::error::Error; +use crate::storage::types::RelationshipByAddr1; +use crate::storage::{RawDatabase, Storage}; +use heed::RwTxn; +use heed::{types::UnalignedSlice, DatabaseFlags}; +use nostr_types::{EventAddr, Id}; +use speedy::{Readable, Writable}; +use std::sync::Mutex; + +// Kind:Pubkey:d-tag -> RelationshipByAddr1:Id +// (has dups) + +static RELATIONSHIPS_BY_ADDR1_DB_CREATE_LOCK: Mutex<()> = Mutex::new(()); +static mut RELATIONSHIPS_BY_ADDR1_DB: Option = None; + +impl Storage { + pub(super) fn db_relationships_by_addr1(&self) -> Result { + unsafe { + if let Some(db) = RELATIONSHIPS_BY_ADDR1_DB { + Ok(db) + } else { + // Lock. This drops when anything returns. + let _lock = RELATIONSHIPS_BY_ADDR1_DB_CREATE_LOCK.lock(); + + // In case of a race, check again + if let Some(db) = RELATIONSHIPS_BY_ADDR1_DB { + return Ok(db); + } + + // Create it. We know that nobody else is doing this and that + // it cannot happen twice. + let mut txn = self.env.write_txn()?; + let db = self + .env + .database_options() + .types::, UnalignedSlice>() + .flags(DatabaseFlags::DUP_SORT) // NOT FIXED, RelationshipByAddr1 serialized isn't. + .name("relationships_by_addr1") + .create(&mut txn)?; + txn.commit()?; + RELATIONSHIPS_BY_ADDR1_DB = Some(db); + Ok(db) + } + } + } + + pub(crate) fn write_relationship_by_addr1<'a>( + &'a self, + addr: EventAddr, + related: Id, + relationship_by_addr: RelationshipByAddr1, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let key = relationships_by_addr1_into_key(&addr); + let value = relationships_by_addr1_into_value(relationship_by_addr, related)?; + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + self.db_relationships_by_addr1()?.put(txn, &key, &value)?; + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } + + pub(crate) fn find_relationships_by_addr1( + &self, + addr: &EventAddr, + ) -> Result, Error> { + let key = relationships_by_addr1_into_key(addr); + let txn = self.env.read_txn()?; + let iter = match self + .db_relationships_by_addr1()? + .get_duplicates(&txn, &key)? + { + Some(iter) => iter, + None => return Ok(vec![]), + }; + let mut output: Vec<(Id, RelationshipByAddr1)> = Vec::new(); + for result in iter { + let (_key, val) = result?; + let (rel, id) = relationships_by_addr1_from_value(val)?; + output.push((id, rel)); + } + Ok(output) + } +} + +fn relationships_by_addr1_into_key(ea: &EventAddr) -> Vec { + let u: u32 = ea.kind.into(); + let mut key: Vec = u.to_be_bytes().as_slice().to_owned(); + key.extend(ea.author.as_bytes()); + key.extend(ea.d.as_bytes()); + key +} + +/* +fn relationships_by_addr1_from_key(key: &[u8]) -> Result { + let u = u32::from_be_bytes(key[..4].try_into().unwrap()); + let kind: EventKind = u.into(); + let pubkey: PublicKey = PublicKey::from_bytes(&key[4..4+32], true)?; + let d: String = String::from_utf8_lossy(&key[4+32..]).to_string(); + Ok(EventAddr { + d, + relays: vec![], + kind, + author: pubkey + }) +} + */ + +fn relationships_by_addr1_into_value( + relationship_by_addr: RelationshipByAddr1, + id: Id, +) -> Result, Error> { + let mut value: Vec = relationship_by_addr.write_to_vec()?; + value.extend(id.as_slice()); + Ok(value) +} + +fn relationships_by_addr1_from_value(value: &[u8]) -> Result<(RelationshipByAddr1, Id), Error> { + let (result, len) = RelationshipByAddr1::read_with_length_from_buffer(value); + let relationship_by_addr = result?; + let id = Id(value[len..len + 32].try_into().unwrap()); + Ok((relationship_by_addr, id)) +} diff --git a/gossip-lib/src/storage/relationships_by_id1.rs b/gossip-lib/src/storage/relationships_by_id1.rs new file mode 100644 index 000000000..a6c0657ec --- /dev/null +++ b/gossip-lib/src/storage/relationships_by_id1.rs @@ -0,0 +1,100 @@ +use crate::error::Error; +use crate::storage::types::RelationshipById1; +use crate::storage::{RawDatabase, Storage}; +use heed::types::UnalignedSlice; +use heed::RwTxn; +use nostr_types::Id; +use speedy::{Readable, Writable}; +use std::sync::Mutex; + +// Id:Id -> RelationshipById1 +// key: id.as_slice(), id.as_slice() | Id(val[32..64].try_into()?) +// val: relationship_by_id.write_to_vec() | RelationshipById1::read_from_buffer(val) + +// NOTE: this means the SECOND Id relates to the FIRST Id, e.g. +// id2 replies to id1 +// id2 reacts to id1 +// id2 deletes id1 +// id2 is a zap receipt on id1 + +static RELATIONSHIPS_BY_ID1_DB_CREATE_LOCK: Mutex<()> = Mutex::new(()); +static mut RELATIONSHIPS_BY_ID1_DB: Option = None; + +impl Storage { + pub(super) fn db_relationships_by_id1(&self) -> Result { + unsafe { + if let Some(db) = RELATIONSHIPS_BY_ID1_DB { + Ok(db) + } else { + // Lock. This drops when anything returns. + let _lock = RELATIONSHIPS_BY_ID1_DB_CREATE_LOCK.lock(); + + // In case of a race, check again + if let Some(db) = RELATIONSHIPS_BY_ID1_DB { + return Ok(db); + } + + // Create it. We know that nobody else is doing this and that + // it cannot happen twice. + let mut txn = self.env.write_txn()?; + let db = self + .env + .database_options() + .types::, UnalignedSlice>() + // no .flags needed? + .name("relationships_by_id1") + .create(&mut txn)?; + txn.commit()?; + RELATIONSHIPS_BY_ID1_DB = Some(db); + Ok(db) + } + } + } + + pub(crate) fn write_relationship_by_id1<'a>( + &'a self, + id: Id, + related: Id, + relationship_by_id: RelationshipById1, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let mut key = id.as_ref().as_slice().to_owned(); + key.extend(related.as_ref()); + let value = relationship_by_id.write_to_vec()?; + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + self.db_relationships_by_id1()?.put(txn, &key, &value)?; + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } + + pub(crate) fn find_relationships_by_id1( + &self, + id: Id, + ) -> Result, Error> { + let start_key = id.as_slice(); + let txn = self.env.read_txn()?; + let iter = self + .db_relationships_by_id1()? + .prefix_iter(&txn, start_key)?; + let mut output: Vec<(Id, RelationshipById1)> = Vec::new(); + for result in iter { + let (key, val) = result?; + let id2 = Id(key[32..64].try_into().unwrap()); + let relationship_by_id = RelationshipById1::read_from_buffer(val)?; + output.push((id2, relationship_by_id)); + } + Ok(output) + } +} From 9f761b8a18e05941caf4edf0e0f9c766f8c7c584 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 2 Dec 2023 11:25:16 +1300 Subject: [PATCH 03/81] Switch code to use new relationship databases --- gossip-bin/src/ui/feed/notedata.rs | 2 +- gossip-bin/src/ui/help/stats.rs | 18 ++- gossip-lib/src/relationship.rs | 7 +- gossip-lib/src/storage/migrations/m1.rs | 14 +-- gossip-lib/src/storage/mod.rs | 149 ++++++++++++------------ 5 files changed, 97 insertions(+), 93 deletions(-) diff --git a/gossip-bin/src/ui/feed/notedata.rs b/gossip-bin/src/ui/feed/notedata.rs index d17b8b3b0..85aeea9fe 100644 --- a/gossip-bin/src/ui/feed/notedata.rs +++ b/gossip-bin/src/ui/feed/notedata.rs @@ -85,7 +85,7 @@ impl NoteData { let delegation = event.delegation(); - let deletion = GLOBALS.storage.get_deletion(event.id).unwrap_or(None); + let deletion = GLOBALS.storage.get_deletion(&event).unwrap_or(None); let (reactions, self_already_reacted) = GLOBALS .storage diff --git a/gossip-bin/src/ui/help/stats.rs b/gossip-bin/src/ui/help/stats.rs index de011decf..dfaa843c0 100644 --- a/gossip-bin/src/ui/help/stats.rs +++ b/gossip-bin/src/ui/help/stats.rs @@ -82,12 +82,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr )); ui.add_space(6.0); - ui.label(format!( - "Event Relationships: {} records", - GLOBALS.storage.get_relationships_len().unwrap_or(0) - )); - ui.add_space(6.0); - ui.label(format!( "Event Seen on Relay: {} records", GLOBALS.storage.get_event_seen_on_relay_len().unwrap_or(0) @@ -129,5 +123,17 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr GLOBALS.storage.get_person_lists_len().unwrap_or(0) )); ui.add_space(6.0); + + ui.label(format!( + "Event Relationships By Id: {} records", + GLOBALS.storage.get_relationships_by_id_len().unwrap_or(0) + )); + ui.add_space(6.0); + + ui.label(format!( + "Event Relationships By Addr: {} records", + GLOBALS.storage.get_relationships_by_addr_len().unwrap_or(0) + )); + ui.add_space(6.0); }); } diff --git a/gossip-lib/src/relationship.rs b/gossip-lib/src/relationship.rs index 53f218434..abeba75b6 100644 --- a/gossip-lib/src/relationship.rs +++ b/gossip-lib/src/relationship.rs @@ -1,2 +1,5 @@ -/// Relationship type, aliased to the latest version -pub type Relationship = crate::storage::types::Relationship1; +/// Relationship type by Id, aliased to the latest version +pub type RelationshipById = crate::storage::types::RelationshipById1; + +/// Relationship type by EventAddr, aliased to the latest version +pub type RelationshipByAddr = crate::storage::types::RelationshipByAddr1; diff --git a/gossip-lib/src/storage/migrations/m1.rs b/gossip-lib/src/storage/migrations/m1.rs index b757dca67..61388a60b 100644 --- a/gossip-lib/src/storage/migrations/m1.rs +++ b/gossip-lib/src/storage/migrations/m1.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use crate::relationship::Relationship; +use crate::storage::types::Relationship1; use crate::storage::Storage; use heed::RwTxn; use nostr_types::{EventReference, EventV1}; @@ -47,7 +47,7 @@ impl Storage { // replies to match event.replies_to() { Some(EventReference::Id(id, _, _)) => { - self.write_relationship1(id, event.id, Relationship::Reply, Some(txn))?; + self.write_relationship1(id, event.id, Relationship1::Reply, Some(txn))?; } Some(EventReference::Addr(_ea)) => { // will only work if we already have it... yuck. @@ -65,7 +65,7 @@ impl Storage { self.write_relationship1( reacted_to_id, // event reacted to event.id, // the reaction event id - Relationship::Reaction(event.pubkey, reaction), + Relationship1::Reaction(event.pubkey, reaction), Some(txn), )?; } @@ -77,7 +77,7 @@ impl Storage { self.write_relationship1( reacted_to_id, // event reacted to event.id, // the reaction event id - Relationship::Reaction(event.pubkey, reaction), + Relationship1::Reaction(event.pubkey, reaction), Some(txn), )?; } @@ -93,7 +93,7 @@ impl Storage { self.write_relationship1( deleted_event_id, event.id, - Relationship::Deletion(reason.clone()), + Relationship1::Deletion(reason.clone()), Some(txn), )?; } @@ -103,7 +103,7 @@ impl Storage { self.write_relationship1( deleted_event_id, event.id, - Relationship::Deletion(reason.clone()), + Relationship1::Deletion(reason.clone()), Some(txn), )?; } @@ -116,7 +116,7 @@ impl Storage { self.write_relationship1( zapdata.id, event.id, - Relationship::ZapReceipt(event.pubkey, zapdata.amount), + Relationship1::ZapReceipt(event.pubkey, zapdata.amount), Some(txn), )?; } diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 953f7cbb5..b905df853 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -42,7 +42,7 @@ use crate::globals::GLOBALS; use crate::people::{Person, PersonList}; use crate::person_relay::PersonRelay; use crate::profile::Profile; -use crate::relationship::Relationship; +use crate::relationship::{RelationshipByAddr, RelationshipById}; use crate::relay::Relay; use gossip_relay_picker::Direction; use heed::types::UnalignedSlice; @@ -191,8 +191,6 @@ impl Storage { let _ = self.db_hashtags()?; let _ = self.db_people()?; let _ = self.db_person_relays()?; - let _ = self.db_relationships()?; - let _ = self.db_reprel()?; let _ = self.db_relationships_by_id()?; let _ = self.db_relationships_by_addr()?; let _ = self.db_relays()?; @@ -268,16 +266,6 @@ impl Storage { self.db_person_relays1() } - #[inline] - pub(crate) fn db_relationships(&self) -> Result { - self.db_relationships1() - } - - #[inline] - pub(crate) fn db_reprel(&self) -> Result { - self.db_reprel1() - } - #[inline] pub(crate) fn db_relationships_by_addr(&self) -> Result { self.db_relationships_by_addr1() @@ -359,19 +347,6 @@ impl Storage { Ok(self.db_event_tag_index()?.len(&txn)?) } - /// The number of records in the relationships table - pub fn get_relationships_len(&self) -> Result { - let txn = self.env.read_txn()?; - Ok(self.db_relationships()?.len(&txn)?) - } - - /// The number of records in the reprel table - #[inline] - pub fn get_reprel_len(&self) -> Result { - let txn = self.env.read_txn()?; - Ok(self.db_reprel()?.len(&txn)?) - } - /// The number of records in the relationships_by_addr table #[inline] pub fn get_relationships_by_addr_len(&self) -> Result { @@ -475,7 +450,7 @@ impl Storage { // Delete from relationships // (unfortunately because of the 2nd Id in the tag, we have to scan the whole thing) let mut deletions: Vec> = Vec::new(); - for result in self.db_relationships()?.iter(&txn)? { + for result in self.db_relationships_by_id()?.iter(&txn)? { let (key, _val) = result?; let id = Id(key[0..32].try_into()?); if ids.contains(&id) { @@ -489,7 +464,7 @@ impl Storage { } tracing::info!("PRUNE: deleting {} relationships", deletions.len()); for deletion in deletions.drain(..) { - self.db_relationships()?.delete(&mut txn, &deletion)?; + self.db_relationships_by_id()?.delete(&mut txn, &deletion)?; } // delete from events @@ -1279,7 +1254,7 @@ impl Storage { let start_key: &[u8] = id.as_slice(); - for result in self.db_relationships()?.prefix_iter(txn, start_key)? { + for result in self.db_relationships_by_id()?.prefix_iter(txn, start_key)? { let (_key, val) = result?; deletions.push(val.to_owned()); } @@ -1287,7 +1262,7 @@ impl Storage { // actual deletion done in second pass // (deleting during interation does not work in LMDB) for deletion in deletions.drain(..) { - self.db_relationships()?.delete(txn, &deletion)?; + self.db_relationships_by_id()?.delete(txn, &deletion)?; } } @@ -1795,14 +1770,14 @@ impl Storage { /// The second Id relates to the first Id, /// e.g. related replies to id, or related deletes id #[inline] - pub(crate) fn write_relationship<'a>( + pub(crate) fn write_relationship_by_id<'a>( &'a self, id: Id, related: Id, - relationship: Relationship, + relationship_by_id: RelationshipById, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - self.write_relationship1(id, related, relationship, rw_txn) + self.write_relationship_by_id1(id, related, relationship_by_id, rw_txn) } /// Find relationships belonging to the given event @@ -1810,26 +1785,29 @@ impl Storage { /// The found Ids relates to the passed in Id, /// e.g. result id replies to id, or result id deletes id #[inline] - pub fn find_relationships(&self, id: Id) -> Result, Error> { - self.find_relationships1(id) + pub fn find_relationships_by_id(&self, id: Id) -> Result, Error> { + self.find_relationships_by_id1(id) } /// Write a relationship between an event and an EventAddr (replaceable) #[inline] - pub(crate) fn write_reprel<'a>( + pub(crate) fn write_relationship_by_addr<'a>( &'a self, addr: EventAddr, related: Id, - relationship: Relationship, + relationship_by_addr: RelationshipByAddr, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - self.write_reprel1(addr, related, relationship, rw_txn) + self.write_relationship_by_addr1(addr, related, relationship_by_addr, rw_txn) } /// Find relationships belonging to the given event to replaceable events #[inline] - pub fn find_reprels(&self, addr: &EventAddr) -> Result, Error> { - self.find_reprels1(addr) + pub fn find_relationships_by_addr( + &self, + addr: &EventAddr, + ) -> Result, Error> { + self.find_relationships_by_addr1(addr) } /// Get replies to the given event @@ -1846,10 +1824,10 @@ impl Storage { pub fn get_non_replaceable_replies(&self, id: Id) -> Result, Error> { Ok(self - .find_relationships(id)? + .find_relationships_by_id(id)? .iter() .filter_map(|(id, rel)| { - if *rel == Relationship::Reply { + if *rel == RelationshipById::Reply { Some(*id) } else { None @@ -1860,10 +1838,10 @@ impl Storage { pub fn get_replaceable_replies(&self, addr: &EventAddr) -> Result, Error> { Ok(self - .find_reprels(addr)? + .find_relationships_by_addr(addr)? .iter() .filter_map(|(id, rel)| { - if *rel == Relationship::Reply { + if *rel == RelationshipByAddr::Reply { Some(*id) } else { None @@ -1882,10 +1860,10 @@ impl Storage { // Collect up to one reaction per pubkey let mut phase1: HashMap = HashMap::new(); - for (_, rel) in self.find_relationships(id)? { - if let Relationship::Reaction(pubkey, reaction) = rel { + for (_, rel) in self.find_relationships_by_id(id)? { + if let RelationshipById::Reaction { by, reaction } = rel { if let Some(target_event) = &maybe_target_event { - if target_event.pubkey == pubkey { + if target_event.pubkey == by { // Do not let people like their own post continue; } @@ -1895,8 +1873,8 @@ impl Storage { } else { '+' }; - phase1.insert(pubkey, symbol); - if Some(pubkey) == GLOBALS.signer.public_key() { + phase1.insert(by, symbol); + if Some(by) == GLOBALS.signer.public_key() { self_already_reacted = true; } } @@ -1919,28 +1897,20 @@ impl Storage { /// Get the zap total of a given event pub fn get_zap_total(&self, id: Id) -> Result { let mut total = MilliSatoshi(0); - for (_, rel) in self.find_relationships(id)? { - if let Relationship::ZapReceipt(_pk, millisats) = rel { - total = total + millisats; + for (_, rel) in self.find_relationships_by_id(id)? { + if let RelationshipById::ZapReceipt { by: _, amount } = rel { + total = total + amount; } } Ok(total) } /// Get whether an event was deleted, and if so the optional reason - pub fn get_deletion(&self, id: Id) -> Result, Error> { - for (target_id, rel) in self.find_relationships(id)? { - if let Relationship::Deletion(deletion) = rel { - if let Some(delete_event) = self.read_event(id)? { - if let Some(target_event) = self.read_event(target_id)? { - // Only if the authors match - if target_event.pubkey == delete_event.pubkey { - return Ok(Some(deletion)); - } - } else { - // presume the authors will match for now - return Ok(Some(deletion)); - } + pub fn get_deletion(&self, maybe_deleted_event: &Event) -> Result, Error> { + for (_id, rel) in self.find_relationships_by_id(maybe_deleted_event.id)? { + if let RelationshipById::Deletion { by, reason } = rel { + if maybe_deleted_event.pubkey == by { + return Ok(Some(reason)); } } } @@ -1960,10 +1930,20 @@ impl Storage { // replies to match event.replies_to() { Some(EventReference::Id(id, _, _)) => { - self.write_relationship(id, event.id, Relationship::Reply, Some(txn))?; + self.write_relationship_by_id( + id, + event.id, + RelationshipById::Reply, + Some(txn), + )?; } Some(EventReference::Addr(ea)) => { - self.write_reprel(ea, event.id, Relationship::Reply, Some(txn))?; + self.write_relationship_by_addr( + ea, + event.id, + RelationshipByAddr::Reply, + Some(txn), + )?; } None => (), } @@ -1973,10 +1953,13 @@ impl Storage { if let Some(reacted_to_event) = self.read_event(reacted_to_id)? { // Only if they are different people (no liking your own posts) if reacted_to_event.pubkey != event.pubkey { - self.write_relationship( + self.write_relationship_by_id( reacted_to_id, // event reacted to event.id, // the reaction event id - Relationship::Reaction(event.pubkey, reaction), + RelationshipById::Reaction { + by: event.pubkey, + reaction, + }, Some(txn), )?; } @@ -1986,10 +1969,13 @@ impl Storage { // We filter bad ones when reading them back too, so even if this // turns out to be a reaction by the author, they can't like // their own post - self.write_relationship( + self.write_relationship_by_id( reacted_to_id, // event reacted to event.id, // the reaction event id - Relationship::Reaction(event.pubkey, reaction), + RelationshipById::Reaction { + by: event.pubkey, + reaction, + }, Some(txn), )?; invalidate.push(reacted_to_id); @@ -2015,10 +2001,13 @@ impl Storage { } } if !deleted { - self.write_relationship( + self.write_relationship_by_id( *id, event.id, - Relationship::Deletion(reason.clone()), + RelationshipById::Deletion { + by: event.pubkey, + reason: reason.clone(), + }, Some(txn), )?; } @@ -2040,10 +2029,13 @@ impl Storage { } } if !deleted { - self.write_reprel( + self.write_relationship_by_addr( ea.clone(), event.id, - Relationship::Deletion(reason.clone()), + RelationshipByAddr::Deletion { + by: event.pubkey, + reason: reason.clone(), + }, Some(txn), )?; } @@ -2055,10 +2047,13 @@ impl Storage { // zaps match event.zaps() { Ok(Some(zapdata)) => { - self.write_relationship( + self.write_relationship_by_id( zapdata.id, event.id, - Relationship::ZapReceipt(event.pubkey, zapdata.amount), + RelationshipById::ZapReceipt { + by: event.pubkey, + amount: zapdata.amount, + }, Some(txn), )?; From ba2dce3cb298eb148e9e37fbabc10f97f0fdef04 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 07:28:03 +1300 Subject: [PATCH 04/81] Strip some old relationship database code (keep what is needed for migrations) --- gossip-lib/src/storage/relationships1.rs | 16 +---- gossip-lib/src/storage/reprel1.rs | 85 ------------------------ 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/gossip-lib/src/storage/relationships1.rs b/gossip-lib/src/storage/relationships1.rs index 97481fe48..b6f9e88bc 100644 --- a/gossip-lib/src/storage/relationships1.rs +++ b/gossip-lib/src/storage/relationships1.rs @@ -4,7 +4,7 @@ use crate::storage::{RawDatabase, Storage}; use heed::types::UnalignedSlice; use heed::RwTxn; use nostr_types::Id; -use speedy::{Readable, Writable}; +use speedy::Writable; use std::sync::Mutex; // Id:Id -> Relationship1 @@ -78,18 +78,4 @@ impl Storage { Ok(()) } - - pub(crate) fn find_relationships1(&self, id: Id) -> Result, Error> { - let start_key = id.as_slice(); - let txn = self.env.read_txn()?; - let iter = self.db_relationships1()?.prefix_iter(&txn, start_key)?; - let mut output: Vec<(Id, Relationship1)> = Vec::new(); - for result in iter { - let (key, val) = result?; - let id2 = Id(key[32..64].try_into().unwrap()); - let relationship = Relationship1::read_from_buffer(val)?; - output.push((id2, relationship)); - } - Ok(output) - } } diff --git a/gossip-lib/src/storage/reprel1.rs b/gossip-lib/src/storage/reprel1.rs index 3cd461526..a9ab55669 100644 --- a/gossip-lib/src/storage/reprel1.rs +++ b/gossip-lib/src/storage/reprel1.rs @@ -1,10 +1,6 @@ use crate::error::Error; -use crate::storage::types::Relationship1; use crate::storage::{RawDatabase, Storage}; -use heed::RwTxn; use heed::{types::UnalignedSlice, DatabaseFlags}; -use nostr_types::{EventAddr, Id}; -use speedy::{Readable, Writable}; use std::sync::Mutex; // Kind:Pubkey:d-tag -> Relationship1:Id @@ -43,85 +39,4 @@ impl Storage { } } } - - pub(crate) fn write_reprel1<'a>( - &'a self, - addr: EventAddr, - related: Id, - relationship: Relationship1, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result<(), Error> { - let key = reprel1_into_key(&addr); - let value = reprel1_into_value(relationship, related)?; - let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - self.db_reprel1()?.put(txn, &key, &value)?; - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - - Ok(()) - } - - pub(crate) fn find_reprels1( - &self, - addr: &EventAddr, - ) -> Result, Error> { - let key = reprel1_into_key(addr); - let txn = self.env.read_txn()?; - let iter = match self.db_reprel1()?.get_duplicates(&txn, &key)? { - Some(iter) => iter, - None => return Ok(vec![]), - }; - let mut output: Vec<(Id, Relationship1)> = Vec::new(); - for result in iter { - let (_key, val) = result?; - let (rel, id) = reprel1_from_value(val)?; - output.push((id, rel)); - } - Ok(output) - } -} - -fn reprel1_into_key(ea: &EventAddr) -> Vec { - let u: u32 = ea.kind.into(); - let mut key: Vec = u.to_be_bytes().as_slice().to_owned(); - key.extend(ea.author.as_bytes()); - key.extend(ea.d.as_bytes()); - key -} - -/* -fn reprel1_from_key(key: &[u8]) -> Result { - let u = u32::from_be_bytes(key[..4].try_into().unwrap()); - let kind: EventKind = u.into(); - let pubkey: PublicKey = PublicKey::from_bytes(&key[4..4+32], true)?; - let d: String = String::from_utf8_lossy(&key[4+32..]).to_string(); - Ok(EventAddr { - d, - relays: vec![], - kind, - author: pubkey - }) -} - */ - -fn reprel1_into_value(relationship: Relationship1, id: Id) -> Result, Error> { - let mut value: Vec = relationship.write_to_vec()?; - value.extend(id.as_slice()); - Ok(value) -} - -fn reprel1_from_value(value: &[u8]) -> Result<(Relationship1, Id), Error> { - let (result, len) = Relationship1::read_with_length_from_buffer(value); - let relationship = result?; - let id = Id(value[len..len + 32].try_into().unwrap()); - Ok((relationship, id)) } From f4bda3a7775f4e6acb5d4f4d6a376988d7ea4fbd Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 2 Dec 2023 12:07:48 +1300 Subject: [PATCH 05/81] Rework processing of relationships --- gossip-lib/src/process.rs | 425 ++++++++++++++++++++++- gossip-lib/src/storage/migrations/m17.rs | 2 +- gossip-lib/src/storage/mod.rs | 161 --------- 3 files changed, 421 insertions(+), 167 deletions(-) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index dad047ef5..996136146 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -4,10 +4,12 @@ use crate::filter::EventFilterAction; use crate::globals::GLOBALS; use crate::people::PersonList; use crate::person_relay::PersonRelay; +use crate::relationship::{RelationshipByAddr, RelationshipById}; use async_recursion::async_recursion; +use heed::RwTxn; use nostr_types::{ - Event, EventKind, EventReference, Metadata, NostrBech32, PublicKey, RelayUrl, SimpleRelayList, - Tag, Unixtime, + Event, EventAddr, EventKind, EventReference, Id, Metadata, NostrBech32, PublicKey, RelayUrl, + SimpleRelayList, Tag, Unixtime, }; use std::sync::atomic::Ordering; @@ -187,9 +189,7 @@ pub async fn process_new_event( } // Save event relationships (whether from a relay or not) - let invalid_ids = GLOBALS - .storage - .process_relationships_of_event(event, None)?; + let invalid_ids = process_relationships_of_event(event, None)?; // Invalidate UI events indicated by those relationships GLOBALS.ui_notes_to_invalidate.write().extend(&invalid_ids); @@ -389,3 +389,418 @@ async fn process_somebody_elses_contact_list(event: &Event) -> Result<(), Error> Ok(()) } + +/// Process relationships of an event. +/// This returns IDs that should be UI invalidated (must be redrawn) +pub(crate) fn process_relationships_of_event<'a>( + event: &Event, + rw_txn: Option<&mut RwTxn<'a>>, +) -> Result, Error> { + let mut invalidate: Vec = Vec::new(); + + let mut f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + // replies to + match event.replies_to() { + Some(EventReference::Id(id, _, _)) => { + GLOBALS.storage.write_relationship_by_id( + id, + event.id, + RelationshipById::Reply, + Some(txn), + )?; + } + Some(EventReference::Addr(ea)) => { + GLOBALS.storage.write_relationship_by_addr( + ea, + event.id, + RelationshipByAddr::Reply, + Some(txn), + )?; + } + None => (), + } + + // timestamps + if event.kind == EventKind::Timestamp { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::Timestamp, + Some(txn), + )?; + } + } + } + + // deletes + if let Some((vec, reason)) = event.deletes() { + for er in vec.iter() { + match er { + EventReference::Id(id, _, _) => { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::Deletion { + by: event.pubkey, + reason: reason.clone(), + }, + Some(txn), + )?; + + // Actually delete at this point in some cases + if let Some(deleted_event) = GLOBALS.storage.read_event(*id)? { + invalidate.push(deleted_event.id); + if deleted_event.pubkey != event.pubkey { + // No further processing if authors do not match + continue; + } + if !deleted_event.kind.is_feed_displayable() { + // Otherwise actually delete (PITA to do otherwise) + GLOBALS.storage.delete_event(deleted_event.id, Some(txn))?; + } + } + } + EventReference::Addr(ea) => { + GLOBALS.storage.write_relationship_by_addr( + ea.clone(), + event.id, + RelationshipByAddr::Deletion { + by: event.pubkey, + reason: reason.clone(), + }, + Some(txn), + )?; + + // Actually delete at this point in some cases + if let Some(deleted_event) = GLOBALS + .storage + .get_replaceable_event(ea.kind, ea.author, &ea.d)? + { + invalidate.push(deleted_event.id); + if deleted_event.pubkey != event.pubkey { + // No further processing if authors do not match + continue; + } + if !deleted_event.kind.is_feed_displayable() { + // Otherwise actually delete (PITA to do otherwise) + GLOBALS.storage.delete_event(deleted_event.id, Some(txn))?; + } + } + } + } + } + } + + // reacts to + if let Some((reacted_to_id, reaction, _maybe_url)) = event.reacts_to() { + // NOTE: reactions may precede the event they react to. So we cannot validate here. + GLOBALS.storage.write_relationship_by_id( + reacted_to_id, // event reacted to + event.id, // the reaction event id + RelationshipById::Reaction { + by: event.pubkey, + reaction, + }, + Some(txn), + )?; + invalidate.push(reacted_to_id); + } + + // labels + if event.kind == EventKind::Label { + // Get the label from the "l" tag + let mut label = ""; + let mut namespace = ""; + for t in &event.tags { + if let Tag::Other { tag, data } = t { + if tag == "l" && !data.is_empty() { + label = &data[0]; + if data.len() >= 2 { + namespace = &data[1]; + } + } + } + } + + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::Labels { + label: label.to_owned(), + namespace: namespace.to_owned(), + }, + Some(txn), + )?; + } + } + } + + // ListMutesThread + if event.kind == EventKind::MuteList { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::ListMutesThread, + Some(txn), + )?; + } + } + } + + // ListPins + if event.kind == EventKind::PinList { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::ListPins, + Some(txn), + )?; + } + } + } + + // ListBookmarks + if event.kind == EventKind::BookmarkList { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::ListBookmarks, + Some(txn), + )?; + } + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::ListBookmarks, + Some(txn), + )?; + } + } + } + } + + // BookmarkSets + if event.kind == EventKind::BookmarkSets { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::ListBookmarks, + Some(txn), + )?; + } + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::ListBookmarks, + Some(txn), + )?; + } + } + } + } + + // CurationSets + if event.kind == EventKind::CurationSets { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::Curation, + Some(txn), + )?; + } + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::Curation, + Some(txn), + )?; + } + } + } + } + + if event.kind == EventKind::LiveChatMessage { + for tag in &event.tags { + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::LiveChatMessage, + Some(txn), + )?; + } + } + } + } + + if event.kind == EventKind::BadgeAward { + for tag in &event.tags { + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::BadgeAward, + Some(txn), + )?; + } + } + } + } + + if event.kind == EventKind::HandlerRecommendation { + for tag in &event.tags { + if let Tag::Address { + kind, pubkey, d, .. + } = tag + { + if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) { + let event_addr = EventAddr { + d: d.to_owned(), + relays: vec![], + kind: *kind, + author: pubkey, + }; + GLOBALS.storage.write_relationship_by_addr( + event_addr, + event.id, + RelationshipByAddr::HandlerRecommendation, + Some(txn), + )?; + } + } + } + } + + if event.kind == EventKind::Reporting { + for tag in &event.tags { + if let Tag::Event { + id, + recommended_relay_url: Some(rru), + .. + } = tag + { + let report = &rru.0; + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::Reports(report.to_owned()), + Some(txn), + )?; + } + } + } + + // zaps + match event.zaps() { + Ok(Some(zapdata)) => { + GLOBALS.storage.write_relationship_by_id( + zapdata.id, + event.id, + RelationshipById::ZapReceipt { + by: event.pubkey, + amount: zapdata.amount, + }, + Some(txn), + )?; + + invalidate.push(zapdata.id); + } + Err(e) => tracing::error!("Invalid zap receipt: {}", e), + _ => {} + } + + // JobResult + if event.kind.is_job_result() { + for tag in &event.tags { + if let Tag::Event { id, .. } = tag { + GLOBALS.storage.write_relationship_by_id( + *id, + event.id, + RelationshipById::JobResult, + Some(txn), + )?; + } + } + } + + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = GLOBALS.storage.get_write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(invalidate) +} diff --git a/gossip-lib/src/storage/migrations/m17.rs b/gossip-lib/src/storage/migrations/m17.rs index ba248a8ea..f06e2faee 100644 --- a/gossip-lib/src/storage/migrations/m17.rs +++ b/gossip-lib/src/storage/migrations/m17.rs @@ -31,7 +31,7 @@ impl Storage { for result in self.db_events()?.iter(&loop_txn)? { let (_key, val) = result?; let event = EventV2::read_from_buffer(val)?; - self.process_relationships_of_event(&event, Some(txn))?; + crate::process::process_relationships_of_event(&event, Some(txn))?; } Ok(()) } diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index b905df853..e557360a8 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1917,167 +1917,6 @@ impl Storage { Ok(None) } - /// Process relationships of an event. - /// This returns IDs that should be UI invalidated (must be redrawn) - pub fn process_relationships_of_event<'a>( - &'a self, - event: &Event, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result, Error> { - let mut invalidate: Vec = Vec::new(); - - let mut f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - // replies to - match event.replies_to() { - Some(EventReference::Id(id, _, _)) => { - self.write_relationship_by_id( - id, - event.id, - RelationshipById::Reply, - Some(txn), - )?; - } - Some(EventReference::Addr(ea)) => { - self.write_relationship_by_addr( - ea, - event.id, - RelationshipByAddr::Reply, - Some(txn), - )?; - } - None => (), - } - - // reacts to - if let Some((reacted_to_id, reaction, _maybe_url)) = event.reacts_to() { - if let Some(reacted_to_event) = self.read_event(reacted_to_id)? { - // Only if they are different people (no liking your own posts) - if reacted_to_event.pubkey != event.pubkey { - self.write_relationship_by_id( - reacted_to_id, // event reacted to - event.id, // the reaction event id - RelationshipById::Reaction { - by: event.pubkey, - reaction, - }, - Some(txn), - )?; - } - invalidate.push(reacted_to_id); - } else { - // Store the reaction to the event we dont have yet. - // We filter bad ones when reading them back too, so even if this - // turns out to be a reaction by the author, they can't like - // their own post - self.write_relationship_by_id( - reacted_to_id, // event reacted to - event.id, // the reaction event id - RelationshipById::Reaction { - by: event.pubkey, - reaction, - }, - Some(txn), - )?; - invalidate.push(reacted_to_id); - } - } - - // deletes - if let Some((vec, reason)) = event.deletes() { - for er in vec.iter() { - match er { - EventReference::Id(id, _, _) => { - let mut deleted = false; - if let Some(deleted_event) = self.read_event(*id)? { - invalidate.push(deleted_event.id); - if deleted_event.pubkey != event.pubkey { - // No further processing if authors do not match - continue; - } - if !deleted_event.kind.is_feed_displayable() { - // Otherwise actually delete (PITA to do otherwise) - self.delete_event(deleted_event.id, Some(txn))?; - deleted = true; - } - } - if !deleted { - self.write_relationship_by_id( - *id, - event.id, - RelationshipById::Deletion { - by: event.pubkey, - reason: reason.clone(), - }, - Some(txn), - )?; - } - } - EventReference::Addr(ea) => { - let mut deleted = false; - if let Some(deleted_event) = - self.get_replaceable_event(ea.kind, ea.author, &ea.d)? - { - invalidate.push(deleted_event.id); - if deleted_event.pubkey != event.pubkey { - // No further processing if authors do not match - continue; - } - if !deleted_event.kind.is_feed_displayable() { - // Otherwise actually delete (PITA to do otherwise) - self.delete_event(deleted_event.id, Some(txn))?; - deleted = true; - } - } - if !deleted { - self.write_relationship_by_addr( - ea.clone(), - event.id, - RelationshipByAddr::Deletion { - by: event.pubkey, - reason: reason.clone(), - }, - Some(txn), - )?; - } - } - } - } - } - - // zaps - match event.zaps() { - Ok(Some(zapdata)) => { - self.write_relationship_by_id( - zapdata.id, - event.id, - RelationshipById::ZapReceipt { - by: event.pubkey, - amount: zapdata.amount, - }, - Some(txn), - )?; - - invalidate.push(zapdata.id); - } - Err(e) => tracing::error!("Invalid zap receipt: {}", e), - _ => {} - } - - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - - Ok(invalidate) - } - /// Write a person record #[inline] pub fn write_person<'a>( From 756ed3218e27ff6e1a54e1477b784c0de2a32488 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 07:33:03 +1300 Subject: [PATCH 06/81] Storage::rebuild_relationships --- gossip-lib/src/storage/mod.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index e557360a8..264fd5b43 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -2352,4 +2352,37 @@ impl Storage { map.remove(&list); self.write_person_lists(pubkey, map, rw_txn) } + + /// Rebuild relationships + pub fn rebuild_relationships<'a>( + &'a self, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + tracing::info!("Rebuilding relationships..."); + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + // Iterate through all events + let loop_txn = self.env.read_txn()?; + for result in self.db_events()?.iter(&loop_txn)? { + let (_key, val) = result?; + let event = Event::read_from_buffer(val)?; + crate::process::process_relationships_of_event(&event, Some(txn))?; + } + self.set_flag_rebuild_relationships_needed(false, Some(txn))?; + Ok(()) + }; + + match rw_txn { + Some(txn) => { + f(txn)?; + } + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } } From 96819fb21e2f183485fd7667e2f2ac6475094e81 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 07:43:48 +1300 Subject: [PATCH 07/81] storage: cleanup: def_flag!() macro --- gossip-bin/src/ui/mod.rs | 2 +- gossip-bin/src/ui/wizard/follow_people.rs | 6 +- gossip-bin/src/ui/wizard/mod.rs | 2 +- gossip-bin/src/ui/wizard/welcome_gossip.rs | 2 +- gossip-bin/src/ui/wizard/wizard_state.rs | 2 +- gossip-lib/src/storage/mod.rs | 121 ++++++++------------- 6 files changed, 51 insertions(+), 84 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index fab4991f3..3bd3268a5 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -572,7 +572,7 @@ impl GossipUi { // Possibly enter the wizard instead let mut wizard_state: WizardState = Default::default(); - let wizard_complete = GLOBALS.storage.read_wizard_complete(); + let wizard_complete = GLOBALS.storage.get_flag_wizard_complete(); if !wizard_complete { if let Some(wp) = wizard::start_wizard_page(&mut wizard_state) { start_page = Page::Wizard(wp); diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index 8e7b95e51..2ca728009 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -146,7 +146,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr .to_overlord .send(ToOverlordMessage::PushPersonList(PersonList::Followed)); - let _ = GLOBALS.storage.write_wizard_complete(true, None); + let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); } @@ -156,7 +156,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr label = label.color(app.theme.accent_color()); } if ui.button(label).clicked() { - let _ = GLOBALS.storage.write_wizard_complete(true, None); + let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); } } else { @@ -164,7 +164,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr let mut label = RichText::new(" > Finish"); label = label.color(app.theme.accent_color()); if ui.button(label).clicked() { - let _ = GLOBALS.storage.write_wizard_complete(true, None); + let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); } } diff --git a/gossip-bin/src/ui/wizard/mod.rs b/gossip-bin/src/ui/wizard/mod.rs index 8b1c73602..6c27e34df 100644 --- a/gossip-bin/src/ui/wizard/mod.rs +++ b/gossip-bin/src/ui/wizard/mod.rs @@ -198,7 +198,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ui.add_space(20.0); if wp != WizardPage::FollowPeople { if ui.button(" X Exit this Wizard").clicked() { - let _ = GLOBALS.storage.write_wizard_complete(true, None); + let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); } } diff --git a/gossip-bin/src/ui/wizard/welcome_gossip.rs b/gossip-bin/src/ui/wizard/welcome_gossip.rs index e1868f46d..98ab40cb6 100644 --- a/gossip-bin/src/ui/wizard/welcome_gossip.rs +++ b/gossip-bin/src/ui/wizard/welcome_gossip.rs @@ -40,7 +40,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr if ui.button(" > Just follow people (no account)").clicked() { app.wizard_state.new_user = false; app.wizard_state.follow_only = true; - let _ = GLOBALS.storage.write_following_only(true, None); + let _ = GLOBALS.storage.set_flag_following_only(true, None); app.page = Page::Wizard(WizardPage::FollowPeople); } } diff --git a/gossip-bin/src/ui/wizard/wizard_state.rs b/gossip-bin/src/ui/wizard/wizard_state.rs index 192e5c891..0fb293436 100644 --- a/gossip-bin/src/ui/wizard/wizard_state.rs +++ b/gossip-bin/src/ui/wizard/wizard_state.rs @@ -52,7 +52,7 @@ impl Default for WizardState { } impl WizardState { pub fn update(&mut self) { - self.follow_only = GLOBALS.storage.read_following_only(); + self.follow_only = GLOBALS.storage.get_flag_following_only(); self.pubkey = GLOBALS.signer.public_key(); self.has_private_key = GLOBALS.signer.is_ready(); diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 264fd5b43..a2f5e9ffd 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -122,6 +122,48 @@ macro_rules! def_setting { }; } +macro_rules! def_flag { + ($field:ident, $string:literal, $default:expr) => { + paste! { + pub fn []<'a>( + &'a self, + $field: bool, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let bytes = $field.write_to_vec()?; + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + Ok(self.general.put(txn, $string, &bytes)?) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } + + pub fn [](&self) -> bool { + let txn = match self.env.read_txn() { + Ok(txn) => txn, + Err(_) => return $default, + }; + + match self.general.get(&txn, $string) { + Err(_) => $default, + Ok(None) => $default, + Ok(Some(bytes)) => bool::read_from_buffer(bytes).unwrap_or($default), + } + } + } + }; +} + type RawDatabase = Database, UnalignedSlice>; /// The LMDB storage engine. @@ -607,83 +649,8 @@ impl Storage { Ok(lists.get(&list).copied()) } - /// Write a flag, whether the user is only following people with no account (or not) - pub fn write_following_only<'a>( - &'a self, - following_only: bool, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result<(), Error> { - let bytes = following_only.write_to_vec()?; - - let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - self.general.put(txn, b"following_only", &bytes)?; - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - - Ok(()) - } - - /// Read a flag, whether the user is only following people with no account (or not) - pub fn read_following_only(&self) -> bool { - let txn = match self.env.read_txn() { - Ok(txn) => txn, - Err(_) => return false, - }; - - match self.general.get(&txn, b"following_only") { - Err(_) => false, - Ok(None) => false, - Ok(Some(bytes)) => bool::read_from_buffer(bytes).unwrap_or(false), - } - } - - /// Write a flag, whether the onboarding wizard has completed - pub fn write_wizard_complete<'a>( - &'a self, - wizard_complete: bool, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result<(), Error> { - let bytes = wizard_complete.write_to_vec()?; - - let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - self.general.put(txn, b"wizard_complete", &bytes)?; - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - - Ok(()) - } - - /// Read a flag, whether the onboarding wizard has completed - pub fn read_wizard_complete(&self) -> bool { - let txn = match self.env.read_txn() { - Ok(txn) => txn, - Err(_) => return false, - }; - - match self.general.get(&txn, b"wizard_complete") { - Err(_) => false, - Ok(None) => false, - Ok(Some(bytes)) => bool::read_from_buffer(bytes).unwrap_or(false), - } - } + def_flag!(following_only, b"following_only", false); + def_flag!(wizard_complete, b"wizard_complete", false); // Settings ---------------------------------------------------------- From 82eea24f512bd079d60ab2a2f08221c023bbd355 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 07:45:10 +1300 Subject: [PATCH 08/81] storage: flag: rebuild_relationships_needed --- gossip-lib/src/storage/mod.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index a2f5e9ffd..cebda6668 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -651,6 +651,11 @@ impl Storage { def_flag!(following_only, b"following_only", false); def_flag!(wizard_complete, b"wizard_complete", false); + def_flag!( + rebuild_relationships_needed, + b"rebuild_relationships_needed", + false + ); // Settings ---------------------------------------------------------- From ee05f4f04d2e1d810bcd6d92cf78118d0722edc4 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 30 Nov 2023 16:00:00 +1300 Subject: [PATCH 09/81] SETUP_AND_SHUTDOWN process doc --- docs/SETUP_AND_SHUTDOWN.md | 48 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 docs/SETUP_AND_SHUTDOWN.md diff --git a/docs/SETUP_AND_SHUTDOWN.md b/docs/SETUP_AND_SHUTDOWN.md new file mode 100644 index 000000000..eef505ee1 --- /dev/null +++ b/docs/SETUP_AND_SHUTDOWN.md @@ -0,0 +1,48 @@ +# Setup and Shutdown + +This may change, but this is approximately the order of events. + +- bin::main() + - setup logging + - lib::init() + - storage::init() + - trigger database into existence + - migrate + - signer::init() + - load from settings in storage + - deletation init + - setup wait-for-login state + - setup async runtime + - optionally handle command-line command and exit, else + - spawn (two threads as below) + +- spawn-thread + - lib::run() + - overlord::run() + - maybe wait-for-login (the UI has to do it) + - start fetcher + - start People tasks + - start relay picker + - pick relays + - subscribe discover + - subscribe outbox + - subscribe inbox + - loop + - Get and handle messages + - or if shutdown variable is set, exit this loop + - storage::sync() + - set shutdown variable + - message minions to shutdown + - wait for minions to shutdown + - end of spawn-thread + +- main-thread + - ui::run() + - Setup and run the UI + - if wait-for-login, prompt for password and login + - once logged in, indicate such so the overlord can start, and run UI as normal + - If shutdown variable is set, exit + - Signal overlord to shutdown + - Wait for spawn-thread to end + - lib::shutdown() + - storage::sync() From 9c400a37ca0b8f0e90e4363aca1c75d56d9c9353 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 30 Nov 2023 14:38:29 +1300 Subject: [PATCH 10/81] Wait for login on startup --- gossip-bin/src/main.rs | 8 +++++++ gossip-bin/src/ui/mod.rs | 44 ++++++++++++++++++++++++++++++++++ gossip-bin/src/ui/you/mod.rs | 10 +++----- gossip-lib/src/globals.rs | 8 ++++++- gossip-lib/src/lib.rs | 8 +++++++ gossip-lib/src/overlord/mod.rs | 12 ++++++++++ gossip-lib/src/signer.rs | 6 +++++ 7 files changed, 88 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/main.rs b/gossip-bin/src/main.rs index dfb65c6bf..ea0e951f1 100644 --- a/gossip-bin/src/main.rs +++ b/gossip-bin/src/main.rs @@ -11,6 +11,7 @@ mod ui; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::Error; use gossip_lib::GLOBALS; +use std::sync::atomic::Ordering; use std::{env, thread}; use tracing_subscriber::filter::{EnvFilter, LevelFilter}; @@ -74,6 +75,13 @@ fn main() -> Result<(), Error> { tracing::error!("{}", e); } + // Make sure the overlord knows to shut down + GLOBALS.shutting_down.store(true, Ordering::Relaxed); + + // Make sure the overlord isn't stuck on waiting for login + GLOBALS.wait_for_login.store(false, Ordering::Relaxed); + GLOBALS.wait_for_login_notify.notify_one(); + // Tell the async parties to close down if let Err(e) = initiate_shutdown() { tracing::error!("{}", e); diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 3bd3268a5..2470f466a 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1177,6 +1177,11 @@ impl eframe::App for GossipUi { relays::entry_dialog(ctx, self); } + // If login is forced, it takes over + if GLOBALS.wait_for_login.load(Ordering::Relaxed) { + return force_login(self, ctx); + } + // Wizard does its own panels if let Page::Wizard(wp) = self.page { return wizard::update(self, ctx, frame, wp); @@ -1848,3 +1853,42 @@ impl GossipUi { }) } } + +fn force_login(app: &mut GossipUi, ctx: &Context) { + egui::CentralPanel::default() + .frame({ + let frame = egui::Frame::central_panel(&app.theme.get_style()); + frame.inner_margin(egui::Margin { + left: 20.0, + right: 10.0, + top: 10.0, + bottom: 0.0, + }) + }) + .show(ctx, |ui| { + ui.heading("Passphrase Needed"); + you::offer_unlock_priv_key(app, ui); + + ui.add_space(10.0); + ui.label("We need to rebuild some data which may require decrypting DMs and Giftwraps to rebuild properly. For this reason, you need to login before the data migration runs."); + + ui.add_space(15.0); + + /* + if ui.button("Skip").clicked() { + // Stop waiting for login + GLOBALS + .wait_for_login + .store(false, std::sync::atomic::Ordering::Relaxed); + GLOBALS.wait_for_login_notify.notify_one(); + } + */ + + ui.add_space(60.0); + ui.separator(); + ui.add_space(10.0); + + ui.label("In case you cannot login, here is your escape hatch:"); + you::offer_delete(app, ui); + }); +} diff --git a/gossip-bin/src/ui/you/mod.rs b/gossip-bin/src/ui/you/mod.rs index f6b73bfc8..58afd8184 100644 --- a/gossip-bin/src/ui/you/mod.rs +++ b/gossip-bin/src/ui/you/mod.rs @@ -174,17 +174,13 @@ pub(super) fn offer_unlock_priv_key(app: &mut GossipUi, ui: &mut Ui) { app.unlock_needs_focus = false; } if response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UnlockKey(app.password.clone())); + let _ = gossip_lib::Overlord::unlock_key(app.password.clone()); app.password.zeroize(); app.password = "".to_owned(); app.draft_needs_focus = true; } if ui.button("Unlock Private Key").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UnlockKey(app.password.clone())); + let _ = gossip_lib::Overlord::unlock_key(app.password.clone()); app.password.zeroize(); app.password = "".to_owned(); app.draft_needs_focus = true; @@ -423,7 +419,7 @@ fn offer_delete_or_import_pub_key(app: &mut GossipUi, ui: &mut Ui) { } } -fn offer_delete(app: &mut GossipUi, ui: &mut Ui) { +pub(super) fn offer_delete(app: &mut GossipUi, ui: &mut Ui) { ui.heading("DELETE This Identity"); ui.horizontal_wrapped(|ui| { diff --git a/gossip-lib/src/globals.rs b/gossip-lib/src/globals.rs index 110d9437a..3e7f5cf7b 100644 --- a/gossip-lib/src/globals.rs +++ b/gossip-lib/src/globals.rs @@ -17,7 +17,7 @@ use regex::Regex; use rhai::{Engine, AST}; use std::collections::HashSet; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize}; -use tokio::sync::{broadcast, mpsc, Mutex, RwLock}; +use tokio::sync::{broadcast, mpsc, Mutex, Notify, RwLock}; /// The state that a Zap is in (it moves through 5 states before it is complete) #[derive(Debug, Clone)] @@ -126,6 +126,10 @@ pub struct Globals { /// Filter pub(crate) filter_engine: Engine, pub(crate) filter: Option, + + // Wait for login + pub wait_for_login: AtomicBool, + pub wait_for_login_notify: Notify, } lazy_static! { @@ -181,6 +185,8 @@ lazy_static! { events_processed: AtomicU32::new(0), filter_engine, filter, + wait_for_login: AtomicBool::new(false), + wait_for_login_notify: Notify::new(), } }; } diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index fb17409f7..5f407dac3 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -155,6 +155,14 @@ pub fn init() -> Result<(), Error> { // Load delegation tag GLOBALS.delegation.load()?; + // If we have a key but have not unlocked it + if GLOBALS.signer.is_loaded() && !GLOBALS.signer.is_ready() { + // Indicate to the overlord and UI to wait for login before continuing + GLOBALS + .wait_for_login + .store(true, std::sync::atomic::Ordering::Relaxed); + } + Ok(()) } diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 046e60331..85ff79ad6 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -153,6 +153,18 @@ impl Overlord { } async fn run_inner(&mut self) -> Result<(), Error> { + // Maybe wait for UI login + if GLOBALS.wait_for_login.load(Ordering::Relaxed) { + tracing::error!("DEBUG: overlord waiting for login"); + GLOBALS.wait_for_login_notify.notified().await; + tracing::error!("DEBUG: overlord finished waiting for login"); + } + + // Check for shutdown (we might not have gotten a login) + if GLOBALS.shutting_down.load(Ordering::Relaxed) { + return Ok(()); + } + // Start the fetcher crate::fetcher::Fetcher::start()?; diff --git a/gossip-lib/src/signer.rs b/gossip-lib/src/signer.rs index a1877cda3..1cb650ca8 100644 --- a/gossip-lib/src/signer.rs +++ b/gossip-lib/src/signer.rs @@ -152,6 +152,12 @@ impl Signer { // Index any GiftWraps that weren't indexed due to not having a // private key ready GLOBALS.storage.index_unindexed_giftwraps()?; + + // Update wait for login condition + GLOBALS + .wait_for_login + .store(false, std::sync::atomic::Ordering::Relaxed); + GLOBALS.wait_for_login_notify.notify_one(); } Ok(()) From 1fb851e0c965cbf06f2ea0a7928c3f8c9d9a264f Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 08:01:08 +1300 Subject: [PATCH 11/81] Only wait for login on startup if we need to rebuild relationships; then rebuild them --- gossip-lib/src/lib.rs | 15 +++++++++------ gossip-lib/src/overlord/mod.rs | 5 +++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index 5f407dac3..eaaa98ac3 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -155,12 +155,15 @@ pub fn init() -> Result<(), Error> { // Load delegation tag GLOBALS.delegation.load()?; - // If we have a key but have not unlocked it - if GLOBALS.signer.is_loaded() && !GLOBALS.signer.is_ready() { - // Indicate to the overlord and UI to wait for login before continuing - GLOBALS - .wait_for_login - .store(true, std::sync::atomic::Ordering::Relaxed); + // If we need to rebuild relationships + if GLOBALS.storage.get_flag_rebuild_relationships_needed() { + // And we have a key but have not unlocked it + if GLOBALS.signer.is_loaded() && !GLOBALS.signer.is_ready() { + // Indicate to the overlord and UI to wait for login before continuing + GLOBALS + .wait_for_login + .store(true, std::sync::atomic::Ordering::Relaxed); + } } Ok(()) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 85ff79ad6..44470eb76 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -165,6 +165,11 @@ impl Overlord { return Ok(()); } + // If we need to rebuild relationships, do so now + if GLOBALS.storage.get_flag_rebuild_relationships_needed() { + GLOBALS.storage.rebuild_relationships(None)?; + } + // Start the fetcher crate::fetcher::Fetcher::start()?; From 2017aa45bd1b95551b8038e395a6ed929852a40d Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 2 Dec 2023 11:21:10 +1300 Subject: [PATCH 12/81] storage: migrate 18 - set flag to rebuild relationships Also we modified m17 to just set the flag too, so it doesn't rebuild twice --- gossip-lib/src/storage/migrations/m17.rs | 10 +----- gossip-lib/src/storage/migrations/m18.rs | 39 ++++++++++++++++++++++++ gossip-lib/src/storage/migrations/mod.rs | 5 ++- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 gossip-lib/src/storage/migrations/m18.rs diff --git a/gossip-lib/src/storage/migrations/m17.rs b/gossip-lib/src/storage/migrations/m17.rs index f06e2faee..e7df5f3ec 100644 --- a/gossip-lib/src/storage/migrations/m17.rs +++ b/gossip-lib/src/storage/migrations/m17.rs @@ -1,8 +1,6 @@ use crate::error::Error; use crate::storage::Storage; use heed::RwTxn; -use nostr_types::EventV2; -use speedy::Readable; impl Storage { pub(super) fn m17_trigger(&self) -> Result<(), Error> { @@ -26,13 +24,7 @@ impl Storage { } fn m17_reindex_event_relationships<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> { - // Iterate through all events - let loop_txn = self.env.read_txn()?; - for result in self.db_events()?.iter(&loop_txn)? { - let (_key, val) = result?; - let event = EventV2::read_from_buffer(val)?; - crate::process::process_relationships_of_event(&event, Some(txn))?; - } + self.set_flag_rebuild_relationships_needed(true, Some(txn))?; Ok(()) } } diff --git a/gossip-lib/src/storage/migrations/m18.rs b/gossip-lib/src/storage/migrations/m18.rs new file mode 100644 index 000000000..7226fa9fe --- /dev/null +++ b/gossip-lib/src/storage/migrations/m18.rs @@ -0,0 +1,39 @@ +use crate::error::Error; +use crate::storage::Storage; +use heed::RwTxn; + +impl Storage { + pub(super) fn m18_trigger(&self) -> Result<(), Error> { + let _ = self.db_relationships1(); + let _ = self.db_reprel1(); + let _ = self.db_relationships_by_id1(); + let _ = self.db_relationships_by_addr1(); + Ok(()) + } + + pub(super) fn m18_migrate<'a>( + &'a self, + prefix: &str, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { + // Info message + tracing::info!("{prefix}: ..."); + + // Migrate + self.m18_move_to_new_relationships_storage(txn)?; + + Ok(()) + } + + fn m18_move_to_new_relationships_storage<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> { + // Clear old relationships tables (we don't have an interface to delete it) + self.db_relationships1()?.clear(txn)?; + self.db_reprel1()?.clear(txn)?; + + self.set_flag_rebuild_relationships_needed(true, Some(txn))?; + Ok(()) + + } +} + + diff --git a/gossip-lib/src/storage/migrations/mod.rs b/gossip-lib/src/storage/migrations/mod.rs index 1b379887e..05554cf2d 100644 --- a/gossip-lib/src/storage/migrations/mod.rs +++ b/gossip-lib/src/storage/migrations/mod.rs @@ -9,6 +9,7 @@ mod m14; mod m15; mod m16; mod m17; +mod m18; mod m2; mod m3; mod m4; @@ -23,7 +24,7 @@ use crate::error::{Error, ErrorKind}; use heed::RwTxn; impl Storage { - const MAX_MIGRATION_LEVEL: u32 = 17; + const MAX_MIGRATION_LEVEL: u32 = 18; /// Initialize the database from empty pub(super) fn init_from_empty(&self) -> Result<(), Error> { @@ -77,6 +78,7 @@ impl Storage { 15 => self.m15_trigger()?, 16 => self.m16_trigger()?, 17 => self.m17_trigger()?, + 18 => self.m18_trigger()?, _ => panic!("Unreachable migration level"), } @@ -103,6 +105,7 @@ impl Storage { 15 => self.m15_migrate(&prefix, txn)?, 16 => self.m16_migrate(&prefix, txn)?, 17 => self.m17_migrate(&prefix, txn)?, + 18 => self.m18_migrate(&prefix, txn)?, _ => panic!("Unreachable migration level"), }; From bf9fe1ae97a7fd83b9080e0a6fca7348b5e29a51 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 08:18:55 +1300 Subject: [PATCH 13/81] fmt m18 --- gossip-lib/src/storage/migrations/m18.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gossip-lib/src/storage/migrations/m18.rs b/gossip-lib/src/storage/migrations/m18.rs index 7226fa9fe..d7d6d7323 100644 --- a/gossip-lib/src/storage/migrations/m18.rs +++ b/gossip-lib/src/storage/migrations/m18.rs @@ -25,15 +25,15 @@ impl Storage { Ok(()) } - fn m18_move_to_new_relationships_storage<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> { + fn m18_move_to_new_relationships_storage<'a>( + &'a self, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { // Clear old relationships tables (we don't have an interface to delete it) self.db_relationships1()?.clear(txn)?; self.db_reprel1()?.clear(txn)?; self.set_flag_rebuild_relationships_needed(true, Some(txn))?; Ok(()) - } } - - From 2b9462c7481716177acb3c6ad9ed8a300b4f02e8 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 08:19:08 +1300 Subject: [PATCH 14/81] process: ignore incoming events that are already deleted --- gossip-lib/src/process.rs | 40 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 996136146..a81bd00d4 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -102,6 +102,46 @@ pub async fn process_new_event( return Ok(()); // No more processing needed for existing event. } + // Ignore if the event is already deleted (by id) + for (_id, relbyid) in GLOBALS.storage.find_relationships_by_id(event.id)? { + if let RelationshipById::Deletion { by, reason: _ } = relbyid { + if by == event.pubkey { + tracing::trace!( + "{}: Deleted Event: {} {:?} @{}", + seen_on.as_ref().map(|r| r.as_str()).unwrap_or("_"), + subscription.as_ref().unwrap_or(&"_".to_string()), + event.kind, + event.created_at + ); + return Ok(()); + } + } + } + + // Ignore if the event is already deleted (by address) + if let Some(parameter) = event.parameter() { + let ea = EventAddr { + d: parameter.to_owned(), + relays: vec![], + kind: event.kind, + author: event.pubkey, + }; + for (_id, relbyaddr) in GLOBALS.storage.find_relationships_by_addr(&ea)? { + if let RelationshipByAddr::Deletion { by, reason: _ } = relbyaddr { + if by == event.pubkey { + tracing::trace!( + "{}: Deleted Event: {} {:?} @{}", + seen_on.as_ref().map(|r| r.as_str()).unwrap_or("_"), + subscription.as_ref().unwrap_or(&"_".to_string()), + event.kind, + event.created_at + ); + return Ok(()); + } + } + } + } + // Save event // Bail if the event is an already-replaced replaceable event if event.kind.is_replaceable() { From 6d7d5e311fbf840af1c5d565f294c7093b978e07 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 28 Nov 2023 14:28:28 +1300 Subject: [PATCH 15/81] PeopleLists page started --- gossip-bin/src/ui/mod.rs | 27 +++- gossip-bin/src/ui/people/list.rs | 205 ++++++++++++++++++++++++++++++ gossip-bin/src/ui/people/lists.rs | 61 +++++++++ gossip-bin/src/ui/people/mod.rs | 6 + 4 files changed, 295 insertions(+), 4 deletions(-) create mode 100644 gossip-bin/src/ui/people/list.rs create mode 100644 gossip-bin/src/ui/people/lists.rs diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 2470f466a..92db29268 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -105,8 +105,10 @@ enum Page { DmChatList, Feed(FeedKind), PeopleFollowNew, // deprecated, will separately be part of the list page - PeopleFollowed, - PeopleMuted, + PeopleLists, + PeopleList(PersonList), + PeopleFollowed, // deprecated, will use PeopleList(PersonList) + PeopleMuted, // deprecated, will use PeopleList(PersonList) Person(PublicKey), YourKeys, YourMetadata, @@ -141,6 +143,8 @@ impl Page { Page::DmChatList => (SubMenu::Feeds.as_str(), "Private chats".into()), Page::Feed(feedkind) => ("Feed", feedkind.to_string()), Page::PeopleFollowNew => (SubMenu::People.as_str(), "Follow new".into()), + Page::PeopleLists => (SubMenu::People.as_str(), "Lists".into()), + Page::PeopleList(list) => ("People", list.name()), Page::PeopleFollowed => (SubMenu::People.as_str(), "Followed".into()), Page::PeopleMuted => (SubMenu::People.as_str(), "Muted".into()), Page::Person(pk) => { @@ -187,7 +191,11 @@ impl Page { match self { Page::DmChatList => cat_name(self), Page::Feed(_) => name_cat(self), - Page::PeopleFollowNew | Page::PeopleFollowed | Page::PeopleMuted => cat_name(self), + Page::PeopleFollowNew + | Page::PeopleLists + | Page::PeopleList(_) + | Page::PeopleFollowed + | Page::PeopleMuted => cat_name(self), Page::Person(_) => name_cat(self), Page::YourKeys | Page::YourMetadata | Page::YourDelegation => cat_name(self), Page::Wizard(_) => name_cat(self), @@ -410,6 +418,7 @@ struct GossipUi { add_relay: String, // dep follow_clear_needs_confirm: bool, mute_clear_needs_confirm: bool, + clear_list_needs_confirm: bool, password: String, password2: String, password3: String, @@ -651,6 +660,7 @@ impl GossipUi { add_relay: "".to_owned(), follow_clear_needs_confirm: false, mute_clear_needs_confirm: false, + clear_list_needs_confirm: false, password: "".to_owned(), password2: "".to_owned(), password3: "".to_owned(), @@ -728,7 +738,11 @@ impl GossipUi { GLOBALS.feed.set_feed_to_person(pubkey.to_owned()); self.close_all_menus(ctx); } - Page::PeopleFollowNew | Page::PeopleFollowed | Page::PeopleMuted | Page::Person(_) => { + Page::PeopleFollowNew + | Page::PeopleLists + | Page::PeopleFollowed + | Page::PeopleMuted + | Page::Person(_) => { self.open_menu(ctx, SubMenu::People); } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { @@ -892,6 +906,7 @@ impl GossipUi { self.get_openable_menu(ui, ctx, SubMenu::People); cstate.show_body_indented(&header_response, ui, |ui| { self.add_menu_item_page(ui, Page::PeopleFollowNew); + self.add_menu_item_page(ui, Page::PeopleLists); self.add_menu_item_page(ui, Page::PeopleFollowed); self.add_menu_item_page(ui, Page::PeopleMuted); }); @@ -1265,6 +1280,8 @@ impl eframe::App for GossipUi { .fill({ match self.page { Page::PeopleFollowNew + | Page::PeopleLists + | Page::PeopleList(_) | Page::PeopleFollowed | Page::PeopleMuted | Page::Person(_) => { @@ -1284,6 +1301,8 @@ impl eframe::App for GossipUi { Page::DmChatList => dm_chat_list::update(self, ctx, frame, ui), Page::Feed(_) => feed::update(self, ctx, frame, ui), Page::PeopleFollowNew + | Page::PeopleLists + | Page::PeopleList(_) | Page::PeopleFollowed | Page::PeopleMuted | Page::Person(_) => people::update(self, ctx, frame, ui), diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs new file mode 100644 index 000000000..40683f88a --- /dev/null +++ b/gossip-bin/src/ui/people/list.rs @@ -0,0 +1,205 @@ +use super::{GossipUi, Page}; +use crate::ui::widgets; +use eframe::egui; +use egui::{Context, RichText, Ui}; +use gossip_lib::comms::ToOverlordMessage; +use gossip_lib::{Person, PersonList, GLOBALS}; + +pub(super) fn update( + app: &mut GossipUi, + ctx: &Context, + _frame: &mut eframe::Frame, + ui: &mut Ui, + list: PersonList, +) { + let members = GLOBALS + .storage + .get_people_in_list(list) + .unwrap_or_default(); + + let mut people: Vec<(Person, bool)> = Vec::new(); + for (pk, public) in &members { + if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { + people.push((person, *public)); + } else { + let person = Person::new(pk.to_owned()); + let _ = GLOBALS.storage.write_person(&person, None); + people.push((person, *public)); + } + } + people.sort_by(|a,b| a.0.cmp(&b.0)); + + ui.add_space(12.0); + + let latest_event_data = GLOBALS + .people + .latest_person_list_event_data + .get(&list) + .map(|v| v.value().clone()) + .unwrap_or_default(); + + let mut asof = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + asof = formatted; + } + } + + let txt = if let Some(private_len) = latest_event_data.private_len { + format!( + "REMOTE: {} (public_len={} private_len={})", + asof, latest_event_data.public_len, private_len + ) + } else { + format!( + "REMOTE: {} (public_len={})", + asof, latest_event_data.public_len + ) + }; + + ui.label(RichText::new(txt).size(15.0)) + .on_hover_text("This is the data in the latest list event fetched from relays"); + + ui.add_space(10.0); + + ui.horizontal(|ui| { + ui.add_space(30.0); + + if ui + .button("↓ Overwrite ↓") + .on_hover_text( + "This imports data from the latest event, erasing anything that is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: false, + }); + } + if ui + .button("↓ Merge ↓") + .on_hover_text( + "This imports data from the latest event, merging it into what is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: true, + }); + } + + if GLOBALS.signer.is_ready() { + if ui + .button("↑ Publish ↑") + .on_hover_text("This publishes the list to your relays") + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(list)); + } + } + + if GLOBALS.signer.is_ready() { + if app.clear_list_needs_confirm { + if ui.button("CANCEL").clicked() { + app.clear_list_needs_confirm = false; + } + if ui.button("YES, CLEAR ALL").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + app.clear_list_needs_confirm = false; + } + } else { + if ui.button("Clear All").clicked() { + app.clear_list_needs_confirm = true; + } + } + } + }); + + ui.add_space(10.0); + + let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { + Ok(Some(date)) => date, + Ok(None) => 0, + Err(e) => { + tracing::error!("{}", e); + 0 + } + }; + + let mut ledit = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + ledit = formatted; + } + } + ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0)) + .on_hover_text("This is the local (and effective) list"); + + if !GLOBALS.signer.is_ready() { + ui.add_space(10.0); + ui.horizontal_wrapped(|ui| { + ui.label("You need to "); + if ui.link("setup your identity").clicked() { + app.set_page(ctx, Page::YourKeys); + } + ui.label(" to push."); + }); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + ui.heading(format!("{} ({})", list.name(), people.len())); + ui.add_space(14.0); + + app.vert_scroll_area().show(ui, |ui| { + for (person, public) in people.iter() { + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + if widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() { + app.set_page(ctx, Page::Person(person.pubkey)); + }; + + ui.vertical(|ui| { + ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); + GossipUi::render_person_name_line(app, ui, person, false); + if !GLOBALS + .storage + .have_persons_relays(person.pubkey) + .unwrap_or(false) + { + ui.label( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ); + } + }); + + // FIXME indicate if public or not + }); + + ui.add_space(4.0); + ui.separator(); + } + }); +} diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs new file mode 100644 index 000000000..e5f643539 --- /dev/null +++ b/gossip-bin/src/ui/people/lists.rs @@ -0,0 +1,61 @@ +use super::{GossipUi, Page}; +use eframe::egui; +use egui::{Context, Ui}; +use gossip_lib::comms::ToOverlordMessage; +use gossip_lib::{PersonList, GLOBALS}; + +pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.add_space(10.0); + ui.horizontal_wrapped(|ui| { + ui.add_space(2.0); + ui.heading("Lists"); + }); + + ui.add_space(10.0); + + let all_lists = PersonList::all_lists(); + for (list, listname) in all_lists { + let count = GLOBALS + .storage + .get_people_in_list(list) + .map(|v| v.len()) + .unwrap_or(0); + ui.horizontal(|ui| { + ui.label(format!("({}) ", count)); + if ui.link(listname).clicked() { + app.set_page(ctx, Page::PeopleList(list)); + }; + if matches!(list, PersonList::Custom(_)) { + if ui.button("DELETE").clicked() { + // FIXME -- confirm with a popup, then call the delete() function (see below) + GLOBALS + .status_queue + .write() + .write("Person List Delete is NOT YET IMPLEMENTED".to_string()); + } + } + }); + } + if ui.button("Create a new list").clicked() { + // FIXME -- prompt for a name with a popup, then call the create() function (see below) + GLOBALS + .status_queue + .write() + .write("Person List Create is NOT YET IMPLEMENTED".to_string()); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); +} + +fn delete(list: PersonList) { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + let _ = list.deallocate(None); +} + +fn create(name: &str) { + let _ = PersonList::allocate(name, None); +} diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index a2fcdf8e5..d9c959edb 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -4,12 +4,18 @@ use egui::{Context, Ui}; mod follow; mod followed; +mod list; +mod lists; mod muted; mod person; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::PeopleFollowNew { follow::update(app, ctx, _frame, ui); + } else if app.page == Page::PeopleLists { + lists::update(app, ctx, _frame, ui); + } else if let Page::PeopleList(plist) = app.page { + list::update(app, ctx, _frame, ui, plist); } else if app.page == Page::PeopleFollowed { followed::update(app, ctx, _frame, ui); } else if app.page == Page::PeopleMuted { From 6ce1d8ef0830fbe2911c4dce072fd5b7ddb9a089 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 29 Nov 2023 09:39:07 +1300 Subject: [PATCH 16/81] Remove Followed/Muted pages (use PersonList(_) page instead) --- gossip-bin/src/ui/help/mod.rs | 6 +- gossip-bin/src/ui/mod.rs | 20 +-- gossip-bin/src/ui/people/followed.rs | 210 --------------------------- gossip-bin/src/ui/people/mod.rs | 6 - gossip-bin/src/ui/people/muted.rs | 193 ------------------------ 5 files changed, 4 insertions(+), 431 deletions(-) delete mode 100644 gossip-bin/src/ui/people/followed.rs delete mode 100644 gossip-bin/src/ui/people/muted.rs diff --git a/gossip-bin/src/ui/help/mod.rs b/gossip-bin/src/ui/help/mod.rs index e4fadbac3..0ed497a2d 100644 --- a/gossip-bin/src/ui/help/mod.rs +++ b/gossip-bin/src/ui/help/mod.rs @@ -67,7 +67,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.horizontal_wrapped(|ui| { ui.label("On the"); if ui.link("People > Followed").clicked() { - app.set_page(ctx, Page::PeopleFollowed); + app.set_page(ctx, Page::PeopleList(PersonList::Followed)); } ui.label("page, press [↓ Pull ↓ Overwrite] to pull down the people you follow. They won't have metadata just yet."); }); @@ -91,7 +91,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.horizontal_wrapped(|ui| { ui.label("Back on the"); if ui.link("People > Followed").clicked() { - app.set_page(ctx, Page::PeopleFollowed); + app.set_page(ctx, Page::PeopleList(PersonList::Followed)); } ui.label("page, once the relay picking has settled down, press [Refresh Metadata]. Then give it some time. It might not be able to find everybody just yet."); }); @@ -187,7 +187,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.horizontal_wrapped(|ui| { ui.label("On the"); if ui.link("People > Followed").clicked() { - app.set_page(ctx, Page::PeopleFollowed); + app.set_page(ctx, Page::PeopleList(PersonList::Followed)); } ui.label("page, once the relay picking has settled down, press [Refresh Metadata]. Then give it some time. It might not be able to find everybody just yet."); }); diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 92db29268..45c99725c 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -107,8 +107,6 @@ enum Page { PeopleFollowNew, // deprecated, will separately be part of the list page PeopleLists, PeopleList(PersonList), - PeopleFollowed, // deprecated, will use PeopleList(PersonList) - PeopleMuted, // deprecated, will use PeopleList(PersonList) Person(PublicKey), YourKeys, YourMetadata, @@ -145,8 +143,6 @@ impl Page { Page::PeopleFollowNew => (SubMenu::People.as_str(), "Follow new".into()), Page::PeopleLists => (SubMenu::People.as_str(), "Lists".into()), Page::PeopleList(list) => ("People", list.name()), - Page::PeopleFollowed => (SubMenu::People.as_str(), "Followed".into()), - Page::PeopleMuted => (SubMenu::People.as_str(), "Muted".into()), Page::Person(pk) => { let name = gossip_lib::names::best_name_from_pubkey_lookup(pk); ("Profile", name) @@ -193,9 +189,7 @@ impl Page { Page::Feed(_) => name_cat(self), Page::PeopleFollowNew | Page::PeopleLists - | Page::PeopleList(_) - | Page::PeopleFollowed - | Page::PeopleMuted => cat_name(self), + | Page::PeopleList(_) => cat_name(self), Page::Person(_) => name_cat(self), Page::YourKeys | Page::YourMetadata | Page::YourDelegation => cat_name(self), Page::Wizard(_) => name_cat(self), @@ -416,8 +410,6 @@ struct GossipUi { // User entry: general follow_someone: String, add_relay: String, // dep - follow_clear_needs_confirm: bool, - mute_clear_needs_confirm: bool, clear_list_needs_confirm: bool, password: String, password2: String, @@ -658,8 +650,6 @@ impl GossipUi { delegatee_tag_str: "".to_owned(), follow_someone: "".to_owned(), add_relay: "".to_owned(), - follow_clear_needs_confirm: false, - mute_clear_needs_confirm: false, clear_list_needs_confirm: false, password: "".to_owned(), password2: "".to_owned(), @@ -740,8 +730,6 @@ impl GossipUi { } Page::PeopleFollowNew | Page::PeopleLists - | Page::PeopleFollowed - | Page::PeopleMuted | Page::Person(_) => { self.open_menu(ctx, SubMenu::People); } @@ -907,8 +895,6 @@ impl GossipUi { cstate.show_body_indented(&header_response, ui, |ui| { self.add_menu_item_page(ui, Page::PeopleFollowNew); self.add_menu_item_page(ui, Page::PeopleLists); - self.add_menu_item_page(ui, Page::PeopleFollowed); - self.add_menu_item_page(ui, Page::PeopleMuted); }); self.after_openable_menu(ui, &cstate); } @@ -1282,8 +1268,6 @@ impl eframe::App for GossipUi { Page::PeopleFollowNew | Page::PeopleLists | Page::PeopleList(_) - | Page::PeopleFollowed - | Page::PeopleMuted | Page::Person(_) => { if self.theme.dark_mode { ctx.style().visuals.panel_fill @@ -1303,8 +1287,6 @@ impl eframe::App for GossipUi { Page::PeopleFollowNew | Page::PeopleLists | Page::PeopleList(_) - | Page::PeopleFollowed - | Page::PeopleMuted | Page::Person(_) => people::update(self, ctx, frame, ui), Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { you::update(self, ctx, frame, ui) diff --git a/gossip-bin/src/ui/people/followed.rs b/gossip-bin/src/ui/people/followed.rs deleted file mode 100644 index a2ed92a76..000000000 --- a/gossip-bin/src/ui/people/followed.rs +++ /dev/null @@ -1,210 +0,0 @@ -use super::{GossipUi, Page}; -use crate::ui::widgets; -use eframe::egui; -use egui::{Context, RichText, Ui}; -use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::{Person, PersonList, GLOBALS}; -use nostr_types::PublicKey; - -pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - let followed_pubkeys: Vec = GLOBALS - .storage - .get_people_in_list(PersonList::Followed) - .unwrap_or_default() - .drain(..) - .map(|(pk, _)| pk) - .collect(); - let mut people: Vec = Vec::new(); - for pk in &followed_pubkeys { - if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { - people.push(person); - } else { - let person = Person::new(pk.to_owned()); - let _ = GLOBALS.storage.write_person(&person, None); - people.push(person); - } - } - people.sort(); - - ui.add_space(12.0); - - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&PersonList::Followed) - .map(|v| v.value().clone()) - .unwrap_or_default(); - - let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - asof = formatted; - } - } - - ui.label( - RichText::new(format!( - "REMOTE: {} (len={})", - asof, latest_event_data.public_len - )) - .size(15.0), - ) - .on_hover_text("This is the data in the latest ContactList event fetched from relays"); - - ui.add_space(10.0); - - ui.horizontal(|ui| { - ui.add_space(30.0); - - if ui - .button("↓ Overwrite ↓") - .on_hover_text( - "This pulls down your Contact List, erasing anything that is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: PersonList::Followed, - merge: false, - }); - } - if ui - .button("↓ Merge ↓") - .on_hover_text( - "This pulls down your Contact List, merging it into what is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: PersonList::Followed, - merge: true, - }); - } - - if GLOBALS.signer.is_ready() { - if ui - .button("↑ Publish ↑") - .on_hover_text("This publishes your Contact List") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::PushPersonList(PersonList::Followed)); - } - } - - if GLOBALS.signer.is_ready() { - if app.follow_clear_needs_confirm { - if ui.button("CANCEL").clicked() { - app.follow_clear_needs_confirm = false; - } - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(PersonList::Followed)); - app.follow_clear_needs_confirm = false; - } - } else { - if ui.button("Clear All").clicked() { - app.follow_clear_needs_confirm = true; - } - } - } - - if ui - .button("Refresh Metadata") - .on_hover_text( - "This will seek out metadata (name, avatar, etc) on each person in the list below", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::RefreshSubscribedMetadata); - } - }); - - ui.add_space(10.0); - - let last_contact_list_edit = match GLOBALS - .storage - .get_person_list_last_edit_time(PersonList::Followed) - { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_contact_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; - } - } - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0)) - .on_hover_text("This is the local (and effective) following list"); - - if !GLOBALS.signer.is_ready() { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { - ui.label("You need to "); - if ui.link("setup your identity").clicked() { - app.set_page(ctx, Page::YourKeys); - } - ui.label(" to push."); - }); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading(format!("People Followed ({})", people.len())); - ui.add_space(18.0); - - app.vert_scroll_area().show(ui, |ui| { - for person in people.iter() { - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - if widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); - }; - - ui.vertical(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - GossipUi::render_person_name_line(app, ui, person, false); - if !GLOBALS - .storage - .have_persons_relays(person.pubkey) - .unwrap_or(false) - { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), - ); - } - }); - }); - - ui.add_space(4.0); - - ui.separator(); - } - }); -} diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index d9c959edb..bce75f257 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -3,10 +3,8 @@ use eframe::egui; use egui::{Context, Ui}; mod follow; -mod followed; mod list; mod lists; -mod muted; mod person; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { @@ -16,10 +14,6 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra lists::update(app, ctx, _frame, ui); } else if let Page::PeopleList(plist) = app.page { list::update(app, ctx, _frame, ui, plist); - } else if app.page == Page::PeopleFollowed { - followed::update(app, ctx, _frame, ui); - } else if app.page == Page::PeopleMuted { - muted::update(app, ctx, _frame, ui); } else if matches!(app.page, Page::Person(_)) { person::update(app, ctx, _frame, ui); } diff --git a/gossip-bin/src/ui/people/muted.rs b/gossip-bin/src/ui/people/muted.rs deleted file mode 100644 index 92e7be797..000000000 --- a/gossip-bin/src/ui/people/muted.rs +++ /dev/null @@ -1,193 +0,0 @@ -use super::{GossipUi, Page}; -use crate::ui::widgets; -use eframe::egui; -use egui::{Context, RichText, Ui}; -use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::{Person, PersonList, GLOBALS}; -use nostr_types::PublicKey; - -pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - let muted_pubkeys: Vec = GLOBALS - .storage - .get_people_in_list(PersonList::Muted) - .unwrap_or_default() - .drain(..) - .map(|(pk, _)| pk) - .collect(); - let mut people: Vec = Vec::new(); - for pk in &muted_pubkeys { - if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { - people.push(person); - } else { - let person = Person::new(pk.to_owned()); - let _ = GLOBALS.storage.write_person(&person, None); - people.push(person); - } - } - people.sort(); - - ui.add_space(12.0); - - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&PersonList::Muted) - .map(|v| v.value().clone()) - .unwrap_or_default(); - - let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - asof = formatted; - } - } - - let txt = if let Some(private_len) = latest_event_data.private_len { - format!( - "REMOTE: {} (public_len={} private_len={})", - asof, latest_event_data.public_len, private_len - ) - } else { - format!( - "REMOTE: {} (public_len={})", - asof, latest_event_data.public_len - ) - }; - ui.label(RichText::new(txt).size(15.0)) - .on_hover_text("This is the data in the latest MuteList event fetched from relays"); - - ui.add_space(10.0); - - ui.horizontal(|ui| { - ui.add_space(30.0); - - if ui - .button("↓ Overwrite ↓") - .on_hover_text("This pulls down your Mute List, erasing anything that is already here") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: PersonList::Muted, - merge: false, - }); - } - if ui - .button("↓ Merge ↓") - .on_hover_text("This pulls down your Mute List, merging it into what is already here") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: PersonList::Muted, - merge: true, - }); - } - - if GLOBALS.signer.is_ready() { - if ui - .button("↑ Publish ↑") - .on_hover_text("This publishes your Mute List") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::PushPersonList(PersonList::Muted)); - } - } - - if GLOBALS.signer.is_ready() { - if app.mute_clear_needs_confirm { - if ui.button("CANCEL").clicked() { - app.mute_clear_needs_confirm = false; - } - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(PersonList::Muted)); - app.mute_clear_needs_confirm = false; - } - } else { - if ui.button("Clear All").clicked() { - app.mute_clear_needs_confirm = true; - } - } - } - }); - - ui.add_space(10.0); - - let last_mute_list_edit = match GLOBALS - .storage - .get_person_list_last_edit_time(PersonList::Muted) - { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_mute_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; - } - } - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0)) - .on_hover_text("This is the local (and effective) mute list"); - - if !GLOBALS.signer.is_ready() { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { - ui.label("You need to "); - if ui.link("setup your identity").clicked() { - app.set_page(ctx, Page::YourKeys); - } - ui.label(" to push."); - }); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading(format!("People who are Muted ({})", people.len())); - ui.add_space(10.0); - - app.vert_scroll_area().show(ui, |ui| { - for person in people.iter() { - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - if widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); - }; - - ui.vertical(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - GossipUi::render_person_name_line(app, ui, person, false); - - if ui.button("UNMUTE").clicked() { - let _ = GLOBALS.people.mute(&person.pubkey, false, true); - } - }); - }); - - ui.add_space(4.0); - - ui.separator(); - } - }); -} From 358a04c612f050012bfe7532367aa5f0ca7a7d4d Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 29 Nov 2023 13:29:21 +1300 Subject: [PATCH 17/81] lib: DeletePersonList (locally, and events at relays) --- gossip-lib/src/comms.rs | 3 + gossip-lib/src/overlord/mod.rs | 139 +++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index b8351f463..df89839f9 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -31,6 +31,9 @@ pub enum ToOverlordMessage { /// Calls [delegation_reset](crate::Overlord::delegation_reset) DelegationReset, + /// Calls [delete_person_list](crate::Overlord::delete_person_list) + DeletePersonList(PersonList), + /// Calls [delete_post](crate::Overlord::delete_post) DeletePost(Id), diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 44470eb76..a14376d60 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -564,6 +564,9 @@ impl Overlord { ToOverlordMessage::DelegationReset => { Self::delegation_reset().await?; } + ToOverlordMessage::DeletePersonList(list) => { + self.delete_person_list(list).await?; + } ToOverlordMessage::DeletePost(id) => { self.delete_post(id).await?; } @@ -855,6 +858,142 @@ impl Overlord { Ok(()) } + /// Delete a person list + pub async fn delete_person_list(&mut self, list: PersonList) + -> Result<(), Error> + { + let public_key = match GLOBALS.signer.public_key() { + Some(pk) => pk, + None => { + GLOBALS + .status_queue + .write() + .write("Sign in to delete lists.".to_string()); + return Ok(()); + } + }; + + let name = list.name(); + + // Delete the list locally + GLOBALS.people.clear_person_list(list)?; + list.deallocate(None)?; + tracing::error!("DEBUG: deleted locally: {}", name); + + // Find all local-storage events that define the list + let bad_events = GLOBALS.storage.find_events( + &[EventKind::FollowSets], + &[public_key], + None, + |event| event.parameter() == Some(name.clone()), + false + )?; + tracing::error!("DEBUG: deleting {} local events for list={}", + bad_events.len(), name); + + // Delete those events locally + for bad_event in &bad_events { + GLOBALS.storage.delete_event(bad_event.id, None)?; + tracing::error!("DEBUG: deleting event={} from local events for list={}", + bad_event.id.as_hex_string(), name); + } + + // Generate a deletion event for those events + let event = { + // Include an "a" tag for the entire group + let mut tags: Vec = vec![ + Tag::Address { + kind: EventKind::FollowSets, + pubkey: public_key.into(), + d: name.clone(), + relay_url: None, + marker: None, + trailing: Vec::new(), + } + ]; + + // Include "e" tags for each event + for bad_event in &bad_events { + tags.push(Tag::Event { + id: bad_event.id, + recommended_relay_url: None, + marker: None, + trailing: Vec::new(), + }); + } + + let pre_event = PreEvent { + pubkey: public_key, + created_at: Unixtime::now().unwrap(), + kind: EventKind::EventDeletion, + tags: vec![ + + ], + content: "".to_owned(), // FIXME, option to supply a delete reason + }; + + // Should we add a pow? Maybe the relay needs it. + GLOBALS.signer.sign_preevent(pre_event, None, None)? + }; + + // Process this event locally + crate::process::process_new_event(&event, None, None, false, false).await?; + + // Determine which relays to post this to + let mut relay_urls: Vec = Vec::new(); + { + // Get all of the relays that we write to + let write_relays: Vec = GLOBALS + .storage + .filter_relays(|r| r.has_usage_bits(Relay::WRITE) && r.rank != 0)? + .iter() + .map(|relay| relay.url.clone()) + .collect(); + relay_urls.extend(write_relays); + + // Get all of the relays this events were seen on + for bad_event in &bad_events { + let seen_on: Vec = GLOBALS + .storage + .get_event_seen_on_relay(bad_event.id)? + .iter() + .map(|(url, _time)| url.to_owned()) + .collect(); + + for url in &seen_on { + tracing::error!("SEEN ON {}", &url); + } + + relay_urls.extend(seen_on); + } + + relay_urls.sort(); + relay_urls.dedup(); + } + + // Send event to all these relays + for url in relay_urls { + // Send it the event to post + tracing::debug!("Asking {} to delete", &url); + + tracing::error!("DEBUG: deleting list from {}", &url); + + self.engage_minion( + url.to_owned(), + vec![RelayJob { + reason: RelayConnectionReason::PostEvent, + payload: ToMinionPayload { + job_id: rand::random::(), + detail: ToMinionPayloadDetail::PostEvent(Box::new(event.clone())), + }, + }], + ) + .await?; + } + + Ok(()) + } + /// Delete a post pub async fn delete_post(&mut self, id: Id) -> Result<(), Error> { let tags: Vec = vec![Tag::Event { From df7af04299e4921526750d289ddcb16aa188d5e0 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 29 Nov 2023 12:06:47 +1300 Subject: [PATCH 18/81] More person list pages work --- gossip-bin/src/ui/people/list.rs | 45 ++++++++++++++++++++----------- gossip-bin/src/ui/people/lists.rs | 23 +++++----------- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 40683f88a..7b7706cb0 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -12,22 +12,26 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { - let members = GLOBALS - .storage - .get_people_in_list(list) - .unwrap_or_default(); + let people = { + let members = GLOBALS + .storage + .get_people_in_list(list) + .unwrap_or_default(); + + let mut people: Vec<(Person, bool)> = Vec::new(); - let mut people: Vec<(Person, bool)> = Vec::new(); - for (pk, public) in &members { - if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { - people.push((person, *public)); - } else { - let person = Person::new(pk.to_owned()); - let _ = GLOBALS.storage.write_person(&person, None); - people.push((person, *public)); + for (pk, public) in &members { + if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { + people.push((person, *public)); + } else { + let person = Person::new(pk.to_owned()); + let _ = GLOBALS.storage.write_person(&person, None); + people.push((person, *public)); + } } - } - people.sort_by(|a,b| a.0.cmp(&b.0)); + people.sort_by(|a,b| a.0.cmp(&b.0)); + people + }; ui.add_space(12.0); @@ -193,11 +197,20 @@ pub(super) fn update( .color(app.theme.warning_marker_text_color()), ); } - }); - // FIXME indicate if public or not + ui.horizontal(|ui| { + if crate::ui::components::switch_simple(ui, *public).clicked() { + let _ = GLOBALS.storage.add_person_to_list(&person.pubkey, list, !*public, None); + } + ui.label(if *public { "public" } else { "private" }); + }); + }); }); + if ui.button("Remove").clicked() { + let _ = GLOBALS.storage.remove_person_from_list(&person.pubkey, list, None); + } + ui.add_space(4.0); ui.separator(); } diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index e5f643539..c8affb984 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -27,17 +27,17 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }; if matches!(list, PersonList::Custom(_)) { if ui.button("DELETE").clicked() { - // FIXME -- confirm with a popup, then call the delete() function (see below) - GLOBALS - .status_queue - .write() - .write("Person List Delete is NOT YET IMPLEMENTED".to_string()); + // FIXME -- confirm with a popup first! + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::DeletePersonList(list)); } } }); } if ui.button("Create a new list").clicked() { - // FIXME -- prompt for a name with a popup, then call the create() function (see below) + // FIXME -- prompt for a name with a popup, then create with: + // let _ = PersonList::allocate(name, None); GLOBALS .status_queue .write() @@ -48,14 +48,3 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.separator(); ui.add_space(10.0); } - -fn delete(list: PersonList) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(list)); - let _ = list.deallocate(None); -} - -fn create(name: &str) { - let _ = PersonList::allocate(name, None); -} From cd36343f97008a31d30f8b3411862f940c964f53 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 29 Nov 2023 09:31:24 +1300 Subject: [PATCH 19/81] Subscribe to the user's FollowSets events on their outbox --- gossip-lib/src/overlord/minion/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index f99b52135..90c14b001 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -628,6 +628,7 @@ impl Minion { //EventKind::RecommendRelay, EventKind::ContactList, EventKind::MuteList, + EventKind::FollowSets, EventKind::RelayList, ], // these are all replaceable, no since required From ea8eb0607082b049749ceb3a4fe94fdcb21a72e0 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 08:40:27 +1300 Subject: [PATCH 20/81] UI screen to wait for data migration --- gossip-bin/src/ui/mod.rs | 29 +++++++++++++++---- gossip-bin/src/ui/people/list.rs | 18 +++++++----- gossip-lib/src/globals.rs | 4 +++ gossip-lib/src/overlord/mod.rs | 48 ++++++++++++++++++-------------- 4 files changed, 65 insertions(+), 34 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 45c99725c..5b97fff07 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -187,9 +187,7 @@ impl Page { match self { Page::DmChatList => cat_name(self), Page::Feed(_) => name_cat(self), - Page::PeopleFollowNew - | Page::PeopleLists - | Page::PeopleList(_) => cat_name(self), + Page::PeopleFollowNew | Page::PeopleLists | Page::PeopleList(_) => cat_name(self), Page::Person(_) => name_cat(self), Page::YourKeys | Page::YourMetadata | Page::YourDelegation => cat_name(self), Page::Wizard(_) => name_cat(self), @@ -728,9 +726,7 @@ impl GossipUi { GLOBALS.feed.set_feed_to_person(pubkey.to_owned()); self.close_all_menus(ctx); } - Page::PeopleFollowNew - | Page::PeopleLists - | Page::Person(_) => { + Page::PeopleFollowNew | Page::PeopleLists | Page::Person(_) => { self.open_menu(ctx, SubMenu::People); } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { @@ -1183,6 +1179,11 @@ impl eframe::App for GossipUi { return force_login(self, ctx); } + // If data migration, show that screen + if GLOBALS.wait_for_data_migration.load(Ordering::Relaxed) { + return wait_for_data_migration(self, ctx); + } + // Wizard does its own panels if let Page::Wizard(wp) = self.page { return wizard::update(self, ctx, frame, wp); @@ -1893,3 +1894,19 @@ fn force_login(app: &mut GossipUi, ctx: &Context) { you::offer_delete(app, ui); }); } + +fn wait_for_data_migration(app: &mut GossipUi, ctx: &Context) { + egui::CentralPanel::default() + .frame({ + let frame = egui::Frame::central_panel(&app.theme.get_style()); + frame.inner_margin(egui::Margin { + left: 20.0, + right: 10.0, + top: 10.0, + bottom: 0.0, + }) + }) + .show(ctx, |ui| { + ui.label("Please wait for the data migration to complete..."); + }); +} diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 7b7706cb0..969927287 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -13,10 +13,7 @@ pub(super) fn update( list: PersonList, ) { let people = { - let members = GLOBALS - .storage - .get_people_in_list(list) - .unwrap_or_default(); + let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); let mut people: Vec<(Person, bool)> = Vec::new(); @@ -29,7 +26,7 @@ pub(super) fn update( people.push((person, *public)); } } - people.sort_by(|a,b| a.0.cmp(&b.0)); + people.sort_by(|a, b| a.0.cmp(&b.0)); people }; @@ -200,7 +197,12 @@ pub(super) fn update( ui.horizontal(|ui| { if crate::ui::components::switch_simple(ui, *public).clicked() { - let _ = GLOBALS.storage.add_person_to_list(&person.pubkey, list, !*public, None); + let _ = GLOBALS.storage.add_person_to_list( + &person.pubkey, + list, + !*public, + None, + ); } ui.label(if *public { "public" } else { "private" }); }); @@ -208,7 +210,9 @@ pub(super) fn update( }); if ui.button("Remove").clicked() { - let _ = GLOBALS.storage.remove_person_from_list(&person.pubkey, list, None); + let _ = GLOBALS + .storage + .remove_person_from_list(&person.pubkey, list, None); } ui.add_space(4.0); diff --git a/gossip-lib/src/globals.rs b/gossip-lib/src/globals.rs index 3e7f5cf7b..4210ee69c 100644 --- a/gossip-lib/src/globals.rs +++ b/gossip-lib/src/globals.rs @@ -130,6 +130,9 @@ pub struct Globals { // Wait for login pub wait_for_login: AtomicBool, pub wait_for_login_notify: Notify, + + // Wait for data migration + pub wait_for_data_migration: AtomicBool, } lazy_static! { @@ -187,6 +190,7 @@ lazy_static! { filter, wait_for_login: AtomicBool::new(false), wait_for_login_notify: Notify::new(), + wait_for_data_migration: AtomicBool::new(false), } }; } diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index a14376d60..4a7467122 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -167,7 +167,13 @@ impl Overlord { // If we need to rebuild relationships, do so now if GLOBALS.storage.get_flag_rebuild_relationships_needed() { + GLOBALS + .wait_for_data_migration + .store(true, Ordering::Relaxed); GLOBALS.storage.rebuild_relationships(None)?; + GLOBALS + .wait_for_data_migration + .store(false, Ordering::Relaxed); } // Start the fetcher @@ -859,9 +865,7 @@ impl Overlord { } /// Delete a person list - pub async fn delete_person_list(&mut self, list: PersonList) - -> Result<(), Error> - { + pub async fn delete_person_list(&mut self, list: PersonList) -> Result<(), Error> { let public_key = match GLOBALS.signer.public_key() { Some(pk) => pk, None => { @@ -886,31 +890,35 @@ impl Overlord { &[public_key], None, |event| event.parameter() == Some(name.clone()), - false + false, )?; - tracing::error!("DEBUG: deleting {} local events for list={}", - bad_events.len(), name); + tracing::error!( + "DEBUG: deleting {} local events for list={}", + bad_events.len(), + name + ); // Delete those events locally for bad_event in &bad_events { GLOBALS.storage.delete_event(bad_event.id, None)?; - tracing::error!("DEBUG: deleting event={} from local events for list={}", - bad_event.id.as_hex_string(), name); + tracing::error!( + "DEBUG: deleting event={} from local events for list={}", + bad_event.id.as_hex_string(), + name + ); } // Generate a deletion event for those events let event = { // Include an "a" tag for the entire group - let mut tags: Vec = vec![ - Tag::Address { - kind: EventKind::FollowSets, - pubkey: public_key.into(), - d: name.clone(), - relay_url: None, - marker: None, - trailing: Vec::new(), - } - ]; + let mut tags: Vec = vec![Tag::Address { + kind: EventKind::FollowSets, + pubkey: public_key.into(), + d: name.clone(), + relay_url: None, + marker: None, + trailing: Vec::new(), + }]; // Include "e" tags for each event for bad_event in &bad_events { @@ -926,9 +934,7 @@ impl Overlord { pubkey: public_key, created_at: Unixtime::now().unwrap(), kind: EventKind::EventDeletion, - tags: vec![ - - ], + tags: vec![], content: "".to_owned(), // FIXME, option to supply a delete reason }; From 4d8ecd706c594ed7178b53ded2b370163404c8fc Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 09:17:39 +1300 Subject: [PATCH 21/81] FIX: do not delete from relationships when deleting an event --- gossip-lib/src/storage/mod.rs | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index cebda6668..b3fb62280 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1219,24 +1219,8 @@ impl Storage { // Delete from event_viewed self.db_event_viewed()?.delete(txn, id.as_slice())?; - // Delete from relationships where the id is the first one - { - // save the actual keys to delete - let mut deletions: Vec> = Vec::new(); - - let start_key: &[u8] = id.as_slice(); - - for result in self.db_relationships_by_id()?.prefix_iter(txn, start_key)? { - let (_key, val) = result?; - deletions.push(val.to_owned()); - } - - // actual deletion done in second pass - // (deleting during interation does not work in LMDB) - for deletion in deletions.drain(..) { - self.db_relationships_by_id()?.delete(txn, &deletion)?; - } - } + // DO NOT delete from relationships. The related event still applies in case + // this event comes back, ESPECIALLY deletion relationships! // We cannot delete from numerous indexes because the ID // is in the value, not in the key. From c4abf207dddcaee0d4f898f9326f12c3e5059556 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 09:34:02 +1300 Subject: [PATCH 22/81] Update enabled_event_kinds() --- gossip-lib/src/feed.rs | 88 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 11 deletions(-) diff --git a/gossip-lib/src/feed.rs b/gossip-lib/src/feed.rs index 027ceacf7..85d508bfe 100644 --- a/gossip-lib/src/feed.rs +++ b/gossip-lib/src/feed.rs @@ -494,17 +494,83 @@ pub fn enabled_event_kinds() -> Vec { EventKind::iter() .filter(|k| { - ((*k != EventKind::Reaction) || reactions) - && ((*k != EventKind::Repost) || reposts) - && ((*k != EventKind::LongFormContent) || show_long_form) - && ((*k != EventKind::EncryptedDirectMessage) || direct_messages) - && ((*k != EventKind::DmChat) || direct_messages) - && ((*k != EventKind::GiftWrap) || direct_messages) - && ((*k != EventKind::Zap) || enable_zap_receipts) - && (*k != EventKind::ChannelMessage) // not yet implemented - && (*k != EventKind::LiveChatMessage) // not yet implemented - && (*k != EventKind::CommunityPost) // not yet implemented - && (*k != EventKind::DraftLongFormContent) // not yet implemented + *k == EventKind::Metadata + || *k == EventKind::TextNote + //|| *k == EventKind::RecommendRelay + || *k == EventKind::ContactList + || ((*k == EventKind::EncryptedDirectMessage) && direct_messages) + || *k == EventKind::EventDeletion + || ((*k == EventKind::Repost) && reposts) + || ((*k == EventKind::Reaction) && reactions) + //|| *k == EventKind::BadgeAward + //|| *k == EventKind::Seal // -- never subscribed to + || ((*k == EventKind::DmChat) && direct_messages) + || ((*k == EventKind::GenericRepost) && reposts) + //|| *k == EventKind::ChannelCreation + //|| *k == EventKind::ChannelMetadata + //|| *k == EventKind::ChannelMessage + //|| *k == EventKind::ChannelHideMessage + //|| *k == EventKind::ChannelMuteUser + //|| *k == EventKind::PublicChatReserved45 + //|| *k == EventKind::PublicChatReserved46 + //|| *k == EventKind::PublicChatReserved47 + //|| *k == EventKind::PublicChatReserved48 + //|| *k == EventKind::PublicChatReserved49 + // || *k == EventKind::Timestamp + || ((*k == EventKind::GiftWrap) && direct_messages) + // || *k == EventKind::FileMetadata + // || *k == EventKind::LiveChatMessage + // || *k == EventKind::ProblemTracker + // || *k == EventKind::Reporting + // || *k == EventKind::Label + // || *k == EventKind::CommunityPost + // || *k == EventKind::CommunityPostApproval + // || *k == EventKind::JobFeedback + // || *k == EventKind::ZapGoal + || *k == EventKind::ZapRequest + || ((*k == EventKind::Zap) && enable_zap_receipts) + // || *k == EventKind::Highlights + || *k == EventKind::MuteList + // || *k == EventKind::PinList + || *k == EventKind::RelayList + // || *k == EventKind::BookmarkList + // || *k == EventKind::CommunityList + // || *k == EventKind::PublicChatsList + // || *k == EventKind::BlockedRelaysList + // || *k == EventKind::SearchRelaysList + // || *k == EventKind::InterestsList + // || *k == EventKind::UserEmojiList + // || *k == EventKind::WalletInfo + // || *k == EventKind::Auth -- never subscribed to + // || *k == EventKind::WalletRequest + // || *k == EventKind::WalletResponse + // || *k == EventKind::NostrConnect + // || *k == EventKind::HttpAuth + || *k == EventKind::FollowSets + // || *k == EventKind::GenericSets + // || *k == EventKind::RelaySets + // || *k == EventKind::BookmarkSets + // || *k == EventKind::CurationSets + // || *k == EventKind::ProfileBadges + // || *k == EventKind::BadgeDefinition + // || *k == EventKind::InterestSets + // || *k == EventKind::CreateUpdateStall + // || *k == EventKind::CreateUpdateProduct + || ((*k == EventKind::LongFormContent) && show_long_form) + // || *k == EventKind::DraftLongFormContent + // || *k == EventKind::EmojiSets + // || *k == EventKind::AppSpecificData + // || *k == EventKind::LiveEvent + // || *k == EventKind::UserStatus + // || *k == EventKind::ClassifiedListing + // || *k == EventKind::DraftClassifiedListing + // || *k == EventKind::DateBasedCalendarEvent + // || *k == EventKind::TimeBasedCalendarEvent + // || *k == EventKind::Calendar + // || *k == EventKind::CalendarEventRsvp + // || *k == EventKind::HandlerRecommendation + // || *k == EventKind::HandlerInformation + // || *k == EventKind::CommunityDefinition }) .collect() } From 0747d534296f24b070432980df7d9e518dfff7c2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 10:04:22 +1300 Subject: [PATCH 23/81] comment --- gossip-lib/src/overlord/minion/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 90c14b001..0fa3565a8 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -641,7 +641,8 @@ impl Minion { since: Some(giftwrap_since), ..Default::default() }, - // Posts I wrote recently + // Events I posted recently, including feed_displayable and + // augments (deletions, reactions, timestamp, label,reporting, and zap) Filter { authors: vec![pkh], kinds: crate::feed::feed_related_event_kinds(false), // not DMs From 6eedc22aa76840146430daa223b84923db332aff Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 10:11:53 +1300 Subject: [PATCH 24/81] FIX: lib DeletePersonList --- gossip-lib/src/overlord/mod.rs | 55 +++++++++++++++++----------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 4a7467122..fc745c159 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -866,24 +866,24 @@ impl Overlord { /// Delete a person list pub async fn delete_person_list(&mut self, list: PersonList) -> Result<(), Error> { + // Delete the list locally + GLOBALS.people.clear_person_list(list)?; + list.deallocate(None)?; + let name = list.name(); + + // If we are only following, nothing else needed + if GLOBALS.storage.get_flag_following_only() { + return Ok(()); + } + let public_key = match GLOBALS.signer.public_key() { Some(pk) => pk, None => { - GLOBALS - .status_queue - .write() - .write("Sign in to delete lists.".to_string()); + // Odd. how do they have a list if they have no pubkey? return Ok(()); } }; - let name = list.name(); - - // Delete the list locally - GLOBALS.people.clear_person_list(list)?; - list.deallocate(None)?; - tracing::error!("DEBUG: deleted locally: {}", name); - // Find all local-storage events that define the list let bad_events = GLOBALS.storage.find_events( &[EventKind::FollowSets], @@ -892,20 +892,24 @@ impl Overlord { |event| event.parameter() == Some(name.clone()), false, )?; - tracing::error!( - "DEBUG: deleting {} local events for list={}", - bad_events.len(), - name - ); + + // If no list events, we are done + if bad_events.is_empty() { + return Ok(()); + } // Delete those events locally for bad_event in &bad_events { GLOBALS.storage.delete_event(bad_event.id, None)?; - tracing::error!( - "DEBUG: deleting event={} from local events for list={}", - bad_event.id.as_hex_string(), - name - ); + } + + // Require sign in to delete further + if !GLOBALS.signer.is_ready() { + GLOBALS + .status_queue + .write() + .write("The list was only deleted locally because you are not signed in. The list may reappear on restart.".to_string()); + return Ok(()); } // Generate a deletion event for those events @@ -934,8 +938,8 @@ impl Overlord { pubkey: public_key, created_at: Unixtime::now().unwrap(), kind: EventKind::EventDeletion, - tags: vec![], - content: "".to_owned(), // FIXME, option to supply a delete reason + tags, + content: "Deleting person list".to_owned(), }; // Should we add a pow? Maybe the relay needs it. @@ -979,11 +983,6 @@ impl Overlord { // Send event to all these relays for url in relay_urls { - // Send it the event to post - tracing::debug!("Asking {} to delete", &url); - - tracing::error!("DEBUG: deleting list from {}", &url); - self.engage_minion( url.to_owned(), vec![RelayJob { From 50032b9499f88ca9083c98d98c30d80a05ac1192 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 14:22:14 +1300 Subject: [PATCH 25/81] Storage::get_deletions() now handles multiple deletions of the same event --- gossip-bin/src/ui/feed/note/mod.rs | 10 +++++----- gossip-bin/src/ui/feed/notedata.rs | 8 ++++---- gossip-lib/src/storage/mod.rs | 7 ++++--- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/gossip-bin/src/ui/feed/note/mod.rs b/gossip-bin/src/ui/feed/note/mod.rs index 7e0ff849a..af676e30c 100644 --- a/gossip-bin/src/ui/feed/note/mod.rs +++ b/gossip-bin/src/ui/feed/note/mod.rs @@ -80,7 +80,7 @@ pub(super) fn render_note( let skip = ((note_data.muted() && app.settings.hide_mutes_entirely) && !matches!(app.page, Page::Feed(FeedKind::DmChat(_))) && !matches!(app.page, Page::Feed(FeedKind::Person(_)))) - || (note_data.deletion.is_some() && !app.settings.show_deleted_events); + || (!note_data.deletions.is_empty() && !app.settings.show_deleted_events); if skip { return; @@ -379,7 +379,7 @@ fn render_note_inner( _ => {} } - if note.deletion.is_some() { + if !note.deletions.is_empty() { let color = app.theme.warning_marker_text_color(); ui.label( RichText::new("DELETED") @@ -507,7 +507,7 @@ fn render_note_inner( GLOBALS.dismissed.blocking_write().push(note.event.id); } if Some(note.event.pubkey) == app.settings.public_key - && note.deletion.is_none() + && note.deletions.is_empty() { if ui.button("Delete").clicked() { let _ = GLOBALS @@ -636,13 +636,13 @@ fn render_note_inner( ui, ctx, note_ref.clone(), - note.deletion.is_some(), + !note.deletions.is_empty(), content_margin_left, content_pull_top, ); // deleted? - if let Some(delete_reason) = ¬e.deletion { + for delete_reason in ¬e.deletions { Frame::none() .inner_margin(Margin { left: footer_margin_left, diff --git a/gossip-bin/src/ui/feed/notedata.rs b/gossip-bin/src/ui/feed/notedata.rs index 85aeea9fe..471c52b9f 100644 --- a/gossip-bin/src/ui/feed/notedata.rs +++ b/gossip-bin/src/ui/feed/notedata.rs @@ -36,8 +36,8 @@ pub(super) struct NoteData { /// Lists the author is on pub lists: HashMap, - /// Deletion reason if any - pub deletion: Option, + /// Deletion reasons if any + pub deletions: Vec, /// Do we consider this note as being a repost of another? pub repost: Option, @@ -85,7 +85,7 @@ impl NoteData { let delegation = event.delegation(); - let deletion = GLOBALS.storage.get_deletion(&event).unwrap_or(None); + let deletions = GLOBALS.storage.get_deletions(&event).unwrap_or_default(); let (reactions, self_already_reacted) = GLOBALS .storage @@ -234,7 +234,7 @@ impl NoteData { delegation, author, lists, - deletion, + deletions, repost, embedded_event, mentions, diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index b3fb62280..d9a0bd210 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -1862,15 +1862,16 @@ impl Storage { } /// Get whether an event was deleted, and if so the optional reason - pub fn get_deletion(&self, maybe_deleted_event: &Event) -> Result, Error> { + pub fn get_deletions(&self, maybe_deleted_event: &Event) -> Result, Error> { + let mut reasons: Vec = Vec::new(); for (_id, rel) in self.find_relationships_by_id(maybe_deleted_event.id)? { if let RelationshipById::Deletion { by, reason } = rel { if maybe_deleted_event.pubkey == by { - return Ok(Some(reason)); + reasons.push(reason); } } } - Ok(None) + Ok(reasons) } /// Write a person record From d1bdf732c040b3fb511cc1dd0e2a3c2344de47fb Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sun, 3 Dec 2023 13:12:41 +1300 Subject: [PATCH 26/81] Fix gossip-bin feature set --- gossip-bin/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/gossip-bin/Cargo.toml b/gossip-bin/Cargo.toml index 4c8dfbf0a..a3b22283e 100644 --- a/gossip-bin/Cargo.toml +++ b/gossip-bin/Cargo.toml @@ -15,6 +15,7 @@ lang-cjk = [ "gossip-lib/lang-cjk" ] video-ffmpeg = [ "egui-video", "sdl2" ] native-tls = [ "gossip-lib/native-tls" ] rustls-tls = [ "gossip-lib/rustls-tls" ] +rustls-tls-native = [ "gossip-lib/rustls-tls-native" ] [dependencies] bech32 = "0.9" From ef51ac6a3c99a0f307f48ab8f50a62fd4a4edebb Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 09:28:10 +1300 Subject: [PATCH 27/81] Preserve unused mute tags from prior event when generating MuteList events --- gossip-lib/src/people.rs | 69 +++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index f5753aa68..0d8797016 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -127,11 +127,11 @@ impl People { }; for (person_list, _) in PersonList::all_lists() { - if let Ok(Some(event)) = - GLOBALS - .storage - .get_replaceable_event(person_list.event_kind(), pk, "") - { + if let Ok(Some(event)) = GLOBALS.storage.get_replaceable_event( + person_list.event_kind(), + pk, + &person_list.name(), + ) { self.latest_person_list_event_data.insert( person_list, PersonListEventData { @@ -600,8 +600,41 @@ impl People { PersonList::Custom(_) => EventKind::FollowSets, }; - // Build public p-tags - let mut tags: Vec = Vec::new(); + // Pull the existing event (maybe) + let existing_event: Option = match kind { + EventKind::ContactList | EventKind::MuteList => { + // We fetch for ContactList to preserve the contents + // We fetch for MuteList to preserve 't', 'e', and "word" tags + GLOBALS.storage.get_replaceable_event(kind, my_pubkey, "")? + } + // We don't need to preserve anything from FollowSets events + _ => None, + }; + + let mut public_tags: Vec = Vec::new(); + + // For mute lists, preserve 't', 'e' and 'word' tags from the previous + // event so as to not clobber them, they may be used on other clients + if kind == EventKind::MuteList { + if let Some(ref event) = existing_event { + for tag in &event.tags { + match tag { + Tag::Hashtag { .. } => { + public_tags.push(tag.clone()); + } + Tag::Event { .. } => { + public_tags.push(tag.clone()); + } + Tag::Other { .. } => { + public_tags.push(tag.clone()); + } + _ => (), + } + } + } + }; + + // Build the public tags for (pubkey, public) in people.iter() { if !*public { continue; @@ -626,7 +659,7 @@ impl People { None }; - tags.push(Tag::Pubkey { + public_tags.push(Tag::Pubkey { pubkey: pubkey.into(), recommended_relay_url, petname, @@ -636,7 +669,7 @@ impl People { // Add d-tag if using FollowSets if matches!(person_list, PersonList::Custom(_)) { - tags.push(Tag::Identifier { + public_tags.push(Tag::Identifier { d: person_list.name(), trailing: vec![], }); @@ -644,30 +677,28 @@ impl People { let content = { if kind == EventKind::ContactList { - match GLOBALS.storage.get_replaceable_event( - EventKind::ContactList, - my_pubkey, - "", - )? { + // Preserve the contents of any existing kind-3 event for use by + // other clients + match existing_event { Some(c) => c.content, None => "".to_owned(), } } else { - // Build private p-tags (except for ContactList) - let mut private_p_tags: Vec = Vec::new(); + // Build private tags (except for ContactList) + let mut private_tags: Vec = Vec::new(); for (pubkey, public) in people.iter() { if *public { continue; } - private_p_tags.push(Tag::Pubkey { + private_tags.push(Tag::Pubkey { pubkey: pubkey.into(), recommended_relay_url: None, petname: None, trailing: vec![], }); } - let private_tags_string = serde_json::to_string(&private_p_tags)?; + let private_tags_string = serde_json::to_string(&private_tags)?; GLOBALS.signer.encrypt( &my_pubkey, &private_tags_string, @@ -680,7 +711,7 @@ impl People { pubkey: my_pubkey, created_at: Unixtime::now().unwrap(), kind, - tags, + tags: public_tags, content, }; From 784b4a796d42d4c3abf74211c0001d4779f92d63 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 09:44:43 +1300 Subject: [PATCH 28/81] Fix personlist overwrite/merge not working, which fixes date update --- gossip-bin/src/ui/people/list.rs | 60 ++++++++++++++++---------------- gossip-lib/src/overlord/mod.rs | 18 ++++++---- 2 files changed, 41 insertions(+), 37 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 969927287..50786b2f2 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -68,36 +68,36 @@ pub(super) fn update( ui.horizontal(|ui| { ui.add_space(30.0); - if ui - .button("↓ Overwrite ↓") - .on_hover_text( - "This imports data from the latest event, erasing anything that is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: false, - }); - } - if ui - .button("↓ Merge ↓") - .on_hover_text( - "This imports data from the latest event, merging it into what is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: true, - }); - } - if GLOBALS.signer.is_ready() { + if ui + .button("↓ Overwrite ↓") + .on_hover_text( + "This imports data from the latest event, erasing anything that is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: false, + }); + } + if ui + .button("↓ Merge ↓") + .on_hover_text( + "This imports data from the latest event, merging it into what is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: true, + }); + } + if ui .button("↑ Publish ↑") .on_hover_text("This publishes the list to your relays") @@ -157,7 +157,7 @@ pub(super) fn update( if ui.link("setup your identity").clicked() { app.set_page(ctx, Page::YourKeys); } - ui.label(" to push."); + ui.label(" to manage list events."); }); } diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index fc745c159..0ea89575b 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -155,9 +155,7 @@ impl Overlord { async fn run_inner(&mut self) -> Result<(), Error> { // Maybe wait for UI login if GLOBALS.wait_for_login.load(Ordering::Relaxed) { - tracing::error!("DEBUG: overlord waiting for login"); GLOBALS.wait_for_login_notify.notified().await; - tracing::error!("DEBUG: overlord finished waiting for login"); } // Check for shutdown (we might not have gotten a login) @@ -2493,18 +2491,24 @@ impl Overlord { /// Update the local mute list from the last MuteList event received. pub async fn update_person_list(&mut self, list: PersonList, merge: bool) -> Result<(), Error> { + // We need a private key to decrypt the content + if !GLOBALS.signer.is_ready() { + GLOBALS.status_queue.write().write( + "You need to be logged in to update a PersonList due to encrypted contents" + .to_string(), + ); + return Ok(()); + } + // we cannot do anything without an identity setup first - let my_pubkey = match GLOBALS.storage.read_setting_public_key() { - Some(pk) => pk, - None => return Err(ErrorKind::NoPublicKey.into()), - }; + let my_pubkey = GLOBALS.storage.read_setting_public_key().unwrap(); // Load the latest PersonList event from the database let event = { if let Some(event) = GLOBALS .storage - .get_replaceable_event(list.event_kind(), my_pubkey, "")? + .get_replaceable_event(list.event_kind(), my_pubkey, &list.name())? { event.clone() } else { From 1da3257af85b1c03d20b26bec484f61cfcc9ec3e Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 10:13:06 +1300 Subject: [PATCH 29/81] Setting to login at startup; If not a migration, allows skipping. --- gossip-bin/src/ui/mod.rs | 26 ++++++++++++++++---------- gossip-bin/src/ui/settings/id.rs | 5 +++++ gossip-lib/src/lib.rs | 16 +++++++++++----- gossip-lib/src/overlord/mod.rs | 3 --- gossip-lib/src/settings.rs | 4 ++++ gossip-lib/src/storage/mod.rs | 1 + 6 files changed, 37 insertions(+), 18 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 5b97fff07..743e10502 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1871,20 +1871,26 @@ fn force_login(app: &mut GossipUi, ctx: &Context) { ui.heading("Passphrase Needed"); you::offer_unlock_priv_key(app, ui); - ui.add_space(10.0); - ui.label("We need to rebuild some data which may require decrypting DMs and Giftwraps to rebuild properly. For this reason, you need to login before the data migration runs."); + let data_migration = GLOBALS.wait_for_data_migration.load(Ordering::Relaxed); + + // If there is a data migration, explain + if data_migration { + ui.add_space(10.0); + ui.label("We need to rebuild some data which may require decrypting DMs and Giftwraps to rebuild properly. For this reason, you need to login before the data migration runs."); + } ui.add_space(15.0); - /* - if ui.button("Skip").clicked() { - // Stop waiting for login - GLOBALS - .wait_for_login - .store(false, std::sync::atomic::Ordering::Relaxed); - GLOBALS.wait_for_login_notify.notify_one(); + // If there is not a data migration, allow them to skip login + if ! data_migration { + if ui.button("Skip").clicked() { + // Stop waiting for login + GLOBALS + .wait_for_login + .store(false, std::sync::atomic::Ordering::Relaxed); + GLOBALS.wait_for_login_notify.notify_one(); + } } - */ ui.add_space(60.0); ui.separator(); diff --git a/gossip-bin/src/ui/settings/id.rs b/gossip-bin/src/ui/settings/id.rs index b8629cb44..980f62ad1 100644 --- a/gossip-bin/src/ui/settings/id.rs +++ b/gossip-bin/src/ui/settings/id.rs @@ -30,5 +30,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.label("(NOTE: changing this will not re-encrypt any existing encrypted private key)"); ui.add(Slider::new(&mut app.settings.log_n, 18..=22).text("logN iteratons")); + // Login at startup + ui.add_space(20.0); + ui.checkbox(&mut app.settings.login_at_startup, "Login at startup") + .on_hover_text("If set, you will be prompted for your password before gossip starts up."); + ui.add_space(20.0); } diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index eaaa98ac3..de2cc70b3 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -155,11 +155,17 @@ pub fn init() -> Result<(), Error> { // Load delegation tag GLOBALS.delegation.load()?; - // If we need to rebuild relationships - if GLOBALS.storage.get_flag_rebuild_relationships_needed() { - // And we have a key but have not unlocked it - if GLOBALS.signer.is_loaded() && !GLOBALS.signer.is_ready() { - // Indicate to the overlord and UI to wait for login before continuing + // If we have a key but have not unlocked it + if GLOBALS.signer.is_loaded() && !GLOBALS.signer.is_ready() { + // If we need to rebuild relationships + if GLOBALS.storage.get_flag_rebuild_relationships_needed() { + GLOBALS + .wait_for_login + .store(true, std::sync::atomic::Ordering::Relaxed); + GLOBALS + .wait_for_data_migration + .store(true, std::sync::atomic::Ordering::Relaxed); + } else if GLOBALS.storage.read_setting_login_at_startup() { GLOBALS .wait_for_login .store(true, std::sync::atomic::Ordering::Relaxed); diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 0ea89575b..2d6c9d20b 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -165,9 +165,6 @@ impl Overlord { // If we need to rebuild relationships, do so now if GLOBALS.storage.get_flag_rebuild_relationships_needed() { - GLOBALS - .wait_for_data_migration - .store(true, Ordering::Relaxed); GLOBALS.storage.rebuild_relationships(None)?; GLOBALS .wait_for_data_migration diff --git a/gossip-lib/src/settings.rs b/gossip-lib/src/settings.rs index 4addc4a79..ff652aa4a 100644 --- a/gossip-lib/src/settings.rs +++ b/gossip-lib/src/settings.rs @@ -41,6 +41,7 @@ pub struct Settings { // ID settings pub public_key: Option, pub log_n: u8, + pub login_at_startup: bool, // Network settings pub offline: bool, @@ -132,6 +133,7 @@ impl Default for Settings { Settings { public_key: default_setting!(public_key), log_n: default_setting!(log_n), + login_at_startup: default_setting!(login_at_startup), offline: default_setting!(offline), load_avatars: default_setting!(load_avatars), load_media: default_setting!(load_media), @@ -213,6 +215,7 @@ impl Settings { Settings { public_key: load_setting!(public_key), log_n: load_setting!(log_n), + login_at_startup: load_setting!(login_at_startup), offline: load_setting!(offline), load_avatars: load_setting!(load_avatars), load_media: load_setting!(load_media), @@ -290,6 +293,7 @@ impl Settings { let mut txn = GLOBALS.storage.get_write_txn()?; save_setting!(public_key, self, txn); save_setting!(log_n, self, txn); + save_setting!(login_at_startup, self, txn); save_setting!(offline, self, txn); save_setting!(load_avatars, self, txn); save_setting!(load_media, self, txn); diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index d9a0bd210..4f83c9d19 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -663,6 +663,7 @@ impl Storage { // setting value def_setting!(public_key, b"public_key", Option::, None); def_setting!(log_n, b"log_n", u8, 18); + def_setting!(login_at_startup, b"login_at_startup", bool, true); def_setting!(offline, b"offline", bool, false); def_setting!(load_avatars, b"load_avatars", bool, true); def_setting!(load_media, b"load_media", bool, true); From a6d59f7bd8420e5bafaf1c263b71b9c9b1411e91 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 11:05:14 +1300 Subject: [PATCH 30/81] ui: Pop-up confirmation when deleting a person list --- gossip-bin/src/ui/mod.rs | 2 ++ gossip-bin/src/ui/people/lists.rs | 34 ++++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 743e10502..b2291d296 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -420,6 +420,7 @@ struct GossipUi { entering_search_page: bool, editing_petname: bool, petname: String, + deleting_list: Option, // Collapsed threads collapsed: Vec, @@ -660,6 +661,7 @@ impl GossipUi { entering_search_page: false, editing_petname: false, petname: "".to_owned(), + deleting_list: None, collapsed: vec![], opened: HashSet::new(), visible_note_ids: vec![], diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index c8affb984..ddde8339f 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -1,6 +1,7 @@ use super::{GossipUi, Page}; use eframe::egui; -use egui::{Context, Ui}; +use egui::{Context, Ui, Vec2}; +use egui_winit::egui::vec2; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{PersonList, GLOBALS}; @@ -27,10 +28,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }; if matches!(list, PersonList::Custom(_)) { if ui.button("DELETE").clicked() { - // FIXME -- confirm with a popup first! - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::DeletePersonList(list)); + app.deleting_list = Some(list); } } }); @@ -47,4 +45,30 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.add_space(10.0); ui.separator(); ui.add_space(10.0); + + if let Some(list) = app.deleting_list { + const DLG_SIZE: Vec2 = vec2(250.0, 120.0); + let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical_centered(|ui| { + ui.label("Are you sure you want to delete:"); + ui.add_space(5.0); + ui.heading(list.name()); + ui.add_space(5.0); + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.deleting_list = None; + } + if ui.button("Delete").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::DeletePersonList(list)); + app.deleting_list = None; + } + }); + }); + }); + if ret.inner.clicked() { + app.deleting_list = None; + } + } } From 7b97349b00af6d03cbb5872dacb91469bbf7b521 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 11:10:49 +1300 Subject: [PATCH 31/81] Fix people needing relay lists to include all people lists, not just Followed --- gossip-lib/src/people.rs | 2 +- gossip-lib/src/storage/types/person2.rs | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 0d8797016..65f6d117c 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -198,7 +198,7 @@ impl People { .read_setting_relay_list_becomes_stale_hours() as i64; if let Ok(vec) = GLOBALS.storage.filter_people(|p| { - p.is_in_list(PersonList::Followed) + p.is_subscribed_to() && p.relay_list_last_received < stale && among_these.contains(&p.pubkey) }) { diff --git a/gossip-lib/src/storage/types/person2.rs b/gossip-lib/src/storage/types/person2.rs index 3986e527d..4192d2c22 100644 --- a/gossip-lib/src/storage/types/person2.rs +++ b/gossip-lib/src/storage/types/person2.rs @@ -124,6 +124,13 @@ impl Person2 { .is_person_in_list(&self.pubkey, list) .unwrap_or(false) } + + pub fn is_subscribed_to(&self) -> bool { + GLOBALS + .storage + .is_person_subscribed_to(&self.pubkey) + .unwrap_or(false) + } } impl PartialEq for Person2 { From 0afacb2ac46a935d43cbb7bfaf65133982b8b957 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 11:02:44 +1300 Subject: [PATCH 32/81] Follow someone on a list; retire FollowNew page (but keep menus for follow/mute) --- gossip-bin/src/ui/help/mod.rs | 4 +- gossip-bin/src/ui/mod.rs | 30 +++++----- gossip-bin/src/ui/people/follow.rs | 71 ----------------------- gossip-bin/src/ui/people/list.rs | 67 ++++++++++++++++++++- gossip-bin/src/ui/people/mod.rs | 5 +- gossip-bin/src/ui/wizard/follow_people.rs | 25 +++++--- gossip-lib/src/comms.rs | 6 +- gossip-lib/src/nip05.rs | 10 +++- gossip-lib/src/overlord/mod.rs | 36 ++++++++---- gossip-lib/src/people.rs | 27 ++++----- 10 files changed, 148 insertions(+), 133 deletions(-) delete mode 100644 gossip-bin/src/ui/people/follow.rs diff --git a/gossip-bin/src/ui/help/mod.rs b/gossip-bin/src/ui/help/mod.rs index 0ed497a2d..cc151f1fd 100644 --- a/gossip-bin/src/ui/help/mod.rs +++ b/gossip-bin/src/ui/help/mod.rs @@ -162,8 +162,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.indent("quickstartpullcontacts", |ui| { ui.horizontal_wrapped(|ui| { ui.label("On the"); - if ui.link("People > Follow Someone New").clicked() { - app.set_page(ctx, Page::PeopleFollowNew); + if ui.link("People > Lists> Followed").clicked() { + app.set_page(ctx, Page::PeopleList(PersonList::Followed)); } ui.label("page, follow somebody."); }); diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index b2291d296..fc52140ad 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -104,7 +104,6 @@ pub fn run() -> Result<(), Error> { enum Page { DmChatList, Feed(FeedKind), - PeopleFollowNew, // deprecated, will separately be part of the list page PeopleLists, PeopleList(PersonList), Person(PublicKey), @@ -140,7 +139,6 @@ impl Page { match self { Page::DmChatList => (SubMenu::Feeds.as_str(), "Private chats".into()), Page::Feed(feedkind) => ("Feed", feedkind.to_string()), - Page::PeopleFollowNew => (SubMenu::People.as_str(), "Follow new".into()), Page::PeopleLists => (SubMenu::People.as_str(), "Lists".into()), Page::PeopleList(list) => ("People", list.name()), Page::Person(pk) => { @@ -187,7 +185,7 @@ impl Page { match self { Page::DmChatList => cat_name(self), Page::Feed(_) => name_cat(self), - Page::PeopleFollowNew | Page::PeopleLists | Page::PeopleList(_) => cat_name(self), + Page::PeopleLists | Page::PeopleList(_) => cat_name(self), Page::Person(_) => name_cat(self), Page::YourKeys | Page::YourMetadata | Page::YourDelegation => cat_name(self), Page::Wizard(_) => name_cat(self), @@ -406,6 +404,7 @@ struct GossipUi { delegatee_tag_str: String, // User entry: general + entering_follow_someone_on_list: bool, follow_someone: String, add_relay: String, // dep clear_list_needs_confirm: bool, @@ -647,6 +646,7 @@ impl GossipUi { editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), + entering_follow_someone_on_list: false, follow_someone: "".to_owned(), add_relay: "".to_owned(), clear_list_needs_confirm: false, @@ -728,7 +728,7 @@ impl GossipUi { GLOBALS.feed.set_feed_to_person(pubkey.to_owned()); self.close_all_menus(ctx); } - Page::PeopleFollowNew | Page::PeopleLists | Page::Person(_) => { + Page::PeopleLists | Page::Person(_) => { self.open_menu(ctx, SubMenu::People); } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { @@ -891,7 +891,6 @@ impl GossipUi { let (mut cstate, header_response) = self.get_openable_menu(ui, ctx, SubMenu::People); cstate.show_body_indented(&header_response, ui, |ui| { - self.add_menu_item_page(ui, Page::PeopleFollowNew); self.add_menu_item_page(ui, Page::PeopleLists); }); self.after_openable_menu(ui, &cstate); @@ -1268,10 +1267,7 @@ impl eframe::App for GossipUi { }) .fill({ match self.page { - Page::PeopleFollowNew - | Page::PeopleLists - | Page::PeopleList(_) - | Page::Person(_) => { + Page::PeopleLists | Page::PeopleList(_) | Page::Person(_) => { if self.theme.dark_mode { ctx.style().visuals.panel_fill } else { @@ -1287,10 +1283,9 @@ impl eframe::App for GossipUi { match self.page { Page::DmChatList => dm_chat_list::update(self, ctx, frame, ui), Page::Feed(_) => feed::update(self, ctx, frame, ui), - Page::PeopleFollowNew - | Page::PeopleLists - | Page::PeopleList(_) - | Page::Person(_) => people::update(self, ctx, frame, ui), + Page::PeopleLists | Page::PeopleList(_) | Page::Person(_) => { + people::update(self, ctx, frame, ui) + } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { you::update(self, ctx, frame, ui) } @@ -1378,9 +1373,14 @@ impl GossipUi { } } if !followed && ui.button("Follow").clicked() { - let _ = GLOBALS.people.follow(&person.pubkey, true, true); + let _ = GLOBALS + .people + .follow(&person.pubkey, true, PersonList::Followed, true); } else if followed && ui.button("Unfollow").clicked() { - let _ = GLOBALS.people.follow(&person.pubkey, false, true); + let _ = + GLOBALS + .people + .follow(&person.pubkey, false, PersonList::Followed, true); } // Do not show 'Mute' if this is yourself diff --git a/gossip-bin/src/ui/people/follow.rs b/gossip-bin/src/ui/people/follow.rs deleted file mode 100644 index 729d734d0..000000000 --- a/gossip-bin/src/ui/people/follow.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::{GossipUi, Page}; -use eframe::egui; -use egui::{Context, Ui}; -use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::GLOBALS; -use nostr_types::{Profile, PublicKey}; - -pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { - ui.add_space(2.0); - ui.heading("Follow Someone"); - }); - - ui.add_space(10.0); - - ui.label( - "NOTICE: Gossip doesn't update the filters when you follow someone yet, so you have to restart the client to fetch their events. Will fix soon. -", - ); - - ui.label("NOTICE: use CTRL-V to paste (middle/right click won't work)"); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - ui.heading("Follow someone"); - - ui.horizontal(|ui| { - ui.label("Enter"); - ui.add( - text_edit_line!(app, app.follow_someone) - .hint_text("npub1, hex key, nprofile1, or user@domain"), - ); - }); - if ui.button("follow").clicked() { - if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, true)); - app.set_page(ctx, Page::Person(pubkey)); - } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.follow_someone.trim(), true) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, true)); - app.set_page(ctx, Page::Person(pubkey)); - } else if let Ok(profile) = Profile::try_from_bech32_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowNprofile(profile.clone(), true)); - app.set_page(ctx, Page::Person(profile.pubkey)); - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), - true, - )); - } else { - GLOBALS - .status_queue - .write() - .write("Invalid pubkey.".to_string()); - } - app.follow_someone = "".to_owned(); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); -} diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 50786b2f2..aa6cfe557 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -1,9 +1,11 @@ use super::{GossipUi, Page}; use crate::ui::widgets; use eframe::egui; -use egui::{Context, RichText, Ui}; +use egui::{Context, RichText, Ui, Vec2}; +use egui_winit::egui::vec2; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{Person, PersonList, GLOBALS}; +use nostr_types::{Profile, PublicKey}; pub(super) fn update( app: &mut GossipUi, @@ -162,12 +164,19 @@ pub(super) fn update( } ui.add_space(10.0); + + if ui.button("Follow New").clicked() { + app.entering_follow_someone_on_list = true; + } + ui.separator(); ui.add_space(10.0); ui.heading(format!("{} ({})", list.name(), people.len())); ui.add_space(14.0); + ui.separator(); + app.vert_scroll_area().show(ui, |ui| { for (person, public) in people.iter() { ui.horizontal(|ui| { @@ -219,4 +228,60 @@ pub(super) fn update( ui.separator(); } }); + + if app.entering_follow_someone_on_list { + const DLG_SIZE: Vec2 = vec2(400.0, 200.0); + let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.heading("Follow someone"); + + ui.horizontal(|ui| { + ui.label("Enter"); + ui.add( + text_edit_line!(app, app.follow_someone) + .hint_text("npub1, hex key, nprofile1, or user@domain"), + ); + }); + if ui.button("follow").clicked() { + if let Ok(pubkey) = + PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.entering_follow_someone_on_list = false; + } else if let Ok(pubkey) = + PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.entering_follow_someone_on_list = false; + } else if let Ok(profile) = + Profile::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( + profile.clone(), + list, + true, + )); + app.entering_follow_someone_on_list = false; + } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( + app.follow_someone.trim().to_owned(), + list, + true, + )); + } else { + GLOBALS + .status_queue + .write() + .write("Invalid pubkey.".to_string()); + } + app.follow_someone = "".to_owned(); + } + }); + if ret.inner.clicked() { + app.entering_follow_someone_on_list = false; + } + } } diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index bce75f257..be557840a 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -2,15 +2,12 @@ use super::{GossipUi, Page}; use eframe::egui; use egui::{Context, Ui}; -mod follow; mod list; mod lists; mod person; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - if app.page == Page::PeopleFollowNew { - follow::update(app, ctx, _frame, ui); - } else if app.page == Page::PeopleLists { + if app.page == Page::PeopleLists { lists::update(app, ctx, _frame, ui); } else if let Page::PeopleList(plist) = app.page { list::update(app, ctx, _frame, ui, plist); diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index 2ca728009..eb2acb59a 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -95,24 +95,31 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr } if ui.button("follow").clicked() { if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, true)); + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( + pubkey, + PersonList::Followed, + true, + )); } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.follow_someone.trim(), true) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, true)); + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( + pubkey, + PersonList::Followed, + true, + )); } else if let Ok(profile) = Profile::try_from_bech32_string(app.follow_someone.trim(), true) { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowNprofile(profile, true)); + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( + profile, + PersonList::Followed, + true, + )); } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( app.follow_someone.trim().to_owned(), + PersonList::Followed, true, )); } else { diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index df89839f9..70fbfc7dd 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -53,13 +53,13 @@ pub enum ToOverlordMessage { FetchEventAddr(EventAddr), /// Calls [follow_pubkey](crate::Overlord::follow_pubkey) - FollowPubkey(PublicKey, bool), + FollowPubkey(PublicKey, PersonList, bool), /// Calls [follow_nip05](crate::Overlord::follow_nip05) - FollowNip05(String, bool), + FollowNip05(String, PersonList, bool), /// Calls [follow_nprofile](crate::Overlord::follow_nprofile) - FollowNprofile(Profile, bool), + FollowNprofile(Profile, PersonList, bool), /// Calls [generate_private_key](crate::Overlord::generate_private_key) GeneratePrivateKey(String), diff --git a/gossip-lib/src/nip05.rs b/gossip-lib/src/nip05.rs index 4ad1c4d3a..58b873692 100644 --- a/gossip-lib/src/nip05.rs +++ b/gossip-lib/src/nip05.rs @@ -1,6 +1,6 @@ use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; -use crate::people::Person; +use crate::people::{Person, PersonList}; use crate::person_relay::PersonRelay; use nostr_types::{Metadata, Nip05, PublicKey, RelayUrl, Unixtime}; use std::sync::atomic::Ordering; @@ -91,7 +91,11 @@ pub async fn validate_nip05(person: Person) -> Result<(), Error> { Ok(()) } -pub async fn get_and_follow_nip05(nip05: String, public: bool) -> Result<(), Error> { +pub async fn get_and_follow_nip05( + nip05: String, + list: PersonList, + public: bool, +) -> Result<(), Error> { // Split their DNS ID let (user, domain) = parse_nip05(&nip05)?; @@ -116,7 +120,7 @@ pub async fn get_and_follow_nip05(nip05: String, public: bool) -> Result<(), Err .await?; // Mark as followed, publicly - GLOBALS.people.follow(&pubkey, true, public)?; + GLOBALS.people.follow(&pubkey, true, list, public)?; tracing::info!("Followed {}", &nip05); diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 2d6c9d20b..06cbeaaef 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -586,14 +586,14 @@ impl Overlord { ToOverlordMessage::FetchEventAddr(ea) => { self.fetch_event_addr(ea).await?; } - ToOverlordMessage::FollowPubkey(pubkey, public) => { - self.follow_pubkey(pubkey, public).await?; + ToOverlordMessage::FollowPubkey(pubkey, list, public) => { + self.follow_pubkey(pubkey, list, public).await?; } - ToOverlordMessage::FollowNip05(nip05, public) => { - Self::follow_nip05(nip05, public).await?; + ToOverlordMessage::FollowNip05(nip05, list, public) => { + Self::follow_nip05(nip05, list, public).await?; } - ToOverlordMessage::FollowNprofile(nprofile, public) => { - self.follow_nprofile(nprofile, public).await?; + ToOverlordMessage::FollowNprofile(nprofile, list, public) => { + self.follow_nprofile(nprofile, list, public).await?; } ToOverlordMessage::GeneratePrivateKey(password) => { Self::generate_private_key(password).await?; @@ -1152,17 +1152,22 @@ impl Overlord { } /// Follow a person by `PublicKey` - pub async fn follow_pubkey(&mut self, pubkey: PublicKey, public: bool) -> Result<(), Error> { - GLOBALS.people.follow(&pubkey, true, public)?; + pub async fn follow_pubkey( + &mut self, + pubkey: PublicKey, + list: PersonList, + public: bool, + ) -> Result<(), Error> { + GLOBALS.people.follow(&pubkey, true, list, public)?; self.subscribe_discover(vec![pubkey], None).await?; tracing::debug!("Followed {}", &pubkey.as_hex_string()); Ok(()) } /// Follow a person by a nip-05 address - pub async fn follow_nip05(nip05: String, public: bool) -> Result<(), Error> { + pub async fn follow_nip05(nip05: String, list: PersonList, public: bool) -> Result<(), Error> { std::mem::drop(tokio::spawn(async move { - if let Err(e) = crate::nip05::get_and_follow_nip05(nip05, public).await { + if let Err(e) = crate::nip05::get_and_follow_nip05(nip05, list, public).await { tracing::error!("{}", e); } })); @@ -1170,8 +1175,15 @@ impl Overlord { } /// Follow a person by a `Profile` (nprofile1...) - pub async fn follow_nprofile(&mut self, nprofile: Profile, public: bool) -> Result<(), Error> { - GLOBALS.people.follow(&nprofile.pubkey, true, public)?; + pub async fn follow_nprofile( + &mut self, + nprofile: Profile, + list: PersonList, + public: bool, + ) -> Result<(), Error> { + GLOBALS + .people + .follow(&nprofile.pubkey, true, list, public)?; // Set their relays for relay in nprofile.relays.iter() { diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 65f6d117c..738fa0d0f 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -719,27 +719,28 @@ impl People { } /// Follow (or unfollow) the public key - pub fn follow(&self, pubkey: &PublicKey, follow: bool, public: bool) -> Result<(), Error> { + pub fn follow( + &self, + pubkey: &PublicKey, + follow: bool, + list: PersonList, + public: bool, + ) -> Result<(), Error> { let mut txn = GLOBALS.storage.get_write_txn()?; if follow { - GLOBALS.storage.add_person_to_list( - pubkey, - PersonList::Followed, - public, - Some(&mut txn), - )?; + GLOBALS + .storage + .add_person_to_list(pubkey, list, public, Some(&mut txn))?; } else { - GLOBALS.storage.remove_person_from_list( - pubkey, - PersonList::Followed, - Some(&mut txn), - )?; + GLOBALS + .storage + .remove_person_from_list(pubkey, list, Some(&mut txn))?; } GLOBALS.ui_people_to_invalidate.write().push(*pubkey); GLOBALS.storage.set_person_list_last_edit_time( - PersonList::Followed, + list, Unixtime::now().unwrap().0, Some(&mut txn), )?; From 645588b7c9874c7abcad006138b8d08d82241127 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 11:49:52 +1300 Subject: [PATCH 33/81] Replace degraded Person menu with it's last item People Lists --- gossip-bin/src/ui/mod.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index fc52140ad..31248e292 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -139,7 +139,7 @@ impl Page { match self { Page::DmChatList => (SubMenu::Feeds.as_str(), "Private chats".into()), Page::Feed(feedkind) => ("Feed", feedkind.to_string()), - Page::PeopleLists => (SubMenu::People.as_str(), "Lists".into()), + Page::PeopleLists => ("Person Lists", "Person Lists".into()), Page::PeopleList(list) => ("People", list.name()), Page::Person(pk) => { let name = gossip_lib::names::best_name_from_pubkey_lookup(pk); @@ -197,7 +197,6 @@ impl Page { #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] enum SubMenu { Feeds, - People, Relays, Account, Help, @@ -207,7 +206,6 @@ impl SubMenu { fn as_str(&self) -> &'static str { match self { SubMenu::Feeds => "Feeds", - SubMenu::People => "People", SubMenu::Relays => "Relays", SubMenu::Account => "Account", SubMenu::Help => "Help", @@ -217,7 +215,6 @@ impl SubMenu { fn as_id_str(&self) -> &'static str { match self { SubMenu::Feeds => "feeds_submenu_id", - SubMenu::People => "people_submenu_id", SubMenu::Account => "account_submenu_id", SubMenu::Relays => "relays_submenu_id", SubMenu::Help => "help_submenu_id", @@ -494,7 +491,6 @@ impl GossipUi { let mut submenu_ids: HashMap = HashMap::new(); submenu_ids.insert(SubMenu::Feeds, egui::Id::new(SubMenu::Feeds.as_id_str())); - submenu_ids.insert(SubMenu::People, egui::Id::new(SubMenu::People.as_id_str())); submenu_ids.insert( SubMenu::Account, egui::Id::new(SubMenu::Account.as_id_str()), @@ -729,7 +725,7 @@ impl GossipUi { self.close_all_menus(ctx); } Page::PeopleLists | Page::Person(_) => { - self.open_menu(ctx, SubMenu::People); + self.close_all_menus(ctx); } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { self.open_menu(ctx, SubMenu::Account); @@ -886,15 +882,14 @@ impl GossipUi { } } - // ---- People SubMenu ---- + // People Lists + if self + .add_selected_label(ui, self.page == Page::PeopleLists, "People Lists") + .clicked() { - let (mut cstate, header_response) = - self.get_openable_menu(ui, ctx, SubMenu::People); - cstate.show_body_indented(&header_response, ui, |ui| { - self.add_menu_item_page(ui, Page::PeopleLists); - }); - self.after_openable_menu(ui, &cstate); + self.set_page(ctx, Page::PeopleLists); } + // ---- Relays SubMenu ---- { let (mut cstate, header_response) = From 01d4c262ec4d22c9bf642f8c69c394d664962147 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 12:04:43 +1300 Subject: [PATCH 34/81] Create new person list code --- gossip-bin/src/ui/mod.rs | 4 ++++ gossip-bin/src/ui/people/lists.rs | 37 ++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 31248e292..ea710a0d4 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -417,6 +417,8 @@ struct GossipUi { editing_petname: bool, petname: String, deleting_list: Option, + creating_list: bool, + new_list_name: String, // Collapsed threads collapsed: Vec, @@ -658,6 +660,8 @@ impl GossipUi { editing_petname: false, petname: "".to_owned(), deleting_list: None, + creating_list: false, + new_list_name: "".to_owned(), collapsed: vec![], opened: HashSet::new(), visible_note_ids: vec![], diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index ddde8339f..ab17146e6 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -34,12 +34,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }); } if ui.button("Create a new list").clicked() { - // FIXME -- prompt for a name with a popup, then create with: - // let _ = PersonList::allocate(name, None); - GLOBALS - .status_queue - .write() - .write("Person List Create is NOT YET IMPLEMENTED".to_string()); + app.creating_list = true; } ui.add_space(10.0); @@ -70,5 +65,35 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra if ret.inner.clicked() { app.deleting_list = None; } + } else if app.creating_list { + const DLG_SIZE: Vec2 = vec2(250.0, 120.0); + let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical_centered(|ui| { + ui.heading("Creating a new Person List"); + ui.add(text_edit_line!(app, app.new_list_name)); + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.creating_list = false; + } + if ui.button("Create").clicked() { + if !app.new_list_name.is_empty() { + if let Err(e) = PersonList::allocate(&app.new_list_name, None) { + GLOBALS.status_queue.write().write(format!("{}", e)); + } else { + app.creating_list = false; + } + } else { + GLOBALS + .status_queue + .write() + .write("Person List name must not be empty".to_string()); + } + } + }); + }); + }); + if ret.inner.clicked() { + app.deleting_list = None; + } } } From 92d6a19c7ea17cb126752a431b2f7140fbfb994f Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 12:21:13 +1300 Subject: [PATCH 35/81] Slight improvement of login page --- gossip-bin/src/ui/mod.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index ea710a0d4..8af408fc8 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1869,7 +1869,7 @@ fn force_login(app: &mut GossipUi, ctx: &Context) { }) }) .show(ctx, |ui| { - ui.heading("Passphrase Needed"); + ui.heading("Login"); you::offer_unlock_priv_key(app, ui); let data_migration = GLOBALS.wait_for_data_migration.load(Ordering::Relaxed); @@ -1891,14 +1891,14 @@ fn force_login(app: &mut GossipUi, ctx: &Context) { .store(false, std::sync::atomic::Ordering::Relaxed); GLOBALS.wait_for_login_notify.notify_one(); } - } - - ui.add_space(60.0); - ui.separator(); - ui.add_space(10.0); + } else { + ui.add_space(60.0); + ui.separator(); + ui.add_space(10.0); - ui.label("In case you cannot login, here is your escape hatch:"); - you::offer_delete(app, ui); + ui.label("In case you cannot login, here is your escape hatch:"); + you::offer_delete(app, ui); + } }); } From 1bc943cd6e41534197bce938f3af5d12504a8a36 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 4 Dec 2023 14:38:48 +1300 Subject: [PATCH 36/81] When subscribing to EncryptedDirectMessaages, include a #p tag of the user (so relays can check it) --- gossip-lib/src/overlord/minion/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 0fa3565a8..a17d84e49 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -480,8 +480,8 @@ impl Minion { } }; - // Allow all feed related event kinds (including DMs) - let event_kinds = crate::feed::feed_related_event_kinds(true); + // Allow all feed related event kinds (excluding DMs) + let event_kinds = crate::feed::feed_related_event_kinds(false); if !followed_pubkeys.is_empty() { let pkp: Vec = followed_pubkeys.iter().map(|pk| pk.into()).collect(); @@ -765,11 +765,12 @@ impl Minion { // globally, and have to be limited to recent ones. let mut authors: Vec = dmchannel.keys().iter().map(|k| k.into()).collect(); - authors.push(pkh); + authors.push(pkh.clone()); let filters: Vec = vec![Filter { authors, kinds: vec![EventKind::EncryptedDirectMessage], + p: vec![pkh], // tagging the user ..Default::default() }]; From 5ab6ddac5366c793de85ee97c9a429712789fb57 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 10:25:02 +1300 Subject: [PATCH 37/81] Shorten the fetcher_metadata_looptime_ms default from 3 seconds to 1.75 seconds --- gossip-lib/src/storage/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 4f83c9d19..52b42e8c6 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -829,7 +829,7 @@ impl Storage { fetcher_metadata_looptime_ms, b"fetcher_metadata_looptime_ms", u64, - 3000 + 1750 ); def_setting!(fetcher_looptime_ms, b"fetcher_looptime_ms", u64, 1800); def_setting!( From 22aecfb8e8ab8fb5abfefe5e389203df484df8aa Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 10:39:27 +1300 Subject: [PATCH 38/81] Improve logic for when to fetch metadata --- gossip-lib/src/overlord/mod.rs | 10 +++ gossip-lib/src/people.rs | 140 +++++++++++++++++++-------------- 2 files changed, 91 insertions(+), 59 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 06cbeaaef..8f15605dc 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2439,6 +2439,11 @@ impl Overlord { /// Subscribe, fetch, and update metadata for the person pub async fn update_metadata(&mut self, pubkey: PublicKey) -> Result<(), Error> { + + // Indicate that we are doing this, as the People manager wants to know + // for it's retry logic + GLOBALS.people.metadata_fetch_initiated(&[pubkey]); + let best_relays = GLOBALS.storage.get_best_relays(pubkey, Direction::Write)?; let num_relays_per_person = GLOBALS.storage.read_setting_num_relays_per_person(); @@ -2471,6 +2476,11 @@ impl Overlord { &mut self, mut pubkeys: Vec, ) -> Result<(), Error> { + + // Indicate that we are doing this, as the People manager wants to know + // for it's retry logic + GLOBALS.people.metadata_fetch_initiated(&pubkeys); + let num_relays_per_person = GLOBALS.storage.read_setting_num_relays_per_person(); let mut map: HashMap> = HashMap::new(); for pubkey in pubkeys.drain(..) { diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 738fa0d0f..d4a3f6c26 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -62,13 +62,15 @@ pub struct People { // the person's NIP-05 when that metadata come in. We remember this here. recheck_nip05: DashSet, - // People that need metadata, which the UI has asked for. These people - // might simply not be loaded from the database yet. - need_metadata: DashSet, + // People of interest that the UI is showing, whose metadata should + // be updated if it is stale. + people_of_interest: DashSet, - // People who we already tried to get their metadata. We only try once - // per gossip run (this set only grows) - tried_metadata: DashSet, + // Metadata fetches in progress. Once these get too old we can remove them + // and consider them to have failed. + // This only relates to the Metadata event, not subsequent avatar or nip05 + // loads. + fetching_metadata: DashMap, /// Latest person list event data for each PersonList pub latest_person_list_event_data: DashMap, @@ -88,8 +90,8 @@ impl People { avatars_temp: DashMap::new(), avatars_pending_processing: DashSet::new(), recheck_nip05: DashSet::new(), - need_metadata: DashSet::new(), - tried_metadata: DashSet::new(), + people_of_interest: DashSet::new(), + fetching_metadata: DashMap::new(), latest_person_list_event_data: DashMap::new(), } } @@ -224,74 +226,94 @@ impl People { Ok(()) } - /// If this person doesn't have metadata, and we are automatically fetching - /// metadata, then add this person to the list of people that need metadata. + /// Mark this person as a person who the UI wants fresh metadata for. + /// maybe_fetch_metadata() will do the processing later on. pub fn person_of_interest(&self, pubkey: PublicKey) { - // Don't get metadata if disabled + // Don't set if metadata if disabled if !GLOBALS.storage.read_setting_automatically_fetch_metadata() { return; } - // Don't try over and over. We try just once per gossip run. - if self.tried_metadata.contains(&pubkey) { - return; - } - - match GLOBALS.storage.read_person(&pubkey) { - Ok(Some(person)) => { - // We need metadata if it is missing or old - let need = { - // Metadata refresh interval - let now = Unixtime::now().unwrap(); - let stale = Duration::from_secs( - 60 * 60 * GLOBALS.storage.read_setting_metadata_becomes_stale_hours(), - ); - person.metadata_created_at.is_none() - || person.metadata_last_received < (now - stale).0 - }; - if !need { - return; - } - - // Record that we need it. - // the periodic task will take care of it. - if !self.need_metadata.contains(&pubkey) { - self.need_metadata.insert(pubkey); - } - } - _ => { - // Trigger a future create and load - self.create_if_missing(pubkey); + self.people_of_interest.insert(pubkey); + } - // Don't load metadata now, we may have it on disk and get - // it from the future load. - } + /// The overlord calls this to indicate that it is fetching metadata + /// for this person from relays + pub fn metadata_fetch_initiated(&self, pubkeys: &[PublicKey]) { + let now = Unixtime::now().unwrap(); + for pubkey in pubkeys { + self.fetching_metadata.insert(*pubkey, now); } } /// This is run periodically. It checks the database first, only then does it /// ask the overlord to update the metadata from the relays. async fn maybe_fetch_metadata(&self) { - let mut verified_need: Vec = Vec::new(); - - // Take from self.need_metadata; - let mut need_metadata: Vec = self - .need_metadata + // Take everybody out of self.people_of_interest, into a local var + let mut people_of_interest: Vec = self + .people_of_interest .iter() .map(|refmulti| refmulti.key().to_owned()) .collect(); - self.need_metadata.clear(); + self.people_of_interest.clear(); - if !need_metadata.is_empty() { - tracing::debug!("Periodic metadata fetch for {} people", need_metadata.len()); + if !people_of_interest.is_empty() { + tracing::trace!( + "Periodic metadata check against {} people", + people_of_interest.len() + ); } - for pubkey in need_metadata.drain(..) { - tracing::debug!("Seeking metadata for {}", pubkey.as_hex_string()); - verified_need.push(pubkey); - self.tried_metadata.insert(pubkey); + let now = Unixtime::now().unwrap(); + let stale = Duration::from_secs( + 60 * 60 * GLOBALS.storage.read_setting_metadata_becomes_stale_hours(), + ); + + let mut verified_need: Vec = Vec::new(); + + for pubkey in people_of_interest.drain(..) { + // If we already tried fetching_metadata (within the stale period) + // skip them + // NOTE: if we tried and it never came in, odds are low that trying + // again will make any difference. Either the person doesn't have + // metadata or we don't have their proper relays. So a shorter timeout + // in this circumstance isn't such a great idea. + if let Some(fetching_asof) = self.fetching_metadata.get(&pubkey) { + if fetching_asof.0 >= (now - stale).0 { + continue; + } else { + // remove stale entry + self.fetching_metadata.remove(&pubkey); + } + } + + match GLOBALS.storage.read_person(&pubkey) { + Ok(Some(person)) => { + // We need metadata if it is missing or old + let need = { + // Metadata refresh interval + person.metadata_created_at.is_none() + || person.metadata_last_received < (now - stale).0 + }; + if !need { + continue; + } + + tracing::debug!("Seeking metadata for {}", pubkey.as_hex_string()); + verified_need.push(pubkey); + } + _ => { + // Trigger a future create and load + self.create_if_missing(pubkey); + // Don't load metadata now, we may have it on disk and get + // it from the future load. + } + } } + // This fires off the minions to fetch metadata events + // When they come in, process.rs handles it by calling + // GLOBALS.people.update_metadata() [down below] let _ = GLOBALS .to_overlord .send(ToOverlordMessage::UpdateMetadataInBulk(verified_need)); @@ -307,6 +329,9 @@ impl People { metadata: Metadata, asof: Unixtime, ) -> Result<(), Error> { + // Remove from fetching metadata (fetch is complete) + self.fetching_metadata.remove(pubkey); + // Sync in from database first self.create_all_if_missing(&[*pubkey])?; @@ -322,9 +347,6 @@ impl People { person.metadata_last_received = now.0; GLOBALS.storage.write_person(&person, None)?; - // Remove from the list of people that need metadata - self.need_metadata.remove(pubkey); - // Determine whether it is fresh let fresh = match person.metadata_created_at { Some(metadata_created_at) => asof.0 > metadata_created_at, From 2490f55651adbc27f4bcdf65d5e1d0ccc1513961 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 11:20:18 +1300 Subject: [PATCH 39/81] Fix: update person list last edit times --- gossip-lib/src/overlord/mod.rs | 2 - gossip-lib/src/storage/mod.rs | 48 +++++++++++++++++--- gossip-lib/src/storage/types/person_list1.rs | 34 +++++++++++--- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 8f15605dc..089c71126 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2439,7 +2439,6 @@ impl Overlord { /// Subscribe, fetch, and update metadata for the person pub async fn update_metadata(&mut self, pubkey: PublicKey) -> Result<(), Error> { - // Indicate that we are doing this, as the People manager wants to know // for it's retry logic GLOBALS.people.metadata_fetch_initiated(&[pubkey]); @@ -2476,7 +2475,6 @@ impl Overlord { &mut self, mut pubkeys: Vec, ) -> Result<(), Error> { - // Indicate that we are doing this, as the People manager wants to know // for it's retry logic GLOBALS.people.metadata_fetch_initiated(&pubkeys); diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 52b42e8c6..595faefc4 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -2294,9 +2294,27 @@ impl Storage { public: bool, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - let mut map = self.read_person_lists(pubkey)?; - map.insert(list, public); - self.write_person_lists(pubkey, map, rw_txn) + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + let mut map = self.read_person_lists(pubkey)?; + map.insert(list, public); + self.write_person_lists(pubkey, map, Some(txn))?; + let now = Unixtime::now().unwrap(); + self.set_person_list_last_edit_time(list, now.0, Some(txn))?; + Ok(()) + }; + + match rw_txn { + Some(txn) => { + f(txn)?; + } + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) } /// Remove a person from a list @@ -2306,9 +2324,27 @@ impl Storage { list: PersonList, rw_txn: Option<&mut RwTxn<'a>>, ) -> Result<(), Error> { - let mut map = self.read_person_lists(pubkey)?; - map.remove(&list); - self.write_person_lists(pubkey, map, rw_txn) + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + let mut map = self.read_person_lists(pubkey)?; + map.remove(&list); + self.write_person_lists(pubkey, map, Some(txn))?; + let now = Unixtime::now().unwrap(); + self.set_person_list_last_edit_time(list, now.0, Some(txn))?; + Ok(()) + }; + + match rw_txn { + Some(txn) => { + f(txn)?; + } + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) } /// Rebuild relationships diff --git a/gossip-lib/src/storage/types/person_list1.rs b/gossip-lib/src/storage/types/person_list1.rs index 8f8c2c27b..981299515 100644 --- a/gossip-lib/src/storage/types/person_list1.rs +++ b/gossip-lib/src/storage/types/person_list1.rs @@ -1,7 +1,7 @@ use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; use heed::RwTxn; -use nostr_types::EventKind; +use nostr_types::{EventKind, Unixtime}; use speedy::{Readable, Writable}; /// Lists people can be added to @@ -70,7 +70,7 @@ impl PersonList1 { } /// Allocate a new PersonList1 with the given name - pub fn allocate(name: &str, txn: Option<&mut RwTxn<'_>>) -> Result { + pub fn allocate<'a>(name: &str, txn: Option<&mut RwTxn<'a>>) -> Result { // Do not allocate for well-known names if name == "Followed" { return Ok(PersonList1::Followed); @@ -93,10 +93,32 @@ impl PersonList1 { continue; } map.insert(i, name.to_owned()); - GLOBALS - .storage - .write_setting_custom_person_list_map(&map, txn)?; - return Ok(PersonList1::Custom(i)); + + let list = PersonList1::Custom(i); + + let f = |txn: &mut RwTxn<'a>| -> Result { + // Now (creation) is when it was last edited + let now = Unixtime::now().unwrap(); + GLOBALS + .storage + .set_person_list_last_edit_time(list, now.0, Some(txn))?; + + GLOBALS + .storage + .write_setting_custom_person_list_map(&map, Some(txn))?; + + Ok(PersonList1::Custom(i)) + }; + + return match txn { + Some(txn) => f(txn), + None => { + let mut txn = GLOBALS.storage.get_write_txn()?; + let output = f(&mut txn)?; + txn.commit()?; + Ok(output) + } + }; } Err(ErrorKind::NoSlotsRemaining.into()) From 28d36abdea9dc4960038df0f2348b017e5f1965b Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 11:25:45 +1300 Subject: [PATCH 40/81] Fetch metadata for people when you enter their page, irrespective of when it was most recently checked --- gossip-bin/src/ui/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 8af408fc8..987204f4a 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -728,9 +728,17 @@ impl GossipUi { GLOBALS.feed.set_feed_to_person(pubkey.to_owned()); self.close_all_menus(ctx); } - Page::PeopleLists | Page::Person(_) => { + Page::PeopleLists => { self.close_all_menus(ctx); } + Page::Person(pubkey) => { + self.close_all_menus(ctx); + // Fetch metadata for that person at the page switch + // (this bypasses checking if it was done recently) + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdateMetadata(*pubkey)); + } Page::YourKeys | Page::YourMetadata | Page::YourDelegation => { self.open_menu(ctx, SubMenu::Account); } From 75236a734a1aac769403cc9de18024d26016d1ed Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 11:29:12 +1300 Subject: [PATCH 41/81] Always give some sort of feedback if updating a person list fails --- gossip-lib/src/overlord/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 089c71126..ca30b828e 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2529,6 +2529,10 @@ impl Overlord { { event.clone() } else { + GLOBALS + .status_queue + .write() + .write("Could not find a person-list event to update from".to_string()); return Ok(()); // we have no event to update from, so we are done } }; From e92fcf2a37874da57013cc58c59f44601dfed5c8 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 11:37:49 +1300 Subject: [PATCH 42/81] debug.sh --- debug.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 debug.sh diff --git a/debug.sh b/debug.sh new file mode 100755 index 000000000..d38ffb357 --- /dev/null +++ b/debug.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cargo build --features=lang-cjk,video-ffmpeg --release && \ + RUST_BACKTRACE=1 RUST_LOG="info,gossip_lib=debug" ./target/release/gossip "$@" \ + | tee gossip.log.txt + From ff83ec86e76d1866e27bb41bbe9552ec4cc3dd42 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Tue, 5 Dec 2023 12:11:01 +1300 Subject: [PATCH 43/81] Do not try to decrypt empty contents; Also allow merging personlist w/o login if it has no contents --- gossip-lib/src/overlord/mod.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index ca30b828e..8718079ae 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -2506,19 +2506,19 @@ impl Overlord { Ok(()) } - /// Update the local mute list from the last MuteList event received. + /// Update the local person list from the last event received. pub async fn update_person_list(&mut self, list: PersonList, merge: bool) -> Result<(), Error> { - // We need a private key to decrypt the content - if !GLOBALS.signer.is_ready() { - GLOBALS.status_queue.write().write( - "You need to be logged in to update a PersonList due to encrypted contents" - .to_string(), - ); - return Ok(()); - } - // we cannot do anything without an identity setup first - let my_pubkey = GLOBALS.storage.read_setting_public_key().unwrap(); + let my_pubkey = match GLOBALS.storage.read_setting_public_key() { + Some(pk) => pk, + None => { + GLOBALS + .status_queue + .write() + .write("You cannot update person lists without an identity".to_string()); + return Ok(()); + } + }; // Load the latest PersonList event from the database let event = { @@ -2537,6 +2537,15 @@ impl Overlord { } }; + // If we need to decrypt contents, we must have a private key ready + if list != PersonList::Followed && !event.content.is_empty() && !GLOBALS.signer.is_ready() { + GLOBALS.status_queue.write().write( + "You need to be logged in to update a PersonList due to encrypted contents" + .to_string(), + ); + return Ok(()); + } + let now = Unixtime::now().unwrap(); let mut txn = GLOBALS.storage.get_write_txn()?; @@ -2572,7 +2581,7 @@ impl Overlord { } // Private entries - if list != PersonList::Followed { + if list != PersonList::Followed && !event.content.is_empty() { let decrypted_content = GLOBALS.signer.decrypt_nip04(&my_pubkey, &event.content)?; let tags: Vec = serde_json::from_slice(&decrypted_content)?; From 0b82798a8d43ebf91e4ebdf658b920498c11ab1e Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Mon, 4 Dec 2023 19:38:15 -0600 Subject: [PATCH 44/81] widgets::page_header - unify right edge spacing --- gossip-bin/src/ui/relays/active.rs | 1 - gossip-bin/src/ui/relays/coverage.rs | 1 - gossip-bin/src/ui/relays/known.rs | 1 - gossip-bin/src/ui/relays/mine.rs | 1 - gossip-bin/src/ui/widgets/mod.rs | 8 ++++---- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index b0d95588c..6208a7d03 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -14,7 +14,6 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra let is_editing = app.relays.edit.is_some(); widgets::page_header(ui, Page::RelaysActivityMonitor.name(), |ui| { ui.set_enabled(!is_editing); - ui.add_space(20.0); super::configure_list_btn(app, ui); ui.add_space(20.0); super::relay_filter_combo(app, ui); diff --git a/gossip-bin/src/ui/relays/coverage.rs b/gossip-bin/src/ui/relays/coverage.rs index f5985b60f..af51cab82 100644 --- a/gossip-bin/src/ui/relays/coverage.rs +++ b/gossip-bin/src/ui/relays/coverage.rs @@ -128,7 +128,6 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra app.settings.num_relays_per_person ), |ui| { - ui.add_space(20.0); ui.spacing_mut().button_padding *= 2.0; if ui .button("Pick Relays Again") diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index 593963e27..f403548fd 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -10,7 +10,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr let is_editing = app.relays.edit.is_some(); widgets::page_header(ui, Page::RelaysKnownNetwork.name(), |ui| { ui.set_enabled(!is_editing); - ui.add_space(20.0); super::configure_list_btn(app, ui); ui.add_space(20.0); super::relay_filter_combo(app, ui); diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index 3ab44e040..2a765c029 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -11,7 +11,6 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr let is_editing = app.relays.edit.is_some(); widgets::page_header(ui, Page::RelaysMine.name(), |ui| { ui.set_enabled(!is_editing); - ui.add_space(20.0); super::configure_list_btn(app, ui); ui.add_space(20.0); super::relay_filter_combo(app, ui); diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 850439010..4a33d3d56 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -50,10 +50,10 @@ pub fn page_header( ui.add_space(2.0); ui.heading(title); }); - ui.with_layout( - egui::Layout::right_to_left(egui::Align::Center), - right_aligned_content, - ); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.add_space(20.0); + right_aligned_content(ui); + }); }); ui.add_space(10.0); }); From 559d8659f54273c189dbf1daa213b08fd925ba6e Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Mon, 4 Dec 2023 19:39:03 -0600 Subject: [PATCH 45/81] PeopleList: Style same as DM chat list --- gossip-bin/src/ui/mod.rs | 2 +- gossip-bin/src/ui/people/lists.rs | 76 +++++++++++++++++++------------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 987204f4a..a12cc913e 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -1274,7 +1274,7 @@ impl eframe::App for GossipUi { }) .fill({ match self.page { - Page::PeopleLists | Page::PeopleList(_) | Page::Person(_) => { + Page::Person(_) => { if self.theme.dark_mode { ctx.style().visuals.panel_fill } else { diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index ab17146e6..a07f316f6 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -1,45 +1,63 @@ +use crate::ui::widgets; + use super::{GossipUi, Page}; use eframe::egui; use egui::{Context, Ui, Vec2}; -use egui_winit::egui::vec2; +use egui_winit::egui::{vec2, Label, RichText, Sense}; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{PersonList, GLOBALS}; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { - ui.add_space(2.0); - ui.heading("Lists"); + widgets::page_header(ui, Page::PeopleLists.name(), |ui| { + if ui.button("Create a new list").clicked() { + app.creating_list = true; + } }); - ui.add_space(10.0); - + let enable_scroll = true; let all_lists = PersonList::all_lists(); - for (list, listname) in all_lists { - let count = GLOBALS - .storage - .get_people_in_list(list) - .map(|v| v.len()) - .unwrap_or(0); - ui.horizontal(|ui| { - ui.label(format!("({}) ", count)); - if ui.link(listname).clicked() { - app.set_page(ctx, Page::PeopleList(list)); - }; - if matches!(list, PersonList::Custom(_)) { - if ui.button("DELETE").clicked() { - app.deleting_list = Some(list); + let color = app.theme.accent_color(); + + app.vert_scroll_area() + .id_source("people_lists_scroll") + .enable_scrolling(enable_scroll) + .show(ui, |ui| { + for (list, listname) in all_lists { + let count = GLOBALS + .storage + .get_people_in_list(list) + .map(|v| v.len()) + .unwrap_or(0); + let row_response = widgets::list_entry::make_frame(ui).show(ui, |ui| { + ui.set_min_width(ui.available_width()); + + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.add(Label::new(RichText::new(listname).heading().color(color))); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + if matches!(list, PersonList::Custom(_)) { + if ui.link("delete list").clicked() { + app.deleting_list = Some(list); + } + } + }); + }); + ui.horizontal(|ui| { + ui.label(format!("Entries: {} ", count)); + }); + }); + }); + if row_response + .response + .interact(Sense::click()) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { + app.set_page(ctx, Page::PeopleList(list)); } } }); - } - if ui.button("Create a new list").clicked() { - app.creating_list = true; - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); if let Some(list) = app.deleting_list { const DLG_SIZE: Vec2 = vec2(250.0, 120.0); From d8970baf3a4f6e6b5fbddd9ce783d7de332c2eb6 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Mon, 4 Dec 2023 20:05:52 -0600 Subject: [PATCH 46/81] PersonList: Adjust layout of modal popups --- gossip-bin/src/ui/people/lists.rs | 80 ++++++++++++++++++------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index a07f316f6..0e8c4e6d0 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -62,21 +62,24 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra if let Some(list) = app.deleting_list { const DLG_SIZE: Vec2 = vec2(250.0, 120.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { - ui.vertical_centered(|ui| { + ui.vertical(|ui| { ui.label("Are you sure you want to delete:"); - ui.add_space(5.0); + ui.add_space(10.0); ui.heading(list.name()); - ui.add_space(5.0); - ui.horizontal(|ui| { - if ui.button("Cancel").clicked() { - app.deleting_list = None; - } - if ui.button("Delete").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::DeletePersonList(list)); - app.deleting_list = None; - } + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.deleting_list = None; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + if ui.button("Delete").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::DeletePersonList(list)); + app.deleting_list = None; + } + }) + }); }); }); }); @@ -86,32 +89,41 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra } else if app.creating_list { const DLG_SIZE: Vec2 = vec2(250.0, 120.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { - ui.vertical_centered(|ui| { - ui.heading("Creating a new Person List"); - ui.add(text_edit_line!(app, app.new_list_name)); - ui.horizontal(|ui| { - if ui.button("Cancel").clicked() { - app.creating_list = false; - } - if ui.button("Create").clicked() { - if !app.new_list_name.is_empty() { - if let Err(e) = PersonList::allocate(&app.new_list_name, None) { - GLOBALS.status_queue.write().write(format!("{}", e)); - } else { - app.creating_list = false; - } - } else { - GLOBALS - .status_queue - .write() - .write("Person List name must not be empty".to_string()); + ui.vertical(|ui| { + ui.heading("New List"); + ui.add_space(10.0); + ui.add(text_edit_line!(app, app.new_list_name).hint_text("list name")); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.creating_list = false; + app.new_list_name.clear(); } - } + + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + if ui.button("Create").clicked() { + if !app.new_list_name.is_empty() { + if let Err(e) = PersonList::allocate(&app.new_list_name, None) { + GLOBALS.status_queue.write().write(format!("{}", e)); + } else { + app.creating_list = false; + app.new_list_name.clear(); + } + } else { + GLOBALS + .status_queue + .write() + .write("Person List name must not be empty".to_string()); + } + } + }); + }); }); }); }); if ret.inner.clicked() { - app.deleting_list = None; + app.creating_list = false; + app.new_list_name.clear(); } } } From f5c2adbde4095f8e9451610979156c3a7b3dd690 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 6 Dec 2023 17:14:09 +1300 Subject: [PATCH 47/81] cargo fmt --- gossip-bin/src/ui/people/lists.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index 0e8c4e6d0..06ac81a9f 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -66,12 +66,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.label("Are you sure you want to delete:"); ui.add_space(10.0); ui.heading(list.name()); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { if ui.button("Cancel").clicked() { app.deleting_list = None; } - ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { if ui.button("Delete").clicked() { let _ = GLOBALS .to_overlord @@ -93,14 +93,14 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.heading("New List"); ui.add_space(10.0); ui.add(text_edit_line!(app, app.new_list_name).hint_text("list name")); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { if ui.button("Cancel").clicked() { app.creating_list = false; app.new_list_name.clear(); } - ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { if ui.button("Create").clicked() { if !app.new_list_name.is_empty() { if let Err(e) = PersonList::allocate(&app.new_list_name, None) { From 2371a49a4553bd2667b6199372b121513870fb25 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 7 Dec 2023 07:27:40 +1300 Subject: [PATCH 48/81] Fix gossip-bin feature set (again) --- gossip-bin/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip-bin/Cargo.toml b/gossip-bin/Cargo.toml index a3b22283e..798982dbb 100644 --- a/gossip-bin/Cargo.toml +++ b/gossip-bin/Cargo.toml @@ -10,7 +10,7 @@ default-run = "gossip" edition = "2021" [features] -default = [ "rustls-tls" ] +default = [ "rustls-tls-native" ] lang-cjk = [ "gossip-lib/lang-cjk" ] video-ffmpeg = [ "egui-video", "sdl2" ] native-tls = [ "gossip-lib/native-tls" ] From 24387d1f1989b48da0f7d0c9663a17ee6a271238 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 7 Dec 2023 08:35:14 +1300 Subject: [PATCH 49/81] Preserve tags better when generating lists --- gossip-lib/src/people.rs | 186 +++++++++++++++++++++++++-------------- 1 file changed, 119 insertions(+), 67 deletions(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index d4a3f6c26..620b73e87 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -602,6 +602,49 @@ impl People { .collect()) } + fn preserve_tags(old_tags: &[Tag], new_kind: EventKind) -> Vec { + let mut tags = Vec::new(); + + if new_kind == EventKind::MuteList { + // For mute lists, preserve 't', 'e' and 'word' tags from the previous event + // so as to not clobber them, they may be used on other clients + for t in old_tags { + match t { + Tag::Hashtag { .. } => { + tags.push(t.clone()); + } + Tag::Event { .. } => { + tags.push(t.clone()); + } + Tag::Other { tag, .. } => { + if tag == "word" { + tags.push(t.clone()); + } + } + _ => (), + } + } + } else if new_kind == EventKind::FollowSets { + // For FollowSets we should preserve "title", "image" and "description" + for t in old_tags { + match t { + Tag::Title { .. } => { + tags.push(t.clone()); + } + Tag::Other { tag, .. } => { + if tag == "image" || tag == "description" { + tags.push(t.clone()); + } + } + _ => (), + } + } + } + // For ContactList we don't need to preserve anything (see NIP-02) + + tags + } + pub(crate) async fn generate_person_list_event( &self, person_list: PersonList, @@ -622,80 +665,73 @@ impl People { PersonList::Custom(_) => EventKind::FollowSets, }; - // Pull the existing event (maybe) + // Pull the existing event let existing_event: Option = match kind { EventKind::ContactList | EventKind::MuteList => { // We fetch for ContactList to preserve the contents // We fetch for MuteList to preserve 't', 'e', and "word" tags GLOBALS.storage.get_replaceable_event(kind, my_pubkey, "")? } - // We don't need to preserve anything from FollowSets events + EventKind::FollowSets => { + // We fetch for FollowSets to preserve various tags we don't use + GLOBALS + .storage + .get_replaceable_event(kind, my_pubkey, &person_list.name())? + } _ => None, }; - let mut public_tags: Vec = Vec::new(); + // Build the public tags + let public_tags: Vec = { + let mut tags = Vec::new(); - // For mute lists, preserve 't', 'e' and 'word' tags from the previous - // event so as to not clobber them, they may be used on other clients - if kind == EventKind::MuteList { + // Preserve public tags from existing event if let Some(ref event) = existing_event { - for tag in &event.tags { - match tag { - Tag::Hashtag { .. } => { - public_tags.push(tag.clone()); - } - Tag::Event { .. } => { - public_tags.push(tag.clone()); - } - Tag::Other { .. } => { - public_tags.push(tag.clone()); - } - _ => (), - } - } + tags = Self::preserve_tags(&event.tags, kind); } - }; - // Build the public tags - for (pubkey, public) in people.iter() { - if !*public { - continue; - } + for (pubkey, public) in people.iter() { + if !*public { + continue; + } - // Only include petnames in the ContactList (which is only public people) - let petname = if kind == EventKind::ContactList { - if let Some(person) = GLOBALS.storage.read_person(pubkey)? { - person.petname.clone() + // Only include petnames in the ContactList (which is only public people) + let petname = if kind == EventKind::ContactList { + if let Some(person) = GLOBALS.storage.read_person(pubkey)? { + person.petname.clone() + } else { + None + } } else { None - } - } else { - None - }; + }; - // Only include recommended relay urls in public entries, and not in the mute list - let recommended_relay_url = if kind != EventKind::MuteList { - let relays = GLOBALS.storage.get_best_relays(*pubkey, Direction::Write)?; - relays.get(0).map(|(u, _)| u.to_unchecked_url()) - } else { - None - }; + // Only include recommended relay urls in public entries, and not in the mute list + let recommended_relay_url = if kind != EventKind::MuteList { + let relays = GLOBALS.storage.get_best_relays(*pubkey, Direction::Write)?; + relays.get(0).map(|(u, _)| u.to_unchecked_url()) + } else { + None + }; - public_tags.push(Tag::Pubkey { - pubkey: pubkey.into(), - recommended_relay_url, - petname, - trailing: vec![], - }); - } + tags.push(Tag::Pubkey { + pubkey: pubkey.into(), + recommended_relay_url, + petname, + trailing: vec![], + }); + } - // Add d-tag if using FollowSets - if matches!(person_list, PersonList::Custom(_)) { - public_tags.push(Tag::Identifier { - d: person_list.name(), - trailing: vec![], - }); - } + // Add d-tag if using FollowSets + if matches!(person_list, PersonList::Custom(_)) { + tags.push(Tag::Identifier { + d: person_list.name(), + trailing: vec![], + }); + } + + tags + }; let content = { if kind == EventKind::ContactList { @@ -706,20 +742,36 @@ impl People { None => "".to_owned(), } } else { - // Build private tags (except for ContactList) - let mut private_tags: Vec = Vec::new(); - for (pubkey, public) in people.iter() { - if *public { - continue; + let private_tags: Vec = { + let mut tags = Vec::new(); + + // Preserve private tags from existing event + if let Some(ref event) = existing_event { + if person_list != PersonList::Followed && !event.content.is_empty() { + let decrypted_content = + GLOBALS.signer.decrypt_nip04(&my_pubkey, &event.content)?; + let old_tags: Vec = serde_json::from_slice(&decrypted_content)?; + tags = Self::preserve_tags(&old_tags, kind); + } } - private_tags.push(Tag::Pubkey { - pubkey: pubkey.into(), - recommended_relay_url: None, - petname: None, - trailing: vec![], - }); - } + // Build private tags (except for ContactList) + for (pubkey, public) in people.iter() { + if *public { + continue; + } + + tags.push(Tag::Pubkey { + pubkey: pubkey.into(), + recommended_relay_url: None, + petname: None, + trailing: vec![], + }); + } + + tags + }; + let private_tags_string = serde_json::to_string(&private_tags)?; GLOBALS.signer.encrypt( &my_pubkey, From 6d30f925c55b3b980d33e843bb47170180401db0 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 7 Dec 2023 09:40:02 +1300 Subject: [PATCH 50/81] FIX: Allocate person list BEFORE updating person list last event data (else it wont find new list) --- gossip-lib/src/process.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index a81bd00d4..3e385a483 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -257,14 +257,6 @@ pub async fn process_new_event( process_somebody_elses_contact_list(event).await?; } } else if event.kind == EventKind::MuteList || event.kind == EventKind::FollowSets { - if let Some(pubkey) = GLOBALS.signer.public_key() { - if event.pubkey == pubkey { - // Update this data for the UI. We don't actually process the latest event - // until the user gives the go ahead. - GLOBALS.people.update_latest_person_list_event_data(); - } - } - // Allocate a slot for this person list if event.kind == EventKind::FollowSets { // get d-tag @@ -275,6 +267,14 @@ pub async fn process_new_event( } } } + + if let Some(pubkey) = GLOBALS.signer.public_key() { + if event.pubkey == pubkey { + // Update this data for the UI. We don't actually process the latest event + // until the user gives the go ahead. + GLOBALS.people.update_latest_person_list_event_data(); + } + } } else if event.kind == EventKind::RelayList { GLOBALS.storage.process_relay_list(event)?; } else if event.kind == EventKind::Repost { From e801abbe582fa6471fdd56678dd6678e330b7f15 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Wed, 6 Dec 2023 16:05:50 -0600 Subject: [PATCH 51/81] - Lists: Apply general list_entry style - Harmonize more menu feature to more_menu.rs --- gossip-bin/src/ui/feed/post.rs | 2 +- gossip-bin/src/ui/mod.rs | 30 ++- gossip-bin/src/ui/people/list.rs | 337 +++++++++++++++---------- gossip-bin/src/ui/people/mod.rs | 2 + gossip-bin/src/ui/relays/active.rs | 6 +- gossip-bin/src/ui/relays/known.rs | 6 +- gossip-bin/src/ui/relays/mine.rs | 6 +- gossip-bin/src/ui/relays/mod.rs | 92 ++----- gossip-bin/src/ui/widgets/mod.rs | 3 + gossip-bin/src/ui/widgets/more_menu.rs | 117 +++++++++ 10 files changed, 378 insertions(+), 223 deletions(-) create mode 100644 gossip-bin/src/ui/widgets/more_menu.rs diff --git a/gossip-bin/src/ui/feed/post.rs b/gossip-bin/src/ui/feed/post.rs index abcd99059..0e86a87f9 100644 --- a/gossip-bin/src/ui/feed/post.rs +++ b/gossip-bin/src/ui/feed/post.rs @@ -565,7 +565,7 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram app.draft_data.draft.push(emoji); } }); - ui.add_space(20.0); + btn_h_space!(ui); if ui .button(RichText::new("🥩")) .on_hover_text("raw content preview") diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index a12cc913e..7acbc0191 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -10,6 +10,12 @@ macro_rules! text_edit_multiline { }; } +macro_rules! btn_h_space { + ($ui:ident) => { + $ui.add_space(20.0) + }; +} + mod components; mod dm_chat_list; mod feed; @@ -352,6 +358,9 @@ struct GossipUi { // RelayUi relays: relays::RelayUi, + // people::ListUi + people_list: people::ListUi, + // Post rendering render_raw: Option, render_qr: Option, @@ -401,10 +410,8 @@ struct GossipUi { delegatee_tag_str: String, // User entry: general - entering_follow_someone_on_list: bool, follow_someone: String, add_relay: String, // dep - clear_list_needs_confirm: bool, password: String, password2: String, password3: String, @@ -607,6 +614,7 @@ impl GossipUi { qr_codes: HashMap::new(), notes: Notes::new(), relays: relays::RelayUi::new(), + people_list: people::ListUi::new(), render_raw: None, render_qr: None, approved: HashSet::new(), @@ -644,10 +652,8 @@ impl GossipUi { editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), - entering_follow_someone_on_list: false, follow_someone: "".to_owned(), add_relay: "".to_owned(), - clear_list_needs_confirm: false, password: "".to_owned(), password2: "".to_owned(), password3: "".to_owned(), @@ -1331,6 +1337,22 @@ impl GossipUi { } } + pub fn richtext_from_person_nip05(person: &Person) -> RichText { + if let Some(mut nip05) = person.nip05().map(|s| s.to_owned()) { + if nip05.starts_with("_@") { + nip05 = nip05.get(2..).unwrap().to_string(); + } + + if person.nip05_valid { + RichText::new(nip05).monospace() + } else { + RichText::new(nip05).monospace().strikethrough() + } + } else { + RichText::default() + } + } + pub fn render_person_name_line( app: &mut GossipUi, ui: &mut Ui, diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index aa6cfe557..ad536e9d0 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -7,6 +7,22 @@ use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{Person, PersonList, GLOBALS}; use nostr_types::{Profile, PublicKey}; +pub(crate) struct ListUi { + configure_list_menu_active: bool, + entering_follow_someone_on_list: bool, + clear_list_needs_confirm: bool, +} + +impl ListUi { + pub(crate) fn new() -> Self { + Self { + configure_list_menu_active: false, + entering_follow_someone_on_list: false, + clear_list_needs_confirm: false, + } + } +} + pub(super) fn update( app: &mut GossipUi, ctx: &Context, @@ -14,6 +30,8 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { + // prepare data + // TODO cache this to improve performance let people = { let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); @@ -32,8 +50,6 @@ pub(super) fn update( people }; - ui.add_space(12.0); - let latest_event_data = GLOBALS .people .latest_person_list_event_data @@ -50,7 +66,7 @@ pub(super) fn update( } } - let txt = if let Some(private_len) = latest_event_data.private_len { + let remote_text = if let Some(private_len) = latest_event_data.private_len { format!( "REMOTE: {} (public_len={} private_len={})", asof, latest_event_data.public_len, private_len @@ -62,76 +78,6 @@ pub(super) fn update( ) }; - ui.label(RichText::new(txt).size(15.0)) - .on_hover_text("This is the data in the latest list event fetched from relays"); - - ui.add_space(10.0); - - ui.horizontal(|ui| { - ui.add_space(30.0); - - if GLOBALS.signer.is_ready() { - if ui - .button("↓ Overwrite ↓") - .on_hover_text( - "This imports data from the latest event, erasing anything that is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: false, - }); - } - if ui - .button("↓ Merge ↓") - .on_hover_text( - "This imports data from the latest event, merging it into what is already here", - ) - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::UpdatePersonList { - person_list: list, - merge: true, - }); - } - - if ui - .button("↑ Publish ↑") - .on_hover_text("This publishes the list to your relays") - .clicked() - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::PushPersonList(list)); - } - } - - if GLOBALS.signer.is_ready() { - if app.clear_list_needs_confirm { - if ui.button("CANCEL").clicked() { - app.clear_list_needs_confirm = false; - } - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(list)); - app.clear_list_needs_confirm = false; - } - } else { - if ui.button("Clear All").clicked() { - app.clear_list_needs_confirm = true; - } - } - } - }); - - ui.add_space(10.0); - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { Ok(Some(date)) => date, Ok(None) => 0, @@ -149,12 +95,90 @@ pub(super) fn update( ledit = formatted; } } - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len())).size(15.0)) - .on_hover_text("This is the local (and effective) list"); - if !GLOBALS.signer.is_ready() { - ui.add_space(10.0); - ui.horizontal_wrapped(|ui| { + // render page + widgets::page_header(ui, format!("{} ({})", list.name(), people.len()), |ui| { + ui.add_enabled_ui(true, |ui| { + let min_size = vec2(50.0, 20.0); + + widgets::MoreMenu::new(&app) + .with_min_size(min_size) + .show(ui, &mut app.people_list.configure_list_menu_active, |ui|{ + // since we are displaying over an accent color background, load that style + app.theme.accent_button_2_style(ui.style_mut()); + + if ui.button("Clear All").clicked() { + app.people_list.clear_list_needs_confirm = true; + } + + // ui.add_space(8.0); + }); + }); + + btn_h_space!(ui); + + if ui.button("Add contact").clicked() { + app.people_list.entering_follow_someone_on_list = true; + } + }); + + if GLOBALS.signer.is_ready() { + ui.vertical(|ui| { + ui.label(RichText::new(remote_text)) + .on_hover_text("This is the data in the latest list event fetched from relays"); + + ui.add_space(5.0); + + // remote <-> local buttons + ui.horizontal(|ui|{ + if ui + .button("↓ Overwrite ↓") + .on_hover_text( + "This imports data from the latest event, erasing anything that is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: false, + }); + } + if ui + .button("↓ Merge ↓") + .on_hover_text( + "This imports data from the latest event, merging it into what is already here", + ) + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: true, + }); + } + + if ui + .button("↑ Publish ↑") + .on_hover_text("This publishes the list to your relays") + .clicked() + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::PushPersonList(list)); + } + }); + + ui.add_space(5.0); + + // local timestamp + ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len()))) + .on_hover_text("This is the local (and effective) list"); + }); + } else { + ui.horizontal(|ui| { ui.label("You need to "); if ui.link("setup your identity").clicked() { app.set_page(ctx, Page::YourKeys); @@ -163,73 +187,114 @@ pub(super) fn update( }); } - ui.add_space(10.0); - - if ui.button("Follow New").clicked() { - app.entering_follow_someone_on_list = true; + if app.people_list.clear_list_needs_confirm { + const DLG_SIZE: Vec2 = vec2(250.0, 40.0); + if widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical(|ui| { + ui.label("Are you sure you want to clear this list?"); + ui.add_space(10.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.people_list.clear_list_needs_confirm = false; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ + if ui.button("YES, CLEAR ALL").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + app.people_list.clear_list_needs_confirm = false; + } + }); + }); + }); + }); + }).inner.clicked() { + app.people_list.clear_list_needs_confirm = false; + } } - ui.separator(); ui.add_space(10.0); - ui.heading(format!("{} ({})", list.name(), people.len())); - ui.add_space(14.0); - - ui.separator(); - app.vert_scroll_area().show(ui, |ui| { for (person, public) in people.iter() { - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - if widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); - }; - - ui.vertical(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - GossipUi::render_person_name_line(app, ui, person, false); - if !GLOBALS - .storage - .have_persons_relays(person.pubkey) - .unwrap_or(false) - { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), - ); - } - + let row_response = widgets::list_entry::make_frame(ui) + .show(ui, |ui| { ui.horizontal(|ui| { - if crate::ui::components::switch_simple(ui, *public).clicked() { - let _ = GLOBALS.storage.add_person_to_list( - &person.pubkey, - list, - !*public, - None, - ); - } - ui.label(if *public { "public" } else { "private" }); + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + let avatar_height = widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).rect.height(); + + ui.add_space(20.0); + + ui.vertical(|ui| { + ui.set_min_height(avatar_height); + ui.horizontal(|ui| { + ui.label(GossipUi::person_name(person)); + + ui.add_space(10.0); + + if !GLOBALS + .storage + .have_persons_relays(person.pubkey) + .unwrap_or(false) + { + ui.label( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ); + } + }); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); + + ui.add_space(10.0); + + ui.label(GossipUi::richtext_from_person_nip05(person)); + }); + }); + }); + + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + ui.set_min_height(avatar_height); + // actions + if ui.link("Remove").clicked() { + let _ = GLOBALS + .storage + .remove_person_from_list(&person.pubkey, list, None); + } + + ui.add_space(20.0); + + // private / public switch + if crate::ui::components::switch_simple(ui, *public).clicked() { + let _ = GLOBALS.storage.add_person_to_list( + &person.pubkey, + list, + !*public, + None, + ); + } + ui.label(if *public { "public" } else { "private" }); + }); }); - }); }); - - if ui.button("Remove").clicked() { - let _ = GLOBALS - .storage - .remove_person_from_list(&person.pubkey, list, None); + if row_response + .response + .interact(egui::Sense::click()) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() { + app.set_page(ctx, Page::Person(person.pubkey)); } - - ui.add_space(4.0); - ui.separator(); } }); - if app.entering_follow_someone_on_list { + if app.people_list.entering_follow_someone_on_list { const DLG_SIZE: Vec2 = vec2(400.0, 200.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { ui.heading("Follow someone"); @@ -248,14 +313,14 @@ pub(super) fn update( let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.follow_someone.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if let Ok(profile) = Profile::try_from_bech32_string(app.follow_someone.trim(), true) { @@ -264,7 +329,7 @@ pub(super) fn update( list, true, )); - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( app.follow_someone.trim().to_owned(), @@ -281,7 +346,7 @@ pub(super) fn update( } }); if ret.inner.clicked() { - app.entering_follow_someone_on_list = false; + app.people_list.entering_follow_someone_on_list = false; } } } diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index be557840a..684778f6b 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -6,6 +6,8 @@ mod list; mod lists; mod person; +pub(crate) use list::ListUi; + pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::PeopleLists { lists::update(app, ctx, _frame, ui); diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index 6208a7d03..6f89f73b8 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -15,11 +15,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra widgets::page_header(ui, Page::RelaysActivityMonitor.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space if ui diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index f403548fd..9d69a5d66 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -11,11 +11,11 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr widgets::page_header(ui, Page::RelaysKnownNetwork.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); }); diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index 2a765c029..fa412e684 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -12,11 +12,11 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr widgets::page_header(ui, Page::RelaysMine.name(), |ui| { ui.set_enabled(!is_editing); super::configure_list_btn(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_filter_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); super::relay_sort_combo(app, ui); - ui.add_space(20.0); + btn_h_space!(ui); widgets::search_filter_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space widgets::set_important_button_visuals(ui, app); diff --git a/gossip-bin/src/ui/relays/mod.rs b/gossip-bin/src/ui/relays/mod.rs index 4f0a9943d..83a3432a2 100644 --- a/gossip-bin/src/ui/relays/mod.rs +++ b/gossip-bin/src/ui/relays/mod.rs @@ -1,7 +1,7 @@ use std::cmp::Ordering; use super::{widgets, GossipUi, Page}; -use eframe::{egui, epaint::PathShape}; +use eframe::egui; use egui::{Context, Ui}; use egui_winit::egui::{vec2, Id, Rect, RichText}; use gossip_lib::{comms::ToOverlordMessage, Relay, GLOBALS}; @@ -448,83 +448,29 @@ fn entry_dialog_step2(ui: &mut Ui, app: &mut GossipUi) { /// Draw button with configure popup /// pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) { - let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click()); - let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); - let response = if !app.relays.configure_list_menu_active { - response.on_hover_text("Configure List View") - } else { - response - }; - let btn_rect = response.rect; - let color = if response.hovered() { - app.theme.accent_color() - } else { - ui.visuals().text_color() - }; - let mut mesh = egui::Mesh::with_texture((&app.options_symbol).into()); - mesh.add_rect_with_uv( - btn_rect.shrink(2.0), - Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), - color, - ); - painter.add(egui::Shape::mesh(mesh)); + ui.add_enabled_ui(true, |ui| { + let min_size = vec2(180.0, 20.0); - if response.clicked() { - app.relays.configure_list_menu_active ^= true; - } + widgets::MoreMenu::new(app) + .with_min_size(min_size) + .with_hover_text("Configure List View".to_owned()) + .show(ui,&mut app.relays.configure_list_menu_active, |ui|{ + let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - let button_center_bottom = response.rect.center_bottom(); - let seen_on_popup_position = button_center_bottom + vec2(-180.0, widgets::DROPDOWN_DISTANCE); + // since we are displaying over an accent color background, load that style + app.theme.on_accent_style(ui.style_mut()); - let id: Id = "configure-list-menu".into(); - let mut frame = egui::Frame::popup(ui.style()); - let area = egui::Area::new(id) - .movable(false) - .interactable(true) - .order(egui::Order::Foreground) - .fixed_pos(seen_on_popup_position) - .constrain(true); - if app.relays.configure_list_menu_active { - let menuresp = area.show(ui.ctx(), |ui| { - frame.fill = app.theme.accent_color(); - frame.stroke = egui::Stroke::NONE; - // frame.shadow = egui::epaint::Shadow::NONE; - frame.rounding = egui::Rounding::same(5.0); - frame.inner_margin = egui::Margin::symmetric(20.0, 16.0); - frame.show(ui, |ui| { - let path = PathShape::convex_polygon( - [ - button_center_bottom, - button_center_bottom - + vec2(widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE), - button_center_bottom - + vec2(-widgets::DROPDOWN_DISTANCE, widgets::DROPDOWN_DISTANCE), - ] - .to_vec(), - app.theme.accent_color(), - egui::Stroke::NONE, - ); - ui.painter().add(path); - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - - // since we are displaying over an accent color background, load that style - app.theme.on_accent_style(ui.style_mut()); - - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); - ui.label("Show details"); - }); - ui.add_space(8.0); - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); - ui.label("Show hidden relays"); - }); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); + ui.label("Show details"); + }); + ui.add_space(8.0); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); + ui.label("Show hidden relays"); }); }); - if menuresp.response.clicked_elsewhere() && !response.clicked() { - app.relays.configure_list_menu_active = false; - } - } + }); } /// diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 4a33d3d56..d305507c6 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -1,6 +1,9 @@ mod avatar; pub(crate) use avatar::{paint_avatar, AvatarSize}; +mod more_menu; +pub(super) use more_menu::MoreMenu; + mod copy_button; pub(crate) mod list_entry; pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; diff --git a/gossip-bin/src/ui/widgets/more_menu.rs b/gossip-bin/src/ui/widgets/more_menu.rs new file mode 100644 index 000000000..3e665a386 --- /dev/null +++ b/gossip-bin/src/ui/widgets/more_menu.rs @@ -0,0 +1,117 @@ +use eframe::epaint::PathShape; +use egui_winit::egui::{Ui, self, vec2, Rect, Id, Vec2, TextureHandle, Color32}; + +use crate::ui::GossipUi; + +pub(in crate::ui) struct MoreMenu { + id: Id, + min_size: Vec2, + hover_text: Option, + accent_color: Color32, + options_symbol: TextureHandle, +} + +impl MoreMenu { + pub fn new(app: &GossipUi) -> Self { + Self { + id: "more-menu".into(), + min_size: Vec2 { x: 0.0, y: 0.0 }, + hover_text: None, + accent_color: app.theme.accent_color(), + options_symbol: app.options_symbol.clone(), + } + } + + #[allow(unused)] + pub fn with_id(mut self, id: impl Into) -> Self { + self.id = id.into(); + self + } + + #[allow(unused)] + pub fn with_min_size(mut self, min_size: Vec2) -> Self { + self.min_size = min_size; + self + } + + #[allow(unused)] + pub fn with_hover_text(mut self, text: String) -> Self { + self.hover_text = Some(text); + self + } + + pub fn show(&self, ui: &mut Ui, active: &mut bool, content: impl FnOnce(&mut Ui) ) { + let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click()); + let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); + let response = if let Some(text) = &self.hover_text { + if !*active { + response.on_hover_text(text) + } else { + response + } + } else { + response + }; + let btn_rect = response.rect; + let color = if response.hovered() { + self.accent_color + } else { + ui.visuals().text_color() + }; + let mut mesh = egui::Mesh::with_texture((&self.options_symbol).into()); + mesh.add_rect_with_uv( + btn_rect.shrink(2.0), + Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + color, + ); + painter.add(egui::Shape::mesh(mesh)); + + if response.clicked() { + *active ^= true; + } + + let button_center_bottom = response.rect.center_bottom(); + let seen_on_popup_position = button_center_bottom + vec2(-(self.min_size.x - 2.0*super::DROPDOWN_DISTANCE), super::DROPDOWN_DISTANCE); + + let mut frame = egui::Frame::popup(ui.style()); + let area = egui::Area::new(self.id) + .movable(false) + .interactable(true) + .order(egui::Order::Foreground) + .fixed_pos(seen_on_popup_position) + .constrain(true); + if *active { + let menuresp = area.show(ui.ctx(), |ui| { + frame.fill = self.accent_color; + frame.stroke = egui::Stroke::NONE; + // frame.shadow = egui::epaint::Shadow::NONE; + frame.rounding = egui::Rounding::same(5.0); + frame.inner_margin = egui::Margin::symmetric(20.0, 16.0); + frame.show(ui, |ui| { + ui.set_min_size(self.min_size); + let path = PathShape::convex_polygon( + [ + button_center_bottom, + button_center_bottom + + vec2(super::DROPDOWN_DISTANCE, super::DROPDOWN_DISTANCE), + button_center_bottom + + vec2(-super::DROPDOWN_DISTANCE, super::DROPDOWN_DISTANCE), + ] + .to_vec(), + self.accent_color, + egui::Stroke::NONE, + ); + ui.painter().add(path); + + // now show menu content + content(ui); + }); + }); + if menuresp.response.clicked_elsewhere() && !response.clicked() { + *active = false; + } + } + } +} + + From fc982c8ed419349c0a2451aea3f34e9c98863fbe Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 10:21:02 -0600 Subject: [PATCH 52/81] People List: Cache list and limit refresh to every 1 sec, greatly improves scrolling smoothness --- gossip-bin/src/ui/mod.rs | 1 + gossip-bin/src/ui/people/list.rs | 258 ++++++++++++++++++------------- gossip-bin/src/ui/people/mod.rs | 10 ++ 3 files changed, 164 insertions(+), 105 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index 7acbc0191..c81f61ecf 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -735,6 +735,7 @@ impl GossipUi { self.close_all_menus(ctx); } Page::PeopleLists => { + people::enter_page(self); self.close_all_menus(ctx); } Page::Person(pubkey) => { diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index ad536e9d0..3e54c0bac 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -1,3 +1,5 @@ +use std::time::{Instant, Duration}; + use super::{GossipUi, Page}; use crate::ui::widgets; use eframe::egui; @@ -8,6 +10,13 @@ use gossip_lib::{Person, PersonList, GLOBALS}; use nostr_types::{Profile, PublicKey}; pub(crate) struct ListUi { + // cache + cache_last_list: Option, + cache_next_refresh: Instant, + cache_people: Vec<(Person, bool)>, + cache_remote_tag: String, + cache_local_tag: String, + configure_list_menu_active: bool, entering_follow_someone_on_list: bool, clear_list_needs_confirm: bool, @@ -16,6 +25,12 @@ pub(crate) struct ListUi { impl ListUi { pub(crate) fn new() -> Self { Self { + // cache + cache_last_list: None, + cache_next_refresh: Instant::now(), + cache_people: Vec::new(), + cache_remote_tag: String::new(), + cache_local_tag: String::new(), configure_list_menu_active: false, entering_follow_someone_on_list: false, clear_list_needs_confirm: false, @@ -23,6 +38,10 @@ impl ListUi { } } +pub(super) fn enter_page(app: &mut GossipUi, list: PersonList) { + refresh_list_data(app, list); +} + pub(super) fn update( app: &mut GossipUi, ctx: &Context, @@ -30,74 +49,14 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { - // prepare data - // TODO cache this to improve performance - let people = { - let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); - - let mut people: Vec<(Person, bool)> = Vec::new(); - - for (pk, public) in &members { - if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { - people.push((person, *public)); - } else { - let person = Person::new(pk.to_owned()); - let _ = GLOBALS.storage.write_person(&person, None); - people.push((person, *public)); - } - } - people.sort_by(|a, b| a.0.cmp(&b.0)); - people - }; - - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&list) - .map(|v| v.value().clone()) - .unwrap_or_default(); - - let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - asof = formatted; - } - } - - let remote_text = if let Some(private_len) = latest_event_data.private_len { - format!( - "REMOTE: {} (public_len={} private_len={})", - asof, latest_event_data.public_len, private_len - ) - } else { - format!( - "REMOTE: {} (public_len={})", - asof, latest_event_data.public_len - ) - }; - - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; - } + if app.people_list.cache_next_refresh < Instant::now() || + app.people_list.cache_last_list.is_none() || + app.people_list.cache_last_list.unwrap() != list { + refresh_list_data(app, list); } // render page - widgets::page_header(ui, format!("{} ({})", list.name(), people.len()), |ui| { + widgets::page_header(ui, format!("{} ({})", list.name(), app.people_list.cache_people.len()), |ui| { ui.add_enabled_ui(true, |ui| { let min_size = vec2(50.0, 20.0); @@ -124,7 +83,7 @@ pub(super) fn update( if GLOBALS.signer.is_ready() { ui.vertical(|ui| { - ui.label(RichText::new(remote_text)) + ui.label(RichText::new(&app.people_list.cache_remote_tag)) .on_hover_text("This is the data in the latest list event fetched from relays"); ui.add_space(5.0); @@ -174,7 +133,7 @@ pub(super) fn update( ui.add_space(5.0); // local timestamp - ui.label(RichText::new(format!("LOCAL: {} (size={})", ledit, people.len()))) + ui.label(RichText::new(&app.people_list.cache_local_tag)) .on_hover_text("This is the local (and effective) list"); }); } else { @@ -217,6 +176,8 @@ pub(super) fn update( ui.add_space(10.0); app.vert_scroll_area().show(ui, |ui| { + // not nice but needed because of 'app' borrow in closure + let people = app.people_list.cache_people.clone(); for (person, public) in people.iter() { let row_response = widgets::list_entry::make_frame(ui) .show(ui, |ui| { @@ -279,6 +240,7 @@ pub(super) fn update( !*public, None, ); + mark_refresh(app); } ui.label(if *public { "public" } else { "private" }); }); @@ -297,8 +259,12 @@ pub(super) fn update( if app.people_list.entering_follow_someone_on_list { const DLG_SIZE: Vec2 = vec2(400.0, 200.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + // TODO use tagging search here + ui.heading("Follow someone"); + ui.add_space(8.0); + ui.horizontal(|ui| { ui.label("Enter"); ui.add( @@ -306,47 +272,129 @@ pub(super) fn update( .hint_text("npub1, hex key, nprofile1, or user@domain"), ); }); - if ui.button("follow").clicked() { - if let Ok(pubkey) = - PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; - } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; - } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) - { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( - profile.clone(), - list, - true, - )); - app.people_list.entering_follow_someone_on_list = false; - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { - let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), - list, - true, - )); - } else { - GLOBALS - .status_queue - .write() - .write("Invalid pubkey.".to_string()); - } - app.follow_someone = "".to_owned(); - } + + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + if ui.button("follow").clicked() { + if let Ok(pubkey) = + PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.people_list.entering_follow_someone_on_list = false; + } else if let Ok(pubkey) = + PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); + app.people_list.entering_follow_someone_on_list = false; + } else if let Ok(profile) = + Profile::try_from_bech32_string(app.follow_someone.trim(), true) + { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( + profile.clone(), + list, + true, + )); + app.people_list.entering_follow_someone_on_list = false; + } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( + app.follow_someone.trim().to_owned(), + list, + true, + )); + } else { + GLOBALS + .status_queue + .write() + .write("Invalid pubkey.".to_string()); + } + app.follow_someone = "".to_owned(); + } + }); + }); + }); if ret.inner.clicked() { app.people_list.entering_follow_someone_on_list = false; } } } + +fn mark_refresh(app: &mut GossipUi) { + app.people_list.cache_next_refresh = Instant::now(); +} + +fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { + // prepare data + app.people_list.cache_people = { + let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); + + let mut people: Vec<(Person, bool)> = Vec::new(); + + for (pk, public) in &members { + if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) { + people.push((person, *public)); + } else { + let person = Person::new(pk.to_owned()); + let _ = GLOBALS.storage.write_person(&person, None); + people.push((person, *public)); + } + } + people.sort_by(|a, b| a.0.cmp(&b.0)); + people + }; + + let latest_event_data = GLOBALS + .people + .latest_person_list_event_data + .get(&list) + .map(|v| v.value().clone()) + .unwrap_or_default(); + + let mut asof = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + asof = formatted; + } + } + + app.people_list.cache_remote_tag = if let Some(private_len) = latest_event_data.private_len { + format!( + "REMOTE: {} (public_len={} private_len={})", + asof, latest_event_data.public_len, private_len + ) + } else { + format!( + "REMOTE: {} (public_len={})", + asof, latest_event_data.public_len + ) + }; + + let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { + Ok(Some(date)) => date, + Ok(None) => 0, + Err(e) => { + tracing::error!("{}", e); + 0 + } + }; + + let mut ledit = "unknown".to_owned(); + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + ledit = formatted; + } + } + + app.people_list.cache_local_tag = format!("LOCAL: {} (size={})", ledit, app.people_list.cache_people.len()); + + app.people_list.cache_next_refresh = Instant::now() + Duration::new(1, 0); + app.people_list.cache_last_list = Some(list); +} diff --git a/gossip-bin/src/ui/people/mod.rs b/gossip-bin/src/ui/people/mod.rs index 684778f6b..2f104f96c 100644 --- a/gossip-bin/src/ui/people/mod.rs +++ b/gossip-bin/src/ui/people/mod.rs @@ -8,6 +8,16 @@ mod person; pub(crate) use list::ListUi; +pub(super) fn enter_page(app: &mut GossipUi) { + if app.page == Page::PeopleLists { + // nothing yet + } else if let Page::PeopleList(plist) = app.page { + list::enter_page(app, plist); + } else if matches!(app.page, Page::Person(_)) { + // nothing yet + } +} + pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { if app.page == Page::PeopleLists { lists::update(app, ctx, _frame, ui); From 7fd1c31aad0e61ab53a964363b54036d94deb467 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 10:29:21 -0600 Subject: [PATCH 53/81] People List: Say "REMOTE: not found on Active Relays" when no remote data is available --- gossip-bin/src/ui/people/list.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 3e54c0bac..7162cb409 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -354,7 +354,7 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { .map(|v| v.value().clone()) .unwrap_or_default(); - let mut asof = "unknown".to_owned(); + let mut asof = "time unknown".to_owned(); if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" @@ -363,7 +363,9 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } } - app.people_list.cache_remote_tag = if let Some(private_len) = latest_event_data.private_len { + app.people_list.cache_remote_tag = if latest_event_data.when.0 == 0 { + "REMOTE: not found on Active Relays".to_owned() + } else if let Some(private_len) = latest_event_data.private_len { format!( "REMOTE: {} (public_len={} private_len={})", asof, latest_event_data.public_len, private_len @@ -384,12 +386,14 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } }; - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { - if let Ok(formatted) = stamp.format(time::macros::format_description!( - "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" - )) { - ledit = formatted; + let mut ledit = "time unknown".to_owned(); + if last_list_edit > 0 { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(formatted) = stamp.format(time::macros::format_description!( + "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" + )) { + ledit = formatted; + } } } From 2f4db68750e4a64ee048e798d78e91c36a8e2166 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:31:30 -0600 Subject: [PATCH 54/81] rename widgets::search_filter_field() to widgets::search_field() --- gossip-bin/src/ui/relays/active.rs | 2 +- gossip-bin/src/ui/relays/known.rs | 2 +- gossip-bin/src/ui/relays/mine.rs | 2 +- gossip-bin/src/ui/widgets/mod.rs | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gossip-bin/src/ui/relays/active.rs b/gossip-bin/src/ui/relays/active.rs index 6f89f73b8..4a332d203 100644 --- a/gossip-bin/src/ui/relays/active.rs +++ b/gossip-bin/src/ui/relays/active.rs @@ -20,7 +20,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space if ui .button(RichText::new(Page::RelaysCoverage.name())) diff --git a/gossip-bin/src/ui/relays/known.rs b/gossip-bin/src/ui/relays/known.rs index 9d69a5d66..06ada4b50 100644 --- a/gossip-bin/src/ui/relays/known.rs +++ b/gossip-bin/src/ui/relays/known.rs @@ -16,7 +16,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); }); // TBD time how long this takes. We don't want expensive code in the UI diff --git a/gossip-bin/src/ui/relays/mine.rs b/gossip-bin/src/ui/relays/mine.rs index fa412e684..569f81161 100644 --- a/gossip-bin/src/ui/relays/mine.rs +++ b/gossip-bin/src/ui/relays/mine.rs @@ -17,7 +17,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr btn_h_space!(ui); super::relay_sort_combo(app, ui); btn_h_space!(ui); - widgets::search_filter_field(ui, &mut app.relays.search, 200.0); + widgets::search_field(ui, &mut app.relays.search, 200.0); ui.add_space(200.0); // search_field somehow doesn't "take up" space widgets::set_important_button_visuals(ui, app); if ui.button("Advertise Relay List") diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index d305507c6..eb7fa05ab 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -90,16 +90,16 @@ pub fn break_anywhere_hyperlink_to(ui: &mut Ui, text: impl Into, url ui.hyperlink_to(job.job, url); } -pub fn search_filter_field(ui: &mut Ui, field: &mut String, width: f32) -> Response { +pub fn search_field(ui: &mut Ui, field: &mut String, width: f32) -> TextEditOutput { // search field - let response = ui.add( - TextEdit::singleline(field) - .text_color(ui.visuals().widgets.inactive.fg_stroke.color) - .desired_width(width), - ); + let output = TextEdit::singleline(field) + .text_color(ui.visuals().widgets.inactive.fg_stroke.color) + .desired_width(width) + .show(ui); + let rect = Rect::from_min_size( - response.rect.right_top() - vec2(response.rect.height(), 0.0), - vec2(response.rect.height(), response.rect.height()), + output.response.rect.right_top() - vec2(output.response.rect.height(), 0.0), + vec2(output.response.rect.height(), output.response.rect.height()), ); // search clear button @@ -117,7 +117,7 @@ pub fn search_filter_field(ui: &mut Ui, field: &mut String, width: f32) -> Respo field.clear(); } - response + output } pub(super) fn set_important_button_visuals(ui: &mut Ui, app: &GossipUi) { From 9986b08fc89747c5b7874e4153e5260104c65a3b Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:32:53 -0600 Subject: [PATCH 55/81] Extract tagging search into a more general `widgets::show_contact_search()` --- gossip-bin/src/ui/feed/post.rs | 254 +++++--------------- gossip-bin/src/ui/widgets/contact_search.rs | 170 +++++++++++++ gossip-bin/src/ui/widgets/mod.rs | 8 +- 3 files changed, 230 insertions(+), 202 deletions(-) create mode 100644 gossip-bin/src/ui/widgets/contact_search.rs diff --git a/gossip-bin/src/ui/feed/post.rs b/gossip-bin/src/ui/feed/post.rs index 0e86a87f9..c16111b40 100644 --- a/gossip-bin/src/ui/feed/post.rs +++ b/gossip-bin/src/ui/feed/post.rs @@ -9,9 +9,9 @@ use egui_winit::egui::text::CCursor; use egui_winit::egui::text_edit::{CCursorRange, TextEditOutput}; use egui_winit::egui::Id; use gossip_lib::comms::ToOverlordMessage; +use gossip_lib::DmChannel; use gossip_lib::Relay; use gossip_lib::GLOBALS; -use gossip_lib::{DmChannel, Person}; use memoize::memoize; use nostr_types::{ContentSegment, NostrBech32, NostrUrl, ShatteredContent, Tag}; use std::collections::HashMap; @@ -396,38 +396,11 @@ fn real_posting_area(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram let enter_key; (app.draft_data.tagging_search_selected, enter_key) = if app.draft_data.tagging_search_substring.is_some() { - ui.input_mut(|i| { - // enter - let enter = i.count_and_consume_key(Modifiers::NONE, Key::Enter) > 0; - - // up / down - let mut index = app.draft_data.tagging_search_selected.unwrap_or(0); - let down = i.count_and_consume_key(Modifiers::NONE, Key::ArrowDown); - let up = i.count_and_consume_key(Modifiers::NONE, Key::ArrowUp); - index += down; - index = index.min( - app.draft_data - .tagging_search_results - .len() - .saturating_sub(1), - ); - index = index.saturating_sub(up); - - // tab will cycle down and wrap - let tab = i.count_and_consume_key(Modifiers::NONE, Key::Tab); - index += tab; - if index - > app - .draft_data - .tagging_search_results - .len() - .saturating_sub(1) - { - index = 0; - } - - (Some(index), enter) - }) + widgets::capture_keyboard_for_search( + ui, + app.draft_data.tagging_search_results.len(), + app.draft_data.tagging_search_selected, + ) } else { (None, false) }; @@ -687,176 +660,57 @@ fn show_tagging_result( output: &mut TextEditOutput, enter_key: bool, ) { - let pos = if let Some(cursor) = output.cursor_range { - let rect = output.galley.pos_from_cursor(&cursor.primary); // position within textedit - output.text_draw_pos + rect.center_bottom().to_vec2() - } else { - let rect = output.galley.pos_from_cursor(&output.galley.end()); // position within textedit - output.text_draw_pos + rect.center_bottom().to_vec2() - }; + let mut selected = app.draft_data.tagging_search_selected; + widgets::show_contact_search( + ui, + app, + output, + &mut selected, + app.draft_data.tagging_search_results.clone(), + enter_key, + |ui, app, output, pair| { + // remove @ and search text + let search = if let Some(search) = app.draft_data.tagging_search_searched.as_ref() { + search.clone() + } else { + "".to_string() + }; - // always compute the tooltip, but it is only shown when - // is_open is true. This is so we get the animation. - let frame = egui::Frame::popup(ui.style()) - .rounding(egui::Rounding::ZERO) - .inner_margin(egui::Margin::same(0.0)); - let area = egui::Area::new(ui.auto_id_with("compose-tagging-tooltip")) - .fixed_pos(pos) - .movable(false) - .constrain(true) - .interactable(true) - .order(egui::Order::Middle); - - // show search results - if !app.draft_data.tagging_search_results.is_empty() { - area.show(ui.ctx(), |ui| { - frame.show(ui, |ui| { - egui::ScrollArea::vertical() - .max_width(widgets::TAGG_WIDTH) - .max_height(250.0) - .show(ui, |ui| { - // need to clone results to avoid immutable borrow error on app. - let pairs = app.draft_data.tagging_search_results.clone(); - for (i, pair) in pairs.iter().enumerate() { - let avatar = if let Some(avatar) = app.try_get_avatar(ui.ctx(), &pair.1) - { - avatar - } else { - app.placeholder_avatar.clone() - }; - - let frame = egui::Frame::none() - .rounding(egui::Rounding::ZERO) - .inner_margin(egui::Margin::symmetric(10.0, 5.0)); - let mut prepared = frame.begin(ui); - - prepared.content_ui.set_min_width(widgets::TAGG_WIDTH); - prepared.content_ui.set_max_width(widgets::TAGG_WIDTH); - prepared.content_ui.set_min_height(27.0); - - let frame_rect = (prepared.frame.inner_margin - + prepared.frame.outer_margin) - .expand_rect(prepared.content_ui.min_rect()); - - let response = ui - .interact( - frame_rect, - ui.auto_id_with(pair.1.as_hex_string()), - egui::Sense::click(), - ) - .on_hover_cursor(egui::CursorIcon::PointingHand); - - // mouse hover moves selected index - app.draft_data.tagging_search_selected = if response.hovered() { - Some(i) - } else { - app.draft_data.tagging_search_selected - }; - let is_selected = Some(i) == app.draft_data.tagging_search_selected; - - { - // render inside of frame using prepared.content_ui - let ui = &mut prepared.content_ui; - if is_selected { - app.theme.on_accent_style(ui.style_mut()) - } - let person = GLOBALS - .storage - .read_person(&pair.1) - .unwrap_or(Some(Person::new(pair.1))) - .unwrap_or(Person::new(pair.1)); - ui.horizontal(|ui| { - widgets::paint_avatar( - ui, - &person, - &avatar, - widgets::AvatarSize::Mini, - ); - ui.vertical(|ui| { - widgets::truncated_label( - ui, - RichText::new(&pair.0).small(), - widgets::TAGG_WIDTH - 33.0, - ); - - let mut nip05 = - RichText::new(person.nip05().unwrap_or_default()) - .weak() - .small(); - if !person.nip05_valid { - nip05 = nip05.strikethrough() - } - widgets::truncated_label( - ui, - nip05, - widgets::TAGG_WIDTH - 33.0, - ); - }); - }) - }; - - prepared.frame.fill = if is_selected { - app.theme.accent_color() - } else { - egui::Color32::TRANSPARENT - }; - - prepared.end(ui); - - if is_selected { - response.scroll_to_me(None) - } - let clicked = response.clicked(); - if clicked || (enter_key && is_selected) { - // remove @ and search text - let search = if let Some(search) = - app.draft_data.tagging_search_searched.as_ref() - { - search.clone() - } else { - "".to_string() - }; - - // complete name and add replacement - let name = pair.0.clone(); - let nostr_url: NostrUrl = pair.1.into(); - app.draft_data.draft = app - .draft_data - .draft - .as_str() - .replace(&format!("@{}", search), name.as_str()) - .to_string(); - - // move cursor to end of replacement - if let Some(pos) = app.draft_data.draft.find(name.as_str()) { - let cpos = pos + name.len(); - let mut state = output.state.clone(); - let mut ccrange = CCursorRange::default(); - ccrange.primary.index = cpos; - ccrange.secondary.index = cpos; - state.set_ccursor_range(Some(ccrange)); - state.store(ui.ctx(), output.response.id); - - // add it to our replacement list - app.draft_data - .replacements - .insert(name, ContentSegment::NostrUrl(nostr_url)); - app.draft_data.replacements_changed = true; - - // clear tagging search - app.draft_data.tagging_search_substring = None; - } - } - } - }); - }); - }); - } + // complete name and add replacement + let name = pair.0.clone(); + let nostr_url: NostrUrl = pair.1.into(); + app.draft_data.draft = app + .draft_data + .draft + .as_str() + .replace(&format!("@{}", search), name.as_str()) + .to_string(); + + // move cursor to end of replacement + if let Some(pos) = app.draft_data.draft.find(name.as_str()) { + let cpos = pos + name.len(); + let mut state = output.state.clone(); + let mut ccrange = CCursorRange::default(); + ccrange.primary.index = cpos; + ccrange.secondary.index = cpos; + state.set_ccursor_range(Some(ccrange)); + state.store(ui.ctx(), output.response.id); + + // add it to our replacement list + app.draft_data + .replacements + .insert(name, ContentSegment::NostrUrl(nostr_url)); + app.draft_data.replacements_changed = true; + + // clear tagging search + app.draft_data.tagging_search_substring = None; + } + }, + ); - let is_open = app.draft_data.tagging_search_substring.is_some(); - area.show_open_close_animation(ui.ctx(), &frame, is_open); + app.draft_data.tagging_search_selected = selected; - if !is_open { + if app.draft_data.tagging_search_substring.is_none() { // no more search substring, clear results app.draft_data.tagging_search_searched = None; app.draft_data.tagging_search_results.clear(); diff --git a/gossip-bin/src/ui/widgets/contact_search.rs b/gossip-bin/src/ui/widgets/contact_search.rs new file mode 100644 index 000000000..66a8714b5 --- /dev/null +++ b/gossip-bin/src/ui/widgets/contact_search.rs @@ -0,0 +1,170 @@ +use egui_winit::egui::{self, text_edit::TextEditOutput, Key, Modifiers, RichText, Ui}; +use gossip_lib::{Person, GLOBALS}; +use nostr_types::PublicKey; + +use crate::ui::GossipUi; + +pub(in crate::ui) fn show_contact_search( + ui: &mut Ui, + app: &mut GossipUi, + output: &mut TextEditOutput, + selected: &mut Option, + search_results: Vec<(String, PublicKey)>, + enter_key: bool, + on_select_callback: impl Fn(&mut Ui, &mut GossipUi, &mut TextEditOutput, &(String, PublicKey)), +) { + let pos = if let Some(cursor) = output.cursor_range { + let rect = output.galley.pos_from_cursor(&cursor.primary); // position within textedit + output.text_draw_pos + rect.center_bottom().to_vec2() + } else { + let rect = output.galley.pos_from_cursor(&output.galley.end()); // position within textedit + output.text_draw_pos + rect.center_bottom().to_vec2() + }; + + // always compute the tooltip, but it is only shown when + // is_open is true. This is so we get the animation. + let frame = egui::Frame::popup(ui.style()) + .rounding(egui::Rounding::ZERO) + .inner_margin(egui::Margin::same(0.0)); + let area = egui::Area::new(ui.auto_id_with("compose-tagging-tooltip")) + .fixed_pos(pos) + .movable(false) + .constrain(true) + .interactable(true) + .order(egui::Order::Foreground); + + let is_open = !search_results.is_empty(); + + // show search results + if is_open { + area.show(ui.ctx(), |ui| { + frame.show(ui, |ui| { + egui::ScrollArea::vertical() + .max_width(super::TAGG_WIDTH) + .max_height(250.0) + .show(ui, |ui| { + for (i, pair) in search_results.iter().enumerate() { + let avatar = if let Some(avatar) = app.try_get_avatar(ui.ctx(), &pair.1) + { + avatar + } else { + app.placeholder_avatar.clone() + }; + + let frame = egui::Frame::none() + .rounding(egui::Rounding::ZERO) + .inner_margin(egui::Margin::symmetric(10.0, 5.0)); + let mut prepared = frame.begin(ui); + + prepared.content_ui.set_min_width(super::TAGG_WIDTH); + prepared.content_ui.set_max_width(super::TAGG_WIDTH); + prepared.content_ui.set_min_height(27.0); + + let frame_rect = (prepared.frame.inner_margin + + prepared.frame.outer_margin) + .expand_rect(prepared.content_ui.min_rect()); + + let response = ui + .interact( + frame_rect, + ui.auto_id_with(pair.1.as_hex_string()), + egui::Sense::click(), + ) + .on_hover_cursor(egui::CursorIcon::PointingHand); + + // mouse hover moves selected index + *selected = if response.hovered() { + Some(i) + } else { + *selected + }; + let is_selected = Some(i) == *selected; + + { + // render inside of frame using prepared.content_ui + let ui = &mut prepared.content_ui; + if is_selected { + app.theme.on_accent_style(ui.style_mut()) + } + let person = GLOBALS + .storage + .read_person(&pair.1) + .unwrap_or(Some(Person::new(pair.1))) + .unwrap_or(Person::new(pair.1)); + ui.horizontal(|ui| { + super::paint_avatar( + ui, + &person, + &avatar, + super::AvatarSize::Mini, + ); + ui.vertical(|ui| { + super::truncated_label( + ui, + RichText::new(&pair.0).small(), + super::TAGG_WIDTH - 33.0, + ); + + let mut nip05 = + RichText::new(person.nip05().unwrap_or_default()) + .weak() + .small(); + if !person.nip05_valid { + nip05 = nip05.strikethrough() + } + super::truncated_label(ui, nip05, super::TAGG_WIDTH - 33.0); + }); + }) + }; + + prepared.frame.fill = if is_selected { + app.theme.accent_color() + } else { + egui::Color32::TRANSPARENT + }; + + prepared.end(ui); + + if is_selected { + response.scroll_to_me(None) + } + let clicked = response.interact(egui::Sense::click()).clicked(); + if clicked || (enter_key && is_selected) { + on_select_callback(ui, app, output, pair); + } + } + }); + }); + }); + } + + area.show_open_close_animation(ui.ctx(), &frame, is_open); +} + +pub(in crate::ui) fn capture_keyboard_for_search( + ui: &mut Ui, + result_len: usize, + selected: Option, +) -> (Option, bool) { + ui.input_mut(|i| { + // enter + let enter = i.count_and_consume_key(Modifiers::NONE, Key::Enter) > 0; + + // up / down + let mut index = selected.unwrap_or(0); + let down = i.count_and_consume_key(Modifiers::NONE, Key::ArrowDown); + let up = i.count_and_consume_key(Modifiers::NONE, Key::ArrowUp); + index += down; + index = index.min(result_len.saturating_sub(1)); + index = index.saturating_sub(up); + + // tab will cycle down and wrap + let tab = i.count_and_consume_key(Modifiers::NONE, Key::Tab); + index += tab; + if index > result_len.saturating_sub(1) { + index = 0; + } + + (Some(index), enter) + }) +} diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index eb7fa05ab..221be7537 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -1,14 +1,15 @@ mod avatar; pub(crate) use avatar::{paint_avatar, AvatarSize}; -mod more_menu; -pub(super) use more_menu::MoreMenu; +mod contact_search; +pub(super) use contact_search::{capture_keyboard_for_search, show_contact_search}; mod copy_button; pub(crate) mod list_entry; pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; +use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::{ self, vec2, FontSelection, Rect, Response, Sense, TextEdit, Ui, WidgetText, }; @@ -20,6 +21,9 @@ pub use relay_entry::{RelayEntry, RelayEntryView}; mod modal_popup; pub use modal_popup::modal_popup; +mod more_menu; +pub(super) use more_menu::MoreMenu; + mod information_popup; pub use information_popup::InformationPopup; pub use information_popup::ProfilePopup; From d567d23fb0b311c0c2213289f928fca87869155b Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 13:33:46 -0600 Subject: [PATCH 56/81] People List: Add contact search functionality to "Add contact" dialogue --- gossip-bin/src/ui/mod.rs | 4 +- gossip-bin/src/ui/people/list.rs | 399 ++++++++++++++-------- gossip-bin/src/ui/widgets/mod.rs | 2 +- gossip-bin/src/ui/wizard/follow_people.rs | 14 +- 4 files changed, 273 insertions(+), 146 deletions(-) diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index c81f61ecf..56dd2c853 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -410,7 +410,7 @@ struct GossipUi { delegatee_tag_str: String, // User entry: general - follow_someone: String, + add_contact: String, add_relay: String, // dep password: String, password2: String, @@ -652,7 +652,7 @@ impl GossipUi { editing_metadata: false, metadata: Metadata::new(), delegatee_tag_str: "".to_owned(), - follow_someone: "".to_owned(), + add_contact: "".to_owned(), add_relay: "".to_owned(), password: "".to_owned(), password2: "".to_owned(), diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 7162cb409..e0b88dbb1 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -1,9 +1,10 @@ -use std::time::{Instant, Duration}; +use std::time::{Duration, Instant}; use super::{GossipUi, Page}; use crate::ui::widgets; use eframe::egui; use egui::{Context, RichText, Ui, Vec2}; +use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::vec2; use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{Person, PersonList, GLOBALS}; @@ -17,6 +18,12 @@ pub(crate) struct ListUi { cache_remote_tag: String, cache_local_tag: String, + // add contact + add_contact_search: String, + add_contact_searched: Option, + add_contact_search_results: Vec<(String, PublicKey)>, + add_contact_search_selected: Option, + configure_list_menu_active: bool, entering_follow_someone_on_list: bool, clear_list_needs_confirm: bool, @@ -31,6 +38,13 @@ impl ListUi { cache_people: Vec::new(), cache_remote_tag: String::new(), cache_local_tag: String::new(), + + // add contact + add_contact_search: String::new(), + add_contact_searched: None, + add_contact_search_results: Vec::new(), + add_contact_search_selected: None, + configure_list_menu_active: false, entering_follow_someone_on_list: false, clear_list_needs_confirm: false, @@ -49,37 +63,58 @@ pub(super) fn update( ui: &mut Ui, list: PersonList, ) { - if app.people_list.cache_next_refresh < Instant::now() || - app.people_list.cache_last_list.is_none() || - app.people_list.cache_last_list.unwrap() != list { + if app.people_list.cache_next_refresh < Instant::now() + || app.people_list.cache_last_list.is_none() + || app.people_list.cache_last_list.unwrap() != list + { refresh_list_data(app, list); } + // process popups first + if app.people_list.clear_list_needs_confirm { + render_clear_list_confirm_popup(ui, app, list); + } + if app.people_list.entering_follow_someone_on_list { + render_add_contact_popup(ui, app, list); + } + + // disable rest of ui when popups are open + let enabled = !app.people_list.entering_follow_someone_on_list + && !app.people_list.clear_list_needs_confirm; + // render page - widgets::page_header(ui, format!("{} ({})", list.name(), app.people_list.cache_people.len()), |ui| { - ui.add_enabled_ui(true, |ui| { - let min_size = vec2(50.0, 20.0); - - widgets::MoreMenu::new(&app) - .with_min_size(min_size) - .show(ui, &mut app.people_list.configure_list_menu_active, |ui|{ - // since we are displaying over an accent color background, load that style - app.theme.accent_button_2_style(ui.style_mut()); - - if ui.button("Clear All").clicked() { - app.people_list.clear_list_needs_confirm = true; - } + widgets::page_header( + ui, + format!("{} ({})", list.name(), app.people_list.cache_people.len()), + |ui| { + ui.add_enabled_ui(enabled, |ui| { + let min_size = vec2(50.0, 20.0); + + widgets::MoreMenu::new(&app).with_min_size(min_size).show( + ui, + &mut app.people_list.configure_list_menu_active, + |ui| { + // since we are displaying over an accent color background, load that style + app.theme.accent_button_2_style(ui.style_mut()); + + if ui.button("Clear All").clicked() { + app.people_list.clear_list_needs_confirm = true; + } - // ui.add_space(8.0); + // ui.add_space(8.0); + }, + ); }); - }); - btn_h_space!(ui); + btn_h_space!(ui); - if ui.button("Add contact").clicked() { - app.people_list.entering_follow_someone_on_list = true; - } - }); + if ui.button("Add contact").clicked() { + app.people_list.entering_follow_someone_on_list = true; + } + }, + ); + + ui.set_enabled(enabled); if GLOBALS.signer.is_ready() { ui.vertical(|ui| { @@ -146,152 +181,172 @@ pub(super) fn update( }); } - if app.people_list.clear_list_needs_confirm { - const DLG_SIZE: Vec2 = vec2(250.0, 40.0); - if widgets::modal_popup(ui, DLG_SIZE, |ui| { - ui.vertical(|ui| { - ui.label("Are you sure you want to clear this list?"); - ui.add_space(10.0); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT),|ui| { - ui.horizontal(|ui| { - if ui.button("Cancel").clicked() { - app.people_list.clear_list_needs_confirm = false; - } - ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui|{ - if ui.button("YES, CLEAR ALL").clicked() { - let _ = GLOBALS - .to_overlord - .send(ToOverlordMessage::ClearPersonList(list)); - app.people_list.clear_list_needs_confirm = false; - } - }); - }); - }); - }); - }).inner.clicked() { - app.people_list.clear_list_needs_confirm = false; - } - } - ui.add_space(10.0); app.vert_scroll_area().show(ui, |ui| { // not nice but needed because of 'app' borrow in closure let people = app.people_list.cache_people.clone(); for (person, public) in people.iter() { - let row_response = widgets::list_entry::make_frame(ui) - .show(ui, |ui| { - ui.horizontal(|ui| { - // Avatar first - let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { - avatar - } else { - app.placeholder_avatar.clone() - }; - let avatar_height = widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed).rect.height(); - - ui.add_space(20.0); - - ui.vertical(|ui| { - ui.set_min_height(avatar_height); + let row_response = widgets::list_entry::make_frame(ui).show(ui, |ui| { + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + let avatar_height = + widgets::paint_avatar(ui, person, &avatar, widgets::AvatarSize::Feed) + .rect + .height(); + + ui.add_space(20.0); + + ui.vertical(|ui| { + ui.set_min_height(avatar_height); + ui.horizontal(|ui| { + ui.label(GossipUi::person_name(person)); + + ui.add_space(10.0); + + if !GLOBALS + .storage + .have_persons_relays(person.pubkey) + .unwrap_or(false) + { + ui.label( + RichText::new("Relay list not found") + .color(app.theme.warning_marker_text_color()), + ); + } + }); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { - ui.label(GossipUi::person_name(person)); + ui.label( + RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)) + .weak(), + ); ui.add_space(10.0); - if !GLOBALS - .storage - .have_persons_relays(person.pubkey) - .unwrap_or(false) - { - ui.label( - RichText::new("Relay list not found") - .color(app.theme.warning_marker_text_color()), - ); - } - }); - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.horizontal(|ui| { - ui.label(RichText::new(gossip_lib::names::pubkey_short(&person.pubkey)).weak()); - - ui.add_space(10.0); - - ui.label(GossipUi::richtext_from_person_nip05(person)); - }); + ui.label(GossipUi::richtext_from_person_nip05(person)); }); }); + }); - ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { - ui.set_min_height(avatar_height); - // actions - if ui.link("Remove").clicked() { - let _ = GLOBALS + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + ui.set_min_height(avatar_height); + // actions + if ui.link("Remove").clicked() { + let _ = + GLOBALS .storage .remove_person_from_list(&person.pubkey, list, None); - } + } - ui.add_space(20.0); + ui.add_space(20.0); - // private / public switch - if crate::ui::components::switch_simple(ui, *public).clicked() { - let _ = GLOBALS.storage.add_person_to_list( - &person.pubkey, - list, - !*public, - None, - ); - mark_refresh(app); - } - ui.label(if *public { "public" } else { "private" }); - }); + // private / public switch + if crate::ui::components::switch_simple(ui, *public).clicked() { + let _ = GLOBALS.storage.add_person_to_list( + &person.pubkey, + list, + !*public, + None, + ); + mark_refresh(app); + } + ui.label(if *public { "public" } else { "private" }); }); + }); }); if row_response .response .interact(egui::Sense::click()) .on_hover_cursor(egui::CursorIcon::PointingHand) - .clicked() { - app.set_page(ctx, Page::Person(person.pubkey)); + .clicked() + { + app.set_page(ctx, Page::Person(person.pubkey)); } } }); +} - if app.people_list.entering_follow_someone_on_list { - const DLG_SIZE: Vec2 = vec2(400.0, 200.0); - let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { - // TODO use tagging search here - - ui.heading("Follow someone"); - - ui.add_space(8.0); - +fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::PersonList1) { + const DLG_SIZE: Vec2 = vec2(400.0, 240.0); + let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { + let enter_key; + (app.people_list.add_contact_search_selected, enter_key) = + if app.people_list.add_contact_search_results.is_empty() { + (None, false) + } else { + widgets::capture_keyboard_for_search( + ui, + app.people_list.add_contact_search_results.len(), + app.people_list.add_contact_search_selected, + ) + }; + + ui.heading("Add contact to the list"); + ui.add_space(8.0); + + ui.label("Search for known contacts to add"); + ui.add_space(8.0); + + let mut output = + widgets::search_field(ui, &mut app.people_list.add_contact_search, f32::INFINITY); + + let mut selected = app.people_list.add_contact_search_selected; + widgets::show_contact_search( + ui, + app, + &mut output, + &mut selected, + app.people_list.add_contact_search_results.clone(), + enter_key, + |_, app, _, pair| { + app.people_list.add_contact_search = pair.0.clone(); + app.people_list.add_contact_search_results.clear(); + app.people_list.add_contact_search_selected = None; + app.add_contact = pair.1.as_bech32_string(); + }, + ); + app.people_list.add_contact_search_selected = selected; + + recalc_add_contact_search(app, &mut output); + + ui.add_space(8.0); + + ui.label("To add a new contact to this list enter their npub, hex key, nprofle or nip-05 address"); + ui.add_space(8.0); + + ui.add( + text_edit_multiline!(app, app.add_contact) + .desired_width(f32::INFINITY) + .hint_text("npub1, hex key, nprofile1, or user@domain"), + ); + + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { - ui.label("Enter"); - ui.add( - text_edit_line!(app, app.follow_someone) - .hint_text("npub1, hex key, nprofile1, or user@domain"), - ); - }); - - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.horizontal(|ui| { - if ui.button("follow").clicked() { + ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + app.theme.accent_button_1_style(ui.style_mut()); + if ui.button("Add Contact").clicked() { if let Ok(pubkey) = - PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) + PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); app.people_list.entering_follow_someone_on_list = false; } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); app.people_list.entering_follow_someone_on_list = false; } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) + Profile::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( profile.clone(), @@ -299,9 +354,9 @@ pub(super) fn update( true, )); app.people_list.entering_follow_someone_on_list = false; - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), + app.add_contact.trim().to_owned(), list, true, )); @@ -311,15 +366,83 @@ pub(super) fn update( .write() .write("Invalid pubkey.".to_string()); } - app.follow_someone = "".to_owned(); + app.add_contact = "".to_owned(); + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); } }); }); - }); - if ret.inner.clicked() { - app.people_list.entering_follow_someone_on_list = false; + }); + if ret.inner.clicked() { + app.people_list.entering_follow_someone_on_list = false; + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); + } +} + +fn recalc_add_contact_search(app: &mut GossipUi, output: &mut TextEditOutput) { + // only recalc if search text changed + if app.people_list.add_contact_search.len() > 2 && output.cursor_range.is_some() { + if Some(&app.people_list.add_contact_search) + != app.people_list.add_contact_searched.as_ref() + { + let mut pairs = GLOBALS + .people + .search_people_to_tag(app.people_list.add_contact_search.as_str()) + .unwrap_or_default(); + // followed contacts first + pairs.sort_by(|(_, ak), (_, bk)| { + let af = GLOBALS + .storage + .is_person_in_list(ak, gossip_lib::PersonList::Followed) + .unwrap_or(false); + let bf = GLOBALS + .storage + .is_person_in_list(bk, gossip_lib::PersonList::Followed) + .unwrap_or(false); + bf.cmp(&af).then(std::cmp::Ordering::Greater) + }); + app.people_list.add_contact_searched = Some(app.people_list.add_contact_search.clone()); + app.people_list.add_contact_search_results = pairs.to_owned(); } + } else { + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_results.clear(); + } +} + +fn render_clear_list_confirm_popup(ui: &mut Ui, app: &mut GossipUi, list: PersonList) { + const DLG_SIZE: Vec2 = vec2(250.0, 40.0); + if widgets::modal_popup(ui, DLG_SIZE, |ui| { + ui.vertical(|ui| { + ui.label("Are you sure you want to clear this list?"); + ui.add_space(10.0); + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + if ui.button("Cancel").clicked() { + app.people_list.clear_list_needs_confirm = false; + } + ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { + if ui.button("YES, CLEAR ALL").clicked() { + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::ClearPersonList(list)); + app.people_list.clear_list_needs_confirm = false; + } + }); + }); + }); + }); + }) + .inner + .clicked() + { + app.people_list.clear_list_needs_confirm = false; } } @@ -397,7 +520,11 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } } - app.people_list.cache_local_tag = format!("LOCAL: {} (size={})", ledit, app.people_list.cache_people.len()); + app.people_list.cache_local_tag = format!( + "LOCAL: {} (size={})", + ledit, + app.people_list.cache_people.len() + ); app.people_list.cache_next_refresh = Instant::now() + Duration::new(1, 0); app.people_list.cache_last_list = Some(list); diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 221be7537..766328afa 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -11,7 +11,7 @@ pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; use egui_winit::egui::text_edit::TextEditOutput; use egui_winit::egui::{ - self, vec2, FontSelection, Rect, Response, Sense, TextEdit, Ui, WidgetText, + self, vec2, FontSelection, Rect, Sense, TextEdit, Ui, WidgetText, }; pub use nav_item::NavItem; diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index eb2acb59a..114a891eb 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -86,7 +86,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr ui.horizontal(|ui| { ui.label("Follow Someone:"); if ui - .add(text_edit_line!(app, app.follow_someone).hint_text( + .add(text_edit_line!(app, app.add_contact).hint_text( "Enter a key (bech32 npub1 or hex), or an nprofile, or a DNS id (user@domain)", )) .changed() @@ -94,14 +94,14 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr app.wizard_state.error = None; } if ui.button("follow").clicked() { - if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.follow_someone.trim(), true) { + if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( pubkey, PersonList::Followed, true, )); } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.follow_someone.trim(), true) + PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( pubkey, @@ -109,23 +109,23 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr true, )); } else if let Ok(profile) = - Profile::try_from_bech32_string(app.follow_someone.trim(), true) + Profile::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNprofile( profile, PersonList::Followed, true, )); - } else if gossip_lib::nip05::parse_nip05(app.follow_someone.trim()).is_ok() { + } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( - app.follow_someone.trim().to_owned(), + app.add_contact.trim().to_owned(), PersonList::Followed, true, )); } else { app.wizard_state.error = Some("ERROR: Invalid pubkey".to_owned()); } - app.follow_someone = "".to_owned(); + app.add_contact = "".to_owned(); } }); From 8a9645b664596361bf65e30e1c6a66b812228f0f Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 14:54:45 -0600 Subject: [PATCH 57/81] People Lists: Style clear list confirmation --- gossip-bin/src/ui/people/list.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index e0b88dbb1..9ffd8dd73 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -424,15 +424,18 @@ fn render_clear_list_confirm_popup(ui: &mut Ui, app: &mut GossipUi, list: Person ui.add_space(10.0); ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { + app.theme.accent_button_2_style(ui.style_mut()); if ui.button("Cancel").clicked() { app.people_list.clear_list_needs_confirm = false; } ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { + app.theme.accent_button_1_style(ui.style_mut()); if ui.button("YES, CLEAR ALL").clicked() { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::ClearPersonList(list)); app.people_list.clear_list_needs_confirm = false; + mark_refresh(app); } }); }); From 31f824c6a350931b7a3cd23027fe770840ed9855 Mon Sep 17 00:00:00 2001 From: Bu5hm4nn Date: Thu, 7 Dec 2023 15:35:19 -0600 Subject: [PATCH 58/81] People List: Add "Add and continue" button to quickly keep adding people to a list --- gossip-bin/src/ui/people/list.rs | 44 ++++++++++++++++++++++++-------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 9ffd8dd73..e080c3985 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -317,7 +317,7 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P ui.add_space(8.0); - ui.label("To add a new contact to this list enter their npub, hex key, nprofle or nip-05 address"); + ui.label("To add a new contact to this list enter their npub, hex key, nprofile or nip-05 address"); ui.add_space(8.0); ui.add( @@ -329,22 +329,39 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { + let mut try_add = false; + let mut want_close = false; + let mut can_close = false; + app.theme.accent_button_1_style(ui.style_mut()); - if ui.button("Add Contact").clicked() { + if ui.button("Add and close").clicked() { + try_add |= true; + want_close = true; + } + + btn_h_space!(ui); + + app.theme.accent_button_2_style(ui.style_mut()); + if ui.button("Add and continue").clicked() { + try_add |= true; + } + + if try_add { + let mut add_failed = false; if let Ok(pubkey) = PublicKey::try_from_bech32_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS .to_overlord .send(ToOverlordMessage::FollowPubkey(pubkey, list, true)); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if let Ok(profile) = Profile::try_from_bech32_string(app.add_contact.trim(), true) { @@ -353,7 +370,7 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P list, true, )); - app.people_list.entering_follow_someone_on_list = false; + can_close = true; } else if gossip_lib::nip05::parse_nip05(app.add_contact.trim()).is_ok() { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowNip05( app.add_contact.trim().to_owned(), @@ -361,16 +378,23 @@ fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::P true, )); } else { + add_failed = true; GLOBALS .status_queue .write() .write("Invalid pubkey.".to_string()); } - app.add_contact = "".to_owned(); - app.people_list.add_contact_search.clear(); - app.people_list.add_contact_searched = None; - app.people_list.add_contact_search_selected = None; - app.people_list.add_contact_search_results.clear(); + if !add_failed { + app.add_contact = "".to_owned(); + app.people_list.add_contact_search.clear(); + app.people_list.add_contact_searched = None; + app.people_list.add_contact_search_selected = None; + app.people_list.add_contact_search_results.clear(); + } + if want_close && can_close { + app.people_list.entering_follow_someone_on_list = false; + mark_refresh(app); + } } }); }); From 846a3fc4ca9a43a7fa49348e50ae9cc4d53f200a Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 11:54:29 +1300 Subject: [PATCH 59/81] FIX hint --- gossip-bin/src/ui/wizard/import_private_key.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip-bin/src/ui/wizard/import_private_key.rs b/gossip-bin/src/ui/wizard/import_private_key.rs index 906fae733..e446992c6 100644 --- a/gossip-bin/src/ui/wizard/import_private_key.rs +++ b/gossip-bin/src/ui/wizard/import_private_key.rs @@ -19,7 +19,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr if ui .add( text_edit_line!(app, app.import_priv) - .hint_text("nsec1 or hex") + .hint_text("nsec1, hex, or ncryptsec1") .desired_width(f32::INFINITY) .password(true), ) From 345c4655cc6c72ac5c2c1c715d6aa45d35a83cf0 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 6 Dec 2023 17:15:28 +1300 Subject: [PATCH 60/81] define PersonListMetadata --- gossip-lib/src/people.rs | 3 +++ gossip-lib/src/storage/types/mod.rs | 3 +++ .../src/storage/types/person_list_metadata1.rs | 12 ++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 gossip-lib/src/storage/types/person_list_metadata1.rs diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 620b73e87..f29cf1956 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -20,6 +20,9 @@ pub type Person = crate::storage::types::Person2; /// PersonList type, aliased to the latest version pub type PersonList = crate::storage::types::PersonList1; +/// PersonListMetadata type, aliased to the latest version +pub type PersonListMetadata = crate::storage::types::PersonListMetadata1; + /// Person List Compare Data #[derive(Debug, Clone)] pub struct PersonListEventData { diff --git a/gossip-lib/src/storage/types/mod.rs b/gossip-lib/src/storage/types/mod.rs index 5de23f4b7..fe3619e06 100644 --- a/gossip-lib/src/storage/types/mod.rs +++ b/gossip-lib/src/storage/types/mod.rs @@ -7,6 +7,9 @@ pub use person2::Person2; mod person_list1; pub use person_list1::PersonList1; +mod person_list_metadata1; +pub use person_list_metadata1::PersonListMetadata1; + mod person_relay1; pub use person_relay1::PersonRelay1; diff --git a/gossip-lib/src/storage/types/person_list_metadata1.rs b/gossip-lib/src/storage/types/person_list_metadata1.rs new file mode 100644 index 000000000..36bd86778 --- /dev/null +++ b/gossip-lib/src/storage/types/person_list_metadata1.rs @@ -0,0 +1,12 @@ +use nostr_types::Unixtime; +use speedy::{Readable, Writable}; + +#[derive(Debug, Clone, PartialEq, Eq, Readable, Writable)] +pub struct PersonListMetadata1 { + pub dtag: String, + pub name: String, + pub last_edit_time: Unixtime, + pub event_created_at: Unixtime, + pub event_public_len: usize, + pub event_private_len: Option, +} From 58940679e9ee4532cd01123a659b5eefc6644277 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Wed, 6 Dec 2023 17:18:42 +1300 Subject: [PATCH 61/81] storage: db_person_list_metadata database, with functions --- gossip-lib/src/error.rs | 9 + gossip-lib/src/process.rs | 100 +++++++- gossip-lib/src/storage/mod.rs | 70 +++++- .../src/storage/person_lists_metadata1.rs | 214 ++++++++++++++++++ .../storage/types/person_list_metadata1.rs | 15 +- 5 files changed, 404 insertions(+), 4 deletions(-) create mode 100644 gossip-lib/src/storage/person_lists_metadata1.rs diff --git a/gossip-lib/src/error.rs b/gossip-lib/src/error.rs index 443d7b42b..d56601d71 100644 --- a/gossip-lib/src/error.rs +++ b/gossip-lib/src/error.rs @@ -1,4 +1,5 @@ use crate::comms::{ToMinionMessage, ToOverlordMessage}; +use crate::people::PersonList; /// Error kinds that can occur in gossip-lib #[derive(Debug)] @@ -20,6 +21,7 @@ pub enum ErrorKind { NoPublicKey, NoPrivateKey, NoRelay, + NotAPersonListEvent, NoSlotsRemaining, Image(image::error::ImageError), ImageFailure, @@ -29,6 +31,9 @@ pub enum ErrorKind { InvalidDnsId, InvalidUri(http::uri::InvalidUri), InvalidUrl(String), + ListAllocationFailed, + ListAlreadyExists(PersonList), + ListEventMissingDtag, ListIsNotEmpty, ListIsWellKnown, ParseInt(std::num::ParseIntError), @@ -91,6 +96,7 @@ impl std::fmt::Display for Error { NoPublicKey => write!(f, "No public key identity available."), NoPrivateKey => write!(f, "No private key available."), NoRelay => write!(f, "Could not determine a relay to use."), + NotAPersonListEvent => write!(f, "Not a person list event"), NoSlotsRemaining => write!(f, "No custom list slots remaining."), Image(e) => write!(f, "Image: {e}"), ImageFailure => write!(f, "Image Failure"), @@ -100,6 +106,9 @@ impl std::fmt::Display for Error { InvalidDnsId => write!(f, "Invalid DNS ID (nip-05), should be user@domain"), InvalidUri(e) => write!(f, "Invalid URI: {e}"), InvalidUrl(s) => write!(f, "Invalid URL: {s}"), + ListAllocationFailed => write!(f, "List allocation failed (no more slots)"), + ListAlreadyExists(_) => write!(f, "List already exists"), + ListEventMissingDtag => write!(f, "List event missing d-tag"), ListIsNotEmpty => write!(f, "List is not empty"), ListIsWellKnown => write!(f, "List is well known and cannot be deallocated"), ParseInt(e) => write!(f, "Bad integer: {e}"), diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 3e385a483..9bcb62532 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -1,8 +1,8 @@ use crate::comms::ToOverlordMessage; -use crate::error::Error; +use crate::error::{Error, ErrorKind}; use crate::filter::EventFilterAction; use crate::globals::GLOBALS; -use crate::people::PersonList; +use crate::people::{PersonList, PersonListMetadata}; use crate::person_relay::PersonRelay; use crate::relationship::{RelationshipByAddr, RelationshipById}; use async_recursion::async_recursion; @@ -844,3 +844,99 @@ pub(crate) fn process_relationships_of_event<'a>( Ok(invalidate) } + +#[allow(dead_code)] +fn update_or_allocate_person_list_from_event( + event: &Event, + pubkey: PublicKey, +) -> Result<(PersonList, PersonListMetadata), Error> { + let mut txn = GLOBALS.storage.get_write_txn()?; + + // Determine PersonList and fetch Metadata + let (list, mut metadata) = match event.kind { + EventKind::ContactList => { + let list = PersonList::Followed; + let md = GLOBALS + .storage + .get_person_list_metadata(list)? + .unwrap_or_default(); + (list, md) + } + EventKind::MuteList => { + let list = PersonList::Muted; + let md = GLOBALS + .storage + .get_person_list_metadata(list)? + .unwrap_or_default(); + (list, md) + } + EventKind::FollowSets => { + let dtag = match event.parameter() { + Some(dtag) => dtag, + None => return Err(ErrorKind::ListEventMissingDtag.into()), + }; + if let Some((found_list, metadata)) = GLOBALS.storage.find_person_list_by_dtag(&dtag)? { + (found_list, metadata) + } else { + // Allocate new + let metadata = PersonListMetadata { + dtag, + title: "NEW LIST".to_owned(), // updated below + last_edit_time: Unixtime::now().unwrap(), + event_created_at: event.created_at, + event_public_len: 0, // updated below + event_private_len: None, // updated below + }; + let list = GLOBALS + .storage + .allocate_person_list(&metadata, Some(&mut txn))?; + (list, metadata) + } + } + _ => { + // This function does not apply to other event kinds + return Err(ErrorKind::NotAPersonListEvent.into()); + } + }; + + // Update metadata + { + metadata.event_created_at = event.created_at; + + metadata.event_public_len = event + .tags + .iter() + .filter(|t| matches!(t, Tag::Pubkey { .. })) + .count(); + + if event.kind == EventKind::ContactList { + metadata.event_private_len = None; + } else if GLOBALS.signer.is_ready() { + let mut private_len: Option = None; + if let Ok(bytes) = GLOBALS.signer.decrypt_nip04(&pubkey, &event.content) { + if let Ok(vectags) = serde_json::from_slice::>(&bytes) { + private_len = Some( + vectags + .iter() + .filter(|t| matches!(t, Tag::Pubkey { .. })) + .count(), + ); + } + } + metadata.event_private_len = private_len; + } + + if let Some(title) = event.title() { + metadata.title = title.to_owned(); + } + } + + // Save metadata + GLOBALS + .storage + .set_person_list_metadata(list, &metadata, Some(&mut txn))?; + + txn.commit()?; + + Ok((list, metadata)) +} diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 595faefc4..8ba8a386e 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -28,6 +28,7 @@ mod people1; mod people2; mod person_lists1; mod person_lists2; +mod person_lists_metadata1; mod person_relays1; mod relationships1; mod relationships_by_addr1; @@ -39,7 +40,7 @@ mod unindexed_giftwraps1; use crate::dm_channel::{DmChannel, DmChannelData}; use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; -use crate::people::{Person, PersonList}; +use crate::people::{Person, PersonList, PersonListMetadata}; use crate::person_relay::PersonRelay; use crate::profile::Profile; use crate::relationship::{RelationshipByAddr, RelationshipById}; @@ -238,6 +239,7 @@ impl Storage { let _ = self.db_relays()?; let _ = self.db_unindexed_giftwraps()?; let _ = self.db_person_lists()?; + let _ = self.db_person_lists_metadata()?; // Do migrations match self.read_migration_level()? { @@ -333,6 +335,11 @@ impl Storage { self.db_person_lists2() } + #[inline] + pub(crate) fn db_person_lists_metadata(&self) -> Result { + self.db_person_lists_metadata1() + } + // Database length functions --------------------------------- /// The number of records in the general table @@ -649,6 +656,67 @@ impl Storage { Ok(lists.get(&list).copied()) } + /// Get personlist metadata + #[allow(dead_code)] + #[inline] + pub(crate) fn get_person_list_metadata( + &self, + list: PersonList, + ) -> Result, Error> { + self.get_person_list_metadata1(list) + } + + /// Set personlist metadata + #[allow(dead_code)] + #[inline] + pub(crate) fn set_person_list_metadata<'a>( + &'a self, + list: PersonList, + metadata: &PersonListMetadata, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + self.set_person_list_metadata1(list, metadata, rw_txn) + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn get_all_person_list_metadata( + &self, + ) -> Result, Error> { + self.get_all_person_list_metadata1() + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn find_person_list_by_dtag( + &self, + dtag: &str, + ) -> Result, Error> { + self.find_person_list_by_dtag1(dtag) + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn allocate_person_list<'a>( + &'a self, + metadata: &PersonListMetadata, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result { + self.allocate_person_list1(metadata, rw_txn) + } + + #[allow(dead_code)] + #[inline] + pub(crate) fn deallocate_person_list<'a>( + &'a self, + list: PersonList, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + self.deallocate_person_list1(list, rw_txn) + } + + // Flags ------------------------------------------------------------ + def_flag!(following_only, b"following_only", false); def_flag!(wizard_complete, b"wizard_complete", false); def_flag!( diff --git a/gossip-lib/src/storage/person_lists_metadata1.rs b/gossip-lib/src/storage/person_lists_metadata1.rs new file mode 100644 index 000000000..c7bd65a52 --- /dev/null +++ b/gossip-lib/src/storage/person_lists_metadata1.rs @@ -0,0 +1,214 @@ +use super::types::{PersonList1, PersonListMetadata1}; +use crate::error::{Error, ErrorKind}; +use crate::storage::{RawDatabase, Storage}; +use heed::types::UnalignedSlice; +use heed::RwTxn; +use speedy::{Readable, Writable}; +use std::sync::Mutex; + +// PersonList1 -> PersonListMetadata1 // bool is if private or not + +static PERSON_LISTS_METADATA1_DB_CREATE_LOCK: Mutex<()> = Mutex::new(()); +static mut PERSON_LISTS_METADATA1_DB: Option = None; + +impl Storage { + pub(super) fn db_person_lists_metadata1(&self) -> Result { + unsafe { + if let Some(db) = PERSON_LISTS_METADATA1_DB { + Ok(db) + } else { + // Lock. This drops when anything returns. + let _lock = PERSON_LISTS_METADATA1_DB_CREATE_LOCK.lock(); + + // In case of a race, check again + if let Some(db) = PERSON_LISTS_METADATA1_DB { + return Ok(db); + } + + // Create it. We know that nobody else is doing this and that + // it cannot happen twice. + let mut txn = self.env.write_txn()?; + let db = self + .env + .database_options() + .types::, UnalignedSlice>() + // no .flags needed + .name("person_lists_metadata1") + .create(&mut txn)?; + txn.commit()?; + PERSON_LISTS_METADATA1_DB = Some(db); + Ok(db) + } + } + } + + pub(crate) fn get_person_list_metadata1( + &self, + list: PersonList1, + ) -> Result, Error> { + let key: Vec = list.write_to_vec()?; + let txn = self.env.read_txn()?; + Ok(match self.db_person_lists_metadata1()?.get(&txn, &key)? { + None => None, + Some(bytes) => Some(PersonListMetadata1::read_from_buffer(bytes)?), + }) + } + + pub(crate) fn set_person_list_metadata1<'a>( + &'a self, + list: PersonList1, + metadata: &PersonListMetadata1, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let key: Vec = list.write_to_vec()?; + + // Do not allow overwriting dtag or title of well defined lists: + let bytes: Vec = if list == PersonList1::Muted { + let mut md = metadata.to_owned(); + md.dtag = "muted".to_owned(); + md.title = "Muted".to_owned(); + md.write_to_vec()? + } else if list == PersonList1::Followed { + let mut md = metadata.to_owned(); + md.dtag = "followed".to_owned(); + md.title = "Followed".to_owned(); + md.write_to_vec()? + } else { + metadata.write_to_vec()? + }; + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + self.db_person_lists_metadata1()?.put(txn, &key, &bytes)?; + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } + + pub(crate) fn get_all_person_list_metadata1( + &self, + ) -> Result, Error> { + let txn = self.env.read_txn()?; + let mut output: Vec<(PersonList1, PersonListMetadata1)> = Vec::new(); + for result in self.db_person_lists_metadata1()?.iter(&txn)? { + let (key, val) = result?; + let list = PersonList1::read_from_buffer(key)?; + let metadata = PersonListMetadata1::read_from_buffer(val)?; + output.push((list, metadata)); + } + Ok(output) + } + + pub(crate) fn find_person_list_by_dtag1( + &self, + dtag: &str, + ) -> Result, Error> { + let txn = self.env.read_txn()?; + for result in self.db_person_lists_metadata1()?.iter(&txn)? { + let (key, val) = result?; + let list = PersonList1::read_from_buffer(key)?; + let metadata = PersonListMetadata1::read_from_buffer(val)?; + if &metadata.dtag == dtag { + return Ok(Some((list, metadata))); + } + } + Ok(None) + } + + pub(crate) fn allocate_person_list1<'a>( + &'a self, + metadata: &PersonListMetadata1, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result { + // Do not allocate for well-known names + if &metadata.title == "Followed" + || &metadata.title == "Muted" + || &metadata.dtag == "followed" + || &metadata.dtag == "muted" + { + return Err(ErrorKind::ListIsWellKnown.into()); + } + + // Check if it exists first (by dtag match) + if let Some((found_list, _)) = self.find_person_list_by_dtag1(&metadata.dtag)? { + return Err(ErrorKind::ListAlreadyExists(found_list).into()); + } + + let f = |txn: &mut RwTxn<'a>| -> Result { + let mut slot: u8 = 0; + + for i in 2..=255 { + let key: Vec = PersonList1::Custom(i).write_to_vec()?; + if self.db_person_lists_metadata1()?.get(&txn, &key)?.is_none() { + slot = i; + break; + } + } + + if slot < 2 { + return Err(ErrorKind::ListAllocationFailed.into()); + } + + let list = PersonList1::Custom(slot); + let key: Vec = list.write_to_vec()?; + let val: Vec = metadata.write_to_vec()?; + self.db_person_lists_metadata1()?.put(txn, &key, &val)?; + + Ok(list) + }; + + match rw_txn { + Some(txn) => Ok(f(txn)?), + None => { + let mut txn = self.env.write_txn()?; + let list = f(&mut txn)?; + txn.commit()?; + Ok(list) + } + } + } + + /// Deallocate this PersonList1 + pub(crate) fn deallocate_person_list1<'a>( + &'a self, + list: PersonList1, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + if !self.get_people_in_list(list)?.is_empty() { + return Err(ErrorKind::ListIsNotEmpty.into()); + } + + if u8::from(list) < 2 { + return Err(ErrorKind::ListIsWellKnown.into()); + } + + let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { + // note: we dont have to delete the list of people because those + // lists are keyed by pubkey, and we already checked that + // this list is not referenced. + let key: Vec = list.write_to_vec()?; + self.db_person_lists_metadata1()?.delete(txn, &key)?; + Ok(()) + }; + + match rw_txn { + Some(txn) => f(txn)?, + None => { + let mut txn = self.env.write_txn()?; + f(&mut txn)?; + txn.commit()?; + } + }; + + Ok(()) + } +} diff --git a/gossip-lib/src/storage/types/person_list_metadata1.rs b/gossip-lib/src/storage/types/person_list_metadata1.rs index 36bd86778..28e1ad4a4 100644 --- a/gossip-lib/src/storage/types/person_list_metadata1.rs +++ b/gossip-lib/src/storage/types/person_list_metadata1.rs @@ -4,9 +4,22 @@ use speedy::{Readable, Writable}; #[derive(Debug, Clone, PartialEq, Eq, Readable, Writable)] pub struct PersonListMetadata1 { pub dtag: String, - pub name: String, + pub title: String, pub last_edit_time: Unixtime, pub event_created_at: Unixtime, pub event_public_len: usize, pub event_private_len: Option, } + +impl Default for PersonListMetadata1 { + fn default() -> PersonListMetadata1 { + PersonListMetadata1 { + dtag: "".to_owned(), + title: "".to_owned(), + last_edit_time: Unixtime::now().unwrap(), + event_created_at: Unixtime::now().unwrap(), + event_public_len: 0, + event_private_len: None, + } + } +} From 7f6f05625f32c647c56a726d070a16e5650853f2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 7 Dec 2023 11:55:58 +1300 Subject: [PATCH 62/81] storage: migration19, switch to new person list metadata --- gossip-bin/src/commands.rs | 28 ++- gossip-bin/src/ui/feed/mod.rs | 8 +- gossip-bin/src/ui/mod.rs | 18 +- gossip-bin/src/ui/people/list.rs | 30 +-- gossip-bin/src/ui/people/lists.rs | 30 ++- gossip-bin/src/ui/people/person.rs | 12 +- gossip-lib/src/error.rs | 2 + gossip-lib/src/feed.rs | 5 +- gossip-lib/src/lib.rs | 2 +- gossip-lib/src/overlord/mod.rs | 45 +++-- gossip-lib/src/people.rs | 174 ++++++++---------- gossip-lib/src/process.rs | 75 ++------ gossip-lib/src/settings.rs | 5 - gossip-lib/src/storage/migrations/m15.rs | 7 +- gossip-lib/src/storage/migrations/m19.rs | 97 ++++++++++ gossip-lib/src/storage/migrations/mod.rs | 5 +- gossip-lib/src/storage/mod.rs | 118 ++++-------- gossip-lib/src/storage/types/person_list1.rs | 142 +------------- .../storage/types/person_list_metadata1.rs | 2 +- 19 files changed, 364 insertions(+), 441 deletions(-) create mode 100644 gossip-lib/src/storage/migrations/m19.rs diff --git a/gossip-bin/src/commands.rs b/gossip-bin/src/commands.rs index e901caac4..7036c34e9 100644 --- a/gossip-bin/src/commands.rs +++ b/gossip-bin/src/commands.rs @@ -1,8 +1,8 @@ use bech32::FromBase32; -use gossip_lib::PersonList; use gossip_lib::PersonRelay; use gossip_lib::GLOBALS; use gossip_lib::{Error, ErrorKind}; +use gossip_lib::{PersonList, PersonListMetadata}; use nostr_types::{ Event, EventAddr, EventKind, Id, NostrBech32, NostrUrl, PrivateKey, PublicKey, RelayUrl, UncheckedUrl, Unixtime, @@ -269,7 +269,11 @@ pub fn add_person_list(cmd: Command, mut args: env::Args) -> Result<(), Error> { None => return cmd.usage("Missing listname parameter".to_string()), }; - let _list = PersonList::allocate(&listname, None)?; + let mut metadata: PersonListMetadata = Default::default(); + metadata.dtag = listname.clone(); + metadata.title = listname.clone(); + + let _list = GLOBALS.storage.allocate_person_list(&metadata, None)?; Ok(()) } @@ -503,9 +507,9 @@ pub fn print_muted(_cmd: Command) -> Result<(), Error> { } pub fn print_person_lists(_cmd: Command) -> Result<(), Error> { - let lists = PersonList::all_lists(); - for (list, name) in lists.iter() { - println!("LIST {}: {}", u8::from(*list), name); + let all = GLOBALS.storage.get_all_person_list_metadata()?; + for (list, metadata) in all.iter() { + println!("LIST {}: {}", u8::from(*list), metadata.title); let members = GLOBALS.storage.get_people_in_list(*list)?; for (pk, public) in &members { if let Some(person) = GLOBALS.storage.read_person(pk)? { @@ -720,11 +724,15 @@ pub fn rename_person_list(cmd: Command, mut args: env::Args) -> Result<(), Error None => return cmd.usage("Missing newname parameter".to_string()), }; - if let Some(list) = PersonList::from_number(number) { - list.rename(&newname, None)?; - } else { - println!("No list with number={}", number); - } + let list = match PersonList::from_number(number) { + Some(list) => list, + None => { + println!("No list with number={}", number); + return Ok(()); + } + }; + + GLOBALS.storage.rename_person_list(list, newname, None)?; Ok(()) } diff --git a/gossip-bin/src/ui/feed/mod.rs b/gossip-bin/src/ui/feed/mod.rs index 59c3ec1be..607ac7dba 100644 --- a/gossip-bin/src/ui/feed/mod.rs +++ b/gossip-bin/src/ui/feed/mod.rs @@ -53,6 +53,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram match feed_kind { FeedKind::List(list, with_replies) => { + let metadata = GLOBALS + .storage + .get_person_list_metadata(list) + .unwrap_or_default() + .unwrap_or_default(); + let feed = GLOBALS.feed.get_followed(); let id = format!( "{} {}", @@ -65,7 +71,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram egui::Layout::left_to_right(egui::Align::Center), |ui| { add_left_space(ui); - ui.heading(list.name()); + ui.heading(metadata.title); recompute_btn(app, ui); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { diff --git a/gossip-bin/src/ui/mod.rs b/gossip-bin/src/ui/mod.rs index a12cc913e..21bb2f7fe 100644 --- a/gossip-bin/src/ui/mod.rs +++ b/gossip-bin/src/ui/mod.rs @@ -140,7 +140,14 @@ impl Page { Page::DmChatList => (SubMenu::Feeds.as_str(), "Private chats".into()), Page::Feed(feedkind) => ("Feed", feedkind.to_string()), Page::PeopleLists => ("Person Lists", "Person Lists".into()), - Page::PeopleList(list) => ("People", list.name()), + Page::PeopleList(list) => { + let metadata = GLOBALS + .storage + .get_person_list_metadata(*list) + .unwrap_or_default() + .unwrap_or_default(); + ("People", metadata.title) + } Page::Person(pk) => { let name = gossip_lib::names::best_name_from_pubkey_lookup(pk); ("Profile", name) @@ -835,8 +842,11 @@ impl GossipUi { let (mut cstate, header_response) = self.get_openable_menu(ui, ctx, SubMenu::Feeds); cstate.show_body_indented(&header_response, ui, |ui| { - let all_lists = PersonList::all_lists(); - for (list, listname) in all_lists { + let all_lists = GLOBALS + .storage + .get_all_person_list_metadata() + .unwrap_or_default(); + for (list, metadata) in all_lists { // skip muted if list == PersonList::Muted { continue; @@ -844,7 +854,7 @@ impl GossipUi { self.add_menu_item_page_titled( ui, Page::Feed(FeedKind::List(list, self.mainfeed_include_nonroot)), - &listname, + &metadata.title, ); } self.add_menu_item_page( diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index aa6cfe557..50e91f6d6 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -34,15 +34,14 @@ pub(super) fn update( ui.add_space(12.0); - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&list) - .map(|v| v.value().clone()) + let metadata = GLOBALS + .storage + .get_person_list_metadata(list) + .unwrap_or_default() .unwrap_or_default(); let mut asof = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(metadata.event_created_at.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { @@ -50,15 +49,15 @@ pub(super) fn update( } } - let txt = if let Some(private_len) = latest_event_data.private_len { + let txt = if let Some(private_len) = metadata.event_private_len { format!( "REMOTE: {} (public_len={} private_len={})", - asof, latest_event_data.public_len, private_len + asof, metadata.event_public_len, private_len ) } else { format!( "REMOTE: {} (public_len={})", - asof, latest_event_data.public_len + asof, metadata.event_public_len ) }; @@ -132,17 +131,8 @@ pub(super) fn update( ui.add_space(10.0); - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - let mut ledit = "unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(metadata.last_edit_time.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { @@ -172,7 +162,7 @@ pub(super) fn update( ui.separator(); ui.add_space(10.0); - ui.heading(format!("{} ({})", list.name(), people.len())); + ui.heading(format!("{} ({})", metadata.title, people.len())); ui.add_space(14.0); ui.separator(); diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index 06ac81a9f..ba3e41f15 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -5,7 +5,7 @@ use eframe::egui; use egui::{Context, Ui, Vec2}; use egui_winit::egui::{vec2, Label, RichText, Sense}; use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::{PersonList, GLOBALS}; +use gossip_lib::{PersonList, PersonListMetadata, GLOBALS}; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { widgets::page_header(ui, Page::PeopleLists.name(), |ui| { @@ -15,14 +15,18 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }); let enable_scroll = true; - let all_lists = PersonList::all_lists(); + + let all_lists = GLOBALS + .storage + .get_all_person_list_metadata() + .unwrap_or_default(); let color = app.theme.accent_color(); app.vert_scroll_area() .id_source("people_lists_scroll") .enable_scrolling(enable_scroll) .show(ui, |ui| { - for (list, listname) in all_lists { + for (list, metadata) in all_lists { let count = GLOBALS .storage .get_people_in_list(list) @@ -33,7 +37,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.vertical(|ui| { ui.horizontal(|ui| { - ui.add(Label::new(RichText::new(listname).heading().color(color))); + ui.add(Label::new( + RichText::new(metadata.title).heading().color(color), + )); ui.with_layout(egui::Layout::right_to_left(egui::Align::TOP), |ui| { if matches!(list, PersonList::Custom(_)) { @@ -60,12 +66,18 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }); if let Some(list) = app.deleting_list { + let metadata = GLOBALS + .storage + .get_person_list_metadata(list) + .unwrap_or_default() + .unwrap_or_default(); + const DLG_SIZE: Vec2 = vec2(250.0, 120.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { ui.vertical(|ui| { ui.label("Are you sure you want to delete:"); ui.add_space(10.0); - ui.heading(list.name()); + ui.heading(metadata.title); ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { ui.horizontal(|ui| { if ui.button("Cancel").clicked() { @@ -103,7 +115,13 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { if ui.button("Create").clicked() { if !app.new_list_name.is_empty() { - if let Err(e) = PersonList::allocate(&app.new_list_name, None) { + let mut metadata: PersonListMetadata = Default::default(); + metadata.dtag = app.new_list_name.to_owned(); + metadata.title = app.new_list_name.to_owned(); + + if let Err(e) = + GLOBALS.storage.allocate_person_list(&metadata, None) + { GLOBALS.status_queue.write().write(format!("{}", e)); } else { app.creating_list = false; diff --git a/gossip-bin/src/ui/people/person.rs b/gossip-bin/src/ui/people/person.rs index f0f0db8cf..c27cd6131 100644 --- a/gossip-bin/src/ui/people/person.rs +++ b/gossip-bin/src/ui/people/person.rs @@ -13,7 +13,6 @@ use gossip_lib::comms::ToOverlordMessage; use gossip_lib::DmChannel; use gossip_lib::FeedKind; use gossip_lib::Person; -use gossip_lib::PersonList; use gossip_lib::GLOBALS; use nostr_types::{PublicKey, RelayUrl}; use serde_json::Value; @@ -192,9 +191,12 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe ui.separator(); ui.add_space(10.0); - let all_lists = PersonList::all_lists(); + let all_lists = GLOBALS + .storage + .get_all_person_list_metadata() + .unwrap_or_default(); if let Ok(membership_map) = GLOBALS.storage.read_person_lists(&pubkey) { - for (list, listname) in all_lists { + for (list, metadata) in all_lists { ui.horizontal(|ui| { let membership = membership_map.get(&list); let mut inlist = membership.is_some(); @@ -213,7 +215,7 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe } ui.add_space(10.0); if inlist { - ui.label(listname); + ui.label(metadata.title); ui.add_space(10.0); let public = membership.unwrap(); @@ -225,7 +227,7 @@ fn content(app: &mut GossipUi, ctx: &Context, ui: &mut Ui, pubkey: PublicKey, pe .add_person_to_list(&pubkey, list, !*public, None); } } else { - ui.label(RichText::new(listname).weak()); + ui.label(RichText::new(metadata.title).weak()); } }); } diff --git a/gossip-lib/src/error.rs b/gossip-lib/src/error.rs index d56601d71..79f330c77 100644 --- a/gossip-lib/src/error.rs +++ b/gossip-lib/src/error.rs @@ -36,6 +36,7 @@ pub enum ErrorKind { ListEventMissingDtag, ListIsNotEmpty, ListIsWellKnown, + ListNotFound, ParseInt(std::num::ParseIntError), Regex(regex::Error), RelayPickerError(gossip_relay_picker::Error), @@ -111,6 +112,7 @@ impl std::fmt::Display for Error { ListEventMissingDtag => write!(f, "List event missing d-tag"), ListIsNotEmpty => write!(f, "List is not empty"), ListIsWellKnown => write!(f, "List is well known and cannot be deallocated"), + ListNotFound => write!(f, "List was not found"), ParseInt(e) => write!(f, "Bad integer: {e}"), Regex(e) => write!(f, "Regex: {e}"), RelayPickerError(e) => write!(f, "Relay Picker error: {e}"), diff --git a/gossip-lib/src/feed.rs b/gossip-lib/src/feed.rs index 85d508bfe..150c1f427 100644 --- a/gossip-lib/src/feed.rs +++ b/gossip-lib/src/feed.rs @@ -30,7 +30,10 @@ impl std::fmt::Display for FeedKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FeedKind::DmChat(channel) => write!(f, "{}", channel.name()), - FeedKind::List(pl, _) => write!(f, "{}", pl.name()), + FeedKind::List(pl, _) => match GLOBALS.storage.get_person_list_metadata(*pl) { + Ok(Some(md)) => write!(f, "{}", md.title), + _ => write!(f, "UNKNOWN"), + }, FeedKind::Inbox(_) => write!(f, "Inbox"), FeedKind::Thread { id, diff --git a/gossip-lib/src/lib.rs b/gossip-lib/src/lib.rs index de2cc70b3..f93a8f256 100644 --- a/gossip-lib/src/lib.rs +++ b/gossip-lib/src/lib.rs @@ -102,7 +102,7 @@ mod overlord; pub use overlord::Overlord; mod people; -pub use people::{People, Person, PersonList}; +pub use people::{People, Person, PersonList, PersonListMetadata}; mod person_relay; pub use person_relay::PersonRelay; diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index 8718079ae..b6c26f857 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -861,10 +861,19 @@ impl Overlord { /// Delete a person list pub async fn delete_person_list(&mut self, list: PersonList) -> Result<(), Error> { + // Get the metadata first, we need it to delete events + let metadata = match GLOBALS.storage.get_person_list_metadata(list)? { + Some(m) => m, + None => return Ok(()), + }; + // Delete the list locally - GLOBALS.people.clear_person_list(list)?; - list.deallocate(None)?; - let name = list.name(); + let mut txn = GLOBALS.storage.get_write_txn()?; + GLOBALS.storage.clear_person_list(list, Some(&mut txn))?; + GLOBALS + .storage + .deallocate_person_list(list, Some(&mut txn))?; + txn.commit()?; // If we are only following, nothing else needed if GLOBALS.storage.get_flag_following_only() { @@ -884,7 +893,7 @@ impl Overlord { &[EventKind::FollowSets], &[public_key], None, - |event| event.parameter() == Some(name.clone()), + |event| event.parameter().as_ref() == Some(&metadata.dtag), false, )?; @@ -913,7 +922,7 @@ impl Overlord { let mut tags: Vec = vec![Tag::Address { kind: EventKind::FollowSets, pubkey: public_key.into(), - d: name.clone(), + d: metadata.dtag.clone(), relay_url: None, marker: None, trailing: Vec::new(), @@ -1791,6 +1800,11 @@ impl Overlord { /// Publish the user's specified PersonList pub async fn push_person_list(&mut self, list: PersonList) -> Result<(), Error> { + let metadata = match GLOBALS.storage.get_person_list_metadata(list)? { + Some(m) => m, + None => return Ok(()), + }; + let event = GLOBALS.people.generate_person_list_event(list).await?; // process event locally @@ -1803,7 +1817,7 @@ impl Overlord { for relay in relays { // Send it the event to pull our followers - tracing::debug!("Pushing PersonList={} to {}", list.name(), &relay.url); + tracing::debug!("Pushing PersonList={} to {}", metadata.title, &relay.url); self.engage_minion( relay.url.clone(), @@ -2520,13 +2534,19 @@ impl Overlord { } }; + // Get the metadata first + let mut metadata = match GLOBALS.storage.get_person_list_metadata(list)? { + Some(m) => m, + None => return Ok(()), + }; + // Load the latest PersonList event from the database let event = { - if let Some(event) = - GLOBALS - .storage - .get_replaceable_event(list.event_kind(), my_pubkey, &list.name())? - { + if let Some(event) = GLOBALS.storage.get_replaceable_event( + list.event_kind(), + my_pubkey, + &metadata.dtag, + )? { event.clone() } else { GLOBALS @@ -2609,9 +2629,10 @@ impl Overlord { let last_edit = if merge { now } else { event.created_at }; + metadata.last_edit_time = last_edit; GLOBALS .storage - .set_person_list_last_edit_time(list, last_edit.0, Some(&mut txn))?; + .set_person_list_metadata(list, &metadata, Some(&mut txn))?; txn.commit()?; diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index f29cf1956..d03883c02 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -23,30 +23,6 @@ pub type PersonList = crate::storage::types::PersonList1; /// PersonListMetadata type, aliased to the latest version pub type PersonListMetadata = crate::storage::types::PersonListMetadata1; -/// Person List Compare Data -#[derive(Debug, Clone)] -pub struct PersonListEventData { - /// The timestamp of the latest event - pub when: Unixtime, - - /// The number of public entries in the latest event - pub public_len: usize, - - /// The number of private entires in the latest event, or None if it - /// couldn't be computed (not logged in, Following event, or none found) - pub private_len: Option, -} - -impl Default for PersonListEventData { - fn default() -> PersonListEventData { - PersonListEventData { - when: Unixtime(0), - public_len: 0, - private_len: None, - } - } -} - /// Handles people and remembers what needs to be done for each, such as fetching /// metadata or avatars. pub struct People { @@ -74,9 +50,6 @@ pub struct People { // This only relates to the Metadata event, not subsequent avatar or nip05 // loads. fetching_metadata: DashMap, - - /// Latest person list event data for each PersonList - pub latest_person_list_event_data: DashMap, } impl Default for People { @@ -95,14 +68,11 @@ impl People { recheck_nip05: DashSet::new(), people_of_interest: DashSet::new(), fetching_metadata: DashMap::new(), - latest_person_list_event_data: DashMap::new(), } } // Start the periodic task management pub(crate) fn start() { - GLOBALS.people.update_latest_person_list_event_data(); - task::spawn(async { loop { let fetch_metadata_looptime_ms = @@ -122,53 +92,6 @@ impl People { }); } - /// Search local events for the latest PersonList event for each kind of PersonList, - /// and determine their timestamps and lengths, storing result in People. - pub fn update_latest_person_list_event_data(&self) { - // Get public key, or give up - let pk = match GLOBALS.storage.read_setting_public_key() { - Some(pk) => pk, - None => return, - }; - - for (person_list, _) in PersonList::all_lists() { - if let Ok(Some(event)) = GLOBALS.storage.get_replaceable_event( - person_list.event_kind(), - pk, - &person_list.name(), - ) { - self.latest_person_list_event_data.insert( - person_list, - PersonListEventData { - when: event.created_at, - public_len: event - .tags - .iter() - .filter(|t| matches!(t, Tag::Pubkey { .. })) - .count(), - private_len: { - let mut private_len: Option = None; - if !matches!(person_list, PersonList::Followed) - && GLOBALS.signer.is_ready() - { - if let Ok(bytes) = GLOBALS.signer.decrypt_nip04(&pk, &event.content) - { - if let Ok(vectags) = serde_json::from_slice::>(&bytes) - { - private_len = Some(vectags.len()); - } - } - } - private_len - }, - }, - ); - } else { - self.latest_person_list_event_data.remove(&person_list); - } - } - } - /// Get all the pubkeys that the user subscribes to in any list pub fn get_subscribed_pubkeys(&self) -> Vec { // We subscribe to all people in all lists. @@ -656,6 +579,12 @@ impl People { return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()); } + // Get the personlist metadata (dtag, etc) + let metadata = match GLOBALS.storage.get_person_list_metadata(person_list)? { + Some(m) => m, + None => return Err(ErrorKind::ListNotFound.into()), + }; + let my_pubkey = GLOBALS.signer.public_key().unwrap(); // Read the person list in two parts @@ -679,7 +608,7 @@ impl People { // We fetch for FollowSets to preserve various tags we don't use GLOBALS .storage - .get_replaceable_event(kind, my_pubkey, &person_list.name())? + .get_replaceable_event(kind, my_pubkey, &metadata.dtag)? } _ => None, }; @@ -728,7 +657,7 @@ impl People { // Add d-tag if using FollowSets if matches!(person_list, PersonList::Custom(_)) { tags.push(Tag::Identifier { - d: person_list.name(), + d: metadata.dtag.clone(), trailing: vec![], }); } @@ -816,11 +745,12 @@ impl People { } GLOBALS.ui_people_to_invalidate.write().push(*pubkey); - GLOBALS.storage.set_person_list_last_edit_time( - list, - Unixtime::now().unwrap().0, - Some(&mut txn), - )?; + if let Some(mut metadata) = GLOBALS.storage.get_person_list_metadata(list)? { + metadata.last_edit_time = Unixtime::now().unwrap(); + GLOBALS + .storage + .set_person_list_metadata(list, &metadata, Some(&mut txn))?; + } txn.commit()?; @@ -832,11 +762,13 @@ impl People { let mut txn = GLOBALS.storage.get_write_txn()?; GLOBALS.storage.clear_person_list(list, Some(&mut txn))?; - GLOBALS.storage.set_person_list_last_edit_time( - list, - Unixtime::now().unwrap().0, - Some(&mut txn), - )?; + + if let Some(mut metadata) = GLOBALS.storage.get_person_list_metadata(list)? { + metadata.last_edit_time = Unixtime::now().unwrap(); + GLOBALS + .storage + .set_person_list_metadata(list, &metadata, Some(&mut txn))?; + } txn.commit()?; @@ -868,11 +800,17 @@ impl People { .remove_person_from_list(pubkey, PersonList::Muted, Some(&mut txn))?; } - GLOBALS.storage.set_person_list_last_edit_time( - PersonList::Muted, - Unixtime::now().unwrap().0, - Some(&mut txn), - )?; + if let Some(mut metadata) = GLOBALS + .storage + .get_person_list_metadata(PersonList::Muted)? + { + metadata.last_edit_time = Unixtime::now().unwrap(); + GLOBALS.storage.set_person_list_metadata( + PersonList::Muted, + &metadata, + Some(&mut txn), + )?; + } txn.commit()?; @@ -977,3 +915,51 @@ impl People { struct Nip05Patch { nip05: Option, } + +// Determine PersonList and fetches Metadata, allocating if needed. +// This does NOT update that metadata from the event. +pub(crate) fn fetch_current_personlist_matching_event( + event: &Event, +) -> Result<(PersonList, PersonListMetadata), Error> { + let (list, metadata) = match event.kind { + EventKind::ContactList => { + let list = PersonList::Followed; + let md = GLOBALS + .storage + .get_person_list_metadata(list)? + .unwrap_or_default(); + (list, md) + } + EventKind::MuteList => { + let list = PersonList::Muted; + let md = GLOBALS + .storage + .get_person_list_metadata(list)? + .unwrap_or_default(); + (list, md) + } + EventKind::FollowSets => { + let dtag = match event.parameter() { + Some(dtag) => dtag, + None => return Err(ErrorKind::ListEventMissingDtag.into()), + }; + if let Some((found_list, metadata)) = GLOBALS.storage.find_person_list_by_dtag(&dtag)? { + (found_list, metadata) + } else { + // Allocate new + let mut metadata: PersonListMetadata = Default::default(); + metadata.dtag = dtag; + metadata.event_created_at = event.created_at; + // This is slim metadata.. The caller will fix it. + let list = GLOBALS.storage.allocate_person_list(&metadata, None)?; + (list, metadata) + } + } + _ => { + // This function does not apply to other event kinds + return Err(ErrorKind::NotAPersonListEvent.into()); + } + }; + + Ok((list, metadata)) +} diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 9bcb62532..8ad01650b 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -1,5 +1,5 @@ use crate::comms::ToOverlordMessage; -use crate::error::{Error, ErrorKind}; +use crate::error::Error; use crate::filter::EventFilterAction; use crate::globals::GLOBALS; use crate::people::{PersonList, PersonListMetadata}; @@ -247,9 +247,9 @@ pub async fn process_new_event( if event.kind == EventKind::ContactList { if let Some(pubkey) = GLOBALS.signer.public_key() { if event.pubkey == pubkey { - // Update this data for the UI. We don't actually process the latest event - // until the user gives the go ahead. - GLOBALS.people.update_latest_person_list_event_data(); + // Updates stamps and counts, does NOT change membership + let (_personlist, _metadata) = + update_or_allocate_person_list_from_event(event, pubkey)?; } else { process_somebody_elses_contact_list(event).await?; } @@ -257,22 +257,12 @@ pub async fn process_new_event( process_somebody_elses_contact_list(event).await?; } } else if event.kind == EventKind::MuteList || event.kind == EventKind::FollowSets { - // Allocate a slot for this person list - if event.kind == EventKind::FollowSets { - // get d-tag - for tag in event.tags.iter() { - if let Tag::Identifier { d, .. } = tag { - // This will allocate if missing, and will be ok if it exists - PersonList::allocate(d, None)?; - } - } - } - + // Only our own if let Some(pubkey) = GLOBALS.signer.public_key() { if event.pubkey == pubkey { - // Update this data for the UI. We don't actually process the latest event - // until the user gives the go ahead. - GLOBALS.people.update_latest_person_list_event_data(); + // Updates stamps and counts, does NOT change membership + let (_personlist, _metadata) = + update_or_allocate_person_list_from_event(event, pubkey)?; } } } else if event.kind == EventKind::RelayList { @@ -845,7 +835,8 @@ pub(crate) fn process_relationships_of_event<'a>( Ok(invalidate) } -#[allow(dead_code)] +// This updates the event data and maybe the title, but it does NOT update the list +// (that happens only when the user overwrites/merges) fn update_or_allocate_person_list_from_event( event: &Event, pubkey: PublicKey, @@ -853,51 +844,7 @@ fn update_or_allocate_person_list_from_event( let mut txn = GLOBALS.storage.get_write_txn()?; // Determine PersonList and fetch Metadata - let (list, mut metadata) = match event.kind { - EventKind::ContactList => { - let list = PersonList::Followed; - let md = GLOBALS - .storage - .get_person_list_metadata(list)? - .unwrap_or_default(); - (list, md) - } - EventKind::MuteList => { - let list = PersonList::Muted; - let md = GLOBALS - .storage - .get_person_list_metadata(list)? - .unwrap_or_default(); - (list, md) - } - EventKind::FollowSets => { - let dtag = match event.parameter() { - Some(dtag) => dtag, - None => return Err(ErrorKind::ListEventMissingDtag.into()), - }; - if let Some((found_list, metadata)) = GLOBALS.storage.find_person_list_by_dtag(&dtag)? { - (found_list, metadata) - } else { - // Allocate new - let metadata = PersonListMetadata { - dtag, - title: "NEW LIST".to_owned(), // updated below - last_edit_time: Unixtime::now().unwrap(), - event_created_at: event.created_at, - event_public_len: 0, // updated below - event_private_len: None, // updated below - }; - let list = GLOBALS - .storage - .allocate_person_list(&metadata, Some(&mut txn))?; - (list, metadata) - } - } - _ => { - // This function does not apply to other event kinds - return Err(ErrorKind::NotAPersonListEvent.into()); - } - }; + let (list, mut metadata) = crate::people::fetch_current_personlist_matching_event(event)?; // Update metadata { diff --git a/gossip-lib/src/settings.rs b/gossip-lib/src/settings.rs index ff652aa4a..72c904c69 100644 --- a/gossip-lib/src/settings.rs +++ b/gossip-lib/src/settings.rs @@ -5,7 +5,6 @@ use nostr_types::PublicKey; use paste::paste; use serde::{Deserialize, Serialize}; use speedy::{Readable, Writable}; -use std::collections::BTreeMap; macro_rules! load_setting { ($field:ident) => { @@ -59,7 +58,6 @@ pub struct Settings { pub replies_chunk: u64, pub person_feed_chunk: u64, pub overlap: u64, - pub custom_person_list_map: BTreeMap, // Event Selection pub reposts: bool, @@ -145,7 +143,6 @@ impl Default for Settings { replies_chunk: default_setting!(replies_chunk), person_feed_chunk: default_setting!(person_feed_chunk), overlap: default_setting!(overlap), - custom_person_list_map: default_setting!(custom_person_list_map), reposts: default_setting!(reposts), show_long_form: default_setting!(show_long_form), show_mentions: default_setting!(show_mentions), @@ -227,7 +224,6 @@ impl Settings { replies_chunk: load_setting!(replies_chunk), person_feed_chunk: load_setting!(person_feed_chunk), overlap: load_setting!(overlap), - custom_person_list_map: load_setting!(custom_person_list_map), reposts: load_setting!(reposts), show_long_form: load_setting!(show_long_form), show_mentions: load_setting!(show_mentions), @@ -305,7 +301,6 @@ impl Settings { save_setting!(replies_chunk, self, txn); save_setting!(person_feed_chunk, self, txn); save_setting!(overlap, self, txn); - save_setting!(custom_person_list_map, self, txn); save_setting!(reposts, self, txn); save_setting!(show_long_form, self, txn); save_setting!(show_mentions, self, txn); diff --git a/gossip-lib/src/storage/migrations/m15.rs b/gossip-lib/src/storage/migrations/m15.rs index 7da7576d6..5c9287b2a 100644 --- a/gossip-lib/src/storage/migrations/m15.rs +++ b/gossip-lib/src/storage/migrations/m15.rs @@ -2,6 +2,7 @@ use crate::error::Error; use crate::storage::types::PersonList1; use crate::storage::Storage; use heed::RwTxn; +use speedy::Writable; use std::collections::HashMap; impl Storage { @@ -30,7 +31,11 @@ impl Storage { let mut edit_times: HashMap = HashMap::new(); edit_times.insert(PersonList1::Followed, self.read_last_contact_list_edit()?); edit_times.insert(PersonList1::Muted, self.read_last_mute_list_edit()?); - self.write_person_lists_last_edit_times(edit_times, Some(txn))?; + + let bytes = edit_times.write_to_vec()?; + self.general + .put(txn, b"person_lists_last_edit_times", bytes.as_slice())?; + Ok(()) } } diff --git a/gossip-lib/src/storage/migrations/m19.rs b/gossip-lib/src/storage/migrations/m19.rs new file mode 100644 index 000000000..d9f3e4ae1 --- /dev/null +++ b/gossip-lib/src/storage/migrations/m19.rs @@ -0,0 +1,97 @@ +use crate::error::Error; +use crate::storage::types::{PersonList1, PersonListMetadata1}; +use crate::storage::Storage; +use heed::RwTxn; +use nostr_types::Unixtime; +use speedy::Readable; +use std::collections::{BTreeMap, HashMap}; + +impl Storage { + pub(super) fn m19_trigger(&self) -> Result<(), Error> { + Ok(()) + } + + pub(super) fn m19_migrate<'a>( + &'a self, + prefix: &str, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { + // Info message + tracing::info!("{prefix}: ..."); + + // Migrate + self.m19_populate_person_list_metadata(txn)?; + + Ok(()) + } + + fn m19_populate_person_list_metadata<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> { + // read custom_person_list_map setting + let name_map: BTreeMap = { + let maybe_map = match self.general.get(&txn, b"custom_person_list_map") { + Err(_) => None, + Ok(None) => None, + Ok(Some(bytes)) => match >::read_from_buffer(bytes) { + Ok(val) => Some(val), + Err(_) => None, + }, + }; + maybe_map.unwrap_or_else(|| { + let mut m = BTreeMap::new(); + m.insert(0, "Muted".to_owned()); + m.insert(1, "Followed".to_owned()); + m + }) + }; + + let last_edit_times: HashMap = + self.m19_read_person_lists_last_edit_times()?; + + let mut lists: Vec = + name_map.keys().map(|k| PersonList1::from_u8(*k)).collect(); + + for list in lists.drain(..) { + let mut metadata: PersonListMetadata1 = Default::default(); + metadata.last_edit_time = Unixtime( + last_edit_times + .get(&list) + .copied() + .unwrap_or(Unixtime::now().unwrap().0), + ); + if list == PersonList1::Muted { + metadata.dtag = "muted".to_string(); + metadata.title = "Muted".to_string(); + } else if list == PersonList1::Followed { + metadata.dtag = "followed".to_string(); + metadata.title = "Followed".to_string(); + } else { + let name = name_map + .get(&u8::from(list)) + .map(|s| s.as_str()) + .unwrap_or("Unnamed"); + metadata.dtag = name.to_string(); + metadata.title = name.to_string(); + } + + self.set_person_list_metadata1(list, &metadata, Some(txn))?; + } + + // Now remove the two maps + self.general.delete(txn, b"custom_person_list_map")?; + self.general.delete(txn, b"person_lists_last_edit_times")?; + + Ok(()) + } + + /// Read the user's old last ContactList edit time + pub fn m19_read_person_lists_last_edit_times( + &self, + ) -> Result, Error> { + let txn = self.env.read_txn()?; + + match self.general.get(&txn, b"person_lists_last_edit_times")? { + None => Ok(HashMap::new()), + Some(bytes) => Ok(HashMap::::read_from_buffer(bytes)?), + } + } +} diff --git a/gossip-lib/src/storage/migrations/mod.rs b/gossip-lib/src/storage/migrations/mod.rs index 05554cf2d..bccf17311 100644 --- a/gossip-lib/src/storage/migrations/mod.rs +++ b/gossip-lib/src/storage/migrations/mod.rs @@ -10,6 +10,7 @@ mod m15; mod m16; mod m17; mod m18; +mod m19; mod m2; mod m3; mod m4; @@ -24,7 +25,7 @@ use crate::error::{Error, ErrorKind}; use heed::RwTxn; impl Storage { - const MAX_MIGRATION_LEVEL: u32 = 18; + const MAX_MIGRATION_LEVEL: u32 = 19; /// Initialize the database from empty pub(super) fn init_from_empty(&self) -> Result<(), Error> { @@ -79,6 +80,7 @@ impl Storage { 16 => self.m16_trigger()?, 17 => self.m17_trigger()?, 18 => self.m18_trigger()?, + 19 => self.m19_trigger()?, _ => panic!("Unreachable migration level"), } @@ -106,6 +108,7 @@ impl Storage { 16 => self.m16_migrate(&prefix, txn)?, 17 => self.m17_migrate(&prefix, txn)?, 18 => self.m18_migrate(&prefix, txn)?, + 19 => self.m19_migrate(&prefix, txn)?, _ => panic!("Unreachable migration level"), }; diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index 8ba8a386e..a0cbd4dd5 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -54,7 +54,7 @@ use nostr_types::{ }; use paste::paste; use speedy::{Readable, Writable}; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{HashMap, HashSet}; use std::ops::Bound; use self::event_tag_index1::INDEXED_TAGS; @@ -601,65 +601,9 @@ impl Storage { } } - /// Write the user's last PersonList edit times - pub fn write_person_lists_last_edit_times<'a>( - &'a self, - times: HashMap, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result<(), Error> { - let bytes = times.write_to_vec()?; - - let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> { - self.general - .put(txn, b"person_lists_last_edit_times", bytes.as_slice())?; - Ok(()) - }; - - match rw_txn { - Some(txn) => f(txn)?, - None => { - let mut txn = self.env.write_txn()?; - f(&mut txn)?; - txn.commit()?; - } - }; - - Ok(()) - } - - /// Read the user's last ContactList edit time - pub fn read_person_lists_last_edit_times(&self) -> Result, Error> { - let txn = self.env.read_txn()?; - - match self.general.get(&txn, b"person_lists_last_edit_times")? { - None => Ok(HashMap::new()), - Some(bytes) => Ok(HashMap::::read_from_buffer(bytes)?), - } - } - - /// Set a person list last edit time - pub fn set_person_list_last_edit_time<'a>( - &'a self, - list: PersonList, - time: i64, - rw_txn: Option<&mut RwTxn<'a>>, - ) -> Result<(), Error> { - let mut lists = self.read_person_lists_last_edit_times()?; - let _ = lists.insert(list, time); - self.write_person_lists_last_edit_times(lists, rw_txn)?; - Ok(()) - } - - /// Get a person list last edit time - pub fn get_person_list_last_edit_time(&self, list: PersonList) -> Result, Error> { - let lists = self.read_person_lists_last_edit_times()?; - Ok(lists.get(&list).copied()) - } - /// Get personlist metadata - #[allow(dead_code)] #[inline] - pub(crate) fn get_person_list_metadata( + pub fn get_person_list_metadata( &self, list: PersonList, ) -> Result, Error> { @@ -667,9 +611,8 @@ impl Storage { } /// Set personlist metadata - #[allow(dead_code)] #[inline] - pub(crate) fn set_person_list_metadata<'a>( + pub fn set_person_list_metadata<'a>( &'a self, list: PersonList, metadata: &PersonListMetadata, @@ -678,26 +621,26 @@ impl Storage { self.set_person_list_metadata1(list, metadata, rw_txn) } - #[allow(dead_code)] + /// Get all person lists with their metadata #[inline] - pub(crate) fn get_all_person_list_metadata( + pub fn get_all_person_list_metadata( &self, ) -> Result, Error> { self.get_all_person_list_metadata1() } - #[allow(dead_code)] + /// Find a person list by "d" tag #[inline] - pub(crate) fn find_person_list_by_dtag( + pub fn find_person_list_by_dtag( &self, dtag: &str, ) -> Result, Error> { self.find_person_list_by_dtag1(dtag) } - #[allow(dead_code)] + /// Allocate a new person list #[inline] - pub(crate) fn allocate_person_list<'a>( + pub fn allocate_person_list<'a>( &'a self, metadata: &PersonListMetadata, rw_txn: Option<&mut RwTxn<'a>>, @@ -705,9 +648,9 @@ impl Storage { self.allocate_person_list1(metadata, rw_txn) } - #[allow(dead_code)] + /// Deallocate an empty person list #[inline] - pub(crate) fn deallocate_person_list<'a>( + pub fn deallocate_person_list<'a>( &'a self, list: PersonList, rw_txn: Option<&mut RwTxn<'a>>, @@ -715,6 +658,23 @@ impl Storage { self.deallocate_person_list1(list, rw_txn) } + // GINA add more person_list functions, higher level now + pub fn rename_person_list<'a>( + &'a self, + list: PersonList, + newname: String, + rw_txn: Option<&mut RwTxn<'a>>, + ) -> Result<(), Error> { + let mut md = match self.get_person_list_metadata(list)? { + Some(md) => md, + None => return Err(ErrorKind::ListNotFound.into()), + }; + md.title = newname; + md.last_edit_time = Unixtime::now().unwrap(); + self.set_person_list_metadata(list, &md, rw_txn)?; + Ok(()) + } + // Flags ------------------------------------------------------------ def_flag!(following_only, b"following_only", false); @@ -753,17 +713,6 @@ impl Storage { 60 * 60 * 24 * 30 ); def_setting!(overlap, b"overlap", u64, 300); - def_setting!( - custom_person_list_map, - b"custom_person_list_map", - BTreeMap::, - { - let mut m = BTreeMap::new(); - m.insert(0, "Muted".to_owned()); - m.insert(1, "Followed".to_owned()); - m - } - ); def_setting!(reposts, b"reposts", bool, true); def_setting!(show_long_form, b"show_long_form", bool, false); def_setting!(show_mentions, b"show_mentions", bool, true); @@ -2367,7 +2316,11 @@ impl Storage { map.insert(list, public); self.write_person_lists(pubkey, map, Some(txn))?; let now = Unixtime::now().unwrap(); - self.set_person_list_last_edit_time(list, now.0, Some(txn))?; + if let Some(mut metadata) = self.get_person_list_metadata(list)? { + metadata.last_edit_time = now; + self.set_person_list_metadata(list, &metadata, Some(txn))?; + } + Ok(()) }; @@ -2397,7 +2350,10 @@ impl Storage { map.remove(&list); self.write_person_lists(pubkey, map, Some(txn))?; let now = Unixtime::now().unwrap(); - self.set_person_list_last_edit_time(list, now.0, Some(txn))?; + if let Some(mut metadata) = self.get_person_list_metadata(list)? { + metadata.last_edit_time = now; + self.set_person_list_metadata(list, &metadata, Some(txn))?; + } Ok(()) }; diff --git a/gossip-lib/src/storage/types/person_list1.rs b/gossip-lib/src/storage/types/person_list1.rs index 981299515..c3b7c32a7 100644 --- a/gossip-lib/src/storage/types/person_list1.rs +++ b/gossip-lib/src/storage/types/person_list1.rs @@ -1,11 +1,9 @@ -use crate::error::{Error, ErrorKind}; use crate::globals::GLOBALS; -use heed::RwTxn; -use nostr_types::{EventKind, Unixtime}; +use nostr_types::EventKind; use speedy::{Readable, Writable}; /// Lists people can be added to -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, Readable, Writable)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Readable, Writable)] #[repr(u8)] pub enum PersonList1 { Muted = 0, @@ -35,138 +33,14 @@ impl PersonList1 { } pub fn from_number(number: u8) -> Option { - let map = GLOBALS.storage.read_setting_custom_person_list_map(); - for k in map.keys() { - if *k == number { - return Some(Self::from_u8(number)); + let list = Self::from_u8(number); + if matches!(list, PersonList1::Custom(_)) { + match GLOBALS.storage.get_person_list_metadata(list) { + Ok(Some(_)) => Some(list), + _ => None, } - } - None - } - - /// Translate a name (d-tag) to a PersonList1 - pub fn from_name(name: &str) -> Option { - let map = GLOBALS.storage.read_setting_custom_person_list_map(); - for (k, v) in map.iter() { - if v == name { - return Some(Self::from_u8(*k)); - } - } - None - } - - /// All Allocated PersonList1s - pub fn all_lists() -> Vec<(PersonList1, String)> { - let mut output: Vec<(PersonList1, String)> = vec![]; - let map = GLOBALS.storage.read_setting_custom_person_list_map(); - for (k, v) in map.iter() { - match k { - 0 => output.push((PersonList1::Muted, v.clone())), - 1 => output.push((PersonList1::Followed, v.clone())), - _ => output.push((PersonList1::Custom(*k), v.clone())), - } - } - output - } - - /// Allocate a new PersonList1 with the given name - pub fn allocate<'a>(name: &str, txn: Option<&mut RwTxn<'a>>) -> Result { - // Do not allocate for well-known names - if name == "Followed" { - return Ok(PersonList1::Followed); - } else if name == "Muted" { - return Ok(PersonList1::Muted); - } - - let mut map = GLOBALS.storage.read_setting_custom_person_list_map(); - - // Check if it already exists to prevent duplicates - for (k, v) in map.iter() { - if v == name { - return Ok(PersonList1::Custom(*k)); - } - } - - // Find a slot and allocate - for i in 2..255 { - if map.contains_key(&i) { - continue; - } - map.insert(i, name.to_owned()); - - let list = PersonList1::Custom(i); - - let f = |txn: &mut RwTxn<'a>| -> Result { - // Now (creation) is when it was last edited - let now = Unixtime::now().unwrap(); - GLOBALS - .storage - .set_person_list_last_edit_time(list, now.0, Some(txn))?; - - GLOBALS - .storage - .write_setting_custom_person_list_map(&map, Some(txn))?; - - Ok(PersonList1::Custom(i)) - }; - - return match txn { - Some(txn) => f(txn), - None => { - let mut txn = GLOBALS.storage.get_write_txn()?; - let output = f(&mut txn)?; - txn.commit()?; - Ok(output) - } - }; - } - - Err(ErrorKind::NoSlotsRemaining.into()) - } - - /// Deallocate this PersonList1 - pub fn deallocate(&self, txn: Option<&mut RwTxn<'_>>) -> Result<(), Error> { - if !GLOBALS.storage.get_people_in_list(*self)?.is_empty() { - Err(ErrorKind::ListIsNotEmpty.into()) - } else { - if let PersonList1::Custom(i) = self { - let mut map = GLOBALS.storage.read_setting_custom_person_list_map(); - map.remove(i); - GLOBALS - .storage - .write_setting_custom_person_list_map(&map, txn)?; - Ok(()) - } else { - Err(ErrorKind::ListIsWellKnown.into()) - } - } - } - - pub fn rename(&self, name: &str, txn: Option<&mut RwTxn<'_>>) -> Result<(), Error> { - if let PersonList1::Custom(i) = self { - let mut map = GLOBALS.storage.read_setting_custom_person_list_map(); - map.insert(*i, name.to_owned()); - GLOBALS - .storage - .write_setting_custom_person_list_map(&map, txn)?; - Ok(()) } else { - Err(ErrorKind::ListIsWellKnown.into()) - } - } - - /// Get the name (d-tag) of this PersonList1 - pub fn name(&self) -> String { - match *self { - PersonList1::Muted => "Muted".to_string(), - PersonList1::Followed => "Followed".to_string(), - PersonList1::Custom(u) => { - let map = GLOBALS.storage.read_setting_custom_person_list_map(); - match map.get(&u) { - Some(name) => name.to_owned(), - None => "Unallocated".to_owned(), - } - } + Some(list) } } diff --git a/gossip-lib/src/storage/types/person_list_metadata1.rs b/gossip-lib/src/storage/types/person_list_metadata1.rs index 28e1ad4a4..69ae99e4c 100644 --- a/gossip-lib/src/storage/types/person_list_metadata1.rs +++ b/gossip-lib/src/storage/types/person_list_metadata1.rs @@ -17,7 +17,7 @@ impl Default for PersonListMetadata1 { dtag: "".to_owned(), title: "".to_owned(), last_edit_time: Unixtime::now().unwrap(), - event_created_at: Unixtime::now().unwrap(), + event_created_at: Unixtime(0), event_public_len: 0, event_private_len: None, } From 2503d46bc842c16314b289ac15d82b4774f542cd Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 12:38:34 +1300 Subject: [PATCH 63/81] Fix: write-transaction deadlock --- gossip-lib/src/process.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 8ad01650b..dc0c33051 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -841,8 +841,6 @@ fn update_or_allocate_person_list_from_event( event: &Event, pubkey: PublicKey, ) -> Result<(PersonList, PersonListMetadata), Error> { - let mut txn = GLOBALS.storage.get_write_txn()?; - // Determine PersonList and fetch Metadata let (list, mut metadata) = crate::people::fetch_current_personlist_matching_event(event)?; @@ -881,9 +879,7 @@ fn update_or_allocate_person_list_from_event( // Save metadata GLOBALS .storage - .set_person_list_metadata(list, &metadata, Some(&mut txn))?; - - txn.commit()?; + .set_person_list_metadata(list, &metadata, None)?; Ok((list, metadata)) } From b6e19360b972e42e5244c8b71cc8e4181d2e8bed Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 13:21:37 +1300 Subject: [PATCH 64/81] Fix for list titles imported from events being blank --- gossip-lib/src/process.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index dc0c33051..25378c220 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -874,6 +874,11 @@ fn update_or_allocate_person_list_from_event( if let Some(title) = event.title() { metadata.title = title.to_owned(); } + + // If title is empty, use the d-tag + if metadata.title.is_empty() && !metadata.dtag.is_empty() { + metadata.title = metadata.dtag.clone(); + } } // Save metadata From 3e276803aa933291921727cc584490b890c07e51 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 13:39:45 +1300 Subject: [PATCH 65/81] redo start_long_lived_subscriptions after the wizard completes so people don't need to restart --- gossip-bin/src/ui/wizard/follow_people.rs | 11 +- gossip-bin/src/ui/wizard/mod.rs | 16 ++- gossip-lib/src/comms.rs | 3 + gossip-lib/src/overlord/mod.rs | 121 ++++++++++++---------- gossip.log.txt | 19 ++++ 5 files changed, 104 insertions(+), 66 deletions(-) create mode 100644 gossip.log.txt diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index eb2acb59a..e02f61593 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -3,7 +3,7 @@ use crate::ui::{GossipUi, Page}; use eframe::egui; use egui::{Context, RichText, Ui}; use gossip_lib::comms::ToOverlordMessage; -use gossip_lib::{FeedKind, Person, PersonList, GLOBALS}; +use gossip_lib::{Person, PersonList, GLOBALS}; use gossip_relay_picker::Direction; use nostr_types::{Profile, PublicKey}; @@ -153,8 +153,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr .to_overlord .send(ToOverlordMessage::PushPersonList(PersonList::Followed)); - let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); - app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); + super::complete_wizard(app); } ui.add_space(20.0); @@ -163,16 +162,14 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr label = label.color(app.theme.accent_color()); } if ui.button(label).clicked() { - let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); - app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); + super::complete_wizard(app); } } else { ui.add_space(20.0); let mut label = RichText::new(" > Finish"); label = label.color(app.theme.accent_color()); if ui.button(label).clicked() { - let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); - app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); + super::complete_wizard(app); } } } diff --git a/gossip-bin/src/ui/wizard/mod.rs b/gossip-bin/src/ui/wizard/mod.rs index 6c27e34df..1eca83312 100644 --- a/gossip-bin/src/ui/wizard/mod.rs +++ b/gossip-bin/src/ui/wizard/mod.rs @@ -2,6 +2,7 @@ use crate::ui::{GossipUi, Page}; use eframe::egui; use egui::widgets::{Button, Slider}; use egui::{Align, Context, Layout}; +use gossip_lib::comms::ToOverlordMessage; use gossip_lib::{FeedKind, PersonList, Relay, GLOBALS}; mod follow_people; @@ -198,8 +199,7 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram ui.add_space(20.0); if wp != WizardPage::FollowPeople { if ui.button(" X Exit this Wizard").clicked() { - let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); - app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); + complete_wizard(app); } } @@ -264,3 +264,15 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, frame: &mut eframe::Fram }); }); } + +fn complete_wizard(app: &mut GossipUi) { + let _ = GLOBALS.storage.set_flag_wizard_complete(true, None); + app.page = Page::Feed(FeedKind::List(PersonList::Followed, false)); + + // Once the wizard is complete, we need to tell the overlord to re-run + // its startup stuff, because we now have configuration that matters, and + // this way people don't have to restart gossip + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::StartLongLivedSubscriptions); +} diff --git a/gossip-lib/src/comms.rs b/gossip-lib/src/comms.rs index 70fbfc7dd..7c987f335 100644 --- a/gossip-lib/src/comms.rs +++ b/gossip-lib/src/comms.rs @@ -138,6 +138,9 @@ pub enum ToOverlordMessage { /// internal SetDmChannel(DmChannel), + /// Calls [start_long_lived_subscriptions](crate::Overlord::start_long_lived_subscriptions) + StartLongLivedSubscriptions, + /// Calls [subscribe_config](crate::Overlord::subscribe_config) SubscribeConfig(RelayUrl), diff --git a/gossip-lib/src/overlord/mod.rs b/gossip-lib/src/overlord/mod.rs index b6c26f857..98c97eb1b 100644 --- a/gossip-lib/src/overlord/mod.rs +++ b/gossip-lib/src/overlord/mod.rs @@ -177,66 +177,11 @@ impl Overlord { // Start periodic tasks in people manager (after signer) crate::people::People::start(); - // FIXME - if this needs doing, it should be done dynamically as - // new people are encountered, not batch-style on startup. - // Create a person record for every person seen - // Initialize the relay picker GLOBALS.relay_picker.init().await?; - // Pick Relays and start Minions - if !GLOBALS.storage.read_setting_offline() { - self.pick_relays().await; - } - - // Separately subscribe to RelayList discovery for everyone we follow - // We just do this once at startup. Relay lists don't change that frequently. - let followed = GLOBALS.people.get_subscribed_pubkeys(); - self.subscribe_discover(followed, None).await?; - - // Separately subscribe to our outbox events on our write relays - let write_relay_urls: Vec = GLOBALS - .storage - .filter_relays(|r| r.has_usage_bits(Relay::WRITE) && r.rank != 0)? - .iter() - .map(|relay| relay.url.clone()) - .collect(); - for relay_url in write_relay_urls.iter() { - self.engage_minion( - relay_url.to_owned(), - vec![RelayJob { - reason: RelayConnectionReason::Config, - payload: ToMinionPayload { - job_id: rand::random::(), - detail: ToMinionPayloadDetail::SubscribeOutbox, - }, - }], - ) - .await?; - } - - // Separately subscribe to our mentions on our read relays - // NOTE: we also do this on all dynamically connected relays since NIP-65 is - // not in widespread usage. - let read_relay_urls: Vec = GLOBALS - .storage - .filter_relays(|r| r.has_usage_bits(Relay::READ) && r.rank != 0)? - .iter() - .map(|relay| relay.url.clone()) - .collect(); - for relay_url in read_relay_urls.iter() { - self.engage_minion( - relay_url.to_owned(), - vec![RelayJob { - reason: RelayConnectionReason::FetchMentions, - payload: ToMinionPayload { - job_id: rand::random::(), - detail: ToMinionPayloadDetail::SubscribeMentions, - }, - }], - ) - .await?; - } + // Do the startup procedures + self.start_long_lived_subscriptions().await?; 'mainloop: loop { if let Err(e) = self.loop_handler().await { @@ -684,6 +629,9 @@ impl Overlord { ToOverlordMessage::SetDmChannel(dmchannel) => { self.set_dm_channel(dmchannel).await?; } + ToOverlordMessage::StartLongLivedSubscriptions => { + self.start_long_lived_subscriptions().await?; + } ToOverlordMessage::SubscribeConfig(relay_url) => { self.subscribe_config(relay_url).await?; } @@ -2372,6 +2320,65 @@ impl Overlord { Ok(()) } + /// This is done at startup and after the wizard. + pub async fn start_long_lived_subscriptions(&mut self) -> Result<(), Error> { + // Pick Relays and start Minions + if !GLOBALS.storage.read_setting_offline() { + self.pick_relays().await; + } + + // Separately subscribe to RelayList discovery for everyone we follow + // We just do this once at startup. Relay lists don't change that frequently. + let followed = GLOBALS.people.get_subscribed_pubkeys(); + self.subscribe_discover(followed, None).await?; + + // Separately subscribe to our outbox events on our write relays + let write_relay_urls: Vec = GLOBALS + .storage + .filter_relays(|r| r.has_usage_bits(Relay::WRITE) && r.rank != 0)? + .iter() + .map(|relay| relay.url.clone()) + .collect(); + for relay_url in write_relay_urls.iter() { + self.engage_minion( + relay_url.to_owned(), + vec![RelayJob { + reason: RelayConnectionReason::Config, + payload: ToMinionPayload { + job_id: rand::random::(), + detail: ToMinionPayloadDetail::SubscribeOutbox, + }, + }], + ) + .await?; + } + + // Separately subscribe to our mentions on our read relays + // NOTE: we also do this on all dynamically connected relays since NIP-65 is + // not in widespread usage. + let read_relay_urls: Vec = GLOBALS + .storage + .filter_relays(|r| r.has_usage_bits(Relay::READ) && r.rank != 0)? + .iter() + .map(|relay| relay.url.clone()) + .collect(); + for relay_url in read_relay_urls.iter() { + self.engage_minion( + relay_url.to_owned(), + vec![RelayJob { + reason: RelayConnectionReason::FetchMentions, + payload: ToMinionPayload { + job_id: rand::random::(), + detail: ToMinionPayloadDetail::SubscribeMentions, + }, + }], + ) + .await?; + } + + Ok(()) + } + /// Subscribe to the user's configuration events from the given relay pub async fn subscribe_config(&mut self, relay_url: RelayUrl) -> Result<(), Error> { self.engage_minion( diff --git a/gossip.log.txt b/gossip.log.txt new file mode 100644 index 000000000..668fafbb9 --- /dev/null +++ b/gossip.log.txt @@ -0,0 +1,19 @@ +2023-12-07T22:56:27.257056Z  INFO gossip-lib/src/filter.rs:43: Spam filter loaded. +2023-12-07T22:56:27.288275Z  INFO /home/mike/.cargo/registry/src/index.crates.io-6f17d22bba15001f/winit-0.28.7/src/platform_impl/linux/x11/window.rs:156: Guessed window scale factor: 1.0416666666666667 +2023-12-07T22:56:27.405842Z  INFO gossip-bin/src/ui/mod.rs:478: DPI (native): 75 +2023-12-07T22:56:33.171088Z DEBUG gossip-lib/src/overlord/mod.rs:282: Picked wss://nostr-pub.wellorder.net/ covering 151 pubkeys +2023-12-07T22:56:33.171554Z DEBUG gossip-lib/src/overlord/mod.rs:282: Picked wss://nos.lol/ covering 152 pubkeys +2023-12-07T22:56:33.171745Z DEBUG gossip-lib/src/overlord/mod.rs:282: Picked wss://eden.nostr.land/ covering 1 pubkeys +2023-12-07T22:56:33.171772Z DEBUG gossip-lib/src/overlord/mod.rs:277: Done picking relays: All people accounted for. +2023-12-07T22:56:33.399922Z DEBUG gossip-lib/src/overlord/minion/mod.rs:133: wss://nostr.mikedilger.com/: Relay Information: Name="nostr-rs-relay" Description="Mike Dilger's archive relay, not for general use." Pubkey="ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49" Contact="mailto:mike@mikedilger.com" NIPS=[1, 2, 9, 11, 12, 15, 16, 20, 22, 33, 40, 42] Software="https://git.sr.ht/~gheartsfield/nostr-rs-relay" Version="0.8.9" Limitation="Relay Limitation: PaymentRequired="false"" id=""wss://nostr.mikedilger.com"" +2023-12-07T22:56:33.523633Z DEBUG gossip-lib/src/overlord/minion/mod.rs:228: wss://nostr.mikedilger.com/: Connected +2023-12-07T22:56:33.525790Z DEBUG gossip-lib/src/overlord/minion/mod.rs:909: NEW SUBSCRIPTION on wss://nostr.mikedilger.com/ handle=config_feed, id=0 +2023-12-07T22:56:33.602171Z DEBUG gossip-lib/src/process.rs:164: wss://nostr.mikedilger.com/: New Event: config_feed ContactList @1699940805 +2023-12-07T22:56:34.067527Z DEBUG gossip-lib/src/overlord/minion/mod.rs:133: wss://nostr.wine/: Relay Information: Name="nostr.wine" Description="A paid nostr relay for wine enthusiasts and everyone else." Pubkey="4918eb332a41b71ba9a74b1dc64276cfff592e55107b93baae38af3520e55975" Contact="wino@nostr.wine" NIPS=[1, 2, 4, 9, 11, 12, 15, 16, 20, 22, 28, 33, 40, 42] Software="https://nostr.wine" Version="0.3.1" Limitation="Relay Limitation: MaxMessageLength="131072" MaxSubscriptions="50" MaxLimit="1000" MaxSubidLength="71" MaxEventTags="2000" MinPowDifficulty="0" AuthRequired="false" PaymentRequired="true"" PaymentsUrl=https://nostr.wine/invoices Fees=Relay Fees: Admission=[Fee=[18888000 msats Kinds="[]"] ],Subscription=[],Publication=[] icon=""https://image.nostr.build/dda8ffb9d8d87d34c7d0b0b9cf54a0466bfab69939b0c9a2bd430bac1540cadf.jpg"" +2023-12-07T22:56:34.077020Z DEBUG gossip-lib/src/process.rs:164: wss://nostr.mikedilger.com/: New Event: config_feed Metadata @1700863736 +2023-12-07T22:56:34.091929Z DEBUG gossip-lib/src/process.rs:164: wss://nostr.mikedilger.com/: New Event: config_feed FollowSets @1701213881 +2023-12-07T22:56:34.443721Z DEBUG gossip-lib/src/overlord/minion/mod.rs:133: wss://offchain.pub/: Relay Information: Name="offchain.pub" Description="public nostr relay running strfry" Pubkey="6b1b35c6dee48851bac53a4494ca8f819503be00212dbceb899dc03acd7641db" Contact="admin@offchain.pub" NIPS=[1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40] Software="git+https://github.com/hoytech/strfry.git" Version="0.9.6" +2023-12-07T22:56:34.511799Z DEBUG gossip-lib/src/overlord/minion/mod.rs:133: wss://nos.lol/: Relay Information: Name="nos.lol" Description="Generally accepts notes, except spammy ones." Contact="unset" NIPS=[1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40] Software="git+https://github.com/hoytech/strfry.git" Version="0.9.6" +2023-12-07T22:56:34.667891Z DEBUG gossip-lib/src/overlord/minion/mod.rs:133: wss://purplepag.es/: Relay Information: Name="purplepag.es" Description="Nostr's Purple Pages" Pubkey="fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52" Contact="pablof7z.com" NIPS=[1, 2, 4, 9, 11, 12, 16, 20, 22, 28, 33, 40] Software="git+https://github.com/hoytech/strfry.git" Version="0.9.6-7-g7196547" +2023-12-07T22:56:34.691831Z DEBUG gossip-lib/src/overlord/minion/mod.rs:228: wss://nostr.wine/: Connected +DEBUG: SAVING SETTINGS seeking a write txn... From 70492bb7d1a1a769ce6b03f209f4990809f7544a Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 15:48:21 +1300 Subject: [PATCH 66/81] When a new list arrives, populate from it (only require manual if list already exists) --- gossip-lib/src/people.rs | 30 +++++++++++++++--------------- gossip-lib/src/process.rs | 13 ++++++++++++- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index d03883c02..1ae204f12 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -918,25 +918,24 @@ struct Nip05Patch { // Determine PersonList and fetches Metadata, allocating if needed. // This does NOT update that metadata from the event. +// The bool indicates if the list was freshly allocated pub(crate) fn fetch_current_personlist_matching_event( event: &Event, -) -> Result<(PersonList, PersonListMetadata), Error> { - let (list, metadata) = match event.kind { +) -> Result<(PersonList, PersonListMetadata, bool), Error> { + let (list, metadata, new) = match event.kind { EventKind::ContactList => { let list = PersonList::Followed; - let md = GLOBALS - .storage - .get_person_list_metadata(list)? - .unwrap_or_default(); - (list, md) + match GLOBALS.storage.get_person_list_metadata(list)? { + Some(md) => (list, md, false), + None => (list, Default::default(), true), + } } EventKind::MuteList => { let list = PersonList::Muted; - let md = GLOBALS - .storage - .get_person_list_metadata(list)? - .unwrap_or_default(); - (list, md) + match GLOBALS.storage.get_person_list_metadata(list)? { + Some(md) => (list, md, false), + None => (list, Default::default(), true), + } } EventKind::FollowSets => { let dtag = match event.parameter() { @@ -944,7 +943,7 @@ pub(crate) fn fetch_current_personlist_matching_event( None => return Err(ErrorKind::ListEventMissingDtag.into()), }; if let Some((found_list, metadata)) = GLOBALS.storage.find_person_list_by_dtag(&dtag)? { - (found_list, metadata) + (found_list, metadata, false) } else { // Allocate new let mut metadata: PersonListMetadata = Default::default(); @@ -952,7 +951,8 @@ pub(crate) fn fetch_current_personlist_matching_event( metadata.event_created_at = event.created_at; // This is slim metadata.. The caller will fix it. let list = GLOBALS.storage.allocate_person_list(&metadata, None)?; - (list, metadata) + + (list, metadata, true) } } _ => { @@ -961,5 +961,5 @@ pub(crate) fn fetch_current_personlist_matching_event( } }; - Ok((list, metadata)) + Ok((list, metadata, new)) } diff --git a/gossip-lib/src/process.rs b/gossip-lib/src/process.rs index 25378c220..5f6f9a458 100644 --- a/gossip-lib/src/process.rs +++ b/gossip-lib/src/process.rs @@ -842,7 +842,7 @@ fn update_or_allocate_person_list_from_event( pubkey: PublicKey, ) -> Result<(PersonList, PersonListMetadata), Error> { // Determine PersonList and fetch Metadata - let (list, mut metadata) = crate::people::fetch_current_personlist_matching_event(event)?; + let (list, mut metadata, new) = crate::people::fetch_current_personlist_matching_event(event)?; // Update metadata { @@ -886,5 +886,16 @@ fn update_or_allocate_person_list_from_event( .storage .set_person_list_metadata(list, &metadata, None)?; + if new { + // Ask the overlord to populate the list from the event, since it is + // locally new + let _ = GLOBALS + .to_overlord + .send(ToOverlordMessage::UpdatePersonList { + person_list: list, + merge: false, + }); + } + Ok((list, metadata)) } From 3faaa3ff737b6d87e3edd11b32cc6469ca3c2700 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 16:04:20 +1300 Subject: [PATCH 67/81] cargo fmt && clippy --- gossip-bin/src/commands.rs | 8 +++++--- gossip-bin/src/ui/people/lists.rs | 8 +++++--- gossip-lib/src/people.rs | 9 ++++++--- gossip-lib/src/storage/migrations/m19.rs | 18 ++++++++++-------- .../src/storage/person_lists_metadata1.rs | 4 ++-- 5 files changed, 28 insertions(+), 19 deletions(-) diff --git a/gossip-bin/src/commands.rs b/gossip-bin/src/commands.rs index 7036c34e9..407aa1aae 100644 --- a/gossip-bin/src/commands.rs +++ b/gossip-bin/src/commands.rs @@ -269,9 +269,11 @@ pub fn add_person_list(cmd: Command, mut args: env::Args) -> Result<(), Error> { None => return cmd.usage("Missing listname parameter".to_string()), }; - let mut metadata: PersonListMetadata = Default::default(); - metadata.dtag = listname.clone(); - metadata.title = listname.clone(); + let metadata = PersonListMetadata { + dtag: listname.clone(), + title: listname.clone(), + ..Default::default() + }; let _list = GLOBALS.storage.allocate_person_list(&metadata, None)?; Ok(()) diff --git a/gossip-bin/src/ui/people/lists.rs b/gossip-bin/src/ui/people/lists.rs index ba3e41f15..3006dce0d 100644 --- a/gossip-bin/src/ui/people/lists.rs +++ b/gossip-bin/src/ui/people/lists.rs @@ -115,9 +115,11 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.with_layout(egui::Layout::right_to_left(egui::Align::default()), |ui| { if ui.button("Create").clicked() { if !app.new_list_name.is_empty() { - let mut metadata: PersonListMetadata = Default::default(); - metadata.dtag = app.new_list_name.to_owned(); - metadata.title = app.new_list_name.to_owned(); + let metadata = PersonListMetadata { + dtag: app.new_list_name.to_owned(), + title: app.new_list_name.to_owned(), + ..Default::default() + }; if let Err(e) = GLOBALS.storage.allocate_person_list(&metadata, None) diff --git a/gossip-lib/src/people.rs b/gossip-lib/src/people.rs index 1ae204f12..808b83cf4 100644 --- a/gossip-lib/src/people.rs +++ b/gossip-lib/src/people.rs @@ -946,9 +946,12 @@ pub(crate) fn fetch_current_personlist_matching_event( (found_list, metadata, false) } else { // Allocate new - let mut metadata: PersonListMetadata = Default::default(); - metadata.dtag = dtag; - metadata.event_created_at = event.created_at; + let metadata = PersonListMetadata { + dtag, + event_created_at: event.created_at, + ..Default::default() + }; + // This is slim metadata.. The caller will fix it. let list = GLOBALS.storage.allocate_person_list(&metadata, None)?; diff --git a/gossip-lib/src/storage/migrations/m19.rs b/gossip-lib/src/storage/migrations/m19.rs index d9f3e4ae1..0cdb90cfa 100644 --- a/gossip-lib/src/storage/migrations/m19.rs +++ b/gossip-lib/src/storage/migrations/m19.rs @@ -28,7 +28,7 @@ impl Storage { fn m19_populate_person_list_metadata<'a>(&'a self, txn: &mut RwTxn<'a>) -> Result<(), Error> { // read custom_person_list_map setting let name_map: BTreeMap = { - let maybe_map = match self.general.get(&txn, b"custom_person_list_map") { + let maybe_map = match self.general.get(txn, b"custom_person_list_map") { Err(_) => None, Ok(None) => None, Ok(Some(bytes)) => match >::read_from_buffer(bytes) { @@ -51,13 +51,15 @@ impl Storage { name_map.keys().map(|k| PersonList1::from_u8(*k)).collect(); for list in lists.drain(..) { - let mut metadata: PersonListMetadata1 = Default::default(); - metadata.last_edit_time = Unixtime( - last_edit_times - .get(&list) - .copied() - .unwrap_or(Unixtime::now().unwrap().0), - ); + let mut metadata = PersonListMetadata1 { + last_edit_time: Unixtime( + last_edit_times + .get(&list) + .copied() + .unwrap_or(Unixtime::now().unwrap().0), + ), + ..Default::default() + }; if list == PersonList1::Muted { metadata.dtag = "muted".to_string(); metadata.title = "Muted".to_string(); diff --git a/gossip-lib/src/storage/person_lists_metadata1.rs b/gossip-lib/src/storage/person_lists_metadata1.rs index c7bd65a52..6f360b201 100644 --- a/gossip-lib/src/storage/person_lists_metadata1.rs +++ b/gossip-lib/src/storage/person_lists_metadata1.rs @@ -117,7 +117,7 @@ impl Storage { let (key, val) = result?; let list = PersonList1::read_from_buffer(key)?; let metadata = PersonListMetadata1::read_from_buffer(val)?; - if &metadata.dtag == dtag { + if metadata.dtag == dtag { return Ok(Some((list, metadata))); } } @@ -148,7 +148,7 @@ impl Storage { for i in 2..=255 { let key: Vec = PersonList1::Custom(i).write_to_vec()?; - if self.db_person_lists_metadata1()?.get(&txn, &key)?.is_none() { + if self.db_person_lists_metadata1()?.get(txn, &key)?.is_none() { slot = i; break; } From fd95eb75f5454f717c9ea2a0fe557449368764a2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 18:12:40 +1300 Subject: [PATCH 68/81] Fixes to merge --- gossip-bin/src/ui/people/list.rs | 36 ++++++++++++++------------------ 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 6d1db1ab5..6e14ef9fb 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -82,10 +82,16 @@ pub(super) fn update( let enabled = !app.people_list.entering_follow_someone_on_list && !app.people_list.clear_list_needs_confirm; + let metadata = GLOBALS + .storage + .get_person_list_metadata(list) + .unwrap_or_default() + .unwrap_or_default(); + // render page widgets::page_header( ui, - format!("{} ({})", list.name(), app.people_list.cache_people.len()), + format!("{} ({})", metadata.title, app.people_list.cache_people.len()), |ui| { ui.add_enabled_ui(enabled, |ui| { let min_size = vec2(50.0, 20.0); @@ -497,15 +503,14 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { people }; - let latest_event_data = GLOBALS - .people - .latest_person_list_event_data - .get(&list) - .map(|v| v.value().clone()) + let metadata = GLOBALS + .storage + .get_person_list_metadata(list) + .unwrap_or_default() .unwrap_or_default(); let mut asof = "time unknown".to_owned(); - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(latest_event_data.when.0) { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(metadata.event_created_at.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { @@ -513,9 +518,9 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { } } - app.people_list.cache_remote_tag = if latest_event_data.when.0 == 0 { + app.people_list.cache_remote_tag = if metadata.event_created_at.0 == 0 { "REMOTE: not found on Active Relays".to_owned() - } else if let Some(private_len) = latest_event_data.private_len { + } else if let Some(private_len) = metadata.event_private_len { format!( "REMOTE: {} (public_len={} private_len={})", asof, metadata.event_public_len, private_len @@ -527,18 +532,9 @@ fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { ) }; - let last_list_edit = match GLOBALS.storage.get_person_list_last_edit_time(list) { - Ok(Some(date)) => date, - Ok(None) => 0, - Err(e) => { - tracing::error!("{}", e); - 0 - } - }; - let mut ledit = "time unknown".to_owned(); - if last_list_edit > 0 { - if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_list_edit) { + if metadata.last_edit_time.0 > 0 { + if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(metadata.last_edit_time.0) { if let Ok(formatted) = stamp.format(time::macros::format_description!( "[year]-[month repr:short]-[day] ([weekday repr:short]) [hour]:[minute]" )) { From 94c1fb297e2b224385b134795a3d75084d48a32a Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 18:13:44 +1300 Subject: [PATCH 69/81] cargo fmt and clippy --- gossip-bin/src/ui/people/list.rs | 8 +++++-- gossip-bin/src/ui/relays/mod.rs | 26 +++++++++++------------ gossip-bin/src/ui/widgets/mod.rs | 4 +--- gossip-bin/src/ui/widgets/more_menu.rs | 12 ++++++----- gossip-bin/src/ui/wizard/follow_people.rs | 3 +-- 5 files changed, 28 insertions(+), 25 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 6e14ef9fb..503adf49c 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -91,12 +91,16 @@ pub(super) fn update( // render page widgets::page_header( ui, - format!("{} ({})", metadata.title, app.people_list.cache_people.len()), + format!( + "{} ({})", + metadata.title, + app.people_list.cache_people.len() + ), |ui| { ui.add_enabled_ui(enabled, |ui| { let min_size = vec2(50.0, 20.0); - widgets::MoreMenu::new(&app).with_min_size(min_size).show( + widgets::MoreMenu::new(app).with_min_size(min_size).show( ui, &mut app.people_list.configure_list_menu_active, |ui| { diff --git a/gossip-bin/src/ui/relays/mod.rs b/gossip-bin/src/ui/relays/mod.rs index 83a3432a2..25fca53e0 100644 --- a/gossip-bin/src/ui/relays/mod.rs +++ b/gossip-bin/src/ui/relays/mod.rs @@ -454,22 +454,22 @@ pub(super) fn configure_list_btn(app: &mut GossipUi, ui: &mut Ui) { widgets::MoreMenu::new(app) .with_min_size(min_size) .with_hover_text("Configure List View".to_owned()) - .show(ui,&mut app.relays.configure_list_menu_active, |ui|{ - let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); + .show(ui, &mut app.relays.configure_list_menu_active, |ui| { + let size = ui.spacing().interact_size.y * egui::vec2(1.6, 0.8); - // since we are displaying over an accent color background, load that style - app.theme.on_accent_style(ui.style_mut()); + // since we are displaying over an accent color background, load that style + app.theme.on_accent_style(ui.style_mut()); - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); - ui.label("Show details"); - }); - ui.add_space(8.0); - ui.horizontal(|ui| { - crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); - ui.label("Show hidden relays"); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_details, size); + ui.label("Show details"); + }); + ui.add_space(8.0); + ui.horizontal(|ui| { + crate::ui::components::switch_with_size(ui, &mut app.relays.show_hidden, size); + ui.label("Show hidden relays"); + }); }); - }); }); } diff --git a/gossip-bin/src/ui/widgets/mod.rs b/gossip-bin/src/ui/widgets/mod.rs index 766328afa..2da6c88da 100644 --- a/gossip-bin/src/ui/widgets/mod.rs +++ b/gossip-bin/src/ui/widgets/mod.rs @@ -10,9 +10,7 @@ pub use copy_button::{CopyButton, COPY_SYMBOL_SIZE}; mod nav_item; use egui_winit::egui::text_edit::TextEditOutput; -use egui_winit::egui::{ - self, vec2, FontSelection, Rect, Sense, TextEdit, Ui, WidgetText, -}; +use egui_winit::egui::{self, vec2, FontSelection, Rect, Sense, TextEdit, Ui, WidgetText}; pub use nav_item::NavItem; mod relay_entry; diff --git a/gossip-bin/src/ui/widgets/more_menu.rs b/gossip-bin/src/ui/widgets/more_menu.rs index 3e665a386..46aa70b94 100644 --- a/gossip-bin/src/ui/widgets/more_menu.rs +++ b/gossip-bin/src/ui/widgets/more_menu.rs @@ -1,5 +1,5 @@ use eframe::epaint::PathShape; -use egui_winit::egui::{Ui, self, vec2, Rect, Id, Vec2, TextureHandle, Color32}; +use egui_winit::egui::{self, vec2, Color32, Id, Rect, TextureHandle, Ui, Vec2}; use crate::ui::GossipUi; @@ -40,7 +40,7 @@ impl MoreMenu { self } - pub fn show(&self, ui: &mut Ui, active: &mut bool, content: impl FnOnce(&mut Ui) ) { + pub fn show(&self, ui: &mut Ui, active: &mut bool, content: impl FnOnce(&mut Ui)) { let (response, painter) = ui.allocate_painter(vec2(20.0, 20.0), egui::Sense::click()); let response = response.on_hover_cursor(egui::CursorIcon::PointingHand); let response = if let Some(text) = &self.hover_text { @@ -71,7 +71,11 @@ impl MoreMenu { } let button_center_bottom = response.rect.center_bottom(); - let seen_on_popup_position = button_center_bottom + vec2(-(self.min_size.x - 2.0*super::DROPDOWN_DISTANCE), super::DROPDOWN_DISTANCE); + let seen_on_popup_position = button_center_bottom + + vec2( + -(self.min_size.x - 2.0 * super::DROPDOWN_DISTANCE), + super::DROPDOWN_DISTANCE, + ); let mut frame = egui::Frame::popup(ui.style()); let area = egui::Area::new(self.id) @@ -113,5 +117,3 @@ impl MoreMenu { } } } - - diff --git a/gossip-bin/src/ui/wizard/follow_people.rs b/gossip-bin/src/ui/wizard/follow_people.rs index 9ebb369b1..5371f039f 100644 --- a/gossip-bin/src/ui/wizard/follow_people.rs +++ b/gossip-bin/src/ui/wizard/follow_people.rs @@ -100,8 +100,7 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr PersonList::Followed, true, )); - } else if let Ok(pubkey) = - PublicKey::try_from_hex_string(app.add_contact.trim(), true) + } else if let Ok(pubkey) = PublicKey::try_from_hex_string(app.add_contact.trim(), true) { let _ = GLOBALS.to_overlord.send(ToOverlordMessage::FollowPubkey( pubkey, From 597e4f91ef0f63d47b30c7c84abcf99796820032 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Fri, 8 Dec 2023 18:22:26 +1300 Subject: [PATCH 70/81] Unversion PersonList1 in bin --- gossip-bin/src/ui/people/list.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gossip-bin/src/ui/people/list.rs b/gossip-bin/src/ui/people/list.rs index 503adf49c..e920e3564 100644 --- a/gossip-bin/src/ui/people/list.rs +++ b/gossip-bin/src/ui/people/list.rs @@ -282,7 +282,7 @@ pub(super) fn update( }); } -fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: gossip_lib::PersonList1) { +fn render_add_contact_popup(ui: &mut Ui, app: &mut GossipUi, list: PersonList) { const DLG_SIZE: Vec2 = vec2(400.0, 240.0); let ret = crate::ui::widgets::modal_popup(ui, DLG_SIZE, |ui| { let enter_key; @@ -487,7 +487,7 @@ fn mark_refresh(app: &mut GossipUi) { app.people_list.cache_next_refresh = Instant::now(); } -fn refresh_list_data(app: &mut GossipUi, list: gossip_lib::PersonList1) { +fn refresh_list_data(app: &mut GossipUi, list: PersonList) { // prepare data app.people_list.cache_people = { let members = GLOBALS.storage.get_people_in_list(list).unwrap_or_default(); From b44b3fb6bc32aa1a5652d7e70018cd5291320777 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 9 Dec 2023 08:13:15 +1300 Subject: [PATCH 71/81] storage: migration 20 --- gossip-lib/src/storage/migrations/m20.rs | 64 ++++++++++++++++++++++++ gossip-lib/src/storage/migrations/mod.rs | 5 +- 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 gossip-lib/src/storage/migrations/m20.rs diff --git a/gossip-lib/src/storage/migrations/m20.rs b/gossip-lib/src/storage/migrations/m20.rs new file mode 100644 index 000000000..177cb9975 --- /dev/null +++ b/gossip-lib/src/storage/migrations/m20.rs @@ -0,0 +1,64 @@ +use crate::error::Error; +use crate::globals::GLOBALS; +use crate::storage::types::PersonList1; +use crate::storage::Storage; +use heed::RwTxn; +use nostr_types::Tag; + +impl Storage { + pub(super) fn m20_trigger(&self) -> Result<(), Error> { + Ok(()) + } + + pub(super) fn m20_migrate<'a>( + &'a self, + prefix: &str, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { + // Info message + tracing::info!("{prefix}: ..."); + + // Migrate + self.m20_initialize_person_list_event_metadata(txn)?; + + Ok(()) + } + + fn m20_initialize_person_list_event_metadata<'a>( + &'a self, + txn: &mut RwTxn<'a>, + ) -> Result<(), Error> { + // Get public key, or give up + let pk = match self.read_setting_public_key() { + Some(pk) => pk, + None => return Ok(()), + }; + + for (list, mut metadata) in self.get_all_person_list_metadata1()? { + if let Ok(Some(event)) = + self.get_replaceable_event(list.event_kind(), pk, &metadata.dtag) + { + metadata.event_created_at = event.created_at; + metadata.event_public_len = event + .tags + .iter() + .filter(|t| matches!(t, Tag::Pubkey { .. })) + .count(); + metadata.event_private_len = { + let mut private_len: Option = None; + if !matches!(list, PersonList1::Followed) && GLOBALS.signer.is_ready() { + if let Ok(bytes) = GLOBALS.signer.decrypt_nip04(&pk, &event.content) { + if let Ok(vectags) = serde_json::from_slice::>(&bytes) { + private_len = Some(vectags.len()); + } + } + } + private_len + }; + self.set_person_list_metadata1(list, &metadata, Some(txn))?; + } + } + + Ok(()) + } +} diff --git a/gossip-lib/src/storage/migrations/mod.rs b/gossip-lib/src/storage/migrations/mod.rs index bccf17311..03d411787 100644 --- a/gossip-lib/src/storage/migrations/mod.rs +++ b/gossip-lib/src/storage/migrations/mod.rs @@ -12,6 +12,7 @@ mod m17; mod m18; mod m19; mod m2; +mod m20; mod m3; mod m4; mod m5; @@ -25,7 +26,7 @@ use crate::error::{Error, ErrorKind}; use heed::RwTxn; impl Storage { - const MAX_MIGRATION_LEVEL: u32 = 19; + const MAX_MIGRATION_LEVEL: u32 = 20; /// Initialize the database from empty pub(super) fn init_from_empty(&self) -> Result<(), Error> { @@ -81,6 +82,7 @@ impl Storage { 17 => self.m17_trigger()?, 18 => self.m18_trigger()?, 19 => self.m19_trigger()?, + 20 => self.m20_trigger()?, _ => panic!("Unreachable migration level"), } @@ -109,6 +111,7 @@ impl Storage { 17 => self.m17_migrate(&prefix, txn)?, 18 => self.m18_migrate(&prefix, txn)?, 19 => self.m19_migrate(&prefix, txn)?, + 20 => self.m20_migrate(&prefix, txn)?, _ => panic!("Unreachable migration level"), }; From 2b96ddf27a6f243eb0d30ef3a16b0db241986ca3 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Sat, 9 Dec 2023 08:29:27 +1300 Subject: [PATCH 72/81] Remove a temporary comment --- gossip-lib/src/storage/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/gossip-lib/src/storage/mod.rs b/gossip-lib/src/storage/mod.rs index a0cbd4dd5..d5436fc6f 100644 --- a/gossip-lib/src/storage/mod.rs +++ b/gossip-lib/src/storage/mod.rs @@ -658,7 +658,6 @@ impl Storage { self.deallocate_person_list1(list, rw_txn) } - // GINA add more person_list functions, higher level now pub fn rename_person_list<'a>( &'a self, list: PersonList, From 406dd37cb2bf3283913c915d94cc044fc45ab4ba Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 07:58:04 +1300 Subject: [PATCH 73/81] minion: move authenticate() to it's own function --- gossip-lib/src/error.rs | 5 +++ .../src/overlord/minion/handle_websocket.rs | 36 +--------------- gossip-lib/src/overlord/minion/mod.rs | 41 ++++++++++++++++++- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/gossip-lib/src/error.rs b/gossip-lib/src/error.rs index 79f330c77..5ab37d252 100644 --- a/gossip-lib/src/error.rs +++ b/gossip-lib/src/error.rs @@ -1,5 +1,6 @@ use crate::comms::{ToMinionMessage, ToOverlordMessage}; use crate::people::PersonList; +use nostr_types::RelayUrl; /// Error kinds that can occur in gossip-lib #[derive(Debug)] @@ -20,6 +21,7 @@ pub enum ErrorKind { Nostr(nostr_types::Error), NoPublicKey, NoPrivateKey, + NoPrivateKeyForAuth(RelayUrl), NoRelay, NotAPersonListEvent, NoSlotsRemaining, @@ -96,6 +98,9 @@ impl std::fmt::Display for Error { Nostr(e) => write!(f, "Nostr: {e}"), NoPublicKey => write!(f, "No public key identity available."), NoPrivateKey => write!(f, "No private key available."), + NoPrivateKeyForAuth(u) => { + write!(f, "No private key available, cannot AUTH to relay: {}", u) + } NoRelay => write!(f, "Could not determine a relay to use."), NotAPersonListEvent => write!(f, "Not a person list event"), NoSlotsRemaining => write!(f, "No custom list slots remaining."), diff --git a/gossip-lib/src/overlord/minion/handle_websocket.rs b/gossip-lib/src/overlord/minion/handle_websocket.rs index 774ebe5a5..b259db88d 100644 --- a/gossip-lib/src/overlord/minion/handle_websocket.rs +++ b/gossip-lib/src/overlord/minion/handle_websocket.rs @@ -2,9 +2,7 @@ use super::Minion; use crate::comms::ToOverlordMessage; use crate::error::Error; use crate::globals::GLOBALS; -use futures_util::sink::SinkExt; -use nostr_types::{ClientMessage, EventKind, PreEvent, RelayMessage, Tag, Unixtime}; -use tungstenite::protocol::Message as WsMessage; +use nostr_types::{RelayMessage, Unixtime}; impl Minion { pub(super) async fn handle_nostr_message(&mut self, ws_message: String) -> Result<(), Error> { @@ -159,37 +157,7 @@ impl Minion { } } RelayMessage::Auth(challenge) => { - if !GLOBALS.signer.is_ready() { - tracing::warn!("AUTH required on {}, but we have no key", &self.url); - return Ok(()); - } - let pubkey = match GLOBALS.signer.public_key() { - Some(pk) => pk, - None => return Ok(()), - }; - let pre_event = PreEvent { - pubkey, - created_at: Unixtime::now().unwrap(), - kind: EventKind::Auth, - tags: vec![ - Tag::Other { - tag: "relay".to_string(), - data: vec![self.url.as_str().to_owned()], - }, - Tag::Other { - tag: "challenge".to_string(), - data: vec![challenge], - }, - ], - content: "".to_string(), - }; - let event = GLOBALS.signer.sign_preevent(pre_event, None, None)?; - let msg = ClientMessage::Auth(Box::new(event)); - let wire = serde_json::to_string(&msg)?; - self.last_message_sent = wire.clone(); - let ws_stream = self.stream.as_mut().unwrap(); - ws_stream.send(WsMessage::Text(wire)).await?; - tracing::info!("Authenticated to {}", &self.url); + let _ = self.authenticate(challenge).await?; } RelayMessage::Closed(subid, message) => { let handle = self diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index a17d84e49..3b0111bd5 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -16,8 +16,8 @@ use http::uri::{Parts, Scheme}; use http::Uri; use mime::Mime; use nostr_types::{ - ClientMessage, EventAddr, EventKind, Filter, Id, IdHex, PublicKey, PublicKeyHex, - RelayInformationDocument, RelayUrl, Unixtime, + ClientMessage, EventAddr, EventKind, Filter, Id, IdHex, PreEvent, PublicKey, PublicKeyHex, + RelayInformationDocument, RelayUrl, Tag, Unixtime, }; use reqwest::Response; use std::borrow::Cow; @@ -955,6 +955,43 @@ impl Minion { Ok(()) } + async fn authenticate(&mut self, challenge: String) -> Result { + if !GLOBALS.signer.is_ready() { + return Err(ErrorKind::NoPrivateKeyForAuth(self.url.clone()).into()); + } + let pubkey = match GLOBALS.signer.public_key() { + Some(pk) => pk, + None => { + return Err(ErrorKind::NoPrivateKeyForAuth(self.url.clone()).into()); + } + }; + let pre_event = PreEvent { + pubkey, + created_at: Unixtime::now().unwrap(), + kind: EventKind::Auth, + tags: vec![ + Tag::Other { + tag: "relay".to_string(), + data: vec![self.url.as_str().to_owned()], + }, + Tag::Other { + tag: "challenge".to_string(), + data: vec![challenge], + }, + ], + content: "".to_string(), + }; + let event = GLOBALS.signer.sign_preevent(pre_event, None, None)?; + let id = event.id; + let msg = ClientMessage::Auth(Box::new(event)); + let wire = serde_json::to_string(&msg)?; + self.last_message_sent = wire.clone(); + let ws_stream = self.stream.as_mut().unwrap(); + ws_stream.send(WsMessage::Text(wire)).await?; + tracing::info!("Authenticated to {}", &self.url); + Ok(id) + } + // This replictes reqwest Response text_with_charset to handle decoding // whatever charset they used into UTF-8, as well as counting the bytes. async fn text_with_charset( From 386972d658aa60f1b7e8dffbe54bcd5202822f09 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 07:59:20 +1300 Subject: [PATCH 74/81] minions: downgrade most log error messages to warn --- gossip-lib/src/overlord/minion/handle_websocket.rs | 2 +- gossip-lib/src/overlord/minion/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gossip-lib/src/overlord/minion/handle_websocket.rs b/gossip-lib/src/overlord/minion/handle_websocket.rs index b259db88d..b4cf225f8 100644 --- a/gossip-lib/src/overlord/minion/handle_websocket.rs +++ b/gossip-lib/src/overlord/minion/handle_websocket.rs @@ -12,7 +12,7 @@ impl Minion { let relay_message: RelayMessage = match serde_json::from_str(&ws_message) { Ok(rm) => rm, Err(e) => { - tracing::error!( + tracing::warn!( "RELAY MESSAGE NOT DESERIALIZING ({}) ({}): starts with \"{}\"", self.url, e, diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 3b0111bd5..c546dd13e 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -248,7 +248,7 @@ impl Minion { } } Err(e) => { - tracing::error!("{}", e); + tracing::warn!("{}", e); if let ErrorKind::Websocket(_) = e.kind { return Err(e); @@ -263,7 +263,7 @@ impl Minion { let ws_stream = self.stream.as_mut().unwrap(); if !ws_stream.is_terminated() { if let Err(e) = ws_stream.send(WsMessage::Close(None)).await { - tracing::error!("websocket close error: {}", e); + tracing::warn!("websocket close error: {}", e); return Err(e.into()); } } @@ -873,7 +873,7 @@ impl Minion { job_id: u64, ) -> Result<(), Error> { if filters.is_empty() { - tracing::error!("EMPTY FILTERS handle={} jobid={}", handle, job_id); + tracing::warn!("EMPTY FILTERS handle={} jobid={}", handle, job_id); return Ok(()); } From 27cdd604920aeadacc113ebb8896530cf608bbc2 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 08:02:31 +1300 Subject: [PATCH 75/81] minions: split subscribe() into subscribe() and send_subscription() --- gossip-lib/src/overlord/minion/mod.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index c546dd13e..dc91c8a89 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -914,7 +914,15 @@ impl Minion { ); } - let req_message = self.subscription_map.get(handle).unwrap().req_message(); + self.send_subscription(handle).await?; + Ok(()) + } + + async fn send_subscription(&mut self, handle: &str) -> Result<(), Error> { + let req_message = match self.subscription_map.get(handle) { + Some(sub) => sub.req_message(), + None => return Ok(()), // Not much we can do. It is not there. + }; let wire = serde_json::to_string(&req_message)?; let websocket_stream = self.stream.as_mut().unwrap(); tracing::trace!("{}: Sending {}", &self.url, &wire); From 75409ee13236b191b509d87eef21f3dbd3040646 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 08:04:37 +1300 Subject: [PATCH 76/81] minions: get_events() to use subscribe() like everyone else --- gossip-lib/src/overlord/minion/mod.rs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index dc91c8a89..9d84e05bd 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -805,28 +805,11 @@ impl Minion { tracing::trace!("{}: Event Filter: {} events", &self.url, filter.ids.len()); // create a handle for ourselves + // This is always a fresh subscription because they handle keeps changing let handle = format!("temp_events_{}", self.next_events_subscription_id); self.next_events_subscription_id += 1; - // save the subscription - let id = self.subscription_map.add(&handle, job_id, vec![filter]); - tracing::debug!( - "NEW SUBSCRIPTION on {} handle={}, id={}", - &self.url, - handle, - &id - ); - - // get the request message - let req_message = self.subscription_map.get(&handle).unwrap().req_message(); - - // Subscribe on the relay - let websocket_stream = self.stream.as_mut().unwrap(); - let wire = serde_json::to_string(&req_message)?; - self.last_message_sent = wire.clone(); - websocket_stream.send(WsMessage::Text(wire.clone())).await?; - - tracing::trace!("{}: Sent {}", &self.url, &wire); + self.subscribe(vec![filter], &handle, job_id).await?; Ok(()) } From 9863c8c428b57697d8cf7a82fe380307d52f7e5c Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 08:08:51 +1300 Subject: [PATCH 77/81] minions: remember if we are waiting for auth --- gossip-lib/src/overlord/minion/handle_websocket.rs | 3 ++- gossip-lib/src/overlord/minion/mod.rs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/minion/handle_websocket.rs b/gossip-lib/src/overlord/minion/handle_websocket.rs index b4cf225f8..2c66089aa 100644 --- a/gossip-lib/src/overlord/minion/handle_websocket.rs +++ b/gossip-lib/src/overlord/minion/handle_websocket.rs @@ -157,7 +157,8 @@ impl Minion { } } RelayMessage::Auth(challenge) => { - let _ = self.authenticate(challenge).await?; + let id = self.authenticate(challenge).await?; + self.waiting_for_auth = Some(id); } RelayMessage::Closed(subid, message) => { let handle = self diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 9d84e05bd..33cd07611 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -50,6 +50,7 @@ pub struct Minion { postings: HashSet, sought_events: HashMap, last_message_sent: String, + waiting_for_auth: Option, } impl Minion { @@ -78,6 +79,7 @@ impl Minion { postings: HashSet::new(), sought_events: HashMap::new(), last_message_sent: String::new(), + waiting_for_auth: None, }) } } From 3246e966905ef1567df49682ddba5f5dd72a6a82 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 06:51:57 +1300 Subject: [PATCH 78/81] minions: Handle CLOSED better; cork and retry subscriptions as needed --- .../src/overlord/minion/handle_websocket.rs | 59 ++++++++++++++++++- gossip-lib/src/overlord/minion/mod.rs | 43 ++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) diff --git a/gossip-lib/src/overlord/minion/handle_websocket.rs b/gossip-lib/src/overlord/minion/handle_websocket.rs index 2c66089aa..410289863 100644 --- a/gossip-lib/src/overlord/minion/handle_websocket.rs +++ b/gossip-lib/src/overlord/minion/handle_websocket.rs @@ -133,7 +133,16 @@ impl Minion { }; // If we are waiting for a response for this id, process - if self.postings.contains(&id) { + if self.waiting_for_auth.is_some() && self.waiting_for_auth.unwrap() == id { + self.waiting_for_auth = None; + if !ok { + // Auth failed. Let's disconnect + tracing::warn!("AUTH failed to {}: {}", &self.url, ok_message); + self.keepgoing = false; + } else { + self.try_resubscribe_to_corked().await?; + } + } else if self.postings.contains(&id) { if ok { // Save seen_on data // (it was already processed by the overlord before the minion got it, @@ -167,6 +176,54 @@ impl Minion { .unwrap_or_else(|| "_".to_owned()); tracing::info!("{}: Closed: {}: {}", &self.url, handle, message); + + let mut retry = false; + + // Check the machine-readable prefix + if let Some(prefix) = message.split(':').next() { + match prefix { + "duplicate" => { + // not much we can do; it SHOULD replace dup REQ subs, not complain. + tracing::warn!( + "{} not accepting {} due to duplicate is strange.", + &self.url, + handle + ); + } + "pow" => { + tracing::warn!( + "{} wants POW for {} but we do not do POW on demand.", + &self.url, + handle + ); + } + "rate-limited" => { + retry = true; + } + "invalid" => {} + "error" => {} + "auth-required" => { + if self.waiting_for_auth.is_none() { + tracing::warn!("{} says AUTH required for {}, but it has not AUTH challenged us yet", &self.url, handle); + } + retry = true; + } + "restricted" => {} + _ => { + tracing::warn!("{} closed with unknown prefix {}", &self.url, prefix); + } + } + } + + if retry { + // Save as corked, try it again later + self.corked_subscriptions + .push((handle, Unixtime::now().unwrap())); + } else { + // Remove the subscription + tracing::info!("{}: removed subscription {}", &self.url, handle); + let _ = self.subscription_map.remove(&handle); + } } } diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 33cd07611..5033e1bc0 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -51,6 +51,7 @@ pub struct Minion { sought_events: HashMap, last_message_sent: String, waiting_for_auth: Option, + corked_subscriptions: Vec<(String, Unixtime)>, } impl Minion { @@ -80,6 +81,7 @@ impl Minion { sought_events: HashMap::new(), last_message_sent: String::new(), waiting_for_auth: None, + corked_subscriptions: Vec::new(), }) } } @@ -297,6 +299,9 @@ impl Minion { _ = task_timer.tick() => { // Update subscription for sought events self.get_events().await?; + + // Try to subscribe to corked subscriptions + self.try_resubscribe_to_corked().await?; }, to_minion_message = self.from_overlord.recv() => { let to_minion_message = match to_minion_message { @@ -555,6 +560,11 @@ impl Minion { // Subscribe to anybody mentioning the user on the relays the user reads from // (and any other relay for the time being until nip65 is in widespread use) async fn subscribe_mentions(&mut self, job_id: u64) -> Result<(), Error> { + // If we have already subscribed to mentions, do not resubscribe + if self.subscription_map.has("mentions_feed") { + return Ok(()); + } + let mut filters: Vec = Vec::new(); // Compute how far to look back @@ -816,6 +826,32 @@ impl Minion { Ok(()) } + async fn try_resubscribe_to_corked(&mut self) -> Result<(), Error> { + // Do not do this if we are waiting for AUTH + if self.waiting_for_auth.is_some() { + return Ok(()); + } + + // Apply subscriptions that were waiting for auth + let mut handles = std::mem::take(&mut self.corked_subscriptions); + + let now = Unixtime::now().unwrap(); + + for (handle, when) in handles.drain(..) { + // Do not try if we just inserted it within the last second + if when - now < Duration::from_secs(1) { + // re-insert + self.corked_subscriptions.push((handle, when)); + continue; + } + + tracing::info!("Sending corked subscription {} to {}", handle, &self.url); + self.send_subscription(&handle).await?; + } + + Ok(()) + } + async fn get_event_addr(&mut self, job_id: u64, ea: EventAddr) -> Result<(), Error> { // create a handle for ourselves let handle = format!("temp_event_addr_{}", self.next_events_subscription_id); @@ -899,6 +935,13 @@ impl Minion { ); } + if self.waiting_for_auth.is_some() { + // Save this, subscribe after AUTH completes + self.corked_subscriptions + .push((handle.to_owned(), Unixtime::now().unwrap())); + return Ok(()); + } + self.send_subscription(handle).await?; Ok(()) } From cc0b138f0e87bee9270209c6671bb26a0c2d54af Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 06:51:57 +1300 Subject: [PATCH 79/81] minions: extend periodic resubscribe timer to 3 seconds --- gossip-lib/src/overlord/minion/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index 5033e1bc0..f87ce3083 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -286,8 +286,8 @@ impl Minion { ping_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); ping_timer.tick().await; // use up the first immediate tick. - // Periodic Task timer (2 sec) - let mut task_timer = tokio::time::interval(std::time::Duration::new(2, 0)); + // Periodic Task timer (3 sec) + let mut task_timer = tokio::time::interval(std::time::Duration::new(3, 0)); task_timer.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); task_timer.tick().await; // use up the first immediate tick. From c33700c91b95719b42bbcefe40876781ce8a54c3 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 08:28:21 +1300 Subject: [PATCH 80/81] Cork metadata and subscribe to it only after previous subscriptions complete --- gossip-lib/src/overlord/minion/mod.rs | 36 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/gossip-lib/src/overlord/minion/mod.rs b/gossip-lib/src/overlord/minion/mod.rs index f87ce3083..27bb9d94d 100644 --- a/gossip-lib/src/overlord/minion/mod.rs +++ b/gossip-lib/src/overlord/minion/mod.rs @@ -52,6 +52,7 @@ pub struct Minion { last_message_sent: String, waiting_for_auth: Option, corked_subscriptions: Vec<(String, Unixtime)>, + corked_metadata: Vec<(u64, Vec)>, } impl Minion { @@ -82,6 +83,7 @@ impl Minion { last_message_sent: String::new(), waiting_for_auth: None, corked_subscriptions: Vec::new(), + corked_metadata: Vec::new(), }) } } @@ -300,7 +302,7 @@ impl Minion { // Update subscription for sought events self.get_events().await?; - // Try to subscribe to corked subscriptions + // Try to subscribe to corked subscriptions and metadata self.try_resubscribe_to_corked().await?; }, to_minion_message = self.from_overlord.recv() => { @@ -832,11 +834,33 @@ impl Minion { return Ok(()); } + // Subscribe to metadata + if !self.subscription_map.has("temp_subscribe_metadata") && !self.corked_metadata.is_empty() + { + let mut corked_metadata = std::mem::take(&mut self.corked_metadata); + let mut combined_job_id: Option = None; + let mut combined_pubkeys: Vec = Vec::new(); + for (job_id, pubkeys) in corked_metadata.drain(..) { + if combined_job_id.is_none() { + combined_job_id = Some(job_id) + } else { + // Tell the overlord this job id is over (it got combined into + // another job_id) + self.to_overlord.send(ToOverlordMessage::MinionJobComplete( + self.url.clone(), + job_id, + ))?; + } + combined_pubkeys.extend(pubkeys); + } + + self.temp_subscribe_metadata(combined_job_id.unwrap(), combined_pubkeys) + .await?; + } + // Apply subscriptions that were waiting for auth let mut handles = std::mem::take(&mut self.corked_subscriptions); - let now = Unixtime::now().unwrap(); - for (handle, when) in handles.drain(..) { // Do not try if we just inserted it within the last second if when - now < Duration::from_secs(1) { @@ -872,6 +896,12 @@ impl Minion { job_id: u64, mut pubkeys: Vec, ) -> Result<(), Error> { + if self.subscription_map.has("temp_subscribe_metadata") { + // Save for later + self.corked_metadata.push((job_id, pubkeys)); + return Ok(()); + } + let pkhp: Vec = pubkeys.drain(..).map(|pk| pk.into()).collect(); tracing::trace!("Temporarily subscribing to metadata on {}", &self.url); From 4232fbb415a2db05a6f0daa6f949ae4f1d816f23 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 11 Dec 2023 08:33:20 +1300 Subject: [PATCH 81/81] Downgrade a fetcher log --- gossip-lib/src/fetcher.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gossip-lib/src/fetcher.rs b/gossip-lib/src/fetcher.rs index ac9bf24f5..25bbe92ac 100644 --- a/gossip-lib/src/fetcher.rs +++ b/gossip-lib/src/fetcher.rs @@ -336,7 +336,7 @@ impl Fetcher { } } FailOutcome::NotModified => { - tracing::info!("FETCH {url}: Succeeded: {message}"); + tracing::debug!("FETCH {url}: Succeeded: {message}"); let _ = filetime::set_file_mtime(cache_file.as_path(), filetime::FileTime::now()); self.urls.write().unwrap().remove(&url);