From 17b700af32d91352c38ba6a043a76cd03c8cb548 Mon Sep 17 00:00:00 2001 From: Simon Tatham Date: Sun, 7 Jan 2024 22:17:27 +0000 Subject: [PATCH] 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. --- TODO.md | 7 -- src/client.rs | 68 +++++++++++++++++ src/file.rs | 97 +++++++++++++++++++++++- src/text.rs | 205 +++++++++++++++++++++++++++++++++++++++++--------- src/types.rs | 22 +++++- 5 files changed, 352 insertions(+), 47 deletions(-) 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, -- 2.30.2