feat: swipe gestures, pin/mute, last seen, emoji fix, push/rename fixes

- Add swipe-to-reveal in mobile chat list (right=pin, left=mute/leave)
- Pin/mute API endpoints with is_pinned DB column migration
- Show pin icon on pinned chats, mute icon on muted chats
- Pinned chats sort to top of list
- Fix push notifications sent to message sender (filter uid)
- Remove debug console.log from WS handler
- Fix chat rename not updating in sidebar without reload
- Fix emoji picker z-index and positioning on mobile (z-50, bottom-full)
- Add last seen time in chat header for private chats

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ai
2026-05-23 17:08:18 +03:00
parent 20daa2ca2f
commit 9262ece939
9 changed files with 184 additions and 43 deletions

View File

@@ -91,6 +91,7 @@ export async function initDB() {
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar TEXT DEFAULT NULL;
ALTER TABLE chats ADD COLUMN IF NOT EXISTS avatar TEXT DEFAULT NULL;
ALTER TABLE users ADD COLUMN IF NOT EXISTS position TEXT DEFAULT NULL;
ALTER TABLE chat_members ADD COLUMN IF NOT EXISTS is_pinned BOOLEAN DEFAULT FALSE;
`);
// Seed admin if no users

View File

@@ -16,7 +16,7 @@ export default async function chatRoutes(app: FastifyInstance) {
const { rows } = await pool.query(`
SELECT
c.id, c.type, c.title, c.description, c.avatar_color, c.avatar, c.is_public, c.created_at,
cm.role, cm.last_read_at,
cm.role, cm.last_read_at, cm.is_pinned, cm.is_muted,
(SELECT content FROM messages WHERE chat_id = c.id AND is_deleted = FALSE ORDER BY created_at DESC LIMIT 1) AS last_message,
(SELECT created_at FROM messages WHERE chat_id = c.id AND is_deleted = FALSE ORDER BY created_at DESC LIMIT 1) AS last_message_at,
(SELECT COUNT(*) FROM messages WHERE chat_id = c.id AND is_deleted = FALSE AND created_at > cm.last_read_at) AS unread_count,
@@ -44,7 +44,7 @@ export default async function chatRoutes(app: FastifyInstance) {
) END AS private_user_id
FROM chats c
JOIN chat_members cm ON cm.chat_id = c.id AND cm.user_id = $1
ORDER BY COALESCE(
ORDER BY cm.is_pinned DESC, COALESCE(
(SELECT created_at FROM messages WHERE chat_id = c.id AND is_deleted = FALSE ORDER BY created_at DESC LIMIT 1),
c.created_at
) DESC
@@ -65,6 +65,8 @@ export default async function chatRoutes(app: FastifyInstance) {
memberCount: parseInt(r.member_count),
privateUserId: r.private_user_id,
createdAt: r.created_at,
isPinned: r.is_pinned,
isMuted: r.is_muted,
}));
});
@@ -308,6 +310,24 @@ export default async function chatRoutes(app: FastifyInstance) {
return { ok: true };
});
// Pin/unpin chat for current user
app.put('/:id/pin', async (req) => {
const { id: userId } = req.user as { id: string };
const { id } = req.params as { id: string };
const { pinned } = req.body as { pinned: boolean };
await pool.query('UPDATE chat_members SET is_pinned = $1 WHERE chat_id = $2 AND user_id = $3', [pinned, id, userId]);
return { ok: true };
});
// Mute/unmute chat for current user
app.put('/:id/mute', async (req) => {
const { id: userId } = req.user as { id: string };
const { id } = req.params as { id: string };
const { muted } = req.body as { muted: boolean };
await pool.query('UPDATE chat_members SET is_muted = $1 WHERE chat_id = $2 AND user_id = $3', [muted, id, userId]);
return { ok: true };
});
// Leave chat
app.delete('/:id/leave', async (req, reply) => {
const { id: userId } = req.user as { id: string };

View File

@@ -79,7 +79,6 @@ export function setupWebSocket(app: FastifyInstance) {
ws.on('message', async (raw: any, isBinary: boolean) => {
try {
const str = isBinary ? raw.toString('utf8') : raw.toString();
console.log('[WS] received:', str.substring(0, 200));
const { type, payload } = JSON.parse(str);
if (type === 'typing') {
@@ -134,7 +133,7 @@ export function setupWebSocket(app: FastifyInstance) {
const members = await getChatMemberIds(payload.chatId);
broadcast(members, { type: 'new_message', payload: fullMsg });
await sendPushToOfflineUsers(members, {
await sendPushToOfflineUsers(members.filter(id => id !== uid), {
title: sender.display_name,
body: payload.content.substring(0, 100),
chatId: payload.chatId