Skip to main content

max / makenotwork

2.8 KB · 91 lines History Blame Raw
1 //! Feed page: items from followed users, projects, and tags.
2
3 use axum::extract::{Query, State};
4 use axum::response::IntoResponse;
5 use serde::Deserialize;
6 use tower_sessions::Session;
7
8 use crate::{
9 auth::AuthUser,
10 constants,
11 db,
12 error::Result,
13 helpers::get_csrf_token,
14 templates::FeedTemplate,
15 types::DiscoverItem,
16 AppState,
17 };
18
19 /// Query parameters for the feed page.
20 #[derive(Debug, Deserialize)]
21 pub struct FeedQuery {
22 pub page: Option<u32>,
23 }
24
25 /// Build a sliding window of page numbers for pagination controls.
26 pub(super) fn build_pagination_range(current_page: u32, total_pages: u32) -> Vec<u32> {
27 if total_pages <= constants::PAGINATION_WINDOW_SIZE {
28 (1..=total_pages).collect()
29 } else {
30 let start = current_page.saturating_sub(2).max(1);
31 let end = (start + 4).min(total_pages);
32 let start = end.saturating_sub(4).max(1);
33 (start..=end).collect()
34 }
35 }
36
37 /// GET /feed: paginated feed of items from followed users, projects, and tags.
38 #[tracing::instrument(skip_all, name = "feed::feed_page")]
39 pub(super) async fn feed_page(
40 State(state): State<AppState>,
41 session: Session,
42 AuthUser(user): AuthUser,
43 Query(query): Query<FeedQuery>,
44 ) -> Result<impl IntoResponse> {
45 let csrf_token = get_csrf_token(&session).await;
46 let session_user = Some(user.clone());
47
48 let page = query.page.unwrap_or(1).max(1);
49 // Widen to i64 BEFORE multiplying — `(page - 1) * FEED_PAGE_SIZE` in u32
50 // overflows for a large `?page=` (garbage offset in release, panic in debug).
51 let offset = (page as i64 - 1) * constants::FEED_PAGE_SIZE as i64;
52
53 let total_items = db::follows::count_followed_feed_items(&state.db, user.id).await? as u32;
54 let total_pages = (total_items + constants::FEED_PAGE_SIZE - 1) / constants::FEED_PAGE_SIZE.max(1);
55
56 let db_items = db::follows::get_followed_feed_items(
57 &state.db,
58 user.id,
59 constants::FEED_PAGE_SIZE as i64,
60 offset,
61 )
62 .await?;
63
64 let items: Vec<DiscoverItem> = db_items.into_iter().map(DiscoverItem::from).collect();
65
66 // Compute the "showing X–Y" labels in i64 (saturating) to avoid the u32
67 // overflow `offset as u32 + FEED_PAGE_SIZE` would hit for a large `?page=`.
68 let showing_start = if total_items == 0 {
69 0
70 } else {
71 offset.saturating_add(1).clamp(0, u32::MAX as i64) as u32
72 };
73 let showing_end = offset
74 .saturating_add(constants::FEED_PAGE_SIZE as i64)
75 .min(total_items as i64)
76 .clamp(0, u32::MAX as i64) as u32;
77 let pagination_range = build_pagination_range(page, total_pages);
78
79 Ok(FeedTemplate {
80 csrf_token,
81 session_user,
82 items,
83 total_items,
84 current_page: page,
85 total_pages,
86 pagination_range,
87 showing_start,
88 showing_end,
89 })
90 }
91