455 lines
12 KiB
Rust
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 }
|
|
}
|
|
}
|