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

455 lines
12 KiB
Rust

use crate::{
error::Error,
extensions::ProfileExt,
middleware::UserProfile,
nav::NavState,
pagination::{Page, PageNum, PageSource, Pagination, SearchPage, SearchPagination},
views::ProfileView,
ActixLoader, State,
};
use actix_web::{web, HttpRequest, HttpResponse, Scope};
use hyaenidae_profiles::store::{File, Profile};
use hyaenidae_toolkit::{Button, TextInput};
use i18n_embed_fl::fl;
use std::collections::HashMap;
use uuid::Uuid;
const PER_PAGE: usize = 8;
pub(super) fn scope() -> Scope {
web::scope("/discover").route("", web::get().to(profile_list))
}
async fn profile_list(
loader: ActixLoader,
req: HttpRequest,
profile: UserProfile,
page: Option<web::Query<ProfilePage>>,
search: Option<web::Query<Search>>,
page_num: Option<web::Query<PageNumQuery>>,
nav_state: NavState,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
let page = page.map(|query| query.into_inner().into());
let page_num = page_num.map(|query| query.into_inner().into());
let search = search.and_then(|search_query| {
if search_query.0.term.trim().is_empty() {
None
} else {
Some(search_query.0.term.trim().to_owned())
}
});
let state = if let Some(term) = search {
ProfileListState::search(
req.path().to_owned(),
profile.0.id(),
term,
page_num,
&state,
)
.await?
} else {
ProfileListState::build(req.path().to_owned(), profile.0.id(), page, &state).await?
};
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::profiles::discover(cursor, &loader, &state, &nav_state)
})
}
#[derive(Debug)]
enum PageKind {
Linear(Page),
Search(SearchPage),
}
#[derive(Debug)]
pub struct ProfileListState {
path: String,
cache: ProfileCache,
profiles: PageKind,
}
#[derive(Clone, Copy, Debug, serde::Deserialize)]
#[serde(untagged)]
enum ProfilePage {
Max { max: Uuid },
Min { min: Uuid },
}
#[derive(Clone, Copy, Debug, serde::Deserialize)]
struct PageNumQuery {
page: usize,
}
#[derive(Clone, Debug, serde::Deserialize)]
struct Search {
term: String,
}
#[derive(Debug, Default)]
struct ProfileCache {
profiles: HashMap<Uuid, Profile>,
files: HashMap<Uuid, File>,
blocks: HashMap<Uuid, bool>,
following: HashMap<Uuid, bool>,
follow_requested: HashMap<Uuid, bool>,
}
struct ProfilePager<'b> {
cache: &'b mut ProfileCache,
store: &'b hyaenidae_profiles::State,
viewer: Uuid,
}
impl PageKind {
fn items(&self) -> &[Uuid] {
match self {
PageKind::Linear(page) => &page.items,
PageKind::Search(page) => &page.items,
}
}
}
impl ProfileListState {
pub(crate) fn search_path(&self) -> &str {
&self.path
}
pub(crate) fn search_input(&self, loader: &ActixLoader) -> TextInput {
let input =
TextInput::new("term").placeholder(&fl!(loader, "discover-users-search-placeholder"));
match &self.profiles {
PageKind::Search(search) => input.value(&search.term),
_ => input,
}
}
pub(crate) fn profiles<'a>(&'a self) -> impl Iterator<Item = ProfileView<'a>> + 'a {
self.profiles.items().iter().filter_map(move |profile_id| {
let profile = self.cache.profiles.get(profile_id)?;
let icon = profile
.icon()
.and_then(|file_id| self.cache.files.get(&file_id));
let banner = profile
.banner()
.and_then(|file_id| self.cache.files.get(&file_id));
Some(ProfileView {
profile,
icon,
banner,
})
})
}
pub(crate) fn profile_buttons<'a>(
&'a self,
loader: &ActixLoader,
view: ProfileView<'a>,
) -> Vec<Button> {
let mut buttons = vec![];
if self
.cache
.follow_requested
.get(&view.profile.id())
.map(|b| *b)
.unwrap_or(false)
{
buttons.push(
Button::primary(&fl!(loader, "cancel-follow-button"))
.form(&view.profile.unfollow_path()),
);
} else if self
.cache
.following
.get(&view.profile.id())
.map(|b| *b)
.unwrap_or(false)
{
buttons.push(
Button::primary(&fl!(loader, "unfollow-button"))
.form(&view.profile.unfollow_path()),
);
} else {
buttons.push(
Button::primary(&fl!(loader, "follow-button")).form(&view.profile.follow_path()),
);
}
buttons
}
pub(crate) fn has_nav(&self) -> bool {
match &self.profiles {
PageKind::Linear(page) => page.next.is_some() || page.prev.is_some(),
PageKind::Search(page) => page.next.is_some() || page.prev.is_some(),
}
}
pub(crate) fn nav(&self, loader: &ActixLoader) -> Vec<Button> {
let mut nav = vec![];
match &self.profiles {
PageKind::Linear(page) => {
if let Some(prev) = page.prev {
nav.push(
Button::secondary(&fl!(loader, "previous-button"))
.href(&format!("{}?min={}", self.path, prev)),
);
}
if let Some(next) = page.next {
nav.push(
Button::secondary(&fl!(loader, "next-button"))
.href(&format!("{}?max={}", self.path, next)),
);
}
}
PageKind::Search(page) => {
if let Some(prev) = page.prev {
nav.push(
Button::secondary(&fl!(loader, "previous-button"))
.href(&format!("{}?term={}&page={}", self.path, page.term, prev)),
);
}
if let Some(next) = page.next {
nav.push(
Button::secondary(&fl!(loader, "next-button"))
.href(&format!("{}?term={}&page={}", self.path, page.term, next)),
);
}
}
};
nav
}
async fn search(
path: String,
viewer: Uuid,
term: String,
page: Option<PageNum>,
state: &State,
) -> Result<Self, Error> {
let store = state.profiles.clone();
let state = web::block(move || {
let mut cache = ProfileCache::new();
let profiles = SearchPage::from_pagination(
ProfilePager {
cache: &mut cache,
store: &store,
viewer,
},
PER_PAGE,
term,
page,
);
let state = ProfileListState {
path,
cache,
profiles: PageKind::Search(profiles),
};
Ok(state) as Result<_, Error>
})
.await??;
Ok(state)
}
async fn build(
path: String,
viewer: Uuid,
source: Option<PageSource>,
state: &State,
) -> Result<Self, Error> {
let store = state.profiles.clone();
let state = web::block(move || {
let mut cache = ProfileCache::new();
let profiles = Page::from_pagination(
ProfilePager {
cache: &mut cache,
store: &store,
viewer,
},
PER_PAGE,
source,
);
let state = ProfileListState {
path,
cache,
profiles: PageKind::Linear(profiles),
};
Ok(state) as Result<Self, Error>
})
.await??;
Ok(state)
}
}
impl ProfileCache {
fn new() -> Self {
Self::default()
}
}
impl<'b> ProfilePager<'b> {
fn filter_profile(&mut self, profile_id: Uuid) -> Option<Uuid> {
if profile_id == self.viewer {
return None;
}
if let Some(block) = self.cache.blocks.get(&profile_id) {
if *block {
return None;
}
} else {
let block = self
.store
.store
.view
.blocks
.by_forward(self.viewer, profile_id)
.ok()?
.is_some()
|| self
.store
.store
.view
.blocks
.by_forward(profile_id, self.viewer)
.ok()?
.is_some();
self.cache.blocks.insert(profile_id, block);
if block {
return None;
}
}
if !self.cache.following.contains_key(&profile_id) {
let following = self
.store
.store
.view
.follows
.by_forward(profile_id, self.viewer)
.ok()?
.is_some();
self.cache.following.insert(profile_id, following);
}
if !self.cache.follow_requested.contains_key(&profile_id) {
let follow_requested = self
.store
.store
.view
.follow_requests
.by_forward(profile_id, self.viewer)
.ok()?
.is_some();
self.cache
.follow_requested
.insert(profile_id, follow_requested);
}
if !self.cache.profiles.contains_key(&profile_id) {
let profile = self.store.store.profiles.by_id(profile_id).ok()??;
let mut file_ids = vec![];
file_ids.extend(profile.icon());
file_ids.extend(profile.banner());
self.cache.profiles.insert(profile.id(), profile);
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);
}
}
}
Some(profile_id)
}
}
impl<'b> Pagination for ProfilePager<'b> {
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.profiles
.older_than(max)
.filter_map(move |profile_id| self.filter_profile(profile_id)),
)
}
fn from_min<'a>(&'a mut self, min: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.profiles
.newer_than(min)
.filter_map(move |profile_id| self.filter_profile(profile_id)),
)
}
fn from_start<'a>(&'a mut self) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.profiles
.all()
.filter_map(move |profile_id| self.filter_profile(profile_id)),
)
}
}
impl<'b> SearchPagination for ProfilePager<'b> {
fn from_term<'a>(
&'a mut self,
term: &'a str,
) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.store
.store
.profiles
.search(term)
.filter_map(move |profile_id| self.filter_profile(profile_id)),
)
}
}
impl From<ProfilePage> for PageSource {
fn from(page: ProfilePage) -> Self {
match page {
ProfilePage::Max { max } => PageSource::OlderThan(max),
ProfilePage::Min { min } => PageSource::NewerThan(min),
}
}
}
impl From<PageNumQuery> for PageNum {
fn from(page: PageNumQuery) -> Self {
PageNum { page: page.page }
}
}