From e8403d821b29beebc0e58efef39a77c85fa52993 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Thu, 4 Jan 2024 08:30:49 +0000 Subject: [PATCH] Implement the various user lists. Lists of users who followed or boosted a given post, or who a given user follows or is followed by. But something goes wrong when you try to extend these feeds. I'll debug that in a moment ... but first, let's write some debugging framework. --- src/client.rs | 29 +++++++++++ src/file.rs | 139 +++++++++++++++++++++++++++++++++++++++++++++++++- src/text.rs | 4 ++ src/tui.rs | 9 +++- 4 files changed, 179 insertions(+), 2 deletions(-) diff --git a/src/client.rs b/src/client.rs index 351a709..6128671 100644 --- a/src/client.rs +++ b/src/client.rs @@ -22,6 +22,10 @@ pub enum FeedId { User(String, Boosts, Replies), Mentions, Ego, + Favouriters(String), + Boosters(String), + Followers(String), + Followees(String), } #[derive(Debug, PartialEq, Eq, Clone)] @@ -521,6 +525,18 @@ impl Client { .param("types[]", "follow") .param("types[]", "favourite") } + FeedId::Favouriters(id) => { + Req::get(&format!("v1/statuses/{id}/favourited_by")) + } + FeedId::Boosters(id) => { + Req::get(&format!("v1/statuses/{id}/reblogged_by")) + } + FeedId::Followers(id) => { + Req::get(&format!("v1/accounts/{id}/followers")) + } + FeedId::Followees(id) => { + Req::get(&format!("v1/accounts/{id}/following")) + } }; let req = match ext { @@ -600,6 +616,19 @@ impl Client { } nots.iter().rev().map(|not| not.id.clone()).collect() } + FeedId::Favouriters(..) | FeedId::Boosters(..) | + FeedId::Followers(..) | FeedId::Followees(..) => { + let acs: Vec = match serde_json::from_str(&body) { + Ok(acs) => Ok(acs), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } + }?; + for ac in &acs { + self.cache_account(ac); + } + acs.iter().rev().map(|ac| ac.id.clone()).collect() + } }; let any_new = !ids.is_empty(); diff --git a/src/file.rs b/src/file.rs index 2b386d5..7c13dc2 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,3 +1,4 @@ +use itertools::Itertools; use std::cmp::{min, max}; use std::collections::{HashMap, HashSet}; @@ -31,6 +32,10 @@ trait FileDataSource { fn try_extend(&self, client: &mut Client) -> Result; fn updated(&self, feeds_updated: &HashSet) -> bool; fn extendable(&self) -> bool; + + fn single_id(&self) -> String { + panic!("Should only call this if the FileType sets CAN_LIST"); + } } struct FeedSource { @@ -83,10 +88,21 @@ impl FileDataSource for StaticSource { } fn updated(&self, _feeds_updated: &HashSet) -> bool { false } fn extendable(&self) -> bool { false } + fn single_id(&self) -> String { + self.ids.iter().exactly_one() + .expect("Should only call this on singleton StaticSources") + .to_owned() + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum CanList { + Nothing, ForPost, ForUser, } trait FileType { type Item: TextFragment + Sized; + const CAN_LIST: CanList = CanList::Nothing; fn get_from_client(id: &str, client: &mut Client) -> Result; @@ -130,6 +146,18 @@ impl FileType for EgoNotificationFeedType { } } +struct UserListFeedType {} +impl FileType for UserListFeedType { + type Item = UserListEntry; + + fn get_from_client(id: &str, client: &mut Client) -> + Result + { + let ac = client.account_by_id(&id)?; + Ok(UserListEntry::from_account(&ac, client)) + } +} + struct FileContents { source: Source, header: FileHeader, @@ -203,6 +231,7 @@ enum SelectionPurpose { #[derive(Debug, PartialEq, Eq, Clone, Copy)] enum UIMode { Normal, + ListSubmenu, Select(HighlightType, SelectionPurpose), } @@ -260,7 +289,7 @@ impl File { let mut lines = Vec::new(); let highlight = match self.ui_mode { - UIMode::Normal => None, + UIMode::Normal | UIMode::ListSubmenu => None, UIMode::Select(htype, _purpose) => match self.selection { None => None, Some((item, sub)) => if item == index { @@ -783,6 +812,11 @@ impl } else { fs }; + let fs = if Type::CAN_LIST != CanList::Nothing { + fs.add(Pr('l'), "List", 40) + } else { + fs + }; let fs = if Type::Item::can_highlight(HighlightType::Status) { fs.add(Pr('i'), "Post Info", 38) } else { @@ -822,6 +856,19 @@ impl full_items * mult + start_line, total_items * mult) } + UIMode::ListSubmenu => { + let fs = match Type::CAN_LIST { + CanList::ForUser => fs + .add(Pr('I'), "List Followers", 99) + .add(Pr('O'), "List Followed", 99), + CanList::ForPost => fs + .add(Pr('F'), "List Favouriters", 99) + .add(Pr('B'), "List Boosters", 99), + CanList::Nothing => + panic!("Then we shouldn't be in this submenu"), + }; + fs.add(Pr('Q'), "Quit", 100) + } UIMode::Select(_htype, purpose) => { let fs = match purpose { SelectionPurpose::ExamineUser => @@ -974,6 +1021,48 @@ impl } } + Pr('l') | Pr('L') => { + if Type::CAN_LIST != CanList::Nothing { + self.ui_mode = UIMode::ListSubmenu; + } + LogicalAction::Nothing + } + + _ => LogicalAction::Nothing, + } + UIMode::ListSubmenu => match key { + Pr('f') | Pr('F') => if Type::CAN_LIST == CanList::ForPost { + LogicalAction::Goto(UtilityActivity::ListStatusFavouriters( + self.contents.source.single_id()).into()) + } else { + LogicalAction::Nothing + } + + Pr('b') | Pr('B') => if Type::CAN_LIST == CanList::ForPost { + LogicalAction::Goto(UtilityActivity::ListStatusBoosters( + self.contents.source.single_id()).into()) + } else { + LogicalAction::Nothing + } + + Pr('i') | Pr('I') => if Type::CAN_LIST == CanList::ForUser { + LogicalAction::Goto(UtilityActivity::ListUserFollowers( + self.contents.source.single_id()).into()) + } else { + LogicalAction::Nothing + } + + Pr('o') | Pr('O') => if Type::CAN_LIST == CanList::ForUser { + LogicalAction::Goto(UtilityActivity::ListUserFollowees( + self.contents.source.single_id()).into()) + } else { + LogicalAction::Nothing + } + + Pr('q') | Pr('Q') => { + self.ui_mode = UIMode::Normal; + LogicalAction::Nothing + } _ => LogicalAction::Nothing, } UIMode::Select(_, purpose) => match key { @@ -1061,6 +1150,52 @@ pub fn ego_log(client: &mut Client) -> Ok(Box::new(file)) } +pub fn list_status_favouriters(client: &mut Client, id: &str) -> + Result, ClientError> +{ + let file = File::::new( + client, FeedSource::new(FeedId::Favouriters(id.to_owned())), + ColouredString::uniform( + &format!("Users who favourited post {id}"), 'H'))?; + Ok(Box::new(file)) +} + +pub fn list_status_boosters(client: &mut Client, id: &str) -> + Result, ClientError> +{ + let file = File::::new( + client, FeedSource::new(FeedId::Boosters(id.to_owned())), + ColouredString::uniform( + &format!("Users who boosted post {id}"), 'H'))?; + Ok(Box::new(file)) +} + +pub fn list_user_followers(client: &mut Client, id: &str) -> + Result, ClientError> +{ + let ac = client.account_by_id(&id)?; + let name = client.fq(&ac.acct); + + let file = File::::new( + client, FeedSource::new(FeedId::Followers(id.to_owned())), + ColouredString::uniform( + &format!("Users who follow {name}"), 'H'))?; + Ok(Box::new(file)) +} + +pub fn list_user_followees(client: &mut Client, id: &str) -> + Result, ClientError> +{ + let ac = client.account_by_id(&id)?; + let name = client.fq(&ac.acct); + + let file = File::::new( + client, FeedSource::new(FeedId::Followees(id.to_owned())), + ColouredString::uniform( + &format!("Users who {name} follows"), 'H'))?; + Ok(Box::new(file)) +} + pub fn hashtag_timeline(client: &mut Client, tag: &str) -> Result, ClientError> { @@ -1074,6 +1209,7 @@ pub fn hashtag_timeline(client: &mut Client, tag: &str) -> struct ExamineUserFileType {} impl FileType for ExamineUserFileType { type Item = ExamineUserDisplay; + const CAN_LIST: CanList = CanList::ForUser; fn get_from_client(id: &str, client: &mut Client) -> Result @@ -1099,6 +1235,7 @@ pub fn examine_user(client: &mut Client, account_id: &str) -> struct DetailedStatusFileType {} impl FileType for DetailedStatusFileType { type Item = DetailedStatusDisplay; + const CAN_LIST: CanList = CanList::ForPost; fn get_from_client(id: &str, client: &mut Client) -> Result diff --git a/src/text.rs b/src/text.rs index 0c4019c..9a6e5c2 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1141,6 +1141,10 @@ impl UserListEntry { account_desc: format!("{} ({})", nameline, account), } } + + pub fn from_account(ac: &Account, client: &mut Client) -> Self { + Self::new(&client.fq(&ac.acct), &ac.display_name) + } } impl TextFragment for UserListEntry { diff --git a/src/tui.rs b/src/tui.rs index 943c450..e6756d3 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -475,7 +475,14 @@ fn new_activity_state(activity: Activity, client: &mut Client, Ok(post_menu(post.expect("how did we get here without a Post?"))), Activity::Util(UtilityActivity::ThreadFile(ref id, full)) => view_thread(client, id, full), - _ => todo!(), + Activity::Util(UtilityActivity::ListStatusFavouriters(ref id)) => + list_status_favouriters(client, id), + Activity::Util(UtilityActivity::ListStatusBoosters(ref id)) => + list_status_boosters(client, id), + Activity::Util(UtilityActivity::ListUserFollowers(ref id)) => + list_user_followers(client, id), + Activity::Util(UtilityActivity::ListUserFollowees(ref id)) => + list_user_followees(client, id), }; result.expect("FIXME: need to implement the Error Log here") -- 2.30.2