feat: add tags/labels for saved items

- Add free-form tags per item: chip display with remove, inline add (comma-separated, grid view only), and a toolbar tag-filter popover listing every tag in use
- Search now also matches tags; tags sync across devices like every other item field
- Fix Firefox manifest.json compliance for AMO submission: replace the placeholder gecko id with a real UUID, add the now-required data_collection_permissions (bookmarksInfo, since synced items are personal browsing/bookmark data), and bump strict_min_version to 140.0 (142.0 for gecko_android) to match
- Add description.md (elevator pitch / short / detailed descriptions for store listings), PRIVACY.md, and .gitignore for the dist/ packaging output
- Reword all em-dash punctuation across docs and code comments per user preference
This commit is contained in:
2026-07-14 12:44:17 +10:00
parent c5bd7e38bf
commit 232524ad97
10 changed files with 446 additions and 49 deletions
+14 -10
View File
@@ -3,7 +3,7 @@
// and newtab.html (via <script>, Firefox event page + the shelf UI).
//
// Sync storage only ever holds a lightweight metadata projection of each
// item (no screenshot thumbnails chrome.storage.sync caps a single key at
// item (no screenshot thumbnails; chrome.storage.sync caps a single key at
// 8KB), one key per item ("m_<id>") so the per-key cap is never a problem;
// the 512-key / ~100KB total quota is the real ceiling. All sync writes are
// best-effort: if the user isn't signed in, is offline, or is over quota,
@@ -13,6 +13,7 @@
const SYNC_KEY_PREFIX = "m_";
const SYNCED_IDS_KEY = "shelfSyncedIds";
const FAVICON_SYNC_MAX_LEN = 1000;
const TAGS_SYNC_MAX_COUNT = 20;
function syncKeyFor(id) {
return SYNC_KEY_PREFIX + id;
@@ -20,7 +21,7 @@ function syncKeyFor(id) {
// Shared by the reminder highlight and the auto-delete sweep, in both
// background.js and newtab.js, so the two periods can never drift apart.
// Months are approximated as 30 days no need for calendar precision here.
// Months are approximated as 30 days; no need for calendar precision here.
const UNIT_MS = { days: 86400000, weeks: 604800000, months: 2592000000 };
function periodMs(amount, unit) {
@@ -32,7 +33,7 @@ function periodMs(amount, unit) {
// invariant as the settings-panel validation, as a hard safety net: even if
// prefs somehow end up misconfigured (e.g. edited directly in storage), this
// refuses to delete anything until the auto-delete period genuinely exceeds
// the reminder period so a user is always reminded before something is
// the reminder period, so a user is always reminded before something is
// removed, never surprised by it.
function isOverdueForDeletion(item, prefs) {
if (!prefs || !prefs.autoDeleteEnabled || item.archived) return false;
@@ -54,16 +55,17 @@ function metaFromItem(item) {
updatedAt: item.updatedAt || item.dateAdded,
surfaceCount: item.surfaceCount || 0,
lastSurfacedAt: item.lastSurfacedAt || 0,
tags: (item.tags || []).slice(0, TAGS_SYNC_MAX_COUNT),
};
}
// "Worth a second look" spotlight: proactively resurfaces a few old,
// forgotten, unarchived items on the new tab page. The more times an item
// has been surfaced without being acted on, the longer before it's shown
// again so it never nags forever on the same links, but never fully goes
// again, so it never nags forever on the same links, but never fully goes
// silent either (it always comes back eventually).
const SPOTLIGHT_BACKOFF_DAYS = [3, 7, 14, 30, 60];
// Used when the reminder feature is off (its default state) — otherwise
// Used when the reminder feature is off (its default state). Otherwise
// "old enough to matter" would have no threshold at all and the spotlight
// would silently do nothing for most users.
const SPOTLIGHT_FALLBACK_MIN_AGE_MS = periodMs(14, "days");
@@ -122,7 +124,7 @@ async function writeSyncedIds(idSet) {
// Pulls remote adds/edits into `items` and pushes local adds/edits/deletes
// out to chrome.storage.sync. Returns the merged items array; does not
// persist it — callers are responsible for saving the result locally.
// persist it. Callers are responsible for saving the result locally.
async function reconcileWithSync(items) {
let syncItems, previouslySynced;
try {
@@ -135,7 +137,7 @@ async function reconcileWithSync(items) {
const localById = new Map(items.map((i) => [i.id, i]));
// Items that were synced before but are no longer in sync storage were
// deleted on another device drop them here too.
// deleted on another device, so drop them here too.
let merged = items.filter((item) => !(previouslySynced.has(item.id) && !syncById.has(item.id)));
// Pull in remote adds/edits.
@@ -143,8 +145,8 @@ async function reconcileWithSync(items) {
const local = localById.get(meta.id);
if (!local) {
// Absent locally but previously synced means *this* device deleted it;
// don't resurrect it from a remote copy that hasn't caught up yet
// let the deletion below propagate out instead.
// don't resurrect it from a remote copy that hasn't caught up yet.
// Let the deletion below propagate out instead.
if (previouslySynced.has(meta.id)) continue;
merged.push({
id: meta.id,
@@ -158,6 +160,7 @@ async function reconcileWithSync(items) {
updatedAt: meta.updatedAt,
surfaceCount: meta.surfaceCount || 0,
lastSurfacedAt: meta.lastSurfacedAt || 0,
tags: meta.tags || [],
});
} else if ((meta.updatedAt || 0) > (local.updatedAt || local.dateAdded || 0)) {
local.title = meta.title;
@@ -168,6 +171,7 @@ async function reconcileWithSync(items) {
local.updatedAt = meta.updatedAt;
local.surfaceCount = meta.surfaceCount || 0;
local.lastSurfacedAt = meta.lastSurfacedAt || 0;
local.tags = meta.tags || [];
}
}
@@ -198,7 +202,7 @@ async function reconcileWithSync(items) {
if (removals.length) await chrome.storage.sync.remove(removals);
await writeSyncedIds(nextSyncedIds);
} catch {
// Best effort local storage remains the source of truth for this device.
// Best effort: local storage remains the source of truth for this device.
}
return merged;