feat: show unread Updates on app icon (Closes #1278)
This commit is contained in:
1 parent
71631c6731
commit
be9de6596f
9 files changed
+306
-2
No files matched your search
@@ -241,6 +241,8 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.offline-status[hidden] { display:none; }
|
||||
.offline-work-controls { display:flex; gap:10px; align-items:center; flex-wrap:wrap; width:100%; padding-top:2px; }
|
||||
.offline-work-controls label { display:flex; gap:8px; align-items:center; min-height:44px; }
|
||||
.app-badge-setting { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
||||
.app-badge-control { min-height:44px; }
|
||||
.push-update-control { min-height:44px; }
|
||||
.offline-work-controls input { width:20px; height:20px; }
|
||||
.offline-work-controls button { min-height:44px; }
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
const qs = (s, el=document) => el.querySelector(s);
|
||||
const announceWork = message => qs('#my-work-action-status').textContent = message;
|
||||
const fmt = (d) => new Date(d).toLocaleString();
|
||||
const mobileAppBadge = createMobileAppBadge({
|
||||
control:qs('#app-badge-control'), status:qs('#app-badge-status'),
|
||||
container:qs('#app-badge-setting'), navigator, storage:localStorage,
|
||||
});
|
||||
mobileAppBadge.start();
|
||||
const cardPlanning = createCardPlanning(document);
|
||||
const mobileComposerViewport = createMobileComposerViewport({
|
||||
viewport: window.visualViewport,
|
||||
@@ -1487,6 +1492,7 @@
|
||||
},
|
||||
onPagination: pagination => {
|
||||
notificationPagination = pagination;
|
||||
mobileAppBadge.reconcile(pagination.total, {authoritative:true});
|
||||
const loaded = Math.min(pagination.total, pagination.page * 50);
|
||||
qs('#notification-page-status').textContent = pagination.total ?
|
||||
loaded + ' of ' + pagination.total + ' unread updates loaded.' : '';
|
||||
@@ -8292,6 +8298,8 @@
|
||||
protectStorage:() => deviceStorage.requestPersistence(),
|
||||
notificationReadiness:() => pushController?.notificationReadiness()
|
||||
|| {state:'unavailable', detail:'Update notifications are unavailable.'},
|
||||
appBadgeReadiness:() => mobileAppBadge.readiness(),
|
||||
enableAppBadge:() => mobileAppBadge.enable(),
|
||||
enablePush:async () => {
|
||||
const controller = await pushControllerReady;
|
||||
if (!controller) return;
|
||||
|
||||
@@ -133,6 +133,10 @@
|
||||
<div><strong>Notify me about new updates</strong><p class="small" id="device-setup-push-status" role="status"></p></div>
|
||||
<button class="device-setup-action" id="device-setup-push" type="button">Enable</button>
|
||||
</li>
|
||||
<li class="device-setup-step" id="device-setup-app-badge-step">
|
||||
<div><strong>Show unread Updates on the app icon</strong><p class="small" id="device-setup-app-badge-status" role="status"></p></div>
|
||||
<button class="device-setup-action" id="device-setup-app-badge" type="button">Enable</button>
|
||||
</li>
|
||||
<li class="device-setup-step">
|
||||
<div><strong>Remind me about deadlines</strong><p class="small" id="device-setup-deadline-status" role="status"></p></div>
|
||||
<div class="device-setup-deadline-controls">
|
||||
@@ -210,6 +214,10 @@
|
||||
<div class="offline-work-controls">
|
||||
<label for="keep-work-offline"><input id="keep-work-offline" type="checkbox" /> Keep My Work available offline</label>
|
||||
<label for="delivery-receipts"><input id="delivery-receipts" type="checkbox" /> Notify me when queued work finishes</label>
|
||||
<span class="app-badge-setting" id="app-badge-setting">
|
||||
<label class="app-badge-control" for="app-badge-control"><input id="app-badge-control" type="checkbox" /> Show unread Updates on the app icon</label>
|
||||
<span class="small" id="app-badge-status" role="status" aria-live="polite"></span>
|
||||
</span>
|
||||
<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>
|
||||
@@ -2173,6 +2181,7 @@
|
||||
<script src="static/mobile-launch.js"></script>
|
||||
<script src="static/mobile-insights.js"></script>
|
||||
<script src="static/mobile-app-shortcuts.js"></script>
|
||||
<script src="static/mobile-app-badge.js"></script>
|
||||
<script src="static/install-app.js"></script>
|
||||
<script src="static/private-device-data.js"></script>
|
||||
<script src="static/device-storage.js"></script>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
(function(root, factory) {
|
||||
if (typeof module === 'object' && module.exports) module.exports = factory;
|
||||
else root.createMobileAppBadge = factory;
|
||||
})(typeof self !== 'undefined' ? self : this, function createMobileAppBadge({
|
||||
control,
|
||||
status,
|
||||
container,
|
||||
navigator,
|
||||
storage,
|
||||
}) {
|
||||
const ENABLED_KEY = 'stackchain.app-badge.enabled.v1';
|
||||
let enabled = false;
|
||||
let confirmedCount = 0;
|
||||
let renderedCount = null;
|
||||
|
||||
function available() {
|
||||
return typeof navigator?.setAppBadge === 'function'
|
||||
&& typeof navigator?.clearAppBadge === 'function';
|
||||
}
|
||||
|
||||
async function render() {
|
||||
if (!enabled || !available() || renderedCount === confirmedCount) return true;
|
||||
try {
|
||||
if (confirmedCount > 0) await navigator.setAppBadge(confirmedCount);
|
||||
else await navigator.clearAppBadge();
|
||||
renderedCount = confirmedCount;
|
||||
if (status) status.textContent = 'App icon badge is on and up to date.';
|
||||
return true;
|
||||
} catch (_error) {
|
||||
if (status) status.textContent = 'Could not update the app icon badge. Stackchain still has the confirmed count.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function change() {
|
||||
enabled = Boolean(control?.checked);
|
||||
if (enabled) storage?.setItem(ENABLED_KEY, 'true');
|
||||
else storage?.removeItem(ENABLED_KEY);
|
||||
if (!enabled && available()) {
|
||||
try {
|
||||
await navigator.clearAppBadge();
|
||||
renderedCount = null;
|
||||
if (status) status.textContent = 'App icon badge is off.';
|
||||
return true;
|
||||
} catch (_error) {
|
||||
if (status) status.textContent = 'App icon badge is off, but the browser could not clear the old count.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return render();
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (!available()) {
|
||||
if (container) container.hidden = true;
|
||||
if (control) control.disabled = true;
|
||||
if (status) status.textContent = 'App icon badges are unavailable in this browser.';
|
||||
return false;
|
||||
}
|
||||
enabled = storage?.getItem(ENABLED_KEY) === 'true';
|
||||
if (control) {
|
||||
control.checked = enabled;
|
||||
control.addEventListener('change', change);
|
||||
}
|
||||
if (status) status.textContent = enabled ? 'App icon badge is on.' : 'App icon badge is off.';
|
||||
return true;
|
||||
}
|
||||
|
||||
function readiness() {
|
||||
if (!available()) return {state:'unavailable', detail:'App icon badges are unavailable in this browser.'};
|
||||
return enabled
|
||||
? {state:'complete', detail:'The app icon shows the confirmed unread Updates count.'}
|
||||
: {state:'incomplete', detail:'Show the confirmed unread Updates count without opening Stackchain.'};
|
||||
}
|
||||
|
||||
async function enable() {
|
||||
if (!available()) return false;
|
||||
if (control) control.checked = true;
|
||||
await change();
|
||||
return true;
|
||||
}
|
||||
|
||||
async function reconcile(count, {authoritative = false} = {}) {
|
||||
if (!authoritative || !Number.isSafeInteger(count) || count < 0) return false;
|
||||
confirmedCount = count;
|
||||
return render();
|
||||
}
|
||||
|
||||
return {start, change, reconcile, readiness, enable};
|
||||
});
|
||||
@@ -9,8 +9,9 @@
|
||||
['offline', options.offlineButton, options.offlineStatus, options.enableOffline, 'Enable'],
|
||||
['protection', options.protectionButton, options.protectionStatus, options.protectStorage, 'Protect'],
|
||||
['push', options.pushButton, options.pushStatus, options.enablePush, 'Enable'],
|
||||
['appBadge', options.appBadgeButton, options.appBadgeStatus, options.enableAppBadge, 'Enable'],
|
||||
['deadline', options.deadlineButton, options.deadlineStatus, options.enableDeadline, 'Enable'],
|
||||
];
|
||||
].filter(([_name, button, status, action]) => button && status && action);
|
||||
let trigger = options.launcher;
|
||||
let ownsDetour = false;
|
||||
let backgroundInert = null;
|
||||
@@ -167,10 +168,12 @@ function mountMobileDeviceSetup(options) {
|
||||
sheet:qs('#device-setup-sheet'), installButton:qs('#device-setup-install'),
|
||||
offlineButton:qs('#device-setup-offline'), pushButton:qs('#device-setup-push'),
|
||||
protectionButton:qs('#device-setup-protection'),
|
||||
appBadgeButton:qs('#device-setup-app-badge'),
|
||||
deadlineButton:qs('#device-setup-deadline'),
|
||||
installStatus:qs('#device-setup-install-status'), offlineStatus:qs('#device-setup-offline-status'),
|
||||
protectionStatus:qs('#device-setup-protection-status'),
|
||||
pushStatus:qs('#device-setup-push-status'), deadlineStatus:qs('#device-setup-deadline-status'),
|
||||
appBadgeStatus:qs('#device-setup-app-badge-status'),
|
||||
readyStatus:qs('#device-setup-ready-status'),
|
||||
promptCard:qs('#device-readiness-card'), promptSummary:qs('#device-readiness-summary'),
|
||||
promptLauncher:qs('#finish-device-setup'), promptDismiss:qs('#dismiss-device-readiness'),
|
||||
@@ -193,12 +196,14 @@ function mountMobileDeviceSetup(options) {
|
||||
: {state:'incomplete', detail:'Private My Work and Today data are not saved offline.'},
|
||||
protection:options.storageProtectionReadiness(),
|
||||
push:options.notificationReadiness(),
|
||||
appBadge:options.appBadgeReadiness(),
|
||||
deadline:options.deadlineReadiness(),
|
||||
}),
|
||||
install:() => options.installApp.install(),
|
||||
enableOffline:options.enableOffline,
|
||||
protectStorage:options.protectStorage,
|
||||
enablePush:options.enablePush,
|
||||
enableAppBadge:options.enableAppBadge,
|
||||
enableDeadline:options.enableDeadline,
|
||||
});
|
||||
}
|
||||
@@ -190,6 +190,7 @@ const SHELL = [
|
||||
BASE + 'static/mobile-launch.js',
|
||||
BASE + 'static/mobile-insights.js',
|
||||
BASE + 'static/mobile-app-shortcuts.js',
|
||||
BASE + 'static/mobile-app-badge.js',
|
||||
BASE + 'static/install-app.js',
|
||||
BASE + 'static/private-data-inventory.js',
|
||||
BASE + 'static/private-device-data.js',
|
||||
|
||||
Reference in new issue
Block a user