//! Pin CRUD operations on `MessageRepo`. //! //! One row per pinned message. Channels can have multiple pinned messages. //! `position` auto-calculated as `MAX(position) + 1` within the channel. use chrono::Utc; use sqlx::Row; use uuid::Uuid; use crate::ImksResult; use crate::models::message_pin::{MessagePin, PinDetail}; use super::message_repo::MessageRepo; impl MessageRepo { /// Pin a message in a channel. Computes the next position automatically. pub async fn pin_message( &self, channel_id: Uuid, message_id: Uuid, pinned_by: Uuid, ) -> ImksResult { let id = Uuid::now_v7(); let now = Utc::now(); let mut tx = self.pool().begin().await?; sqlx::query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))") .bind(channel_id.to_string()) .execute(&mut *tx) .await?; let max_pos: Option = sqlx::query_scalar( "SELECT COALESCE(MAX(position), -1) FROM message_pin WHERE channel_id = $1", ) .bind(channel_id) .fetch_one(&mut *tx) .await?; let position = max_pos.unwrap_or(-1) + 1; let pin = sqlx::query_as::<_, MessagePin>( r#" INSERT INTO message_pin (id, channel_id, message_id, pinned_by, position, created_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (channel_id, message_id) DO NOTHING RETURNING * "#, ) .bind(id) .bind(channel_id) .bind(message_id) .bind(pinned_by) .bind(position) .bind(now) .fetch_optional(&mut *tx) .await? .ok_or_else(|| crate::ImksError::InvalidInput("Message already pinned".into()))?; tx.commit().await?; Ok(pin) } /// Unpin a message from a channel. pub async fn unpin_message(&self, channel_id: Uuid, message_id: Uuid) -> ImksResult { let result = sqlx::query("DELETE FROM message_pin WHERE channel_id = $1 AND message_id = $2") .bind(channel_id) .bind(message_id) .execute(self.pool()) .await?; Ok(result.rows_affected() > 0) } /// List all pinned messages in a channel, newest first, joined with message content. pub async fn list_pins(&self, channel_id: Uuid) -> ImksResult> { let rows = sqlx::query( r#" SELECT p.*, m.body AS message_body, m.author_id AS message_author_id, m.created_at AS message_created_at FROM message_pin p JOIN message m ON m.id = p.message_id WHERE p.channel_id = $1 AND m.deleted_at IS NULL ORDER BY p.position ASC "#, ) .bind(channel_id) .fetch_all(self.pool()) .await?; let result = rows .into_iter() .map(|row| PinDetail { pin: MessagePin { id: row.get("id"), channel_id: row.get("channel_id"), message_id: row.get("message_id"), pinned_by: row.get("pinned_by"), position: row.get("position"), created_at: row.get("created_at"), }, message_body: row.get("message_body"), message_author_id: row.get("message_author_id"), message_created_at: row.get("message_created_at"), }) .collect(); Ok(result) } }