use itertools::Itertools;
use regex::Regex;
+use std::cell::RefCell;
use std::cmp::{min, max};
use std::collections::{HashMap, HashSet, hash_map};
+use std::rc::Rc;
use super::activity_stack::{
NonUtilityActivity, UtilityActivity, OverlayActivity,
StatusInfo,
Favourite,
Boost,
+ Unfold,
Thread,
Reply,
Vote,
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SearchDirection { Up, Down }
-#[derive(Default)]
struct FileDisplayStyles {
selected_poll_id: Option<String>,
selected_poll_options: HashSet<usize>,
+ unfolded: Option<Rc<RefCell<HashSet<String>>>>,
}
+
+impl FileDisplayStyles {
+ fn new(unfolded: Option<Rc<RefCell<HashSet<String>>>>) -> Self {
+ FileDisplayStyles {
+ selected_poll_id: None,
+ selected_poll_options: HashSet::new(),
+ unfolded,
+ }
+ }
+}
+
impl DisplayStyleGetter for FileDisplayStyles {
fn poll_options(&self, id: &str) -> Option<HashSet<usize>> {
self.selected_poll_id.as_ref().and_then(
)
}
- fn unfolded(&self, _id: &str) -> bool { true }
+ fn unfolded(&self, id: &str) -> bool {
+ self.unfolded.as_ref().is_some_and(|set| set.borrow().contains(id))
+ }
}
struct File<Type: FileType, Source: FileDataSource> {
impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
fn new(client: &mut Client, source: Source, desc: ColouredString,
file_desc: Type, saved_pos: Option<&SavedFilePos>,
- show_new: bool) ->
+ unfolded: Option<Rc<RefCell<HashSet<String>>>>, show_new: bool) ->
Result<Self, ClientError>
{
source.init(client)?;
selection: None,
selection_restrict_to_item: None,
select_aux: None,
- display_styles: FileDisplayStyles::default(),
+ display_styles: FileDisplayStyles::new(unfolded),
file_desc,
search_direction: None,
last_search: None,
Err(_) => LogicalAction::Beep,
}
}
+ SelectionPurpose::Unfold => {
+ let did_something = if let Some(ref rc) =
+ self.display_styles.unfolded
+ {
+ let mut unfolded = rc.borrow_mut();
+ if !unfolded.remove(&id) {
+ unfolded.insert(id);
+ }
+ true
+ } else {
+ false
+ };
+
+ if did_something {
+ self.rendered.clear();
+ self.clip_pos_within_item();
+ }
+ LogicalAction::Nothing
+ }
SelectionPurpose::Reply => LogicalAction::Goto(
UtilityActivity::ComposeReply(id).into()),
SelectionPurpose::Thread => LogicalAction::Goto(
} else {
fs
};
+ let fs = if Type::Item::can_highlight(HighlightType::Status) &&
+ self.display_styles.unfolded.is_some()
+ {
+ fs.add(Pr('u'), "Unfold", 39)
+ } else {
+ fs
+ };
let fs = if Type::Item::can_highlight(
HighlightType::WholeStatus)
{
.add(Ctrl('V'), "Submit Vote", 98)
}
}
+ SelectionPurpose::Unfold => {
+ let unfolded = self.contents.id_at_index(item)
+ .map_or(false, |id| self.display_styles.unfolded(id));
+ let verb = if unfolded { "Fold" } else { "Unfold" };
+ fs.add(Pr('U'), verb, 98)
+ }
};
fs.add(Pr('+'), "Down", 99)
.add(Pr('-'), "Up", 99)
}
}
+ Pr('u') | Pr('U') => {
+ if Type::Item::can_highlight(HighlightType::FoldableStatus)
+ && self.display_styles.unfolded.is_some()
+ {
+ self.start_selection(HighlightType::FoldableStatus,
+ SelectionPurpose::Unfold,
+ client)
+ } else {
+ LogicalAction::Nothing
+ }
+ }
+
Pr('f') | Pr('F') => {
if Type::Item::can_highlight(HighlightType::WholeStatus) {
self.start_selection(HighlightType::WholeStatus,
}
pub fn home_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
+ unfolded: Rc<RefCell<HashSet<String>>>,
client: &mut Client) ->
Result<Box<dyn ActivityState>, ClientError>
{
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Home timeline <H>",
- "HHHHHHHHHHHHHHHHHKH"), desc, pos, false)?;
+ "HHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
Ok(Box::new(file))
}
pub fn local_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
+ unfolded: Rc<RefCell<HashSet<String>>>,
client: &mut Client) ->
Result<Box<dyn ActivityState>, ClientError>
{
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Local public timeline <L>",
- "HHHHHHHHHHHHHHHHHHHHHHHHHKH"), desc, pos, false)?;
+ "HHHHHHHHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
Ok(Box::new(file))
}
pub fn public_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
+ unfolded: Rc<RefCell<HashSet<String>>>,
client: &mut Client) ->
Result<Box<dyn ActivityState>, ClientError>
{
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Public timeline <P>",
- "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, false)?;
+ "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
Ok(Box::new(file))
}
pub fn mentions(file_positions: &HashMap<FeedId, SavedFilePos>,
+ unfolded: Rc<RefCell<HashSet<String>>>,
client: &mut Client, is_interrupt: bool) ->
Result<Box<dyn ActivityState>, ClientError>
{
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Mentions [ESC][R]",
- "HHHHHHHHHHHHKKKHHKH"), desc, pos, is_interrupt)?;
+ "HHHHHHHHHHHHKKKHHKH"), desc, pos, Some(unfolded), is_interrupt)?;
Ok(Box::new(file))
}
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Ego Log [ESC][L][L][E]",
- "HHHHHHHHHHHKKKHHKHHKHHKH"), desc, pos, false)?;
+ "HHHHHHHHHHHKKKHHKHHKHHKH"), desc, pos, None, false)?;
Ok(Box::new(file))
}
pub fn user_posts(
- file_positions: &HashMap<FeedId, SavedFilePos>, client: &mut Client,
- user: &str, boosts: Boosts, replies: Replies)
+ file_positions: &HashMap<FeedId, SavedFilePos>,
+ unfolded: Rc<RefCell<HashSet<String>>>,
+ client: &mut Client, user: &str, boosts: Boosts, replies: Replies)
-> Result<Box<dyn ActivityState>, ClientError>
{
let feed = FeedId::User(user.to_owned(), boosts, replies);
let file = File::new(
client, FeedSource::new(feed), ColouredString::general(
"Public timeline <P>",
- "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, false)?;
+ "HHHHHHHHHHHHHHHHHHHKH"), desc, pos, Some(unfolded), false)?;
Ok(Box::new(file))
}
client, FeedSource::new(FeedId::Favouriters(id.to_owned())),
ColouredString::uniform(
&format!("Users who favourited post {id}"), 'H'),
- UserListFeedType{}, None, false)?;
+ UserListFeedType{}, None, None, false)?;
Ok(Box::new(file))
}
client, FeedSource::new(FeedId::Boosters(id.to_owned())),
ColouredString::uniform(
&format!("Users who boosted post {id}"), 'H'),
- UserListFeedType{}, None, false)?;
+ UserListFeedType{}, None, None, false)?;
Ok(Box::new(file))
}
client, FeedSource::new(FeedId::Followers(id.to_owned())),
ColouredString::uniform(
&format!("Users who follow {name}"), 'H'),
- UserListFeedType{}, None, false)?;
+ UserListFeedType{}, None, None, false)?;
Ok(Box::new(file))
}
client, FeedSource::new(FeedId::Followees(id.to_owned())),
ColouredString::uniform(
&format!("Users who {name} follows"), 'H'),
- UserListFeedType{}, None, false)?;
+ UserListFeedType{}, None, None, false)?;
Ok(Box::new(file))
}
-pub fn hashtag_timeline(client: &mut Client, tag: &str) ->
+pub fn hashtag_timeline(unfolded: Rc<RefCell<HashSet<String>>>,
+ client: &mut Client, tag: &str) ->
Result<Box<dyn ActivityState>, ClientError>
{
let title = ColouredString::uniform(
let desc = StatusFeedType::with_feed(feed);
let file = File::new(
client, FeedSource::new(FeedId::Hashtag(tag.to_owned())), title,
- desc, None, false)?;
+ desc, None, Some(unfolded), false)?;
Ok(Box::new(file))
}
let file = File::new(
client, StaticSource::singleton(ac.id), title, ExamineUserFileType{},
- Some(&FilePosition::item_top(isize::MIN).into()), false)?;
+ Some(&FilePosition::item_top(isize::MIN).into()), None, false)?;
Ok(Box::new(file))
}
}
}
-pub fn view_single_post(client: &mut Client, status_id: &str) ->
+pub fn view_single_post(unfolded: Rc<RefCell<HashSet<String>>>,
+ client: &mut Client, status_id: &str) ->
Result<Box<dyn ActivityState>, ClientError>
{
let st = client.status_by_id(status_id)?;
let file = File::new(
client, StaticSource::singleton(st.id), title,
DetailedStatusFileType{},
- Some(&FilePosition::item_top(isize::MIN).into()), false)?;
+ Some(&FilePosition::item_top(isize::MIN).into()),
+ Some(unfolded), false)?;
Ok(Box::new(file))
}
-pub fn view_thread(client: &mut Client, start_id: &str, full: bool) ->
+pub fn view_thread(unfolded: Rc<RefCell<HashSet<String>>>,
+ client: &mut Client, start_id: &str, full: bool) ->
Result<Box<dyn ActivityState>, ClientError>
{
let mut make_vec = |id: &str| -> Result<Vec<String>, ClientError> {
let file = File::new(
client, StaticSource::vector(ids), title,
StatusFeedType::without_feed(),
- Some(&FilePosition::item_top(index).into()), false)?;
+ Some(&FilePosition::item_top(index).into()), Some(unfolded), false)?;
Ok(Box::new(file))
}
use super::coloured_string::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
-pub enum HighlightType { User, Status, WholeStatus, PollOption }
+pub enum HighlightType {
+ User,
+ Status,
+ WholeStatus,
+ FoldableStatus,
+ PollOption,
+}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Highlight(pub HighlightType, pub usize);
}
impl TextFragment for InReplyToLine {
- fn render_highlighted(&self, width: usize, _highlight: Option<Highlight>,
+ fn render_highlighted(&self, width: usize, highlight: Option<Highlight>,
style: &dyn DisplayStyleGetter)
-> Vec<ColouredString>
{
+ let mut highlight = highlight;
+ let highlighting =
+ highlight.consume(HighlightType::FoldableStatus, 1) == Some(0);
let which_para = match self.warning.as_ref() {
Some(folded) => if !style.unfolded(&self.id) {
folded
} else {
first_line.clone()
};
- vec! { result.truncate(width).into() }
+ let result = result.truncate(width);
+ let result = if highlighting {
+ result.recolour('*')
+ } else {
+ result.into()
+ };
+ vec! { result }
+ }
+
+ fn can_highlight(htype: HighlightType) -> bool where Self : Sized {
+ htype == HighlightType::FoldableStatus
+ }
+
+ fn count_highlightables(&self, htype: HighlightType) -> usize {
+ match htype {
+ HighlightType::FoldableStatus => {
+ if self.warning.is_some() {
+ 1
+ } else {
+ 0
+ }
+ }
+ _ => 0,
+ }
+ }
+
+ fn highlighted_id(&self, highlight: Option<Highlight>) -> Option<String> {
+ match highlight {
+ Some(Highlight(HighlightType::FoldableStatus, 0)) => {
+ if self.warning.is_some() {
+ Some(self.id.clone())
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
}
}
let post = Html::new(
"<p><span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@stoat\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>stoat</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@weasel\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>weasel</span></a></span> take a look at this otter!</p><p><span class=\"h-card\" translate=\"no\"><a href=\"https://some.instance/@badger\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">@<span>badger</span></a></span> might also like it</p>");
- let irt = InReplyToLine::new(post.to_para());
+ let irt = InReplyToLine::new(post.to_para(), None, "123");
assert_eq!(irt.render(48), vec!{
ColouredString::general(
"Re: take a look at this otter! @badger might...",
let mut lines = Vec::new();
let mut highlight = highlight;
+ push_fragment(&mut lines, self.sep.render(width));
+ push_fragment(&mut lines, self.from.render_highlighted_update(
+ width, &mut highlight, style));
+ push_fragment(&mut lines, self.via.render_highlighted_update(
+ width, &mut highlight, style));
+ push_fragment(&mut lines, self.vis.render(width));
+ push_fragment(&mut lines, self.irt.render_highlighted_update(
+ width, &mut highlight, style));
+ push_fragment(&mut lines, self.blank.render(width));
+
let highlighting_this_status =
highlight.consume(HighlightType::Status, 1) == Some(0) ||
- highlight.consume(HighlightType::WholeStatus, 1) == Some(0);
+ highlight.consume(HighlightType::WholeStatus, 1) == Some(0) ||
+ (self.warning.is_some() &&
+ highlight.consume(HighlightType::FoldableStatus, 1) == Some(0));
let push_fragment_opt_highlight = if highlighting_this_status {
push_fragment_highlighted
} else {
push_fragment
};
- push_fragment(&mut lines, self.sep.render(width));
- push_fragment(&mut lines, self.from.render_highlighted_update(
- width, &mut highlight, &DefaultDisplayStyle));
- push_fragment(&mut lines, self.via.render_highlighted_update(
- width, &mut highlight, &DefaultDisplayStyle));
- push_fragment(&mut lines, self.vis.render(width));
- push_fragment(&mut lines, self.irt.render(width));
- push_fragment(&mut lines, self.blank.render(width));
-
let folded = self.warning.is_some() && !style.unfolded(&self.id);
if let Some(warning_text) = &self.warning {
let mut para = Paragraph::new();
htype == HighlightType::User ||
htype == HighlightType::Status ||
htype == HighlightType::WholeStatus ||
+ htype == HighlightType::FoldableStatus ||
htype == HighlightType::PollOption
}
}
HighlightType::Status => 1,
HighlightType::WholeStatus => 1,
+ HighlightType::FoldableStatus => {
+ self.irt.count_highlightables(htype) +
+ if self.warning.is_some() {1} else {0}
+ }
HighlightType::PollOption => self.poll.as_ref()
.filter(|poll| poll.eligible)
}
Some(Highlight(HighlightType::WholeStatus, 0)) |
Some(Highlight(HighlightType::Status, 0)) => Some(self.id.clone()),
+
+ Some(Highlight(HighlightType::FoldableStatus, _)) => {
+ let mut highlight = highlight;
+ if let result @ Some(..) = self.irt.highlighted_id_update(
+ &mut highlight)
+ {
+ return result;
+ }
+
+ if self.warning.is_some() &&
+ highlight.consume(HighlightType::FoldableStatus, 1) ==
+ Some(0)
+ {
+ return Some(self.id.clone());
+ }
+
+ None
+ }
+
Some(Highlight(HighlightType::PollOption, i)) => self.poll.as_ref()
.filter(|poll| poll.eligible)
.filter(|poll| i < poll.options.len())
prelude::{Buffer, CrosstermBackend, Rect, Terminal},
style::{Style, Color, Modifier},
};
+use std::cell::RefCell;
use std::cmp::min;
use std::collections::{BTreeMap, HashMap, HashSet, hash_map};
use std::io::{Stdout, Write, stdout};
use std::fs::File;
+use std::rc::Rc;
use std::time::Duration;
use unicode_width::UnicodeWidthStr;
overlay_activity_state: Option<Box<dyn ActivityState>>,
last_area: Option<Rect>,
file_positions: HashMap<FeedId, SavedFilePos>,
+ unfolded_posts: Rc<RefCell<HashSet<String>>>,
cfgloc: ConfigLocation,
}
last_area: None,
file_positions: HashMap::new(),
cfgloc,
+ unfolded_posts: Rc::new(RefCell::new(HashSet::new())),
}
}
Activity::Util(UtilityActivity::LogsMenu2) =>
Ok(logs_menu_2()),
Activity::NonUtil(NonUtilityActivity::HomeTimelineFile) =>
- home_timeline(&self.file_positions, client),
+ home_timeline(&self.file_positions,
+ self.unfolded_posts.clone(), client),
Activity::NonUtil(NonUtilityActivity::PublicTimelineFile) =>
- public_timeline(&self.file_positions, client),
+ public_timeline(&self.file_positions,
+ self.unfolded_posts.clone(), client),
Activity::NonUtil(NonUtilityActivity::LocalTimelineFile) =>
- local_timeline(&self.file_positions, client),
+ local_timeline(&self.file_positions,
+ self.unfolded_posts.clone(), client),
Activity::NonUtil(NonUtilityActivity::HashtagTimeline(ref id)) =>
- hashtag_timeline(client, id),
+ hashtag_timeline(self.unfolded_posts.clone(), client, id),
Activity::Util(UtilityActivity::ReadMentions) =>
- mentions(&self.file_positions, client, is_interrupt),
+ mentions(&self.file_positions, self.unfolded_posts.clone(),
+ client, is_interrupt),
Activity::Util(UtilityActivity::EgoLog) =>
ego_log(&self.file_positions, client),
Activity::Overlay(OverlayActivity::GetUserToExamine) =>
Activity::Util(UtilityActivity::ExamineUser(ref name)) =>
examine_user(client, name),
Activity::Util(UtilityActivity::InfoStatus(ref id)) =>
- view_single_post(client, id),
+ view_single_post(self.unfolded_posts.clone(), client, id),
Activity::NonUtil(NonUtilityActivity::ComposeToplevel) =>
compose_post(client, post.unwrap_or_else(Post::new)),
Activity::NonUtil(NonUtilityActivity::PostComposeMenu) =>
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),
+ view_thread(self.unfolded_posts.clone(), client, id, full),
Activity::Util(UtilityActivity::ListStatusFavouriters(ref id)) =>
list_status_favouriters(client, id),
Activity::Util(UtilityActivity::ListStatusBoosters(ref id)) =>
list_user_followees(client, id),
Activity::NonUtil(NonUtilityActivity::UserPosts(
ref user, boosts, replies)) =>
- user_posts(&self.file_positions, client, user, boosts, replies),
+ user_posts(&self.file_positions, self.unfolded_posts.clone(),
+ client, user, boosts, replies),
};
result.expect("FIXME: need to implement the Error Log here")