chiark / gitweb /
Support unfolding and refolding of sensitive toots.
authorSimon Tatham <anakin@pobox.com>
Wed, 10 Jan 2024 08:41:48 +0000 (08:41 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 11 Jan 2024 08:14:20 +0000 (08:14 +0000)
There's a new highlight type for this, so that it can apply to
InReplyTo lines as well as statuses themselves.

The set of unfolded sensitive statuses needs to be shared between lots
of views of the data, so I've put it in the TuiLogicalState, under a
RefCell so that ActivityStates can all borrow it easily, not just to
look up in but also to modify.

TODO.md
src/file.rs
src/text.rs
src/tui.rs

diff --git a/TODO.md b/TODO.md
index 92386e66c89f101dd9944fb86e0b793d0ca571e4..404f03cc590392ec320c32ed47bc9d99463e8918 100644 (file)
--- a/TODO.md
+++ b/TODO.md
@@ -40,12 +40,6 @@ way Mono did it.
 
 ## Reading
 
-### Sensitive-content markers
-
-Currently we display sensitive-content tags, but then display the rest
-of the post anyway. It would be better to (at least have the option
-to) hide the post and then be able to show it on demand.
-
 ### Scrolling to keep the selection in view
 
 Various keyboard commands let you select a user or a post from a file
index 5b0e5e1f5840a858e91b0ad8d7093336d7b0dece..4928b9a7b0b6d4d353c371f323c63abfb7755e71 100644 (file)
@@ -1,7 +1,9 @@
 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,
@@ -296,6 +298,7 @@ enum SelectionPurpose {
     StatusInfo,
     Favourite,
     Boost,
+    Unfold,
     Thread,
     Reply,
     Vote,
@@ -312,11 +315,22 @@ enum UIMode {
 #[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(
@@ -328,7 +342,9 @@ impl DisplayStyleGetter for FileDisplayStyles {
         )
     }
 
-    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> {
@@ -350,7 +366,7 @@ 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)?;
@@ -413,7 +429,7 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             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,
@@ -896,6 +912,25 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
                         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(
@@ -1056,6 +1091,13 @@ impl<Type: FileType, Source: FileDataSource>
                 } 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)
                 {
@@ -1164,6 +1206,12 @@ impl<Type: FileType, Source: FileDataSource>
                                 .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)
@@ -1257,6 +1305,18 @@ impl<Type: FileType, Source: FileDataSource>
                     }
                 }
 
+                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,
@@ -1465,6 +1525,7 @@ impl<Type: FileType, Source: FileDataSource>
 }
 
 pub fn home_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
+                     unfolded: Rc<RefCell<HashSet<String>>>,
                      client: &mut Client) ->
     Result<Box<dyn ActivityState>, ClientError>
 {
@@ -1474,11 +1535,12 @@ pub fn home_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
     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>
 {
@@ -1488,11 +1550,12 @@ pub fn local_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
     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>
 {
@@ -1502,11 +1565,12 @@ pub fn public_timeline(file_positions: &HashMap<FeedId, SavedFilePos>,
     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>
 {
@@ -1516,7 +1580,7 @@ pub fn mentions(file_positions: &HashMap<FeedId, SavedFilePos>,
     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))
 }
 
@@ -1530,13 +1594,14 @@ pub fn ego_log(file_positions: &HashMap<FeedId, SavedFilePos>,
     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);
@@ -1545,7 +1610,7 @@ pub fn user_posts(
     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))
 }
 
@@ -1556,7 +1621,7 @@ pub fn list_status_favouriters(client: &mut Client, id: &str) ->
         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))
 }
 
@@ -1567,7 +1632,7 @@ pub fn list_status_boosters(client: &mut Client, id: &str) ->
         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))
 }
 
@@ -1581,7 +1646,7 @@ pub fn list_user_followers(client: &mut Client, id: &str) ->
         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))
 }
 
@@ -1595,11 +1660,12 @@ pub fn list_user_followees(client: &mut Client, id: &str) ->
         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(
@@ -1608,7 +1674,7 @@ pub fn hashtag_timeline(client: &mut Client, tag: &str) ->
     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))
 }
 
@@ -1636,7 +1702,7 @@ pub fn examine_user(client: &mut Client, account_id: &str) ->
 
     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))
 }
 
@@ -1653,7 +1719,8 @@ impl FileType for DetailedStatusFileType {
     }
 }
 
-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)?;
@@ -1663,11 +1730,13 @@ pub fn view_single_post(client: &mut Client, status_id: &str) ->
     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> {
@@ -1705,6 +1774,6 @@ pub fn view_thread(client: &mut Client, start_id: &str, full: bool) ->
     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))
 }
index 845914d774ff515c4784485f436d6d013306d8f7..6833f452694e12d74d35d4569c38f1bf44a0e1a6 100644 (file)
@@ -12,7 +12,13 @@ use super::tui::OurKey;
 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);
@@ -986,10 +992,13 @@ impl InReplyToLine {
 }
 
 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
@@ -1008,7 +1017,43 @@ impl TextFragment for InReplyToLine {
         } 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,
+        }
     }
 }
 
@@ -1017,7 +1062,7 @@ fn test_in_reply_to() {
     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...",
@@ -1931,24 +1976,27 @@ impl TextFragment for StatusDisplay {
         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();
@@ -2007,6 +2055,7 @@ impl TextFragment for StatusDisplay {
         htype == HighlightType::User ||
             htype == HighlightType::Status ||
             htype == HighlightType::WholeStatus ||
+            htype == HighlightType::FoldableStatus ||
             htype == HighlightType::PollOption
     }
 
@@ -2018,6 +2067,10 @@ impl TextFragment for StatusDisplay {
             }
             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)
@@ -2043,6 +2096,25 @@ impl TextFragment for StatusDisplay {
             }
             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())
index a79c7fd82d97d4cceabd0d7f68821f837fd4f51d..e1de89247962a62b94db1fb6cc68a5c9894c8009 100644 (file)
@@ -10,10 +10,12 @@ use ratatui::{
     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;
 
@@ -514,6 +516,7 @@ struct TuiLogicalState {
     overlay_activity_state: Option<Box<dyn ActivityState>>,
     last_area: Option<Rect>,
     file_positions: HashMap<FeedId, SavedFilePos>,
+    unfolded_posts: Rc<RefCell<HashSet<String>>>,
     cfgloc: ConfigLocation,
 }
 
@@ -529,6 +532,7 @@ impl TuiLogicalState {
             last_area: None,
             file_positions: HashMap::new(),
             cfgloc,
+            unfolded_posts: Rc::new(RefCell::new(HashSet::new())),
         }
     }
 
@@ -772,15 +776,19 @@ impl TuiLogicalState {
             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) =>
@@ -794,7 +802,7 @@ impl TuiLogicalState {
             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) =>
@@ -814,7 +822,7 @@ impl TuiLogicalState {
                 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)) =>
@@ -825,7 +833,8 @@ impl TuiLogicalState {
                 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")