use chrono::{Duration, Utc}; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, info, instrument, warn}; use uuid::Uuid; /// Représente un changement de statut typing pour un utilisateur #[derive(Debug, Clone)] pub struct TypingStatusChange { pub user_id: Uuid, pub conversation_id: Uuid, pub is_typing: bool, } /// Manager pour gérer les typing indicators pub struct TypingIndicatorManager { /// Map de conversation ID vers map de user ID vers timestamp de dernière activité typing_users: Arc>>>>, /// Durée après laquelle un user n'est plus considéré comme "en train de taper" timeout_duration: Duration, } impl TypingIndicatorManager { pub fn new() -> Self { Self { typing_users: Arc::new(RwLock::new(HashMap::new())), timeout_duration: Duration::seconds(3), } } /// Marquer qu'un user est en train de taper dans une conversation #[instrument(skip(self))] pub async fn user_started_typing(&self, user_id: Uuid, conversation_id: Uuid) { let mut typing = self.typing_users.write().await; let conversation_typing = typing.entry(conversation_id).or_insert_with(HashMap::new); conversation_typing.insert(user_id, Utc::now()); info!( user_id = %user_id, conversation_id = %conversation_id, "User started typing" ); } /// Retirer un user de la liste des users en train de taper #[instrument(skip(self))] pub async fn user_stopped_typing(&self, user_id: Uuid, conversation_id: Uuid) { let mut typing = self.typing_users.write().await; if let Some(conversation_typing) = typing.get_mut(&conversation_id) { conversation_typing.remove(&user_id); info!( user_id = %user_id, conversation_id = %conversation_id, "User stopped typing" ); } } /// Obtenir la liste des users en train de taper dans une conversation pub async fn get_typing_users(&self, conversation_id: Uuid) -> Vec { let typing = self.typing_users.read().await; if let Some(conversation_typing) = typing.get(&conversation_id) { let now = Utc::now(); let mut active_users = Vec::new(); for (user_id, last_activity) in conversation_typing.iter() { let elapsed = now.signed_duration_since(*last_activity); if elapsed < self.timeout_duration { active_users.push(*user_id); } } active_users } else { Vec::new() } } /// Détecter les utilisateurs dont le timeout a expiré et les retirer /// Retourne la liste des changements de statut (is_typing = false) #[instrument(skip(self))] pub async fn monitor_timeouts(&self) -> Vec { let mut typing = self.typing_users.write().await; let now = Utc::now(); let mut expired_changes = Vec::new(); for (conversation_id, conversation_typing) in typing.iter_mut() { let mut expired_users = Vec::new(); for (user_id, last_activity) in conversation_typing.iter() { let elapsed = now.signed_duration_since(*last_activity); if elapsed >= self.timeout_duration { expired_users.push(*user_id); } } // Retirer les utilisateurs expirés et créer les changements de statut for user_id in expired_users { conversation_typing.remove(&user_id); expired_changes.push(TypingStatusChange { user_id, conversation_id: *conversation_id, is_typing: false, }); debug!( user_id = %user_id, conversation_id = %conversation_id, "User typing timeout expired" ); } } // Retirer les conversations vides typing.retain(|_conversation_id, users| !users.is_empty()); if !expired_changes.is_empty() { debug!( count = expired_changes.len(), "Detected expired typing indicators" ); } expired_changes } /// Nettoyer les users expirés de manière périodique (méthode legacy, utiliser monitor_timeouts) #[deprecated(note = "Use monitor_timeouts() instead")] pub async fn cleanup_expired(&self) { let _ = self.monitor_timeouts().await; } } impl Default for TypingIndicatorManager { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_typing_indicator_manager() { let manager = TypingIndicatorManager::new(); let conv1 = Uuid::new_v4(); let user1 = Uuid::new_v4(); let user2 = Uuid::new_v4(); // Test user_started_typing manager.user_started_typing(user1, conv1).await; manager.user_started_typing(user2, conv1).await; let typing_users = manager.get_typing_users(conv1).await; assert!(typing_users.contains(&user1)); assert!(typing_users.contains(&user2)); // Test user_stopped_typing manager.user_stopped_typing(user1, conv1).await; let typing_users = manager.get_typing_users(conv1).await; assert!(!typing_users.contains(&user1)); assert!(typing_users.contains(&user2)); // Test monitor_timeouts let expired = manager.monitor_timeouts().await; assert!(expired.is_empty()); // Pas encore expiré } }