feat: schedule mobile notification quiet hours (Closes #1392)
CI / lint (pull_request) Successful in 3m38s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 6m29s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-25 11:17:40 +00:00
1 parent 2acd9431e0
commit cc74b7b8db
9 files changed
+487 -28

No files matched your search

+3
View File
@@ -264,6 +264,9 @@ textarea { resize: vertical; min-height: 120px; }
.app-badge-control { min-height:44px; }
.push-update-control { min-height:44px; }
.offline-work-controls input { width:20px; height:20px; }
.push-quiet-hours-times { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; width:100%; }
.push-quiet-hours-times label { min-width:0; }
.offline-work-controls .push-quiet-hours-times input { box-sizing:border-box; width:100%; min-width:0; height:44px; }
.offline-work-controls button { min-height:44px; }
.offline-today-readiness { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
.offline-today-readiness[hidden] { display:none; }
+6
View File
@@ -224,6 +224,12 @@
<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-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>
<label for="push-quiet-end">Until <input id="push-quiet-end" type="time" value="07:00" /></label>
</div>
<span class="small" id="push-quiet-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>
+45 -1
View File
@@ -5,6 +5,10 @@
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
startDayControl, startDayStatus, startDayHour,
followingControl, followingStatus,
quietControl = globalThis.document?.querySelector('#push-quiet-hours'),
quietStart = globalThis.document?.querySelector('#push-quiet-start'),
quietEnd = globalThis.document?.querySelector('#push-quiet-end'),
quietStatus = globalThis.document?.querySelector('#push-quiet-status'),
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview, onReviewDeadlines,
notification, serviceWorker, fetchJson,
}) {
@@ -302,6 +306,36 @@
}
}
async function changeQuietHours() {
if (!quietControl) return false;
for (const item of [quietControl, quietStart, quietEnd]) if (item) item.disabled = true;
try {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
await fetchJson('api/v1/push-subscription/quiet-hours', {
method:'PUT',
headers:{'Content-Type':'application/json'},
body:JSON.stringify({
enabled:quietControl.checked,
start:quietStart?.value || '22:00',
end:quietEnd?.value || '07:00',
timezone,
}),
});
configuration.quiet_hours_enabled = quietControl.checked;
configuration.quiet_hours_start = quietStart?.value || '22:00';
configuration.quiet_hours_end = quietEnd?.value || '07:00';
quietStatus.textContent = quietControl.checked
? `Routine alerts paused from ${configuration.quiet_hours_start} to ${configuration.quiet_hours_end} local time.`
: 'Routine alert quiet hours are off for this device.';
return true;
} catch (_error) {
quietStatus.textContent = 'Could not save quiet hours. Check your connection and try again.';
return false;
} finally {
for (const item of [quietControl, quietStart, quietEnd]) if (item) item.disabled = false;
}
}
async function enableDeadline() {
deadlineControl.checked = true;
return changeDeadline();
@@ -340,6 +374,9 @@
deadlineControl?.addEventListener('change', changeDeadline);
startDayControl?.addEventListener('change', changeStartDay);
followingControl?.addEventListener('change', changeFollowing);
quietControl?.addEventListener('change', changeQuietHours);
quietStart?.addEventListener('change', changeQuietHours);
quietEnd?.addEventListener('change', changeQuietHours);
deadlineSnoozeReview?.addEventListener('click', reviewSnoozedDeadlines);
configuration = await fetchJson('api/v1/push-subscription');
if (!configuration.available) {
@@ -347,6 +384,7 @@
if (deadlineControl) deadlineControl.disabled = true;
if (startDayControl) startDayControl.disabled = true;
if (followingControl) followingControl.disabled = true;
if (quietControl) quietControl.disabled = true;
status.textContent = 'New update notifications are not available on this server.';
return;
}
@@ -354,6 +392,9 @@
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 (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';
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);
@@ -367,8 +408,11 @@
if (followingStatus) followingStatus.textContent = configuration.following_enabled
? 'Following change alerts enabled for this device.'
: 'Following change 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, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
return {init, change, changeDeadline, changeStartDay, changeFollowing, changeQuietHours, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
});