Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104// Renders the AIM buddy list: Online / Idle / Offline groups, each a sorted set// of buddy rows with the running-man status icon.//// Rendering is INCREMENTAL: row elements are created once and reused (keyed by// DID), updated in place, and moved between groups as state changes. A full// innerHTML rebuild would recreate every <img> on every render — and the// last-seen backfill fires many renders — which made the avatars flicker.
import { esc, relTime } from './util.js';import { verbFor } from './status.js';
// Displayed label for a buddy (the bold name in the row), used for render + sort.const nameOf = (b) => b.profile?.displayName?.trim() || b.profile?.handle || b.did;
const subOf = (b) => !b.lastSeen ? 'no recent activity' : b.lastSeenApprox ? `last seen ${relTime(b.lastSeen)}` // from rev backfill (action unknown) : `${verbFor(b.lastAction)} · ${relTime(b.lastSeen)}`; // from a live/replay event
// The little yellow running man (appearance follows the parent .buddy.<state>).const RUNNER = `<svg class="runner" viewBox="0 0 24 24" aria-hidden="true"> <g fill="none" stroke-linecap="round" stroke-linejoin="round"> <g class="ink" stroke="#5a4500" stroke-width="4.2"> <circle cx="15.5" cy="5" r="2.6" fill="#5a4500"/> <path d="M15 8 L11 14"/><path d="M15 9.5 L19.5 8.2"/><path d="M15 9.5 L12 11.5"/> <path d="M11 14 L15 19"/><path d="M11 14 L7 18"/> </g> <g class="body" stroke="#f5c518" stroke-width="2.6"> <circle cx="15.5" cy="5" r="2.4" fill="#f5c518"/> <path d="M15 8 L11 14"/><path d="M15 9.5 L19.5 8.2"/><path d="M15 9.5 L12 11.5"/> <path d="M11 14 L15 19"/><path d="M11 14 L7 18"/> </g> </g></svg>`;
// Build a row element once. The avatar <img> here is never recreated, so it// won't reload/flicker across renders.function createRow(b) { const li = document.createElement('li'); li.dataset.did = b.did; const name = nameOf(b); const av = b.profile?.avatar ? `<img class="bd-av" src="${esc(b.profile.avatar)}" alt="" loading="lazy">` : `<span class="bd-av blank">${esc((name[0] || '?').toUpperCase())}</span>`; li.innerHTML = `${RUNNER}${av} <span class="bd-text"><span class="bd-name"></span><span class="bd-sub"></span></span>`; return li;}
// Update only the volatile bits (state class, name, subtext) — never the avatar.function updateRow(el, b, flashing) { el.className = `buddy ${b.state}${flashing.has(b.did) ? ' flash' : ''}`; el.title = `@${b.profile?.handle || ''} — open IM`; el.querySelector('.bd-name').textContent = nameOf(b); el.querySelector('.bd-sub').textContent = subOf(b);}
// Make `ul`'s children exactly `list` (in order), reusing cached row elements.function reconcile(ul, list, rows, flashing) { let prev = null; for (const b of list) { let el = rows.get(b.did); if (!el) { el = createRow(b); rows.set(b.did, el); } updateRow(el, b, flashing); const target = prev ? prev.nextSibling : ul.firstChild; if (el !== target) ul.insertBefore(el, target); prev = el; } // drop any trailing leftovers (rows that left this group) while (prev ? prev.nextSibling : ul.firstChild) (prev ? prev.nextSibling : ul.firstChild).remove();}
function ensureSkeleton(listEl) { if (listEl.querySelector('.group')) return; listEl.innerHTML = ['online', 'idle', 'offline'].map((k) => `<div class="group" data-group="${k}"><div class="group-hd"></div><ul class="buddies"></ul></div>`).join('');}
const LABELS = { online: 'Online', idle: 'Idle', offline: 'Offline' };
export function renderBuddyList(listEl, countEl, buddies, { groupsOpen, flashing }) { ensureSkeleton(listEl); const rows = listEl._rows || (listEl._rows = new Map());
const all = [...buddies.values()]; const grouped = { online: [], idle: [], offline: [] }; for (const b of all) grouped[b.state].push(b);
for (const key of ['online', 'idle', 'offline']) { const list = grouped[key].sort((a, b) => nameOf(a).localeCompare(nameOf(b), undefined, { sensitivity: 'base' })); const groupEl = listEl.querySelector(`.group[data-group="${key}"]`); const open = groupsOpen[key]; groupEl.className = `group ${open ? 'open' : 'closed'}`; groupEl.querySelector('.group-hd').innerHTML = `<span class="tw">${open ? '▼' : '▶'}</span> ${LABELS[key]} (${list.length})`; const ul = groupEl.querySelector('.buddies'); if (open) reconcile(ul, list, rows, flashing); else ul.replaceChildren(); // collapsed: don't hold rows for it }
countEl.textContent = `Buddies (${grouped.online.length + grouped.idle.length}/${all.length})`; for (const [did, el] of rows) if (!el.isConnected) rows.delete(did); // prune detached}