---
title: "Commentaires"
description: "Système de commentaires threadés sur les médias, avec réponses jusqu'à COMMENT_MAX_DEPTH niveaux (défaut : 5, soit depth ∈ [0, 4])."
---

Système de commentaires threadés sur les médias, avec réponses jusqu'à **`COMMENT_MAX_DEPTH` niveaux** (défaut : 5, soit `depth ∈ [0, 4]`).

## Modèle

| Champ        | Notes |
|--------------|-------|
| `id`         | UUID v4 du commentaire. |
| `mediaId`    | UUID du média porteur. |
| `author`     | Bloc **public** de l'auteur — **le même** que l'`author` des médias (`id`, `username`, `nickname`, `displayName`, `bio`, `avatarUrl`, `isVerified`, `level`, …). `null` si le compte a été purgé (RGPD). Remplace l'ancien `userId` plat : lire `author.id`. |
| `parentId`   | UUID du commentaire parent, ou `null` pour un top-level. |
| `rootId`     | UUID du top-level ancestral. Pour un top-level, `rootId = id` (dénormalisation : permet le fetch d'un thread complet avec un seul `WHERE root_id = ?`, sans CTE récursive). |
| `depth`      | 0 pour un top-level, +1 par niveau de réponse. Plafonné à `COMMENT_MAX_DEPTH - 1`. |
| `body`       | Texte du commentaire. `null` quand le commentaire est soft-supprimé. |
| `replyCount` | Nombre d'enfants directs **non supprimés**. Maintenu applicativement. |
| `createdAt`  | Date d'écriture. |
| `editedAt`   | Date de la dernière édition ; `null` si jamais édité. |
| `deletedAt`  | Date de soft-delete ; `null` si actif. |

Quand `deletedAt !== null` : la ligne est conservée pour ne pas orphelinser les enfants, mais `body` est renvoyé à `null` et `meta.deleted = true` est ajouté à la ressource.

## Compteurs

- `media_comment.reply_count` : enfants directs non supprimés (utilisé pour les listings de réponses).
- `media_stats.comments_count` : top-level non supprimés (exposé dans la ressource Media en tant que `commentsCount`, utilisé pour `meta.total` du listing `/api/media/{mediaId}/comments`).

Les deux sont maintenus **dans la même transaction** que l'INSERT / soft-delete par [MediaCommentService](../src/Domain/Media/Comment/MediaCommentService.php). Contrairement aux likes/dislikes (triggers), le choix applicatif évite des triggers complexes autour du soft-delete.

## Variables d'environnement

| Var                            | Défaut | Rôle |
|--------------------------------|--------|------|
| `COMMENT_MAX_LENGTH`           | 2000   | Longueur max du `body` (en caractères UTF-8, via `mb_strlen`). |
| `COMMENT_MAX_DEPTH`            | 5      | Niveaux de profondeur autorisés (depth ∈ [0, max-1]). |
| `COMMENT_EDIT_WINDOW_MINUTES`  | 15     | Fenêtre d'édition après création. `0` = édition désactivée, `-1` = illimitée, `N>0` = N minutes. |
| `COMMENT_DELETE_POLICY`        | both   | Qui peut soft-supprimer : `author`, `owner` (propriétaire du média), ou `both`. |

## Notifications

- Top-level → l'**owner** du média reçoit `media.comment.received` (déduplique sur `(media, actor)` dans la fenêtre).
- Réponse → l'auteur du **commentaire parent** reçoit `media.comment.reply.received` (déduplique sur `(parent, actor)`).
- Pas de notif sur soi-même (commenter / répondre à son propre contenu reste autorisé mais silencieux).
- Pas de notif lors de l'édition ni du soft-delete.

## Indexation Meili

Les commentaires ne sont **pas** indexés dans Meilisearch. Le volume peut être élevé et il n'y a pas de cas d'usage de recherche full-text aujourd'hui ; un index pourra être ajouté plus tard sans casser l'API (les ressources resteront identiques).

## `GET /api/media/{mediaId}/comments`

Liste les commentaires **top-level** d'un média, du plus récent au plus ancien. Paginé en keyset (cursor) — convention partagée avec les autres listings. Public (pas d'auth requise).

Query :
- `limit` (1..100, def 20)
- `cursor` / `before` (mutuellement exclusifs)

`meta.total` lit le compteur dénormalisé `media_stats.comments_count` (top-level non supprimés). Les commentaires soft-supprimés sont retournés avec `body: null` + `meta.deleted = true`.

[ListMediaCommentsAction](../src/Http/Action/Api/Media/Comment/ListMediaCommentsAction.php)

Erreurs :
- `404 Media not found`
- `400 Invalid cursor`
- `422 Invalid media id`

---

## `POST /api/media/{mediaId}/comments`

Crée un commentaire top-level OU une réponse. **Auth requise** (Bearer). L'utilisateur doit avoir confirmé son e-mail et ne pas être banni.

Body (flat ou JSON:API) :

```json
{ "body": "Joli cliché !", "parentId": null }
```

