//! Per-user read state — maps to `message_read_state` table. //! //! Tracks the last message each user has read in each channel. //! Used for unread badges, "mark as read", and notification suppression. //! One row per (channel_id, user_id), upserted on each read. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// A user's read progress in one channel. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageReadState { pub id: Uuid, pub channel_id: Uuid, pub user_id: Uuid, /// The last message id the user has read (cursor). pub last_read_message_id: Option, /// When the user last opened / scrolled through this channel. pub last_read_at: Option>, /// Total unread message count (denormalized for fast badge display). pub unread_count: i64, /// Total unread @mentions for this channel (denormalized). pub unread_mentions: i64, pub created_at: DateTime, pub updated_at: DateTime, } /// Summary of a user's read state for the client-side channel list. /// /// Includes unread badge count and mention count for a single channel. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReadStateSummary { pub channel_id: Uuid, pub unread_count: i64, pub unread_mentions: i64, pub has_unread: bool, } impl From for ReadStateSummary { fn from(s: MessageReadState) -> Self { Self { channel_id: s.channel_id, unread_count: s.unread_count, unread_mentions: s.unread_mentions, has_unread: s.unread_count > 0, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_read_state_summary_conversion() { let state = MessageReadState { id: Uuid::now_v7(), channel_id: Uuid::now_v7(), user_id: Uuid::now_v7(), last_read_message_id: None, last_read_at: None, unread_count: 5, unread_mentions: 2, created_at: Utc::now(), updated_at: Utc::now(), }; let summary: ReadStateSummary = state.into(); assert!(summary.has_unread); assert_eq!(summary.unread_count, 5); assert_eq!(summary.unread_mentions, 2); } #[test] fn test_read_state_summary_no_unread() { let state = MessageReadState { id: Uuid::now_v7(), channel_id: Uuid::now_v7(), user_id: Uuid::now_v7(), last_read_message_id: None, last_read_at: None, unread_count: 0, unread_mentions: 0, created_at: Utc::now(), updated_at: Utc::now(), }; let summary: ReadStateSummary = state.into(); assert!(!summary.has_unread); } }