This commit is contained in:
2026-09-11 08:52:04 +07:00
parent b7a020767e
commit b7221ef814
8 changed files with 895 additions and 190 deletions
+67 -13
View File
@@ -42,6 +42,19 @@ impl InboxItem {
pub fn is_unread(&self) -> bool {
!self.archived && !self.unread_ids.is_empty()
}
/// Recompute the unread and archived flags from `state`.
pub fn apply_state(&mut self, state: &InboxReadState) {
self.unread_ids = self
.events
.iter()
.rev()
.filter(|event| !state.is_read(event))
.map(|event| event.id)
.collect();
self.archived = self.events.iter().all(|event| state.is_archived(event));
}
}
/// Root issue, patch or pull request of a notification event.
@@ -112,25 +125,18 @@ where
let root_event = lookup(root);
let unread_ids = events
.iter()
.rev()
.filter(|event| !state.is_read(event))
.map(|event| event.id)
.collect();
let archived = events.iter().all(|event| state.is_archived(event));
InboxItem {
let mut item = InboxItem {
root,
root_kind: root_event.as_ref().map(|event| event.kind),
address: root_event
.as_ref()
.and_then(|event| event.tags.coordinates().next()),
events,
unread_ids,
archived,
}
unread_ids: Vec::new(),
archived: false,
};
item.apply_state(state);
item
})
.collect();
@@ -174,6 +180,13 @@ impl InboxReadState {
}
}
/// Mark one event archived. Events at or before the cutoff are already archived.
pub fn mark_archived(&mut self, event: &Event) {
if event.created_at > self.archived_before {
self.archived_ids.insert(event.id);
}
}
/// Mark every non-self event read, anchoring the cutoff ten days back.
pub fn mark_all_read(&mut self, all: &[Event], me: PublicKey, now: Timestamp) {
let cutoff = now - MARK_ALL_WINDOW;
@@ -626,6 +639,47 @@ mod tests {
assert_eq!(state.read_ids, HashSet::from([fresh.id]));
}
#[test]
fn mark_archived_skips_events_at_or_before_the_cutoff() {
let now = Timestamp::from_secs(1_000_000_000);
let event = issue(&keys(2), now.as_secs() - 1000);
let mut state = InboxReadState {
archived_before: now,
..Default::default()
};
state.mark_archived(&event);
assert!(state.archived_ids.is_empty());
let mut state = InboxReadState::default();
state.mark_archived(&event);
assert_eq!(state.archived_ids, HashSet::from([event.id]));
}
#[test]
fn apply_state_recomputes_unread_and_archived() {
let now = Timestamp::from_secs(1_000_000_000);
let first = issue(&keys(2), now.as_secs() - 2000);
let second = issue(&keys(2), now.as_secs() - 1000);
let mut item = InboxItem {
root: first.id,
root_kind: None,
address: None,
events: vec![second.clone(), first.clone()],
unread_ids: Vec::new(),
archived: false,
};
let state = InboxReadState {
read_before: first.created_at,
..Default::default()
};
item.apply_state(&state);
assert_eq!(item.unread_ids, vec![second.id]);
assert!(!item.archived);
}
#[test]
fn serde_round_trip_preserves_state() {
let first = issue(&keys(1), 100);