collab per-card unread, detail stale banner, image sync race fix
- per-card unread dot on board (collab-buckets): backend tracks last_seen_at per card, poll exposes unread_cards, frontend decorates matching board cards; cleared on any open path via detail.opened hook - detail.opened + cards.reloaded hooks on the hyprcrd event bus - stale banner in detail view with manual Reload when the currently viewed card got updated by a peer — no content swap under the user - collab image sync: push attachments BEFORE card (race fix) + pull-side self-heal scans every collab card for missing uploads each poll - relay user deletion now purges chat_messages sent by deleted user so ghost threads stop appearing for their peers - calendar header nav buttons styled as square bordered buttons that honor --ui-radius-button; title bar widened to 280px so font variants don't wrap - .toolbar-btn now uses var(--ui-radius-button) instead of hardcoded 3px Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
39e94b8015
commit
26f95ba079
10 files changed
+324
-35
No files matched your search
@@ -24,6 +24,8 @@ const $ = (id) => document.getElementById(id);
|
||||
|
||||
// uid -> collab bucket meta (from /api/collab-buckets)
|
||||
let collabByUid = new Map();
|
||||
// card_id (number) -> true for cards with foreign edits we haven't opened yet
|
||||
let unreadCards = new Set();
|
||||
let pollHandle = null;
|
||||
const POLL_INTERVAL_MS = 30000;
|
||||
|
||||
@@ -92,6 +94,12 @@ async function pollOnce() {
|
||||
const n = res.outbox_processed;
|
||||
showQueuedToast(`${n} queued bucket${n === 1 ? '' : 's'} synced to the relay.`);
|
||||
}
|
||||
// Per-card unread list: cards with foreign edits we haven't opened
|
||||
// yet. Decorated on the board via decorateBoardUnread().
|
||||
if (Array.isArray(res.unread_cards)) {
|
||||
unreadCards = new Set(res.unread_cards.map(Number));
|
||||
decorateBoardUnread();
|
||||
}
|
||||
// Phase 4: edit-lock list comes back with the poll
|
||||
if (Array.isArray(res.locks)) {
|
||||
const next = new Map();
|
||||
@@ -638,12 +646,71 @@ function decorateBoardLocks() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk every rendered board card and add / remove the "red dot" unread
|
||||
* indicator for cards with foreign edits we haven't opened yet.
|
||||
* Idempotent — safe to re-run on every board re-render.
|
||||
*/
|
||||
function decorateBoardUnread() {
|
||||
const board = document.getElementById('board');
|
||||
if (!board) return;
|
||||
for (const el of board.querySelectorAll('.board-card[data-id]')) {
|
||||
const cid = parseInt(el.dataset.id);
|
||||
const isUnread = unreadCards.has(cid);
|
||||
el.classList.toggle('collab-card-unread', isUnread);
|
||||
let dot = el.querySelector('.collab-card-dot');
|
||||
if (isUnread && !dot) {
|
||||
dot = document.createElement('span');
|
||||
dot.className = 'collab-card-dot';
|
||||
el.appendChild(dot);
|
||||
} else if (!isUnread && dot) {
|
||||
dot.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear a card's unread state locally + on the backend. Called from
|
||||
// - board click (piggy-backing on the natural open-card interaction)
|
||||
// - detail.opened hook (when the detail view opens via any path:
|
||||
// keyboard shortcut, related-card link, backlink, search result, etc.)
|
||||
function clearCardUnread(cid) {
|
||||
cid = parseInt(cid);
|
||||
if (!cid || !unreadCards.has(cid)) return;
|
||||
unreadCards.delete(cid);
|
||||
const el = document.querySelector(`#board .board-card[data-id="${cid}"]`);
|
||||
if (el) {
|
||||
el.classList.remove('collab-card-unread');
|
||||
el.querySelector('.collab-card-dot')?.remove();
|
||||
}
|
||||
hyprcrd.api('api/collab-buckets/mark-card-seen', {
|
||||
method: 'POST', body: { card_id: cid },
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
function setupCardClickInterceptor() {
|
||||
const board = document.getElementById('board');
|
||||
if (board) {
|
||||
board.addEventListener('click', (e) => {
|
||||
const el = e.target.closest('.board-card[data-id]');
|
||||
if (el) clearCardUnread(el.dataset.id);
|
||||
});
|
||||
}
|
||||
// Detail view has its own open path (keyboard shortcuts, backlinks,
|
||||
// related-card navigation) that doesn't route through a board-card
|
||||
// click. Listen for the explicit detail.opened hook so every open
|
||||
// method clears the dot.
|
||||
hyprcrd.hooks?.on?.('detail.opened', (data) => clearCardUnread(data?.id));
|
||||
}
|
||||
|
||||
// Re-decorate whenever the board re-renders (reuses the existing #board
|
||||
// MutationObserver pattern).
|
||||
function setupBoardObserver() {
|
||||
const board = document.getElementById('board');
|
||||
if (!board) return;
|
||||
const obs = new MutationObserver(() => decorateBoardLocks());
|
||||
const obs = new MutationObserver(() => {
|
||||
decorateBoardLocks();
|
||||
decorateBoardUnread();
|
||||
});
|
||||
obs.observe(board, { childList: true, subtree: false });
|
||||
}
|
||||
|
||||
@@ -899,12 +966,14 @@ async function init() {
|
||||
}
|
||||
setupBarObserver();
|
||||
setupClickInterceptor();
|
||||
setupCardClickInterceptor();
|
||||
setupBoardObserver();
|
||||
await loadCollabMeta();
|
||||
await pollOnce();
|
||||
await loadCollabMeta();
|
||||
decorateBucketBar();
|
||||
decorateBoardLocks();
|
||||
decorateBoardUnread();
|
||||
startPolling();
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user