aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-05 00:41:16 -0700
committerAdam Soltys <[email protected]>2026-10-05 00:41:16 -0700
commit5d82c98970e3fa7ac4153294e0e27a5cb1a7f480 (patch)
tree656ac4498e31580a187b4454b948dfa819066e6f /src/features
parent898cb33aba31a37be1e1a5b7e2ded3200dd29180 (diff)
downloadcoinosv3-5d82c98970e3fa7ac4153294e0e27a5cb1a7f480.tar.gz
coinosv3-5d82c98970e3fa7ac4153294e0e27a5cb1a7f480.zip
Custom feeds sync across devices: their own sync slot, newest edit wins, deletions remembered
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
Diffstat (limited to 'src/features')
-rw-r--r--src/features/messages.js50
1 files changed, 45 insertions, 5 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index e0c5b4c..c32c1ce 100644
--- a/src/features/messages.js
+++ b/src/features/messages.js
@@ -52,6 +52,7 @@ import { qrSvg } from '../qr.js';
import { COMMUNITY, EPOCH } from '../community.js';
import COMMUNITY_DIRECTORY from '../community-directory.json';
import { assetUrl } from '../asset-url.js';
+import { mergeFeeds } from '../feed-sync.js';
const DM_RELAYS = ['wss://relay.coinos.io', 'wss://nos.lol'];
const NOTIFIER = 'https://nwcpush.coinos.io';
const APP_BASE = 'https://v3.coinos.io';
@@ -397,6 +398,38 @@ export function messagesFeature(ctx) {
});
}
+ // Custom feeds sync across devices too (mergeFeeds: newest edit wins,
+ // deletions remembered).
+ if (wallet.registerCacheExtension && !wallet._feedsSync) {
+ wallet._feedsSync = true;
+ wallet.registerCacheExtension({
+ domain: 'feeds',
+ mergeAlways: true,
+ save: () => {
+ const m = wallet.loadFeatureState('messages', {});
+ const feeds = m.feeds || [], gone = m.feedsGone || {};
+ return feeds.length || Object.keys(gone).length ? { feeds, feedsGone: gone } : {};
+ },
+ load: (d) => {
+ if (!Array.isArray(d.feeds) && !d.feedsGone) return;
+ const m = wallet.loadFeatureState('messages', {});
+ const got = mergeFeeds({ feeds: m.feeds, gone: m.feedsGone }, { feeds: d.feeds, gone: d.feedsGone });
+ if (!got) return;
+ m.feeds = got.feeds; m.feedsGone = got.gone;
+ wallet.saveFeatureState('messages', m);
+ stCache = null;
+ try { for (const id of got.touched) feedStates.delete(id); } catch {} // its query may have changed
+ scheduleRepaint();
+ },
+ });
+ }
+ // A feed added, edited or deleted here goes out on the next sync.
+ let feedsPush = null;
+ const feedsChanged = () => {
+ clearTimeout(feedsPush);
+ feedsPush = setTimeout(() => { try { wallet.saveCache(); } catch {} }, 1000);
+ };
+
// Per-conversation dots for the list screen: newer than when we last had
// that thread (or any channel of that room) open.
const dmUnread = (pk, msgs) =>
@@ -4198,7 +4231,7 @@ export function messagesFeature(ctx) {
STARTER_BITCOIN_PACK ? { id: 'starter-bitcoin', name: 'Bitcoin', follows: false, authors: [], packs: [STARTER_BITCOIN_PACK], topics: [], at } : null,
{ id: 'starter-gardenstr', name: '#gardenstr', follows: false, authors: [], packs: [], topics: ['gardenstr'], at },
].filter(Boolean);
- for (const f of feeds) if (!s.feeds.some((x) => x.id === f.id)) s.feeds.push(f);
+ for (const f of feeds) if (!s.feeds.some((x) => x.id === f.id) && !(s.feedsGone || {})[f.id]) s.feeds.push(f);
save(s);
if (onlyIfNoFollows) {
// "New" here means new to coinos, not new to nostr: anyone with a
@@ -4727,7 +4760,8 @@ export function messagesFeature(ctx) {
for (const [d, ev] of newest) {
const gone = (s.listsGone || {})[d];
if (gone && ev.created_at <= gone) continue; // deleted here; a relay still serving it
- const cur = s.feeds.find((f) => f.d === d);
+ // a feed synced from another device before its list link did is that list
+ const cur = s.feeds.find((f) => f.d === d) || s.feeds.find((f) => !f.d && f.id === d);
if (cur && (cur.listAt || 0) >= ev.created_at) continue;
const got = packFromEvent(ev);
const priv = await privateMembers(ev);
@@ -4740,7 +4774,7 @@ export function messagesFeature(ctx) {
feedStates.delete(cur ? cur.id : 'list:' + d); // its query changed
changed = true;
}
- if (changed) { save(s); scheduleRepaint(); }
+ if (changed) { save(s); feedsChanged(); scheduleRepaint(); }
})().finally(() => { listsJob = null; });
return listsJob;
}
@@ -4759,7 +4793,7 @@ export function messagesFeature(ctx) {
if (!ok) throw new Error(t('msgSendFailed'));
const s = st();
const cur = s.feeds.find((f) => f.id === def.id);
- if (cur) { cur.d = d; cur.listAt = created_at; save(s); }
+ if (cur) { cur.d = d; cur.listAt = created_at; cur.at = Date.now(); save(s); feedsChanged(); }
return d;
}
// NIP-09: ask the relays to drop the list — and remember that we did, so
@@ -4832,6 +4866,7 @@ export function messagesFeature(ctx) {
def.authors = had ? def.authors.filter((x) => x !== pk) : [...(def.authors || []), pk].slice(0, FEED_AUTHORS_MAX);
def.at = Date.now();
save(s);
+ feedsChanged();
feedStates.delete(def.id);
render();
if (def.d) publishFollowSet(def).catch((e) => { if (!(e instanceof NoIdentity)) toast(e.message || String(e)); });
@@ -5275,7 +5310,7 @@ export function messagesFeature(ctx) {
for (const r of relays) { try { resetRelay(r); } catch {} } // chosen by hand: tried again even if it was sick
if (def.builtin) { const s = st(); s.builtinRelays = { ...(s.builtinRelays || {}), [def.id]: relays }; save(s); }
else if (adhocFeeds.has(def.id)) def.relays = relays;
- else { const s = st(); s.feeds = s.feeds.map((f) => (f.id === def.id ? { ...f, relays } : f)); save(s); }
+ else { const s = st(); s.feeds = s.feeds.map((f) => (f.id === def.id ? { ...f, relays, at: Date.now() } : f)); save(s); feedsChanged(); }
dropFeedState(def.id);
try { wallet.saveFeatureState(feedCacheKey(def.id), []); } catch {}
if (curFeedId === def.id) selectFeed(def.id);
@@ -10492,6 +10527,7 @@ export function messagesFeature(ctx) {
// d, listAt, listContent and priv ride along from the previous copy
if (keep) s.feeds = s.feeds.map((f) => (f.id === id ? { ...f, ...def } : f)); else s.feeds.push(def);
save(s);
+ feedsChanged();
if (e.id && adhocFeeds.has(e.id)) adhocFeeds.delete(e.id);
feedStates.delete(id); // its query changed: rebuild from scratch
ui.feedEdit = null;
@@ -10518,6 +10554,7 @@ export function messagesFeature(ctx) {
s.feeds.push({ id, name: '', follows: false, authors: [], packs: [], topics: [], of: adhoc.of,
...(adhoc.relays && adhoc.relays.length ? { relays: adhoc.relays } : {}), at: Date.now() });
save(s);
+ feedsChanged();
}
adhocFeeds.delete(adhoc.id);
feedStates.delete(adhoc.id);
@@ -10528,7 +10565,10 @@ export function messagesFeature(ctx) {
const s = st();
const def = s.feeds.find((f) => f.id === id);
s.feeds = s.feeds.filter((f) => f.id !== id);
+ const gone = Object.entries(s.feedsGone || {}).sort((a, b) => b[1] - a[1]).slice(0, 199);
+ s.feedsGone = Object.fromEntries([[id, Date.now()], ...gone]);
save(s);
+ feedsChanged();
if (def && def.d) deleteFollowSet(def).catch(() => {}); // after ours: it re-reads the state
feedStates.delete(id);
try { wallet.saveFeatureState(feedCacheKey(id), []); } catch {}