//! Thread participant membership — maps to `message_thread_participant` table. //! //! Tracks which users are part of a thread. A user becomes a participant when //! they reply in a thread, get @mentioned, or are explicitly added. //! Without this table, `message_thread.participants_count` is un-verifiable. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// A user participating in a thread. /// /// Maps to the `message_thread_participant` table. Users become participants /// when they reply, get @mentioned, or are explicitly added. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageThreadParticipant { pub id: Uuid, pub thread_id: Uuid, pub user_id: Uuid, /// How the user joined the thread. pub joined_reason: Option, pub last_read_message_id: Option, pub last_read_at: Option>, pub joined_at: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum JoinReason { /// User sent a reply in the thread. #[default] Reply, /// User was @mentioned in a thread message. Mentioned, /// User was explicitly added by another participant. Added, /// User joined the thread themselves. Joined, } impl JoinReason { pub fn as_str(&self) -> &'static str { match self { Self::Reply => "reply", Self::Mentioned => "mentioned", Self::Added => "added", Self::Joined => "joined", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_participant_serialize() { let p = MessageThreadParticipant { id: Uuid::now_v7(), thread_id: Uuid::now_v7(), user_id: Uuid::now_v7(), joined_reason: Some(JoinReason::Reply.as_str().into()), last_read_message_id: None, last_read_at: None, joined_at: Utc::now(), }; let json = serde_json::to_value(&p).unwrap(); assert_eq!(json["joined_reason"], "reply"); } }