diff options
| author | Adam Soltys <[email protected]> | 2026-10-02 10:20:57 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-02 10:20:57 -0700 |
| commit | af19e72e54b1b3bac210d8c9d74317f662160c18 (patch) | |
| tree | 4899e195ab1f9d2850d51b30e6b5f89ba88a6412 | |
| parent | ef6729ceaf49d2d85bc8920c2c7756086ee7450d (diff) | |
| download | coinosv3-af19e72e54b1b3bac210d8c9d74317f662160c18.tar.gz coinosv3-af19e72e54b1b3bac210d8c9d74317f662160c18.zip | |
Feed form: relays and an every-post firehose; its Back works; a stronger new-post tint
- The new/edit feed form gets a Relays field (read this feed from these
relays only) and "Every post on these relays", which saves a firehose
feed (optionally curated) named after its relay and hides the people,
packs and topics it doesn't need. It used to take a hand-typed
/feed/all?r= link.
- The form's Back did nothing visible: opening it made no history entry,
so Back went to the page behind the feed and left the form up. The
editor (and the gear page's Spending/Savings view) are now part of the
navigation state.
- New-post tint 9% -> 16% green in light mode; dark mode unchanged.
- tools/feed-editor-relays-test.js.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Dmcrt9KyNDKVWFKnb8BREo
| -rw-r--r-- | src/app.js | 9 | ||||
| -rw-r--r-- | src/features/messages.js | 40 | ||||
| -rw-r--r-- | src/i18n.js | 4 | ||||
| -rw-r--r-- | src/locales/ar.json | 6 | ||||
| -rw-r--r-- | src/locales/bn.json | 6 | ||||
| -rw-r--r-- | src/locales/de.json | 6 | ||||
| -rw-r--r-- | src/locales/es.json | 6 | ||||
| -rw-r--r-- | src/locales/fr.json | 6 | ||||
| -rw-r--r-- | src/locales/hi.json | 6 | ||||
| -rw-r--r-- | src/locales/id.json | 6 | ||||
| -rw-r--r-- | src/locales/ja.json | 6 | ||||
| -rw-r--r-- | src/locales/ko.json | 6 | ||||
| -rw-r--r-- | src/locales/mr.json | 6 | ||||
| -rw-r--r-- | src/locales/pt.json | 6 | ||||
| -rw-r--r-- | src/locales/ru.json | 6 | ||||
| -rw-r--r-- | src/locales/sw.json | 6 | ||||
| -rw-r--r-- | src/locales/ta.json | 6 | ||||
| -rw-r--r-- | src/locales/te.json | 6 | ||||
| -rw-r--r-- | src/locales/tr.json | 6 | ||||
| -rw-r--r-- | src/locales/ur.json | 6 | ||||
| -rw-r--r-- | src/locales/vi.json | 6 | ||||
| -rw-r--r-- | src/locales/zh.json | 6 | ||||
| -rw-r--r-- | src/style.css | 5 | ||||
| -rw-r--r-- | tools/feed-editor-relays-test.js | 83 |
24 files changed, 222 insertions, 33 deletions
@@ -795,7 +795,7 @@ function renderInner() { // ui field that decides which page is on screen. const navKey = [ui.screen, ui.tab === 'settings', ui.chatOpen, ui.msgView, ui.msgPeer, ui.msgCommunity, ui.profilePk, ui.settingsPage, ui.addrScan, ui.arkExitPage, ui.txDetail, ui.giftMode, ui.claimStep, ui.nameEditOpen, - ui.noteThread && ui.noteThread.focusId, !!ui.userSearch, !!ui.zapSetup, !!ui.hatShop].join('|'); + ui.noteThread && ui.noteThread.focusId, !!ui.userSearch, !!ui.zapSetup, !!ui.hatShop, !!ui.feedEdit, ui.settingsView].join('|'); // A deep-linked profile landing is ONE page settling, not a chain of // navigations: boot screens shuffle beneath the shell (unlock → restored // wallet), then the resolved profile replaces the shell in place. The key @@ -868,7 +868,7 @@ wallet.subscribe(scheduleRender); // lightbox: a full-screen photo is a place too — the phone's Back must close // it, not pop the screen under it (which once walked a viewer straight back // to the start page, so the photo's × then looked like a logout). -const NAV_FIELDS = ['screen', 'tab', 'txDetail', 'arkMoveDetail', 'arkReconDetail', 'arkExitDetail', 'giftDetail', 'bump', 'giftMode', 'claimStep', 'chatOpen', 'msgView', 'feedId', 'pubProf', 'arkCoinsPage', 'arkExitPage', 'msgCommunity', 'msgPeer', 'profilePk', 'profEdit', 'profEditFilled', 'profOverThread', 'settingsPage', 'nameEditOpen', 'noteThread', 'userSearch', 'zapSetup', 'hatShop', 'lightbox']; +const NAV_FIELDS = ['screen', 'tab', 'txDetail', 'arkMoveDetail', 'arkReconDetail', 'arkExitDetail', 'giftDetail', 'bump', 'giftMode', 'claimStep', 'chatOpen', 'msgView', 'feedId', 'pubProf', 'arkCoinsPage', 'arkExitPage', 'msgCommunity', 'msgPeer', 'profilePk', 'profEdit', 'profEditFilled', 'profOverThread', 'settingsPage', 'nameEditOpen', 'noteThread', 'userSearch', 'zapSetup', 'hatShop', 'lightbox', 'feedEdit', 'settingsView']; function navSnapshot() { const s = {}; for (const f of NAV_FIELDS) s[f] = ui[f] ?? null; @@ -877,6 +877,7 @@ function navSnapshot() { // keystroke must never mint a history entry. if (ui.noteThread) s.noteThread = { rootId: ui.noteThread.rootId, focusId: ui.noteThread.focusId, seed: ui.noteThread.seed }; if (ui.userSearch) s.userSearch = { q: '', rows: null }; + if (ui.feedEdit) s.feedEdit = { ...ui.feedEdit, q: '', rows: null, pq: '', packRows: null, pt: null }; if (ui.zapSetup) s.zapSetup = { pk: ui.zapSetup.pk, npub: ui.zapSetup.npub, eventId: ui.zapSetup.eventId, amount: '21' }; // Keep the editable fields in this history entry so a refresh restores the // draft. Copy them: later keystrokes must not mutate earlier snapshots. @@ -970,7 +971,7 @@ function restoreNavFromHistory(renderNow = true) { // the vault prompt); with an account open now, those screens are stale. // Restoring one painted the start page over a perfectly good wallet. if (['unlock', 'vault'].includes(nav.screen) && activeAccount()) nav.screen = 'wallet'; - for (const f of NAV_FIELDS) if (f in nav) ui[f] = f === 'profEdit' && nav[f] ? structuredClone(nav[f]) : nav[f]; + for (const f of NAV_FIELDS) if (f in nav) ui[f] = (f === 'profEdit' || f === 'feedEdit') && nav[f] ? structuredClone(nav[f]) : nav[f]; ui.lightbox = null; nav.lightbox = null; // an object URL doesn't survive a reload navStack = [nav]; navIndex = 0; @@ -997,7 +998,7 @@ window.addEventListener('popstate', (e) => { } restoringHistory = true; try { - for (const f of NAV_FIELDS) ui[f] = f === 'profEdit' && snap[f] ? structuredClone(snap[f]) : f in snap ? snap[f] : null; + for (const f of NAV_FIELDS) ui[f] = (f === 'profEdit' || f === 'feedEdit') && snap[f] ? structuredClone(snap[f]) : f in snap ? snap[f] : null; if (st && typeof st.i === 'number') navIndex = st.i; else { const i = navStack.findIndex((s) => navSig(s) === navSig(snap)); if (i >= 0) navIndex = i; } render(); diff --git a/src/features/messages.js b/src/features/messages.js index 176c779..68eefda 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -8894,8 +8894,10 @@ export function messagesFeature(ctx) { const isNew = !def || adhocFeeds.has(def.id); ui.feedEdit = def ? { id: def.id, name: def.name || '', follows: !!def.follows, authors: [...(def.authors || [])], priv: [...(def.priv || [])], packs: [...(def.packs || [])], - topics: feedTopics(def).map((x) => '#' + x).join(' '), q: '', rows: null, pq: '', packRows: null, isNew, d: def.d || null, publish: isNew || !!def.d } - : { id: null, name: '', follows: false, authors: [], priv: [], packs: [], topics: '', q: '', rows: null, pq: '', packRows: null, isNew: true, d: null, publish: true }; + topics: feedTopics(def).map((x) => '#' + x).join(' '), q: '', rows: null, pq: '', packRows: null, isNew, d: def.d || null, publish: isNew || !!def.d, + relays: (def.relays || []).map((r) => r.replace(/^wss:\/\//, '')).join(' '), all: !!def.all && !def.builtin, curated: !!def.curated } + : { id: null, name: '', follows: false, authors: [], priv: [], packs: [], topics: '', q: '', rows: null, pq: '', packRows: null, isNew: true, d: null, publish: true, + relays: '', all: false, curated: false }; render(); } const feedPeopleSearcher = makeSearcher((q, rows) => { @@ -8907,12 +8909,20 @@ export function messagesFeature(ctx) { const e = ui.feedEdit; if (!e) return; const topics = [...new Set(e.topics.split(/[\s,]+/).map(normTopic).filter(Boolean))].slice(0, 20); - if (!e.follows && !e.authors.length && !e.packs.length && !topics.length) { toast(t('feedNoQuery')); return; } - const name = e.name.trim().slice(0, 40) || (topics.length ? '#' + topics[0] : t('feedNewName')); + const words = String(e.relays || '').split(/[\s,]+/).filter(Boolean); + const relays = [...new Set(words.map(normRelay).filter(Boolean))].slice(0, 12); + if (relays.length < words.length) { toast(t('feedRelayBad')); return; } + // every post on its relays: a firehose needs the relays, nothing else + if (e.all && !relays.length) { toast(t('feedAllNeedsRelays')); return; } + if (!e.all && !e.follows && !e.authors.length && !e.packs.length && !topics.length) { toast(t('feedNoQuery')); return; } + const name = e.name.trim().slice(0, 40) || (e.all ? relays[0].replace(/^wss:\/\//, '') : topics.length ? '#' + topics[0] : t('feedNewName')); const s = st(); const keep = e.id && !adhocFeeds.has(e.id) && s.feeds.some((f) => f.id === e.id); const id = keep ? e.id : 'f' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); - const def = { id, name, follows: e.follows, authors: e.authors.slice(0, FEED_AUTHORS_MAX), packs: e.packs.slice(0, 20), topics, at: Date.now() }; + const def = e.all + ? { id, name, follows: false, authors: [], packs: [], topics: [], all: true, curated: !!e.curated, relays, at: Date.now() } + : { id, name, follows: e.follows, authors: e.authors.slice(0, FEED_AUTHORS_MAX), packs: e.packs.slice(0, 20), topics, relays, all: false, curated: false, at: Date.now() }; + if (!relays.length) delete def.relays; const prev = keep ? s.feeds.find((f) => f.id === id) : null; // 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); @@ -9008,7 +9018,19 @@ export function messagesFeature(ctx) { h('label', { class: 'col', style: 'gap:4px' }, h('span', { class: 'small muted' }, t('feedName')), h('input', { type: 'text', value: e.name, placeholder: t('feedNameHint'), maxlength: '40', onInput: (ev) => { e.name = ev.target.value; } })), - h('div', { class: 'col', style: 'gap:8px' }, + // where the feed reads, and whether it takes everything there + h('div', { class: 'col', style: 'gap:6px' }, + h('span', { class: 'small muted' }, t('feedRelays')), + h('input', { type: 'text', class: 'feed-relays-input', value: e.relays || '', placeholder: 'nos.lol relay.damus.io', + autocapitalize: 'none', autocomplete: 'off', spellcheck: 'false', onInput: (ev) => { e.relays = ev.target.value; } }), + h('div', { class: 'small faint' }, t('feedRelaysFormHelp')), + h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer' }, + h('input', { type: 'checkbox', class: 'feed-all-toggle', checked: !!e.all, style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', onChange: (ev) => { e.all = ev.target.checked; render(); } }), + h('span', {}, t('feedAllToggle'))), + e.all ? h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer;margin-left:26px' }, + h('input', { type: 'checkbox', checked: !!e.curated, style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', onChange: (ev) => { e.curated = ev.target.checked; } }), + h('span', { class: 'small' }, t('feedAllCurated'))) : null), + e.all ? null : h('div', { class: 'col', style: 'gap:8px' }, h('span', { class: 'small muted' }, t('feedPeople')), h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer' }, h('input', { type: 'checkbox', checked: e.follows, style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', onChange: (ev) => { e.follows = ev.target.checked; render(); } }), @@ -9030,7 +9052,7 @@ export function messagesFeature(ctx) { e.authors.push(r.pk); e.q = ''; e.rows = null; render(); }, (pk, node) => hook('wrapAvatar', pk, node))) : null), - h('div', { class: 'col', style: 'gap:8px' }, + e.all ? null : h('div', { class: 'col', style: 'gap:8px' }, h('span', { class: 'small muted' }, t('feedPacks')), ...e.packs.map((p) => packRow(p, null, h('button', { class: 'btn-sm', type: 'button', 'aria-label': t('remove'), onClick: () => { e.packs = e.packs.filter((x) => packKey(x) !== packKey(p)); render(); } }, '\u00d7'))), @@ -9045,12 +9067,12 @@ export function messagesFeature(ctx) { e.pq = ''; e.packRows = null; render(); })), e.packRows && !e.packRows.length ? h('div', { class: 'small faint' }, t('searchNoResults')) : null), - h('label', { class: 'col', style: 'gap:4px' }, + e.all ? null : h('label', { class: 'col', style: 'gap:4px' }, h('span', { class: 'small muted' }, t('feedTopics')), h('input', { type: 'text', value: e.topics, placeholder: t('feedTopicsHint'), autocapitalize: 'none', autocomplete: 'off', onInput: (ev) => { e.topics = ev.target.value; } }), h('div', { class: 'small faint' }, t('feedTopicsHelp'))), // a feed of people is a nostr list (NIP-51 follow set) unless told not to be - e.authors.length || (e.priv || []).length || e.d + !e.all && (e.authors.length || (e.priv || []).length || e.d) ? h('div', { class: 'col', style: 'gap:4px' }, h('label', { class: 'row gap6', style: 'align-items:center;cursor:pointer' }, h('input', { type: 'checkbox', class: 'feed-list-toggle', checked: !!e.publish, style: 'width:18px;height:18px;accent-color:var(--accent);margin:0', onChange: (ev) => { e.publish = ev.target.checked; } }), diff --git a/src/i18n.js b/src/i18n.js index ac538ed..906ff51 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -282,6 +282,10 @@ const STR = { askNotNow: 'Not now', feedLangLabel: 'Language', feedRelays: 'Relays', + feedRelaysFormHelp: 'Read this feed from these relays only. Leave empty for the usual ones.', + feedAllToggle: 'Every post on these relays (a firehose)', + feedAllCurated: 'Only popular posts: hide bots and posts nobody reacted to', + feedAllNeedsRelays: 'Name the relays to read everything from.', feedRelaysHint: 'Posts are read from these relays. What the feed asks for stays the same.', feedRelayUsual: 'usual', feedRelaysOutbox: 'Right now each person is read from the relays they publish to.', diff --git a/src/locales/ar.json b/src/locales/ar.json index 75971cf..dd81a34 100644 --- a/src/locales/ar.json +++ b/src/locales/ar.json @@ -881,5 +881,9 @@ "followHistRestored": "تمت استعادة {n} متابَعًا.", "securityTitle": "الأمان", "arkCoinsNone": "لا توجد عملات في الإنفاق بعد.", - "profKeyLink": "المفتاح العام" + "profKeyLink": "المفتاح العام", + "feedRelaysFormHelp": "اقرأ هذه الخلاصة من هذه المرحّلات فقط. اتركها فارغة لاستخدام المعتاد.", + "feedAllToggle": "كل منشور على هذه المرحّلات (تدفق كامل)", + "feedAllCurated": "المنشورات الرائجة فقط: إخفاء الروبوتات والمنشورات بلا تفاعل", + "feedAllNeedsRelays": "اذكر المرحّلات التي تقرأ منها كل شيء." } diff --git a/src/locales/bn.json b/src/locales/bn.json index 98bb71c..d93622d 100644 --- a/src/locales/bn.json +++ b/src/locales/bn.json @@ -881,5 +881,9 @@ "followHistRestored": "{n} জনকে আবার ফলো করা হয়েছে।", "securityTitle": "নিরাপত্তা", "arkCoinsNone": "স্পেন্ডিং-এ এখনও কোনো কয়েন নেই।", - "profKeyLink": "পাবলিক কী" + "profKeyLink": "পাবলিক কী", + "feedRelaysFormHelp": "শুধু এই রিলেগুলো থেকে এই ফিড পড়ুন। সাধারণগুলোর জন্য খালি রাখুন।", + "feedAllToggle": "এই রিলেগুলোর প্রতিটি পোস্ট (ফায়ারহোস)", + "feedAllCurated": "শুধু জনপ্রিয় পোস্ট: বট ও প্রতিক্রিয়াহীন পোস্ট লুকান", + "feedAllNeedsRelays": "কোন রিলে থেকে সব পড়বেন তা লিখুন।" } diff --git a/src/locales/de.json b/src/locales/de.json index ce61392..33e1f56 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -881,5 +881,9 @@ "followHistRestored": "{n} Follows wiederhergestellt.", "securityTitle": "Sicherheit", "arkCoinsNone": "Noch keine Coins in Ausgaben.", - "profKeyLink": "Öffentlicher Schlüssel" + "profKeyLink": "Öffentlicher Schlüssel", + "feedRelaysFormHelp": "Diesen Feed nur von diesen Relays lesen. Leer lassen für die üblichen.", + "feedAllToggle": "Jeder Beitrag auf diesen Relays (Firehose)", + "feedAllCurated": "Nur beliebte Beiträge: Bots und Beiträge ohne Reaktionen ausblenden", + "feedAllNeedsRelays": "Nenne die Relays, von denen alles gelesen werden soll." } diff --git a/src/locales/es.json b/src/locales/es.json index c66feac..291da3d 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -881,5 +881,9 @@ "followHistRestored": "Se restauraron {n} seguidos.", "securityTitle": "Seguridad", "arkCoinsNone": "Aún no hay monedas en Gastos.", - "profKeyLink": "Clave pública" + "profKeyLink": "Clave pública", + "feedRelaysFormHelp": "Lee este feed solo de estos relays. Déjalo vacío para usar los habituales.", + "feedAllToggle": "Todas las publicaciones de estos relays (firehose)", + "feedAllCurated": "Solo publicaciones populares: oculta bots y publicaciones sin reacciones", + "feedAllNeedsRelays": "Indica los relays de los que leerlo todo." } diff --git a/src/locales/fr.json b/src/locales/fr.json index 970004e..31d07ba 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -881,5 +881,9 @@ "followHistRestored": "{n} abonnements restaurés.", "securityTitle": "Sécurité", "arkCoinsNone": "Aucune pièce dans Dépenses pour l’instant.", - "profKeyLink": "Clé publique" + "profKeyLink": "Clé publique", + "feedRelaysFormHelp": "Lire ce fil uniquement depuis ces relais. Laisser vide pour les relais habituels.", + "feedAllToggle": "Toutes les publications de ces relais (firehose)", + "feedAllCurated": "Seulement les publications populaires : masquer les bots et les publications sans réaction", + "feedAllNeedsRelays": "Indiquez les relais dont tout lire." } diff --git a/src/locales/hi.json b/src/locales/hi.json index 686bd6b..0d410e4 100644 --- a/src/locales/hi.json +++ b/src/locales/hi.json @@ -881,5 +881,9 @@ "followHistRestored": "{n} फ़ॉलो बहाल किए गए।", "securityTitle": "सुरक्षा", "arkCoinsNone": "स्पेंडिंग में अभी कोई कॉइन नहीं।", - "profKeyLink": "पब्लिक की" + "profKeyLink": "पब्लिक की", + "feedRelaysFormHelp": "यह फ़ीड सिर्फ़ इन रिले से पढ़ें। सामान्य रिले के लिए खाली छोड़ें।", + "feedAllToggle": "इन रिले की हर पोस्ट (फ़ायरहोज़)", + "feedAllCurated": "सिर्फ़ लोकप्रिय पोस्ट: बॉट और बिना प्रतिक्रिया वाली पोस्ट छिपाएँ", + "feedAllNeedsRelays": "वे रिले बताएँ जिनसे सब कुछ पढ़ना है।" } diff --git a/src/locales/id.json b/src/locales/id.json index a5b4f32..82c8e30 100644 --- a/src/locales/id.json +++ b/src/locales/id.json @@ -217,5 +217,9 @@ "followHistRestored": "{n} ikutan dipulihkan.", "securityTitle": "Keamanan", "arkCoinsNone": "Belum ada koin di Belanja.", - "profKeyLink": "Kunci publik" + "profKeyLink": "Kunci publik", + "feedRelaysFormHelp": "Baca feed ini hanya dari relay ini. Kosongkan untuk relay biasa.", + "feedAllToggle": "Setiap postingan di relay ini (firehose)", + "feedAllCurated": "Hanya postingan populer: sembunyikan bot dan postingan tanpa reaksi", + "feedAllNeedsRelays": "Sebutkan relay untuk membaca semuanya." } diff --git a/src/locales/ja.json b/src/locales/ja.json index b809144..c95f5d0 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -881,5 +881,9 @@ "followHistRestored": "{n}人のフォローを復元しました。", "securityTitle": "セキュリティ", "arkCoinsNone": "支出にはまだコインがありません。", - "profKeyLink": "公開鍵" + "profKeyLink": "公開鍵", + "feedRelaysFormHelp": "このフィードはこれらのリレーからだけ読み込みます。空欄ならいつものリレーです。", + "feedAllToggle": "これらのリレーのすべての投稿(ファイアホース)", + "feedAllCurated": "人気の投稿のみ:ボットと反応のない投稿を隠す", + "feedAllNeedsRelays": "すべてを読み込むリレーを指定してください。" } diff --git a/src/locales/ko.json b/src/locales/ko.json index e2e2500..40a9a40 100644 --- a/src/locales/ko.json +++ b/src/locales/ko.json @@ -880,5 +880,9 @@ "followHistRestored": "팔로우 {n}명을 복원했습니다.", "securityTitle": "보안", "arkCoinsNone": "지출에 아직 코인이 없습니다.", - "profKeyLink": "공개 키" + "profKeyLink": "공개 키", + "feedRelaysFormHelp": "이 피드를 이 릴레이에서만 읽습니다. 비워 두면 평소 릴레이를 씁니다.", + "feedAllToggle": "이 릴레이의 모든 게시물 (파이어호스)", + "feedAllCurated": "인기 게시물만: 봇과 반응 없는 게시물 숨기기", + "feedAllNeedsRelays": "모두 읽어 올 릴레이를 지정하세요." } diff --git a/src/locales/mr.json b/src/locales/mr.json index 6d443bb..8fdb6db 100644 --- a/src/locales/mr.json +++ b/src/locales/mr.json @@ -217,5 +217,9 @@ "followHistRestored": "{n} फॉलो पुनर्संचयित केले.", "securityTitle": "सुरक्षा", "arkCoinsNone": "स्पेंडिंगमध्ये अद्याप कोणतेही कॉइन नाहीत.", - "profKeyLink": "सार्वजनिक की" + "profKeyLink": "सार्वजनिक की", + "feedRelaysFormHelp": "ही फीड फक्त या रिलेवरून वाचा. नेहमीच्या रिलेसाठी रिकामे ठेवा.", + "feedAllToggle": "या रिलेवरील प्रत्येक पोस्ट (फायरहोज)", + "feedAllCurated": "फक्त लोकप्रिय पोस्ट: बॉट्स आणि प्रतिसाद नसलेल्या पोस्ट लपवा", + "feedAllNeedsRelays": "सर्व काही कोणत्या रिलेवरून वाचायचे ते सांगा." } diff --git a/src/locales/pt.json b/src/locales/pt.json index 63d15cf..3c832a2 100644 --- a/src/locales/pt.json +++ b/src/locales/pt.json @@ -881,5 +881,9 @@ "followHistRestored": "{n} seguidos restaurados.", "securityTitle": "Segurança", "arkCoinsNone": "Ainda não há moedas em Gastos.", - "profKeyLink": "Chave pública" + "profKeyLink": "Chave pública", + "feedRelaysFormHelp": "Ler este feed só destes relays. Deixe em branco para os habituais.", + "feedAllToggle": "Todas as publicações destes relays (firehose)", + "feedAllCurated": "Só publicações populares: ocultar bots e publicações sem reações", + "feedAllNeedsRelays": "Indique os relays de onde ler tudo." } diff --git a/src/locales/ru.json b/src/locales/ru.json index 98da4e2..6766b0e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -881,5 +881,9 @@ "followHistRestored": "Восстановлено подписок: {n}.", "securityTitle": "Безопасность", "arkCoinsNone": "В расходах пока нет монет.", - "profKeyLink": "Открытый ключ" + "profKeyLink": "Открытый ключ", + "feedRelaysFormHelp": "Читать эту ленту только с этих реле. Оставьте пустым для обычных.", + "feedAllToggle": "Все посты на этих реле (поток)", + "feedAllCurated": "Только популярные: скрыть ботов и посты без реакций", + "feedAllNeedsRelays": "Укажите реле, с которых читать всё." } diff --git a/src/locales/sw.json b/src/locales/sw.json index efaad77..57ff264 100644 --- a/src/locales/sw.json +++ b/src/locales/sw.json @@ -217,5 +217,9 @@ "followHistRestored": "Umerejesha unaowafuata {n}.", "securityTitle": "Usalama", "arkCoinsNone": "Bado hakuna sarafu kwenye Matumizi.", - "profKeyLink": "Ufunguo wa umma" + "profKeyLink": "Ufunguo wa umma", + "feedRelaysFormHelp": "Soma mlisho huu kutoka relay hizi tu. Acha tupu kwa zile za kawaida.", + "feedAllToggle": "Kila chapisho kwenye relay hizi (firehose)", + "feedAllCurated": "Machapisho maarufu tu: ficha boti na machapisho yasiyo na majibu", + "feedAllNeedsRelays": "Taja relay za kusoma kila kitu." } diff --git a/src/locales/ta.json b/src/locales/ta.json index bc210a3..0cb5244 100644 --- a/src/locales/ta.json +++ b/src/locales/ta.json @@ -217,5 +217,9 @@ "followHistRestored": "{n} பின்தொடர்வுகள் மீட்டெடுக்கப்பட்டன.", "securityTitle": "பாதுகாப்பு", "arkCoinsNone": "செலவுக் கணக்கில் இன்னும் நாணயங்கள் இல்லை.", - "profKeyLink": "பொது விசை" + "profKeyLink": "பொது விசை", + "feedRelaysFormHelp": "இந்த ஊட்டத்தை இந்த ரிலேக்களிலிருந்து மட்டும் படிக்கவும். வழக்கமானவற்றுக்கு காலியாக விடவும்.", + "feedAllToggle": "இந்த ரிலேக்களில் உள்ள ஒவ்வொரு பதிவும் (ஃபயர்ஹோஸ்)", + "feedAllCurated": "பிரபல பதிவுகள் மட்டும்: பாட்களையும் எதிர்வினையற்ற பதிவுகளையும் மறை", + "feedAllNeedsRelays": "அனைத்தையும் படிக்க வேண்டிய ரிலேக்களைக் குறிப்பிடவும்." } diff --git a/src/locales/te.json b/src/locales/te.json index 44d7017..9f468e6 100644 --- a/src/locales/te.json +++ b/src/locales/te.json @@ -215,5 +215,9 @@ "followHistRestored": "{n} ఫాలోలు పునరుద్ధరించబడ్డాయి.", "securityTitle": "భద్రత", "arkCoinsNone": "ఖర్చు ఖాతాలో ఇంకా నాణేలు లేవు.", - "profKeyLink": "పబ్లిక్ కీ" + "profKeyLink": "పబ్లిక్ కీ", + "feedRelaysFormHelp": "ఈ ఫీడ్ను ఈ రిలేల నుంచి మాత్రమే చదవండి. సాధారణ వాటికి ఖాళీగా ఉంచండి.", + "feedAllToggle": "ఈ రిలేలలోని ప్రతి పోస్ట్ (ఫైర్హోస్)", + "feedAllCurated": "జనాదరణ పొందిన పోస్ట్లు మాత్రమే: బాట్లు, స్పందన లేని పోస్ట్లను దాచు", + "feedAllNeedsRelays": "అన్నీ చదవాల్సిన రిలేలను పేర్కొనండి." } diff --git a/src/locales/tr.json b/src/locales/tr.json index f22cb0e..6c441b2 100644 --- a/src/locales/tr.json +++ b/src/locales/tr.json @@ -880,5 +880,9 @@ "followHistRestored": "{n} takip geri yüklendi.", "securityTitle": "Güvenlik", "arkCoinsNone": "Harcama’da henüz coin yok.", - "profKeyLink": "Açık anahtar" + "profKeyLink": "Açık anahtar", + "feedRelaysFormHelp": "Bu akışı yalnızca bu rölelerden oku. Her zamankiler için boş bırak.", + "feedAllToggle": "Bu rölelerdeki her gönderi (firehose)", + "feedAllCurated": "Yalnızca popüler gönderiler: botları ve tepki almamış gönderileri gizle", + "feedAllNeedsRelays": "Her şeyin okunacağı röleleri yaz." } diff --git a/src/locales/ur.json b/src/locales/ur.json index a018085..c6a6fe4 100644 --- a/src/locales/ur.json +++ b/src/locales/ur.json @@ -217,5 +217,9 @@ "followHistRestored": "{n} فالو بحال ہو گئے۔", "securityTitle": "سیکیورٹی", "arkCoinsNone": "خرچ میں ابھی کوئی کوائن نہیں۔", - "profKeyLink": "پبلک کی" + "profKeyLink": "پبلک کی", + "feedRelaysFormHelp": "یہ فیڈ صرف ان ریلے سے پڑھیں۔ معمول کے ریلے کے لیے خالی چھوڑ دیں۔", + "feedAllToggle": "ان ریلے پر ہر پوسٹ (فائر ہوز)", + "feedAllCurated": "صرف مقبول پوسٹس: بوٹس اور بغیر ردعمل والی پوسٹس چھپائیں", + "feedAllNeedsRelays": "وہ ریلے بتائیں جن سے سب کچھ پڑھنا ہے۔" } diff --git a/src/locales/vi.json b/src/locales/vi.json index fefac38..963aa21 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -217,5 +217,9 @@ "followHistRestored": "Đã khôi phục {n} người theo dõi.", "securityTitle": "Bảo mật", "arkCoinsNone": "Chưa có coin nào trong Chi tiêu.", - "profKeyLink": "Khóa công khai" + "profKeyLink": "Khóa công khai", + "feedRelaysFormHelp": "Chỉ đọc bảng tin này từ các relay này. Để trống để dùng relay thường dùng.", + "feedAllToggle": "Mọi bài đăng trên các relay này (firehose)", + "feedAllCurated": "Chỉ bài phổ biến: ẩn bot và bài không ai tương tác", + "feedAllNeedsRelays": "Hãy nêu các relay để đọc mọi thứ." } diff --git a/src/locales/zh.json b/src/locales/zh.json index e09b385..c73d88e 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -881,5 +881,9 @@ "followHistRestored": "已恢复 {n} 个关注。", "securityTitle": "安全", "arkCoinsNone": "支出账户中还没有币。", - "profKeyLink": "公钥" + "profKeyLink": "公钥", + "feedRelaysFormHelp": "仅从这些中继读取此动态。留空则使用常用中继。", + "feedAllToggle": "这些中继上的每一条帖子(全量流)", + "feedAllCurated": "仅热门帖子:隐藏机器人和无人互动的帖子", + "feedAllNeedsRelays": "请填写要读取全部内容的中继。" } diff --git a/src/style.css b/src/style.css index 7ef428c..5bb171a 100644 --- a/src/style.css +++ b/src/style.css @@ -964,12 +964,15 @@ html.no-scroll { overflow: hidden; scrollbar-gutter: auto; } side shadows carry the tint through the list's padding to its edges. */ .notes-feed > .row { transition: background-color 3s ease-out, box-shadow 3s ease-out; } .notes-feed > .row.note-fresh { - --fresh-bg: color-mix(in srgb, var(--green) 9%, var(--paper)); + /* 9% was a whisper on white; dark mode keeps it (the green carries more there) */ + --fresh-bg: color-mix(in srgb, var(--green) 16%, var(--paper)); background-color: var(--fresh-bg); box-shadow: -14px 0 0 var(--fresh-bg), 14px 0 0 var(--fresh-bg); } +:root[data-theme="dark"] .notes-feed > .row.note-fresh { --fresh-bg: color-mix(in srgb, var(--green) 9%, var(--paper)); } + /* Inline move-money panel on the balance card. */ .balance-move { gap: 10px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); text-align: left; } /* balance carousel: the other face peeks at the card's edge and drags into diff --git a/tools/feed-editor-relays-test.js b/tools/feed-editor-relays-test.js new file mode 100644 index 0000000..7e3edb5 --- /dev/null +++ b/tools/feed-editor-relays-test.js @@ -0,0 +1,83 @@ +// The new-feed form: its Back closes it (it used to send the page back +// behind the feed and leave the form up), and it can make a firehose — +// every post on the relays you name — as a saved feed. +// Run: bun tools/feed-editor-relays-test.js +import puppeteer from 'puppeteer-core'; +import { generateMnemonic } from '@scure/bip39'; +import { wordlist } from '@scure/bip39/wordlists/english'; +import { buildHtml } from '../build.js'; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +let ok = true; +const check = (n, c, d = '') => { console.log(` ${c ? '✓' : '✗'} ${n}${d ? ' — ' + d : ''}`); if (!c) ok = false; }; +const html = await buildHtml({ minify: true, pwa: false }); +const server = Bun.serve({ port: 0, fetch: () => new Response(html, { headers: { 'content-type': 'text/html' } }) }); +const browser = await puppeteer.launch({ executablePath: process.env.CHROME || '/usr/bin/google-chrome', headless: 'new', args: ['--no-sandbox'] }); +const page = await browser.newPage(); +const errs = []; page.on('pageerror', (e) => errs.push(String(e))); +// record what the page asks which relay, without letting it publish anything +await page.evaluateOnNewDocument(() => { + window.__reqs = []; + const Real = window.WebSocket; + window.WebSocket = function (url, ...a) { + const ws = new Real(url, ...a); + const send = ws.send.bind(ws); + ws.send = (data) => { + try { const m = JSON.parse(data); if (m[0] === 'REQ') window.__reqs.push({ url: String(url), filters: m.slice(2) }); if (m[0] === 'EVENT') return; } catch {} + return send(data); + }; + return ws; + }; + window.WebSocket.prototype = Real.prototype; Object.assign(window.WebSocket, Real); +}); +const click = (t) => page.evaluate((x) => { const e = [...document.querySelectorAll('button')].find((n) => n.textContent.trim().toLowerCase().includes(x)); if (e) { e.click(); return true; } return false; }, t); +const text = () => page.evaluate(() => document.body.innerText); +const waitFor = async (fn, ms = 15000) => { for (let i = 0; i < ms / 200; i++) { if (await page.evaluate(fn)) return true; await sleep(200); } return false; }; +try { + await page.setViewport({ width: 390, height: 844 }); + await page.goto(server.url.origin, { waitUntil: 'domcontentloaded' }); + await page.evaluate(() => localStorage.setItem('btc-wallet-network', 'regtest')); + await page.reload({ waitUntil: 'domcontentloaded' }); await sleep(400); + await click('create a new wallet'); await sleep(500); await click('to import'); await sleep(400); + await page.waitForSelector('textarea'); await page.type('textarea', generateMnemonic(wordlist)); await click('open wallet'); + await waitFor(() => /receive/i.test(document.body.innerText), 20000); + await page.click('[aria-label="Feed"]'); + await page.waitForSelector('.feed-chip.add', { timeout: 15000 }); + + // Back closes the form and leaves the feed where it was + await page.click('.feed-chip.add'); await sleep(400); + check('the + chip opens the new-feed form', !!(await page.$('.feed-relays-input'))); + await page.click('.chat-back'); await sleep(600); + check("the form's Back closes it", !(await page.$('.feed-relays-input'))); + check('...back on the feed, not some page behind it', await page.evaluate(() => history.state?.nav?.msgView === 'feed' && !!document.querySelector('.feed-chip.add'))); + await page.click('.feed-chip.add'); await sleep(400); + await page.goBack(); await sleep(600); + check('the browser Back closes it too', !(await page.$('.feed-relays-input')) && !!(await page.$('.feed-chip.add'))); + + // a firehose from one relay + await page.click('.feed-chip.add'); await sleep(400); + check('the people/packs/topics sections are there before the firehose is ticked', /people/i.test(await text()) && /topics/i.test(await text())); + await page.type('.feed-relays-input', 'nos.lol'); + await page.click('.feed-all-toggle'); await sleep(200); + check('ticking "every post" hides what a firehose does not need', !/add a person/i.test(await text()) && !(await page.$('input[placeholder*="#"]'))); + await page.evaluate(() => { window.__reqs = []; }); + await click('save'); await sleep(2500); + const chips = await page.evaluate(() => [...document.querySelectorAll('.feed-chip')].map((c) => (c.classList.contains('on') ? '*' : '') + c.textContent.trim())); + check('saved as a feed named after its relay, and opened', chips.includes('*nos.lol'), JSON.stringify(chips)); + const asks = await page.evaluate(() => window.__reqs.filter((r) => r.filters.some((f) => (f.kinds || []).includes(1) && !f.authors && !f['#t'] && !f.ids && !f['#e']))); + // (names, reactions and replies for what it shows are still asked about on the usual relays) + check('it asks nos.lol for every post, no authors or topics', asks.some((r) => /nos\.lol/.test(r.url)), JSON.stringify(asks.map((r) => r.url))); + check('...and asks no other relay for the posts themselves', asks.length > 0 && asks.every((r) => /nos\.lol/.test(r.url)), JSON.stringify([...new Set(asks.map((r) => r.url))])); + + // a firehose needs relays + await page.click('.feed-chip.add'); await sleep(400); + await page.click('.feed-all-toggle'); await sleep(200); + await click('save'); await sleep(400); + check('a firehose with no relays is refused, with a reason', !!(await page.$('.feed-relays-input')) && /name the relays/i.test(await text())); + check('no page errors', errs.length === 0, errs.join('; ').slice(0, 300)); +} catch (e) { + check('run completed', false, e.stack); +} finally { + await browser.close(); server.stop(true); +} +console.log(ok ? '\n✅ the feed form: Back works, and relays make a firehose' : '\n❌ failures above'); +process.exit(ok ? 0 : 1); |
