From: Simon Tatham Date: Sun, 7 Jan 2024 22:17:27 +0000 (+0000) Subject: Add support for polls in other users' posts. X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=17b700af32d91352c38ba6a043a76cd03c8cb548;p=mastodonochrome.git Add support for polls in other users' posts. That is, we can't post them ourselves yet, but we can see them when other users post them, and vote in them. --- diff --git a/TODO.md b/TODO.md index 4e3b708..f818640 100644 --- a/TODO.md +++ b/TODO.md @@ -47,13 +47,6 @@ not even make sense without. Probably better to go all the way, and actually hide the post until told otherwise. -### Polls - -If someone posts a toot containing a poll, we should show it, and -offer an option to vote in it. - -An expired poll should show its results. - ### Scrolling to keep the selection in view Various keyboard commands let you select a user or a post from a file diff --git a/src/client.rs b/src/client.rs index 0e16783..7f0110c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -62,6 +62,7 @@ pub struct Client { accounts: HashMap, statuses: HashMap, notifications: HashMap, + polls: HashMap, feeds: HashMap, instance: Option, permit_write: bool, @@ -186,6 +187,9 @@ impl ReqParam for &String { impl ReqParam for i32 { fn param_value(self) -> String { self.to_string() } } +impl ReqParam for usize { + fn param_value(self) -> String { self.to_string() } +} impl ReqParam for bool { fn param_value(self) -> String { match self { @@ -353,6 +357,7 @@ impl Client { accounts: HashMap::new(), statuses: HashMap::new(), notifications: HashMap::new(), + polls: HashMap::new(), feeds: HashMap::new(), instance: None, permit_write: false, @@ -437,6 +442,9 @@ impl Client { fn cache_status(&mut self, st: &Status) { self.cache_account(&st.account); + if let Some(poll) = &st.poll { + self.cache_poll(poll); + } self.statuses.insert(st.id.to_string(), st.clone()); if let Some(ref sub) = st.reblog { self.statuses.insert(sub.id.to_string(), *sub.clone()); @@ -451,6 +459,10 @@ impl Client { self.notifications.insert(n.id.to_string(), n.clone()); } + fn cache_poll(&mut self, poll: &Poll) { + self.polls.insert(poll.id.to_string(), poll.clone()); + } + pub fn account_by_id(&mut self, id: &str) -> Result { if let Some(st) = self.accounts.get(id) { return Ok(st.clone()); @@ -477,6 +489,31 @@ impl Client { Ok(ac) } + pub fn poll_by_id(&mut self, id: &str) -> Result { + if let Some(st) = self.polls.get(id) { + return Ok(st.clone()); + } + + let (url, rsp) = self.api_request(Req::get( + &("v1/polls/".to_owned() + id)))?; + let rspstatus = rsp.status(); + let poll: Poll = if !rspstatus.is_success() { + Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) + } else { + match serde_json::from_str(&rsp.text()?) { + Ok(poll) => Ok(poll), + Err(e) => Err(ClientError::UrlError( + url.clone(), e.to_string())), + } + }?; + if poll.id != id { + return Err(ClientError::UrlError( + url, format!("request returned wrong poll id {}", &poll.id))); + } + self.cache_poll(&poll); + Ok(poll) + } + pub fn account_relationship_by_id(&mut self, id: &str) -> Result { @@ -510,6 +547,11 @@ impl Client { // we had cached st.account = ac.clone(); } + if let Some(poll) = st.poll.as_ref().and_then( + |poll| self.polls.get(&poll.id)) { + // Ditto with the poll, if any + st.poll = Some(poll.clone()); + } return Ok(st); } @@ -1085,4 +1127,30 @@ impl Client { } Ok(ctx) } + + pub fn vote_in_poll(&mut self, id: &str, + choices: impl Iterator) + -> Result<(), ClientError> + { + let choices: Vec<_> = choices.collect(); + let mut req = Req::post(&format!("v1/polls/{id}/votes")); + for choice in choices { + req = req.param("choices[]", choice); + } + let (url, rsp) = self.api_request(req)?; + let rspstatus = rsp.status(); + // Cache the returned poll so as to update its faved/boosted flags + let poll: Poll = if !rspstatus.is_success() { + Err(ClientError::UrlError(url, rspstatus.to_string())) + } else { + match serde_json::from_str(&rsp.text()?) { + Ok(poll) => Ok(poll), + Err(e) => { + Err(ClientError::UrlError(url, e.to_string())) + } + } + }?; + self.cache_poll(&poll); + Ok(()) + } } diff --git a/src/file.rs b/src/file.rs index f0458c3..ffc1c2b 100644 --- a/src/file.rs +++ b/src/file.rs @@ -298,6 +298,7 @@ enum SelectionPurpose { Boost, Thread, Reply, + Vote, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -318,7 +319,9 @@ struct File { last_size: Option<(usize, usize)>, ui_mode: UIMode, selection: Option<(isize, usize)>, + selection_restrict_to_item: Option, select_aux: Option, // distinguishes fave from unfave, etc + poll_options_selected: HashSet, file_desc: Type, search_direction: Option, last_search: Option, @@ -389,7 +392,9 @@ impl File { last_size: None, ui_mode: UIMode::Normal, selection: None, + selection_restrict_to_item: None, select_aux: None, + poll_options_selected: HashSet::new(), file_desc, search_direction: None, last_search: None, @@ -416,8 +421,15 @@ impl File { _ => None, }; + let poll_options = + if self.selection_restrict_to_item == Some(index) { + Some(&self.poll_options_selected) + } else { + None + }; + for line in self.contents.get(index) - .render_highlighted(w, highlight) + .render_highlighted(w, highlight, poll_options) { for frag in line.split(w) { lines.push(frag.to_owned()); @@ -676,6 +688,11 @@ impl File { fn change_selection_to(&mut self, new_selection: Option<(isize, usize)>, none_ok: bool, client: &mut Client) -> LogicalAction { + if self.selection_restrict_to_item.is_some_and(|restricted| + new_selection.is_some_and(|(item, _)| item != restricted)) { + return LogicalAction::Beep; + } + let result = if new_selection.is_some() { self.rerender_selected_item(); self.selection = new_selection; @@ -756,12 +773,33 @@ impl File { self.change_selection_to(new_selection, false, client) } + fn vote(&mut self) -> LogicalAction { + let (item, sub) = self.selection.expect( + "we should only call this if we have a selection"); + self.selection_restrict_to_item = Some(item); + if self.contents.get(item).is_multiple_choice_poll() { + if self.poll_options_selected.contains(&sub) { + self.poll_options_selected.remove(&sub); + } else { + self.poll_options_selected.insert(sub); + } + } else { + self.poll_options_selected.clear(); + self.poll_options_selected.insert(sub); + } + dbg!(&self.poll_options_selected); + self.rerender_selected_item(); + self.ensure_enough_rendered(); + LogicalAction::Nothing + } + fn end_selection(&mut self) { if self.selection.is_some() { self.rerender_selected_item(); self.selection = None; self.ensure_enough_rendered(); } + self.poll_options_selected.clear(); self.ui_mode = UIMode::Normal; } @@ -831,6 +869,16 @@ impl File { UtilityActivity::ComposeReply(id).into()), SelectionPurpose::Thread => LogicalAction::Goto( UtilityActivity::ThreadFile(id, alt).into()), + SelectionPurpose::Vote => { + match client.vote_in_poll( + &id, self.poll_options_selected.iter().copied()) { + Ok(_) => { + self.contents.update_items(client); + LogicalAction::Nothing + } + Err(_) => LogicalAction::Beep, + } + } } } else { LogicalAction::Beep @@ -984,6 +1032,13 @@ impl } else { fs }; + let fs = if Type::Item::can_highlight( + HighlightType::PollOption) + { + fs.add(Ctrl('V'), "Vote", 10) + } else { + fs + }; let fs = fs .add(Pr('/'), "Search Down", 20) .add(Pr('\\'), "Search Up", 20) @@ -1055,6 +1110,26 @@ impl fs.add(Space, "Thread Context", 98) .add(Pr('F'), "Full Thread", 97) } + SelectionPurpose::Vote => { + // Different verb for selecting items + // depending on whether the vote lets you + // select more than one + let verb = if self.contents.get(item) + .is_multiple_choice_poll() + { "Toggle" } else { "Select" }; + + // If you've selected nothing yet, prioritise + // the keypress for selecting something. + // Otherwise, prioritise the one for + // submitting your answer. + if self.poll_options_selected.is_empty() { + fs.add(Space, verb, 98) + .add(Ctrl('V'), "Submit Vote", 97) + } else { + fs.add(Space, verb, 97) + .add(Ctrl('V'), "Submit Vote", 98) + } + } }; fs.add(Pr('+'), "Down", 99) .add(Pr('-'), "Up", 99) @@ -1188,6 +1263,16 @@ impl } } + Ctrl('V') => { + if Type::Item::can_highlight(HighlightType::PollOption) { + self.start_selection(HighlightType::PollOption, + SelectionPurpose::Vote, + client) + } else { + LogicalAction::Nothing + } + } + Pr('l') | Pr('L') => { if Type::CAN_LIST != CanList::Nothing { self.ui_mode = UIMode::ListSubmenu; @@ -1274,7 +1359,10 @@ impl _ => LogicalAction::Nothing, } UIMode::Select(_, purpose) => match key { - Space => self.complete_selection(client, false), + Space => match purpose { + SelectionPurpose::Vote => self.vote(), + _ => self.complete_selection(client, false), + } Pr('d') | Pr('D') => match purpose { SelectionPurpose::Favourite | SelectionPurpose::Boost => self.complete_selection(client, true), @@ -1285,6 +1373,11 @@ impl self.complete_selection(client, true), _ => LogicalAction::Nothing, } + Ctrl('V') => match purpose { + SelectionPurpose::Vote => + self.complete_selection(client, false), + _ => LogicalAction::Nothing, + } Pr('-') | Up => self.selection_up(client), Pr('+') | Down => self.selection_down(client), Pr('q') | Pr('Q') => self.abort_selection(), diff --git a/src/text.rs b/src/text.rs index a2bfd70..227934c 100644 --- a/src/text.rs +++ b/src/text.rs @@ -2,7 +2,7 @@ use chrono::{DateTime, Local, Utc}; #[cfg(test)] use chrono::NaiveDateTime; use core::cmp::{min, max}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; use unicode_width::UnicodeWidthStr; use super::html; @@ -12,7 +12,7 @@ use super::tui::OurKey; use super::coloured_string::{ColouredString, ColouredStringSlice}; #[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum HighlightType { User, Status, WholeStatus } +pub enum HighlightType { User, Status, WholeStatus, PollOption } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct Highlight(pub HighlightType, pub usize); @@ -42,7 +42,7 @@ impl ConsumableHighlight for Option { pub trait TextFragment { fn render(&self, width: usize) -> Vec { - self.render_highlighted(width, None) + self.render_highlighted(width, None, None) } fn can_highlight(_htype: HighlightType) -> bool where Self : Sized { @@ -53,25 +53,30 @@ pub trait TextFragment { fn highlighted_id(&self, _highlight: Option) -> Option { None } - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + poll_options: Option<&HashSet>) -> Vec; + fn is_multiple_choice_poll(&self) -> bool { false } + fn render_highlighted_update( - &self, width: usize, highlight: &mut Option) + &self, width: usize, highlight: &mut Option, + poll_options: Option<&HashSet>) -> Vec { let (new_highlight, text) = match *highlight { Some(Highlight(htype, index)) => { let count = self.count_highlightables(htype); if index < count { - (None, self.render_highlighted(width, *highlight)) + (None, self.render_highlighted( + width, *highlight, poll_options)) } else { (Some(Highlight(htype, index - count)), - self.render_highlighted(width, None)) + self.render_highlighted(width, None, poll_options)) } } None => { - (None, self.render_highlighted(width, None)) + (None, self.render_highlighted(width, None, poll_options)) } }; *highlight = new_highlight; @@ -109,10 +114,12 @@ impl TextFragment for Option { None => 0, } } - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + poll_options: Option<&HashSet>) -> Vec { match self { - Some(ref inner) => inner.render_highlighted(width, highlight), + Some(ref inner) => inner.render_highlighted( + width, highlight, poll_options), None => Vec::new(), } } @@ -132,11 +139,14 @@ impl TextFragment for Vec { fn count_highlightables(&self, htype: HighlightType) -> usize { self.iter().map(|x| x.count_highlightables(htype)).sum() } - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut highlight = highlight; + // Ignore poll_options, because that's only used when we're + // rendering a single item containing the poll in question itertools::concat(self.iter().map( - |x| x.render_highlighted_update(width, &mut highlight))) + |x| x.render_highlighted_update(width, &mut highlight, None))) } fn highlighted_id(&self, highlight: Option) -> Option { let mut highlight = highlight; @@ -166,7 +176,8 @@ impl BlankLine { } impl TextFragment for BlankLine { - fn render_highlighted(&self, _width: usize, _highlight: Option) + fn render_highlighted(&self, _width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { Self::render_static() @@ -205,7 +216,8 @@ fn format_date(date: DateTime) -> String { } impl TextFragment for SeparatorLine { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut suffix = ColouredString::plain(""); @@ -259,7 +271,8 @@ impl EditorHeaderSeparator { } impl TextFragment for EditorHeaderSeparator { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { vec! { @@ -312,7 +325,8 @@ impl UsernameHeader { } impl TextFragment for UsernameHeader { - fn render_highlighted(&self, _width: usize, highlight: Option) + fn render_highlighted(&self, _width: usize, highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let header = ColouredString::plain(&format!("{}: ", self.header)); @@ -540,7 +554,8 @@ fn test_para_build() { } impl TextFragment for Paragraph { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut lines = Vec::new(); @@ -646,7 +661,8 @@ impl FileHeader { } impl TextFragment for FileHeader { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let elephants = width >= 16; @@ -766,7 +782,8 @@ impl Html { } impl TextFragment for Html { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { match self { @@ -861,7 +878,8 @@ impl ExtendableIndicator { } impl TextFragment for ExtendableIndicator { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let message = if self.primed { @@ -933,7 +951,8 @@ impl InReplyToLine { } impl TextFragment for InReplyToLine { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let rendered_para = self.para.render(width - min(width, 3)); @@ -981,7 +1000,8 @@ impl VisibilityLine { } impl TextFragment for VisibilityLine { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let line = match self.vis { @@ -1060,7 +1080,8 @@ impl NotificationLog { } impl TextFragment for NotificationLog { - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut full_para = Paragraph::new().set_indent(0, 2); @@ -1172,7 +1193,8 @@ impl UserListEntry { } impl TextFragment for UserListEntry { - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut para = Paragraph::new().set_indent(0, 2); @@ -1244,7 +1266,8 @@ impl Media { } impl TextFragment for Media { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut lines: Vec<_> = ColouredString::uniform(&self.url, 'M') @@ -1436,7 +1459,8 @@ fn test_filestatus_build() { } impl TextFragment for FileStatusLineFinal { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut line = ColouredString::plain(""); @@ -1624,7 +1648,8 @@ impl MenuKeypressLine { } impl TextFragment for MenuKeypressLine { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let ourwidth = self.lmaxwid + self.rmaxwid + 3; // " = " in the middle @@ -1697,7 +1722,8 @@ impl CentredInfoLine { } impl TextFragment for CentredInfoLine { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let twidth = width.saturating_sub(1); @@ -1709,6 +1735,14 @@ impl TextFragment for CentredInfoLine { } } +struct Poll { + title: Paragraph, + options: Vec<(bool, ColouredString)>, + id: String, + eligible: bool, + multiple: bool, +} + pub struct StatusDisplay { sep: SeparatorLine, from: UsernameHeader, @@ -1717,6 +1751,7 @@ pub struct StatusDisplay { vis: Option, content: Html, media: Vec, + poll: Option, blank: BlankLine, id: String, } @@ -1759,6 +1794,59 @@ impl StatusDisplay { Media::new(&m.url, desc_ref) }).collect(); + let poll = st.poll.map(|poll| { + let mut extras = Vec::new(); + let voters = poll.voters_count.unwrap_or(poll.votes_count); + if poll.multiple { + extras.push(ColouredString::uniform("multiple choice", 'K')); + } + if poll.expired { + extras.push(ColouredString::uniform("expired", 'H')); + extras.push(ColouredString::uniform( + &format!("{} voters", voters), 'H')); + } else { + if let Some(date) = poll.expires_at { + extras.push(ColouredString::uniform( + &format!("expires {}", format_date(date)), 'H')); + } else { + extras.push(ColouredString::uniform("no expiry", 'H')); + } + extras.push(ColouredString::uniform( + &format!("{} voters so far", voters), 'H')); + } + let mut desc = ColouredString::uniform("Poll: ", 'H'); + for (i, extra) in extras.iter().enumerate() { + if i > 0 { + desc.push_str(&ColouredString::uniform(", ", 'H').slice()); + } + desc.push_str(&extra.slice()); + } + + let title = Paragraph::new().set_indent(0, 2).add(&desc); + + let mut options = Vec::new(); + for (thisindex, opt) in poll.options.iter().enumerate() { + let voted = poll.own_votes.as_ref().map_or(false, |votes| { + votes.iter().any(|i| *i == thisindex) + }); + let mut desc = ColouredString::plain(" "); + desc.push_str(&ColouredString::plain(&opt.title).slice()); + if let Some(n) = opt.votes_count { + desc.push_str(&ColouredString::uniform( + &format!(" ({})", n), 'H').slice()); + } + options.push((voted, desc)); + } + + Poll { + title, + options, + id: poll.id.clone(), + eligible: poll.voted != Some(true) && !poll.expired, + multiple: poll.multiple, + } + }); + StatusDisplay { sep, from, @@ -1767,6 +1855,7 @@ impl StatusDisplay { vis, content, media, + poll, blank: BlankLine::new(), id: st.id, } @@ -1784,7 +1873,8 @@ fn push_fragment_highlighted(lines: &mut Vec, } impl TextFragment for StatusDisplay { - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + poll_options_selected: Option<&HashSet>) -> Vec { let mut lines = Vec::new(); @@ -1801,9 +1891,9 @@ impl TextFragment for StatusDisplay { push_fragment(&mut lines, self.sep.render(width)); push_fragment(&mut lines, self.from.render_highlighted_update( - width, &mut highlight)); + width, &mut highlight, None)); push_fragment(&mut lines, self.via.render_highlighted_update( - width, &mut highlight)); + width, &mut highlight, None)); push_fragment(&mut lines, self.vis.render(width)); push_fragment(&mut lines, self.irt.render(width)); push_fragment(&mut lines, self.blank.render(width)); @@ -1817,13 +1907,35 @@ impl TextFragment for StatusDisplay { push_fragment_opt_highlight(&mut lines, m.render(width)); push_fragment_opt_highlight(&mut lines, self.blank.render(width)); } + if let Some(poll) = &self.poll { + push_fragment_opt_highlight(&mut lines, poll.title.render(width)); + for (i, (voted, desc)) in poll.options.iter().enumerate() { + let highlighting_this_option = + highlight.consume(HighlightType::PollOption, 1) == Some(0); + let voted = poll_options_selected.map_or( + *voted, |opts| opts.contains(&i)); + let option = Paragraph::new().set_indent(0, 2).add( + &ColouredString::uniform( + if voted {"[X]"} else {"[ ]"}, 'H')) + .add(&desc); + let push_fragment_opt_highlight = if highlighting_this_option { + push_fragment_highlighted + } else { + push_fragment_opt_highlight + }; + push_fragment_opt_highlight(&mut lines, option.render(width)); + } + push_fragment_opt_highlight(&mut lines, self.blank.render(width)); + } lines } fn can_highlight(htype: HighlightType) -> bool where Self : Sized { - htype == HighlightType::User || htype == HighlightType::Status || - htype == HighlightType::WholeStatus + htype == HighlightType::User || + htype == HighlightType::Status || + htype == HighlightType::WholeStatus || + htype == HighlightType::PollOption } fn count_highlightables(&self, htype: HighlightType) -> usize { @@ -1834,6 +1946,10 @@ impl TextFragment for StatusDisplay { } HighlightType::Status => 1, HighlightType::WholeStatus => 1, + + HighlightType::PollOption => self.poll.as_ref() + .filter(|poll| poll.eligible) + .map_or(0, |poll| poll.options.len()), } } @@ -1855,9 +1971,17 @@ impl TextFragment for StatusDisplay { } Some(Highlight(HighlightType::WholeStatus, 0)) | Some(Highlight(HighlightType::Status, 0)) => Some(self.id.clone()), + Some(Highlight(HighlightType::PollOption, i)) => self.poll.as_ref() + .filter(|poll| poll.eligible) + .filter(|poll| i < poll.options.len()) + .map(|poll| poll.id.clone()), _ => None, } } + + fn is_multiple_choice_poll(&self) -> bool { + self.poll.as_ref().map_or(false, |poll| poll.multiple) + } } pub struct DetailedStatusDisplay { @@ -2019,14 +2143,15 @@ impl DetailedStatusDisplay { } impl TextFragment for DetailedStatusDisplay { - fn render_highlighted(&self, width: usize, highlight: Option) + fn render_highlighted(&self, width: usize, highlight: Option, + poll_options: Option<&HashSet>) -> Vec { let mut lines = Vec::new(); let mut highlight = highlight; push_fragment(&mut lines, self.sd.render_highlighted_update( - width, &mut highlight)); + width, &mut highlight, poll_options)); push_fragment(&mut lines, self.sep.render(width)); push_fragment(&mut lines, self.blank.render(width)); @@ -2095,7 +2220,8 @@ impl TextFragment for DetailedStatusDisplay { } fn can_highlight(htype: HighlightType) -> bool where Self : Sized { - htype == HighlightType::User || htype == HighlightType::Status + htype == HighlightType::User || htype == HighlightType::Status || + htype == HighlightType::PollOption } fn count_highlightables(&self, htype: HighlightType) -> usize { @@ -2148,6 +2274,10 @@ impl TextFragment for DetailedStatusDisplay { } None } + + fn is_multiple_choice_poll(&self) -> bool { + self.sd.is_multiple_choice_poll() + } } pub struct ExamineUserDisplay { @@ -2358,7 +2488,8 @@ impl ExamineUserDisplay { } impl TextFragment for ExamineUserDisplay { - fn render_highlighted(&self, width: usize, _highlight: Option) + fn render_highlighted(&self, width: usize, _highlight: Option, + _poll_options: Option<&HashSet>) -> Vec { let mut lines = Vec::new(); diff --git a/src/types.rs b/src/types.rs index fce4e20..e512e97 100644 --- a/src/types.rs +++ b/src/types.rs @@ -165,6 +165,26 @@ pub struct StatusMention { pub acct: String, } +#[derive(Deserialize, Debug, Clone)] +pub struct Poll { + pub id: String, + pub expires_at: Option>, + pub multiple: bool, + pub expired: bool, + pub votes_count: u64, + pub voters_count: Option, // if !multiple, doesn't need to be separate + pub options: Vec, + // pub emojis: Vec, + pub voted: Option, + pub own_votes: Option>, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct PollOption { + pub title: String, + pub votes_count: Option, +} + #[derive(Deserialize, Debug, Clone)] pub struct Status { pub id: String, @@ -187,7 +207,7 @@ pub struct Status { pub in_reply_to_id: Option, pub in_reply_to_account_id: Option, pub reblog: Option>, - // pub poll: Option, + pub poll: Option, // pub card: Option, pub language: Option, pub text: Option,