chiark / gitweb /
Implement favouriting and boosting posts.
authorSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 08:02:03 +0000 (08:02 +0000)
committerSimon Tatham <anakin@pobox.com>
Thu, 4 Jan 2024 08:16:16 +0000 (08:16 +0000)
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.

src/client.rs
src/file.rs

index fd1f667d7adca32538576f3c30ca1898f2a0e81b..7cdf0a3be61a58cb1317023d9bdf10fdaa1724f6 100644 (file)
@@ -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)
+    }
 }
index b39ef27cb30de92b11acc063c0c4b1d676db4813..3a778596316c825180db99949435ee06ad3424d5 100644 (file)
@@ -140,7 +140,10 @@ struct FileContents<Type: FileType, Source: FileDataSource> {
 impl<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
     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<Type: FileType, Source: FileDataSource> FileContents<Type,Source> {
 enum SelectionPurpose {
     ExamineUser,
     StatusInfo,
+    Favourite,
+    Boost,
 }
 
 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
@@ -205,6 +210,7 @@ struct File<Type: FileType, Source: FileDataSource> {
     last_size: Option<(usize, usize)>,
     ui_mode: UIMode,
     selection: Option<(isize, usize)>,
+    select_aux: Option<bool>, // distinguishes fave from unfave, etc
 }
 
 impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
@@ -239,6 +245,7 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
             last_size: None,
             ui_mode: UIMode::Normal,
             selection: None,
+            select_aux: None,
         };
         Ok(ff)
     }
@@ -509,7 +516,8 @@ impl<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
 
         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<Type: FileType, Source: FileDataSource> File<Type, Source> {
     }
 
     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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             }
         };
 
+        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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             }
         };
 
-        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<Type: FileType, Source: FileDataSource> File<Type, Source> {
             }
         };
 
-        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<Type: FileType, Source: FileDataSource> File<Type, Source> {
         }
     }
 
-    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<Type: FileType, Source: FileDataSource>
                         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<Type: FileType, Source: FileDataSource>
                 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<Type: FileType, Source: FileDataSource>
                 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<Type: FileType, Source: FileDataSource>
 
                 _ => 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,
             }