feat: sync notification badge + clarify cloud storage support
Notification system:
- When sync applies data (file folder, browser account, or sync code),
ss_sync_notification is written to local storage with the source.
- Service worker catches it via storage.onChanged, shows a purple 'SYN'
badge on the extension icon that persists until Options is opened, and
fires a desktop notification ('Settings updated via sync folder — open
Options to review').
- Clicking the desktop notification opens the Options page directly.
- On service worker wake, SYN badge is restored if the notification was
not yet dismissed.
- Opening Options clears ss_sync_notification, resets the badge, and
sends a sync:notification-seen message to the service worker.
- Added 'notifications' permission to both manifests.
Cloud storage clarity:
- Options page now explicitly lists that the folder sync works with any
cloud storage that has a desktop sync client: Dropbox, OneDrive, Google
Drive, iCloud Drive, Box, pCloud, Nextcloud, Synology Drive, etc.
https://claude.ai/code/session_01TKpSR9M8JgHLXCp5CeDsQP
This commit is contained in:
@@ -12,7 +12,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"scripting"
|
"scripting",
|
||||||
|
"notifications"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://claude.ai/*",
|
"https://claude.ai/*",
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
"scripting"
|
"scripting",
|
||||||
|
"notifications"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://claude.ai/*",
|
"https://claude.ai/*",
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ const messageHandlers = {
|
|||||||
sendResponse({ count: tabCounts.get(tabId) || 0 });
|
sendResponse({ count: tabCounts.get(tabId) || 0 });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async 'sync:notification-seen'() {
|
||||||
|
// Options page opened — clear the sync badge and pending notification flag
|
||||||
|
await api.storage.local.remove('ss_sync_notification');
|
||||||
|
api.action.setBadgeText({ text: '' });
|
||||||
|
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
||||||
|
},
|
||||||
|
|
||||||
async 'update:settings'(message) {
|
async 'update:settings'(message) {
|
||||||
await Storage.saveSettings(message.settings);
|
await Storage.saveSettings(message.settings);
|
||||||
|
|
||||||
@@ -309,6 +316,33 @@ api.storage.onChanged.addListener(async (changes, areaName) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a sync operation applied new data, show badge + notification
|
||||||
|
if (areaName === 'local' && changes.ss_sync_notification?.newValue) {
|
||||||
|
const notif = changes.ss_sync_notification.newValue;
|
||||||
|
const sourceLabel = {
|
||||||
|
'file': 'sync folder',
|
||||||
|
'browser-sync': 'browser account sync',
|
||||||
|
'code': 'sync code import',
|
||||||
|
}[notif.source] || 'sync';
|
||||||
|
|
||||||
|
// Purple badge — persists until Options is opened
|
||||||
|
api.action.setBadgeText({ text: 'SYN' });
|
||||||
|
api.action.setBadgeBackgroundColor({ color: '#7c3aed' });
|
||||||
|
|
||||||
|
// Desktop notification
|
||||||
|
try {
|
||||||
|
api.notifications.create('ss-sync-applied', {
|
||||||
|
type: 'basic',
|
||||||
|
iconUrl: 'icons/icon48.svg',
|
||||||
|
title: 'Silent Send — Settings Synced',
|
||||||
|
message: `Settings updated via ${sourceLabel}. Open Options to review.`,
|
||||||
|
priority: 1,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Notifications permission not granted — badge is still visible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When sync storage changes (another device pushed new data), pull it into local
|
// When sync storage changes (another device pushed new data), pull it into local
|
||||||
if (areaName === 'sync' && (changes.ss_sync_meta || Object.keys(changes).some(k => k.startsWith('ss_sync_chunk_')))) {
|
if (areaName === 'sync' && (changes.ss_sync_meta || Object.keys(changes).some(k => k.startsWith('ss_sync_chunk_')))) {
|
||||||
const settings = await Storage.getSettings();
|
const settings = await Storage.getSettings();
|
||||||
@@ -318,6 +352,14 @@ api.storage.onChanged.addListener(async (changes, areaName) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clicking a sync notification opens the Options page
|
||||||
|
api.notifications.onClicked.addListener((notificationId) => {
|
||||||
|
if (notificationId === 'ss-sync-applied') {
|
||||||
|
api.runtime.openOptionsPage();
|
||||||
|
api.notifications.clear(notificationId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// --- Set initial state ---
|
// --- Set initial state ---
|
||||||
api.runtime.onInstalled.addListener(async () => {
|
api.runtime.onInstalled.addListener(async () => {
|
||||||
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
api.action.setBadgeBackgroundColor({ color: '#6b7280' });
|
||||||
@@ -329,6 +371,14 @@ api.runtime.onInstalled.addListener(async () => {
|
|||||||
(async () => {
|
(async () => {
|
||||||
const settings = await Storage.getSettings();
|
const settings = await Storage.getSettings();
|
||||||
await updateIcon(settings);
|
await updateIcon(settings);
|
||||||
|
|
||||||
|
// Restore the SYN badge if the user hasn't opened Options since the last sync
|
||||||
|
const stored = await api.storage.local.get('ss_sync_notification');
|
||||||
|
if (stored.ss_sync_notification) {
|
||||||
|
api.action.setBadgeText({ text: 'SYN' });
|
||||||
|
api.action.setBadgeBackgroundColor({ color: '#7c3aed' });
|
||||||
|
}
|
||||||
|
|
||||||
if (settings.browserSync) {
|
if (settings.browserSync) {
|
||||||
await SilentSendSync.pullFromSyncStorage();
|
await SilentSendSync.pullFromSyncStorage();
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -65,7 +65,7 @@ const SilentSendSync = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._applyData(data);
|
await this._applyData(data, 'code');
|
||||||
return { success: true, importTime: new Date(data.lastModified).toLocaleString() };
|
return { success: true, importTime: new Date(data.lastModified).toLocaleString() };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { success: false, reason: e.message };
|
return { success: false, reason: e.message };
|
||||||
@@ -131,7 +131,7 @@ const SilentSendSync = {
|
|||||||
const json = chunkKeys.map(k => chunkResult[k] || '').join('');
|
const json = chunkKeys.map(k => chunkResult[k] || '').join('');
|
||||||
const data = JSON.parse(json);
|
const data = JSON.parse(json);
|
||||||
|
|
||||||
await this._applyData(data);
|
await this._applyData(data, 'browser-sync');
|
||||||
return { imported: true, time: new Date(data.lastModified).toLocaleString() };
|
return { imported: true, time: new Date(data.lastModified).toLocaleString() };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('[Silent Send] pullFromSyncStorage failed:', e);
|
console.warn('[Silent Send] pullFromSyncStorage failed:', e);
|
||||||
@@ -154,8 +154,12 @@ const SilentSendSync = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
async _applyData(data) {
|
async _applyData(data, source = 'unknown') {
|
||||||
const toSet = { ss_lastModified: data.lastModified };
|
const toSet = {
|
||||||
|
ss_lastModified: data.lastModified,
|
||||||
|
// Signal the service worker to show a badge/notification
|
||||||
|
ss_sync_notification: { source, time: Date.now() },
|
||||||
|
};
|
||||||
if (data.identity !== undefined) toSet.ss_identity = data.identity;
|
if (data.identity !== undefined) toSet.ss_identity = data.identity;
|
||||||
if (data.mappings !== undefined) toSet.ss_mappings = data.mappings;
|
if (data.mappings !== undefined) toSet.ss_mappings = data.mappings;
|
||||||
if (data.settings !== undefined) toSet.ss_settings = data.settings;
|
if (data.settings !== undefined) toSet.ss_settings = data.settings;
|
||||||
|
|||||||
@@ -147,8 +147,9 @@
|
|||||||
Pick the same folder in each browser once. Changes are written to
|
Pick the same folder in each browser once. Changes are written to
|
||||||
<code>silent-send-sync.json</code> automatically whenever settings change,
|
<code>silent-send-sync.json</code> automatically whenever settings change,
|
||||||
and read back whenever this page is opened or regains focus.
|
and read back whenever this page is opened or regains focus.
|
||||||
Use a shared folder (Dropbox, OneDrive, iCloud Drive, or any network share)
|
Works with <strong>any cloud storage that has a desktop sync client</strong> —
|
||||||
for cross-computer sync too.
|
Dropbox, OneDrive, Google Drive, iCloud Drive, Box, pCloud, Nextcloud,
|
||||||
|
Synology Drive, or any network share. Just pick the cloud-synced folder.
|
||||||
</p>
|
</p>
|
||||||
<div class="bulk-actions" style="align-items:center">
|
<div class="bulk-actions" style="align-items:center">
|
||||||
<button class="btn btn-primary" id="btnPickSyncFolder">Choose Sync Folder</button>
|
<button class="btn btn-primary" id="btnPickSyncFolder">Choose Sync Folder</button>
|
||||||
|
|||||||
@@ -96,6 +96,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- File-based auto-sync ---
|
// --- File-based auto-sync ---
|
||||||
|
// Tell the service worker the user has seen any pending sync notification
|
||||||
|
api.runtime.sendMessage({ type: 'sync:notification-seen' }).catch(() => {});
|
||||||
|
await api.storage.local.remove('ss_sync_notification');
|
||||||
|
|
||||||
await initFileSync();
|
await initFileSync();
|
||||||
|
|
||||||
$('#btnPickSyncFolder').addEventListener('click', pickSyncFolder);
|
$('#btnPickSyncFolder').addEventListener('click', pickSyncFolder);
|
||||||
@@ -559,7 +563,7 @@ async function checkFileSyncUpdate() {
|
|||||||
|
|
||||||
const local = await SilentSendSync._getAllData();
|
const local = await SilentSendSync._getAllData();
|
||||||
if (data.lastModified > (local.lastModified || 0)) {
|
if (data.lastModified > (local.lastModified || 0)) {
|
||||||
await SilentSendSync._applyData(data);
|
await SilentSendSync._applyData(data, 'file');
|
||||||
mappings = await Storage.getMappings();
|
mappings = await Storage.getMappings();
|
||||||
settings = await Storage.getSettings();
|
settings = await Storage.getSettings();
|
||||||
$('#browserSync').checked = settings.browserSync === true;
|
$('#browserSync').checked = settings.browserSync === true;
|
||||||
|
|||||||
Reference in New Issue
Block a user