chiark / gitweb /
Basically correct-looking display of toots \o/
authorSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 14:18:28 +0000 (14:18 +0000)
committerSimon Tatham <anakin@pobox.com>
Fri, 29 Dec 2023 18:17:34 +0000 (18:17 +0000)
src/client.rs
src/file.rs
src/text.rs

index a2120d541b2645a38aec4a1bd10fe2a3bba498fa..b9393d4e939d40eac739ba84aacdfd39c0fd79e8 100644 (file)
@@ -50,6 +50,21 @@ impl From<reqwest::Error> for ClientError {
     }
 }
 
+impl std::fmt::Display for ClientError {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) ->
+        Result<(), std::fmt::Error>
+    {
+        match self {
+            ClientError::InternalError(ref msg) =>
+                write!(f, "internal failure: {}", msg),
+            ClientError::UrlParseError(ref url, ref msg) =>
+                write!(f, "Parse failure {} (retrieving URL: {})", msg, url),
+            ClientError::UrlError(ref url, ref msg) =>
+                write!(f, "{} (retrieving URL: {})", msg, url),
+        }
+    }
+}
+
 // Our own struct to collect the pieces of an HTTP request before we
 // pass it on to reqwests. Allows incremental adding of request parameters.
 struct Req {
@@ -127,6 +142,13 @@ impl Client {
         self.permit_write = permit;
     }
 
+    pub fn fq(&self, acct: &str) -> String {
+        match acct.contains('@') {
+            true => acct.to_owned(),
+            false => acct.to_owned() + "@" + &self.auth.instance_domain,
+        }
+    }
+
     fn api_request(&self, req: Req) ->
         Result<(String, reqwest::blocking::RequestBuilder), ClientError>
     {
index da7e921ae0b20f24db79c1de9a5919dfa3066c64..ad2c8b47ab2369fb86411953d164e4437d7f25a2 100644 (file)
@@ -33,7 +33,7 @@ impl FeedFile {
             let st = client.status_by_id(&id)
                 .expect("Any id stored in a Feed should also be cached")
                 .clone();
-            self.items.push(Box::new(StatusDisplay::new(st)));
+            self.items.push(Box::new(StatusDisplay::new(st, client)));
         }
     }
 }
index f19e62fb258d878986afdcd61a0dba8fc6696af7..e55c4340d6d5ee3d570c44fbd4bc3acf3370282f 100644 (file)
@@ -6,6 +6,7 @@ use std::collections::{HashMap, BTreeMap, BTreeSet};
 use unicode_width::UnicodeWidthStr;
 
 use super::html;
+use super::client::Client;
 use super::types::*;
 use super::tui::OurKey;
 use super::coloured_string::{ColouredString, ColouredStringSlice};
