diff --git a/web/src/hooks/useRealtime.ts b/web/src/hooks/useRealtime.ts index e1e5739..73ad9fa 100644 --- a/web/src/hooks/useRealtime.ts +++ b/web/src/hooks/useRealtime.ts @@ -102,7 +102,7 @@ export function useRealtime() { ws.send(JSON.stringify({ event: "message_read", payload: { chat_id: chatId, message_id: message.id } })); chatStore.clearUnread(chatId); } else if (wasInserted) { - chatStore.incrementUnread(chatId); + chatStore.incrementUnread(chatId, hasMentionForUser(message.text, authStore.me?.username ?? null)); } maybeShowBrowserNotification(chatId, message, chatStore.activeChatId); } @@ -331,3 +331,21 @@ function maybeShowBrowserNotification(chatId: number, message: Message, activeCh }; })(); } + +function hasMentionForUser(text: string | null, username: string | null): boolean { + if (!text || !username) { + return false; + } + const normalizedUsername = username.toLowerCase(); + const mentionRegex = /(^|[^A-Za-z0-9_])@([A-Za-z0-9_]{3,50})(?![A-Za-z0-9_])/g; + let match: RegExpExecArray | null; + while (true) { + match = mentionRegex.exec(text); + if (!match) { + return false; + } + if ((match[2] ?? "").toLowerCase() === normalizedUsername) { + return true; + } + } +} diff --git a/web/src/store/chatStore.ts b/web/src/store/chatStore.ts index ec2acca..9be51fd 100644 --- a/web/src/store/chatStore.ts +++ b/web/src/store/chatStore.ts @@ -73,7 +73,7 @@ interface ChatState { removeMessage: (chatId: number, messageId: number) => void; restoreMessages: (chatId: number, messages: Message[]) => void; clearChatMessages: (chatId: number) => void; - incrementUnread: (chatId: number) => void; + incrementUnread: (chatId: number, hasMention?: boolean) => void; clearUnread: (chatId: number) => void; setTypingUsers: (chatId: number, userIds: number[]) => void; setReplyToMessage: (chatId: number, message: Message | null) => void; @@ -123,7 +123,9 @@ export const useChatStore = create((set, get) => ({ ...state.loadingMoreByChat, [chatId]: false }, - chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, unread_count: 0 } : chat)) + chats: state.chats.map((chat) => + chat.id === chatId ? { ...chat, unread_count: 0, unread_mentions_count: 0 } : chat + ) })); const lastMessage = sorted[sorted.length - 1]; if (lastMessage) { @@ -315,17 +317,29 @@ export const useChatStore = create((set, get) => ({ ...state.unreadBoundaryByChat, [chatId]: 0 }, - chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, unread_count: 0 } : chat)) + chats: state.chats.map((chat) => + chat.id === chatId ? { ...chat, unread_count: 0, unread_mentions_count: 0 } : chat + ) })), - incrementUnread: (chatId) => + incrementUnread: (chatId, hasMention = false) => set((state) => ({ chats: state.chats.map((chat) => - chat.id === chatId ? { ...chat, unread_count: (chat.unread_count ?? 0) + 1 } : chat + chat.id === chatId + ? { + ...chat, + unread_count: (chat.unread_count ?? 0) + 1, + unread_mentions_count: hasMention + ? (chat.unread_mentions_count ?? 0) + 1 + : (chat.unread_mentions_count ?? 0) + } + : chat ) })), clearUnread: (chatId) => set((state) => ({ - chats: state.chats.map((chat) => (chat.id === chatId ? { ...chat, unread_count: 0 } : chat)), + chats: state.chats.map((chat) => + chat.id === chatId ? { ...chat, unread_count: 0, unread_mentions_count: 0 } : chat + ), unreadBoundaryByChat: { ...state.unreadBoundaryByChat, [chatId]: 0