Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,12 @@ function observeDisplayedMessages() {
void postJson('/api/messages/read/bulk', { message_ids: ids }).then(async response => {
if (generation !== _exchangeGeneration || org !== getSessionScopedOrgId()) return;
if (response?.success !== true) throw new Error('Read state was not saved');
_readRevision += 1;
// A full update receipt confirms every submitted ID, including loaded
// older pages. Partial receipts require read-back; never clear all IDs.
if (response.updated_count === ids.length) {
const newlyRead = _currentMessages.filter(message => ids.includes(message.concept_id)
&& isUnreadMessage(message)).length;
_readRevision += 1;
if (_exchange) _exchange.shared_unread_count = Math.max(0,
(_exchange.shared_unread_count || 0) - newlyRead);
_currentMessages.forEach(message => {
Expand All @@ -314,6 +314,9 @@ function observeDisplayedMessages() {
if (_currentMessages.some(m => ids.includes(m.concept_id) && isUnreadMessage(m))) showReadFailure();
else {
root.querySelector('.message-read-status')?.remove();
// The ensuing catalogue request must capture a settled read;
// otherwise its authoritative count is rejected as in-flight.
ids.forEach(id => _pendingReads.delete(`${generation}:${id}`));
document.dispatchEvent(new CustomEvent('von:conversation-contribution'));
}
}).catch(() => {
Expand Down
15 changes: 14 additions & 1 deletion tests/browser/messageUnreadNavigation.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ const server = http.createServer(async (req, res) => {
};
return;
}
if (req.url.startsWith('/api/messages/conversation-catalogue')) {
return res.end(JSON.stringify({ conversations: ['bob', 'carol'].map(other => ({
session_id: `messages:#V#${other}`, source_kind: 'message_exchange', viewer_id: '#V#alice',
participant_ids: ['#V#alice', `#V#${other}`], other_participant_ids: [`#V#${other}`],
shared_unread_count: readMessages.has(`${other}-final`) ? 0 : 1
})) }));
}
if (req.url === '/api/messages/exchange') {
let body = ''; for await (const chunk of req) body += chunk;
const { before, participant_ids } = JSON.parse(body); cursors.push(before);
Expand Down Expand Up @@ -172,7 +179,12 @@ const server = http.createServer(async (req, res) => {
participant_ids: ['#V#alice', other], other_participant_ids: [other],
session_name: other, shared_unread_count: count });
}, { other, count });
await open('#V#bob', 1);
await reading.evaluate(async () => {
const { refreshMessageCatalogue } = await import('/static/js/components/conversationCatalogue.js');
document.addEventListener('von:conversation-contribution', () => void refreshMessageCatalogue());
});
// An outdated opening count must converge with the tile after a read.
await open('#V#bob', 2);
const unreadButton = reading.locator('.message-jump-unread');
await expect(unreadButton).toBeVisible();
await expect.poll(() => Boolean(pendingRead)).toBe(true);
Expand All @@ -188,6 +200,7 @@ const server = http.createServer(async (req, res) => {
pendingRead(); pendingRead = null;
await expect(unreadButton).toBeHidden();
results[results.length - 1].zeroUnreadHiddenWithOlderHistory = true;
results[results.length - 1].postReadCatalogueReconciled = true;
results[results.length - 1].switchAndReopenCorrect = true;
await reading.close();
}
Expand Down
24 changes: 24 additions & 0 deletions tests/frontend/messageExchangeView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,27 @@ test('a catalogue count during an in-flight read is not decremented twice', asyn
expect(document.querySelector('.is-unread')).toBeNull();
expect(document.querySelector('.message-jump-unread').hidden).toBe(false);
});


test.each([0, 1])('post-read catalogue refresh reconciles stale counts after a receipt of %i', async updatedCount => {
const observers = mockReadObserver();
const api = require(base + 'apiService.js');
api.postJson.mockResolvedValueOnce(page(['last'], 'older', ['last']));
const panel = require(base + 'components/messagePanel.js');
const exchange = { ...row('#V#bob'), shared_unread_count: 2 };
await panel.openMessageExchange(exchange);
const staleRefresh = panel.captureMessageExchangeUnreadRefresh();
let reconcile;
document.addEventListener('von:conversation-contribution', () => {
reconcile = panel.captureMessageExchangeUnreadRefresh();
}, { once: true });
api.postJson.mockResolvedValueOnce({ success: true, updated_count: updatedCount })
.mockResolvedValueOnce(page(['last'], 'older'));
observers[0].callback([visibleEntry()]); await flush();
expect(document.querySelector('.is-unread')).toBeNull();
expect(reconcile).toBeDefined();
reconcile([{ ...exchange, shared_unread_count: 0 }]);
expect(document.querySelector('.message-jump-unread').hidden).toBe(true);
staleRefresh([exchange]);
expect(document.querySelector('.message-jump-unread').hidden).toBe(true);
});
Loading