179 lines
5.7 KiB
Rust
179 lines
5.7 KiB
Rust
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<RwLock<HashMap<Uuid, HashMap<Uuid, chrono::DateTime<Utc>>>>>,
|
|
/// 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<Uuid> {
|
|
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<TypingStatusChange> {
|
|
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é
|
|
}
|
|
}
|