From 6d89afdf447f83bd06bce36c4697053c17328488 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Mon, 23 Jan 2023 10:27:46 +1300 Subject: [PATCH] Mute --- src/db/mod.rs | 6 +++- src/db/schema16.sql | 2 ++ src/overlord/mod.rs | 1 + src/people.rs | 70 +++++++++++++++++++++++++++++++++++------ src/ui/feed.rs | 15 +++++++++ src/ui/mod.rs | 4 ++- src/ui/people/mod.rs | 5 +++ src/ui/people/muted.rs | 60 +++++++++++++++++++++++++++++++++++ src/ui/people/person.rs | 19 +++++------ 9 files changed, 161 insertions(+), 21 deletions(-) create mode 100644 src/db/schema16.sql create mode 100644 src/ui/people/muted.rs diff --git a/src/db/mod.rs b/src/db/mod.rs index 15e5c4be..b320b233 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -103,13 +103,16 @@ macro_rules! apply_sql { } fn upgrade(db: &Connection, mut version: u16) -> Result<(), Error> { - let current_version = 15; + let current_version = 16; if version > current_version { panic!( "Database version {} is newer than this binary which expects version {}.", version, current_version ); } + + // note to developers: we cannot make this into a loop because include_str! included + // by apply_sql! requires a static string, not a dynamically formatted one. apply_sql!(db, version, 1, "schema1.sql"); apply_sql!(db, version, 2, "schema2.sql"); apply_sql!(db, version, 3, "schema3.sql"); @@ -125,6 +128,7 @@ fn upgrade(db: &Connection, mut version: u16) -> Result<(), Error> { apply_sql!(db, version, 13, "schema13.sql"); apply_sql!(db, version, 14, "schema14.sql"); apply_sql!(db, version, 15, "schema15.sql"); + apply_sql!(db, version, 16, "schema16.sql"); tracing::info!("Database is at version {}", version); Ok(()) } diff --git a/src/db/schema16.sql b/src/db/schema16.sql new file mode 100644 index 00000000..17247292 --- /dev/null +++ b/src/db/schema16.sql @@ -0,0 +1,2 @@ +ALTER TABLE person ADD COLUMN + muted INTEGER DEFAULT 0; diff --git a/src/overlord/mod.rs b/src/overlord/mod.rs index 8f240b22..954f4899 100644 --- a/src/overlord/mod.rs +++ b/src/overlord/mod.rs @@ -740,6 +740,7 @@ impl Overlord { } // Add all the 'p' tags from the note we are replying to (except our own) + // FIXME: Should we avoid taging people who are muted? for tag in &event.tags { if let Tag::Pubkey { pubkey, .. } = tag { if pubkey.0 != public_key.as_hex_string() { diff --git a/src/people.rs b/src/people.rs index 906f7474..2e0d022d 100644 --- a/src/people.rs +++ b/src/people.rs @@ -23,6 +23,7 @@ pub struct DbPerson { pub nip05_last_checked: Option, pub followed: u8, pub followed_last_updated: i64, + pub muted: u8, } impl DbPerson { @@ -35,6 +36,7 @@ impl DbPerson { nip05_last_checked: None, followed: 0, followed_last_updated: 0, + muted: 0, } } @@ -263,8 +265,11 @@ impl People { )); } + // NOTE: We also load all muted people, so that we can render the list of people + // who are muted, so they can be found and unmuted as necessary. + let sql = "SELECT pubkey, metadata, metadata_at, nip05_valid, nip05_last_checked, \ - followed, followed_last_updated FROM person WHERE followed=1" + followed, followed_last_updated, muted FROM person WHERE followed=1 OR muted=1" .to_owned(); let output: Result, Error> = task::spawn_blocking(move || { @@ -288,6 +293,7 @@ impl People { nip05_last_checked: row.get(4)?, followed: row.get(5)?, followed_last_updated: row.get(6)?, + muted: row.get(7)?, }); } Ok(output) @@ -560,7 +566,7 @@ impl People { } pub async fn async_follow(&self, pubkeyhex: &PublicKeyHex, follow: bool) -> Result<(), Error> { - let f: u8 = u8::from(follow); + let follow: u8 = u8::from(follow); // Follow in database let sql = "INSERT INTO PERSON (pubkey, followed) values (?, ?) \ @@ -570,14 +576,14 @@ impl People { let maybe_db = GLOBALS.db.blocking_lock(); let db = maybe_db.as_ref().unwrap(); let mut stmt = db.prepare(sql)?; - stmt.execute((&pubkeyhex2.0, &f, &f))?; + stmt.execute((&pubkeyhex2.0, &follow, &follow))?; Ok::<(), Error>(()) }) .await??; // Make sure memory matches if let Some(mut dbperson) = self.people.get_mut(pubkeyhex) { - dbperson.followed = f; + dbperson.followed = follow; } else { // load if let Some(person) = Self::fetch_one(pubkeyhex).await? { @@ -605,7 +611,8 @@ impl People { // Follow in database let sql = format!( - "UPDATE person SET followed=1, followed_last_updated=? WHERE pubkey IN ({}) and followed_last_updated Result<(), Error> { + let mute: u8 = u8::from(mute); + + // Mute in database + let sql = "INSERT INTO PERSON (pubkey, muted) values (?, ?) \ + ON CONFLICT(pubkey) DO UPDATE SET muted=?"; + let pubkeyhex2 = pubkeyhex.to_owned(); + task::spawn_blocking(move || { + let maybe_db = GLOBALS.db.blocking_lock(); + let db = maybe_db.as_ref().unwrap(); + let mut stmt = db.prepare(sql)?; + stmt.execute((&pubkeyhex2.0, &mute, &mute))?; + Ok::<(), Error>(()) + }) + .await??; + + // Make sure memory matches + if let Some(mut dbperson) = self.people.get_mut(pubkeyhex) { + dbperson.muted = mute; + } else { + // load + if let Some(person) = Self::fetch_one(pubkeyhex).await? { + self.people.insert(pubkeyhex.to_owned(), person); + } + } + + Ok(()) + } + pub async fn update_nip05_last_checked(&self, pubkeyhex: PublicKeyHex) -> Result<(), Error> { let maybe_db = GLOBALS.db.lock().await; let db = maybe_db.as_ref().unwrap(); @@ -735,7 +782,7 @@ impl People { async fn fetch(criteria: Option<&str>) -> Result, Error> { let sql = "SELECT pubkey, metadata, metadata_at, \ nip05_valid, nip05_last_checked, \ - followed, followed_last_updated FROM person" + followed, followed_last_updated, muted FROM person" .to_owned(); let sql = match criteria { None => sql, @@ -763,6 +810,7 @@ impl People { nip05_last_checked: row.get(4)?, followed: row.get(5)?, followed_last_updated: row.get(6)?, + muted: row.get(7)?, }); } Ok(output) @@ -786,7 +834,7 @@ impl People { let sql = format!( "SELECT pubkey, metadata, metadata_at, \ nip05_valid, nip05_last_checked, \ - followed, followed_last_updated \ + followed, followed_last_updated, muted \ FROM person WHERE pubkey IN ({})", repeat_vars(pubkeys.len()) ); @@ -821,6 +869,7 @@ impl People { nip05_last_checked: row.get(4)?, followed: row.get(5)?, followed_last_updated: row.get(6)?, + muted: row.get(7)?, }); } @@ -834,8 +883,8 @@ impl People { /* async fn insert(person: DbPerson) -> Result<(), Error> { let sql = "INSERT OR IGNORE INTO person (pubkey, metadata, metadata_at, \ - nip05_valid, nip05_last_checked, followed, followed_last_updated) \ - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"; + nip05_valid, nip05_last_checked, followed, followed_last_updated, muted) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"; task::spawn_blocking(move || { let maybe_db = GLOBALS.db.blocking_lock(); @@ -856,6 +905,7 @@ impl People { &person.nip05_last_checked, &person.followed, &person.followed_last_updated, + &person.muted, ))?; Ok::<(), Error>(()) }) diff --git a/src/ui/feed.rs b/src/ui/feed.rs index 87924c47..4ad95db6 100644 --- a/src/ui/feed.rs +++ b/src/ui/feed.rs @@ -273,6 +273,13 @@ fn render_post_maybe_fake( } let event = maybe_event.unwrap(); + // Do not render muted people, not even fake-renders offscreen + if let Some(person) = GLOBALS.people.get(&event.pubkey.into()) { + if person.muted > 0 { + return; + } + }; + let screen_rect = ctx.input().screen_rect; // Rect let pos2 = ui.next_widget_position(); @@ -503,6 +510,14 @@ fn render_post_actual( if ui.button("Dismiss").clicked() { GLOBALS.dismissed.blocking_write().push(event.id); } + if ui.button("Mute").clicked() { + GLOBALS.people.mute(&event.pubkey.into(), true); + } + if person.followed == 0 && ui.button("Follow").clicked() { + GLOBALS.people.follow(&event.pubkey.into(), true); + } else if ui.button("Unfollow").clicked() { + GLOBALS.people.follow(&event.pubkey.into(), false); + } if ui.button("Update Metadata").clicked() { let _ = GLOBALS .to_overlord diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 507bc5d3..605bfe4f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -59,6 +59,7 @@ enum Page { Feed(FeedKind), PeopleList, PeopleFollow, + PeopleMuted, Person(PublicKeyHex), YourKeys, YourMetadata, @@ -284,6 +285,7 @@ impl eframe::App for GossipUi { .add(SelectableLabel::new( self.page == Page::PeopleList || self.page == Page::PeopleFollow + || self.page == Page::PeopleMuted || matches!(self.page, Page::Person(_)), "People", )) @@ -350,7 +352,7 @@ impl eframe::App for GossipUi { egui::CentralPanel::default().show(ctx, |ui| match self.page { Page::Feed(_) => feed::update(self, ctx, frame, ui), - Page::PeopleList | Page::PeopleFollow | Page::Person(_) => { + Page::PeopleList | Page::PeopleFollow | Page::PeopleMuted | Page::Person(_) => { people::update(self, ctx, frame, ui) } Page::YourKeys | Page::YourMetadata => you::update(self, ctx, frame, ui), diff --git a/src/ui/people/mod.rs b/src/ui/people/mod.rs index 77e0e692..b79637d9 100644 --- a/src/ui/people/mod.rs +++ b/src/ui/people/mod.rs @@ -8,6 +8,7 @@ use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2}; use std::sync::atomic::Ordering; mod follow; +mod muted; mod person; pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { @@ -21,6 +22,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra ui.separator(); ui.selectable_value(&mut app.page, Page::PeopleFollow, "Follow Someone New"); ui.separator(); + ui.selectable_value(&mut app.page, Page::PeopleMuted, "Muted"); + ui.separator(); if let Some(person) = &maybe_person { ui.selectable_value( &mut app.page, @@ -106,6 +109,8 @@ pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Fra }); } else if app.page == Page::PeopleFollow { follow::update(app, ctx, _frame, ui); + } else if app.page == Page::PeopleMuted { + muted::update(app, ctx, _frame, ui); } else if matches!(app.page, Page::Person(_)) { person::update(app, ctx, _frame, ui); } diff --git a/src/ui/people/muted.rs b/src/ui/people/muted.rs new file mode 100644 index 00000000..5a1efa3f --- /dev/null +++ b/src/ui/people/muted.rs @@ -0,0 +1,60 @@ +use super::{GossipUi, Page}; +use crate::globals::GLOBALS; +use crate::people::DbPerson; +use crate::AVATAR_SIZE_F32; +use eframe::egui; +use egui::{Context, Image, RichText, ScrollArea, Sense, Ui, Vec2}; +use std::sync::atomic::Ordering; + +pub(super) fn update(app: &mut GossipUi, ctx: &Context, _frame: &mut eframe::Frame, ui: &mut Ui) { + ui.add_space(30.0); + + let people: Vec = GLOBALS + .people + .get_all() + .drain(..) + .filter(|p| p.muted == 1) + .collect(); + + ui.heading(format!("People who are Muted ({})", people.len())); + ui.add_space(10.0); + + ScrollArea::vertical().show(ui, |ui| { + for person in people.iter() { + if person.muted != 1 { + continue; + } + + ui.horizontal(|ui| { + // Avatar first + let avatar = if let Some(avatar) = app.try_get_avatar(ctx, &person.pubkey) { + avatar + } else { + app.placeholder_avatar.clone() + }; + let size = AVATAR_SIZE_F32 + * GLOBALS.pixels_per_point_times_100.load(Ordering::Relaxed) as f32 + / 100.0; + if ui + .add(Image::new(&avatar, Vec2 { x: size, y: size }).sense(Sense::click())) + .clicked() + { + app.set_page(Page::Person(person.pubkey.clone())); + }; + + ui.vertical(|ui| { + ui.label(RichText::new(GossipUi::hex_pubkey_short(&person.pubkey)).weak()); + GossipUi::render_person_name_line(ui, person); + + if ui.button("UNMUTE").clicked() { + GLOBALS.people.mute(&person.pubkey, false); + } + }); + }); + + ui.add_space(4.0); + + ui.separator(); + } + }); +} diff --git a/src/ui/people/person.rs b/src/ui/people/person.rs index 579dd07e..adfe8212 100644 --- a/src/ui/people/person.rs +++ b/src/ui/people/person.rs @@ -60,15 +60,16 @@ fn content( ui.label(RichText::new(GossipUi::hex_pubkey_short(&pubkeyhex)).weak()); GossipUi::render_person_name_line(ui, &person); - #[allow(clippy::collapsible_else_if)] - if person.followed == 0 { - if ui.button("FOLLOW").clicked() { - GLOBALS.people.follow(&pubkeyhex, true); - } - } else { - if ui.button("UNFOLLOW").clicked() { - GLOBALS.people.follow(&pubkeyhex, false); - } + if person.followed == 0 && ui.button("FOLLOW").clicked() { + GLOBALS.people.follow(&pubkeyhex, true); + } else if person.followed == 1 && ui.button("UNFOLLOW").clicked() { + GLOBALS.people.follow(&pubkeyhex, false); + } + + if person.muted == 0 && ui.button("MUTE").clicked() { + GLOBALS.people.mute(&pubkeyhex, true); + } else if person.muted == 1 && ui.button("UNMUTE").clicked() { + GLOBALS.people.mute(&pubkeyhex, false); } if ui.button("UPDATE METADATA").clicked() {