From f34423735a790773b8c482f9b72ac2adaf381267 Mon Sep 17 00:00:00 2001 From: rays Date: Fri, 16 Oct 2026 19:28:52 +0000 Subject: [PATCH] Signals' page copy says what happens, not that Jev does it; Auto spins while it picks (#263) Co-Authored-By: Claude Opus 5.5 --- crates/ipx/src/web.rs | 4 ++-- web/CHANGELOG.md | 2 +- web/app.css | 5 +++++ web/src/dialogs.ts | 4 ++-- web/src/items.ts | 2 +- web/src/signals.ts | 17 +++++++++-------- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/ipx/src/web.rs b/crates/ipx/src/web.rs index 4f223f5..e0096c1 100644 --- a/crates/ipx/src/web.rs +++ b/crates/ipx/src/web.rs @@ -518,7 +518,7 @@ async fn pick_signal_icon( Json(body): Json, ) -> Result, ApiError> { let Ok(key) = std::env::var("TYPESAFE_KEY") else { - return Err(ApiError::not_found("this server has no TypeSafe key to pick icons with")); + return Err(ApiError::not_found("picking an icon is not set up on this server")); }; let name = signal_name(&body.name)?; let words = clean_words(body.keywords)?; @@ -538,7 +538,7 @@ async fn pick_signal_icon( .await .map_err(|e| { tracing::warn!(error = %format!("{e:#}"), "Jev could not pick an icon"); - ApiError::bad_gateway("Jev could not pick an icon just now") + ApiError::bad_gateway("no icon could be picked just now") })?; Ok(Json(serde_json::json!({ "icon": icon }))) } diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index 836d622..55500b3 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -13,7 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name, the words to listen for and, if those are not enough, a sentence saying what you mean, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder first in the feed list, under All Subscriptions, closed or open as an OPML is, with its own page listing them and a + there for a new one (#255, #256, #265). Each can have an icon of its own, searched for among all of Font Awesome Free, in place of its initials (#261), or Jev's pick of one, which a Signal made without an icon gets by itself; each Signal's row counts its items and unread as a feed's does (#263). +- Signals pick what matters out of the noise of every feed you subscribe to: New signal takes a name, the words to listen for and, if those are not enough, a sentence saying what you mean, and its page lists the items from your feeds that are about it, read and unread as in their own feeds. Settings takes your TypeSafe key, which Signals need (#254). They are kept in a Signals folder first in the feed list, under All Subscriptions, closed or open as an OPML is, with its own page listing them and a + there for a new one (#255, #256, #265). Each can have an icon of its own, searched for among all of Font Awesome Free, in place of its initials (#261), or one picked for it with Auto, which a Signal made without an icon gets by itself; each Signal's row counts its items and unread as a feed's does (#263). - A list opens at once when you have seen it before or the page fetched it ahead: All Subscriptions and the feeds with something unread are fetched when the page loads, and a feed or filter as the pointer or a finger reaches it, then refreshed behind once shown. A list that has to be waited for shows a spinner instead of nothing (#247). Currently Listening and the Directory's front page are fetched ahead the same way and open at once (#248). ### Changed diff --git a/web/app.css b/web/app.css index 7ada77e..48ffb7f 100644 --- a/web/app.css +++ b/web/app.css @@ -607,6 +607,11 @@ input:focus,select:focus{outline:0;border-color:var(--accent)} .iconcell{height:40px;border-radius:8px;display:grid;place-items:center;color:var(--dim)} .iconcell:hover,.iconcell[aria-selected="true"]{background:var(--raise);color:var(--fg)} .iconcell .ficon{width:20px;height:20px} +/* Auto in the icon picker, while an icon is picked: the scan spinner, beside the word. */ +.dlink.busy::after{ + content:"";display:inline-block;width:10px;height:10px;margin-left:6px;vertical-align:-1px;border-radius:50%; + border:2px solid var(--line);border-top-color:var(--accent);animation:ipxspin .8s linear infinite; +} /* Only a folder has a triangle, hung in the margin so every feed's art lines up with the places above it. The button is the row's full height and 24 px wide: a near miss used to open the folder's page, and 16 px left the triangle cramped against the art. */ diff --git a/web/src/dialogs.ts b/web/src/dialogs.ts index 313d958..2f7edec 100644 --- a/web/src/dialogs.ts +++ b/web/src/dialogs.ts @@ -444,8 +444,8 @@ async function prefsModal(){
- Signals pick what matters out of everything you subscribe to, using TypeSafe's - Jev on your own key, from typesafe.ai. + Signals pick what matters out of everything you subscribe to, on your own + key from typesafe.ai. The key is kept on this server and never shown again.
diff --git a/web/src/items.ts b/web/src/items.ts index 4417aee..c95af5d 100644 --- a/web/src/items.ts +++ b/web/src/items.ts @@ -134,7 +134,7 @@ function renderEntries(){ // Said only for a search, where the words typed are why; an empty feed or tab speaks for itself. if(S.q) box.innerHTML='

Nothing matches that search.

'; // A new Signal is empty while Jev reads, which would otherwise look broken. - else if(sg&&S.filter==='all') box.innerHTML='

Nothing has come through yet. Jev reads new items as your feeds are checked.

'; + else if(sg&&S.filter==='all') box.innerHTML='

Nothing has come through yet. New items are read as your feeds are checked.

'; return; } for(const e of S.entries) box.appendChild(epEl(e)); diff --git a/web/src/signals.ts b/web/src/signals.ts index 6c99da6..53519e7 100644 --- a/web/src/signals.ts +++ b/web/src/signals.ts @@ -139,7 +139,7 @@ function signalModal(sg?: Signal){
@@ -147,10 +147,10 @@ function signalModal(sg?: Signal){
${chipsHTML(sg?.keywords||[],'sgkwin')}
- TypeSafe's Jev reads every item in the feeds you subscribe to, the last 30 - days of them and each new one, and pulls out the ones about this subject: its name, these - words and your sentence together. The rest stays noise, in its own feed. Each item is one - call on your TypeSafe key.${sg?' A change here reads everything again.':''}
+ Every item in the feeds you subscribe to is read, the last 30 days of them + and each new one, and the ones about this subject are pulled out: its name, these words and + your sentence together. The rest stays noise, in its own feed. Each item is one call on your + TypeSafe key.${sg?' A change here reads everything again.':''} ${nokey?'

Add your TypeSafe key in Settings first: without one, nothing is picked out.

':''}
`); @@ -179,14 +179,15 @@ function signalModal(sg?: Signal){ }; $('#sgnoicon').onclick=()=>{ icon=null; drawPick(); $('#sgpicker').hidden=true; }; $('#sgjev').onclick=async()=>{ - const b=$('#sgjev'); b.disabled=true; b.textContent='Asking Jev…'; + // A spinner while it is picked: what picks it is nobody's concern. + const b=$('#sgjev'); b.disabled=true; b.classList.add('busy'); b.setAttribute('aria-busy','true'); try{ const got=await jevIcon($('#sgname').value.trim(),chipWords($('#sgkw')).filter((w): w is string=>!!w), $('#sgdesc').value.trim()||null); if(got){ icon=got; drawPick(); $('#sgpicker').hidden=true; $('#sgicon').setAttribute('aria-expanded','false'); } else toast('No icon matches those words'); }catch(e){ toast(why(e),true); } - finally{ b.disabled=false; b.textContent='Auto'; } + finally{ b.disabled=false; b.classList.remove('busy'); b.removeAttribute('aria-busy'); } }; $('#sgname').focus(); $('#sgsave').onclick=async()=>{ @@ -204,7 +205,7 @@ function signalModal(sg?: Signal){ if(!sg&&!icon) jevIcon(name,keywords,description) .then(got=>{ if(got) return api(`/api/signals/${r.id}`,{method:'PATCH',body:JSON.stringify({icon:got})}).then(refreshSignals); }) .catch(()=>{}); - closeModal(); toast(sg?'Saved. Jev reads everything again':'Listening. What Jev picks out shows up here'); + closeModal(); toast(sg?'Saved. Everything is read again':'Listening. What comes through shows up here'); // Open, so the new one is there to see. if(!expanded.has(SIGNALS)) toggleGroup(SIGNALS); await loadFeeds(true); selectFeed(':signal:'+(sg?sg.id:r.id));