821537186e
- 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
93 lines
2.8 KiB
Rust
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);
|
|
}
|
|
}
|