feat: chat — suppression de post, messages privés, boîte compacte, link preview #191
CI / lint (push) Successful in 2m50s
CI / security (push) Successful in 1m35s
CI / test (push) Successful in 4m28s
CI / build (push) Successful in 1m28s
CI / e2e (push) Successful in 17m11s

This commit is contained in:
2026-10-08 22:33:29 -04:00
parent 55a9fcc0f4
commit f00a8bea8f
23 changed files with 946 additions and 66 deletions
+61
View File
@@ -90,6 +90,67 @@
- `initSidebarFilter` (`sidebar.js`) : `routeFilter`/`routeClear` routent
vers `filterChatMessages()` (texte **et** auteur, insensible à la casse).
## #191 — Suppression, messages privés, boîte compacte, link preview (2026-10-08)
### A. Suppression d'un post
- `DELETE /api/chat/{id}` et `DELETE /api/chat/dm/{peer}/{id}` :
**auteur du message ou admin** (403 sinon), 404 si l'id n'existe pas.
- Store : `delete_message(vault, path, id)` — écriture atomique,
`False` si absent ; broadcast SSE `chat_deleted` `{vault, path, id}`.
- Frontend : bouton 🗑 (`.file-chat-del`, visible au survol) rendu dans
`_messageEl` uniquement si l'URL de suppression est fournie **et**
que l'utilisateur est auteur/admin ; `confirm()` natif ; retrait local
+ retrait en direct chez tous via `onChatDeleted` (relais `sync.js`).
### B. Messages privés 2 utilisateurs
- Store : `DM_VAULT = "__dm__"`, `dm_path(a, b)` = paire **triée** → les
deux participants lisent/écrivent le même document, indépendamment de
l'ordre.
- Routes : `GET /api/chat/users` (destinataires — usernames +
display_names, **sans soi-même**, aucun hash/mot de passe exposé),
`GET/POST /api/chat/dm/{username}` (404 utilisateur inconnu, 400 DM à
soi), `DELETE /api/chat/dm/{username}/{id}`.
- Frontend : rangée de pastilles `.chat-channels` (« Général » + un
bouton par utilisateur) au-dessus du fil ; `_channel` état courant,
`_channelKey`/`_channelUrl`/`_channelDelUrl` ; placeholder dédié
`chat.placeholder_dm` ; non-lus **par canal** (compteurs dans
`localStorage`, l'ancien mélange timestamp/compteur est devenu un
compteur unique `{"general": n, "dm:x": n}`, remis à zéro à l'ouverture,
pastille = total) ; les DM SSE routés par `_pairOf(path)`.
### C. Boîte d'édition compacte
- Bouton d'envoi réduit à une icône `send` (`.file-chat-send-icon`) dans
les deux formulaires (panneau document + sidebar) ; la saisie garde
toute la largeur. `safeCreateIcons()` à la fin du `_renderShell`.
### D. Link preview
- `build_preview(text)` (store) : 1ʳᵉ URL du texte → garde SSRF
`_assert_public_http_url` (host privé/loopback rejeté, schémas non
http refusés) → `httpx.get` 5 s, 512 Ko, `follow_redirects` (limite
httpx = 20) → parse OG par regex (`og:title` avec repli `<title>`,
`og:description`, `og:image`, `og:site_name`) →
`{url, title, description, image, site}`.
- **Best-effort** : n'importe quelle erreur (URL morte, timeout, garde)
→ `None`, le message passe quand même ; cache borné **200 entrées**.
- Frontend : `_previewEl` rend une carte cliquable (vignette lazy
`no-referrer`, site en capitales, titre/description tronqués 2 lignes)
sous le corps du message.
- Schéma : `ChatMessageItem.preview`.
### Tests #191
- pytest `TestDelete` (3), `TestPrivateChat` (8), `TestLinkPreview` (6,
dont le happy path avec `httpx.get` mocké — un `max_redirects`
inexistante sur `httpx.get` levait un TypeError silencieusement avalé
par le `try`, testé par `test_build_preview_happy_path_parses_og`).
- JSDOM `filechat.test.mjs` +5 : carte preview, bouton supprimer
(droit auteur/admin), `onChatDeleted`, compteurs non-lus,
routage DM isolé du général (16 au total).
## Décisions
- **SSE plutôt qu'un second WebSocket** : le transport de #62 (EventSource