//! Feed page: items from followed users, projects, and tags. use axum::extract::{Query, State}; use axum::response::IntoResponse; use serde::Deserialize; use tower_sessions::Session; use crate::{ auth::AuthUser, constants, db, error::Result, helpers::get_csrf_token, templates::FeedTemplate, types::DiscoverItem, AppState, }; /// Query parameters for the feed page. #[derive(Debug, Deserialize)] pub struct FeedQuery { pub page: Option, } /// Build a sliding window of page numbers for pagination controls. pub(super) fn build_pagination_range(current_page: u32, total_pages: u32) -> Vec { if total_pages <= constants::PAGINATION_WINDOW_SIZE { (1..=total_pages).collect() } else { let start = current_page.saturating_sub(2).max(1); let end = (start + 4).min(total_pages); let start = end.saturating_sub(4).max(1); (start..=end).collect() } } /// GET /feed: paginated feed of items from followed users, projects, and tags. #[tracing::instrument(skip_all, name = "feed::feed_page")] pub(super) async fn feed_page( State(state): State, session: Session, AuthUser(user): AuthUser, Query(query): Query, ) -> Result { let csrf_token = get_csrf_token(&session).await; let session_user = Some(user.clone()); let page = query.page.unwrap_or(1).max(1); // Widen to i64 BEFORE multiplying — `(page - 1) * FEED_PAGE_SIZE` in u32 // overflows for a large `?page=` (garbage offset in release, panic in debug). let offset = (page as i64 - 1) * constants::FEED_PAGE_SIZE as i64; let total_items = db::follows::count_followed_feed_items(&state.db, user.id).await? as u32; let total_pages = (total_items + constants::FEED_PAGE_SIZE - 1) / constants::FEED_PAGE_SIZE.max(1); let db_items = db::follows::get_followed_feed_items( &state.db, user.id, constants::FEED_PAGE_SIZE as i64, offset, ) .await?; let items: Vec = db_items.into_iter().map(DiscoverItem::from).collect(); // Compute the "showing X–Y" labels in i64 (saturating) to avoid the u32 // overflow `offset as u32 + FEED_PAGE_SIZE` would hit for a large `?page=`. let showing_start = if total_items == 0 { 0 } else { offset.saturating_add(1).clamp(0, u32::MAX as i64) as u32 }; let showing_end = offset .saturating_add(constants::FEED_PAGE_SIZE as i64) .min(total_items as i64) .clamp(0, u32::MAX as i64) as u32; let pagination_range = build_pagination_range(page, total_pages); Ok(FeedTemplate { csrf_token, session_user, items, total_items, current_page: page, total_pages, pagination_range, showing_start, showing_end, }) }