298 lines
8 KiB
Rust
298 lines
8 KiB
Rust
|
use crate::{
|
||
|
error::Error,
|
||
|
extensions::ProfileExt,
|
||
|
middleware::UserProfile,
|
||
|
nav::NavState,
|
||
|
pagination::{Page, PageSource, Pagination},
|
||
|
views::ProfileView,
|
||
|
State,
|
||
|
};
|
||
|
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
||
|
use hyaenidae_profiles::store::{File, Profile};
|
||
|
use hyaenidae_toolkit::Button;
|
||
|
use std::collections::HashMap;
|
||
|
use uuid::Uuid;
|
||
|
|
||
|
const PER_PAGE: usize = 12;
|
||
|
|
||
|
pub(super) fn scope() -> Scope {
|
||
|
web::scope("/discover").route("", web::get().to(profile_list))
|
||
|
}
|
||
|
|
||
|
async fn profile_list(
|
||
|
req: HttpRequest,
|
||
|
profile: UserProfile,
|
||
|
page: Option<web::Query<ProfilePage>>,
|
||
|
nav_state: NavState,
|
||
|
state: web::Data<State>,
|
||
|
) -> Result<HttpResponse, Error> {
|
||
|
let page = page.map(|query| query.into_inner().into());
|
||
|
|
||
|
let state = ProfileListState::build(req.path(), profile.0.id(), page, &state).await?;
|
||
|
|
||
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||
|
crate::templates::profiles::discover(cursor, &state, &nav_state)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
pub struct ProfileListState {
|
||
|
path: String,
|
||
|
cache: ProfileCache,
|
||
|
profiles: Page,
|
||
|
}
|
||
|
|
||
|
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||
|
#[serde(untagged)]
|
||
|
enum ProfilePage {
|
||
|
Max { max: Uuid },
|
||
|
Min { min: Uuid },
|
||
|
}
|
||
|
|
||
|
#[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 ProfileListState {
|
||
|
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, 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::secondary("Cancel Follow Request").form(&view.profile.unfollow_path()),
|
||
|
);
|
||
|
} else if self
|
||
|
.cache
|
||
|
.following
|
||
|
.get(&view.profile.id())
|
||
|
.map(|b| *b)
|
||
|
.unwrap_or(false)
|
||
|
{
|
||
|
buttons.push(Button::secondary("Unfollow").form(&view.profile.unfollow_path()));
|
||
|
} else {
|
||
|
buttons.push(Button::secondary("Follow").form(&view.profile.follow_path()));
|
||
|
}
|
||
|
|
||
|
buttons.push(Button::secondary("Block").form(&view.profile.block_path()));
|
||
|
buttons.push(Button::primary_outline("Report").href(&view.profile.report_path()));
|
||
|
|
||
|
buttons
|
||
|
}
|
||
|
|
||
|
pub(crate) fn has_nav(&self) -> bool {
|
||
|
self.profiles.next.is_some() || self.profiles.prev.is_some()
|
||
|
}
|
||
|
|
||
|
pub(crate) fn nav(&self) -> Vec<Button> {
|
||
|
let mut nav = vec![];
|
||
|
|
||
|
if let Some(prev) = self.profiles.prev {
|
||
|
nav.push(Button::secondary("Previous").href(&format!("{}?min={}", self.path, prev)));
|
||
|
}
|
||
|
|
||
|
if let Some(next) = self.profiles.next {
|
||
|
nav.push(Button::secondary("Next").href(&format!("{}?max={}", self.path, next)));
|
||
|
}
|
||
|
|
||
|
nav
|
||
|
}
|
||
|
|
||
|
async fn build(
|
||
|
path: &str,
|
||
|
viewer: Uuid,
|
||
|
source: Option<PageSource>,
|
||
|
state: &State,
|
||
|
) -> Result<Self, Error> {
|
||
|
let store = state.profiles.clone();
|
||
|
let path = path.to_owned();
|
||
|
|
||
|
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,
|
||
|
};
|
||
|
|
||
|
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 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 From<ProfilePage> for PageSource {
|
||
|
fn from(page: ProfilePage) -> Self {
|
||
|
match page {
|
||
|
ProfilePage::Max { max } => PageSource::OlderThan(max),
|
||
|
ProfilePage::Min { min } => PageSource::NewerThan(min),
|
||
|
}
|
||
|
}
|
||
|
}
|