Skip to content

Commit b879197

Browse files
committed
Manage unread messages (mostly locally).
1 parent f76653c commit b879197

12 files changed

Lines changed: 267 additions & 40 deletions

File tree

Public/app/css/main.css

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1980,6 +1980,11 @@ input[type="text"]:disabled, textarea:disabled {
19801980
white-space: nowrap;
19811981
}
19821982

1983+
.chat-last-message.unread {
1984+
font-weight: 600;
1985+
color: hsl(var(--foreground));
1986+
}
1987+
19831988
.chat-muted-indicator {
19841989
display: flex;
19851990
align-items: center;
@@ -2002,26 +2007,18 @@ input[type="text"]:disabled, textarea:disabled {
20022007
}
20032008

20042009
.chat-badge {
2005-
border-radius: calc(var(--radius) - 2px);
2006-
border: 1px solid transparent;
2007-
padding-top: 0.125rem;
2008-
padding-bottom: 0.125rem;
2010+
border-radius: 10px;
20092011
font-size: 0.75rem;
20102012
font-weight: 600;
2011-
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
2012-
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
2013-
transition-duration: 150ms;
20142013
background-color: hsl(var(--primary));
20152014
color: hsl(var(--primary-foreground));
20162015
box-shadow: var(--shadow-soft);
20172016
flex-shrink: 0;
20182017
min-width: 20px;
2019-
height: 1.25rem;
2018+
height: 20px;
20202019
display: flex;
20212020
align-items: center;
20222021
justify-content: center;
2023-
padding-left: 0.375rem;
2024-
padding-right: 0.375rem;
20252022
}
20262023

20272024
.chat-content {
@@ -2284,7 +2281,7 @@ input[type="text"]:disabled, textarea:disabled {
22842281
}
22852282

22862283
.message-status-read-mark {
2287-
display: none;
2284+
position: relative;
22882285
}
22892286

22902287
/* Context Menu - Unified menu styles */

Public/app/js/api.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,21 @@ async function apiDeleteMessage(chatId, messageId) {
554554
return await handleResponse(response);
555555
}
556556

557+
async function apiMarkAsRead(chatId, messageId) {
558+
const accessToken = getAccessToken();
559+
if (!accessToken) {
560+
throw new Error('No access token available');
561+
}
562+
563+
const response = await fetch(`/chats/${chatId}/messages/${messageId}/read`, {
564+
method: 'PUT',
565+
headers: {
566+
'Authorization': `Bearer ${accessToken}`
567+
}
568+
});
569+
return await handleResponse(response);
570+
}
571+
557572
async function apiUploadFile(file, fileName, contentType, onProgress = null, onXhrCreated = null) {
558573
const accessToken = getAccessToken();
559574
if (!accessToken) {

Public/app/js/chat.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ let oldestMessageId = null; // Track the oldest message ID we've loaded
88
let hasMoreMessages = true; // Track if there are more messages to load
99
let editingMessage = null; // Track the message being edited
1010
let replyingToMessage = null; // Track the message being replied to
11+
var lastReferenceReadMessageId = null; // Track the ID of either last outgoing message that has been read by others or last incoming message (as a reference point for read status)
1112

1213
// Load messages for a chat
1314
async function loadMessages(chatId, initialLoad = false) {
@@ -63,7 +64,7 @@ async function loadMessages(chatId, initialLoad = false) {
6364
}
6465

6566
// Display messages (will prepend if not initial load, clear and append if initial)
66-
displayMessages(messages, initialLoad, hasMoreMessages);
67+
displayMessages(messages, initialLoad);
6768

6869
// Restore scroll position after DOM update (only if not initial load)
6970
if (!initialLoad) {
@@ -228,7 +229,7 @@ function updateSingleMessageGrouping(messageElement, index, allMessageElements)
228229
}
229230

230231
// Display messages in the chat area
231-
function displayMessages(messages, isInitialLoad = false, hasMoreMessages = true) {
232+
function displayMessages(messages, isInitialLoad = false) {
232233
const prepend = !isInitialLoad;
233234
console.log(`Displaying ${messages.length} messages... (prepend: ${prepend})`);
234235

@@ -262,8 +263,6 @@ function displayMessages(messages, isInitialLoad = false, hasMoreMessages = true
262263
let previousMessageInBatch = null;
263264
for (let i = 0; i < messages.length; i++) {
264265
const message = messages[i];
265-
// If there are no more messages, the very first (top) message should have a date header
266-
const isTopMessage = !hasMoreMessages && i === messages.length - 1;
267266
addMessageToChat(message, true, prepend);
268267
if (prepend) {
269268
previousMessageInBatch = message;
@@ -275,7 +274,10 @@ function displayMessages(messages, isInitialLoad = false, hasMoreMessages = true
275274
messageElements.forEach((messageElement, index) => {
276275
updateSingleMessageGrouping(messageElement, index, messageElements);
277276
});
278-
277+
278+
// Mark messages as read
279+
showCurrentChatMessagesAsRead();
280+
279281
// Handle scrolling (only for initial load)
280282
if (isInitialLoad) {
281283
// Scroll to bottom instantly when loading messages
@@ -498,6 +500,14 @@ function createMessageElement(message) {
498500
if (hasAttachments && messageDiv.dataset.attachments) {
499501
messageDiv.dataset.currentAttachmentIndex = '0';
500502
}
503+
504+
if (isOwnMessage(message) && message.id <= lastReferenceReadMessageId) {
505+
// remove hidden class to show read mark
506+
const readMark = messageDiv.querySelector('.message-status-read-mark');
507+
if (readMark) {
508+
readMark.classList.remove('hidden');
509+
}
510+
}
501511

502512
// Add long press handler for context menu
503513
addLongPressHandler(messageDiv, {
@@ -512,7 +522,28 @@ function createMessageElement(message) {
512522
return messageDiv;
513523
}
514524

515-
// Navigate message attachments
525+
function showCurrentChatMessagesAsRead() {
526+
const messagesContainer = document.getElementById('messagesContainer');
527+
if (!messagesContainer || !lastReferenceReadMessageId) return;
528+
529+
// Query only hidden read marks (more efficient - only elements that need updating)
530+
const hiddenReadMarks = messagesContainer.querySelectorAll('.message-status-read-mark.hidden');
531+
532+
hiddenReadMarks.forEach(readMark => {
533+
// Find the parent message element
534+
const messageElement = readMark.closest('.message-row.own');
535+
if (!messageElement) return;
536+
537+
const messageId = messageElement.dataset.messageId;
538+
if (!messageId) return;
539+
540+
// If this message ID is <= the reference message ID, show as read
541+
if (messageId <= lastReferenceReadMessageId) {
542+
readMark.classList.remove('hidden');
543+
}
544+
});
545+
}
546+
516547
function navigateMessageAttachment(messageId, direction) {
517548
const messageDiv = document.querySelector(`[data-message-id="${messageId}"]`)?.closest('.message-row');
518549
if (!messageDiv) return;
@@ -1031,6 +1062,13 @@ function initializeMessageInput() {
10311062

10321063
messageInput.addEventListener('input', adjustHeight);
10331064

1065+
messageInput.addEventListener('focus', function() {
1066+
// Mark chat as read when message input is focused
1067+
if (currentChatId) {
1068+
markChatAsRead(currentChatId);
1069+
}
1070+
});
1071+
10341072
messageInput.addEventListener('keydown', function(e) {
10351073
if (e.key === 'Escape') {
10361074
e.preventDefault();
@@ -1142,6 +1180,9 @@ async function sendMessage(textOverride = null) {
11421180
// Display message immediately with sending state
11431181
await addMessageToChat(message);
11441182

1183+
// Mark chat as read if not already (since user is sending a message, we can assume they've seen the chat)
1184+
markChatAsRead(currentChatId);
1185+
11451186
// Update chat list with new message
11461187
updateChatListWithMessage(message);
11471188

@@ -1241,6 +1282,18 @@ async function addMessageToChat(message, bulkAddition = false, prepend = false)
12411282
noMessages.remove();
12421283
}
12431284

1285+
// Check if message has read marks from other users to update lastReferenceReadMessageId
1286+
const hasOtherUserReadMarks = message.readMarks?.some(mark =>
1287+
mark.user && mark.user.id !== currentUser.info.id
1288+
);
1289+
1290+
if (hasOtherUserReadMarks || !isOwnMessage(message)) {
1291+
// Only update if this ID is greater than current lastReferenceReadMessageId
1292+
if (!lastReferenceReadMessageId || message.id > lastReferenceReadMessageId) {
1293+
lastReferenceReadMessageId = message.id;
1294+
}
1295+
}
1296+
12441297
// Date header is now handled inside the message element by updateSingleMessageGrouping
12451298
// No need to check for date headers here
12461299

Public/app/js/helpers.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,38 @@ function playNotificationSound() {
375375
});
376376
}
377377

378+
// Helper functions for managing unread count in localStorage
379+
function getStorageUnreadCount(chat) {
380+
if (!chat || !chat.id) return null;
381+
const key = `unreadCount_${chat.id}`;
382+
const value = localStorage.getItem(key);
383+
return value ? parseInt(value, 10) : null;
384+
}
385+
386+
function setStorageUnreadCount(chat, count) {
387+
if (!chat || !chat.id) return;
388+
const key = `unreadCount_${chat.id}`;
389+
if (count > 0) {
390+
localStorage.setItem(key, count.toString());
391+
} else {
392+
localStorage.removeItem(key);
393+
}
394+
}
395+
396+
function isMessageReadByCurrentUser(message) {
397+
if (!message || isOwnMessage(message)) return true;
398+
if (!message.readMarks) return false;
399+
return message.readMarks.some(mark => mark.user.id === currentUser.info.id);
400+
}
401+
402+
function getUnreadCount(chat) {
403+
let unreadCount = getStorageUnreadCount(chat);
404+
if (!unreadCount) {
405+
unreadCount = isMessageReadByCurrentUser(chat.lastMessage) ? 0 : 1;
406+
}
407+
return unreadCount;
408+
}
409+
378410
// Check if a string is a debug command prefix
379411
function stringIsDebugPrefix(str) {
380412
return str.startsWith('/debug') || str.startsWith('/d');

0 commit comments

Comments
 (0)