stackchain-dashboard/frontend/following.js
timmy 846c28001e
All checks were successful
CI / lint (pull_request) Successful in 4m8s
CI / build-release (pull_request) Successful in 9s
CI / browser-journey (pull_request) Successful in 6m50s
CI / release-candidate (pull_request) Has been skipped
fix: defer Following sync until user intent
2026-08-23 04:45:27 +00:00

108 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(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:[]};
function publish(status, error) {
const state = {status, revision:snapshot.revision, items:[...snapshot.items]};
if (error) state.error = error;
options.render?.(state);
if (status === 'ready') options.onCount?.(snapshot.items.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) : [],
};
publish('ready');
return snapshot;
} catch (error) {
if (requestGeneration === generation) publish('error', error);
throw error;
}
}
function open(index) {
const item = snapshot.items[Number(index)];
if (!item) return false;
Promise.resolve(options.onOpen?.({...item, kind:'issue'})).catch(() => {});
return true;
}
return {load, open, count:() => snapshot.items.length};
}
function attachFollowing(onOpen) {
const document = globalThis.document;
const query = selector => document.querySelector(selector);
const escapeHtml = value => String(value ?? '').replace(/[&<>"']/g, character =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[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) {
const list = query('#following-list');
const status = query('#following-status');
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.');
status.textContent = 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) =>
'<button class="following-card" type="button" data-following-index="' + index + '"><span><strong>' +
escapeHtml(item.title) + '</strong><small>' + escapeHtml(item.repository + ' #' + item.number +
' · ' + item.state + ' · ' + formatTime(item.updated_at)) +
'</small></span><span aria-hidden="true"></span></button>').join('');
list.querySelectorAll('[data-following-index]').forEach(button => button.addEventListener('click', () => {
query('#following-sheet').close();
feature.open(Number(button.dataset.followingIndex));
}));
}
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 ? ' watched issue' : ' watched issues'));
},
onOpen,
});
query('#close-following').addEventListener('click', () => query('#following-sheet').close());
query('#retry-following').addEventListener('click', () => feature.load().catch(() => {}));
return {
load:feature.load,
open() {
const sheet = query('#following-sheet');
if (!sheet.open) sheet.showModal();
feature.load().catch(() => {});
return 'opened-following';
},
};
}
return {createFollowing, attachFollowing};
});