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, ) -> Result { 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(()) as Result<_, Error> }) .await??; Ok(to_notifications_page()) } async fn clear_comments( profile: UserProfile, state: web::Data, ) -> Result { 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(()) as Result<_, Error> }) .await??; Ok(to_notifications_page()) } async fn accept_all(profile: UserProfile, state: web::Data) -> Result { 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 || 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) -> Result { 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 || 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, profile: UserProfile, state: web::Data, ) -> Result { 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)) as Result }) .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, profile: UserProfile, state: web::Data, ) -> Result { 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)) as Result }) .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, profile: UserProfile, state: web::Data, ) -> Result { 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]))?) as Result<_, Error> }) .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 { self.parent.and_then(|c| { if c.profile_id() == self.self_id { Some(c.id()) } else { None } }) } fn submission_path(&self) -> Option { 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 { self.submission_path() .map(|path| Link::new_tab(&path).plain(true)) } fn reply_to_path(&self) -> Option { 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 { 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, profile_hm: HashMap, file_hm: HashMap, submission_hm: HashMap, fr_profile_hm: HashMap, comments: Vec, follow_requests: Vec, count: u64, self_id: Uuid, } impl NotificationsView { pub(crate) fn comments<'a>(&'a self) -> impl Iterator> + '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> + '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 { 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) as Result<_, Error> }) .await??; Ok(view) } } pub(crate) async fn total_for_profile(profile_id: Uuid, state: &State) -> Result { 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) as Result<_, Error> }) .await??; Ok(count) } async fn notifications_page( loader: ActixLoader, profile: UserProfile, nav_state: NavState, state: web::Data, ) -> Result { 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 { Ok(to_notifications_page()) } fn to_notifications_page() -> HttpResponse { crate::redirect("/notifications") }