//! Thread CRUD operations on `MessageRepo`. use chrono::Utc; use uuid::Uuid; use crate::ImksResult; use crate::models::message_thread::MessageThread; use crate::models::message_thread_participant::MessageThreadParticipant; use super::message_repo::MessageRepo; impl MessageRepo { /// Create a new thread anchored on a root message. pub async fn create_thread( &self, root_message_id: Uuid, channel_id: Uuid, created_by: Uuid, ) -> ImksResult { let id = Uuid::now_v7(); let now = Utc::now(); sqlx::query_as::<_, MessageThread>( r#" INSERT INTO message_thread ( id, channel_id, root_message_id, created_by, replies_count, participants_count, resolved, created_at, updated_at ) VALUES ($1, $2, $3, $4, 0, 0, FALSE, $5, $5) ON CONFLICT (root_message_id) DO NOTHING RETURNING * "#, ) .bind(id) .bind(channel_id) .bind(root_message_id) .bind(created_by) .bind(now) .fetch_optional(self.pool()) .await? .ok_or_else(|| crate::ImksError::InvalidInput("Thread already exists".into())) } /// Get a thread by its ID. pub async fn get_thread(&self, thread_id: Uuid) -> ImksResult> { sqlx::query_as::<_, MessageThread>("SELECT * FROM message_thread WHERE id = $1") .bind(thread_id) .fetch_optional(self.pool()) .await .map_err(Into::into) } /// Get a thread by its root message ID. pub async fn get_thread_by_root( &self, root_message_id: Uuid, ) -> ImksResult> { sqlx::query_as::<_, MessageThread>( "SELECT * FROM message_thread WHERE root_message_id = $1", ) .bind(root_message_id) .fetch_optional(self.pool()) .await .map_err(Into::into) } /// List threads in a channel. pub async fn list_threads(&self, channel_id: Uuid) -> ImksResult> { sqlx::query_as::<_, MessageThread>( r#" SELECT * FROM message_thread WHERE channel_id = $1 ORDER BY last_reply_at DESC NULLS LAST "#, ) .bind(channel_id) .fetch_all(self.pool()) .await .map_err(Into::into) } /// Increment thread reply counter and update last reply info. pub async fn bump_thread(&self, thread_id: Uuid, message_id: Uuid) -> ImksResult<()> { let now = Utc::now(); sqlx::query( r#" UPDATE message_thread SET replies_count = replies_count + 1, last_reply_message_id = $1, last_reply_at = $2, updated_at = $2 WHERE id = $3 "#, ) .bind(message_id) .bind(now) .bind(thread_id) .execute(self.pool()) .await?; Ok(()) } /// Resolve or unresolve a thread. pub async fn resolve_thread( &self, thread_id: Uuid, resolved_by: Uuid, resolved: bool, ) -> ImksResult<()> { if resolved { sqlx::query( r#" UPDATE message_thread SET resolved = TRUE, resolved_by = $1, resolved_at = $2, updated_at = $2 WHERE id = $3 "#, ) .bind(resolved_by) .bind(Utc::now()) .bind(thread_id) .execute(self.pool()) .await?; } else { sqlx::query( r#" UPDATE message_thread SET resolved = FALSE, resolved_by = NULL, resolved_at = NULL, updated_at = $1 WHERE id = $2 "#, ) .bind(Utc::now()) .bind(thread_id) .execute(self.pool()) .await?; } Ok(()) } } impl MessageRepo { /// Add a participant to a thread (or update their join reason). pub async fn add_thread_participant( &self, thread_id: Uuid, user_id: Uuid, joined_reason: &str, ) -> ImksResult { let id = Uuid::now_v7(); let now = Utc::now(); let participant = sqlx::query_as::<_, MessageThreadParticipant>( r#" INSERT INTO message_thread_participant (id, thread_id, user_id, joined_reason, joined_at) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (thread_id, user_id) DO UPDATE SET joined_reason = EXCLUDED.joined_reason RETURNING * "#, ) .bind(id) .bind(thread_id) .bind(user_id) .bind(joined_reason) .bind(now) .fetch_one(self.pool()) .await?; sqlx::query( "UPDATE message_thread SET participants_count = (SELECT COUNT(*) FROM message_thread_participant WHERE thread_id = $1) WHERE id = $1", ) .bind(thread_id) .execute(self.pool()) .await?; Ok(participant) } /// Remove a participant from a thread. pub async fn remove_thread_participant( &self, thread_id: Uuid, user_id: Uuid, ) -> ImksResult { let result = sqlx::query( "DELETE FROM message_thread_participant WHERE thread_id = $1 AND user_id = $2", ) .bind(thread_id) .bind(user_id) .execute(self.pool()) .await?; if result.rows_affected() > 0 { sqlx::query( "UPDATE message_thread SET participants_count = GREATEST(participants_count - 1, 0) WHERE id = $1", ) .bind(thread_id) .execute(self.pool()) .await?; } Ok(result.rows_affected() > 0) } /// List all participants in a thread. pub async fn list_thread_participants( &self, thread_id: Uuid, ) -> ImksResult> { sqlx::query_as::<_, MessageThreadParticipant>( "SELECT * FROM message_thread_participant WHERE thread_id = $1 ORDER BY joined_at", ) .bind(thread_id) .fetch_all(self.pool()) .await .map_err(Into::into) } }