veza/veza-chat-server/src/typing_indicator.rs

131 lines
4.2 KiB
Rust
Raw Normal View History

2025-12-03 19:33:26 +00:00
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;
}
}