A Signal's own icon, from all of Font Awesome Free, searchable (#261)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 18:36:08 +00:00
parent 9e5dcbb8c4
commit b18c92d1f5
15 changed files with 269 additions and 31 deletions

View File

@@ -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<String>,
pub description: Option<String>,
pub icon: Option<String>,
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<Option<&'a str>>,
pub icon: Option<Option<&'a str>>,
}
/// 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<Option<&str>>,
) -> Result<bool> {
/// 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<bool> {
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");