hyaenidae/src/feed.rs
2021-04-02 12:07:19 -05:00

251 lines
7 KiB
Rust

use crate::{
error::{Error, OptionExt},
middleware::UserProfile,
nav::NavState,
pagination::{Page, PageSource, Pagination},
views::SubmissionView,
ActixLoader, State,
};
use actix_web::{web, HttpResponse, Scope};
use hyaenidae_profiles::store::{File, Profile, Submission};
use hyaenidae_toolkit::Button;
use i18n_embed_fl::fl;
use std::collections::HashMap;
use uuid::Uuid;
pub(super) fn scope() -> Scope {
web::scope("/feed").route("", web::get().to(feed_page))
}
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[serde(untagged)]
enum PageQuery {
Min { min: Uuid },
Max { max: Uuid },
}
impl From<PageQuery> for PageSource {
fn from(p: PageQuery) -> Self {
match p {
PageQuery::Max { max } => PageSource::OlderThan(max),
PageQuery::Min { min } => PageSource::NewerThan(min),
}
}
}
async fn feed_page(
loader: ActixLoader,
profile: UserProfile,
page: Option<web::Query<PageQuery>>,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let view =
ViewFeedState::build(profile.0.id(), page.map(|p| p.into_inner().into()), &state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::feed::index(cursor, &loader, &view, &nav_state)
})
}
#[derive(Debug, Default)]
struct ViewFeedCache {
submissions: HashMap<Uuid, Submission>,
files: HashMap<Uuid, File>,
profiles: HashMap<Uuid, Profile>,
}
impl ViewFeedCache {
fn new() -> Self {
Self::default()
}
}
pub struct ViewFeedState {
cache: ViewFeedCache,
pub(crate) profile: Profile,
submission_page: Page,
}
impl ViewFeedState {
pub(crate) fn submissions<'a>(&'a self) -> impl Iterator<Item = SubmissionView<'a>> + 'a {
self.submission_page
.items
.iter()
.filter_map(move |submission_id| {
let submission = self.cache.submissions.get(submission_id)?;
let author = self.cache.profiles.get(&submission.profile_id())?;
let files = submission
.files()
.iter()
.filter_map(move |file_id| self.cache.files.get(file_id))
.collect();
Some(SubmissionView {
author,
submission,
files,
})
})
}
pub(crate) fn has_submissions(&self) -> bool {
!self.submission_page.items.is_empty()
}
pub(crate) fn has_nav(&self) -> bool {
self.submission_page.next.is_some() || self.submission_page.prev.is_some()
}
pub(crate) fn nav(&self, loader: &ActixLoader) -> Vec<Button> {
let mut buttons = vec![];
if let Some(id) = self.submission_page.prev {
buttons
.push(Button::secondary(&fl!(loader, "previous-button")).href(&self.min_path(id)));
}
if let Some(id) = self.submission_page.next {
buttons.push(Button::secondary(&fl!(loader, "next-button")).href(&self.max_path(id)));
}
buttons
}
fn min_path(&self, id: Uuid) -> String {
format!("/feed?min={}", id)
}
fn max_path(&self, id: Uuid) -> String {
format!("/feed?max={}", id)
}
async fn build(
profile_id: Uuid,
source: Option<PageSource>,
state: &State,
) -> Result<Self, Error> {
let store = state.profiles.clone();
let state = web::block(move || {
let mut cache = ViewFeedCache::new();
let profile = store.store.profiles.by_id(profile_id)?.req()?;
if let Some(file_id) = profile.icon() {
if !cache.files.contains_key(&file_id) {
let file = store.store.files.by_id(file_id)?.req()?;
cache.files.insert(file.id(), file);
}
}
if let Some(file_id) = profile.banner() {
if !cache.files.contains_key(&file_id) {
let file = store.store.files.by_id(file_id)?.req()?;
cache.files.insert(file.id(), file);
}
}
let submission_page = Page::from_pagination(
FeedPager {
profile_id,
store: &store,
cache: &mut cache,
},
24,
source,
);
let state = ViewFeedState {
cache,
profile,
submission_page,
};
Ok(state) as Result<Self, Error>
})
.await??;
Ok(state)
}
}
struct FeedPager<'b> {
profile_id: Uuid,
store: &'b hyaenidae_profiles::State,
cache: &'b mut ViewFeedCache,
}
impl<'b> Pagination for FeedPager<'b> {
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.view
.submissions
.older_than_for_profile(self.profile_id, max)
.filter_map(move |submission_id| self.cache_submission(submission_id)),
)
}
fn from_min<'a>(&'a mut self, min: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.view
.submissions
.newer_than_for_profile(self.profile_id, min)
.filter_map(move |submission_id| self.cache_submission(submission_id)),
)
}
fn from_start<'a>(&'a mut self) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.view
.submissions
.for_profile(self.profile_id)
.filter_map(move |submission_id| self.cache_submission(submission_id)),
)
}
}
impl<'a> FeedPager<'a> {
fn cache_submission(&mut self, submission_id: Uuid) -> Option<Uuid> {
if !self.cache.submissions.contains_key(&submission_id) {
let submission = self.store.store.submissions.by_id(submission_id).ok()??;
let mut file_ids = vec![];
if !self.cache.profiles.contains_key(&submission.profile_id()) {
let profile = self
.store
.store
.profiles
.by_id(submission.profile_id())
.ok()??;
file_ids.extend(profile.icon());
file_ids.extend(profile.banner());
self.cache.profiles.insert(profile.id(), profile);
}
file_ids.extend(submission.files());
for file_id in file_ids {
if !self.cache.files.contains_key(&file_id) {
let file = self.store.store.files.by_id(file_id).ok()??;
self.cache.files.insert(file.id(), file);
}
}
self.cache.submissions.insert(submission.id(), submission);
}
Some(submission_id)
}
}