+use itertools::Itertools;
use std::cmp::{min, max};
use std::collections::{HashMap, HashSet};
fn try_extend(&self, client: &mut Client) -> Result<bool, ClientError>;
fn updated(&self, feeds_updated: &HashSet<FeedId>) -> bool;
fn extendable(&self) -> bool;
+
+ fn single_id(&self) -> String {
+ panic!("Should only call this if the FileType sets CAN_LIST");
+ }
}
struct FeedSource {
}
fn updated(&self, _feeds_updated: &HashSet<FeedId>) -> 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<Self::Item, ClientError>;
}
}
+struct UserListFeedType {}
+impl FileType for UserListFeedType {
+ type Item = UserListEntry;
+
+ fn get_from_client(id: &str, client: &mut Client) ->
+ Result<Self::Item, ClientError>
+ {
+ let ac = client.account_by_id(&id)?;
+ Ok(UserListEntry::from_account(&ac, client))
+ }
+}
+
struct FileContents<Type: FileType, Source: FileDataSource> {
source: Source,
header: FileHeader,
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum UIMode {
Normal,
+ ListSubmenu,
Select(HighlightType, SelectionPurpose),
}
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 {
} 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 {
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 =>
}
}
+ 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 {
Ok(Box::new(file))
}
+pub fn list_status_favouriters(client: &mut Client, id: &str) ->
+ Result<Box<dyn ActivityState>, ClientError>
+{
+ let file = File::<UserListFeedType, _>::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<Box<dyn ActivityState>, ClientError>
+{
+ let file = File::<UserListFeedType, _>::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<Box<dyn ActivityState>, ClientError>
+{
+ let ac = client.account_by_id(&id)?;
+ let name = client.fq(&ac.acct);
+
+ let file = File::<UserListFeedType, _>::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<Box<dyn ActivityState>, ClientError>
+{
+ let ac = client.account_by_id(&id)?;
+ let name = client.fq(&ac.acct);
+
+ let file = File::<UserListFeedType, _>::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<Box<dyn ActivityState>, ClientError>
{
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<Self::Item, ClientError>
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<Self::Item, ClientError>