diff --git a/crates/ipx/CHANGELOG.md b/crates/ipx/CHANGELOG.md index 7876894..c0f9c2a 100644 --- a/crates/ipx/CHANGELOG.md +++ b/crates/ipx/CHANGELOG.md @@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Signals: a feed of your own, picked out of the noise of everything you subscribe to, holding the items TypeSafe's Jev says are about the subject you give it: a name, words, and a sentence of your own if they are not enough, from the last 30 days and every new one, judged on your own TypeSafe key (`/api/signals`, and `typesafe_key` on `/api/me`) (#254). +- Signals: a feed of your own, picked out of the noise of everything you subscribe to, holding the items TypeSafe's Jev says are about the subject you give it: a name, words, and a sentence of your own if they are not enough, from the last 30 days and every new one, judged on your own TypeSafe key, each with an icon of its own if you like (`/api/signals`, and `typesafe_key` on `/api/me`) (#254). ### Changed diff --git a/crates/ipx/src/db.rs b/crates/ipx/src/db.rs index 46b637b..4fdb7a3 100644 --- a/crates/ipx/src/db.rs +++ b/crates/ipx/src/db.rs @@ -113,6 +113,10 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> { orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?; } } + if orm.execute_unprepared("SELECT icon FROM signals LIMIT 0").await.is_err() { + let sql = "ALTER TABLE signals ADD COLUMN icon TEXT"; + orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?; + } if orm.execute_unprepared("SELECT pause_ms FROM deadair_prefs LIMIT 0").await.is_err() { let sql = "ALTER TABLE deadair_prefs ADD COLUMN pause_ms BIGINT"; orm.execute_unprepared(sql).await.with_context(|| sql.to_owned())?; @@ -2547,9 +2551,19 @@ pub struct SignalRow { pub name: String, pub keywords: Vec, pub description: Option, + pub icon: Option, pub unread: i64, } +/// What a Signal is changed to: each field None to leave it as it is. +#[derive(Default)] +pub struct SignalChange<'a> { + pub name: Option<&'a str>, + pub words: Option<&'a [String]>, + pub description: Option>, + pub icon: Option>, +} + /// One Signal's question about an item: what it is called, its words, and its owner's sentence. #[derive(Debug, Clone)] pub struct Ask { @@ -2591,7 +2605,7 @@ impl Db { let user = a.p(user_id); let one = id.map(|i| format!("AND g.id = {}", a.p(i))).unwrap_or_default(); let sql = format!( - "SELECT g.id, g.name, g.keywords, g.description, + "SELECT g.id, g.name, g.keywords, g.description, g.icon, (SELECT count(*) FROM signal_verdicts v JOIN entries e ON e.feed_id = v.feed_id AND e.guid = v.guid JOIN subscriptions s ON s.user_id = g.user_id AND s.feed_id = v.feed_id @@ -2609,6 +2623,7 @@ impl Db { name: r.try_get("", "name")?, keywords: keywords(r.try_get("", "keywords")?).unwrap_or_default(), description: r.try_get("", "description")?, + icon: r.try_get("", "icon")?, unread: r.try_get("", "unread")?, }) }) @@ -2629,17 +2644,11 @@ impl Db { Ok(m.id) } - /// Renames a Signal, or gives it new words or a new sentence. Either of those puts every item - /// to Jev again, since what it said was about the old ones; a new name does too, since the - /// question is about the name (#259). False for a Signal that is not this person's. - pub async fn set_signal( - &self, - user_id: i64, - id: i64, - name: Option<&str>, - words: Option<&[String]>, - description: Option>, - ) -> Result { + /// Changes a Signal. A new name, words or sentence puts every item to Jev again, since what it + /// said was about the old question (#259); a new icon does not. False for a Signal that is not + /// this person's. + pub async fn set_signal(&self, user_id: i64, id: i64, change: SignalChange<'_>) -> Result { + let SignalChange { name, words, description, icon } = change; if self.signal(user_id, id).await?.is_none() { return Ok(false); } @@ -2653,8 +2662,13 @@ impl Db { if let Some(d) = description { m.description = Set(d.map(str::to_owned)); } + if let Some(i) = icon { + m.icon = Set(i.map(str::to_owned)); + } if name.is_some() || words.is_some() || description.is_some() { signal_verdicts::Entity::delete_many().filter(signal_verdicts::Column::SignalId.eq(id)).exec(&self.orm).await?; + } + if name.is_some() || words.is_some() || description.is_some() || icon.is_some() { m.update(&self.orm).await?; } Ok(true) @@ -3396,15 +3410,19 @@ mod tests { assert_eq!(db.signal(1, f1).await.unwrap().unwrap().unread, 0, "read in its feed is read in the Signal"); assert!(db.signal(2, f1).await.unwrap().is_none(), "someone else's Signal is not found"); - assert!(!db.set_signal(2, f1, Some("mine"), None, None).await.unwrap()); + assert!(!db.set_signal(2, f1, SignalChange { name: Some("mine"), ..Default::default() }).await.unwrap()); assert!(!db.remove_signal(2, f1).await.unwrap()); - db.set_signal(1, f1, None, Some(&words(&["f1"])), None).await.unwrap(); + db.set_signal(1, f1, SignalChange { words: Some(&words(&["f1"])), ..Default::default() }).await.unwrap(); assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 0, "new keywords are judged afresh"); db.record_verdicts("f", "x", &[(f1, true)]).await.unwrap(); - db.set_signal(1, f1, None, None, Some(Some("Grand prix racing"))).await.unwrap(); + db.set_signal(1, f1, SignalChange { description: Some(Some("Grand prix racing")), ..Default::default() }).await.unwrap(); assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 0, "so is a new sentence"); assert_eq!(db.signal(1, f1).await.unwrap().unwrap().description.as_deref(), Some("Grand prix racing")); + db.record_verdicts("f", "x", &[(f1, true)]).await.unwrap(); + db.set_signal(1, f1, SignalChange { icon: Some(Some("solid/flag-checkered")), ..Default::default() }).await.unwrap(); + assert_eq!(db.signal_count(1, f1, Filter::All, None).await.unwrap(), 1, "a new icon asks nothing again"); + assert_eq!(db.signal(1, f1).await.unwrap().unwrap().icon.as_deref(), Some("solid/flag-checkered")); assert!(db.remove_signal(1, food).await.unwrap()); db.record_verdicts("f", "x", &[(food, true)]).await.unwrap(); assert_eq!(db.signals(1).await.unwrap().len(), 1, "a verdict for a removed Signal is not kept"); diff --git a/crates/ipx/src/entity.rs b/crates/ipx/src/entity.rs index 37e19f9..daea149 100644 --- a/crates/ipx/src/entity.rs +++ b/crates/ipx/src/entity.rs @@ -308,6 +308,10 @@ pub mod signals { /// are not enough (#259). #[sea_orm(column_type = "Text", nullable)] pub description: Option, + /// Its icon, a Font Awesome Free one as `style/name` (`solid/robot`), in place of its + /// initials (#261). + #[sea_orm(column_type = "Text", nullable)] + pub icon: Option, pub created: i64, } diff --git a/crates/ipx/src/web.rs b/crates/ipx/src/web.rs index 321a091..f6ff6fc 100644 --- a/crates/ipx/src/web.rs +++ b/crates/ipx/src/web.rs @@ -429,6 +429,24 @@ struct SignalBody { /// What its owner means by it, in their words: a string sets it, null or empty clears it. #[serde(default, deserialize_with = "double_option")] description: Option>, + /// A Font Awesome Free icon, `solid/robot`: a string sets it, null or empty clears it. + #[serde(default, deserialize_with = "double_option")] + icon: Option>, +} + +/// The page puts the name into an address (`/icons/