A pinned item goes to the top of its list
order_sql takes pinned_first, which puts coalesce(s.flagged, 0) DESC ahead of the chosen sort, so pins lead every list in whatever order is asked for and on every page of it. Not when sorting by the pin column itself, where the direction is the point, and not for Currently Listening. Pinning now asks for the list again so the row moves at once, instead of redrawing it where it stood. Closes #35. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
29
src/db.rs
29
src/db.rs
@@ -694,7 +694,10 @@ fn scope_sql(feed_id: Option<&str>, user_param: u8) -> String {
|
||||
///
|
||||
/// ponytail: file type and size look at the item's first and largest file. The row shows the file
|
||||
/// it summarises, which is almost always that one; sort by that one if they ever disagree.
|
||||
pub fn order_sql(col: &str, dir: &str) -> String {
|
||||
/// `pinned_first` puts your pinned items above the rest, each part in the order asked for, so a
|
||||
/// pin keeps something at the top of its list (issue #35). Not when sorting by the pin itself,
|
||||
/// where the direction chosen is the point.
|
||||
pub fn order_sql(col: &str, dir: &str, pinned_first: bool) -> String {
|
||||
let expr = match col {
|
||||
"kept" => "coalesce(s.flagged, 0)",
|
||||
"title" => "lower(coalesce(e.title, ''))",
|
||||
@@ -704,7 +707,8 @@ pub fn order_sql(col: &str, dir: &str) -> String {
|
||||
_ => "coalesce(e.published, e.first_seen)",
|
||||
};
|
||||
let dir = if dir == "asc" { "ASC" } else { "DESC" };
|
||||
format!("{expr} {dir}, coalesce(e.published, e.first_seen) DESC, e.rowid DESC")
|
||||
let pins = if pinned_first && col != "kept" { "coalesce(s.flagged, 0) DESC, " } else { "" };
|
||||
format!("{pins}{expr} {dir}, coalesce(e.published, e.first_seen) DESC, e.rowid DESC")
|
||||
}
|
||||
|
||||
/// Which slice of a feed the UI is asking for.
|
||||
@@ -1663,7 +1667,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
let order = |col: &str, dir: &str| -> Vec<String> {
|
||||
db.entries_in(1, None, Filter::All, None, 0, 50, &order_sql(col, dir))
|
||||
db.entries_in(1, None, Filter::All, None, 0, 50, &order_sql(col, dir, false))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.guid)
|
||||
@@ -1677,9 +1681,20 @@ mod tests {
|
||||
assert_eq!(order("published", "desc"), ["c", "b", "a"]);
|
||||
db.set_entry_flag(1, "f", "a", EntryFlag::Flagged, true).unwrap();
|
||||
assert_eq!(order("kept", "desc")[0], "a");
|
||||
// Pinned first: the pinned banana tops every sort, the rest in the order asked for.
|
||||
let pinned = |col: &str, dir: &str| -> Vec<String> {
|
||||
db.entries_in(1, None, Filter::All, None, 0, 50, &order_sql(col, dir, true))
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|e| e.guid)
|
||||
.collect()
|
||||
};
|
||||
assert_eq!(pinned("published", "desc"), ["a", "c", "b"]);
|
||||
assert_eq!(pinned("title", "desc"), ["a", "c", "b"]);
|
||||
assert_eq!(pinned("kept", "asc")[2], "a", "sorting by the pin itself keeps its direction");
|
||||
// An unknown column or direction is newest first; the name itself never reaches the SQL.
|
||||
assert_eq!(order("title; DROP TABLE entries", "sideways"), ["c", "b", "a"]);
|
||||
assert!(!order_sql("x'; --", "asc").contains("x'"));
|
||||
assert!(!order_sql("x'; --", "asc", false).contains("x'"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1728,7 +1743,7 @@ mod tests {
|
||||
// Starring and position are just as private.
|
||||
db.set_entry_flag(1, "f", "b", EntryFlag::Flagged, true).unwrap();
|
||||
db.set_position(2, "f", "b", 42, Some(600)).unwrap();
|
||||
let order = order_sql("published", "desc");
|
||||
let order = order_sql("published", "desc", false);
|
||||
let page = |user| db.entries_in(user, Some("f"), Filter::All, None, 0, 50, &order).unwrap();
|
||||
let (ray, sam) = (page(1), page(2));
|
||||
let ray_b = ray.iter().find(|e| e.guid == "b").unwrap();
|
||||
@@ -1898,7 +1913,7 @@ mod tests {
|
||||
|
||||
for f in [Filter::All, Filter::Unread, Filter::Downloaded, Filter::Flagged, Filter::InProgress] {
|
||||
// Both paths must run without erroring, and agree with each other.
|
||||
let order = order_sql("published", "desc");
|
||||
let order = order_sql("published", "desc", false);
|
||||
let rows = db.entries_in(7, Some("f"), f, None, 0, 50, &order).unwrap();
|
||||
let n = db.count_in(7, Some("f"), f, None).unwrap();
|
||||
assert_eq!(rows.len() as i64, n, "{f:?} count disagrees with the page");
|
||||
@@ -1917,7 +1932,7 @@ mod tests {
|
||||
"search is case-insensitive and covers the description");
|
||||
|
||||
// Currently Listening: started, not finished, and not just an accidental tap.
|
||||
let order = order_sql("published", "desc");
|
||||
let order = order_sql("published", "desc", false);
|
||||
let listening = || {
|
||||
let rows = db.entries_in(7, Some("f"), Filter::InProgress, None, 0, 50, &order).unwrap();
|
||||
rows.into_iter().map(|e| e.guid).collect::<Vec<_>>()
|
||||
|
||||
Reference in New Issue
Block a user