//! Read state CRUD operations on `MessageRepo`. //! //! One row per (channel, user). Upserted on each read; ON CONFLICT DO UPDATE //! advances the cursor and recalculates unread counts. use chrono::Utc; use uuid::Uuid; use crate::ImksResult; use crate::models::message_read_state::MessageReadState; use super::message_repo::MessageRepo; impl MessageRepo { /// Mark a channel as read up to a given message for a user. /// Recalculates unread_count and unread_mentions from the database. pub async fn mark_read( &self, channel_id: Uuid, user_id: Uuid, last_read_message_id: Uuid, ) -> ImksResult { let now = Utc::now(); let unread_count: i64 = sqlx::query_scalar( r#" SELECT COUNT(*)::BIGINT FROM message WHERE channel_id = $1 AND deleted_at IS NULL AND id > $2 AND author_id != $3 "#, ) .bind(channel_id) .bind(last_read_message_id) .bind(user_id) .fetch_one(self.pool()) .await?; let unread_mentions: i64 = sqlx::query_scalar( r#" SELECT COUNT(*)::BIGINT FROM message_mention mm WHERE mm.channel_id = $1 AND mm.mentioned_user_id = $2 AND mm.message_id > $3 AND mm.read_at IS NULL "#, ) .bind(channel_id) .bind(user_id) .bind(last_read_message_id) .fetch_one(self.pool()) .await?; let id = Uuid::now_v7(); sqlx::query_as::<_, MessageReadState>( r#" INSERT INTO message_read_state ( id, channel_id, user_id, last_read_message_id, last_read_at, unread_count, unread_mentions, created_at, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $8) ON CONFLICT (channel_id, user_id) DO UPDATE SET last_read_message_id = EXCLUDED.last_read_message_id, last_read_at = EXCLUDED.last_read_at, unread_count = EXCLUDED.unread_count, unread_mentions = EXCLUDED.unread_mentions, updated_at = EXCLUDED.updated_at RETURNING * "#, ) .bind(id) .bind(channel_id) .bind(user_id) .bind(last_read_message_id) .bind(now) .bind(unread_count) .bind(unread_mentions) .fetch_one(self.pool()) .await .map_err(Into::into) } /// Get a user's read state for a channel. pub async fn get_read_state( &self, channel_id: Uuid, user_id: Uuid, ) -> ImksResult> { sqlx::query_as::<_, MessageReadState>( "SELECT * FROM message_read_state WHERE channel_id = $1 AND user_id = $2", ) .bind(channel_id) .bind(user_id) .fetch_optional(self.pool()) .await .map_err(Into::into) } /// Get read state summaries for all channels a user participates in. pub async fn get_user_read_states(&self, user_id: Uuid) -> ImksResult> { sqlx::query_as::<_, MessageReadState>("SELECT * FROM message_read_state WHERE user_id = $1") .bind(user_id) .fetch_all(self.pool()) .await .map_err(Into::into) } }