@@ -1035,15 +1036,22 @@ pub struct Media {
 }
 
 impl Media {
-    pub fn new(url: &str, description: &str)
+    pub fn new(url: &str, description: Option<&str>)
                -> Self {
-        let mut paras = description
-            .split('\n')
-            .map(|x| Paragraph::new()
-                 .set_indent(2, 2)
-                 .add(&ColouredString::uniform(x, 'm')))
-            .collect();
-        trim_para_list(&mut paras);
+        let paras = match description {
+            None => Vec::new(),
+            Some(description) => {
+                let mut paras = description
+                    .split('\n')
+                    .map(|x| Paragraph::new()
+                         .set_indent(2, 2)
+                         .add(&ColouredString::uniform(x, 'm')))
+                    .collect();
+                trim_para_list(&mut paras);
+                paras
+            },
+        };
+
         Media {
             url: url.to_owned(),
             description: paras,
@@ -1064,7 +1072,7 @@ impl TextFragment for Media {
 #[test]
 fn test_media() {
     assert_eq!(Media::new("https://example.com/example.png",
-                          "A picture of an example, sitting on an example, with examples dotted around the example landscape.\n\nThis doesn't really make sense, but whatever.").render(50), vec! {
+                          Some("A picture of an example, sitting on an example, with examples dotted around the example landscape.\n\nThis doesn't really make sense, but whatever.")).render(50), vec! {
             ColouredString::uniform("https://example.com/example.png", 'M'),
             ColouredString::general("  A picture of an example, sitting on an example,",
                                     "  mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm"),
@@ -1482,33 +1490,100 @@ fn test_menu_keypress() {
 }
 
 pub struct StatusDisplay {
-    st: Status,
-    via: Option<Account>,
+    sep: SeparatorLine,
+    from: UsernameHeader,
+    via: Option<UsernameHeader>,
+    irt: Option<InReplyToLine>,
+    content: Vec<Paragraph>,
+    media: Vec<Media>,
+    blank: BlankLine,
 }
 
 impl StatusDisplay {
-    pub fn new(st: Status) -> Self {
+    pub fn new(st: Status, client: &mut Client) -> Self {
         let (st, via) = match st.reblog {
             Some(b) => (*b, Some(st.account)),
             None => (st, None),
         };
 
+        let sep = SeparatorLine::new(
+            Some(st.created_at),
+            st.favourited == Some(true),
+            st.reblogged == Some(true));
+
+        let from = UsernameHeader::from(
+            &client.fq(&st.account.acct), &st.account.display_name);
+
+        let via = match via {
+            None => None,
+            Some(booster) => Some(UsernameHeader::via(
+                &client.fq(&booster.acct), &booster.display_name)),
+        };
+
+        let irt = match &st.in_reply_to_id {
+            None => None,
+            Some(id) => {
+                let parent_text = match client.status_by_id(id) {
+                    Ok(st) => parse_html(&st.content),
+                    Err(e) => {
+                        vec! { Paragraph::new().add(&ColouredString::plain(
+                            &format!("[unavailable: {}]", e)
+                        )) }
+                    },
+                };
+                Some(InReplyToLine::new(&parent_text))
+            },
+        };
+
+        let content = parse_html(&st.content);
+
+        let media = st.media_attachments.iter().map(|m| {
+            let desc_ref = match &m.description {
+                Some(s) => Some(&s as &str),
+                None => None,
+            };
+            Media::new(&m.url, desc_ref)
+        }).collect();
+
         StatusDisplay {
-            st: st,
+            sep: sep,
+            from: from,
             via: via,
+            irt: irt,
+            content: content,
+            media: media,
+            blank: BlankLine::new(),
         }
     }
 }
 
+fn push_fragment(lines: &mut Vec<ColouredString>, frag: Vec<ColouredString>) {
+    lines.extend(frag.iter().map(|line| line.to_owned()));
+}
+
 impl TextFragment for StatusDisplay {
     fn render(&self, width: usize) -> Vec<ColouredString> {
         let mut lines = Vec::new();
 
-        let sep = SeparatorLine::new(
-            Some(self.st.created_at),
-            self.st.favourited == Some(true),
-            self.st.reblogged == Some(true));
-        lines.extend(sep.render(width).iter().map(|line| line.to_owned()));
+        push_fragment(&mut lines, self.sep.render(width));
+        push_fragment(&mut lines, self.from.render(width));
+        if let Some(via) = &self.via {
+            push_fragment(&mut lines, via.render(width));
+        }
+        if let Some(irt) = &self.irt {
+            push_fragment(&mut lines,irt.render(width));
+        }
+        push_fragment(&mut lines, self.blank.render(width));
+        for para in &self.content {
+            push_fragment(&mut lines, para.render(width));
+        }
+        if self.content.len() > 0 {
+            push_fragment(&mut lines, self.blank.render(width));
+        }
+        for m in &self.media {
+            push_fragment(&mut lines, m.render(width));
+            push_fragment(&mut lines, self.blank.render(width));
+        }
 
         lines
     }