Skip to main content

nmp_threading/
block.rs

1//! `TimelineBlock` — the grouper's output unit. A timeline payload is a
2//! `Vec<TimelineBlock>`; each block renders either as one standalone event
3//! card or as a Twitter-style stacked module with a connecting vertical line.
4
5use nmp_core::substrate::EventId;
6use serde::{Deserialize, Serialize};
7
8use crate::pointer::ThreadPointer;
9
10/// Either one event on its own, or a chained module of contextually related
11/// events (root-first newest-last when fully chained; see [`crate::Grouper`]).
12#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
13pub enum TimelineBlock {
14    Standalone {
15        /// The single event rendered by this block.
16        id: EventId,
17        /// Terminal root pointer resolved for the event. `None` when the
18        /// event is itself a thread root; `Some` when the event is a reply
19        /// that could not be stitched into a chain (parent absent, leaf
20        /// taken, or `max_module_size` hit). Preserving the pointer lets
21        /// renderers flag the block as a partial-chain head rather than
22        /// mistaking the reply for a root.
23        #[serde(default, skip_serializing_if = "Option::is_none")]
24        root: Option<ThreadPointer>,
25    },
26    Module {
27        /// Event ids in display order: root-first (oldest) to leaf (newest).
28        events: Vec<EventId>,
29        /// True when an ancestor in the chain is missing from the local store
30        /// OR the lookback between adjacent events exceeded `ModulePolicy
31        /// ::max_lookback_gap_secs` OR the chain's resolved root pointer is
32        /// not the top event's id.
33        has_gap: bool,
34        /// Terminal root pointer used for adjacent-module collapse. `None`
35        /// when the module's top event is itself a thread root.
36        #[serde(default, skip_serializing_if = "Option::is_none")]
37        root: Option<ThreadPointer>,
38    },
39}
40
41impl TimelineBlock {
42    /// Length of the block in events (1 for standalone).
43    #[must_use]
44    pub fn len(&self) -> usize {
45        match self {
46            Self::Standalone { .. } => 1,
47            Self::Module { events, .. } => events.len(),
48        }
49    }
50
51    /// True when the block carries no events. Always `false` in practice —
52    /// the grouper never emits empty modules.
53    #[must_use]
54    pub fn is_empty(&self) -> bool {
55        self.len() == 0
56    }
57}