131 lines
4.2 KiB
Rust
131 lines
4.2 KiB
Rust
|
|
use std::collections::HashMap;
|
||
|
|
use std::sync::Arc;
|
||
|
|
use tokio::sync::RwLock;
|
||
|
|
use chrono::{Duration, Utc};
|
||
|
|
use tracing::{info, debug, instrument};
|
||
|
|
|
||
|
|
/// 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<String, HashMap<String, 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 set_typing(&self, conversation_id: &str, user_id: &str) {
|
||
|
|
let mut typing = self.typing_users.write().await;
|
||
|
|
|
||
|
|
let conversation_typing = typing
|
||
|
|
.entry(conversation_id.to_string())
|
||
|
|
.or_insert_with(HashMap::new);
|
||
|
|
|
||
|
|
conversation_typing.insert(user_id.to_string(), 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 stop_typing(&self, conversation_id: &str, user_id: &str) {
|
||
|
|
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: &str) -> Vec<String> {
|
||
|
|
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.clone());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
active_users
|
||
|
|
} else {
|
||
|
|
Vec::new()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Nettoyer les users expirés de manière périodique
|
||
|
|
pub async fn cleanup_expired(&self) {
|
||
|
|
let mut typing = self.typing_users.write().await;
|
||
|
|
let now = Utc::now();
|
||
|
|
|
||
|
|
for conversation_typing in typing.values_mut() {
|
||
|
|
conversation_typing.retain(|_user_id, last_activity| {
|
||
|
|
let elapsed = now.signed_duration_since(*last_activity);
|
||
|
|
elapsed < self.timeout_duration
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Retirer les conversations vides
|
||
|
|
typing.retain(|_conversation_id, users| !users.is_empty());
|
||
|
|
|
||
|
|
debug!("Cleaned up expired typing indicators");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
|
||
|
|
// Test set_typing
|
||
|
|
manager.set_typing("conv1", "user1").await;
|
||
|
|
manager.set_typing("conv1", "user2").await;
|
||
|
|
|
||
|
|
let typing_users = manager.get_typing_users("conv1").await;
|
||
|
|
assert!(typing_users.contains(&"user1".to_string()));
|
||
|
|
assert!(typing_users.contains(&"user2".to_string()));
|
||
|
|
|
||
|
|
// Test stop_typing
|
||
|
|
manager.stop_typing("conv1", "user1").await;
|
||
|
|
|
||
|
|
let typing_users = manager.get_typing_users("conv1").await;
|
||
|
|
assert!(!typing_users.contains(&"user1".to_string()));
|
||
|
|
assert!(typing_users.contains(&"user2".to_string()));
|
||
|
|
|
||
|
|
// Test cleanup
|
||
|
|
manager.cleanup_expired().await;
|
||
|
|
}
|
||
|
|
}
|