```json
{
  "data": {
    "type": "mediaComments",
    "attributes": {
      "body": "Merci pour la réponse 🙏",
      "parentId": "0192c4e3-…"
    }
  }
}
```

- `body` (string, requis, trimé non-vide, ≤ `COMMENT_MAX_LENGTH`).
- `parentId` (UUID, optionnel). Si renseigné, le parent doit appartenir au **même média**, ne pas être soft-supprimé, et `parent.depth + 1` doit rester < `COMMENT_MAX_DEPTH`.

Réponse `201` : ressource `mediaComments` (cf. modèle plus haut). [CreateMediaCommentAction](../src/Http/Action/Api/Media/Comment/CreateMediaCommentAction.php)

Erreurs (`meta.code`) :
- `422 comment.bodyMissing` — body absent / vide après trim.
- `422 comment.bodyTooLong` — > `COMMENT_MAX_LENGTH`.
- `404 comment.mediaNotFound` — média inconnu.
- `404 comment.parentNotFound` — parent inconnu ou rattaché à un autre média.
- `403 comment.parentDeleted` — répondre à un commentaire supprimé est refusé.
- `422 comment.depthExceeded` — profondeur max atteinte.
- `403 comment.actorNotConfirmed` / `comment.actorBanned`.

---

## `GET /api/media/comments/{commentId}/thread`

Renvoie le **thread complet** ancré au top-level dont dépend `commentId`. Si `commentId` pointe sur une réponse profonde, on remonte automatiquement à son `rootId` — la réponse est donc toujours la conversation entière (utile pour un deep-link arrivant depuis une notification).

Pas de pagination (le thread est borné par `COMMENT_MAX_DEPTH`). Les nœuds sont retournés à plat, triés par `(depth ASC, createdAt ASC, id ASC)` ; le client reconstruit l'arbre via `parentId`.

[GetMediaCommentThreadAction](../src/Http/Action/Api/Media/Comment/GetMediaCommentThreadAction.php)

Erreurs :
- `404 Comment not found`
- `422 Invalid comment id`

---

## `GET /api/media/comments/{commentId}/replies`

Liste les **enfants directs** d'un commentaire, du plus ancien au plus récent (ordre chronologique de réponse). Keyset-paginé.

Query : `limit` (1..100, def 20), `cursor` / `before`.

`meta.total` reprend le `replyCount` du parent (dénormalisé). [ListMediaCommentRepliesAction](../src/Http/Action/Api/Media/Comment/ListMediaCommentRepliesAction.php)

Erreurs :
- `404 Comment not found`
- `400 Invalid cursor`
- `422 Invalid comment id`

---

## `PATCH /api/media/comments/{commentId}`

Édite le `body` d'un commentaire. **Auth requise**. Seul l'auteur peut éditer, et uniquement dans la fenêtre `COMMENT_EDIT_WINDOW_MINUTES` (`0` = édition désactivée, `-1` = illimitée).

Body (flat ou JSON:API) — mêmes shapes que la création, mais sans `parentId` (le re-parentage n'est jamais autorisé) :

```json
{ "body": "..." }
```

Réponse `200` : ressource mise à jour avec `editedAt` rempli. [UpdateMediaCommentAction](../src/Http/Action/Api/Media/Comment/UpdateMediaCommentAction.php)

Erreurs (`meta.code`) :
- `422 comment.bodyMissing` / `comment.bodyTooLong`.
- `404 comment.notFound`.
- `403 comment.forbidden` — pas l'auteur.
- `410 comment.alreadyDeleted` — éditer un commentaire supprimé est refusé.
- `403 comment.editWindowClosed` — fenêtre expirée (ou `COMMENT_EDIT_WINDOW_MINUTES=0`).
- `403 comment.actorNotConfirmed` / `comment.actorBanned`.

---

## `DELETE /api/media/comments/{commentId}`

Soft-supprime le commentaire selon `COMMENT_DELETE_POLICY` :

- `author` — seul l'auteur peut supprimer.
- `owner` — seul le propriétaire du média peut supprimer.
- `both` (défaut) — l'un ou l'autre.

La ligne est conservée pour ne pas orphelinser ses enfants. Le `body` cesse d'être renvoyé (`null`) et `meta.deleted = true` est ajouté à la ressource. Les compteurs `reply_count` du parent (si réponse) ou `media_stats.comments_count` (si top-level) sont décrémentés dans la même transaction.

Réponse `200` : ressource redactée (pas de `204` pour permettre au client de mettre à jour sa vue en place). [DeleteMediaCommentAction](../src/Http/Action/Api/Media/Comment/DeleteMediaCommentAction.php)

Erreurs (`meta.code`) :
- `404 comment.notFound`.
- `410 comment.alreadyDeleted` — opération idempotente côté UX, mais l'API signale l'état.
- `403 comment.forbidden` — la politique en vigueur n'autorise pas le caller.
- `403 comment.actorNotConfirmed` / `comment.actorBanned`.

---
