convo_renderable.rs (5398B)
1 use chrono::{DateTime, Local, NaiveDate, NaiveDateTime, Utc}; 2 use nostrdb::NoteKey; 3 4 use crate::cache::NotePkg; 5 6 pub struct ConversationRenderable { 7 items: Vec<ConversationItem>, 8 } 9 10 #[derive(Debug)] 11 pub enum ConversationItem { 12 Date(NaiveDate), 13 Message { msg_type: MessageType, key: NoteKey }, 14 } 15 16 #[derive(PartialEq, Copy, Clone, Debug)] 17 pub enum MessageType { 18 Standalone, 19 FirstInSeries, 20 MiddleInSeries, 21 LastInSeries, 22 } 23 24 impl ConversationRenderable { 25 pub fn new(ordered_msgs: &[NotePkg]) -> Self { 26 Self { 27 items: generate_conversation_renderable(ordered_msgs), 28 } 29 } 30 31 pub fn get(&self, index: usize) -> Option<&ConversationItem> { 32 self.items.get(index) 33 } 34 35 pub fn len(&self) -> usize { 36 self.items.len() 37 } 38 39 pub fn is_empty(&self) -> bool { 40 self.items.is_empty() 41 } 42 } 43 44 // ordered_msgs ordered from newest to oldest. Need it ordered oldest to newest 45 fn generate_conversation_renderable(ordered_msgs: &[NotePkg]) -> Vec<ConversationItem> { 46 let mut items = Vec::with_capacity(ordered_msgs.len()); 47 let midnight_anchor = local_midnight_anchor(); 48 49 let mut iter = ordered_msgs.iter().rev(); 50 51 let Some(mut prev) = iter.next() else { 52 return vec![]; 53 }; 54 55 let mut prev_anchor_dist = days_since_anchor(&midnight_anchor, prev.note_ref.created_at); 56 57 items.push(ConversationItem::Date(prev_anchor_dist.dt)); 58 59 let Some(mut cur) = iter.next() else { 60 items.push(ConversationItem::Message { 61 msg_type: cur_message_type( 62 None, 63 AnchoredPkg { 64 pkg: prev, 65 distance: &prev_anchor_dist, 66 }, 67 None, 68 ), 69 key: prev.note_ref.key, 70 }); 71 return items; 72 }; 73 74 let mut cur_anchor_dist = days_since_anchor(&midnight_anchor, cur.note_ref.created_at); 75 items.push(ConversationItem::Message { 76 msg_type: cur_message_type( 77 None, 78 AnchoredPkg { 79 pkg: prev, 80 distance: &prev_anchor_dist, 81 }, 82 Some(AnchoredPkg { 83 pkg: cur, 84 distance: &cur_anchor_dist, 85 }), 86 ), 87 key: prev.note_ref.key, 88 }); 89 90 for next in iter { 91 if prev_anchor_dist.days_from_anchor != cur_anchor_dist.days_from_anchor { 92 items.push(ConversationItem::Date(cur_anchor_dist.dt)); 93 } 94 95 let next_anchor_dist = days_since_anchor(&midnight_anchor, next.note_ref.created_at); 96 items.push(ConversationItem::Message { 97 msg_type: cur_message_type( 98 Some(AnchoredPkg { 99 pkg: prev, 100 distance: &prev_anchor_dist, 101 }), 102 AnchoredPkg { 103 pkg: cur, 104 distance: &cur_anchor_dist, 105 }, 106 Some(AnchoredPkg { 107 pkg: next, 108 distance: &next_anchor_dist, 109 }), 110 ), 111 key: cur.note_ref.key, 112 }); 113 114 prev = cur; 115 prev_anchor_dist = cur_anchor_dist; 116 cur = next; 117 cur_anchor_dist = next_anchor_dist; 118 } 119 120 if prev_anchor_dist.days_from_anchor != cur_anchor_dist.days_from_anchor { 121 items.push(ConversationItem::Date(cur_anchor_dist.dt)); 122 } 123 124 items.push(ConversationItem::Message { 125 msg_type: cur_message_type( 126 Some(AnchoredPkg { 127 pkg: prev, 128 distance: &prev_anchor_dist, 129 }), 130 AnchoredPkg { 131 pkg: cur, 132 distance: &cur_anchor_dist, 133 }, 134 None, 135 ), 136 key: cur.note_ref.key, 137 }); 138 139 items 140 } 141 142 struct AnchoredPkg<'a> { 143 pkg: &'a NotePkg, 144 distance: &'a AnchorDistance, 145 } 146 147 static GROUPING_SECS: u64 = 60; 148 fn cur_message_type( 149 prev: Option<AnchoredPkg>, 150 cur: AnchoredPkg, 151 next: Option<AnchoredPkg>, 152 ) -> MessageType { 153 let prev_link = prev.as_ref().is_some_and(|p| series_between(&cur, p)); 154 let next_link = next.as_ref().is_some_and(|n| series_between(&cur, n)); 155 156 match (prev_link, next_link) { 157 (false, false) => MessageType::Standalone, 158 (false, true) => MessageType::FirstInSeries, 159 (true, false) => MessageType::LastInSeries, 160 (true, true) => MessageType::MiddleInSeries, 161 } 162 } 163 164 fn series_between(a: &AnchoredPkg, b: &AnchoredPkg) -> bool { 165 a.distance.days_from_anchor == b.distance.days_from_anchor 166 && a.pkg.author == b.pkg.author 167 && a.distance.unix_ts.abs_diff(b.distance.unix_ts) < GROUPING_SECS 168 } 169 170 fn local_midnight_anchor() -> NaiveDateTime { 171 let epoch_utc = DateTime::<Utc>::UNIX_EPOCH; 172 let epoch_local = epoch_utc.with_timezone(&Local); 173 174 epoch_local.date_naive().and_hms_opt(0, 0, 0).unwrap() 175 } 176 177 fn days_since_anchor(anchor: &NaiveDateTime, timestamp: u64) -> AnchorDistance { 178 let dt = DateTime::from_timestamp(timestamp as i64, 0) 179 .unwrap() 180 .with_timezone(&Local) 181 .naive_local(); 182 183 AnchorDistance { 184 days_from_anchor: anchor.signed_duration_since(dt).num_days() as u64, 185 dt: dt.date(), 186 unix_ts: timestamp, 187 } 188 } 189 190 struct AnchorDistance { 191 days_from_anchor: u64, // distance in anchor, in days 192 unix_ts: u64, 193 dt: NaiveDate, 194 }