From: Simon Tatham Date: Fri, 29 Dec 2023 14:18:28 +0000 (+0000) Subject: Basically correct-looking display of toots \o/ X-Git-Url: https://www.chiark.greenend.org.uk/ucgi/~ian/git?a=commitdiff_plain;h=e38e24c487074ca4a7f00bd8edd6f28a4079e30b;p=mastodonochrome.git Basically correct-looking display of toots \o/ --- diff --git a/src/client.rs b/src/client.rs index a2120d5..b9393d4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -50,6 +50,21 @@ impl From 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> { diff --git a/src/file.rs b/src/file.rs index da7e921..ad2c8b4 100644 --- a/src/file.rs +++ b/src/file.rs @@ -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))); } } } diff --git a/src/text.rs b/src/text.rs index f19e62f..e55c434 100644 --- a/src/text.rs +++ b/src/text.rs @@ -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, + sep: SeparatorLine, + from: UsernameHeader, + via: Option, + irt: Option, + content: Vec, + media: Vec, + 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, frag: Vec) { + lines.extend(frag.iter().map(|line| line.to_owned())); +} + impl TextFragment for StatusDisplay { fn render(&self, width: usize) -> Vec { 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 }