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}