notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

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 }