diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 71 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | src/style.css | 2 |
3 files changed, 67 insertions, 8 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index b80c87b..48596f2 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -441,6 +441,9 @@ export function messagesFeature(ctx) { // A feed added, edited or deleted here goes out on the next sync. let feedsPush = null; const feedsChanged = () => { + // Everything and Follows + lists are built from the other feeds: a + // changed list rebuilds them next time they're opened + for (const id of [MIX, FOLLOWS_LISTS]) { const c = feedStates.get(id); if (c && c !== feed) feedStates.delete(id); } clearTimeout(feedsPush); feedsPush = setTimeout(() => { try { wallet.saveCache(); } catch {} }, 1000); }; @@ -4710,15 +4713,45 @@ export function messagesFeature(ctx) { // Two feeds everyone has: the people you follow, and everything the coinos // relay carries. Each remembers its own relay choice in the state. const EVERYTHING = 'all'; + // Amethyst's feed model, for the people who asked: everyone you follow plus + // everyone on your lists, and on top of that ("Everything") posts from + // anyone on a hashtag you follow — your topic feeds' tags and your NIP-51 + // interests (kind 10015). Built from your feeds each time, never stored; + // offered once there's something beyond your follows to add. + const MIX = 'mix', FOLLOWS_LISTS = 'follows-lists', INTERESTS_KIND = 10015; + const INTERESTS = 'interests'; // feature state: { tags, at } from your kind 10015 + function listsUnion() { + const authors = new Set(), priv = new Set(), packs = new Map(); + for (const f of st().feeds) { + for (const pk of f.authors || []) authors.add(pk); + for (const pk of f.priv || []) priv.add(pk); + for (const p of f.packs || []) packs.set(packKey(p), p); + } + return { authors: [...authors], priv: [...priv], packs: [...packs.values()] }; + } + function followedTopics() { + const out = new Set(); + try { for (const x of (wallet.loadFeatureState(INTERESTS, {}) || {}).tags || []) out.add(normTopic(x)); } catch {} + for (const f of st().feeds) { + if (f.follows || (f.authors || []).length || (f.packs || []).length || (f.priv || []).length) continue; + for (const x of f.topics || []) out.add(normTopic(x)); + } + out.delete(''); + return [...out].slice(0, 30); + } const feedList = () => { const br = st().builtinRelays || {}; + const lists = listsUnion(), orTopics = followedTopics(); + const hasLists = lists.authors.length || lists.priv.length || lists.packs.length; return [ + ...(hasLists || orTopics.length ? [{ id: MIX, name: t('feedMixName'), follows: true, ...lists, orTopics, builtin: true, relays: br[MIX] || [] }] : []), + ...(hasLists && orTopics.length ? [{ id: FOLLOWS_LISTS, name: t('feedFollowsListsName'), follows: true, ...lists, builtin: true, relays: br[FOLLOWS_LISTS] || [] }] : []), { id: FOLLOWING, name: t('feedFollowingName'), follows: true, builtin: true, relays: br[FOLLOWING] || [] }, { id: EVERYTHING, name: t('feedPopular'), follows: false, authors: [], packs: [], topics: [], all: true, curated: true, builtin: true, relays: oldPopularDefault(br[EVERYTHING]) ? [] : br[EVERYTHING] || [] }, ...st().feeds]; }; function feedDef(id = curFeedId) { - if (id === FOLLOWING || id === EVERYTHING) return feedList().find((f) => f.id === id); + if (id === FOLLOWING || id === EVERYTHING || id === MIX || id === FOLLOWS_LISTS) return feedList().find((f) => f.id === id) || null; return st().feeds.find((f) => f.id === id) || adhocFeeds.get(id) || null; } const normTopic = (x) => String(x || '').trim().replace(/^#/, '').toLowerCase(); @@ -4824,6 +4857,8 @@ export function messagesFeature(ctx) { resolvePacks(def); return out; } + // posts from anyone on these tags, as well as the feed's people (Everything) + const orTopicsOf = (def) => [...new Set(((def && def.orTopics) || []).map(normTopic).filter(Boolean))]; const feedTopics = (def = feedDef()) => [...new Set(((def && def.topics) || []).map(normTopic).filter(Boolean))].slice(0, 20); const feedHasQuery = (def = feedDef()) => !!def && (!!def.all || feedAuthors(def).length > 0 || feedTopics(def).length > 0); // A feed's own relay list, when it has one, replaces the usual choice @@ -4965,7 +5000,18 @@ export function messagesFeature(ctx) { const mine = await Promise.resolve(relaysOf(me)).catch(() => []); const relays = [...new Set([...zapRelays(), ...(mine || []), ...PACK_RELAYS])]; let evs; - try { evs = await queryOn(relays, { kinds: [LIST_KIND], authors: [me] }, 5000); } catch { listsAt = 0; return; } + try { evs = await queryOn(relays, { kinds: [LIST_KIND, INTERESTS_KIND], authors: [me] }, 5000); } catch { listsAt = 0; return; } + // your followed hashtags (NIP-51 interests): Everything reads them + const interest = (evs || []).filter((e) => e.kind === INTERESTS_KIND).sort((a, b) => b.created_at - a.created_at)[0]; + if (interest) { + const prev = wallet.loadFeatureState(INTERESTS, {}) || {}; + if (!(prev.at >= interest.created_at)) { + wallet.saveFeatureState(INTERESTS, { tags: interest.tags.filter((x) => x[0] === 't' && x[1]).map((x) => normTopic(x[1])), at: interest.created_at }); + feedStates.delete(MIX); + scheduleRepaint(); + } + } + evs = (evs || []).filter((e) => e.kind === LIST_KIND); const newest = new Map(); for (const ev of evs || []) { const d = dTagOf(ev); @@ -5340,8 +5386,11 @@ export function messagesFeature(ctx) { return; } try { - await queryFeed(feedRelaysOr(def, def && def.all ? FIREHOSE_RELAYS : NOTE_RELAYS), { kinds: FEED_KINDS, limit: FEED_PAGE, - ...(authors.length ? { authors } : {}), ...(topics.length ? { '#t': topics } : {}) }, merge, c, 3000); + const orT = orTopicsOf(def); + await Promise.all([ + queryFeed(feedRelaysOr(def, def && def.all ? FIREHOSE_RELAYS : NOTE_RELAYS), { kinds: FEED_KINDS, limit: FEED_PAGE, + ...(authors.length ? { authors } : {}), ...(topics.length ? { '#t': topics } : {}) }, merge, c, 3000), + orT.length ? queryFeed(feedRelaysOr(def, TOPIC_RELAYS), { kinds: FEED_KINDS, limit: FEED_PAGE, '#t': orT }, merge, c, 3000) : null]); } catch {} finally { if (!c.stopped) { c.status = 'ready'; @@ -5354,10 +5403,11 @@ export function messagesFeature(ctx) { // by its people, on its topics — stand in until the relays answer. function borrowedNotes(def) { if (!def || def.all) return []; - const authors = new Set(feedAuthors(def)), topics = new Set(feedTopics(def)); + const authors = new Set(feedAuthors(def)), topics = new Set(feedTopics(def)), orT = new Set(orTopicsOf(def)); if (!authors.size && !topics.size) return []; - const fits = (e) => (!authors.size || authors.has(e.pubkey)) - && (!topics.size || (e.tags || []).some((x) => x[0] === 't' && topics.has(normTopic(x[1])))); + const tagged = (e, set) => (e.tags || []).some((x) => x[0] === 't' && set.has(normTopic(x[1]))); + const fits = (e) => ((!authors.size || authors.has(e.pubkey)) + && (!topics.size || tagged(e, topics))) || (orT.size && tagged(e, orT)); const seen = new Set(), out = []; for (const o of feedStates.values()) { for (const e of o.notes) if (!seen.has(e.id) && fits(e) && !hidden(e)) { seen.add(e.id); out.push(e); } @@ -6408,6 +6458,10 @@ export function messagesFeature(ctx) { const tag = topics.length ? { '#t': topics } : {}; let got = false; if (def.curated) return popularPass(extra, merge, c, def); + const orT = orTopicsOf(def); + const orPass = orT.length + ? queryFeed(feedRelaysOr(def, TOPIC_RELAYS), { kinds, '#t': orT, limit, ...extra }, merge, c).then((x) => { if (x) got = true; }) + : null; if (!authors.length) { // topics alone: nobody's outbox to read, so the wide relays, one ask; // the firehose asks its relays for everything @@ -6445,6 +6499,7 @@ export function messagesFeature(ctx) { await readPlan(additional); })()]); } + await orPass; return got; } // Whatever a refresh finds goes behind the pill whenever posts are already @@ -6605,6 +6660,8 @@ export function messagesFeature(ctx) { const authors = feedAuthors(def); if (def.curated) { const iv = setInterval(() => { if (c === feed && ui.chatOpen && ui.msgView === 'feed') refreshFeed({ force: true, live: true }, c).catch(() => {}); }, 120_000); feedUnsubs.push(() => clearInterval(iv)); return; } if (!authors.length) { feedUnsubs.push(subscribeOn(feedRelaysOr(def, def.all ? FIREHOSE_RELAYS : TOPIC_RELAYS), { kinds: FEED_KINDS, ...tag, since }, on)); return; } + const orT = orTopicsOf(def); + if (orT.length) feedUnsubs.push(subscribeOn(feedRelaysOr(def, TOPIC_RELAYS), { kinds: FEED_KINDS, '#t': orT, since }, on)); const plan = feedRelays(def) ? [{ relays: feedRelays(def), authors }] : outboxPlan(authors); for (const { relays, authors: a } of plan) for (let i = 0; i < a.length; i += REQ_AUTHORS) diff --git a/src/i18n.js b/src/i18n.js index 29f77c3..0ec6366 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -272,6 +272,8 @@ const STR = { currencyLabel: 'Show amounts in', feedTitle: 'Feed', feedFollowingName: 'Following', + feedMixName: 'Everything', + feedFollowsListsName: 'Follows + lists', feedNew: 'New feed', feedNewName: 'New feed', feedEdit: 'Edit feed', diff --git a/src/style.css b/src/style.css index df90cbe..dabd7be 100644 --- a/src/style.css +++ b/src/style.css @@ -640,7 +640,7 @@ html:has(#app .chat-card), body:has(#app .chat-card) { overflow: hidden; } .chat-thread-row { display: flex; gap: 10px; align-items: center; cursor: pointer; } /* list rows read at message size: names 16, previews/member counts 15 — the generic .small (13px) made the inbox feel like fine print */ -.chat-thread-row .chat-name { font-size: 16px; } +.chat-thread-row .chat-name { font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .chat-thread-row .small { font-size: 15px; } .chat-preview { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .thread-when { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; } |
