feat: notify devices when Following changes (Closes #1313)
CI / lint (pull_request) Successful in 3m45s
CI / build-release (pull_request) Successful in 7s
CI / browser-journey (pull_request) Successful in 6m8s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-23 17:16:28 +00:00
1 parent 187b0decae
commit 0efd44fd90
13 files changed
+491 -7

No files matched your search

+2
View File
@@ -8310,6 +8310,8 @@
startDayControl:qs('#push-start-day'),
startDayStatus:qs('#push-start-day-status'),
startDayHour:qs('#push-start-day-hour'),
followingControl:qs('#push-following'),
followingStatus:qs('#push-following-status'),
deadlineSnooze:qs('#deadline-snooze'),
deadlineSnoozeStatus:qs('#deadline-snooze-status'),
deadlineSnoozeReview:qs('#review-snoozed-deadlines'),
+2
View File
@@ -221,6 +221,8 @@
<label class="push-update-control" for="push-updates"><input id="push-updates" type="checkbox" /> Notify me about new updates</label>
<span class="small" id="push-update-status" role="status" aria-live="polite"></span>
<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-deadlines"><input id="push-deadlines" type="checkbox" /> Notify me about deadlines</label>
<label for="push-deadline-hour">Reminder hour <select id="push-deadline-hour" aria-label="Deadline reminder local hour"></select></label>
<label for="push-deadline-days">Warn me <select id="push-deadline-days" aria-label="Deadline reminder horizon"><option value="0">Due today</option><option value="2" selected>Next 2 days</option><option value="7">Next 7 days</option></select></label>
+48 -2
View File
@@ -4,6 +4,7 @@
})(typeof self !== 'undefined' ? self : this, function createPushNotifications({
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
startDayControl, startDayStatus, startDayHour,
followingControl, followingStatus,
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview, onReviewDeadlines,
notification, serviceWorker, fetchJson,
}) {
@@ -129,12 +130,15 @@
if (testControl) testControl.hidden = true;
if (deadlineControl) deadlineControl.checked = false;
if (startDayControl) startDayControl.checked = false;
if (followingControl) followingControl.checked = false;
configuration.subscribed = false;
configuration.deadline_enabled = false;
configuration.start_day_enabled = false;
configuration.following_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.';
}
async function ensureSubscription() {
@@ -266,13 +270,45 @@
}
}
async function changeFollowing() {
followingControl.disabled = true;
try {
const registration = await serviceWorker.ready;
let subscription = await registration.pushManager.getSubscription();
if (followingControl.checked) pendingIntent = 'following';
if (followingControl.checked && !subscription) subscription = await ensureSubscription();
if (followingControl.checked && !subscription) {
followingControl.checked = false;
followingStatus.textContent = status.textContent;
return false;
}
await fetchJson('api/v1/push-subscription/following', {
method:'PUT',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({enabled:followingControl.checked}),
});
configuration.following_enabled = followingControl.checked;
pendingIntent = null;
followingStatus.textContent = followingControl.checked
? 'Following change alerts enabled for this device.'
: 'Following change alerts are off for this device.';
return true;
} catch (_error) {
followingControl.checked = !followingControl.checked;
followingStatus.textContent = 'Could not change Following alerts. Check your connection and try again.';
return false;
} finally {
followingControl.disabled = false;
}
}
async function enableDeadline() {
deadlineControl.checked = true;
return changeDeadline();
}
async function recoverPermission(intent = null) {
if (!pendingIntent && ['updates', 'deadline', 'start-day'].includes(intent)) pendingIntent = intent;
if (!pendingIntent && ['updates', 'deadline', 'start-day', 'following'].includes(intent)) pendingIntent = intent;
if (!pendingIntent || notification.permission !== 'granted') return false;
if (recoveryPromise) return recoveryPromise;
recoveryPromise = (async () => {
@@ -284,6 +320,10 @@
startDayControl.checked = true;
return changeStartDay();
}
if (pendingIntent === 'following') {
followingControl.checked = true;
return changeFollowing();
}
return Boolean(await enable());
})();
try {
@@ -299,18 +339,21 @@
testControl?.addEventListener('click', testDelivery);
deadlineControl?.addEventListener('change', changeDeadline);
startDayControl?.addEventListener('change', changeStartDay);
followingControl?.addEventListener('change', changeFollowing);
deadlineSnoozeReview?.addEventListener('click', reviewSnoozedDeadlines);
configuration = await fetchJson('api/v1/push-subscription');
if (!configuration.available) {
control.disabled = true;
if (deadlineControl) deadlineControl.disabled = true;
if (startDayControl) startDayControl.disabled = true;
if (followingControl) followingControl.disabled = true;
status.textContent = 'New update notifications are not available on this server.';
return;
}
control.checked = Boolean(configuration.subscribed);
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 (deadlineHour) deadlineHour.value = String(configuration.reminder_hour ?? 9);
if (deadlineDays) deadlineDays.value = String(configuration.reminder_days ?? 2);
if (startDayHour) startDayHour.value = String(configuration.start_day_reminder_hour ?? 9);
@@ -321,8 +364,11 @@
if (startDayStatus) startDayStatus.textContent = configuration.start_day_enabled
? `Start-day reminder enabled for ${formattedHour(configuration.start_day_reminder_hour)} local time.`
: 'Start-day reminders are off for this device.';
if (followingStatus) followingStatus.textContent = configuration.following_enabled
? 'Following change alerts enabled for this device.'
: 'Following change alerts are off for this device.';
renderDeadlineSnooze();
}
return {init, change, changeDeadline, changeStartDay, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
return {init, change, changeDeadline, changeStartDay, changeFollowing, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
});
+18
View File
@@ -649,6 +649,7 @@ self.addEventListener('push', event => {
const updateCount = Number(payload.update_count);
const unreadCount = typeof payload.unread_count === 'number' ? payload.unread_count : NaN;
const deadlineCount = Number(payload.deadline_count);
const followingCount = Number(payload.following_count);
const planDate = String(payload.plan_date || '');
if (
route === '#/my-work/start-day'
@@ -685,6 +686,23 @@ self.addEventListener('push', event => {
));
return;
}
if (
route === '#/my-work/following'
&& /^stackchain-following-[0-9a-f]{16}$/.test(tag)
&& Number.isSafeInteger(followingCount)
&& followingCount > 0
&& followingCount <= 50
) {
event.waitUntil(self.registration.showNotification(
followingCount + ' watched item' + (followingCount === 1 ? '' : 's') + ' changed',
{
body: 'Open Following to review the latest activity.',
tag,
data: {route},
}
));
return;
}
if (
route === '#/my-work/updates'
&& tag === 'stackchain-update-digest'