feat: notify pending Human Gate decisions (Closes #1437)
This commit is contained in:
1 parent
ad15f15e3c
commit
7158e00b97
13 files changed
+494
-17
No files matched your search
@@ -661,6 +661,7 @@
|
||||
|
||||
const humanGatesOnChange = (snapshot, state)=>{
|
||||
queueCounts.gate = snapshot.pending_count;
|
||||
appBadge.reconcile('human-gates', snapshot.pending_count, state.authoritative === true);
|
||||
queueCounts.gateUnavailable = state.available === false;
|
||||
preparationItems.gate = snapshot.items;
|
||||
mobileTaskDock.updateQueues(queueCounts);
|
||||
@@ -8433,6 +8434,8 @@
|
||||
startDayHour:qs('#push-start-day-hour'),
|
||||
followingControl:qs('#push-following'),
|
||||
followingStatus:qs('#push-following-status'),
|
||||
humanGateControl:qs('#push-human-gates'),
|
||||
humanGateStatus:qs('#push-human-gates-status'),
|
||||
deadlineSnooze:qs('#deadline-snooze'),
|
||||
deadlineSnoozeStatus:qs('#deadline-snooze-status'),
|
||||
deadlineSnoozeReview:qs('#review-snoozed-deadlines'),
|
||||
|
||||
@@ -233,6 +233,8 @@
|
||||
<button class="secondary" id="push-test" type="button" hidden>Send test notification</button>
|
||||
<label class="push-update-control" for="push-following"><input id="push-following" type="checkbox" /> Notify me when Following changes</label>
|
||||
<span class="small" id="push-following-status" role="status" aria-live="polite"></span>
|
||||
<label class="push-update-control" for="push-human-gates"><input id="push-human-gates" type="checkbox" /> Notify me when release decisions are waiting</label>
|
||||
<span class="small" id="push-human-gates-status" role="status" aria-live="polite"></span>
|
||||
<label class="push-update-control" for="push-quiet-hours"><input id="push-quiet-hours" type="checkbox" /> Pause routine alerts on a schedule</label>
|
||||
<div class="push-quiet-hours-times">
|
||||
<label for="push-quiet-start">From <input id="push-quiet-start" type="time" value="22:00" /></label>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
}) {
|
||||
const ENABLED_KEY = 'stackchain.app-badge.enabled.v1';
|
||||
let enabled = false;
|
||||
const confirmedCounts = {updates:0, following:0};
|
||||
const confirmedCounts = {updates:0, following:0, 'human-gates':0};
|
||||
let renderedCount = null;
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
}
|
||||
|
||||
async function render() {
|
||||
const confirmedCount = Math.min(9999, confirmedCounts.updates + confirmedCounts.following);
|
||||
const confirmedCount = Math.min(
|
||||
9999, confirmedCounts.updates + confirmedCounts.following + confirmedCounts['human-gates']
|
||||
);
|
||||
if (!enabled || !available() || renderedCount === confirmedCount) return true;
|
||||
try {
|
||||
if (confirmedCount > 0) await navigator.setAppBadge(confirmedCount);
|
||||
@@ -61,10 +63,12 @@
|
||||
if (enabled) {
|
||||
if (confirmedCounts.updates > 0) await syncCount('updates', confirmedCounts.updates);
|
||||
if (confirmedCounts.following > 0) await syncCount('following', confirmedCounts.following);
|
||||
if (confirmedCounts['human-gates'] > 0) await syncCount('human-gates', confirmedCounts['human-gates']);
|
||||
}
|
||||
if (!enabled && available()) {
|
||||
confirmedCounts.updates = 0;
|
||||
confirmedCounts.following = 0;
|
||||
confirmedCounts['human-gates'] = 0;
|
||||
try {
|
||||
await navigator.clearAppBadge();
|
||||
renderedCount = null;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
|
||||
startDayControl, startDayStatus, startDayHour,
|
||||
followingControl, followingStatus,
|
||||
humanGateControl, humanGateStatus,
|
||||
quietControl = globalThis.document?.querySelector('#push-quiet-hours'),
|
||||
quietStart = globalThis.document?.querySelector('#push-quiet-start'),
|
||||
quietEnd = globalThis.document?.querySelector('#push-quiet-end'),
|
||||
@@ -135,14 +136,17 @@
|
||||
if (deadlineControl) deadlineControl.checked = false;
|
||||
if (startDayControl) startDayControl.checked = false;
|
||||
if (followingControl) followingControl.checked = false;
|
||||
if (humanGateControl) humanGateControl.checked = false;
|
||||
configuration.subscribed = false;
|
||||
configuration.deadline_enabled = false;
|
||||
configuration.start_day_enabled = false;
|
||||
configuration.following_enabled = false;
|
||||
configuration.human_gates_enabled = false;
|
||||
pendingIntent = null;
|
||||
status.textContent = 'New update notifications are off for this device.';
|
||||
if (deadlineStatus) deadlineStatus.textContent = 'Deadline reminders are off for this device.';
|
||||
if (followingStatus) followingStatus.textContent = 'Following change alerts are off for this device.';
|
||||
if (humanGateStatus) humanGateStatus.textContent = 'Human Gate decision alerts are off for this device.';
|
||||
}
|
||||
|
||||
async function ensureSubscription() {
|
||||
@@ -306,6 +310,38 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function changeHumanGates() {
|
||||
humanGateControl.disabled = true;
|
||||
try {
|
||||
const registration = await serviceWorker.ready;
|
||||
let subscription = await registration.pushManager.getSubscription();
|
||||
if (humanGateControl.checked) pendingIntent = 'human-gates';
|
||||
if (humanGateControl.checked && !subscription) subscription = await ensureSubscription();
|
||||
if (humanGateControl.checked && !subscription) {
|
||||
humanGateControl.checked = false;
|
||||
humanGateStatus.textContent = status.textContent;
|
||||
return false;
|
||||
}
|
||||
await fetchJson('api/v1/push-subscription/human-gates', {
|
||||
method:'PUT',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({enabled:humanGateControl.checked}),
|
||||
});
|
||||
configuration.human_gates_enabled = humanGateControl.checked;
|
||||
pendingIntent = null;
|
||||
humanGateStatus.textContent = humanGateControl.checked
|
||||
? 'Human Gate decision alerts enabled for this device.'
|
||||
: 'Human Gate decision alerts are off for this device.';
|
||||
return true;
|
||||
} catch (_error) {
|
||||
humanGateControl.checked = !humanGateControl.checked;
|
||||
humanGateStatus.textContent = 'Could not change Human Gate alerts. Check your connection and try again.';
|
||||
return false;
|
||||
} finally {
|
||||
humanGateControl.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function changeQuietHours() {
|
||||
if (!quietControl) return false;
|
||||
for (const item of [quietControl, quietStart, quietEnd]) if (item) item.disabled = true;
|
||||
@@ -342,7 +378,7 @@
|
||||
}
|
||||
|
||||
async function recoverPermission(intent = null) {
|
||||
if (!pendingIntent && ['updates', 'deadline', 'start-day', 'following'].includes(intent)) pendingIntent = intent;
|
||||
if (!pendingIntent && ['updates', 'deadline', 'start-day', 'following', 'human-gates'].includes(intent)) pendingIntent = intent;
|
||||
if (!pendingIntent || notification.permission !== 'granted') return false;
|
||||
if (recoveryPromise) return recoveryPromise;
|
||||
recoveryPromise = (async () => {
|
||||
@@ -358,6 +394,10 @@
|
||||
followingControl.checked = true;
|
||||
return changeFollowing();
|
||||
}
|
||||
if (pendingIntent === 'human-gates') {
|
||||
humanGateControl.checked = true;
|
||||
return changeHumanGates();
|
||||
}
|
||||
return Boolean(await enable());
|
||||
})();
|
||||
try {
|
||||
@@ -374,6 +414,7 @@
|
||||
deadlineControl?.addEventListener('change', changeDeadline);
|
||||
startDayControl?.addEventListener('change', changeStartDay);
|
||||
followingControl?.addEventListener('change', changeFollowing);
|
||||
humanGateControl?.addEventListener('change', changeHumanGates);
|
||||
quietControl?.addEventListener('change', changeQuietHours);
|
||||
quietStart?.addEventListener('change', changeQuietHours);
|
||||
quietEnd?.addEventListener('change', changeQuietHours);
|
||||
@@ -384,6 +425,7 @@
|
||||
if (deadlineControl) deadlineControl.disabled = true;
|
||||
if (startDayControl) startDayControl.disabled = true;
|
||||
if (followingControl) followingControl.disabled = true;
|
||||
if (humanGateControl) humanGateControl.disabled = true;
|
||||
if (quietControl) quietControl.disabled = true;
|
||||
status.textContent = 'New update notifications are not available on this server.';
|
||||
return;
|
||||
@@ -392,6 +434,7 @@
|
||||
if (deadlineControl) deadlineControl.checked = Boolean(configuration.deadline_enabled);
|
||||
if (startDayControl) startDayControl.checked = Boolean(configuration.start_day_enabled);
|
||||
if (followingControl) followingControl.checked = Boolean(configuration.following_enabled);
|
||||
if (humanGateControl) humanGateControl.checked = Boolean(configuration.human_gates_enabled);
|
||||
if (quietControl) quietControl.checked = Boolean(configuration.quiet_hours_enabled);
|
||||
if (quietStart) quietStart.value = configuration.quiet_hours_start || '22:00';
|
||||
if (quietEnd) quietEnd.value = configuration.quiet_hours_end || '07:00';
|
||||
@@ -408,11 +451,14 @@
|
||||
if (followingStatus) followingStatus.textContent = configuration.following_enabled
|
||||
? 'Following change alerts enabled for this device.'
|
||||
: 'Following change alerts are off for this device.';
|
||||
if (humanGateStatus) humanGateStatus.textContent = configuration.human_gates_enabled
|
||||
? 'Human Gate decision alerts enabled for this device.'
|
||||
: 'Human Gate decision alerts are off for this device.';
|
||||
if (quietStatus) quietStatus.textContent = configuration.quiet_hours_enabled
|
||||
? `Routine alerts paused from ${quietStart.value} to ${quietEnd.value} local time.`
|
||||
: 'Routine alert quiet hours are off for this device.';
|
||||
renderDeadlineSnooze();
|
||||
}
|
||||
|
||||
return {init, change, changeDeadline, changeStartDay, changeFollowing, changeQuietHours, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
|
||||
return {init, change, changeDeadline, changeStartDay, changeFollowing, changeHumanGates, changeQuietHours, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
|
||||
});
|
||||
@@ -43,7 +43,7 @@ function createAppBadgePreference() {
|
||||
async getCounts() {
|
||||
const database = await open();
|
||||
const counts = {};
|
||||
for (const channel of ['updates', 'following']) {
|
||||
for (const channel of ['updates', 'following', 'human-gates']) {
|
||||
counts[channel] = await new Promise((resolve, reject) => {
|
||||
const request = database.transaction(storeName, 'readonly').objectStore(storeName).get('count:' + channel);
|
||||
request.onsuccess = () => resolve(Number.isSafeInteger(request.result) ? request.result : 0);
|
||||
@@ -67,6 +67,7 @@ function createAppBadgePreference() {
|
||||
async clearCounts() {
|
||||
await this.setCount('updates', 0);
|
||||
await this.setCount('following', 0);
|
||||
await this.setCount('human-gates', 0);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -74,7 +75,7 @@ const appBadgePreference = self.__STACKCHAIN_APP_BADGE_PREFERENCE || createAppBa
|
||||
let renderedBackgroundBadgeCount = null;
|
||||
|
||||
async function reconcileBackgroundAppBadge(channel, count) {
|
||||
if (!['updates', 'following'].includes(channel)
|
||||
if (!['updates', 'following', 'human-gates'].includes(channel)
|
||||
|| !Number.isSafeInteger(count) || count < 0 || count > 9999
|
||||
|| typeof self.registration.setAppBadge !== 'function'
|
||||
|| typeof self.registration.clearAppBadge !== 'function') return false;
|
||||
@@ -84,7 +85,7 @@ async function reconcileBackgroundAppBadge(channel, count) {
|
||||
try {
|
||||
await appBadgePreference.setCount(channel, count);
|
||||
const counts = await appBadgePreference.getCounts();
|
||||
const total = Math.min(9999, counts.updates + counts.following);
|
||||
const total = Math.min(9999, counts.updates + counts.following + counts['human-gates']);
|
||||
if (renderedBackgroundBadgeCount === total) return false;
|
||||
if (total > 0) await self.registration.setAppBadge(total);
|
||||
else await self.registration.clearAppBadge();
|
||||
@@ -623,7 +624,7 @@ self.addEventListener('message', event => {
|
||||
if (!String(event.source?.url || '').startsWith(self.location.origin + BASE)) return;
|
||||
const channel = event.data.channel;
|
||||
const count = event.data.count;
|
||||
if (!['updates', 'following'].includes(channel)
|
||||
if (!['updates', 'following', 'human-gates'].includes(channel)
|
||||
|| !Number.isSafeInteger(count) || count < 0 || count > 9999) return;
|
||||
try {
|
||||
if (await appBadgePreference.get()) {
|
||||
@@ -708,6 +709,7 @@ self.addEventListener('push', event => {
|
||||
const unreadCount = typeof payload.unread_count === 'number' ? payload.unread_count : NaN;
|
||||
const deadlineCount = Number(payload.deadline_count);
|
||||
const followingCount = Number(payload.following_count);
|
||||
const humanGateCount = Number(payload.human_gate_count);
|
||||
const planDate = String(payload.plan_date || '');
|
||||
if (
|
||||
route === '#/my-work/start-day'
|
||||
@@ -744,6 +746,25 @@ self.addEventListener('push', event => {
|
||||
));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
route === '#/my-work/human-gates'
|
||||
&& tag === 'stackchain-human-gates-' + humanGateCount
|
||||
&& Number.isSafeInteger(humanGateCount)
|
||||
&& humanGateCount > 0
|
||||
&& humanGateCount <= 50
|
||||
) {
|
||||
event.waitUntil(Promise.all([
|
||||
reconcileBackgroundAppBadge('human-gates', humanGateCount),
|
||||
self.registration.showNotification(
|
||||
humanGateCount + ' release decision' + (humanGateCount === 1 ? ' is' : 's are') + ' waiting', {
|
||||
body: 'Open Human Gates to review ' + (humanGateCount === 1 ? 'it.' : 'them.'),
|
||||
tag,
|
||||
data: {route},
|
||||
}
|
||||
),
|
||||
]));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
route === '#/my-work/following'
|
||||
&& /^stackchain-following-[0-9a-f]{16}$/.test(tag)
|
||||
|
||||
Reference in new issue
Block a user