feat(chat): ответы на сообщения с цитатой и переходом к оригиналу

Кнопка «Ответить» у каждого сообщения: цитата показывается в новом
сообщении и в поле ввода, отмена — ✕ или Escape, переключение треда
сбрасывает ответ. Клик по цитате прокручивает ленту к исходному
сообщению и подсвечивает его.

Цитата повторяет вложение оригинала: имя файла с иконкой, для
картинок — миниатюра WebP через существующий /api/chat/files/:token.

- chat_messages.reply_to_id: FK на саму таблицу с ON DELETE SET NULL,
  миграция и ensureChatTables, индекс
- POST .../messages принимает reply_to_id и проверяет, что родитель
  в этом же треде и не удалён; вложения при ошибке подчищаются
- цитата едет в списке, при отправке и в SSE правки/удаления, так что
  у собеседника она обновляется по живому каналу
- бэкап: reply_to_id в normalizeRestoreData, при restore ссылка
  принимается только если родитель восстановился, иначе NULL
- shorts: скрывать поле «Резидент» при добавлении без разбора строк
This commit is contained in:
dev
2026-10-06 00:33:17 +03:00
parent 85b737c0aa
commit 3ac54f57b4
9 changed files with 258 additions and 26 deletions
+24 -1
View File
@@ -982,7 +982,18 @@ body.report-card-open .report-fields{display:flex}
.msg-act:hover{color:var(--accent);background:var(--bg)}
.msg-act-del:hover{color:#ef4444}
.msg-act svg{width:13px;height:13px}
.msg-deleted-body{font-style:italic;color:var(--muted)}
.msg-reply{display:flex;flex-direction:column;gap:1px;border-left:2px solid var(--accent);background:var(--bg);border-radius:0 7px 7px 0;padding:4px 8px;margin-bottom:2px;min-width:0;text-align:left;font:inherit;cursor:pointer;position:relative}
.msg-reply:hover{background:var(--card);border-left-color:var(--accent)}
.msg-reply-img{display:block;width:100%;max-width:280px;max-height:150px;object-fit:cover;border-radius:6px;background:var(--card);margin-bottom:4px}
.msg-reply-who{font-size:.68rem;color:var(--accent);font-weight:600}
.msg-reply-text{font-size:.75rem;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:280px}
.msg-reply-text[hidden]{display:none}
.msg-reply-file{display:inline-flex;align-items:center;gap:5px;font-size:.75rem;color:var(--muted);max-width:280px;min-width:0}
.msg-reply-file[hidden]{display:none}
.msg-reply-file svg{width:13px;height:13px;flex-shrink:0}
.msg-reply-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.msg-flash{animation:msgFlash 1.4s ease-out}
@keyframes msgFlash{0%,100%{box-shadow:none}12%,55%{box-shadow:0 0 0 3px rgba(37,99,235,.35)}}
.chat-edit-modal{max-width:560px}
.chat-edit-modal textarea{min-height:110px;max-height:40dvh}
.chat-edit-modal textarea:focus{border-color:var(--accent)}
@@ -997,6 +1008,18 @@ body.report-card-open .report-fields{display:flex}
.msg-file-img img{width:100%;height:110px;object-fit:cover;border-radius:6px;display:block;background:var(--bg)}
.msg-file-img .msg-file-name{font-size:.7rem;color:var(--muted)}
.chat-composer{border-top:1px solid var(--border);padding:11px 14px 13px;display:flex;flex-direction:column;gap:8px}
.chat-reply-bar{display:flex;align-items:center;gap:8px;background:var(--bg);border:1px solid var(--accent);border-radius:9px;padding:6px 9px;font-size:.78rem}
.chat-reply-bar[hidden]{display:none}
.chat-reply-bar svg{width:14px;height:14px;color:var(--accent);flex-shrink:0}
.chat-reply-meta{display:flex;flex-direction:column;gap:1px;min-width:0;flex:1}
.chat-reply-who{color:var(--accent);font-weight:600;font-size:.7rem}
.chat-reply-text{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat-reply-text[hidden]{display:none}
.chat-reply-file{display:inline-flex;align-items:center;gap:5px;color:var(--muted);min-width:0}
.chat-reply-file[hidden]{display:none}
.chat-reply-img{display:block;height:44px;width:44px;object-fit:cover;border-radius:6px;background:var(--card);flex-shrink:0}
.chat-reply-file svg{width:13px;height:13px;flex-shrink:0}
.chat-reply-file span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.chat-attach-list{display:flex;flex-wrap:wrap;gap:7px}
.chat-attach-list[hidden]{display:none}
.chat-attach-chip{display:inline-flex;align-items:center;gap:6px;background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:5px 9px;font-size:.75rem;max-width:250px}