Undisclosed project number 1234
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(render): social row types

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 22, 2026, 11:25 PM +0300) commit b16114ba parent 770d1f6b change-id ptsmyzur
+186 -15
+1
Cargo.lock
··· 6005 6005 "tabled", 6006 6006 "terminal_size", 6007 6007 "thiserror 2.0.18", 6008 + "unicode-width 0.2.2", 6008 6009 ] 6009 6010 6010 6011 [[package]]
+1
Cargo.toml
··· 58 58 owo-colors = { version = "4", features = ["supports-colors"] } 59 59 is-terminal = "0.4" 60 60 terminal_size = "0.4" 61 + unicode-width = "0.2" 61 62 flate2 = "1" 62 63 63 64 superjam-core = { path = "crates/superjam-core" }
+1
crates/superjam-render/Cargo.toml
··· 19 19 owo-colors.workspace = true 20 20 is-terminal.workspace = true 21 21 terminal_size.workspace = true 22 + unicode-width.workspace = true
+9 -6
crates/superjam-render/src/lib.rs
··· 16 16 pub use text::StateBadge; 17 17 pub use tree::CommentNode; 18 18 pub use views::{ 19 - AuthIdentitiesView, AuthIdentityRow, AuthStatusView, FollowerCount, FollowingCount, 20 - ISSUE_LIST_COLUMNS, IssueListRow, IssueViewCard, MessageView, PULL_LIST_COLUMNS, ProfileBody, 21 - ProfileCardView, ProfileCounts, PullInterdiffView, PullListRow, PullPatchView, PullRoundEntry, 22 - PullViewCard, REPO_LIST_COLUMNS, RemovedRecordView, RepoCardView, RepoCount, RepoListRow, 23 - RepoUrlView, StarCount, WroteBatchItem, WroteBatchView, WroteRecordView, issue_list_row_cells, 24 - pull_list_row_cells, repo_list_row_cells, 19 + AuthIdentitiesView, AuthIdentityRow, AuthStatusView, FOLLOW_LIST_COLUMNS, FollowListRow, 20 + FollowerCount, FollowingCount, ISSUE_LIST_COLUMNS, IssueListRow, IssueViewCard, MessageView, 21 + PULL_LIST_COLUMNS, ProfileBody, ProfileCardView, ProfileCounts, PullInterdiffView, PullListRow, 22 + PullPatchView, PullRoundEntry, PullViewCard, REACTION_LIST_COLUMNS, REPO_LIST_COLUMNS, 23 + ReactionListRow, RemovedRecordView, RepoCardView, RepoCount, RepoListRow, RepoUrlView, 24 + STAR_LIST_COLUMNS, STAR_ON_REPO_COLUMNS, StarCount, StarListRow, StarSubjectKind, 25 + WroteBatchItem, WroteBatchView, WroteRecordView, follow_list_row_cells, issue_list_row_cells, 26 + pull_list_row_cells, reaction_list_row_cells, repo_list_row_cells, star_list_row_cells, 27 + star_on_repo_row_cells, 25 28 };
+34 -9
crates/superjam-render/src/table.rs
··· 88 88 } 89 89 90 90 pub(crate) fn visible_width(s: &str) -> usize { 91 + use unicode_width::UnicodeWidthChar; 91 92 s.chars() 92 - .fold((false, 0usize), |(in_escape, count), c| { 93 - match (in_escape, c) { 94 - (false, '\u{1b}') => (true, count), 95 - (true, c) if c.is_ascii_alphabetic() => (false, count), 96 - (true, _) => (true, count), 97 - (false, _) => (false, count + 1), 98 - } 99 - }) 100 - .1 93 + .fold( 94 + (false, false, 0usize), 95 + |(in_escape, prev_zwj, count), c| match (in_escape, prev_zwj, c) { 96 + (false, _, '\u{1b}') => (true, false, count), 97 + (true, _, c) if c.is_ascii_alphabetic() => (false, false, count), 98 + (true, _, _) => (true, false, count), 99 + (false, _, '\u{FE0F}') => (false, false, count + 1), 100 + (false, _, '\u{200D}') => (false, true, count), 101 + (false, true, _) => (false, false, count), 102 + (false, false, _) => (false, false, count + c.width().unwrap_or(0)), 103 + }, 104 + ) 105 + .2 101 106 } 102 107 103 108 pub(crate) fn pad_cell(cell: &str, width: usize) -> String { ··· 157 162 assert_eq!(visible_width(colored), 3); 158 163 let combined = "\x1b[1mA\x1b[0m \x1b[32mB\x1b[0m"; 159 164 assert_eq!(visible_width(combined), 3); 165 + } 166 + 167 + #[test] 168 + fn visible_width_counts_emoji_with_vs16() { 169 + assert_eq!(visible_width("\u{2764}\u{FE0F}"), 2); 170 + assert_eq!(visible_width("a\u{2764}\u{FE0F}b"), 4); 171 + assert_eq!(visible_width("\u{1F980}"), 2); 172 + } 173 + 174 + #[test] 175 + fn visible_width_collapses_zwj_sequences() { 176 + assert_eq!(visible_width("\u{1F469}\u{200D}\u{1F680}"), 2); 177 + assert_eq!( 178 + visible_width("\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"), 179 + 2, 180 + ); 181 + assert_eq!( 182 + visible_width("\u{1F3F3}\u{FE0F}\u{200D}\u{1F308}"), 183 + 2, 184 + ); 160 185 } 161 186 162 187 #[test]
+6
crates/superjam-render/src/views/mod.rs
··· 5 5 mod profile; 6 6 mod pull; 7 7 mod repo; 8 + mod social; 8 9 9 10 pub use auth::{AuthIdentitiesView, AuthIdentityRow, AuthStatusView}; 10 11 pub use issue::{ISSUE_LIST_COLUMNS, IssueListRow, IssueViewCard, issue_list_row_cells}; ··· 19 20 pull_list_row_cells, 20 21 }; 21 22 pub use repo::{REPO_LIST_COLUMNS, RepoCardView, RepoListRow, RepoUrlView, repo_list_row_cells}; 23 + pub use social::{ 24 + FOLLOW_LIST_COLUMNS, FollowListRow, REACTION_LIST_COLUMNS, ReactionListRow, 25 + STAR_LIST_COLUMNS, STAR_ON_REPO_COLUMNS, StarListRow, StarSubjectKind, follow_list_row_cells, 26 + reaction_list_row_cells, star_list_row_cells, star_on_repo_row_cells, 27 + };
+134
crates/superjam-render/src/views/social.rs
··· 1 + use jacquard_common::DefaultStr; 2 + use jacquard_common::types::aturi::AtUri; 3 + use jacquard_common::types::cid::Cid; 4 + use jacquard_common::types::string::Datetime; 5 + use serde::Serialize; 6 + use superjam_core::Rkey; 7 + 8 + use crate::palette::Palette; 9 + use crate::table::Column; 10 + use crate::text; 11 + 12 + #[derive(Copy, Clone, Debug, Serialize, PartialEq, Eq)] 13 + #[serde(rename_all = "lowercase")] 14 + pub enum StarSubjectKind { 15 + Repo, 16 + String, 17 + } 18 + 19 + #[derive(Clone, Debug, Serialize)] 20 + pub struct StarListRow { 21 + pub uri: AtUri<DefaultStr>, 22 + #[serde(skip_serializing_if = "Option::is_none")] 23 + pub cid: Option<Cid<DefaultStr>>, 24 + pub rkey: Rkey<DefaultStr>, 25 + pub actor: String, 26 + #[serde(rename = "subjectKind")] 27 + pub subject_kind: StarSubjectKind, 28 + pub subject: String, 29 + #[serde(rename = "createdAt")] 30 + pub created_at: Datetime, 31 + } 32 + 33 + pub const STAR_LIST_COLUMNS: &[Column] = &[ 34 + Column::new("rkey").flex(0).min_width(13), 35 + Column::new("actor").flex(1), 36 + Column::new("kind").flex(0).min_width(4), 37 + Column::new("subject").flex(3), 38 + Column::new("when").flex(0).min_width(10), 39 + ]; 40 + 41 + pub const STAR_ON_REPO_COLUMNS: &[Column] = &[ 42 + Column::new("rkey").flex(0).min_width(13), 43 + Column::new("actor").flex(1), 44 + Column::new("when").flex(0).min_width(10), 45 + ]; 46 + 47 + pub fn star_list_row_cells(row: &StarListRow, palette: Palette) -> Vec<String> { 48 + let kind = match row.subject_kind { 49 + StarSubjectKind::Repo => "repo", 50 + StarSubjectKind::String => "str", 51 + }; 52 + vec![ 53 + palette.accent(row.rkey.as_str()).into_owned(), 54 + row.actor.clone(), 55 + kind.to_owned(), 56 + row.subject.clone(), 57 + palette 58 + .muted(text::date_prefix(row.created_at.as_str())) 59 + .into_owned(), 60 + ] 61 + } 62 + 63 + pub fn star_on_repo_row_cells(row: &StarListRow, palette: Palette) -> Vec<String> { 64 + vec![ 65 + palette.accent(row.rkey.as_str()).into_owned(), 66 + row.actor.clone(), 67 + palette 68 + .muted(text::date_prefix(row.created_at.as_str())) 69 + .into_owned(), 70 + ] 71 + } 72 + 73 + #[derive(Clone, Debug, Serialize)] 74 + pub struct FollowListRow { 75 + pub uri: AtUri<DefaultStr>, 76 + #[serde(skip_serializing_if = "Option::is_none")] 77 + pub cid: Option<Cid<DefaultStr>>, 78 + pub rkey: Rkey<DefaultStr>, 79 + pub actor: String, 80 + pub subject: String, 81 + #[serde(rename = "createdAt")] 82 + pub created_at: Datetime, 83 + } 84 + 85 + pub const FOLLOW_LIST_COLUMNS: &[Column] = &[ 86 + Column::new("rkey").flex(0).min_width(13), 87 + Column::new("actor").flex(1), 88 + Column::new("subject").flex(1), 89 + Column::new("when").flex(0).min_width(10), 90 + ]; 91 + 92 + pub fn follow_list_row_cells(row: &FollowListRow, palette: Palette) -> Vec<String> { 93 + vec![ 94 + palette.accent(row.rkey.as_str()).into_owned(), 95 + row.actor.clone(), 96 + row.subject.clone(), 97 + palette 98 + .muted(text::date_prefix(row.created_at.as_str())) 99 + .into_owned(), 100 + ] 101 + } 102 + 103 + #[derive(Clone, Debug, Serialize)] 104 + pub struct ReactionListRow { 105 + pub uri: AtUri<DefaultStr>, 106 + #[serde(skip_serializing_if = "Option::is_none")] 107 + pub cid: Option<Cid<DefaultStr>>, 108 + pub rkey: Rkey<DefaultStr>, 109 + pub actor: String, 110 + pub emoji: String, 111 + pub subject: String, 112 + #[serde(rename = "createdAt")] 113 + pub created_at: Datetime, 114 + } 115 + 116 + pub const REACTION_LIST_COLUMNS: &[Column] = &[ 117 + Column::new("rkey").flex(0).min_width(13), 118 + Column::new("actor").flex(1), 119 + Column::new("emoji").flex(0).min_width(3), 120 + Column::new("subject").flex(3), 121 + Column::new("when").flex(0).min_width(10), 122 + ]; 123 + 124 + pub fn reaction_list_row_cells(row: &ReactionListRow, palette: Palette) -> Vec<String> { 125 + vec![ 126 + palette.accent(row.rkey.as_str()).into_owned(), 127 + row.actor.clone(), 128 + row.emoji.clone(), 129 + row.subject.clone(), 130 + palette 131 + .muted(text::date_prefix(row.created_at.as_str())) 132 + .into_owned(), 133 + ] 134 + }