feed.rs (4993B)
1 use askama::Template; 2 use axum::{ 3 extract::{Query, State}, 4 response::Html, 5 }; 6 use axum_extra::extract::cookie::CookieJar; 7 use serde::Deserialize; 8 9 use crate::database::{Category, StoryWithMeta}; 10 use crate::database::{self, TARGET_TYPE_STORY}; 11 use crate::error::AppError; 12 use crate::state::AppState; 13 use crate::templates::{ApplicationTemplate, FeedTemplate}; 14 15 const PAGE_LIMIT: usize = 30; 16 17 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 18 pub enum FeedSort { 19 Hot, 20 New, 21 Top, 22 } 23 24 impl Default for FeedSort { 25 fn default() -> Self { 26 FeedSort::Hot 27 } 28 } 29 30 #[derive(Debug, Deserialize)] 31 pub struct FeedQuery { 32 #[serde(default)] 33 pub page: usize, 34 pub tag: Option<i64>, 35 } 36 37 async fn fetch_feed( 38 state: &AppState, 39 jar: &CookieJar, 40 query: &FeedQuery, 41 sort: FeedSort, 42 ) -> Result<(Vec<StoryWithMeta>, Vec<Category>, bool), AppError> { 43 let offset = query.page * PAGE_LIMIT; 44 let user_id = state.get_user_id(&jar).await; 45 let tag_id = query.tag; 46 let page = query.page; 47 48 let db = state.db(); 49 let categories = db.get_categories(20).await?; 50 51 let all_stories = match tag_id { 52 Some(tid) => db.get_stories_by_tag(tid, PAGE_LIMIT + 1, offset).await?, 53 None => match sort { 54 FeedSort::Hot => db.get_stories_hot(PAGE_LIMIT + 1, offset).await?, 55 FeedSort::New => db.get_stories_new(PAGE_LIMIT + 1, offset).await?, 56 FeedSort::Top => db.get_stories_top(PAGE_LIMIT + 1, offset).await?, 57 }, 58 }; 59 60 let stories: Vec<_> = all_stories.into_iter().take(PAGE_LIMIT).collect(); 61 let story_ids: Vec<i64> = stories.iter().map(|s| s.id).collect(); 62 63 let voted_ids = if let Some(uid) = user_id { 64 db.get_user_votes(uid, TARGET_TYPE_STORY, &story_ids) 65 .await 66 .unwrap_or_default() 67 } else { 68 vec![] 69 }; 70 71 let meta = db.enrich_stories(&story_ids).await.unwrap_or_default(); 72 73 let reached_end = stories.len() <= PAGE_LIMIT; 74 75 // O(1) lookups instead of O(n) scans 76 let meta_map: std::collections::HashMap<i64, &database::StoryMeta> = 77 meta.iter().map(|m| (m.story_id, m)).collect(); 78 let voted_set: std::collections::HashSet<i64> = 79 voted_ids.into_iter().collect(); 80 81 let mut enriched_stories = Vec::with_capacity(stories.len()); 82 let base_rank = page * PAGE_LIMIT; 83 84 for (idx, story) in stories.into_iter().enumerate() { 85 let story_meta = meta_map.get(&story.id); 86 let tags: Vec<Category> = story_meta 87 .map(|m| m.tags.iter().map(|name| Category { id: 0, name: Some(name.clone()) }).collect()) 88 .unwrap_or_default(); 89 let author_username = story_meta.and_then(|m| m.author_username.clone()); 90 91 let time_ago = database::time_ago(story.published); 92 let user_voted = voted_set.contains(&story.id); 93 let rank = base_rank + idx + 1; 94 95 enriched_stories.push(StoryWithMeta::from_story( 96 story, 97 tags, 98 author_username, 99 time_ago, 100 user_voted, 101 rank, 102 )); 103 } 104 105 Ok((enriched_stories, categories, reached_end)) 106 } 107 108 pub async fn show_feed( 109 State(state): State<AppState>, 110 jar: CookieJar, 111 Query(query): Query<FeedQuery>, 112 ) -> Result<Html<String>, AppError> { 113 let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::Hot).await?; 114 let is_logged_in = state.get_user_id(&jar).await.is_some(); 115 116 let feed = FeedTemplate { 117 stories, 118 tags, 119 page: query.page, 120 tag_id: query.tag, 121 reached_end, 122 sort: "hot".to_string(), 123 is_logged_in, 124 }; 125 126 let app = ApplicationTemplate { 127 content: feed.render()?, 128 }; 129 Ok(Html(app.render()?)) 130 } 131 132 pub async fn show_new( 133 State(state): State<AppState>, 134 jar: CookieJar, 135 Query(query): Query<FeedQuery>, 136 ) -> Result<Html<String>, AppError> { 137 let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::New).await?; 138 let is_logged_in = state.get_user_id(&jar).await.is_some(); 139 140 let feed = FeedTemplate { 141 stories, 142 tags, 143 page: query.page, 144 tag_id: query.tag, 145 reached_end, 146 sort: "new".to_string(), 147 is_logged_in, 148 }; 149 150 let app = ApplicationTemplate { 151 content: feed.render()?, 152 }; 153 Ok(Html(app.render()?)) 154 } 155 156 pub async fn show_top( 157 State(state): State<AppState>, 158 jar: CookieJar, 159 Query(query): Query<FeedQuery>, 160 ) -> Result<Html<String>, AppError> { 161 let (stories, tags, reached_end) = fetch_feed(&state, &jar, &query, FeedSort::Top).await?; 162 let is_logged_in = state.get_user_id(&jar).await.is_some(); 163 164 let feed = FeedTemplate { 165 stories, 166 tags, 167 page: query.page, 168 tag_id: query.tag, 169 reached_end, 170 sort: "top".to_string(), 171 is_logged_in, 172 }; 173 174 let app = ApplicationTemplate { 175 content: feed.render()?, 176 }; 177 Ok(Html(app.render()?)) 178 }