# 📜 Message Search, History Pagination, and Offline Sync

**Date**: 2025-12-05  
**Version**: 1.0.0  
**Statut**: ✅ Implémenté

---

## 📋 Table des matières

1. [Vue d'ensemble](#vue-densemble)
2. [History Pagination](#history-pagination)
3. [Message Search](#message-search)
4. [Offline Sync](#offline-sync)
5. [Spécifications techniques](#spécifications-techniques)
6. [Exemples d'utilisation](#exemples-dutilisation)
7. [Limites et bonnes pratiques](#limites-et-bonnes-pratiques)
8. [Impact sur l'UI](#impact-sur-lui)

---

## 🎯 Vue d'ensemble

Ce document décrit trois fonctionnalités majeures ajoutées au `veza-chat-server` :

1. **History Pagination** : Pagination efficace de l'historique avec cursors `before`/`after`
2. **Message Search** : Recherche textuelle de messages dans une conversation
3. **Offline Sync** : Synchronisation des messages manquants depuis la dernière connexion

Toutes ces fonctionnalités sont :
- ✅ Sécurisées (permissions strictes via `PermissionService`)
- ✅ Performantes (index SQL optimisés)
- ✅ Compatibles avec les statuts (edited, deleted, delivered, read)
- ✅ Disponibles via WebSocket

---

## 📜 History Pagination

### Description

Permet de récupérer l'historique d'une conversation avec pagination par cursors basés sur `created_at`. Plus efficace que l'offset/limit classique car :
- Supporte les insertions concurrentes
- Meilleure performance avec les index
- Pas de problèmes de doublons lors de nouvelles insertions

### Inbound WebSocket Message

```json
{
  "type": "FetchHistory",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "before": "2025-12-05T10:30:00Z",
  "after": null,
  "limit": 50
}
```

**Paramètres**:
- `conversation_id` (UUID, requis) : ID de la conversation
- `before` (DateTime ISO8601, optionnel) : Récupère les messages avant ce timestamp
- `after` (DateTime ISO8601, optionnel) : Récupère les messages après ce timestamp
- `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de messages à récupérer

**Règles**:
- Si `before` est fourni : tri DESC (messages plus anciens)
- Si `after` est fourni : tri ASC (messages plus récents)
- Si les deux sont fournis : messages entre `after` et `before` (tri ASC)
- Si aucun n'est fourni : messages les plus récents (tri DESC)
- Les résultats sont **toujours retournés en ordre ASC** (du plus ancien au plus récent)

### Outbound WebSocket Message

```json
{
  "type": "HistoryChunk",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "messages": [
    {
      "id": "...",
      "conversation_id": "...",
      "sender_id": "...",
      "content": "Hello world",
      "created_at": "2025-12-05T10:00:00Z",
      "is_edited": false,
      "is_deleted": false,
      ...
    }
  ],
  "has_more_before": true,
  "has_more_after": false
}
```

**Champs**:
- `messages` : Liste des messages (toujours triés ASC)
- `has_more_before` : Indique s'il y a des messages plus anciens
- `has_more_after` : Indique s'il y a des messages plus récents

### Exemples d'utilisation

#### Charger les messages les plus récents
```json
{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": null,
  "after": null,
  "limit": 50
}
```

#### Charger les messages plus anciens (scroll up)
```json
{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": "2025-12-05T10:00:00Z",
  "after": null,
  "limit": 50
}
```

#### Charger les nouveaux messages (scroll down)
```json
{
  "type": "FetchHistory",
  "conversation_id": "...",
  "before": null,
  "after": "2025-12-05T10:00:00Z",
  "limit": 50
}
```

### Index SQL

```sql
CREATE INDEX idx_messages_conv_created_at 
ON messages(conversation_id, created_at DESC);

CREATE INDEX idx_messages_conv_created_not_deleted 
ON messages(conversation_id, created_at DESC) 
WHERE is_deleted = false;
```

---

## 🔍 Message Search

### Description

Recherche textuelle de messages dans une conversation. Utilise `ILIKE` avec index trigram pour une recherche performante et insensible à la casse.

### Inbound WebSocket Message

```json
{
  "type": "SearchMessages",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "query": "hello world",
  "limit": 50,
  "offset": 0
}
```

**Paramètres**:
- `conversation_id` (UUID, requis) : ID de la conversation
- `query` (String, requis) : Terme de recherche (ne peut pas être vide)
- `limit` (usize, optionnel, défaut: 50, max: 100) : Nombre de résultats par page
- `offset` (usize, optionnel, défaut: 0) : Offset pour pagination

### Outbound WebSocket Message

```json
{
  "type": "SearchResults",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "messages": [
    {
      "id": "...",
      "content": "Hello world!",
      "created_at": "2025-12-05T10:00:00Z",
      ...
    }
  ],
  "query": "hello world",
  "total": 123
}
```

**Champs**:
- `messages` : Liste des messages correspondants (triés par `created_at DESC`)
- `query` : La requête de recherche originale
- `total` : Nombre total de résultats (pour pagination)

### Exemples d'utilisation

#### Recherche simple
```json
{
  "type": "SearchMessages",
  "conversation_id": "...",
  "query": "meeting",
  "limit": 20,
  "offset": 0
}
```

#### Pagination des résultats
```json
{
  "type": "SearchMessages",
  "conversation_id": "...",
  "query": "meeting",
  "limit": 20,
  "offset": 20
}
```

### Index SQL

```sql
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE INDEX idx_messages_content_trgm 
ON messages USING GIN(content gin_trgm_ops);

CREATE INDEX idx_messages_conv_content_trgm 
ON messages USING GIN(conversation_id, content gin_trgm_ops);
```

### Comportement

- ✅ Recherche insensible à la casse (`ILIKE`)
- ✅ Recherche partielle (contient le terme)
- ✅ Exclut les messages supprimés par défaut
- ✅ Tri par `created_at DESC` (plus récents en premier)

---

## 🔄 Offline Sync

### Description

Synchronise tous les messages manquants depuis la dernière connexion. Inclut :
- Messages créés depuis `since`
- Messages édités depuis `since` (même si créés avant)
- Messages supprimés depuis `since` (même si créés avant)

Permet aux clients mobiles d'avoir une synchronisation fiable après une déconnexion.

### Inbound WebSocket Message

```json
{
  "type": "SyncMessages",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "since": "2025-12-05T09:00:00Z"
}
```

**Paramètres**:
- `conversation_id` (UUID, requis) : ID de la conversation
- `since` (DateTime ISO8601, requis) : Timestamp de la dernière synchronisation

### Outbound WebSocket Message

```json
{
  "type": "SyncChunk",
  "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
  "messages": [
    {
      "id": "...",
      "content": "New message",
      "created_at": "2025-12-05T10:00:00Z",
      "is_edited": false,
      "is_deleted": false,
      ...
    },
    {
      "id": "...",
      "content": "Edited content",
      "created_at": "2025-12-05T08:00:00Z",
      "is_edited": true,
      "edited_at": "2025-12-05T10:30:00Z",
      ...
    },
    {
      "id": "...",
      "content": "Deleted message",
      "created_at": "2025-12-05T08:30:00Z",
      "is_deleted": true,
      "deleted_at": "2025-12-05T10:45:00Z",
      ...
    }
  ],
  "last_sync": "2025-12-05T11:00:00Z"
}
```

**Champs**:
- `messages` : Tous les messages créés ou modifiés depuis `since` (triés par `created_at ASC`)
- `last_sync` : Timestamp actuel (à utiliser pour la prochaine sync)

### Exemples d'utilisation

#### Synchronisation initiale
```json
{
  "type": "SyncMessages",
  "conversation_id": "...",
  "since": "2025-12-05T00:00:00Z"
}
```

#### Synchronisation après déconnexion
```json
{
  "type": "SyncMessages",
  "conversation_id": "...",
  "since": "2025-12-05T09:30:00Z"
}
```

### Index SQL

```sql
CREATE INDEX idx_messages_conv_created_sync 
ON messages(conversation_id, created_at ASC) 
WHERE is_deleted = false;

CREATE INDEX idx_messages_conv_updated_sync 
ON messages(conversation_id, updated_at ASC) 
WHERE is_deleted = false;
```

### Comportement

- ✅ Inclut tous les messages créés depuis `since`
- ✅ Inclut tous les messages édités depuis `since` (même créés avant)
- ✅ Inclut tous les messages supprimés depuis `since` (même créés avant)
- ✅ Tri par `created_at ASC` (du plus ancien au plus récent)
- ✅ Le client doit gérer les updates (édits) et deletes (suppressions)

---

## 🔧 Spécifications techniques

### Repository Methods

#### `fetch_history`
```rust
pub async fn fetch_history(
    &self,
    conversation_id: Uuid,
    before: Option<DateTime<Utc>>,
    after: Option<DateTime<Utc>>,
    limit: usize,
    include_deleted: bool,
) -> Result<(Vec<Message>, bool, bool)>
```

Retourne : `(messages, has_more_before, has_more_after)`

#### `search_messages`
```rust
pub async fn search_messages(
    &self,
    conversation_id: Uuid,
    query: &str,
    limit: usize,
    offset: usize,
    include_deleted: bool,
) -> Result<(Vec<Message>, i64)>
```

Retourne : `(messages, total_count)`

#### `fetch_since`
```rust
pub async fn fetch_since(
    &self,
    conversation_id: Uuid,
    since: DateTime<Utc>,
) -> Result<Vec<Message>>
```

### Permissions

Toutes les fonctionnalités nécessitent :
- `can_read_conversation(user_id, conversation_id)` : L'utilisateur doit avoir accès à la conversation

### Erreurs possibles

- `ChatError::Unauthorized` : Pas de permission pour lire la conversation
- `ChatError::ValidationError` : Query de recherche vide
- `ChatError::InternalError` : Erreur de base de données

---

## 📱 Exemples d'utilisation

### Client Web (React)

```typescript
// History Pagination
const fetchHistory = async (conversationId: string, before?: Date) => {
  ws.send(JSON.stringify({
    type: "FetchHistory",
    conversation_id: conversationId,
    before: before?.toISOString(),
    after: null,
    limit: 50
  }));
};

// Message Search
const searchMessages = async (conversationId: string, query: string) => {
  ws.send(JSON.stringify({
    type: "SearchMessages",
    conversation_id: conversationId,
    query: query,
    limit: 50,
    offset: 0
  }));
};

// Offline Sync
const syncMessages = async (conversationId: string, lastSync: Date) => {
  ws.send(JSON.stringify({
    type: "SyncMessages",
    conversation_id: conversationId,
    since: lastSync.toISOString()
  }));
};
```

### Client Mobile (React Native)

```typescript
// Sync après reconnexion
const syncAfterReconnect = async (conversationId: string) => {
  const lastSync = await AsyncStorage.getItem(`last_sync_${conversationId}`);
  const since = lastSync ? new Date(lastSync) : new Date(0);
  
  ws.send(JSON.stringify({
    type: "SyncMessages",
    conversation_id: conversationId,
    since: since.toISOString()
  }));
  
  // Écouter SyncChunk et mettre à jour last_sync
  ws.on('message', (msg) => {
    if (msg.type === 'SyncChunk') {
      AsyncStorage.setItem(`last_sync_${conversationId}`, msg.last_sync);
      // Mettre à jour l'UI avec les messages
    }
  });
};
```

---

## ⚠️ Limites et bonnes pratiques

### Limites

1. **History Pagination** :
   - `limit` max : 100 messages
   - Utiliser `before`/`after` plutôt que offset pour de meilleures performances

2. **Message Search** :
   - `limit` max : 100 résultats
   - `query` minimum : 1 caractère
   - Recherche partielle (contient), pas de recherche exacte

3. **Offline Sync** :
   - Pas de limite sur le nombre de messages (peut être volumineux)
   - Le client doit gérer les updates et deletes

### Bonnes pratiques

1. **History Pagination** :
   - Toujours utiliser `before` pour charger plus d'anciens messages
   - Utiliser `after` pour charger les nouveaux messages
   - Stocker le `created_at` du premier/dernier message pour la pagination

2. **Message Search** :
   - Implémenter un debounce sur la recherche (300-500ms)
   - Limiter la longueur minimale de la query (3 caractères recommandé)
   - Afficher un indicateur de chargement pendant la recherche

3. **Offline Sync** :
   - Stocker `last_sync` localement (AsyncStorage, localStorage)
   - Sync automatique après reconnexion
   - Gérer les conflits si un message est édité localement et sur le serveur

---

## 🎨 Impact sur l'UI

### History Pagination

**Scroll infini vers le haut** :
```typescript
const [messages, setMessages] = useState<Message[]>([]);
const [hasMore, setHasMore] = useState(true);

const loadMore = async () => {
  if (!hasMore) return;
  
  const oldestMessage = messages[0];
  const before = oldestMessage?.created_at;
  
  fetchHistory(conversationId, before).then((chunk) => {
    setMessages([...chunk.messages, ...messages]);
    setHasMore(chunk.has_more_before);
  });
};
```

### Message Search

**Barre de recherche avec résultats** :
```typescript
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState<Message[]>([]);

const handleSearch = debounce((query: string) => {
  if (query.length < 3) return;
  
  searchMessages(conversationId, query).then((results) => {
    setSearchResults(results.messages);
  });
}, 300);
```

### Offline Sync

**Indicateur de synchronisation** :
```typescript
const [isSyncing, setIsSyncing] = useState(false);

const sync = async () => {
  setIsSyncing(true);
  const lastSync = await getLastSync(conversationId);
  syncMessages(conversationId, lastSync);
  // setIsSyncing(false) dans le handler SyncChunk
};
```

---

## 📊 Performance

### Index utilisés

- `idx_messages_conv_created_at` : Pagination efficace
- `idx_messages_content_trgm` : Recherche textuelle rapide
- `idx_messages_conv_created_sync` : Sync optimisée

### Métriques attendues

- **History Pagination** : < 50ms pour 50 messages
- **Message Search** : < 100ms pour 1000 messages
- **Offline Sync** : < 200ms pour 100 messages

---

## 🔐 Sécurité

- ✅ Toutes les fonctionnalités vérifient les permissions via `PermissionService`
- ✅ Les messages supprimés sont exclus par défaut (sauf si `include_deleted = true`)
- ✅ Validation des paramètres (query non vide, limit max, etc.)
- ✅ Pas d'injection SQL (utilisation de paramètres liés)

---

## 📝 Migration

Pour activer ces fonctionnalités, exécuter :

```bash
psql -d veza_db -f migrations/006_history_search_sync.sql
```

Cette migration crée tous les index nécessaires.

---

**Fin du document**

