feat: chat pagination — infinite scroll / load older messages

- Backend: cursor-based pagination via ?before=<ISO timestamp>
- Frontend: auto-load when scrolling to top (<60px), spinner + manual 'Загрузить ещё' button
- Poll merge: preserves older history when new messages arrive from polling
- hasMoreMsgs flag: shown only when exactly 50 msgs returned (more may exist)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 16:35:38 +03:00
parent ef1fd4246e
commit e7ad9862ba
3 changed files with 77 additions and 22 deletions

View File

@@ -111,7 +111,7 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
// ── GET messages ──────────────────────────────────────────────────────────
fastify.get<RoomParam & { Querystring: { limit?: string } }>(
fastify.get<RoomParam & { Querystring: { limit?: string; before?: string } }>(
'/api/hotels/:slug/chat/rooms/:roomId/messages',
{ onRequest: [fastify.authenticate] },
async (request, reply) => {
@@ -122,8 +122,13 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' })
const limit = Math.min(Number(request.query.limit ?? 50), 100)
const before = request.query.before // ISO timestamp cursor
const userId = request.user.sub
const params: unknown[] = [roomId, hotelId, userId, limit]
const beforeClause = before ? `AND m.created_at < $5` : ''
if (before) params.push(before)
const { rows } = await db.query(
`SELECT m.id, m.room_id, m.sender_id, m.text, m.created_at,
m.is_system, m.system_name, m.attachment_url, m.edited_at, m.deleted_at,
@@ -142,17 +147,20 @@ const chatRoutes: FastifyPluginAsync = async (fastify) => {
) AS reactions
FROM chat_messages m
LEFT JOIN users u ON u.id = m.sender_id
WHERE m.room_id = $1 AND m.hotel_id = $2
WHERE m.room_id = $1 AND m.hotel_id = $2 ${beforeClause}
ORDER BY m.created_at DESC
LIMIT $4`,
[roomId, hotelId, userId, limit],
params,
)
await db.query(
`INSERT INTO chat_read_status (room_id, user_id, last_read) VALUES ($1, $2, NOW())
ON CONFLICT (room_id, user_id) DO UPDATE SET last_read = NOW()`,
[roomId, userId],
)
// Only mark read on initial load (no before cursor)
if (!before) {
await db.query(
`INSERT INTO chat_read_status (room_id, user_id, last_read) VALUES ($1, $2, NOW())
ON CONFLICT (room_id, user_id) DO UPDATE SET last_read = NOW()`,
[roomId, userId],
)
}
return rows.reverse()
},
)