mirror of
https://github.com/mikedilger/gossip.git
synced 2024-09-29 08:21:47 +00:00
Merge remote-tracking branch 'dilger/unstable' into feature/profiles-ui-restyle
This commit is contained in:
commit
7dd1838cc7
@ -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
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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>),
|
||||
|
@ -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."),
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
61
gossip-lib/src/storage/migrations/deprecated.rs
Normal file
61
gossip-lib/src/storage/migrations/deprecated.rs
Normal 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())),
|
||||
}
|
||||
}
|
||||
}
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user