feat: chat — edit/delete messages + emoji reactions

- Migration 074: edited_at, deleted_at columns + chat_reactions table
- Backend: PATCH edit (own only), DELETE soft-delete (own/admin), POST toggle reaction
  fetchMessage helper returns full message with aggregated reactions in one query
- API types: ChatReaction interface, editedAt/deletedAt/reactions in ChatMessage
- ChatWidget: hover toolbar (😊 react · ✏️ edit · 🗑️ delete), inline edit with
  save/cancel (Enter/Esc), reaction emoji picker (8 emojis), reaction pills below
  messages (click to toggle), '· изм.' label on edited messages, soft-deleted
  messages show 'Сообщение удалено' placeholder

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:31:17 +03:00
parent 7ccf752429
commit a29f58282f
4 changed files with 440 additions and 93 deletions

View File

@@ -354,6 +354,12 @@ export const api = {
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/read`),
openDirect: (slug: string, otherUserId: string) =>
req<{ roomId: string }>('POST', `/api/hotels/${slug}/chat/direct/${otherUserId}`),
editMessage: (slug: string, roomId: string, msgId: string, text: string) =>
req<ChatMessage>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}`, { text }),
deleteMessage: (slug: string, roomId: string, msgId: string) =>
req<{ ok: boolean; id: string }>('DELETE', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}`),
toggleReaction: (slug: string, roomId: string, msgId: string, emoji: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages/${msgId}/reactions`, { emoji }),
search: (slug: string, q: string) =>
req<ChatSearchResult[]>('GET', `/api/hotels/${slug}/chat/search?q=${encodeURIComponent(q)}`),
notify: (slug: string, text: string, systemName?: string) =>
@@ -1329,6 +1335,12 @@ export interface ChatRoom {
otherUserId: string | null
}
export interface ChatReaction {
emoji: string
count: number
hasOwn: boolean
}
export interface ChatMessage {
id: string
roomId: string
@@ -1337,9 +1349,12 @@ export interface ChatMessage {
senderRole: string
text: string
createdAt: string
editedAt: string | null
deletedAt: string | null
isSystem: boolean
systemName: string | null
attachmentUrl: string | null
reactions: ChatReaction[]
}
export interface ChatSearchResult {