simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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 }