(function (root, factory) { const exports = factory(); if (typeof module === 'object' && module.exports) { module.exports = exports.createFollowing; module.exports.attachFollowing = exports.attachFollowing; } else { root.createFollowing = exports.createFollowing; root.attachFollowing = exports.attachFollowing; } })(typeof self !== 'undefined' ? self : this, function () { function createFollowing(options) { let generation = 0; let snapshot = {revision:0, items:[]}; let review = null; function publish(status, error) { const state = {status, revision:snapshot.revision, items:[...snapshot.items]}; state.degraded = snapshot.degraded === true; state.refreshFailures = Number(snapshot.refreshFailures) || 0; if (review?.acknowledged.size) state.reviewSummary = { reviewed:review.acknowledged.size, remaining:snapshot.items.filter(item => item.has_unseen_change === true).length, }; if (error) state.error = error; options.render?.(state); if (status === 'ready') options.onCount?.( snapshot.items.filter(item => item.has_unseen_change === true).length); return state; } async function load() { const requestGeneration = ++generation; publish('loading'); try { const result = await options.fetchJson('api/v1/following', {headers:{Accept:'application/json'}}); if (requestGeneration !== generation) return snapshot; snapshot = { revision:Number(result?.revision) || 0, items:Array.isArray(result?.items) ? result.items.slice(0, 50) : [], degraded:result?.degraded === true, refreshFailures:Number(result?.refresh_failures) || 0, }; publish('ready'); return snapshot; } catch (error) { if (requestGeneration === generation) publish('error', error); throw error; } } async function acknowledge(item) { const current = snapshot.items.find(candidate => candidate.repository === item?.repository && Number(candidate.number) === Number(item?.number) && candidate.updated_at === item?.updated_at); if (!current || current.has_unseen_change !== true || typeof options.onAcknowledge !== 'function') return false; await options.onAcknowledge(current); current.has_unseen_change = false; review?.acknowledged.add(current.repository + '#' + current.number + '@' + current.updated_at); publish('ready'); return true; } async function open(index) { const item = snapshot.items[Number(index)]; if (!item) return false; await options.onOpen?.({...item, kind:'issue', following:true}); await acknowledge(item); return true; } async function startReview() { const items = snapshot.items .filter(item => item.has_unseen_change === true) .map(item => ({...item, kind:'issue', following:true})); if (!items.length) return false; review = {items, more:false, active:true, acknowledged:new Set()}; await open(snapshot.items.indexOf(snapshot.items.find(item => item.repository === items[0].repository && Number(item.number) === Number(items[0].number)))); return true; } function finishReview() { const completed = Boolean(review?.completed || (review?.active && !snapshot.items.some(item => item.has_unseen_change === true))); if (review) review.active = false; if (completed && !review.completed) { review.completed = true; options.onReviewComplete?.(); } publish('ready'); return completed; } function retire(item) { const same = candidate => candidate.repository === item?.repository && Number(candidate.number) === Number(item?.number); const index = review?.active ? review.items.findIndex(same) : -1; snapshot.items = snapshot.items.filter(candidate => !same(candidate)); if (index < 0) { publish('ready'); return null; } review.items.splice(index, 1); const next = review.items[index] || null; if (!next) { review.active = false; if (!review.completed) { review.completed = true; options.onReviewComplete?.(); } } publish('ready'); return next ? {...next} : null; } return { load, open, startReview, previewLoaded:acknowledge, finishReview, retire, session:() => review?.active ? {items:[...review.items], more:false} : null, items:() => snapshot.items.map(item => ({...item})), count:() => snapshot.items.length, }; } function attachFollowing(onOpen, hooks = {}) { const document = globalThis.document; const query = selector => document.querySelector(selector); const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, character => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[character]); const formatTime = value => new Date(value).toLocaleString(); const fetchJson = async (url, options) => { const response = await fetch(url, options); const payload = await response.json().catch(() => ({})); if (!response.ok) throw new Error(payload.detail || payload.error || 'Following is temporarily unavailable.'); return payload; }; let feature; function render(state) { hooks.onStatus?.(state.status); const list = query('#following-list'); const status = query('#following-status'); const reviewButton = query('#review-following'); query('#retry-following').hidden = state.status !== 'error'; if (state.status === 'loading') return void (status.textContent = 'Loading watched issues…'); if (state.status === 'error') return void (status.textContent = state.error?.message || 'Following is temporarily unavailable.'); const unseen = state.items.filter(item => item.has_unseen_change === true).length; reviewButton.hidden = unseen === 0; reviewButton.textContent = unseen === 1 ? 'Review new activity' : 'Review ' + unseen + ' new changes'; status.textContent = (state.reviewSummary ? 'Reviewed ' + state.reviewSummary.reviewed + ' changes · ' + state.reviewSummary.remaining + ' still need review. ' : '') + (state.degraded ? 'Some watched issues could not be refreshed. Showing last known details. ' : '') + (state.items.length ? state.items.length + (state.items.length === 1 ? ' watched issue.' : ' watched issues.') : 'No watched issues yet. Watch one from Search to keep it here.'); list.innerHTML = state.items.map((item, index) => '').join(''); list.querySelectorAll('[data-following-index]').forEach(button => button.addEventListener('click', () => { query('#following-sheet').close(); feature.open(Number(button.dataset.followingIndex)).catch(() => {}); })); } feature = createFollowing({ fetchJson, render, onCount:count => { const value = query('[data-mobile-queue-count="following"]'); value.textContent = count; value.closest('button').setAttribute('aria-label', 'Following, ' + count + (count === 1 ? ' unseen change' : ' unseen changes')); hooks.onCount?.(count, feature.items()); }, onOpen, onReviewComplete:hooks.onReviewComplete, onAcknowledge:item => { const [owner, repo] = item.repository.split('/'); return fetchJson('api/v1/following/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + '/issues/' + item.number + '/seen', { method:'PUT', headers:{'Content-Type':'application/json', Accept:'application/json'}, body:JSON.stringify({updated_at:item.updated_at}), }); }, }); query('#close-following').addEventListener('click', () => query('#following-sheet').close()); query('#review-following').addEventListener('click', () => { query('#following-sheet').close(); feature.startReview().catch(() => { if (!query('#following-sheet').open) query('#following-sheet').showModal(); }); }); query('#retry-following').addEventListener('click', () => feature.load().catch(() => {})); return { load:feature.load, review:feature.startReview, open() { const sheet = query('#following-sheet'); if (!sheet.open) sheet.showModal(); feature.load().catch(() => {}); return 'opened-following'; }, session:feature.session, previewLoaded:feature.previewLoaded, retire:feature.retire, returnToFollowing() { const completed = feature.finishReview(); if (completed) return 'completed-following'; const sheet = query('#following-sheet'); if (!sheet.open) sheet.showModal(); globalThis.requestAnimationFrame?.(() => { const target = query('#review-following:not([hidden])') || query('.following-card') || query('#close-following'); target?.focus(); }); return 'returned-following'; }, }; } return {createFollowing, attachFollowing}; });