Merge remote-tracking branch 'dilger/unstable' into feature/profiles-ui-restyle

This commit is contained in:
Bu5hm4nn 2023-10-24 11:48:50 -06:00
commit 7dd1838cc7
14 changed files with 585 additions and 599 deletions

View File

@ -3,12 +3,14 @@ use crate::AVATAR_SIZE_F32;
use eframe::egui;
use egui::{Context, Image, RichText, Sense, Ui, Vec2};
use gossip_lib::comms::ToOverlordMessage;
use gossip_lib::Person;
use gossip_lib::GLOBALS;
use gossip_lib::{Person, PersonList, GLOBALS};
use std::sync::atomic::Ordering;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let followed_pubkeys = GLOBALS.people.get_followed_pubkeys();
let followed_pubkeys = GLOBALS
.storage
.get_people_in_list(PersonList::Followed, None)
.unwrap_or(vec![]);
let mut people: Vec<Person> = Vec::new();
for pk in &followed_pubkeys {
if let Ok(Some(person)) = GLOBALS.storage.read_person(pk) {
@ -23,16 +25,15 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.add_space(12.0);
let last_contact_list_size = GLOBALS
let latest_event_data = GLOBALS
.people
.last_contact_list_size
.load(Ordering::Relaxed);
let last_contact_list_asof = GLOBALS
.people
.last_contact_list_asof
.load(Ordering::Relaxed);
.latest_person_list_event_data
.get(&PersonList::Followed)
.map(|v| v.value().clone())
.unwrap_or(Default::default());
let mut asof = "unknown".to_owned();
if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_contact_list_asof) {
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]"
)) {
@ -42,8 +43,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.label(
RichText::new(format!(
"REMOTE: {} (size={})",
asof, last_contact_list_size
"REMOTE: {} (len={})",
asof, latest_event_data.public_len
))
.size(15.0),
)
@ -63,7 +64,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateFollowing { merge: false });
.send(ToOverlordMessage::UpdatePersonList {
person_list: PersonList::Followed,
merge: false,
});
}
if ui
.button("↓ Merge ↓")
@ -74,7 +78,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateFollowing { merge: true });
.send(ToOverlordMessage::UpdatePersonList {
person_list: PersonList::Followed,
merge: true,
});
}
if GLOBALS.signer.is_ready() {
@ -83,7 +90,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
.on_hover_text("This publishes your Contact List")
.clicked()
{
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushFollow);
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::PushPersonList(PersonList::Followed));
}
}
@ -118,8 +127,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.add_space(10.0);
let last_contact_list_edit = match GLOBALS.storage.read_last_contact_list_edit() {
Ok(date) => date,
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

View File

@ -3,12 +3,14 @@ use crate::AVATAR_SIZE_F32;
use eframe::egui;
use egui::{Context, Image, RichText, Sense, Ui, Vec2};
use gossip_lib::comms::ToOverlordMessage;
use gossip_lib::Person;
use gossip_lib::GLOBALS;
use gossip_lib::{Person, PersonList, GLOBALS};
use std::sync::atomic::Ordering;
pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) {
let muted_pubkeys = GLOBALS.people.get_muted_pubkeys();
let muted_pubkeys = GLOBALS
.storage
.get_people_in_list(PersonList::Muted, None)
.unwrap_or(vec![]);
let mut people: Vec<Person> = Vec::new();
for pk in &muted_pubkeys {
@ -20,14 +22,19 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
people.push(person);
}
}
people.sort_unstable();
people.sort();
ui.add_space(12.0);
let last_mute_list_size = GLOBALS.people.last_mute_list_size.load(Ordering::Relaxed);
let last_mute_list_asof = GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed);
let latest_event_data = GLOBALS
.people
.latest_person_list_event_data
.get(&PersonList::Muted)
.map(|v| v.value().clone())
.unwrap_or(Default::default());
let mut asof = "unknown".to_owned();
if let Ok(stamp) = time::OffsetDateTime::from_unix_timestamp(last_mute_list_asof) {
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]"
)) {
@ -35,7 +42,18 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
}
}
ui.label(RichText::new(format!("REMOTE: {} (size={})", asof, last_mute_list_size)).size(15.0))
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);
@ -50,7 +68,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateMuteList { merge: false });
.send(ToOverlordMessage::UpdatePersonList {
person_list: PersonList::Muted,
merge: false,
});
}
if ui
.button("↓ Merge ↓")
@ -59,7 +80,10 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
{
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateMuteList { merge: true });
.send(ToOverlordMessage::UpdatePersonList {
person_list: PersonList::Muted,
merge: true,
});
}
if GLOBALS.signer.is_ready() {
@ -68,7 +92,9 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
.on_hover_text("This publishes your Mute List")
.clicked()
{
let _ = GLOBALS.to_overlord.send(ToOverlordMessage::PushMuteList);
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::PushPersonList(PersonList::Muted));
}
}
@ -91,8 +117,12 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra
ui.add_space(10.0);
let last_mute_list_edit = match GLOBALS.storage.read_last_mute_list_edit() {
Ok(date) => date,
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

View File

@ -17,7 +17,10 @@ pub(super) fn update(app: &mut GossipUi, _ctx: &Context, _frame: &mut eframe::Fr
if app.wizard_state.contacts_sought {
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::UpdateFollowing { merge: false });
.send(ToOverlordMessage::UpdatePersonList {
person_list: PersonList::Followed,
merge: false,
});
app.wizard_state.contacts_sought = false;
}
@ -139,7 +142,9 @@ 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.to_overlord.send(ToOverlordMessage::PushFollow);
let _ = GLOBALS
.to_overlord
.send(ToOverlordMessage::PushPersonList(PersonList::Followed));
let _ = GLOBALS.storage.write_wizard_complete(true, None);
app.page = Page::Feed(FeedKind::List(PersonList::Followed, false));

View File

@ -1,5 +1,4 @@
use gossip_lib::Relay;
use gossip_lib::GLOBALS;
use gossip_lib::{PersonList, Relay, GLOBALS};
use nostr_types::{Event, EventKind, PublicKey, RelayUrl};
use std::collections::HashSet;
@ -80,7 +79,10 @@ impl WizardState {
.unwrap_or(Vec::new());
}
self.followed = GLOBALS.people.get_followed_pubkeys();
self.followed = GLOBALS
.storage
.get_people_in_list(PersonList::Followed, None)
.unwrap_or(vec![]);
if self.need_discovery_relays() {
let purplepages = RelayUrl::try_from_str("wss://purplepag.es/").unwrap();

View File

@ -1,4 +1,5 @@
use crate::dm_channel::DmChannel;
use crate::people::PersonList;
use nostr_types::{
Event, EventAddr, Id, IdHex, Metadata, MilliSatoshi, Profile, PublicKey, RelayUrl, Tag,
UncheckedUrl,
@ -102,15 +103,12 @@ pub enum ToOverlordMessage {
/// Calls [prune_database](crate::Overlord::prune_database)
PruneDatabase,
/// Calls [push_follow](crate::Overlord::push_follow)
PushFollow,
/// Calls [push_person_list](crate::Overlord::push_person_list)
PushPersonList(PersonList),
/// Calls [push_metadata](crate::Overlord::push_metadata)
PushMetadata(Metadata),
/// Calls [push_mute_list](crate::Overlord::push_mute_list)
PushMuteList,
/// Calls [rank_relay](crate::Overlord::rank_relay)
RankRelay(RelayUrl, u8),
@ -152,17 +150,17 @@ pub enum ToOverlordMessage {
/// Calls [unlock_key](crate::Overlord::unlock_key)
UnlockKey(String),
/// Calls [update_following](crate::Overlord::update_following)
UpdateFollowing { merge: bool },
/// Calls [update_metadata](crate::Overlord::update_metadata)
UpdateMetadata(PublicKey),
/// Calls [update_metadata_in_bulk](crate::Overlord::update_metadata_in_bulk)
UpdateMetadataInBulk(Vec<PublicKey>),
/// Calls [update_mute_list](crate::Overlord::update_mute_list)
UpdateMuteList { merge: bool },
/// Calls [update_person_list](crate::Overlord::update_person_list)
UpdatePersonList {
person_list: PersonList,
merge: bool,
},
/// Calls [visible_notes_changed](crate::Overlord::visible_notes_changed)
VisibleNotesChanged(Vec<Id>),

View File

@ -17,6 +17,7 @@ pub enum ErrorKind {
MpscSend(tokio::sync::mpsc::error::SendError<ToOverlordMessage>),
Nip05KeyNotFound,
Nostr(nostr_types::Error),
NoPublicKey,
NoPrivateKey,
NoRelay,
NoSlotsRemaining,
@ -88,6 +89,7 @@ impl std::fmt::Display for Error {
MpscSend(e) => write!(f, "Error sending mpsc: {e}"),
Nip05KeyNotFound => write!(f, "NIP-05 public key not found"),
Nostr(e) => write!(f, "Nostr: {e}"),
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."),
NoSlotsRemaining => write!(f, "No custom list slots remaining."),

View File

@ -486,6 +486,10 @@ pub fn enabled_event_kinds() -> Vec<EventKind> {
&& ((*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
})
.collect()
}

View File

@ -7,7 +7,7 @@ use crate::comms::{
use crate::dm_channel::DmChannel;
use crate::error::{Error, ErrorKind};
use crate::globals::{ZapState, GLOBALS};
use crate::people::Person;
use crate::people::{Person, PersonList};
use crate::person_relay::PersonRelay;
use crate::relay::Relay;
use crate::tags::{
@ -15,6 +15,7 @@ use crate::tags::{
add_subject_to_tags_if_missing,
};
use gossip_relay_picker::{Direction, RelayAssignment};
use heed::RwTxn;
use http::StatusCode;
use minion::Minion;
use nostr_types::{
@ -639,15 +640,12 @@ impl Overlord {
ToOverlordMessage::PruneDatabase => {
Self::prune_database()?;
}
ToOverlordMessage::PushFollow => {
self.push_follow().await?;
ToOverlordMessage::PushPersonList(person_list) => {
self.push_person_list(person_list).await?;
}
ToOverlordMessage::PushMetadata(metadata) => {
self.push_metadata(metadata).await?;
}
ToOverlordMessage::PushMuteList => {
self.push_mute_list().await?;
}
ToOverlordMessage::RankRelay(relay_url, rank) => {
Self::rank_relay(relay_url, rank)?;
}
@ -690,17 +688,14 @@ impl Overlord {
ToOverlordMessage::UnlockKey(password) => {
Self::unlock_key(password)?;
}
ToOverlordMessage::UpdateFollowing { merge } => {
self.update_following(merge).await?;
}
ToOverlordMessage::UpdateMetadata(pubkey) => {
self.update_metadata(pubkey).await?;
}
ToOverlordMessage::UpdateMetadataInBulk(pubkeys) => {
self.update_metadata_in_bulk(pubkeys).await?;
}
ToOverlordMessage::UpdateMuteList { merge } => {
self.update_mute_list(merge).await?;
ToOverlordMessage::UpdatePersonList { person_list, merge } => {
self.update_person_list(person_list, merge).await?;
}
ToOverlordMessage::VisibleNotesChanged(visible) => {
self.visible_notes_changed(visible).await?;
@ -1599,9 +1594,9 @@ impl Overlord {
Ok(())
}
/// Publish the user's following list
pub async fn push_follow(&mut self) -> Result<(), Error> {
let event = GLOBALS.people.generate_contact_list_event().await?;
/// Publish the user's specified PersonList
pub async fn push_person_list(&mut self, list: PersonList) -> Result<(), Error> {
let event = GLOBALS.people.generate_person_list_event(list).await?;
// process event locally
crate::process::process_new_event(&event, None, None, false, false).await?;
@ -1613,7 +1608,7 @@ impl Overlord {
for relay in relays {
// Send it the event to pull our followers
tracing::debug!("Pushing ContactList to {}", &relay.url);
tracing::debug!("Pushing PersonList={} to {}", list.name(), &relay.url);
self.engage_minion(
relay.url.clone(),
@ -1673,38 +1668,6 @@ impl Overlord {
Ok(())
}
/// Publish the user's mute list
pub async fn push_mute_list(&mut self) -> Result<(), Error> {
let event = GLOBALS.people.generate_mute_list_event().await?;
// process event locally
crate::process::process_new_event(&event, None, None, false, false).await?;
// Push to all of the relays we post to
let relays: Vec<Relay> = GLOBALS
.storage
.filter_relays(|r| r.has_usage_bits(Relay::WRITE) && r.rank != 0)?;
for relay in relays {
// Send it the event to pull our followers
tracing::debug!("Pushing MuteList to {}", &relay.url);
self.engage_minion(
relay.url.clone(),
vec![RelayJob {
reason: RelayConnectionReason::PostMuteList,
payload: ToMinionPayload {
job_id: rand::random::<u64>(),
detail: ToMinionPayloadDetail::PostEvent(Box::new(event.clone())),
},
}],
)
.await?;
}
Ok(())
}
/// Rank a relay from 0 to 9. The default rank is 3. A rank of 0 means the relay will not be used.
/// This represent a user's judgement, and is factored into how suitable a relay is for various
/// purposes.
@ -2245,127 +2208,6 @@ impl Overlord {
Ok(())
}
/// Update the local following list from the last ContactList event received.
pub async fn update_following(&mut self, merge: bool) -> Result<(), Error> {
// Load the latest contact list from the database
let our_contact_list = {
let pubkey = match GLOBALS.signer.public_key() {
Some(pk) => pk,
None => return Ok(()), // we cannot do anything without an identity setup first
};
if let Some(event) = GLOBALS
.storage
.get_replaceable_event(pubkey, EventKind::ContactList)?
{
event.clone()
} else {
return Ok(()); // we have no contact list to update from
}
};
let mut pubkeys: Vec<PublicKey> = Vec::new();
let now = Unixtime::now().unwrap();
let mut txn = GLOBALS.storage.get_write_txn()?;
// 'p' tags represent the author's contacts
for tag in &our_contact_list.tags {
if let Tag::Pubkey {
pubkey,
recommended_relay_url,
petname,
..
} = tag
{
if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) {
// Save the pubkey for actual following them (outside of the loop in a batch)
pubkeys.push(pubkey.to_owned());
// If there is a URL
if let Some(url) = recommended_relay_url
.as_ref()
.and_then(|rru| RelayUrl::try_from_unchecked_url(rru).ok())
{
// Save relay if missing
GLOBALS
.storage
.write_relay_if_missing(&url, Some(&mut txn))?;
// create or update person_relay last_suggested_kind3
let mut pr = match GLOBALS.storage.read_person_relay(pubkey, &url)? {
Some(pr) => pr,
None => PersonRelay::new(pubkey, url.clone()),
};
pr.last_suggested_kind3 = Some(now.0 as u64);
GLOBALS.storage.write_person_relay(&pr, Some(&mut txn))?;
}
// Handle petname
if merge && petname.is_none() {
// In this case, we leave any existing petname, so no need to load the
// person record. But we need to ensure the person exists
GLOBALS
.storage
.write_person_if_missing(&pubkey, Some(&mut txn))?;
} else {
// In every other case we have to load the person and compare
let mut person_needs_save = false;
let mut person = match GLOBALS.storage.read_person(&pubkey)? {
Some(person) => person,
None => {
person_needs_save = true;
Person::new(pubkey.to_owned())
}
};
if *petname != person.petname {
if petname.is_some() {
person_needs_save = true;
person.petname = petname.clone();
} else if !merge {
// In overwrite mode, clear to None
person_needs_save = true;
person.petname = None;
}
}
if person_needs_save {
GLOBALS.storage.write_person(&person, Some(&mut txn))?;
}
}
}
}
}
txn.commit()?;
// Follow all those pubkeys publicly, and unfollow everbody else if merge=false
GLOBALS.people.follow_all(&pubkeys, merge, true)?;
// Update last_contact_list_edit
let last_edit = if merge {
Unixtime::now().unwrap() // now, since superior to the last event
} else {
our_contact_list.created_at
};
GLOBALS
.storage
.write_last_contact_list_edit(last_edit.0, None)?;
// Pick relays again
{
// Refresh person-relay scores
GLOBALS.relay_picker.refresh_person_relay_scores().await?;
// Then pick
self.pick_relays().await;
}
Ok(())
}
/// Subscribe, fetch, and update metadata for the person
pub async fn update_metadata(&mut self, pubkey: PublicKey) -> Result<(), Error> {
let best_relays = GLOBALS.storage.get_best_relays(pubkey, Direction::Write)?;
@ -2427,49 +2269,163 @@ impl Overlord {
Ok(())
}
/// Update the local mute list from the last ContactList event received.
pub async fn update_mute_list(&mut self, merge: bool) -> Result<(), Error> {
// Load the latest MuteList from the database
let our_mute_list = {
let pubkey = match GLOBALS.signer.public_key() {
Some(pk) => pk,
None => return Ok(()), // we cannot do anything without an identity setup first
};
/// 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 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()),
};
// Load the latest PersonList event from the database
let event = {
if let Some(event) = GLOBALS
.storage
.get_replaceable_event(pubkey, EventKind::MuteList)?
.get_replaceable_event(my_pubkey, list.event_kind())?
{
event.clone()
} else {
return Ok(()); // we have no mute list to update from
return Ok(()); // we have no event to update from, so we are done
}
};
let mut pubkeys: Vec<PublicKey> = Vec::new();
let now = Unixtime::now().unwrap();
// 'p' tags represent the author's mutes
for tag in &our_mute_list.tags {
if let Tag::Pubkey { pubkey, .. } = tag {
let mut txn = GLOBALS.storage.get_write_txn()?;
let mut entries: Vec<(PublicKey, bool)> = Vec::new();
// Public entries
for tag in &event.tags {
if let Tag::Pubkey {
pubkey,
recommended_relay_url,
petname,
..
} = tag
{
if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) {
// Save the pubkey
pubkeys.push(pubkey.to_owned());
entries.push((pubkey.to_owned(), true));
// Deal with recommended_relay_urls and petnames
if list == PersonList::Followed {
Self::integrate_rru_and_petname(
&pubkey,
recommended_relay_url,
petname,
now,
merge,
&mut txn,
)?;
}
}
}
}
// Mute all those pubkeys publicly, and unmute everbody else if merge=false
GLOBALS.people.mute_all(&pubkeys, merge, true)?;
// Private entries
if list != PersonList::Followed {
let decrypted_content = GLOBALS.signer.decrypt_nip04(&my_pubkey, &event.content)?;
let tags: Vec<Tag> = serde_json::from_slice(&decrypted_content)?;
for tag in &tags {
if let Tag::Pubkey { pubkey, .. } = tag {
if let Ok(pubkey) = PublicKey::try_from_hex_string(pubkey, true) {
// Save the pubkey
entries.push((pubkey.to_owned(), false));
}
}
}
}
if !merge {
GLOBALS.storage.clear_person_list(list, Some(&mut txn))?;
}
for (pubkey, public) in &entries {
GLOBALS
.storage
.add_person_to_list(pubkey, list, *public, Some(&mut txn))?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
let last_edit = if merge { now } else { event.created_at };
// Update last_must_list_edit
let last_edit = if merge {
Unixtime::now().unwrap() // now, since superior to the last event
} else {
our_mute_list.created_at
};
GLOBALS
.storage
.write_last_mute_list_edit(last_edit.0, None)?;
.set_person_list_last_edit_time(list, last_edit.0, Some(&mut txn))?;
txn.commit()?;
// Pick relays again
if list.subscribe() {
// Refresh person-relay scores
GLOBALS.relay_picker.refresh_person_relay_scores().await?;
// Then pick
self.pick_relays().await;
}
Ok(())
}
fn integrate_rru_and_petname(
pubkey: &PublicKey,
recommended_relay_url: &Option<UncheckedUrl>,
petname: &Option<String>,
now: Unixtime,
merge: bool,
txn: &mut RwTxn,
) -> Result<(), Error> {
// If there is a URL
if let Some(url) = recommended_relay_url
.as_ref()
.and_then(|rru| RelayUrl::try_from_unchecked_url(rru).ok())
{
// Save relay if missing
GLOBALS.storage.write_relay_if_missing(&url, Some(txn))?;
// create or update person_relay last_suggested_kind3
let mut pr = match GLOBALS.storage.read_person_relay(*pubkey, &url)? {
Some(pr) => pr,
None => PersonRelay::new(*pubkey, url.clone()),
};
pr.last_suggested_kind3 = Some(now.0 as u64);
GLOBALS.storage.write_person_relay(&pr, Some(txn))?;
}
// Handle petname
if merge && petname.is_none() {
// In this case, we leave any existing petname, so no need to load the
// person record. But we need to ensure the person exists
GLOBALS.storage.write_person_if_missing(pubkey, Some(txn))?;
} else {
// In every other case we have to load the person and compare
let mut person_needs_save = false;
let mut person = match GLOBALS.storage.read_person(pubkey)? {
Some(person) => person,
None => {
person_needs_save = true;
Person::new(pubkey.to_owned())
}
};
if *petname != person.petname {
if petname.is_some() {
person_needs_save = true;
person.petname = petname.clone();
} else if !merge {
// In overwrite mode, clear to None
person_needs_save = true;
person.petname = None;
}
}
if person_needs_save {
GLOBALS.storage.write_person(&person, Some(txn))?;
}
}
Ok(())
}

View File

@ -5,10 +5,11 @@ use dashmap::{DashMap, DashSet};
use gossip_relay_picker::Direction;
use image::RgbaImage;
use nostr_types::{
Event, EventKind, Metadata, PreEvent, PublicKey, RelayUrl, Tag, UncheckedUrl, Unixtime, Url,
ContentEncryptionAlgorithm, Event, EventKind, Metadata, PreEvent, PublicKey, RelayUrl, Tag,
UncheckedUrl, Unixtime, Url,
};
use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicI64, AtomicUsize, Ordering};
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::task;
@ -19,6 +20,30 @@ pub type Person = crate::storage::types::Person2;
/// PersonList type, aliased to the latest version
pub type PersonList = crate::storage::types::PersonList1;
/// 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<usize>,
}
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 {
@ -45,17 +70,8 @@ pub struct People {
// per gossip run (this set only grows)
tried_metadata: DashSet<PublicKey>,
// Date of the last self-owned contact list we have an event for
pub last_contact_list_asof: AtomicI64,
// Size of the last self-owned contact list we have an event for
pub last_contact_list_size: AtomicUsize,
// Date of the last self-owned mute list we have an event for
pub last_mute_list_asof: AtomicI64,
// Size of the last self-owned mute list we have an event for
pub last_mute_list_size: AtomicUsize,
/// Latest person list event data for each PersonList
pub latest_person_list_event_data: DashMap<PersonList, PersonListEventData>,
}
impl Default for People {
@ -74,67 +90,13 @@ impl People {
recheck_nip05: DashSet::new(),
need_metadata: DashSet::new(),
tried_metadata: DashSet::new(),
last_contact_list_asof: AtomicI64::new(0),
last_contact_list_size: AtomicUsize::new(0),
last_mute_list_asof: AtomicI64::new(0),
last_mute_list_size: AtomicUsize::new(0),
latest_person_list_event_data: DashMap::new(),
}
}
// Start the periodic task management
pub(crate) fn start() {
if let Some(pk) = GLOBALS.signer.public_key() {
// Load our contact list from the database in order to populate
// last_contact_list_asof and last_contact_list_size
if let Ok(Some(event)) = GLOBALS
.storage
.get_replaceable_event(pk, EventKind::ContactList)
{
if event.created_at.0
> GLOBALS
.people
.last_contact_list_asof
.load(Ordering::Relaxed)
{
GLOBALS
.people
.last_contact_list_asof
.store(event.created_at.0, Ordering::Relaxed);
let size = event
.tags
.iter()
.filter(|t| matches!(t, Tag::Pubkey { .. }))
.count();
GLOBALS
.people
.last_contact_list_size
.store(size, Ordering::Relaxed);
}
}
// Load our mute list from the database in order to populate
// last_mute_list_asof and last_mute_list_size
if let Ok(Some(event)) = GLOBALS
.storage
.get_replaceable_event(pk, EventKind::MuteList)
{
if event.created_at.0 > GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed) {
GLOBALS
.people
.last_mute_list_asof
.store(event.created_at.0, Ordering::Relaxed);
let size = event
.tags
.iter()
.filter(|t| matches!(t, Tag::Pubkey { .. }))
.count();
GLOBALS
.people
.last_mute_list_size
.store(size, Ordering::Relaxed);
}
}
}
GLOBALS.people.update_latest_person_list_event_data();
task::spawn(async {
loop {
@ -155,6 +117,52 @@ 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(pk, person_list.event_kind())
{
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<usize> = 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::<Vec<Tag>>(&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<PublicKey> {
// We subscribe to all people in all lists.
@ -168,50 +176,13 @@ impl People {
}
}
/// Get all the pubkeys in the Followed list
pub fn get_followed_pubkeys(&self) -> Vec<PublicKey> {
// We subscribe to all people in all lists.
// This is no longer synonomous with the ContactList list
match GLOBALS
/// Is the person in the list? (returns false on error)
#[inline]
pub fn is_person_in_list(&self, pubkey: &PublicKey, list: PersonList) -> bool {
GLOBALS
.storage
.get_people_in_list(PersonList::Followed, None)
{
Ok(people) => people,
Err(e) => {
tracing::error!("{}", e);
vec![]
}
}
}
/// Get all the pubkeys that the user mutes
pub fn get_muted_pubkeys(&self) -> Vec<PublicKey> {
match GLOBALS.storage.get_people_in_list(PersonList::Muted, None) {
Ok(people) => people,
Err(e) => {
tracing::error!("{}", e);
vec![]
}
}
}
/// Is the given pubkey followed?
pub fn is_followed(&self, pubkey: &PublicKey) -> bool {
match GLOBALS
.storage
.is_person_in_list(pubkey, PersonList::Followed)
{
Ok(answer) => answer,
_ => false,
}
}
/// Is the given pubkey muted?
pub fn is_muted(&self, pubkey: &PublicKey) -> bool {
match GLOBALS.storage.is_person_in_list(pubkey, PersonList::Muted) {
Ok(answer) => answer,
_ => false,
}
.is_person_in_list(pubkey, list)
.unwrap_or(false)
}
/// Get all the pubkeys that need relay lists (from the given set)
@ -609,92 +580,103 @@ impl People {
.collect())
}
pub(crate) async fn generate_contact_list_event(&self) -> Result<Event, Error> {
let mut p_tags: Vec<Tag> = Vec::new();
let pubkeys = self.get_followed_pubkeys();
for pubkey in &pubkeys {
// Get their petname
let mut petname: Option<String> = None;
if let Some(person) = GLOBALS.storage.read_person(pubkey)? {
petname = person.petname.clone();
}
// Get their best relay
let relays = GLOBALS.storage.get_best_relays(*pubkey, Direction::Write)?;
let maybeurl = relays.get(0);
p_tags.push(Tag::Pubkey {
pubkey: (*pubkey).into(),
recommended_relay_url: maybeurl.map(|(u, _)| u.to_unchecked_url()),
petname,
trailing: Vec::new(),
});
pub(crate) async fn generate_person_list_event(
&self,
person_list: PersonList,
) -> Result<Event, Error> {
if !GLOBALS.signer.is_ready() {
return Err((ErrorKind::NoPrivateKey, file!(), line!()).into());
}
let public_key = match GLOBALS.signer.public_key() {
Some(pk) => pk,
None => return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()), // not even a public key
};
let my_pubkey = GLOBALS.signer.public_key().unwrap();
// Get the content from our latest ContactList.
// We don't use the data, but we shouldn't clobber it.
let content = match GLOBALS
// Read the person list in two parts
let public_people = GLOBALS
.storage
.get_replaceable_event(public_key, EventKind::ContactList)?
{
Some(c) => c.content,
None => "".to_owned(),
.get_people_in_list(person_list, Some(true))?;
let private_people = GLOBALS
.storage
.get_people_in_list(person_list, Some(false))?;
// Determine the event kind
let kind = match person_list {
PersonList::Followed => EventKind::ContactList,
PersonList::Muted => EventKind::MuteList,
PersonList::Custom(_) => EventKind::CategorizedPeopleList,
};
let pre_event = PreEvent {
pubkey: public_key,
created_at: Unixtime::now().unwrap(),
kind: EventKind::ContactList,
tags: p_tags,
content,
};
// Build public p-tags
let mut tags: Vec<Tag> = Vec::new();
for pubkey in public_people.iter() {
// 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
};
GLOBALS.signer.sign_preevent(pre_event, None, 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
};
pub(crate) async fn generate_mute_list_event(&self) -> Result<Event, Error> {
let mut p_tags: Vec<Tag> = Vec::new();
let muted_pubkeys = self.get_muted_pubkeys();
for muted_pubkey in &muted_pubkeys {
p_tags.push(Tag::Pubkey {
pubkey: (*muted_pubkey).into(),
recommended_relay_url: None,
petname: None,
tags.push(Tag::Pubkey {
pubkey: pubkey.into(),
recommended_relay_url,
petname,
trailing: vec![],
});
}
let public_key = match GLOBALS.signer.public_key() {
Some(pk) => pk,
None => return Err((ErrorKind::NoPrivateKey, file!(), line!()).into()), // not even a public key
};
// Add d-tag if using CategorizedPeopleList
if matches!(person_list, PersonList::Custom(_)) {
tags.push(Tag::Identifier {
d: person_list.name(),
trailing: vec![],
});
}
// Get the content from our latest MuteList.
// We don't use the data, but we shouldn't clobber it (it is for private mutes
// that we have not implemented yet)
let content = match GLOBALS
.storage
.get_replaceable_event(public_key, EventKind::MuteList)?
{
Some(c) => c.content,
None => "".to_owned(),
let content = {
if kind == EventKind::ContactList {
match GLOBALS
.storage
.get_replaceable_event(my_pubkey, EventKind::ContactList)?
{
Some(c) => c.content,
None => "".to_owned(),
}
} else {
// Build private p-tags (except for ContactList)
let mut private_p_tags: Vec<Tag> = Vec::new();
for pubkey in private_people.iter() {
private_p_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)?;
GLOBALS.signer.encrypt(
&my_pubkey,
&private_tags_string,
ContentEncryptionAlgorithm::Nip04,
)?
}
};
let pre_event = PreEvent {
pubkey: public_key,
pubkey: my_pubkey,
created_at: Unixtime::now().unwrap(),
kind: EventKind::MuteList,
tags: p_tags,
kind,
tags,
content,
};
@ -721,55 +703,17 @@ impl People {
}
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
GLOBALS
.storage
.write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
GLOBALS.storage.set_person_list_last_edit_time(
PersonList::Followed,
Unixtime::now().unwrap().0,
Some(&mut txn),
)?;
txn.commit()?;
Ok(())
}
/// Follow all these public keys.
/// This does not publish any events.
pub(crate) fn follow_all(
&self,
pubkeys: &[PublicKey],
public: bool,
merge: bool,
) -> Result<(), Error> {
let mut txn = GLOBALS.storage.get_write_txn()?;
if !merge {
GLOBALS
.storage
.clear_person_list(PersonList::Followed, Some(&mut txn))?;
}
for pubkey in pubkeys {
GLOBALS.storage.add_person_to_list(
pubkey,
PersonList::Followed,
public,
Some(&mut txn),
)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
GLOBALS
.storage
.write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
txn.commit()?;
// Add the people to the relay_picker for picking
for pubkey in pubkeys.iter() {
GLOBALS.relay_picker.add_someone(pubkey.to_owned())?;
}
Ok(())
}
/// Empty the following list.
/// This does not publish any events.
pub(crate) fn follow_none(&self) -> Result<(), Error> {
@ -778,9 +722,11 @@ impl People {
GLOBALS
.storage
.clear_person_list(PersonList::Followed, Some(&mut txn))?;
GLOBALS
.storage
.write_last_contact_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
GLOBALS.storage.set_person_list_last_edit_time(
PersonList::Followed,
Unixtime::now().unwrap().0,
Some(&mut txn),
)?;
txn.commit()?;
@ -797,9 +743,11 @@ impl People {
GLOBALS
.storage
.clear_person_list(PersonList::Muted, Some(&mut txn))?;
GLOBALS
.storage
.write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
GLOBALS.storage.set_person_list_last_edit_time(
PersonList::Muted,
Unixtime::now().unwrap().0,
Some(&mut txn),
)?;
txn.commit()?;
@ -831,9 +779,11 @@ impl People {
.remove_person_from_list(pubkey, PersonList::Muted, Some(&mut txn))?;
}
GLOBALS
.storage
.write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
GLOBALS.storage.set_person_list_last_edit_time(
PersonList::Muted,
Unixtime::now().unwrap().0,
Some(&mut txn),
)?;
txn.commit()?;
@ -842,39 +792,6 @@ impl People {
Ok(())
}
pub(crate) fn mute_all(
&self,
pubkeys: &[PublicKey],
merge: bool,
public: bool,
) -> Result<(), Error> {
let mut txn = GLOBALS.storage.get_write_txn()?;
if !merge {
GLOBALS
.storage
.clear_person_list(PersonList::Muted, Some(&mut txn))?;
}
for pubkey in pubkeys {
GLOBALS.storage.add_person_to_list(
pubkey,
PersonList::Muted,
public,
Some(&mut txn),
)?;
GLOBALS.ui_people_to_invalidate.write().push(*pubkey);
}
GLOBALS
.storage
.write_last_mute_list_edit(Unixtime::now().unwrap().0, Some(&mut txn))?;
txn.commit()?;
Ok(())
}
// Returns true if the date passed in is newer than what we already had
pub(crate) async fn update_relay_list_stamps(
&self,

View File

@ -2,6 +2,7 @@ use crate::comms::ToOverlordMessage;
use crate::error::Error;
use crate::filter::EventFilterAction;
use crate::globals::GLOBALS;
use crate::people::PersonList;
use crate::person_relay::PersonRelay;
use async_recursion::async_recursion;
use nostr_types::{
@ -63,7 +64,11 @@ pub async fn process_new_event(
}
// Spam filter (displayable and author is not followed)
if event.effective_kind().is_feed_displayable() && !GLOBALS.people.is_followed(&event.pubkey) {
if event.effective_kind().is_feed_displayable()
&& !GLOBALS
.people
.is_person_in_list(&event.pubkey, PersonList::Followed)
{
let author = GLOBALS.storage.read_person(&event.pubkey)?;
match crate::filter::filter(event.clone(), author) {
EventFilterAction::Allow => {}
@ -212,62 +217,32 @@ pub async fn process_new_event(
if event.kind == EventKind::ContactList {
if let Some(pubkey) = GLOBALS.signer.public_key() {
if event.pubkey == pubkey {
// We do not process our own contact list automatically.
// Instead we only process it on user command.
// See Overlord::update_following()
//
// But we do update people.last_contact_list_asof and _size
if event.created_at.0
> GLOBALS
.people
.last_contact_list_asof
.load(Ordering::Relaxed)
{
GLOBALS
.people
.last_contact_list_asof
.store(event.created_at.0, Ordering::Relaxed);
let size = event
.tags
.iter()
.filter(|t| matches!(t, Tag::Pubkey { .. }))
.count();
GLOBALS
.people
.last_contact_list_size
.store(size, Ordering::Relaxed);
}
return Ok(());
// 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 {
process_somebody_elses_contact_list(event).await?;
}
} else {
process_somebody_elses_contact_list(event).await?;
}
} else if event.kind == EventKind::MuteList {
} else if event.kind == EventKind::MuteList || event.kind == EventKind::CategorizedPeopleList {
if let Some(pubkey) = GLOBALS.signer.public_key() {
if event.pubkey == pubkey {
// We do not process our own mute list automatically.
// Instead we only process it on user command.
// See Overlord::update_muted()
//
// But we do update people.last_mute_list_asof and _size
if event.created_at.0 > GLOBALS.people.last_mute_list_asof.load(Ordering::Relaxed) {
GLOBALS
.people
.last_mute_list_asof
.store(event.created_at.0, Ordering::Relaxed);
let size = event
.tags
.iter()
.filter(|t| matches!(t, Tag::Pubkey { .. }))
.count();
GLOBALS
.people
.last_mute_list_size
.store(size, Ordering::Relaxed);
// 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::CategorizedPeopleList {
// 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)?;
}
return Ok(());
}
}
} else if event.kind == EventKind::RelayList {

View File

@ -0,0 +1,61 @@
use super::Storage;
use crate::error::Error;
use heed::RwTxn;
use nostr_types::Unixtime;
impl Storage {
/// Write the user's last ContactList edit time
/// DEPRECATED - use set_person_list_last_edit_time instead
pub(in crate::storage) fn write_last_contact_list_edit<'a>(
&'a self,
when: i64,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<(), Error> {
let bytes = when.to_be_bytes();
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
self.general
.put(txn, b"last_contact_list_edit", 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
/// DEPRECATED - use get_person_list_last_edit_time instead
pub(in crate::storage) fn read_last_contact_list_edit(&self) -> Result<i64, Error> {
let txn = self.env.read_txn()?;
match self.general.get(&txn, b"last_contact_list_edit")? {
None => {
let now = Unixtime::now().unwrap();
Ok(now.0)
}
Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())),
}
}
/// Read the user's last MuteList edit time
/// DEPRECATED - use get_person_list_last_edit_time instead
pub(in crate::storage) fn read_last_mute_list_edit(&self) -> Result<i64, Error> {
let txn = self.env.read_txn()?;
match self.general.get(&txn, b"last_mute_list_edit")? {
None => {
let now = Unixtime::now().unwrap();
Ok(now.0)
}
Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())),
}
}
}

View File

@ -1,3 +1,5 @@
mod deprecated;
use super::types::{
Person2, PersonList1, PersonRelay1, Settings1, Settings2, Theme1, ThemeVariant1,
};
@ -10,7 +12,7 @@ use speedy::{Readable, Writable};
use std::collections::HashMap;
impl Storage {
const MAX_MIGRATION_LEVEL: u32 = 14;
const MAX_MIGRATION_LEVEL: u32 = 15;
pub(super) fn migrate(&self, mut level: u32) -> Result<(), Error> {
if level > Self::MAX_MIGRATION_LEVEL {
@ -137,6 +139,10 @@ impl Storage {
tracing::info!("{prefix}: removing a retired setting...");
self.remove_setting_custom_person_list_names(txn)?;
}
14 => {
tracing::info!("{prefix}: moving person list last edit times...");
self.move_person_list_last_edit_times(txn)?;
}
_ => panic!("Unreachable migration level"),
};
@ -603,4 +609,15 @@ impl Storage {
self.general.delete(txn, b"custom_person_list_names")?;
Ok(())
}
pub fn move_person_list_last_edit_times<'a>(
&'a self,
txn: &mut RwTxn<'a>,
) -> Result<(), Error> {
let mut edit_times: HashMap<PersonList1, i64> = 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))?;
Ok(())
}
}

View File

@ -538,17 +538,17 @@ impl Storage {
}
}
/// Write the user's last ContactList edit time
pub fn write_last_contact_list_edit<'a>(
/// Write the user's last PersonList edit times
pub fn write_person_lists_last_edit_times<'a>(
&'a self,
when: i64,
times: HashMap<PersonList, i64>,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<(), Error> {
let bytes = when.to_be_bytes();
let bytes = times.write_to_vec()?;
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
self.general
.put(txn, b"last_contact_list_edit", bytes.as_slice())?;
.put(txn, b"person_lists_last_edit_times", bytes.as_slice())?;
Ok(())
};
@ -565,57 +565,32 @@ impl Storage {
}
/// Read the user's last ContactList edit time
pub fn read_last_contact_list_edit(&self) -> Result<i64, Error> {
pub fn read_person_lists_last_edit_times(&self) -> Result<HashMap<PersonList, i64>, Error> {
let txn = self.env.read_txn()?;
match self.general.get(&txn, b"last_contact_list_edit")? {
None => {
let now = Unixtime::now().unwrap();
self.write_last_contact_list_edit(now.0, None)?;
Ok(now.0)
}
Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())),
match self.general.get(&txn, b"person_lists_last_edit_times")? {
None => Ok(HashMap::new()),
Some(bytes) => Ok(HashMap::<PersonList, i64>::read_from_buffer(bytes)?),
}
}
/// Write the user's last MuteList edit time
pub fn write_last_mute_list_edit<'a>(
/// Set a person list last edit time
pub fn set_person_list_last_edit_time<'a>(
&'a self,
when: i64,
list: PersonList,
time: i64,
rw_txn: Option<&mut RwTxn<'a>>,
) -> Result<(), Error> {
let bytes = when.to_be_bytes();
let f = |txn: &mut RwTxn<'a>| -> Result<(), Error> {
self.general
.put(txn, b"last_mute_list_edit", bytes.as_slice())?;
Ok(())
};
match rw_txn {
Some(txn) => f(txn)?,
None => {
let mut txn = self.env.write_txn()?;
f(&mut txn)?;
txn.commit()?;
}
};
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(())
}
/// Read the user's last MuteList edit time
pub fn read_last_mute_list_edit(&self) -> Result<i64, Error> {
let txn = self.env.read_txn()?;
match self.general.get(&txn, b"last_mute_list_edit")? {
None => {
let now = Unixtime::now().unwrap();
self.write_last_mute_list_edit(now.0, None)?;
Ok(now.0)
}
Some(bytes) => Ok(i64::from_be_bytes(bytes[..8].try_into().unwrap())),
}
/// Get a person list last edit time
pub fn get_person_list_last_edit_time(&self, list: PersonList) -> Result<Option<i64>, Error> {
let lists = self.read_person_lists_last_edit_times()?;
Ok(lists.get(&list).copied())
}
/// Write a flag, whether the user is only following people with no account (or not)

View File

@ -1,6 +1,7 @@
use crate::error::{Error, ErrorKind};
use crate::globals::GLOBALS;
use heed::RwTxn;
use nostr_types::EventKind;
use speedy::{Readable, Writable};
/// Lists people can be added to
@ -51,14 +52,34 @@ impl PersonList1 {
let mut output: Vec<(PersonList1, String)> = vec![];
let map = GLOBALS.storage.read_setting_custom_person_list_map();
for (k, v) in map.iter() {
output.push((PersonList1::Custom(*k), v.clone()));
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(name: &str, txn: Option<&mut RwTxn<'_>>) -> Result<PersonList1, Error> {
// 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;
@ -69,6 +90,7 @@ impl PersonList1 {
.write_setting_custom_person_list_map(&map, txn)?;
return Ok(PersonList1::Custom(i));
}
Err(ErrorKind::NoSlotsRemaining.into())
}
@ -105,6 +127,15 @@ impl PersonList1 {
}
}
/// Get the event kind matching this PersonList1
pub fn event_kind(&self) -> EventKind {
match *self {
PersonList1::Followed => EventKind::ContactList,
PersonList1::Muted => EventKind::MuteList,
PersonList1::Custom(_) => EventKind::CategorizedPeopleList,
}
}
/// Should we subscribe to events from people in this list?
pub fn subscribe(&self) -> bool {
!matches!(*self, PersonList1::Muted)