545 lines
17 KiB
Rust
545 lines
17 KiB
Rust
use crate::{
|
|
error::{Error, OptionExt},
|
|
extensions::{ProfileExt, SubmissionExt},
|
|
middleware::UserProfile,
|
|
nav::NavState,
|
|
views::OwnedProfileView,
|
|
ActixLoader, State,
|
|
};
|
|
use actix_web::{web, HttpResponse, Scope};
|
|
use futures::stream::{FuturesUnordered, StreamExt};
|
|
use hyaenidae_profiles::store::{Comment, File, Profile, Submission};
|
|
use hyaenidae_toolkit::{Button, Link};
|
|
use i18n_embed_fl::fl;
|
|
use std::collections::HashMap;
|
|
use uuid::Uuid;
|
|
|
|
pub(super) fn scope() -> Scope {
|
|
web::scope("/notifications")
|
|
.service(
|
|
web::resource("")
|
|
.route(web::get().to(notifications_page))
|
|
.route(web::post().to(update_notifications)),
|
|
)
|
|
.service(
|
|
web::scope("/follow-requests")
|
|
.service(
|
|
web::resource("/accept-all")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(accept_all)),
|
|
)
|
|
.service(
|
|
web::resource("/reject-all")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(reject_all)),
|
|
)
|
|
.service(
|
|
web::scope("/{id}")
|
|
.service(
|
|
web::resource("/accept")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(accept_follow_request)),
|
|
)
|
|
.service(
|
|
web::resource("/reject")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(reject_follow_request)),
|
|
),
|
|
),
|
|
)
|
|
.service(
|
|
web::scope("/comments")
|
|
.service(
|
|
web::resource("/clear-all")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(clear_comments)),
|
|
)
|
|
.service(
|
|
web::resource("/{id}/remove")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(remove_comment_notification)),
|
|
),
|
|
)
|
|
.service(
|
|
web::resource("/clear")
|
|
.route(web::get().to(to_notifications_page))
|
|
.route(web::post().to(clear_notifications)),
|
|
)
|
|
}
|
|
|
|
async fn clear_notifications(
|
|
profile: UserProfile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
let profile_id = profile.id();
|
|
let comments = state.profiles.store.view.comments.clone();
|
|
let reacts = state.profiles.store.view.reacts.clone();
|
|
|
|
web::block(move || {
|
|
reacts.clear(profile_id, None)?;
|
|
comments.clear(profile_id, None)?;
|
|
|
|
Ok(())
|
|
})
|
|
.await?;
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn clear_comments(
|
|
profile: UserProfile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
let profile_id = profile.id();
|
|
let comments = state.profiles.store.view.comments.clone();
|
|
web::block(move || {
|
|
comments.clear(profile_id, None)?;
|
|
Ok(())
|
|
})
|
|
.await?;
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn accept_all(profile: UserProfile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
use hyaenidae_profiles::apub::actions::AcceptFollowRequest;
|
|
|
|
let profile_id = profile.id();
|
|
let follow_requests = state.profiles.store.view.follow_request_notifs.clone();
|
|
let requests: Vec<_> =
|
|
web::block(move || Ok(follow_requests.for_profile(profile_id).collect())).await?;
|
|
|
|
let mut unorderd = FuturesUnordered::new();
|
|
|
|
for request_id in requests {
|
|
unorderd.push(state.profiles.run(AcceptFollowRequest::from_id(request_id)));
|
|
}
|
|
|
|
while let Some(res) = unorderd.next().await {
|
|
res?;
|
|
}
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn reject_all(profile: UserProfile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
use hyaenidae_profiles::apub::actions::RejectFollowRequest;
|
|
|
|
let profile_id = profile.id();
|
|
let follow_requests = state.profiles.store.view.follow_request_notifs.clone();
|
|
let requests: Vec<_> =
|
|
web::block(move || Ok(follow_requests.for_profile(profile_id).collect())).await?;
|
|
|
|
let mut unorderd = FuturesUnordered::new();
|
|
|
|
for request_id in requests {
|
|
unorderd.push(state.profiles.run(RejectFollowRequest::from_id(request_id)));
|
|
}
|
|
|
|
while let Some(res) = unorderd.next().await {
|
|
res?;
|
|
}
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn accept_follow_request(
|
|
freq_id: web::Path<Uuid>,
|
|
profile: UserProfile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
use hyaenidae_profiles::apub::actions::AcceptFollowRequest;
|
|
|
|
let profile_id = profile.id();
|
|
let freq_id = freq_id.into_inner();
|
|
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
|
let freq_is_valid = web::block(move || {
|
|
Ok(follow_requests
|
|
.left(freq_id)?
|
|
.map(|left_id| left_id == profile_id)
|
|
.unwrap_or(false))
|
|
})
|
|
.await?;
|
|
|
|
if !freq_is_valid {
|
|
return Ok(crate::to_404());
|
|
}
|
|
|
|
state
|
|
.profiles
|
|
.run(AcceptFollowRequest::from_id(freq_id))
|
|
.await?;
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn reject_follow_request(
|
|
freq_id: web::Path<Uuid>,
|
|
profile: UserProfile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
use hyaenidae_profiles::apub::actions::RejectFollowRequest;
|
|
|
|
let profile_id = profile.id();
|
|
let freq_id = freq_id.into_inner();
|
|
let follow_requests = state.profiles.store.view.follow_requests.clone();
|
|
let freq_is_valid = web::block(move || {
|
|
Ok(follow_requests
|
|
.left(freq_id)?
|
|
.map(|left_id| left_id == profile_id)
|
|
.unwrap_or(false))
|
|
})
|
|
.await?;
|
|
|
|
if !freq_is_valid {
|
|
return Ok(crate::to_404());
|
|
}
|
|
|
|
state
|
|
.profiles
|
|
.run(RejectFollowRequest::from_id(freq_id))
|
|
.await?;
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
async fn remove_comment_notification(
|
|
comment_id: web::Path<Uuid>,
|
|
profile: UserProfile,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
let profile_id = profile.id();
|
|
let comment_id = comment_id.into_inner();
|
|
|
|
let comment_notifs = state.profiles.store.view.comments.clone();
|
|
web::block(move || Ok(comment_notifs.clear(profile_id, Some(vec![comment_id]))?)).await?;
|
|
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
pub(crate) struct CommentView<'a> {
|
|
submission: &'a Submission,
|
|
parent: Option<&'a Comment>,
|
|
comment: &'a Comment,
|
|
author: &'a Profile,
|
|
id: Uuid,
|
|
self_id: Uuid,
|
|
}
|
|
|
|
impl<'a> CommentView<'a> {
|
|
fn comment_reply(&self) -> Option<Uuid> {
|
|
self.parent.and_then(|c| {
|
|
if c.profile_id() == self.self_id {
|
|
Some(c.id())
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
|
|
fn submission_path(&self) -> Option<String> {
|
|
if self.comment_reply().is_none() {
|
|
Some(format!("/submissions/{}", self.comment.submission_id()))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn submission_title(&self) -> String {
|
|
self.submission.title_text()
|
|
}
|
|
|
|
pub(crate) fn submission_link(&self) -> Option<Link> {
|
|
self.submission_path()
|
|
.map(|path| Link::new_tab(&path).plain(true))
|
|
}
|
|
|
|
fn reply_to_path(&self) -> Option<String> {
|
|
if let Some(reply_to_id) = self.comment_reply() {
|
|
Some(format!("/comments/{}", reply_to_id))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub(crate) fn reply_to_link(&self) -> Option<Link> {
|
|
self.reply_to_path()
|
|
.map(|path| Link::new_tab(&path).plain(true))
|
|
}
|
|
|
|
fn comment_path(&self) -> String {
|
|
format!("/comments/{}", self.comment.id())
|
|
}
|
|
|
|
pub(crate) fn view_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::secondary(&fl!(loader, "notification-comment-view"))
|
|
.href(&self.comment_path())
|
|
.new_tab()
|
|
}
|
|
|
|
fn remove_path(&self) -> String {
|
|
format!("/notifications/comments/{}/remove", self.id)
|
|
}
|
|
|
|
pub(crate) fn remove_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::secondary(&fl!(loader, "notification-comment-remove")).form(&self.remove_path())
|
|
}
|
|
|
|
pub(crate) fn author_name(&self) -> String {
|
|
self.author.name()
|
|
}
|
|
|
|
pub(crate) fn author_link(&self) -> Link {
|
|
Link::new_tab(&self.author.view_path()).plain(true)
|
|
}
|
|
}
|
|
|
|
pub(crate) struct FollowRequestView<'a> {
|
|
pub(crate) profile: &'a Profile,
|
|
icon: Option<&'a File>,
|
|
id: Uuid,
|
|
}
|
|
|
|
impl<'a> FollowRequestView<'a> {
|
|
pub(crate) fn accept_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::secondary(&fl!(loader, "follow-accept")).form(&format!(
|
|
"/notifications/follow-requests/{}/accept",
|
|
self.id
|
|
))
|
|
}
|
|
|
|
pub(crate) fn reject_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::secondary(&fl!(loader, "follow-reject")).form(&format!(
|
|
"/notifications/follow-requests/{}/reject",
|
|
self.id
|
|
))
|
|
}
|
|
|
|
pub(crate) fn view(&self) -> OwnedProfileView {
|
|
OwnedProfileView {
|
|
profile: self.profile.clone(),
|
|
icon: self.icon.map(|f| f.clone()),
|
|
banner: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct NotificationsView {
|
|
comment_hm: HashMap<Uuid, Comment>,
|
|
profile_hm: HashMap<Uuid, Profile>,
|
|
file_hm: HashMap<Uuid, File>,
|
|
submission_hm: HashMap<Uuid, Submission>,
|
|
fr_profile_hm: HashMap<Uuid, Uuid>,
|
|
comments: Vec<Uuid>,
|
|
follow_requests: Vec<Uuid>,
|
|
count: u64,
|
|
self_id: Uuid,
|
|
}
|
|
|
|
impl NotificationsView {
|
|
pub(crate) fn comments<'a>(&'a self) -> impl Iterator<Item = CommentView<'a>> + 'a {
|
|
self.comments.iter().filter_map(move |comment_id| {
|
|
let comment = self.comment_hm.get(comment_id)?;
|
|
let author = self.profile_hm.get(&comment.profile_id())?;
|
|
let submission = self.submission_hm.get(&comment.submission_id())?;
|
|
let parent = comment.comment_id().and_then(|id| self.comment_hm.get(&id));
|
|
|
|
Some(CommentView {
|
|
comment,
|
|
parent,
|
|
author,
|
|
submission,
|
|
id: *comment_id,
|
|
self_id: self.self_id,
|
|
})
|
|
})
|
|
}
|
|
|
|
pub(crate) fn follow_requests<'a>(
|
|
&'a self,
|
|
) -> impl Iterator<Item = FollowRequestView<'a>> + 'a {
|
|
self.follow_requests.iter().filter_map(move |fr_id| {
|
|
let profile_id = self.fr_profile_hm.get(fr_id)?;
|
|
let profile = self.profile_hm.get(profile_id)?;
|
|
let icon = profile.icon().and_then(|icon| self.file_hm.get(&icon));
|
|
|
|
Some(FollowRequestView {
|
|
id: *fr_id,
|
|
profile,
|
|
icon,
|
|
})
|
|
})
|
|
}
|
|
|
|
fn clear_path(&self) -> &'static str {
|
|
"/notifications/clear"
|
|
}
|
|
|
|
pub(crate) fn clear_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::primary(&fl!(loader, "notification-clear-button")).form(self.clear_path())
|
|
}
|
|
|
|
fn reject_path(&self) -> &'static str {
|
|
"/notifications/follow-requests/reject-all"
|
|
}
|
|
|
|
pub(crate) fn reject_all_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::primary_outline(&fl!(loader, "follow-reject-all")).form(self.reject_path())
|
|
}
|
|
|
|
fn accept_path(&self) -> &'static str {
|
|
"/notifications/follow-requests/accept-all"
|
|
}
|
|
|
|
pub(crate) fn accept_all_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::primary(&fl!(loader, "follow-accept-all")).form(self.accept_path())
|
|
}
|
|
|
|
fn clear_comments_path(&self) -> &'static str {
|
|
"/notifications/comments/clear-all"
|
|
}
|
|
|
|
pub(crate) fn clear_comments_button(&self, loader: &ActixLoader) -> Button {
|
|
Button::primary(&fl!(loader, "notification-comment-clear")).form(self.clear_comments_path())
|
|
}
|
|
|
|
pub(crate) fn count(&self) -> u64 {
|
|
self.count
|
|
}
|
|
|
|
pub(crate) fn has_follow_requests(&self) -> bool {
|
|
!self.follow_requests.is_empty()
|
|
}
|
|
|
|
pub(crate) fn has_comments(&self) -> bool {
|
|
!self.comments.is_empty()
|
|
}
|
|
|
|
async fn build(profile_id: Uuid, state: &State) -> Result<Self, Error> {
|
|
let count = total_for_profile(profile_id, state).await?;
|
|
|
|
let store = state.profiles.clone();
|
|
|
|
let view = web::block(move || {
|
|
let mut view = NotificationsView {
|
|
comment_hm: HashMap::new(),
|
|
profile_hm: HashMap::new(),
|
|
file_hm: HashMap::new(),
|
|
submission_hm: HashMap::new(),
|
|
fr_profile_hm: HashMap::new(),
|
|
comments: vec![],
|
|
follow_requests: vec![],
|
|
count,
|
|
self_id: profile_id,
|
|
};
|
|
|
|
for comment_id in store.store.view.comments.for_profile(profile_id) {
|
|
if let Some(comment) = store.store.comments.by_id(comment_id)? {
|
|
if !view.profile_hm.contains_key(&comment.profile_id()) {
|
|
let profile = store.store.profiles.by_id(comment.profile_id())?.req()?;
|
|
view.profile_hm.insert(profile.id(), profile);
|
|
}
|
|
if !view.submission_hm.contains_key(&comment.submission_id()) {
|
|
let submission = store
|
|
.store
|
|
.submissions
|
|
.by_id(comment.submission_id())?
|
|
.req()?;
|
|
view.submission_hm.insert(submission.id(), submission);
|
|
}
|
|
if let Some(id) = comment.comment_id() {
|
|
if !view.profile_hm.contains_key(&id) {
|
|
if let Some(comment) = store.store.comments.by_id(id)? {
|
|
view.comment_hm.insert(comment.id(), comment);
|
|
}
|
|
}
|
|
}
|
|
|
|
view.comments.push(comment.id());
|
|
view.comment_hm.insert(comment.id(), comment);
|
|
}
|
|
}
|
|
|
|
for fr_id in store
|
|
.store
|
|
.view
|
|
.follow_request_notifs
|
|
.for_profile(profile_id)
|
|
{
|
|
if let Some(follow_req) = store.store.view.follow_requests.by_id(fr_id)? {
|
|
let icon = if let Some(profile) = view.profile_hm.get(&follow_req.right) {
|
|
profile.icon()
|
|
} else {
|
|
let profile = store.store.profiles.by_id(follow_req.right)?.req()?;
|
|
let icon = profile.icon();
|
|
view.profile_hm.insert(profile.id(), profile);
|
|
icon
|
|
};
|
|
|
|
if let Some(file_id) = icon {
|
|
if !view.file_hm.contains_key(&file_id) {
|
|
if let Some(file) = store.store.files.by_id(file_id)? {
|
|
view.file_hm.insert(file.id(), file);
|
|
}
|
|
}
|
|
}
|
|
|
|
view.fr_profile_hm.insert(follow_req.id, follow_req.right);
|
|
view.follow_requests.push(follow_req.id);
|
|
}
|
|
}
|
|
|
|
Ok(view)
|
|
})
|
|
.await?;
|
|
|
|
Ok(view)
|
|
}
|
|
}
|
|
|
|
pub(crate) async fn total_for_profile(profile_id: Uuid, state: &State) -> Result<u64, Error> {
|
|
let follow_requests = state.profiles.store.view.follow_request_notifs.clone();
|
|
let comments = state.profiles.store.view.comments.clone();
|
|
let reacts = state.profiles.store.view.reacts.clone();
|
|
|
|
let count = web::block(move || {
|
|
let count = follow_requests.count(profile_id)?
|
|
+ comments.count(profile_id)?
|
|
+ reacts.count(profile_id)?;
|
|
Ok(count)
|
|
})
|
|
.await?;
|
|
|
|
Ok(count)
|
|
}
|
|
|
|
async fn notifications_page(
|
|
loader: ActixLoader,
|
|
profile: UserProfile,
|
|
nav_state: NavState,
|
|
state: web::Data<State>,
|
|
) -> Result<HttpResponse, Error> {
|
|
let profile = profile.0;
|
|
let view = NotificationsView::build(profile.id(), &state).await?;
|
|
|
|
crate::rendered(HttpResponse::Ok(), |cursor| {
|
|
crate::templates::notifications::index(cursor, &loader, &view, &nav_state)
|
|
})
|
|
}
|
|
|
|
async fn update_notifications(_: NavState) -> Result<HttpResponse, Error> {
|
|
Ok(to_notifications_page())
|
|
}
|
|
|
|
fn to_notifications_page() -> HttpResponse {
|
|
crate::redirect("/notifications")
|
|
}
|