aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/features/messages.js71
-rw-r--r--src/i18n.js2
-rw-r--r--src/style.css2
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; }