From: Simon Tatham Date: Thu, 4 Jan 2024 08:02:03 +0000 (+0000) Subject: Implement favouriting and boosting posts. X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=1484d354f49144fe5524ce713357822259af0b6d;p=mastodonochrome.git Implement favouriting and boosting posts. For these uses of selection, we have to support different keystrokes to fave and unfave, which means we also need to know which keystroke is valid when. So I've had to plumb the Client through to all of the methods that move the selection, because those methods need to retrieve the newly pointed-to status and cache whether it's already faved/boosted. Otherwise draw(), which can't mutate things, can't find that out to display the right status line. --- diff --git a/src/client.rs b/src/client.rs index fd1f667..7cdf0a3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -834,4 +834,40 @@ impl Client { Ok(()) } } + + pub fn fave_boost_post(&mut self, id: &str, verb: &str) + -> Result<(), ClientError> + { + let (url, req) = self.api_request(Req::post( + &format!("v1/statuses/{id}/{verb}")))?; + let rsp = req.send()?; + let rspstatus = rsp.status(); + // Cache the returned status so as to update its faved/boosted flags + let st: Status = if !rspstatus.is_success() { + Err(ClientError::UrlError(url.clone(), rspstatus.to_string())) + } else { + match serde_json::from_str(&rsp.text()?) { + Ok(st) => Ok(st), + Err(e) => { + Err(ClientError::UrlError(url.clone(), e.to_string())) + } + } + }?; + self.cache_status(&st); + Ok(()) + } + + pub fn favourite_post(&mut self, id: &str, enable: bool) + -> Result<(), ClientError> + { + let verb = if enable {"favourite"} else {"unfavourite"}; + self.fave_boost_post(id, verb) + } + + pub fn boost_post(&mut self, id: &str, enable: bool) + -> Result<(), ClientError> + { + let verb = if enable {"reblog"} else {"unreblog"}; + self.fave_boost_post(id, verb) + } } diff --git a/src/file.rs b/src/file.rs index b39ef27..3a77859 100644 --- a/src/file.rs +++ b/src/file.rs @@ -140,7 +140,10 @@ struct FileContents { impl FileContents { fn update_items(&mut self, client: &mut Client) { // FIXME: if the feed has been extended rather than created, - // we should be able to make less effort than this + // we should be able to make less effort than this. But we + // still need to regenerate any item derived from something we + // _know_ has changed on the server side - in particular, a + // post we just changed fave/boost status on. let (ids, origin) = self.source.get(client); self.origin = origin; @@ -190,6 +193,8 @@ impl FileContents { enum SelectionPurpose { ExamineUser, StatusInfo, + Favourite, + Boost, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -205,6 +210,7 @@ struct File { last_size: Option<(usize, usize)>, ui_mode: UIMode, selection: Option<(isize, usize)>, + select_aux: Option, // distinguishes fave from unfave, etc } impl File { @@ -239,6 +245,7 @@ impl File { last_size: None, ui_mode: UIMode::Normal, selection: None, + select_aux: None, }; Ok(ff) } @@ -509,7 +516,8 @@ impl File { } fn start_selection(&mut self, htype: HighlightType, - purpose: SelectionPurpose) -> LogicalAction + purpose: SelectionPurpose, client: &mut Client) + -> LogicalAction { let item = self.pos.item(); let selection = @@ -518,7 +526,7 @@ impl File { if selection.is_some() { self.ui_mode = UIMode::Select(htype, purpose); - self.change_selection_to(selection, false); + self.change_selection_to(selection, false, client); LogicalAction::Nothing } else { LogicalAction::Beep @@ -526,7 +534,7 @@ impl File { } fn change_selection_to(&mut self, new_selection: Option<(isize, usize)>, - none_ok: bool) -> LogicalAction + none_ok: bool, client: &mut Client) -> LogicalAction { let result = if new_selection.is_some() { self.rerender_selected_item(); @@ -542,10 +550,36 @@ impl File { } }; + self.select_aux = match self.ui_mode { + UIMode::Select(_, SelectionPurpose::Favourite) => { + match self.selected_id(self.selection) { + Some(id) => { + match client.status_by_id(&id) { + Ok(st) => Some(st.favourited == Some(true)), + Err(_) => Some(false), + } + }, + None => Some(false), + } + }, + UIMode::Select(_, SelectionPurpose::Boost) => { + match self.selected_id(self.selection) { + Some(id) => { + match client.status_by_id(&id) { + Ok(st) => Some(st.reblogged == Some(true)), + Err(_) => Some(false), + } + }, + None => Some(false), + } + }, + _ => None, + }; + result } - fn selection_up(&mut self) -> LogicalAction { + fn selection_up(&mut self, client: &mut Client) -> LogicalAction { let htype = match self.ui_mode { UIMode::Select(htype, _purpose) => htype, _ => return LogicalAction::Beep, @@ -560,10 +594,10 @@ impl File { } }; - self.change_selection_to(new_selection, false) + self.change_selection_to(new_selection, false, client) } - fn selection_down(&mut self) -> LogicalAction { + fn selection_down(&mut self, client: &mut Client) -> LogicalAction { let htype = match self.ui_mode { UIMode::Select(htype, _purpose) => htype, _ => return LogicalAction::Beep, @@ -581,7 +615,7 @@ impl File { } }; - self.change_selection_to(new_selection, false) + self.change_selection_to(new_selection, false, client) } fn end_selection(&mut self) { @@ -613,18 +647,48 @@ impl File { } } - fn complete_selection(&mut self) -> LogicalAction { + fn complete_selection(&mut self, client: &mut Client, alt: bool) + -> LogicalAction + { let (_htype, purpose) = match self.ui_mode { UIMode::Select(htype, purpose) => (htype, purpose), _ => return LogicalAction::Beep, }; + match purpose { + SelectionPurpose::Favourite | SelectionPurpose::Boost => { + // alt means unfave; select_aux = Some(already faved?) + if self.select_aux == Some(!alt) { + return LogicalAction::Nothing; + } + } + _ => (), + } + let result = if let Some(id) = self.selected_id(self.selection) { match purpose { SelectionPurpose::ExamineUser => LogicalAction::Goto( UtilityActivity::ExamineUser(id).into()), SelectionPurpose::StatusInfo => LogicalAction::Goto( UtilityActivity::InfoStatus(id).into()), + SelectionPurpose::Favourite => { + match client.favourite_post(&id, !alt) { + Ok(_) => { + self.contents.update_items(client); + LogicalAction::Nothing + } + Err(_) => LogicalAction::Beep, + } + } + SelectionPurpose::Boost => { + match client.boost_post(&id, !alt) { + Ok(_) => { + self.contents.update_items(client); + LogicalAction::Nothing + } + Err(_) => LogicalAction::Beep, + } + } } } else { LogicalAction::Beep @@ -757,6 +821,20 @@ impl fs.add(Space, "Examine", 98), SelectionPurpose::StatusInfo => fs.add(Space, "Info", 98), + SelectionPurpose::Favourite => { + if self.select_aux == Some(true) { + fs.add(Pr('D'), "Unfave", 98) + } else { + fs.add(Space, "Fave", 98) + } + } + SelectionPurpose::Boost => { + if self.select_aux == Some(true) { + fs.add(Pr('D'), "Unboost", 98) + } else { + fs.add(Space, "Boost", 98) + } + } }; fs.add(Pr('+'), "Down", 99) .add(Pr('-'), "Up", 99) @@ -826,7 +904,8 @@ impl Pr('e') | Pr('E') => { if Type::Item::can_highlight(HighlightType::User) { self.start_selection(HighlightType::User, - SelectionPurpose::ExamineUser) + SelectionPurpose::ExamineUser, + client) } else { LogicalAction::Nothing } @@ -835,7 +914,28 @@ impl Pr('i') | Pr('I') => { if Type::Item::can_highlight(HighlightType::Status) { self.start_selection(HighlightType::Status, - SelectionPurpose::StatusInfo) + SelectionPurpose::StatusInfo, + client) + } else { + LogicalAction::Nothing + } + } + + Pr('f') | Pr('F') => { + if Type::Item::can_highlight(HighlightType::WholeStatus) { + self.start_selection(HighlightType::WholeStatus, + SelectionPurpose::Favourite, + client) + } else { + LogicalAction::Nothing + } + } + + Ctrl('B') => { + if Type::Item::can_highlight(HighlightType::WholeStatus) { + self.start_selection(HighlightType::WholeStatus, + SelectionPurpose::Boost, + client) } else { LogicalAction::Nothing } @@ -843,10 +943,15 @@ impl _ => LogicalAction::Nothing, } - UIMode::Select(..) => match key { - Space => self.complete_selection(), - Pr('-') | Up => self.selection_up(), - Pr('+') | Down => self.selection_down(), + UIMode::Select(_, purpose) => match key { + Space => self.complete_selection(client, false), + Pr('d') | Pr('D') => match purpose { + SelectionPurpose::Favourite | SelectionPurpose::Boost => + self.complete_selection(client, true), + _ => LogicalAction::Nothing, + } + Pr('-') | Up => self.selection_up(client), + Pr('+') | Down => self.selection_down(client), Pr('q') | Pr('Q') => self.abort_selection(), _ => LogicalAction::Nothing, }