Files
imks/models/message_read_state.rs
zhenyi 821537186e refactor(tests): reformat code and update dependency management
- Reorganized import statements in adapter tests for better readability
- Replaced or_insert_with(Vec::new) with or_default() in test closures
- Updated Cargo.lock with new dependency versions and checksums
- Added TLS features to tonic dependency configuration
- Included sqlx, chrono, and uuid dependencies with specific features
- Added jsonwebtoken and arc-swap as project dependencies
- Reformatted assertion statements to comply with line length limits
- Adjusted base64 import order in engine codec module
- Updated protobuf include statement formatting
2026-06-11 12:11:05 +08:00

93 lines
2.8 KiB
Rust

//! 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<Uuid>,
/// When the user last opened / scrolled through this channel.
pub last_read_at: Option<DateTime<Utc>>,
/// 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<Utc>,
pub updated_at: DateTime<Utc>,
}
/// 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<MessageReadState> 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);
}
}