feat: chat — photo attachments

- Migration 073: attachment_url column in chat_messages
- upload.ts: add 'chat' folder to ALLOWED_FOLDERS
- chat.ts: accept attachment_url in send message, return it in GET messages
- api.ts: attachmentUrl in ChatMessage type, uploadImage() with raw FormData fetch
- ChatWidget: paperclip button, file preview thumbnail with remove, image display
  inline in message bubbles (clickable, opens original), send enabled with image only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 12:13:07 +03:00
parent d0dbf33701
commit 7ccf752429
5 changed files with 108 additions and 22 deletions

View File

@@ -335,8 +335,21 @@ export const api = {
req<ChatRoom[]>('GET', `/api/hotels/${slug}/chat/rooms`),
getMessages: (slug: string, roomId: string, limit = 50) =>
req<ChatMessage[]>('GET', `/api/hotels/${slug}/chat/rooms/${roomId}/messages?limit=${limit}`),
sendMessage: (slug: string, roomId: string, text: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages`, { text }),
sendMessage: (slug: string, roomId: string, text: string, attachmentUrl?: string) =>
req<ChatMessage>('POST', `/api/hotels/${slug}/chat/rooms/${roomId}/messages`, { text, ...(attachmentUrl ? { attachment_url: attachmentUrl } : {}) }),
uploadImage: async (file: File): Promise<{ url: string }> => {
const form = new FormData()
form.append('file', file)
const token = getToken()
const res = await fetch(`${BASE}/api/upload?folder=chat`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
credentials: 'include',
body: form,
})
if (!res.ok) throw new ApiError(res.status, 'Upload failed')
return res.json() as Promise<{ url: string }>
},
markRead: (slug: string, roomId: string) =>
req<{ ok: boolean }>('PATCH', `/api/hotels/${slug}/chat/rooms/${roomId}/read`),
openDirect: (slug: string, otherUserId: string) =>
@@ -1326,6 +1339,7 @@ export interface ChatMessage {
createdAt: string
isSystem: boolean
systemName: string | null
attachmentUrl: string | null
}
export interface ChatSearchResult {