//! Message read operations — get single, list by channel, list by thread. //! //! All list queries use UUID v7 cursor-based pagination (no OFFSET). use sqlx::Row; use uuid::Uuid; use crate::ImksResult; use crate::models::message::Message; use super::message_repo::MessageRepo; use super::pagination::{CursorPage, clamp_limit}; impl MessageRepo { /// Fetch a single message by ID. /// /// Returns `None` if the message doesn't exist or has been soft-deleted. pub async fn get(&self, message_id: Uuid) -> ImksResult> { let row = sqlx::query_as::<_, Message>( "SELECT * FROM message WHERE id = $1 AND deleted_at IS NULL", ) .bind(message_id) .fetch_optional(self.pool()) .await?; Ok(row) } /// List messages in a channel with cursor-based pagination. /// /// Returns messages in reverse chronological order (newest first). /// Pass `before` as the last message ID from the previous page to /// fetch the next page. pub async fn list_by_channel( &self, channel_id: Uuid, before: Option, limit: Option, ) -> ImksResult> { let effective_limit = clamp_limit(limit); // Fetch one extra row to determine `has_more`. let fetch_limit = effective_limit + 1; let rows = match before { Some(cursor) => { sqlx::query_as::<_, Message>( r#" SELECT * FROM message WHERE channel_id = $1 AND deleted_at IS NULL AND id < $2 ORDER BY id DESC LIMIT $3 "#, ) .bind(channel_id) .bind(cursor) .bind(fetch_limit) .fetch_all(self.pool()) .await? } None => { sqlx::query_as::<_, Message>( r#" SELECT * FROM message WHERE channel_id = $1 AND deleted_at IS NULL ORDER BY id DESC LIMIT $2 "#, ) .bind(channel_id) .bind(fetch_limit) .fetch_all(self.pool()) .await? } }; Ok(CursorPage::from_raw(rows, effective_limit, |m| m.id)) } /// List messages in a thread with cursor-based pagination. pub async fn list_by_thread( &self, thread_id: Uuid, before: Option, limit: Option, ) -> ImksResult> { let effective_limit = clamp_limit(limit); let fetch_limit = effective_limit + 1; let rows = match before { Some(cursor) => { sqlx::query_as::<_, Message>( r#" SELECT * FROM message WHERE thread_id = $1 AND deleted_at IS NULL AND id < $2 ORDER BY id DESC LIMIT $3 "#, ) .bind(thread_id) .bind(cursor) .bind(fetch_limit) .fetch_all(self.pool()) .await? } None => { sqlx::query_as::<_, Message>( r#" SELECT * FROM message WHERE thread_id = $1 AND deleted_at IS NULL ORDER BY id DESC LIMIT $2 "#, ) .bind(thread_id) .bind(fetch_limit) .fetch_all(self.pool()) .await? } }; Ok(CursorPage::from_raw(rows, effective_limit, |m| m.id)) } /// Get message reaction counts grouped by emoji content. /// /// Returns `(content, count)` pairs for the given message. pub async fn get_reaction_counts(&self, message_id: Uuid) -> ImksResult> { let rows = sqlx::query( r#" SELECT content, COUNT(*)::BIGINT AS cnt FROM message_reaction WHERE message_id = $1 GROUP BY content "#, ) .bind(message_id) .fetch_all(self.pool()) .await?; Ok(rows .into_iter() .map(|r| (r.get("content"), r.get("cnt"))) .collect()) } /// Count attachments and embeds for a message. pub async fn get_content_counts(&self, message_id: Uuid) -> ImksResult<(i64, i64)> { let att_row = sqlx::query( "SELECT COUNT(*)::BIGINT AS cnt FROM message_attachment WHERE message_id = $1", ) .bind(message_id) .fetch_one(self.pool()) .await?; let emb_row = sqlx::query("SELECT COUNT(*)::BIGINT AS cnt FROM message_embed WHERE message_id = $1") .bind(message_id) .fetch_one(self.pool()) .await?; Ok((att_row.get("cnt"), emb_row.get("cnt"))) } }