Start working on unread articles

This commit is contained in:
Greg Shuflin 2025-02-05 05:29:40 -08:00
parent 0016ef97bb
commit 6fccda5827
5 changed files with 69 additions and 18 deletions

View File

@ -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<chrono::Utc>\",\n last_checked_time as \"last_checked_time: chrono::DateTime<chrono::Utc>\",\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<chrono::Utc>\",\n f.last_checked_time as \"last_checked_time: chrono::DateTime<chrono::Utc>\",\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"
}

View File

@ -21,6 +21,7 @@ pub struct Feed {
pub added_time: chrono::DateTime<chrono::Utc>,
pub last_checked_time: chrono::DateTime<chrono::Utc>,
pub categorization: Vec<String>,
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<chrono::Utc>",
last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>",
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<chrono::Utc>",
f.last_checked_time as "last_checked_time: chrono::DateTime<chrono::Utc>",
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::<Result<Vec<_>, 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::<String>::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
}
}

View File

@ -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<Entry>,
}
@ -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/<local_id>/state", data = "<state>")]

View File

@ -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;
}

View File

@ -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 <span class="feed-title-separator">-</span> ${feed.name}`;