chiark / gitweb /
Add the Read Mentions feed.
authorSimon Tatham <anakin@pobox.com>
Sun, 31 Dec 2023 08:58:43 +0000 (08:58 +0000)
committerSimon Tatham <anakin@pobox.com>
Sun, 31 Dec 2023 13:04:41 +0000 (13:04 +0000)
src/client.rs
src/file.rs
src/menu.rs
src/tui.rs

index b70341c392c5202bb642fce02ae5157911385452..fbf743dec864a5c8fc72c163ba8e38a46bd1d3fa 100644 (file)
@@ -18,6 +18,7 @@ pub enum FeedId {
     Public,
     Hashtag(String),
     User(String, Boosts, Replies),
+    Mentions,
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -342,6 +343,9 @@ impl Client {
                     .param("exclude_reblogs", *boosts == Boosts::Hide)
                     .param("exclude_replies", *replies == Replies::Hide)
             },
+            FeedId::Mentions => {
+                Req::get("notifications").param("types[]", "mention")
+            },
         };
 
         let req = match ext {
@@ -362,18 +366,52 @@ impl Client {
 
         let (url, req) = self.api_request(req)?;
         let body = req.send()?.text()?;
-        let sts: Vec<Status> = match serde_json::from_str(&body) {
-            Ok(sts) => Ok(sts),
-            Err(e) => {
-                dbg!(&body);
-                Err(ClientError::UrlError(url.clone(), e.to_string()))
+
+        // Decode the JSON response as a different kind of type
+        // depending on the feed. But in all cases we expect to end up
+        // with a list of ids.
+        let ids: VecDeque<String> = match id {
+            FeedId::Home | FeedId::Local | FeedId::Public |
+            FeedId::Hashtag(..) | FeedId::User(..) => {
+                let sts: Vec<Status> = match serde_json::from_str(&body) {
+                    Ok(sts) => Ok(sts),
+                    Err(e) => {
+                        dbg!(&body);
+                        Err(ClientError::UrlError(url.clone(), e.to_string()))
+                    },
+                }?;
+                for st in &sts {
+                    self.cache_status(st);
+                }
+                sts.iter().rev().map(|st| st.id.clone()).collect()
             },
-        }?;
-        let any_new = !sts.is_empty();
-        for st in &sts {
-            self.cache_status(st);
-        }
-        let ids = sts.iter().rev().map(|st| st.id.clone()).collect();
+            FeedId::Mentions => {
+                let mut nots: Vec<Notification> = match serde_json::from_str(
+                    &body) {
+                    Ok(nots) => Ok(nots),
+                    Err(e) => {
+                        dbg!(&body);
+                        Err(ClientError::UrlError(url.clone(), e.to_string()))
+                    },
+                }?;
+
+                // According to the protocol spec, all notifications
+                // of type Mention should have a status in them. We
+                // double-check that here, so that the rest of our
+                // code can safely .unwrap() or .expect() the status
+                // from notifications they get via this feed.
+                nots.retain(|not| {
+                    not.ntype == NotificationType::Mention &&
+                        not.status.is_some()
+                });
+                for not in &nots {
+                    self.cache_notification(not);
+                }
+                nots.iter().rev().map(|not| not.id.clone()).collect()
+            },
+        };
+        let any_new = !ids.is_empty();
+
         match ext {
             FeedExtend::Initial => {
                 self.feeds.insert(id.clone(), Feed {
@@ -471,8 +509,14 @@ impl Client {
 
         match (up.id, up.response) {
             (StreamId::User, StreamResponse::Line(_)) => {
-                self.fetch_feed(&FeedId::Home, FeedExtend::Future)?;
-                updates.insert(FeedId::Home);
+                // This stream interleaves updates to the home
+                // timeline with notifications, so it can cause an
+                // update in more than one thing we think of as a feed.
+                for id in &[FeedId::Home, FeedId::Mentions] {
+                    if self.fetch_feed(id, FeedExtend::Future)? {
+                        updates.insert(id.clone());
+                    }
+                }
             },
 
             // FIXME: we probably _should_ handle EOF from the
index cae8abf60745ffac0106e3da6375c74416ddc1d0..8828ea2681318658f082bbbc3b8a2f19cc2126ac 100644 (file)
@@ -34,6 +34,20 @@ impl FeedType for StatusFeedType {
     }
 }
 
+struct NotificationStatusFeedType {}
+impl FeedType for NotificationStatusFeedType {
+    type Item = StatusDisplay;
+
+    fn get_from_client(id: &str, client: &mut Client) ->
+        Result<Self::Item, ClientError>
+    {
+        let not = client.notification_by_id(&id)?;
+        let st = &not.status.expect(
+            "expected all notifications in this feed would have statuses");
+        Ok(StatusDisplay::new(st.clone(), client))
+    }
+}
+
 struct FeedFileContents<Type: FeedType> {
     id: FeedId,
     header: FileHeader,
@@ -472,3 +486,13 @@ pub fn public_timeline(client: &mut Client) ->
             "HHHHHHHHHHHHHHHHHHHKH"))?;
     Ok(Box::new(file))
 }
+
+pub fn mentions(client: &mut Client) ->
+    Result<Box<dyn ActivityState>, ClientError>
+{
+    let file = FeedFile::<NotificationStatusFeedType>::new(
+        client, FeedId::Mentions, ColouredString::general(
+            "Mentions   [ESC][R]",
+            "HHHHHHHHHHHHKKKHHKH"))?;
+    Ok(Box::new(file))
+}
index edd434f3294240acc898736a88944019e21b0c86..bcfb580a47df00cc10ee1a8785d6ce5f8f5bdc49 100644 (file)
@@ -218,6 +218,9 @@ pub fn utils_menu() -> Box<dyn ActivityState> {
             "Utilities [ESC]",
             "HHHHHHHHHHHKKKH"), false);
 
+    menu.add_action(Pr('R'), "Read Mentions", LogicalAction::Goto(
+        UtilityActivity::ReadMentions.into()));
+    menu.add_blank_line();
     menu.add_action(Pr('E'), "Examine User", LogicalAction::NYI);
     menu.add_action(Pr('Y'), "Examine Yourself", LogicalAction::NYI);
     menu.add_blank_line();
index 331daecd4e76f6e3d93c3f672a26d692b82e737a..d0754c99094dd0e204fbffc506f66577f29abf76 100644 (file)
@@ -411,6 +411,8 @@ fn new_activity_state(activity: Activity, client: &mut Client) ->
             public_timeline(client),
         Activity::NonUtil(NonUtilityActivity::LocalTimelineFile) =>
             local_timeline(client),
+        Activity::Util(UtilityActivity::ReadMentions) =>
+            mentions(client),
         _ => panic!("FIXME"),
     };