Add Profile and Submission report logic
Implement Follow and Block logic
This commit is contained in:
parent
29bdf064e9
commit
f350d718ac
|
@ -7,7 +7,7 @@ use actix_session::Session;
|
|||
use actix_web::{web, HttpRequest, HttpResponse, Scope};
|
||||
use hyaenidae_accounts::User;
|
||||
use hyaenidae_profiles::store::{File, FileStore, ProfileStore, Submission};
|
||||
use hyaenidae_toolkit::Button;
|
||||
use hyaenidae_toolkit::{Button, TextInput};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
@ -83,8 +83,347 @@ pub(super) fn scope() -> Scope {
|
|||
.service(web::resource("/done").route(web::get().to(done))),
|
||||
)
|
||||
.route("/drafts", web::get().to(drafts_page))
|
||||
.route("/@{handle}@{domain}", web::get().to(handle_view))
|
||||
.route("/#{id}", web::get().to(id_view))
|
||||
.service(
|
||||
web::scope("/@{handle}@{domain}")
|
||||
.route("", web::get().to(handle_view))
|
||||
.service(
|
||||
web::resource("/follow")
|
||||
.route(web::get().to(route_to_profile_page))
|
||||
.route(web::post().to(follow)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/unfollow")
|
||||
.route(web::get().to(route_to_profile_page))
|
||||
.route(web::post().to(unfollow)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/block")
|
||||
.route(web::get().to(route_to_profile_page))
|
||||
.route(web::post().to(block)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/unblock")
|
||||
.route(web::get().to(route_to_profile_page))
|
||||
.route(web::post().to(unblock)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/report")
|
||||
.route(web::get().to(report_page))
|
||||
.route(web::post().to(report)),
|
||||
)
|
||||
.route("/report-success", web::get().to(report_success_page)),
|
||||
)
|
||||
.route("/id/{id}", web::get().to(id_view))
|
||||
}
|
||||
|
||||
fn to_profile_page(handle: &str, domain: &str) -> HttpResponse {
|
||||
crate::redirect(&format!("/profiles/@{}@{}", handle, domain))
|
||||
}
|
||||
|
||||
async fn route_to_profile_page(handle: web::Path<(String, String)>) -> HttpResponse {
|
||||
let (handle, domain) = handle.into_inner();
|
||||
to_profile_page(&handle, &domain)
|
||||
}
|
||||
|
||||
async fn follow(
|
||||
handle: web::Path<(String, String)>,
|
||||
self_profile: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
use hyaenidae_profiles::apub::actions::CreateFollowRequest;
|
||||
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?;
|
||||
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let is_blocked =
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?;
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
||||
let follows = state.profiles.store.view.follows.clone();
|
||||
|
||||
let follow_exists = web::block(move || {
|
||||
let exists = follow_requests.by_forward(profile_id, self_id)?.is_some()
|
||||
|| follows.by_forward(profile_id, self_id)?.is_some();
|
||||
|
||||
Ok(exists)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if follow_exists {
|
||||
return Ok(to_profile_page(&handle, &domain));
|
||||
}
|
||||
|
||||
state
|
||||
.profiles
|
||||
.run(CreateFollowRequest::from_profiles(
|
||||
profile.id(),
|
||||
self_profile.id(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(to_profile_page(&handle, &domain))
|
||||
}
|
||||
|
||||
async fn unfollow(
|
||||
handle: web::Path<(String, String)>,
|
||||
self_profile: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
use hyaenidae_profiles::apub::actions::{UndoFollow, UndoFollowRequest};
|
||||
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?;
|
||||
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let is_blocked =
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?;
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
||||
let follows = state.profiles.store.view.follows.clone();
|
||||
|
||||
let (follow_request, follow) = web::block(move || {
|
||||
let follow_request = follow_requests.by_forward(profile_id, self_id)?;
|
||||
let follow = follows.by_forward(profile_id, self_id)?;
|
||||
|
||||
Ok((follow_request, follow))
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(follow_request) = follow_request {
|
||||
state
|
||||
.profiles
|
||||
.run(UndoFollowRequest::from_id(follow_request))
|
||||
.await?;
|
||||
}
|
||||
if let Some(follow) = follow {
|
||||
state.profiles.run(UndoFollow::from_id(follow)).await?;
|
||||
}
|
||||
|
||||
Ok(to_profile_page(&handle, &domain))
|
||||
}
|
||||
|
||||
async fn block(
|
||||
handle: web::Path<(String, String)>,
|
||||
self_profile: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
use hyaenidae_profiles::apub::actions::CreateBlock;
|
||||
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?;
|
||||
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let is_blocked =
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?;
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let block_exists =
|
||||
web::block(move || Ok(blocks.by_forward(profile_id, self_id)?.is_some())).await?;
|
||||
if block_exists {
|
||||
return Ok(to_profile_page(&handle, &domain));
|
||||
}
|
||||
|
||||
state
|
||||
.profiles
|
||||
.run(CreateBlock::from_profiles(profile.id(), self_profile.id()))
|
||||
.await?;
|
||||
|
||||
Ok(to_profile_page(&handle, &domain))
|
||||
}
|
||||
|
||||
async fn unblock(
|
||||
handle: web::Path<(String, String)>,
|
||||
self_profile: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
use hyaenidae_profiles::apub::actions::DeleteBlock;
|
||||
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle.clone(), domain.clone(), &state).await?;
|
||||
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let block = web::block(move || Ok(blocks.by_forward(profile_id, self_id)?)).await?;
|
||||
if let Some(block) = block {
|
||||
state.profiles.run(DeleteBlock::from_id(block)).await?;
|
||||
}
|
||||
|
||||
Ok(to_profile_page(&handle, &domain))
|
||||
}
|
||||
|
||||
pub struct ReportView {
|
||||
pub(crate) profile: Profile,
|
||||
pub(crate) input: TextInput,
|
||||
}
|
||||
|
||||
impl ReportView {
|
||||
fn new(profile: Profile, dark: bool) -> Self {
|
||||
let mut input = TextInput::new("body");
|
||||
input
|
||||
.title("Report")
|
||||
.placeholder("Type your report info here")
|
||||
.textarea()
|
||||
.dark(dark);
|
||||
|
||||
ReportView { profile, input }
|
||||
}
|
||||
}
|
||||
|
||||
async fn profile_from_id(id: Uuid, state: &State) -> Result<Profile, Error> {
|
||||
let profile_store = state.profiles.store.profiles.clone();
|
||||
let file_store = state.profiles.store.files.clone();
|
||||
|
||||
let profile = web::block(move || {
|
||||
let profile = Profile::from_stores(id, &profile_store, &file_store)?;
|
||||
Ok(profile)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
async fn profile_from_handle(
|
||||
handle: String,
|
||||
domain: String,
|
||||
state: &State,
|
||||
) -> Result<Profile, Error> {
|
||||
let profile_store = state.profiles.store.profiles.clone();
|
||||
let file_store = state.profiles.store.files.clone();
|
||||
|
||||
let profile = web::block(move || {
|
||||
let id = profile_store.by_handle(&handle, &domain)?.req()?;
|
||||
let profile = Profile::from_stores(id, &profile_store, &file_store)?;
|
||||
Ok(profile)
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
async fn report_page(
|
||||
self_profile: Profile,
|
||||
handle: web::Path<(String, String)>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle, domain, &state).await?;
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
let is_blocked =
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?;
|
||||
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let view = ReportView::new(profile, nav_state.dark());
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::profiles::report(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
const MAX_REPORT_LEN: usize = 1000;
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct ReportForm {
|
||||
body: String,
|
||||
}
|
||||
|
||||
async fn report(
|
||||
self_profile: Profile,
|
||||
handle: web::Path<(String, String)>,
|
||||
form: web::Form<ReportForm>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
use hyaenidae_profiles::apub::actions::CreateReport;
|
||||
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle, domain, &state).await?;
|
||||
|
||||
let form = form.into_inner();
|
||||
|
||||
let error = if form.body.len() > MAX_REPORT_LEN {
|
||||
format!("Must be shorter than {} characters", MAX_REPORT_LEN)
|
||||
} else if form.body.trim().is_empty() {
|
||||
format!("Must be present")
|
||||
} else {
|
||||
let res = state
|
||||
.profiles
|
||||
.run(CreateReport::from_profile(
|
||||
profile.id(),
|
||||
self_profile.id(),
|
||||
Some(form.body.clone()),
|
||||
))
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => return Ok(to_report_success_page(profile.full_handle())),
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut view = ReportView::new(profile, nav_state.dark());
|
||||
view.input.error_opt(Some(error)).value(&form.body);
|
||||
|
||||
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
||||
crate::templates::profiles::report(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
async fn report_success_page(
|
||||
self_profile: Profile,
|
||||
handle: web::Path<(String, String)>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (handle, domain) = handle.into_inner();
|
||||
let profile = profile_from_handle(handle, domain, &state).await?;
|
||||
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
let is_blocked =
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?;
|
||||
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let view = ReportView::new(profile, nav_state.dark());
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::profiles::report_success(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
fn to_report_success_page(full_handle: String) -> HttpResponse {
|
||||
redirect(&format!("/profiles/{}/report-success", full_handle))
|
||||
}
|
||||
|
||||
fn to_create() -> HttpResponse {
|
||||
|
@ -270,10 +609,34 @@ impl Profile {
|
|||
format!("/profiles/{}", self.full_handle())
|
||||
}
|
||||
|
||||
pub(crate) fn report_path(&self) -> String {
|
||||
format!("/profiles/{}/report", self.full_handle())
|
||||
}
|
||||
|
||||
pub(crate) fn follow_path(&self) -> String {
|
||||
format!("/profiles/{}/follow", self.full_handle())
|
||||
}
|
||||
|
||||
pub(crate) fn unfollow_path(&self) -> String {
|
||||
format!("/profiles/{}/unfollow", self.full_handle())
|
||||
}
|
||||
|
||||
pub(crate) fn block_path(&self) -> String {
|
||||
format!("/profiles/{}/block", self.full_handle())
|
||||
}
|
||||
|
||||
pub(crate) fn unblock_path(&self) -> String {
|
||||
format!("/profiles/{}/unblock", self.full_handle())
|
||||
}
|
||||
|
||||
fn refresh(self, state: &State) -> Result<Self, Error> {
|
||||
Self::from_id(self.inner.id(), state)
|
||||
}
|
||||
|
||||
fn is_suspended(&self) -> bool {
|
||||
self.inner.is_suspended()
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> Uuid {
|
||||
self.inner.id()
|
||||
}
|
||||
|
@ -291,7 +654,11 @@ impl Profile {
|
|||
}
|
||||
|
||||
pub(crate) fn description(&self) -> Option<&str> {
|
||||
self.inner.description()
|
||||
if self.is_suspended() {
|
||||
Some("Profile Suspended")
|
||||
} else {
|
||||
self.inner.description()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> String {
|
||||
|
@ -550,17 +917,19 @@ pub(crate) fn build_submissions(
|
|||
async fn id_view(
|
||||
req: HttpRequest,
|
||||
user: Option<User>,
|
||||
profile: Option<Profile>,
|
||||
self_profile: Option<Profile>,
|
||||
id: web::Path<Uuid>,
|
||||
page: Option<web::Query<SubmissionPage>>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let profile = profile_from_id(id.into_inner(), &state).await?;
|
||||
|
||||
do_public_view(
|
||||
req.uri().path(),
|
||||
user,
|
||||
self_profile,
|
||||
profile,
|
||||
id.into_inner(),
|
||||
page.map(|q| q.into_inner()),
|
||||
nav_state,
|
||||
&state,
|
||||
|
@ -571,26 +940,20 @@ async fn id_view(
|
|||
async fn handle_view(
|
||||
req: HttpRequest,
|
||||
user: Option<User>,
|
||||
profile: Option<Profile>,
|
||||
self_profile: Option<Profile>,
|
||||
handle: web::Path<(String, String)>,
|
||||
page: Option<web::Query<SubmissionPage>>,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (handle, domain) = handle.into_inner();
|
||||
|
||||
let profile_id = state
|
||||
.profiles
|
||||
.store
|
||||
.profiles
|
||||
.by_handle(&handle, &domain)?
|
||||
.req()?;
|
||||
let profile = profile_from_handle(handle, domain, &state).await?;
|
||||
|
||||
do_public_view(
|
||||
req.uri().path(),
|
||||
user,
|
||||
self_profile,
|
||||
profile,
|
||||
profile_id,
|
||||
page.map(|q| q.into_inner()),
|
||||
nav_state,
|
||||
&state,
|
||||
|
@ -603,13 +966,17 @@ pub struct ProfileView {
|
|||
pub(crate) submissions: Vec<SubmissionView>,
|
||||
nav: Vec<Button>,
|
||||
pub(crate) viewer: Option<Uuid>,
|
||||
pub(crate) is_self: bool,
|
||||
buttons: Vec<Button>,
|
||||
}
|
||||
|
||||
impl ProfileView {
|
||||
pub(crate) fn nav(&self) -> Vec<&Button> {
|
||||
self.nav.iter().collect()
|
||||
}
|
||||
|
||||
pub(crate) fn buttons(&self) -> Vec<&Button> {
|
||||
self.buttons.iter().collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubmissionView {
|
||||
|
@ -787,6 +1154,107 @@ impl SubmissionView {
|
|||
}
|
||||
}
|
||||
|
||||
async fn profile_buttons(
|
||||
profile: &Profile,
|
||||
self_profile: Option<&Profile>,
|
||||
is_drafts: bool,
|
||||
dark: bool,
|
||||
state: &State,
|
||||
) -> Result<Vec<Button>, Error> {
|
||||
let mut buttons = vec![];
|
||||
|
||||
if profile.is_suspended() {
|
||||
return Ok(buttons);
|
||||
}
|
||||
|
||||
let is_follow_requested = if let Some(self_profile) = self_profile {
|
||||
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
web::block(move || Ok(follow_requests.by_forward(profile_id, self_id)?.is_some())).await?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let is_follower = if let Some(self_profile) = self_profile {
|
||||
let follows = state.profiles.store.view.follows.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
web::block(move || Ok(follows.by_forward(profile_id, self_id)?.is_some())).await?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let is_blocking = if let Some(self_profile) = self_profile {
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
web::block(move || Ok(blocks.by_forward(profile_id, self_id)?.is_some())).await?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if self_profile
|
||||
.map(|p| p.id() == profile.id())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let drafts = if is_drafts {
|
||||
let view = Button::secondary("View Profile");
|
||||
view.href(&profile.view_path()).dark(dark);
|
||||
view
|
||||
} else {
|
||||
let drafts = Button::secondary("View Drafts");
|
||||
drafts.href("/profiles/drafts").dark(dark);
|
||||
drafts
|
||||
};
|
||||
|
||||
let edit = Button::secondary("Edit Profile");
|
||||
let switch = Button::secondary("Switch Profile");
|
||||
|
||||
edit.href("/profiles/current").dark(dark);
|
||||
switch.href("/profiles/change").dark(dark);
|
||||
|
||||
buttons.push(drafts);
|
||||
buttons.push(edit);
|
||||
buttons.push(switch);
|
||||
} else if is_follower || is_follow_requested {
|
||||
let unfollow = Button::secondary("Unfollow");
|
||||
let block = Button::secondary("Block");
|
||||
let report = Button::primary_outline("Report");
|
||||
|
||||
unfollow.form(&profile.unfollow_path()).dark(dark);
|
||||
block.form(&profile.block_path()).dark(dark);
|
||||
report.href(&profile.report_path()).dark(dark);
|
||||
|
||||
buttons.push(unfollow);
|
||||
buttons.push(block);
|
||||
buttons.push(report);
|
||||
} else if is_blocking {
|
||||
let unblock = Button::secondary("Unblock");
|
||||
let report = Button::primary_outline("Report");
|
||||
|
||||
unblock.form(&profile.unblock_path()).dark(dark);
|
||||
report.href(&profile.report_path()).dark(dark);
|
||||
|
||||
buttons.push(unblock);
|
||||
buttons.push(report);
|
||||
} else if self_profile.is_some() {
|
||||
let follow = Button::secondary("Follow");
|
||||
let block = Button::secondary("Block");
|
||||
let report = Button::primary_outline("Report");
|
||||
|
||||
follow.form(&profile.follow_path()).dark(dark);
|
||||
block.form(&profile.block_path()).dark(dark);
|
||||
report.href(&profile.report_path()).dark(dark);
|
||||
|
||||
buttons.push(follow);
|
||||
buttons.push(block);
|
||||
buttons.push(report);
|
||||
}
|
||||
|
||||
Ok(buttons)
|
||||
}
|
||||
|
||||
async fn drafts_page(
|
||||
req: HttpRequest,
|
||||
profile: Profile,
|
||||
|
@ -806,12 +1274,14 @@ async fn drafts_page(
|
|||
&state,
|
||||
);
|
||||
|
||||
let buttons = profile_buttons(&profile, Some(&profile), true, nav_state.dark(), &state).await?;
|
||||
|
||||
let view = ProfileView {
|
||||
profile,
|
||||
submissions: agg.submissions,
|
||||
nav: agg.nav,
|
||||
viewer: viewed_by,
|
||||
is_self: true,
|
||||
buttons,
|
||||
};
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
|
@ -823,21 +1293,29 @@ async fn do_public_view(
|
|||
base_url: &str,
|
||||
user: Option<User>,
|
||||
self_profile: Option<Profile>,
|
||||
profile_id: Uuid,
|
||||
profile: Profile,
|
||||
page: Option<SubmissionPage>,
|
||||
nav_state: NavState,
|
||||
state: &State,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let profile = Profile::from_id(profile_id, state)?;
|
||||
|
||||
if profile.login_required() && user.is_none() {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let is_self = self_profile
|
||||
.as_ref()
|
||||
.map(|p| p.id() == profile_id)
|
||||
.unwrap_or(false);
|
||||
let is_blocked = if let Some(self_profile) = &self_profile {
|
||||
let blocks = state.profiles.store.view.blocks.clone();
|
||||
let self_id = self_profile.id();
|
||||
let profile_id = profile.id();
|
||||
|
||||
web::block(move || Ok(blocks.by_forward(self_id, profile_id)?.is_some())).await?
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if is_blocked {
|
||||
return Ok(crate::to_404());
|
||||
}
|
||||
|
||||
let viewed_by = self_profile.as_ref().map(|p| p.id());
|
||||
let agg = build_submissions(
|
||||
base_url,
|
||||
|
@ -850,12 +1328,21 @@ async fn do_public_view(
|
|||
state,
|
||||
);
|
||||
|
||||
let buttons = profile_buttons(
|
||||
&profile,
|
||||
self_profile.as_ref(),
|
||||
false,
|
||||
nav_state.dark(),
|
||||
state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let view = ProfileView {
|
||||
profile,
|
||||
submissions: agg.submissions,
|
||||
nav: agg.nav,
|
||||
viewer: viewed_by,
|
||||
is_self,
|
||||
buttons,
|
||||
};
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
|
@ -1145,7 +1632,7 @@ pub struct HandleForm {
|
|||
}
|
||||
|
||||
async fn new_handle(_: User, nav_state: NavState) -> Result<HttpResponse, Error> {
|
||||
let mut handle_input = hyaenidae_toolkit::TextInput::new("handle");
|
||||
let mut handle_input = TextInput::new("handle");
|
||||
handle_input
|
||||
.placeholder("Handle")
|
||||
.title("Handle")
|
||||
|
@ -1198,7 +1685,7 @@ async fn create_handle(
|
|||
}
|
||||
};
|
||||
|
||||
let mut handle_input = hyaenidae_toolkit::TextInput::new("handle");
|
||||
let mut handle_input = TextInput::new("handle");
|
||||
handle_input
|
||||
.placeholder("Handle")
|
||||
.title("Handle")
|
||||
|
@ -1218,7 +1705,7 @@ pub struct BioForm {
|
|||
}
|
||||
|
||||
async fn new_bio(profile: Profile, nav_state: NavState) -> Result<HttpResponse, Error> {
|
||||
let mut display_name = hyaenidae_toolkit::TextInput::new("display_name");
|
||||
let mut display_name = TextInput::new("display_name");
|
||||
display_name
|
||||
.title("Display Name")
|
||||
.placeholder("Display Name")
|
||||
|
@ -1227,7 +1714,7 @@ async fn new_bio(profile: Profile, nav_state: NavState) -> Result<HttpResponse,
|
|||
display_name.value(text);
|
||||
}
|
||||
|
||||
let mut description = hyaenidae_toolkit::TextInput::new("description");
|
||||
let mut description = TextInput::new("description");
|
||||
description
|
||||
.title("Description")
|
||||
.placeholder("Description")
|
||||
|
@ -1280,7 +1767,7 @@ async fn create_bio(
|
|||
}
|
||||
}
|
||||
|
||||
let mut display_name = hyaenidae_toolkit::TextInput::new("display_name");
|
||||
let mut display_name = TextInput::new("display_name");
|
||||
display_name
|
||||
.title("Display Name")
|
||||
.placeholder("Display Name")
|
||||
|
@ -1288,7 +1775,7 @@ async fn create_bio(
|
|||
.error_opt(display_name_error)
|
||||
.dark(nav_state.dark());
|
||||
|
||||
let mut description = hyaenidae_toolkit::TextInput::new("description");
|
||||
let mut description = TextInput::new("description");
|
||||
description
|
||||
.title("Description")
|
||||
.placeholder("Description")
|
||||
|
|
|
@ -23,6 +23,12 @@ pub(super) fn scope() -> Scope {
|
|||
.service(
|
||||
web::scope("/{submission_id}")
|
||||
.route("", web::get().to(submission_page))
|
||||
.service(
|
||||
web::resource("/report")
|
||||
.route(web::get().to(report_page))
|
||||
.route(web::post().to(report)),
|
||||
)
|
||||
.route("/report-success", web::get().to(report_success_page))
|
||||
.service(
|
||||
web::resource("/comment")
|
||||
.route(web::get().to(route_to_update_page))
|
||||
|
@ -56,6 +62,145 @@ pub(super) fn scope() -> Scope {
|
|||
)
|
||||
}
|
||||
|
||||
pub struct ReportView {
|
||||
pub(crate) submission: Submission,
|
||||
pub(crate) profile: Profile,
|
||||
pub(crate) input: TextInput,
|
||||
}
|
||||
|
||||
impl ReportView {
|
||||
async fn build(submission_id: Uuid, dark: bool, state: &State) -> Result<Self, Error> {
|
||||
let submission_store = state.profiles.store.submissions.clone();
|
||||
let file_store = state.profiles.store.files.clone();
|
||||
let submission = web::block(move || {
|
||||
Submission::from_stores(submission_id, &submission_store, &file_store)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let profile_id = submission.profile_id();
|
||||
let profile_store = state.profiles.store.profiles.clone();
|
||||
let file_store = state.profiles.store.files.clone();
|
||||
let profile =
|
||||
web::block(move || Profile::from_stores(profile_id, &profile_store, &file_store))
|
||||
.await?;
|
||||
|
||||
let mut input = TextInput::new("body");
|
||||
input
|
||||
.title("Report")
|
||||
.placeholder("Say why you are reporting this submission")
|
||||
.textarea()
|
||||
.dark(dark);
|
||||
|
||||
Ok(ReportView {
|
||||
submission,
|
||||
profile,
|
||||
input,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn tiles(&self) -> Vec<crate::profiles::SubmissionView> {
|
||||
self.submission
|
||||
.files()
|
||||
.iter()
|
||||
.map(|f| {
|
||||
crate::profiles::SubmissionView::from_parts(
|
||||
&self.submission.inner,
|
||||
&self.profile,
|
||||
f,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
async fn report_page(
|
||||
submission_id: web::Path<Uuid>,
|
||||
profile: Profile,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let view = ReportView::build(submission_id.into_inner(), nav_state.dark(), &state).await?;
|
||||
|
||||
if let Some(res) = can_view(Some(&profile), &view.profile, &view.submission, &state).await? {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::submissions::report(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
struct ReportForm {
|
||||
body: String,
|
||||
}
|
||||
|
||||
const MAX_REPORT_LEN: usize = 1000;
|
||||
|
||||
async fn report(
|
||||
form: web::Form<ReportForm>,
|
||||
submission_id: web::Path<Uuid>,
|
||||
profile: Profile,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut view = ReportView::build(submission_id.into_inner(), nav_state.dark(), &state).await?;
|
||||
|
||||
if let Some(res) = can_view(Some(&profile), &view.profile, &view.submission, &state).await? {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
let form = form.into_inner();
|
||||
|
||||
let error = if form.body.len() > MAX_REPORT_LEN {
|
||||
format!("Must be shorter than {} characters", MAX_REPORT_LEN)
|
||||
} else if form.body.trim().is_empty() {
|
||||
format!("Must be present")
|
||||
} else {
|
||||
use hyaenidae_profiles::apub::actions::CreateReport;
|
||||
let res = state
|
||||
.profiles
|
||||
.run(CreateReport::from_submission(
|
||||
view.submission.id(),
|
||||
view.profile.id(),
|
||||
Some(form.body.clone()),
|
||||
))
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => return Ok(to_report_success_page(view.submission.id())),
|
||||
Err(e) => e.to_string(),
|
||||
}
|
||||
};
|
||||
|
||||
view.input.error_opt(Some(error)).value(&form.body);
|
||||
|
||||
crate::rendered(HttpResponse::BadRequest(), |cursor| {
|
||||
crate::templates::submissions::report(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
async fn report_success_page(
|
||||
submission_id: web::Path<Uuid>,
|
||||
profile: Profile,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let view = ReportView::build(submission_id.into_inner(), nav_state.dark(), &state).await?;
|
||||
|
||||
if let Some(res) = can_view(Some(&profile), &view.profile, &view.submission, &state).await? {
|
||||
return Ok(res);
|
||||
}
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::submissions::report_success(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
fn to_report_success_page(submission_id: Uuid) -> HttpResponse {
|
||||
crate::redirect(&format!("/submissions/{}/report-success", submission_id))
|
||||
}
|
||||
|
||||
pub struct SubmissionState {
|
||||
pub(crate) submission: Submission,
|
||||
pub(crate) title: TextInput,
|
||||
|
@ -177,6 +322,10 @@ impl Submission {
|
|||
format!("/submissions/{}", self.id())
|
||||
}
|
||||
|
||||
pub(crate) fn report_path(&self) -> String {
|
||||
format!("/submissions/{}/report", self.id())
|
||||
}
|
||||
|
||||
pub(crate) fn profile_id(&self) -> Uuid {
|
||||
self.inner.profile_id()
|
||||
}
|
||||
|
@ -407,6 +556,9 @@ fn submission_nav(
|
|||
|
||||
nav.push(button);
|
||||
}
|
||||
let btn = Button::primary_outline("Report");
|
||||
btn.href(&submission.report_path()).dark(nav_state.dark());
|
||||
nav.push(btn);
|
||||
if file_num < file_count.saturating_sub(1) {
|
||||
let button = Button::secondary("Next");
|
||||
button
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
@use crate::templates::layouts::home;
|
||||
@use crate::templates::profiles::view as view_profile;
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_section, card_title}, Card};
|
||||
@use hyaenidae_toolkit::{templates::link, Link};
|
||||
@use hyaenidae_toolkit::templates::select;
|
||||
@use hyaenidae_toolkit::templates::text_input;
|
||||
|
@ -15,7 +15,9 @@
|
|||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Reported Item })
|
||||
@if let Some(profile) = view.profile() {
|
||||
@:view_profile(profile, nav_state.dark())
|
||||
@:card_section({
|
||||
@:view_profile(profile, nav_state.dark())
|
||||
})
|
||||
}
|
||||
@if let Some(cv) = view.comment() {
|
||||
@:card_body({
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
@use crate::{templates::{layouts::home, profiles::{submission_tile, view}}, nav::NavState, profiles::ProfileView};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::templates::button_group;
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_section, card_title}, Card};
|
||||
|
||||
@(pview: &ProfileView, nav_state: &NavState)
|
||||
|
||||
@:home(&pview.profile.name(), pview.profile.description().unwrap_or(&format!("{}'s profile on Hyaenidae", pview.profile.name())), nav_state, {}, {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&pview.profile, nav_state.dark()) })
|
||||
@if pview.is_self {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Profile Actions })
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
&Button::secondary("View Profile").href(&pview.profile.view_path()).dark(nav_state.dark()),
|
||||
&Button::secondary("Edit Profile").href("/profiles/current").dark(nav_state.dark()),
|
||||
&Button::secondary("Switch Profile").href("/profiles/change").dark(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Profile Actions })
|
||||
@:card_body({
|
||||
@:button_group(&pview.buttons())
|
||||
})
|
||||
}
|
||||
})
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Drafts })
|
||||
@:card_section({
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
@use crate::{templates::{layouts::home, profiles::{submission_tile, view}}, nav::NavState, profiles::ProfileView};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::templates::button_group;
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_section, card_title}, Card};
|
||||
|
||||
@(pview: &ProfileView, nav_state: &NavState)
|
||||
|
||||
@:home(&pview.profile.name(), pview.profile.description().unwrap_or(&format!("{}'s profile on Hyaenidae", pview.profile.name())), nav_state, {}, {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&pview.profile, nav_state.dark()) })
|
||||
@if pview.is_self {
|
||||
@if !pview.buttons().is_empty() {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Profile Actions })
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
&Button::secondary("View Drafts").href("/profiles/drafts").dark(nav_state.dark()),
|
||||
&Button::secondary("Edit Profile").href("/profiles/current").dark(nav_state.dark()),
|
||||
&Button::secondary("Switch Profile").href("/profiles/change").dark(nav_state.dark()),
|
||||
])
|
||||
@:button_group(&pview.buttons())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
28
server/templates/profiles/report.rs.html
Normal file
28
server/templates/profiles/report.rs.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
@use crate::{templates::{layouts::home, profiles::view}, nav::NavState, profiles::ReportView};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
@use hyaenidae_toolkit::templates::text_input;
|
||||
|
||||
@(rview: &ReportView, nav_state: &NavState)
|
||||
|
||||
@:home("Report Profile", rview.profile.description().unwrap_or(&format!("Report {}'s profile on Hyaenidae", rview.profile.name())), nav_state, {}, {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), { @:view(&rview.profile, nav_state.dark()) })
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
<form method="POST" action="@rview.profile.report_path()">
|
||||
@:card_title({ Report Profile })
|
||||
@:card_body({
|
||||
<p>
|
||||
Please include any relevant information for moderators to act on this report.
|
||||
</p>
|
||||
@:text_input(&rview.input)
|
||||
})
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
Button::primary("Report").dark(nav_state.dark()),
|
||||
Button::secondary("Back to Profile").href(&rview.profile.view_path()).dark(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
</form>
|
||||
})
|
||||
})
|
||||
|
18
server/templates/profiles/report_success.rs.html
Normal file
18
server/templates/profiles/report_success.rs.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
@use crate::profiles::ReportView;
|
||||
@use crate::nav::NavState;
|
||||
@use crate::templates::layouts::home;
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_title, card_body}, Card};
|
||||
|
||||
@(view: &ReportView, nav_state: &NavState)
|
||||
|
||||
@:home("Profile Reported", &format!("Reported {}", view.profile.name()), nav_state, {}, {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Profile Reported })
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
Button::secondary("Back to Profile").href(&view.profile.view_path()).dark(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
34
server/templates/submissions/report.rs.html
Normal file
34
server/templates/submissions/report.rs.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
@use crate::templates::admin::submission_box;
|
||||
@use crate::templates::layouts::home;
|
||||
@use crate::{nav::NavState, submissions::ReportView};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
@use hyaenidae_toolkit::templates::text_input;
|
||||
|
||||
@(view: &ReportView, nav_state: &NavState)
|
||||
|
||||
@:home("Report Submission", view.submission.description().unwrap_or(&format!("Report {}'s
|
||||
submission on Hyaenidae", view.profile.name())), nav_state, {}, {
|
||||
@:card(&Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({
|
||||
Report @view.submission.title()
|
||||
})
|
||||
@:submission_box(&view.submission, &view.profile, &view.tiles(), nav_state.dark())
|
||||
})
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
<form method="POST" action="@view.submission.report_path()">
|
||||
@:card_title({ Report Submission })
|
||||
@:card_body({
|
||||
<p>
|
||||
Please include any relevant information for moderators to act on this report.
|
||||
</p>
|
||||
@:text_input(&view.input)
|
||||
})
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
Button::primary("Report").dark(nav_state.dark()),
|
||||
Button::secondary("Back to Submission").href(&view.submission.view_path()).dark(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
17
server/templates/submissions/report_success.rs.html
Normal file
17
server/templates/submissions/report_success.rs.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
@use crate::templates::layouts::home;
|
||||
@use crate::{nav::NavState, submissions::ReportView};
|
||||
@use hyaenidae_toolkit::{templates::button_group, Button};
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
|
||||
@(view: &ReportView, nav_state: &NavState)
|
||||
|
||||
@:home("Submission Reported", &format!("Reported {}'s submission", view.profile.name()), nav_state, {}, {
|
||||
@:card(&Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Submission Reported })
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
Button::secondary("Back to Submission").href(&view.submission.view_path()).dark(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue