162 lines
6.1 KiB
JavaScript
162 lines
6.1 KiB
JavaScript
function createTodayProgress({ storage, getLogin = () => '', admit, makeId = () =>
|
|
globalThis.crypto?.randomUUID?.() || String(Date.now()) + '-' + Math.random().toString(16).slice(2),
|
|
maxLength = 2000, maxItems = 20 }) {
|
|
const prefix = 'stackchain.today-progress.v1.';
|
|
const login = () => String(getLogin() || '').trim().toLowerCase();
|
|
const storageKey = () => login() ? prefix + encodeURIComponent(login()) : '';
|
|
const validIdentity = identity => typeof identity === 'string' && identity.length > 0 && identity.length <= 500;
|
|
const validRecord = record => record && typeof record.body === 'string' && record.body.length > 0 &&
|
|
record.body.length <= maxLength && typeof record.operation_id === 'string' && record.operation_id.length > 0 &&
|
|
record.operation_id.length <= 128;
|
|
|
|
function read() {
|
|
const key = storageKey();
|
|
if (!storage || !key) return {};
|
|
try {
|
|
const saved = JSON.parse(storage.getItem(key) || 'null');
|
|
if (!saved || saved.version !== 1 || !saved.drafts || typeof saved.drafts !== 'object' ||
|
|
Array.isArray(saved.drafts)) return {};
|
|
const entries = Object.entries(saved.drafts).filter(([identity, record]) =>
|
|
validIdentity(identity) && validRecord(record)
|
|
).slice(0, maxItems);
|
|
return Object.fromEntries(entries);
|
|
} catch (_error) {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function write(drafts) {
|
|
const key = storageKey();
|
|
if (!storage || !key) return false;
|
|
try {
|
|
if (Object.keys(drafts).length) storage.setItem(key, JSON.stringify({ version:1, drafts }));
|
|
else storage.removeItem(key);
|
|
return true;
|
|
} catch (_error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function load(identity) {
|
|
if (!validIdentity(identity)) return '';
|
|
return read()[identity]?.body || '';
|
|
}
|
|
|
|
function save(identity, value) {
|
|
if (!validIdentity(identity) || !storageKey()) return false;
|
|
const body = String(value || '').trim();
|
|
if (body.length > maxLength) return false;
|
|
const drafts = read();
|
|
if (!body) {
|
|
delete drafts[identity];
|
|
return write(drafts);
|
|
}
|
|
if (!drafts[identity] && Object.keys(drafts).length >= maxItems) return false;
|
|
const previous = drafts[identity];
|
|
drafts[identity] = {
|
|
body,
|
|
operation_id: previous?.body === body ? previous.operation_id : String(makeId()).slice(0, 128),
|
|
};
|
|
return write(drafts);
|
|
}
|
|
|
|
function validTarget(target) {
|
|
return target && ['issue', 'pull'].includes(target.kind) && validIdentity(target.identity) &&
|
|
typeof target.repository === 'string' && target.repository.length > 0 && target.repository.length <= 200 &&
|
|
Number.isInteger(target.number) && target.number > 0;
|
|
}
|
|
|
|
async function post(target, value) {
|
|
if (!validTarget(target)) throw new Error('An active Today issue or pull request is required.');
|
|
if (value !== undefined && !save(target.identity, value)) throw new Error('Progress update could not be saved on this device.');
|
|
const record = read()[target.identity];
|
|
if (!record) throw new Error('Write a progress update before posting.');
|
|
if (typeof admit !== 'function') throw new Error('Progress update delivery is unavailable.');
|
|
const admission = await admit({
|
|
kind:target.kind + '-comment', repository:target.repository, number:target.number,
|
|
body:record.body, operationId:record.operation_id,
|
|
});
|
|
const drafts = read();
|
|
if (drafts[target.identity]?.operation_id === record.operation_id) {
|
|
delete drafts[target.identity];
|
|
write(drafts);
|
|
}
|
|
return admission;
|
|
}
|
|
|
|
return { load, save, discard:identity => save(identity, ''), post };
|
|
}
|
|
|
|
function createTodayProgressView({ progress, currentTarget, qs, announce = () => {}, onAdmitted = () => {} }) {
|
|
const sheet = qs('#today-progress-sheet');
|
|
const body = qs('#today-progress-body');
|
|
const status = qs('#today-progress-status');
|
|
const launcher = qs('[data-mobile-today-update]');
|
|
let openedTarget = null;
|
|
|
|
const update = () => { launcher.hidden = !currentTarget(); };
|
|
const checkpoint = () => {
|
|
if (!openedTarget) return false;
|
|
if (progress.save(openedTarget.identity, body.value)) return true;
|
|
status.textContent = 'Update must be 2,000 characters or fewer and device storage must be available.';
|
|
return false;
|
|
};
|
|
const close = () => {
|
|
if (sheet.open) sheet.close();
|
|
openedTarget = null;
|
|
};
|
|
|
|
launcher.addEventListener('click', () => {
|
|
const target = currentTarget();
|
|
if (!target) return;
|
|
openedTarget = target;
|
|
qs('#today-progress-target').textContent = target.label + (target.title ? ' · ' + target.title : '');
|
|
body.value = progress.load(target.identity);
|
|
status.textContent = '';
|
|
sheet.showModal();
|
|
body.focus();
|
|
});
|
|
qs('#cancel-today-progress').addEventListener('click', () => {
|
|
if (checkpoint()) close();
|
|
});
|
|
sheet.addEventListener('cancel', event => {
|
|
event.preventDefault();
|
|
if (checkpoint()) close();
|
|
});
|
|
qs('#save-today-progress').addEventListener('click', () => {
|
|
if (!checkpoint()) return;
|
|
announce('Progress update saved privately to this Today item.');
|
|
close();
|
|
});
|
|
qs('#post-today-progress').addEventListener('click', async () => {
|
|
const target = currentTarget();
|
|
if (!target || target.identity !== openedTarget?.identity) {
|
|
status.textContent = 'The active Today item changed. Close and open its update again.';
|
|
return;
|
|
}
|
|
const button = qs('#post-today-progress');
|
|
button.disabled = true;
|
|
status.textContent = 'Saving for delivery…';
|
|
try {
|
|
const admission = await progress.post(target, body.value);
|
|
onAdmitted(admission);
|
|
announce(admission.background ?
|
|
'Progress update queued for delivery. Today is still on the same item.' :
|
|
'Progress update saved for next launch. Today is still on the same item.');
|
|
close();
|
|
} catch (error) {
|
|
status.textContent = error.message + ' Your update remains on this item; retry.';
|
|
body.focus();
|
|
} finally {
|
|
button.disabled = false;
|
|
}
|
|
});
|
|
update();
|
|
return { update };
|
|
}
|
|
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = createTodayProgress;
|
|
module.exports.createView = createTodayProgressView;
|
|
}
|