feat: launch planned days with morning reminders (Closes #1166)
This commit is contained in:
1 parent
99b60e5f5f
commit
d86e84253a
21 files changed
+614
-34
No files matched your search
+30
-3
@@ -476,10 +476,15 @@
|
||||
refreshMyWorkView();
|
||||
warmTodayOffline();
|
||||
},
|
||||
onRemotePlan: plan => {
|
||||
onRemotePlan: async plan => {
|
||||
if (!planningOwnerLogin) return;
|
||||
latestTodayPlan = plan;
|
||||
promoteTomorrowIfDue(plan);
|
||||
const startDayLaunch = window.location.hash === '#/my-work/start-day';
|
||||
await promoteTomorrowIfDue(plan);
|
||||
if (startDayLaunch) {
|
||||
window.history.replaceState({}, '', '#/my-work/today');
|
||||
openMobileStartDay();
|
||||
}
|
||||
todayWork.replacePlanning({
|
||||
capacity_minutes: plan.capacity_minutes ?? null,
|
||||
estimates: plan.estimates || {},
|
||||
@@ -8089,7 +8094,10 @@
|
||||
if (workSession.active()) workSession.reconcile();
|
||||
});
|
||||
let pushController = null;
|
||||
for (const selector of [qs('#push-deadline-hour'), qs('#device-setup-deadline-hour')]) {
|
||||
for (const selector of [
|
||||
qs('#push-deadline-hour'), qs('#device-setup-deadline-hour'),
|
||||
qs('#push-start-day-hour'), qs('#device-setup-start-day-hour'),
|
||||
]) {
|
||||
for (let hour = 0; hour < 24; hour += 1) {
|
||||
const option = document.createElement('option');
|
||||
option.value = String(hour);
|
||||
@@ -8112,6 +8120,13 @@
|
||||
qs('#device-setup-deadline-days').addEventListener('change', event => {
|
||||
qs('#push-deadline-days').value = event.target.value;
|
||||
});
|
||||
qs('#push-start-day-hour').addEventListener('change', event => {
|
||||
qs('#device-setup-start-day-hour').value = event.target.value;
|
||||
if (qs('#push-start-day').checked) pushController?.changeStartDay();
|
||||
});
|
||||
qs('#device-setup-start-day-hour').addEventListener('change', event => {
|
||||
qs('#push-start-day-hour').value = event.target.value;
|
||||
});
|
||||
let pushControllerReady = Promise.resolve(null);
|
||||
if ('serviceWorker' in navigator) {
|
||||
pushControllerReady = navigator.serviceWorker.register('service-worker.js').then(async () => {
|
||||
@@ -8124,6 +8139,9 @@
|
||||
deadlineStatus:qs('#push-deadline-status'),
|
||||
deadlineHour:qs('#push-deadline-hour'),
|
||||
deadlineDays:qs('#push-deadline-days'),
|
||||
startDayControl:qs('#push-start-day'),
|
||||
startDayStatus:qs('#push-start-day-status'),
|
||||
startDayHour:qs('#push-start-day-hour'),
|
||||
deadlineSnooze:qs('#deadline-snooze'),
|
||||
deadlineSnoozeStatus:qs('#deadline-snooze-status'),
|
||||
deadlineSnoozeReview:qs('#review-snoozed-deadlines'),
|
||||
@@ -8142,6 +8160,7 @@
|
||||
pushController = controller;
|
||||
qs('#device-setup-deadline-hour').value = qs('#push-deadline-hour').value;
|
||||
qs('#device-setup-deadline-days').value = qs('#push-deadline-days').value;
|
||||
qs('#device-setup-start-day-hour').value = qs('#push-start-day-hour').value;
|
||||
return controller;
|
||||
}).catch(error => {
|
||||
qs('#push-updates').disabled = true;
|
||||
@@ -8152,6 +8171,14 @@
|
||||
qs('#push-updates').disabled = true;
|
||||
qs('#push-update-status').textContent = 'This browser does not support update notifications.';
|
||||
}
|
||||
qs('#device-setup-start-day').addEventListener('click', async () => {
|
||||
const controller = await pushControllerReady;
|
||||
if (!controller) return;
|
||||
qs('#push-start-day-hour').value = qs('#device-setup-start-day-hour').value;
|
||||
qs('#push-start-day').checked = true;
|
||||
await controller.changeStartDay();
|
||||
qs('#device-setup-start-day-status').textContent = qs('#push-start-day-status').textContent;
|
||||
});
|
||||
let deferredInstallPrompt = null;
|
||||
window.addEventListener('beforeinstallprompt', event => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -143,6 +143,14 @@
|
||||
<button class="device-setup-action" id="device-setup-deadline" type="button">Enable</button>
|
||||
</div>
|
||||
</li>
|
||||
<li class="device-setup-step">
|
||||
<div><strong>Start my planned day</strong><p class="small" id="device-setup-start-day-status" role="status">Get one private reminder when Tomorrow is ready.</p></div>
|
||||
<div class="device-setup-deadline-controls">
|
||||
<label for="device-setup-start-day-hour">Local hour</label>
|
||||
<select id="device-setup-start-day-hour" aria-label="Start-day reminder local hour"></select>
|
||||
<button class="device-setup-action" id="device-setup-start-day" type="button">Enable</button>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<section class="device-storage" aria-labelledby="device-storage-heading">
|
||||
<div>
|
||||
@@ -209,6 +217,9 @@
|
||||
<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>
|
||||
<span class="small" id="push-deadline-status" role="status" aria-live="polite"></span>
|
||||
<label class="push-update-control" for="push-start-day"><input id="push-start-day" type="checkbox" /> Remind me when my planned day is ready</label>
|
||||
<label for="push-start-day-hour">Start-day hour <select id="push-start-day-hour" aria-label="Start-day reminder local hour"></select></label>
|
||||
<span class="small" id="push-start-day-status" role="status" aria-live="polite"></span>
|
||||
<label class="push-update-control" for="today-lock-screen"><input id="today-lock-screen" type="checkbox" /> Show active Today controls on my lock screen</label>
|
||||
<span class="small" id="today-lock-screen-status" role="status" aria-live="polite"></span>
|
||||
<button id="clear-offline-work" type="button">Clear offline work data</button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
else root.createPushNotifications = factory;
|
||||
})(typeof self !== 'undefined' ? self : this, function createPushNotifications({
|
||||
control, status, testControl, deadlineControl, deadlineStatus, deadlineHour, deadlineDays,
|
||||
startDayControl, startDayStatus, startDayHour,
|
||||
deadlineSnooze, deadlineSnoozeStatus, deadlineSnoozeReview, onReviewDeadlines,
|
||||
notification, serviceWorker, fetchJson,
|
||||
}) {
|
||||
@@ -127,8 +128,10 @@
|
||||
control.checked = false;
|
||||
if (testControl) testControl.hidden = true;
|
||||
if (deadlineControl) deadlineControl.checked = false;
|
||||
if (startDayControl) startDayControl.checked = false;
|
||||
configuration.subscribed = false;
|
||||
configuration.deadline_enabled = false;
|
||||
configuration.start_day_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.';
|
||||
@@ -225,13 +228,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function changeStartDay() {
|
||||
startDayControl.disabled = true;
|
||||
if (startDayHour) startDayHour.disabled = true;
|
||||
try {
|
||||
const registration = await serviceWorker.ready;
|
||||
let subscription = await registration.pushManager.getSubscription();
|
||||
if (startDayControl.checked) pendingIntent = 'start-day';
|
||||
if (startDayControl.checked && !subscription) subscription = await ensureSubscription();
|
||||
if (startDayControl.checked && !subscription) {
|
||||
startDayControl.checked = false;
|
||||
startDayStatus.textContent = status.textContent;
|
||||
return false;
|
||||
}
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
|
||||
const reminderHour = Number(startDayHour?.value ?? configuration?.start_day_reminder_hour ?? 9);
|
||||
await fetchJson('api/v1/push-subscription/start-day', {
|
||||
method:'PUT',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body:JSON.stringify({enabled:startDayControl.checked, timezone, reminder_hour:reminderHour}),
|
||||
});
|
||||
configuration.start_day_enabled = startDayControl.checked;
|
||||
configuration.start_day_timezone = timezone;
|
||||
configuration.start_day_reminder_hour = reminderHour;
|
||||
pendingIntent = null;
|
||||
startDayStatus.textContent = startDayControl.checked
|
||||
? `Start-day reminder enabled for ${formattedHour(reminderHour)} local time.`
|
||||
: 'Start-day reminders are off for this device.';
|
||||
return true;
|
||||
} catch (_error) {
|
||||
startDayControl.checked = !startDayControl.checked;
|
||||
startDayStatus.textContent = 'Could not change start-day reminders. Check your connection and try again.';
|
||||
return false;
|
||||
} finally {
|
||||
startDayControl.disabled = false;
|
||||
if (startDayHour) startDayHour.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function enableDeadline() {
|
||||
deadlineControl.checked = true;
|
||||
return changeDeadline();
|
||||
}
|
||||
|
||||
async function recoverPermission(intent = null) {
|
||||
if (!pendingIntent && (intent === 'updates' || intent === 'deadline')) pendingIntent = intent;
|
||||
if (!pendingIntent && ['updates', 'deadline', 'start-day'].includes(intent)) pendingIntent = intent;
|
||||
if (!pendingIntent || notification.permission !== 'granted') return false;
|
||||
if (recoveryPromise) return recoveryPromise;
|
||||
recoveryPromise = (async () => {
|
||||
@@ -239,6 +280,10 @@
|
||||
deadlineControl.checked = true;
|
||||
return changeDeadline();
|
||||
}
|
||||
if (pendingIntent === 'start-day') {
|
||||
startDayControl.checked = true;
|
||||
return changeStartDay();
|
||||
}
|
||||
return Boolean(await enable());
|
||||
})();
|
||||
try {
|
||||
@@ -253,24 +298,31 @@
|
||||
control.addEventListener('change', change);
|
||||
testControl?.addEventListener('click', testDelivery);
|
||||
deadlineControl?.addEventListener('change', changeDeadline);
|
||||
startDayControl?.addEventListener('change', changeStartDay);
|
||||
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;
|
||||
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 (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);
|
||||
renderDeliveryHealth();
|
||||
if (deadlineStatus) deadlineStatus.textContent = configuration.deadline_enabled
|
||||
? enabledDeadlineText(configuration.reminder_hour, configuration.reminder_days)
|
||||
: 'Deadline reminders are off for this device.';
|
||||
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.';
|
||||
renderDeadlineSnooze();
|
||||
}
|
||||
|
||||
return {init, change, changeDeadline, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
|
||||
return {init, change, changeDeadline, changeStartDay, enableDeadline, deadlineReadiness, notificationReadiness, recoverPermission};
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
const BASE = new URL('./', self.location.href).pathname;
|
||||
importScripts(BASE + 'static/private-data-registry.js');
|
||||
importScripts(BASE + 'static/background-issue-sync.js');
|
||||
const CACHE = 'stackchain-dashboard-shell-v124';
|
||||
const CACHE = 'stackchain-dashboard-shell-v125';
|
||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||
@@ -500,6 +500,20 @@ self.addEventListener('push', event => {
|
||||
const notificationId = Number(payload.notification_id);
|
||||
const updateCount = Number(payload.update_count);
|
||||
const deadlineCount = Number(payload.deadline_count);
|
||||
const planDate = String(payload.plan_date || '');
|
||||
if (
|
||||
route === '#/my-work/start-day'
|
||||
&& /^\d{4}-\d{2}-\d{2}$/.test(planDate)
|
||||
&& tag === 'stackchain-start-day-' + planDate
|
||||
) {
|
||||
event.waitUntil(self.registration.showNotification('Your planned day is ready', {
|
||||
body: 'Open Stackchain to prepare Today.',
|
||||
tag,
|
||||
actions: [{ action: 'prepare-today', title: 'Prepare Today' }],
|
||||
data: {route},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
route === '#/my-work/agenda'
|
||||
&& protectRoute === '#/my-work/agenda/protect-today'
|
||||
|
||||
Reference in new issue
Block a user