feat: commentaires façon Notion — ancres jaunes, tiroir refondu, réactions emoji sur texte (v7.64.0)
- Réaction sur texte : bouton 🙂+ (« React to selected text ») dans la toolbar de sélection → sélecteur d'émoji EXISTANT en mode réaction (en-tête = texte sélectionné + ✋, onglets masqués, ligne Recent au-dessus de la grille People, Filter… et barre de catégories avec + custom inchangés) - Backend : migration 37 text_reactions (UNIQUE plage × emoji × utilisateur → POST idempotent) + GET/POST /api/pages/{id}/reactions (GET groupé par plage emoji/count/mine, POST toggle, 400 sur plage vide) - Affichage : plage commentée/réagie peinte en jaune (_paintAnchors après render/loadComments/loadReactions), pastille 😀+compte après la plage, clic pastille = rouvrir le sélecteur, clic ancre = ouvrir et centrer le fil - Pastilles = annotations : gtTok/gtMd les ignorent (y compris le chemin rapide innerText) et les offsets _srcOffset/_srcNodeLen les excluent → le texte sauvé est identique (vérifié dans le e2e) - Tiroir refondu : carte de fil (avatar, nom, horodatage, ✓ toggle resolve/reopen, ⋯ → Copy link #cmt-<id>/Delete), saisie « Add a comment... » avec @ et envoi ↑/Entrée ; #cmt-<id> ouvre et centre le fil au chargement ; loadComments + loadReactions à l'ouverture (commentCount enfin renseigné) - Fixes : resolveComment ne pouvait pas rouvrir (toujours resolved:true) ; showCommentBtn ignorait les sélections à ancre élément (bouton 💬 jamais affiché) - Tests : tests/test_v764_comments_reactions.py (8, dont toggle API réel), e2e/v764_comments_reactions.spec.js (8 étapes) — 3/3 specs e2e verts, suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.0 (526 chemins)
This commit is contained in:
@@ -206,3 +206,67 @@ def delete_comment(request: Request, comment_id: int):
|
||||
conn.execute("DELETE FROM comments WHERE id=? OR parent_id=?", (comment_id, comment_id))
|
||||
conn.commit()
|
||||
return {"id": comment_id, "status": "deleted"}
|
||||
|
||||
|
||||
# ── v7.64.0 : réactions emoji sur un texte sélectionné ──────────────────────
|
||||
|
||||
@router.get("/pages/{page_id}/reactions")
|
||||
def list_reactions(request: Request, page_id: int):
|
||||
"""Réactions regroupées par plage de texte : {block_id, start, end, emoji, count, mine}."""
|
||||
user = _current_user(request)
|
||||
with get_conn() as conn:
|
||||
if not conn.execute("SELECT id FROM pages WHERE id=?", (page_id,)).fetchone():
|
||||
raise HTTPException(404, "Page not found")
|
||||
rows = conn.execute(
|
||||
"""SELECT block_id, anchor_start, anchor_end, emoji,
|
||||
COUNT(*) AS count,
|
||||
MAX(CASE WHEN user_id=? THEN 1 ELSE 0 END) AS mine
|
||||
FROM text_reactions
|
||||
WHERE page_id=?
|
||||
GROUP BY block_id, anchor_start, anchor_end, emoji
|
||||
ORDER BY MIN(id)""",
|
||||
(user["id"], page_id),
|
||||
).fetchall()
|
||||
return {"page_id": page_id, "reactions": [dict(r) for r in rows]}
|
||||
|
||||
|
||||
@router.post("/pages/{page_id}/reactions")
|
||||
def toggle_reaction(request: Request, page_id: int, body: dict = Body(default={})):
|
||||
"""Ajoute ou retire la réaction de l'utilisateur sur une plage de texte (toggle)."""
|
||||
user = _current_user(request)
|
||||
block = (body.get("block_id") or "").strip()
|
||||
emoji = (body.get("emoji") or "").strip()
|
||||
try:
|
||||
start = int(body.get("anchor_start"))
|
||||
end = int(body.get("anchor_end"))
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(400, "anchor offsets required") from None
|
||||
if not block or not emoji or start < 0 or end <= start:
|
||||
raise HTTPException(400, "block_id, emoji and a non-empty range are required")
|
||||
uid = user["id"]
|
||||
with get_conn() as conn:
|
||||
if not conn.execute("SELECT id FROM pages WHERE id=?", (page_id,)).fetchone():
|
||||
raise HTTPException(404, "Page not found")
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO users (id, login, full_name, is_admin) VALUES (?,?,?,1)",
|
||||
(uid, user.get("login", "admin"), user.get("full_name", "Admin")),
|
||||
)
|
||||
existing = conn.execute(
|
||||
"""SELECT id FROM text_reactions
|
||||
WHERE page_id=? AND block_id=? AND anchor_start=? AND anchor_end=?
|
||||
AND emoji=? AND user_id=?""",
|
||||
(page_id, block, start, end, emoji, uid),
|
||||
).fetchone()
|
||||
if existing:
|
||||
conn.execute("DELETE FROM text_reactions WHERE id=?", (existing["id"],))
|
||||
status = "removed"
|
||||
else:
|
||||
conn.execute(
|
||||
"""INSERT INTO text_reactions
|
||||
(page_id, block_id, anchor_start, anchor_end, emoji, user_id)
|
||||
VALUES (?,?,?,?,?,?)""",
|
||||
(page_id, block, start, end, emoji, uid),
|
||||
)
|
||||
status = "added"
|
||||
conn.commit()
|
||||
return {"status": status, "emoji": emoji}
|
||||
|
||||
Reference in New Issue
Block a user