From 6fccda5827d4877e81ff5e6fa1859dec71c0e74a Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Wed, 5 Feb 2025 05:29:40 -0800 Subject: [PATCH] Start working on unread articles --- ...f1071db67502d23f7d6d98adc3da2241d57b.json} | 10 ++++-- src/feeds.rs | 35 +++++++++++-------- src/poll.rs | 13 +++++-- static/css/components/sidebar.css | 12 +++++++ static/js/app.js | 17 +++++++++ 5 files changed, 69 insertions(+), 18 deletions(-) rename .sqlx/{query-7c3a826ac9b9105554bed433100db0435c55fca5c239b4e5f58380e14697c3a0.json => query-d73f4bc0e8844f0a5dd4f897a1a1f1071db67502d23f7d6d98adc3da2241d57b.json} (50%) diff --git a/.sqlx/query-7c3a826ac9b9105554bed433100db0435c55fca5c239b4e5f58380e14697c3a0.json b/.sqlx/query-d73f4bc0e8844f0a5dd4f897a1a1f1071db67502d23f7d6d98adc3da2241d57b.json similarity index 50% rename from .sqlx/query-7c3a826ac9b9105554bed433100db0435c55fca5c239b4e5f58380e14697c3a0.json rename to .sqlx/query-d73f4bc0e8844f0a5dd4f897a1a1f1071db67502d23f7d6d98adc3da2241d57b.json index 5ef81ad..5f2570d 100644 --- a/.sqlx/query-7c3a826ac9b9105554bed433100db0435c55fca5c239b4e5f58380e14697c3a0.json +++ b/.sqlx/query-d73f4bc0e8844f0a5dd4f897a1a1f1071db67502d23f7d6d98adc3da2241d57b.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT \n feed_id as \"feed_id: String\",\n name,\n url,\n user_id as \"user_id: String\",\n added_time as \"added_time: chrono::DateTime\",\n last_checked_time as \"last_checked_time: chrono::DateTime\",\n categorization as \"categorization: JsonValue\"\n FROM feeds\n WHERE user_id = ?\n ORDER BY name ASC\n ", + "query": "\n SELECT \n f.feed_id as \"feed_id: String\",\n f.name,\n f.url,\n f.user_id as \"user_id: String\",\n f.added_time as \"added_time: chrono::DateTime\",\n f.last_checked_time as \"last_checked_time: chrono::DateTime\",\n f.categorization as \"categorization: JsonValue\",\n COALESCE(SUM(CASE WHEN e.id IS NOT NULL AND e.marked_read IS NULL THEN 1 ELSE 0 END), 0) as \"unread_count!: i64\"\n FROM feeds f\n LEFT JOIN feed_entries e ON f.feed_id = e.feed_id\n WHERE f.user_id = ?\n GROUP BY f.feed_id, f.name, f.url, f.user_id, f.added_time, f.last_checked_time, f.categorization\n ORDER BY f.name ASC\n ", "describe": { "columns": [ { @@ -37,6 +37,11 @@ "name": "categorization: JsonValue", "ordinal": 6, "type_info": "Null" + }, + { + "name": "unread_count!: i64", + "ordinal": 7, + "type_info": "Int" } ], "parameters": { @@ -49,8 +54,9 @@ false, false, false, + false, false ] }, - "hash": "7c3a826ac9b9105554bed433100db0435c55fca5c239b4e5f58380e14697c3a0" + "hash": "d73f4bc0e8844f0a5dd4f897a1a1f1071db67502d23f7d6d98adc3da2241d57b" } diff --git a/src/feeds.rs b/src/feeds.rs index e1c9c79..4e402e7 100644 --- a/src/feeds.rs +++ b/src/feeds.rs @@ -21,6 +21,7 @@ pub struct Feed { pub added_time: chrono::DateTime, pub last_checked_time: chrono::DateTime, pub categorization: Vec, + pub unread_count: i64, } impl Feed { @@ -34,6 +35,7 @@ impl Feed { added_time: now, last_checked_time: chrono::DateTime::UNIX_EPOCH, categorization: Vec::new(), + unread_count: 0, } } @@ -135,16 +137,19 @@ pub async fn list_feeds( let query = sqlx::query!( r#" SELECT - feed_id as "feed_id: String", - name, - url, - user_id as "user_id: String", - added_time as "added_time: chrono::DateTime", - last_checked_time as "last_checked_time: chrono::DateTime", - categorization as "categorization: JsonValue" - FROM feeds - WHERE user_id = ? - ORDER BY name ASC + f.feed_id as "feed_id: String", + f.name, + f.url, + f.user_id as "user_id: String", + f.added_time as "added_time: chrono::DateTime", + f.last_checked_time as "last_checked_time: chrono::DateTime", + f.categorization as "categorization: JsonValue", + COALESCE(SUM(CASE WHEN e.id IS NOT NULL AND e.marked_read IS NULL THEN 1 ELSE 0 END), 0) as "unread_count!: i64" + FROM feeds f + LEFT JOIN feed_entries e ON f.feed_id = e.feed_id + WHERE f.user_id = ? + GROUP BY f.feed_id, f.name, f.url, f.user_id, f.added_time, f.last_checked_time, f.categorization + ORDER BY f.name ASC "#, user_id ) @@ -187,6 +192,7 @@ pub async fn list_feeds( added_time: row.added_time, last_checked_time: row.last_checked_time, categorization, + unread_count: row.unread_count, }) }) .collect::, Status>>()?; @@ -291,6 +297,7 @@ pub async fn get_feed( added_time: row.added_time, last_checked_time: row.last_checked_time, categorization, + unread_count: 0, })) } @@ -311,13 +318,13 @@ mod tests { assert_eq!(feed.user_id, user_id); assert_eq!(feed.categorization, Vec::::new()); assert_eq!(feed.last_checked_time, chrono::DateTime::UNIX_EPOCH); - + // Feed ID should be a valid UUID - assert!(feed.feed_id.to_string().len() == 36); // UUID string length - + assert!(feed.feed_id.to_string().len() == 36); // UUID string length + // Added time should be recent (within last few seconds) let now = chrono::Utc::now(); let time_diff = now - feed.added_time; - assert!(time_diff.num_seconds().abs() < 5); // Allow 5 second difference + assert!(time_diff.num_seconds().abs() < 5); // Allow 5 second difference } } diff --git a/src/poll.rs b/src/poll.rs index 46de68f..394d338 100644 --- a/src/poll.rs +++ b/src/poll.rs @@ -17,6 +17,7 @@ const MAX_ENTRIES_PER_FEED: i32 = 30; #[serde(crate = "rocket::serde")] pub struct FeedPollResponse { count: usize, + unread_count: usize, entries: Vec, } @@ -244,9 +245,17 @@ pub async fn poll_feed( }; let count = entries.len(); - info!("Returning {} entries for feed {}", count, feed_id); + let unread_count = entries.iter().filter(|e| e.marked_read.is_none()).count(); + info!( + "Returning {} entries ({} unread) for feed {}", + count, unread_count, feed_id + ); - Ok(Json(FeedPollResponse { count, entries })) + Ok(Json(FeedPollResponse { + count, + unread_count, + entries, + })) } #[patch("/entries//state", data = "")] diff --git a/static/css/components/sidebar.css b/static/css/components/sidebar.css index 4aacf31..bff3b4d 100644 --- a/static/css/components/sidebar.css +++ b/static/css/components/sidebar.css @@ -100,4 +100,16 @@ padding: 1rem; background-color: rgba(244, 63, 63, 0.1); border-radius: 4px; +} + +.feed-unread-count { + display: none; + background-color: var(--accent-color); + color: white; + border-radius: 10px; + padding: 2px 6px; + font-size: 0.8rem; + margin-left: 8px; + min-width: 20px; + text-align: center; } \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index 8eb4674..a8a2615 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -148,6 +148,15 @@ function openFeed(feed) { spinner.className = 'feed-spinner'; name.appendChild(spinner); + // Create unread count element + const unreadCount = document.createElement('span'); + unreadCount.className = 'feed-unread-count'; + if (feed.unread_count > 0) { + unreadCount.textContent = feed.unread_count; + unreadCount.style.display = 'inline'; + } + name.appendChild(unreadCount); + name.onclick = async () => { name.classList.add('loading'); try { @@ -158,6 +167,14 @@ function openFeed(feed) { const data = await response.json(); console.log('Feed poll response:', data); renderFeedEntries(data.entries); + // Update the unread count + feed.unread_count = data.unread_count; + if (feed.unread_count > 0) { + unreadCount.textContent = feed.unread_count; + unreadCount.style.display = 'inline'; + } else { + unreadCount.style.display = 'none'; + } // Update the top bar title const topBarTitle = document.querySelector('.top-bar-title'); topBarTitle.innerHTML = `RSS Reader - ${feed.name}`;