feat: show timed Today breaks on lock screen (Closes #1050)
CI / lint (pull_request) Successful in 2m53s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m15s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-17 23:57:29 +00:00
1 parent af86034339
commit ed15392097
4 files changed
+112 -12

No files matched your search

+22 -8
View File
@@ -421,7 +421,7 @@ self.addEventListener('sync', event => {
if (event.tag === 'stackchain-issue-outbox-v1') event.waitUntil(flushAndNotify());
});
async function updateTodayLockScreen(active, running, rawActionToken = '') {
async function updateTodayLockScreen(active, running, rawActionToken = '', rawBreakDeadline = 0) {
const tag = 'stackchain-today-session';
if (!active) {
const notifications = await self.registration.getNotifications({ tag });
@@ -429,12 +429,24 @@ async function updateTodayLockScreen(active, running, rawActionToken = '') {
return;
}
const actionToken = /^[A-Za-z0-9_-]{16,128}$/.test(rawActionToken) ? rawActionToken : '';
await self.registration.showNotification(running ? 'Today session running' : 'Today session paused', {
body: running ? 'Your active Today timer is running.' : 'Your active Today timer is paused.',
const now = Date.now();
const breakDeadline = Number(rawBreakDeadline);
const onBreak = !running && actionToken && Number.isSafeInteger(breakDeadline) &&
breakDeadline > now && breakDeadline <= now + 120 * 60 * 1000;
const title = onBreak ? 'On a Today break' :
running ? 'Today session running' : 'Today session paused';
const body = onBreak ? 'Return at ' + new Date(breakDeadline).toLocaleTimeString([], {
hour:'numeric', minute:'2-digit',
}) : running ? 'Your active Today timer is running.' : 'Your active Today timer is paused.';
await self.registration.showNotification(title, {
body,
tag,
renotify:false,
silent:true,
actions: [
actions: onBreak ? [
{ action:'resume-today', title:'Resume now' },
{ action:'open-today', title:'Open Today' },
] : [
{ action:running ? 'pause-today' : 'resume-today', title:running ? 'Pause' : 'Resume' },
...(actionToken ? [{ action:'finish-today', title:'Finish current' }] : []),
],
@@ -464,7 +476,8 @@ self.addEventListener('message', event => {
event.waitUntil(updateTodayLockScreen(
event.data.active === true,
event.data.running === true,
String(event.data.actionToken || '')
String(event.data.actionToken || ''),
Number(event.data.breakDeadlineAt || 0)
));
}
});
@@ -561,19 +574,20 @@ async function openCanonicalIssueUrl(rawUrl) {
async function applyTodayTimerAction(action, actionToken = '') {
if (!['pause', 'resume', 'complete'].includes(action)) return;
if (action === 'complete' && !/^[A-Za-z0-9_-]{16,128}$/.test(actionToken)) return;
if (actionToken && !/^[A-Za-z0-9_-]{16,128}$/.test(actionToken)) return;
if (action === 'complete' && !actionToken) return;
const route = '#/my-work/today';
const windows = await self.clients.matchAll({ type:'window', includeUncontrolled:true });
const client = windows.find(candidate => candidate.url.startsWith(self.location.origin + BASE));
if (client) {
client.postMessage?.({
type:'stackchain-today-timer-action', action,
...(action === 'complete' ? { actionToken } : {}),
...(actionToken ? { actionToken } : {}),
});
return client.focus?.();
}
const query = '?today_timer_action=' + action +
(action === 'complete' ? '&today_action_token=' + encodeURIComponent(actionToken) : '');
(actionToken ? '&today_action_token=' + encodeURIComponent(actionToken) : '');
const target = new URL(BASE + query + route, self.location.origin).href;
return self.clients.openWindow(target);
}
+11 -3
View File
@@ -21,6 +21,8 @@ function createTodayLockScreen({
onAction = () => {},
}) {
let activeIdentity = '';
let activeActionIdentity = '';
let activeBreakDeadline = 0;
const preferenceKey = () => {
const login = String(getLogin?.() || '').trim().toLowerCase();
return login ? 'stackchain.today-lock-screen.v1.' + encodeURIComponent(login) : '';
@@ -82,13 +84,14 @@ function createTodayLockScreen({
};
const consumeAction = async (action, token = '') => {
if (!enabled() || !['pause', 'resume', 'complete'].includes(action)) return false;
if (action !== 'complete') {
const requiresToken = action === 'complete' || (action === 'resume' && activeBreakDeadline > 0);
if (!requiresToken) {
onAction(action, null);
return true;
}
const pending = readAction();
if (!pending || !token || !activeIdentity || pending.token !== token ||
await fingerprint(token, activeIdentity) !== pending.fingerprint) return false;
await fingerprint(token, activeActionIdentity) !== pending.fingerprint) return false;
if (!clearAction()) return false;
onAction(action, activeIdentity);
return true;
@@ -128,12 +131,17 @@ function createTodayLockScreen({
if (!enabled() || NotificationRef?.permission !== 'granted') return false;
const visible = Boolean(active && snapshot?.identity);
activeIdentity = visible ? snapshot.identity : '';
const pending = visible ? await actionFor(activeIdentity) : null;
const rawBreakDeadline = Number(snapshot?.break_deadline_at);
activeBreakDeadline = visible && Number.isSafeInteger(rawBreakDeadline) && rawBreakDeadline > 0 ?
rawBreakDeadline : 0;
activeActionIdentity = activeIdentity + (activeBreakDeadline ? '\0break:' + activeBreakDeadline : '');
const pending = visible ? await actionFor(activeActionIdentity) : null;
if (!visible) clearAction();
await post({
type:'stackchain-today-lock-screen',
active:visible,
running:visible && Boolean(snapshot.running),
...(activeBreakDeadline ? { breakDeadlineAt:activeBreakDeadline } : {}),
...(pending?.token ? { actionToken:pending.token } : {}),
});
if (visible && !pending) setStatus('Finish current is unavailable because its one-time action could not be saved.');