Add notifications page to manage follow requests
This commit is contained in:
parent
0a64034ea9
commit
c9b0c1a7cf
|
@ -17,8 +17,7 @@ base64 = "0.13.0"
|
|||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
env_logger = "0.8.2"
|
||||
event-listener = "2.5.1"
|
||||
futures-core = "0.3.8"
|
||||
futures-util = "0.3.8"
|
||||
futures = "0.3.11"
|
||||
hyaenidae-accounts = { version = "0.1.0", path = "../accounts" }
|
||||
hyaenidae-profiles = { version = "0.1.0", path = "../profiles" }
|
||||
hyaenidae-toolkit = { version = "0.1.0", path = "../toolkit" }
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
};
|
||||
use actix_web::{dev::Payload, web, FromRequest, HttpRequest, HttpResponse, Scope};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use hyaenidae_accounts::{State as AccountState, User};
|
||||
use hyaenidae_profiles::store::ReportKind;
|
||||
use hyaenidae_toolkit::{Select, TextInput};
|
||||
|
|
|
@ -4,7 +4,7 @@ use background_jobs::{
|
|||
create_server, memory_storage::Storage, ActixJob, Backoff, MaxRetries, QueueHandle,
|
||||
WorkerConfig,
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use hyaenidae_profiles::Spawner;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
|
|
@ -20,6 +20,7 @@ mod error;
|
|||
mod images;
|
||||
mod jobs;
|
||||
mod nav;
|
||||
mod notifications;
|
||||
mod profiles;
|
||||
mod submissions;
|
||||
|
||||
|
@ -128,6 +129,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
.service(submissions::scope())
|
||||
.service(comments::scope())
|
||||
.service(admin::scope())
|
||||
.service(notifications::scope())
|
||||
.default_service(web::route().to(|| async move { to_404() }))
|
||||
})
|
||||
.bind(config.bind_address)?
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use crate::{admin::Admin, profiles::Profile};
|
||||
use actix_web::{dev::Payload, web::Query, FromRequest, HttpRequest};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use crate::{admin::Admin, notifications::total_for_profile, profiles::Profile, State};
|
||||
use actix_web::{
|
||||
dev::Payload,
|
||||
web::{Data, Query},
|
||||
FromRequest, HttpRequest,
|
||||
};
|
||||
use futures::future::LocalBoxFuture;
|
||||
use hyaenidae_accounts::LogoutState;
|
||||
use hyaenidae_toolkit::Button;
|
||||
|
||||
|
@ -15,12 +19,14 @@ impl FromRequest for NavState {
|
|||
let query = Option::<Query<Vec<(String, String)>>>::extract(req);
|
||||
let admin = Option::<Admin>::extract(req);
|
||||
let path = req.uri().path().to_owned();
|
||||
let state = Data::<State>::extract(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let profile = profile.await?;
|
||||
let logout = logout.await?;
|
||||
let query = query.await?;
|
||||
let admin = admin.await?;
|
||||
let state = state.await?;
|
||||
|
||||
let dark = true;
|
||||
let mut nav = vec![];
|
||||
|
@ -35,7 +41,7 @@ impl FromRequest for NavState {
|
|||
account.href("/session/account").dark(dark);
|
||||
logout.dark(dark);
|
||||
|
||||
let profile = if let Some(profile) = profile {
|
||||
let profile_btn = if let Some(profile) = profile.as_ref() {
|
||||
let btn = Button::secondary("Profile");
|
||||
btn.href(&profile.view_path()).dark(dark);
|
||||
btn
|
||||
|
@ -46,7 +52,17 @@ impl FromRequest for NavState {
|
|||
};
|
||||
|
||||
nav.push(submission);
|
||||
nav.push(profile);
|
||||
nav.push(profile_btn);
|
||||
if let Some(profile) = profile.as_ref() {
|
||||
if let Ok(count) = total_for_profile(profile.id(), &state).await {
|
||||
if count > 0 {
|
||||
let btn = Button::secondary("Notifications");
|
||||
btn.href("/notifications").dark(dark);
|
||||
nav.push(btn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nav.push(account);
|
||||
if admin.is_some() {
|
||||
let admin = Button::secondary("Admin");
|
||||
|
|
486
server/src/notifications.rs
Normal file
486
server/src/notifications.rs
Normal file
|
@ -0,0 +1,486 @@
|
|||
use crate::{comments::Comment, error::Error, nav::NavState, profiles::Profile, State};
|
||||
use actix_web::{web, HttpResponse, Scope};
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use hyaenidae_toolkit::{Button, Link};
|
||||
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: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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: Profile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
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: Profile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
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: Profile, state: web::Data<State>) -> Result<HttpResponse, Error> {
|
||||
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: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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: Profile,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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> {
|
||||
comment: &'a Comment,
|
||||
author: &'a Profile,
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl<'a> CommentView<'a> {
|
||||
fn submission_path(&self) -> Option<String> {
|
||||
if self.comment.comment_id().is_none() {
|
||||
Some(format!("/submissions/{}", self.comment.submission_id()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn submission_link(&self, dark: bool) -> Option<Link> {
|
||||
self.submission_path().map(|path| {
|
||||
let mut link = Link::new_tab(&path);
|
||||
link.plain(true).dark(dark);
|
||||
link
|
||||
})
|
||||
}
|
||||
|
||||
fn reply_to_path(&self) -> Option<String> {
|
||||
if let Some(reply_to_id) = self.comment.comment_id() {
|
||||
Some(format!("/comments/{}", reply_to_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn reply_to_link(&self, dark: bool) -> Option<Link> {
|
||||
self.reply_to_path().map(|path| {
|
||||
let mut link = Link::new_tab(&path);
|
||||
link.plain(true).dark(dark);
|
||||
link
|
||||
})
|
||||
}
|
||||
|
||||
fn comment_path(&self) -> String {
|
||||
format!("/comments/{}", self.comment.id())
|
||||
}
|
||||
|
||||
pub(crate) fn view_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary("View");
|
||||
btn.href(&self.comment_path()).new_tab().dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
fn remove_path(&self) -> String {
|
||||
format!("/notifications/comments/{}/remove", self.id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary_outline("Remove");
|
||||
btn.form(&self.remove_path()).dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
pub(crate) fn author_name(&self) -> String {
|
||||
self.author.name()
|
||||
}
|
||||
|
||||
pub(crate) fn author_link(&self, dark: bool) -> Link {
|
||||
let mut link = Link::new_tab(&self.author.view_path());
|
||||
link.plain(true).dark(dark);
|
||||
link
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FollowRequestView<'a> {
|
||||
pub(crate) profile: &'a Profile,
|
||||
id: Uuid,
|
||||
}
|
||||
|
||||
impl<'a> FollowRequestView<'a> {
|
||||
pub(crate) fn accept_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary("Accept");
|
||||
btn.form(&format!(
|
||||
"/notifications/follow-requests/{}/accept",
|
||||
self.id
|
||||
))
|
||||
.dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
pub(crate) fn reject_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary_outline("Reject");
|
||||
btn.form(&format!(
|
||||
"/notifications/follow-requests/{}/reject",
|
||||
self.id
|
||||
))
|
||||
.dark(dark);
|
||||
btn
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NotificationsView {
|
||||
comment_hm: HashMap<Uuid, Comment>,
|
||||
profile_hm: HashMap<Uuid, Profile>,
|
||||
fr_profile_hm: HashMap<Uuid, Uuid>,
|
||||
comments: Vec<Uuid>,
|
||||
follow_requests: Vec<Uuid>,
|
||||
count: u64,
|
||||
}
|
||||
|
||||
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())?;
|
||||
|
||||
Some(CommentView {
|
||||
comment,
|
||||
author,
|
||||
id: *comment_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)?;
|
||||
|
||||
Some(FollowRequestView {
|
||||
id: *fr_id,
|
||||
profile,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_path(&self) -> &'static str {
|
||||
"/notifications/clear"
|
||||
}
|
||||
|
||||
pub(crate) fn clear_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary("Clear All");
|
||||
btn.form(self.clear_path()).dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
fn reject_path(&self) -> &'static str {
|
||||
"/notifications/follow-requests/reject-all"
|
||||
}
|
||||
|
||||
pub(crate) fn reject_all_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary_outline("Reject All");
|
||||
btn.form(self.reject_path()).dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
fn accept_path(&self) -> &'static str {
|
||||
"/notifications/follow-requests/accept-all"
|
||||
}
|
||||
|
||||
pub(crate) fn accept_all_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary("Accept All");
|
||||
btn.form(self.accept_path()).dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
fn clear_comments_path(&self) -> &'static str {
|
||||
"/notifications/comments/clear-all"
|
||||
}
|
||||
|
||||
pub(crate) fn clear_comments_button(&self, dark: bool) -> Button {
|
||||
let btn = Button::primary("Clear Comments");
|
||||
btn.form(self.clear_comments_path()).dark(dark);
|
||||
btn
|
||||
}
|
||||
|
||||
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 comment_store = state.profiles.store.comments.clone();
|
||||
let profile_store = state.profiles.store.profiles.clone();
|
||||
let file_store = state.profiles.store.files.clone();
|
||||
let follow_request_store = state.profiles.store.view.follow_requests.clone();
|
||||
let comment_notifs = state.profiles.store.view.comments.clone();
|
||||
let follow_request_notifs = state.profiles.store.view.follow_request_notifs.clone();
|
||||
|
||||
let view = web::block(move || {
|
||||
let mut view = NotificationsView {
|
||||
comment_hm: HashMap::new(),
|
||||
profile_hm: HashMap::new(),
|
||||
fr_profile_hm: HashMap::new(),
|
||||
comments: vec![],
|
||||
follow_requests: vec![],
|
||||
count,
|
||||
};
|
||||
|
||||
for comment_id in comment_notifs.for_profile(profile_id) {
|
||||
if let Some(comment) = comment_store.by_id(comment_id)? {
|
||||
if !view.profile_hm.contains_key(&comment.profile_id()) {
|
||||
let profile = Profile::from_stores(
|
||||
comment.profile_id(),
|
||||
&profile_store,
|
||||
&file_store,
|
||||
)?;
|
||||
view.profile_hm.insert(profile.id(), profile);
|
||||
}
|
||||
|
||||
view.comments.push(comment.id());
|
||||
view.comment_hm.insert(comment.id(), comment);
|
||||
}
|
||||
}
|
||||
|
||||
for fr_id in follow_request_notifs.for_profile(profile_id) {
|
||||
if let Some(follow_req) = follow_request_store.by_id(fr_id)? {
|
||||
if !view.profile_hm.contains_key(&follow_req.right) {
|
||||
let profile =
|
||||
Profile::from_stores(follow_req.right, &profile_store, &file_store)?;
|
||||
view.profile_hm.insert(profile.id(), profile);
|
||||
}
|
||||
|
||||
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(
|
||||
profile: Profile,
|
||||
nav_state: NavState,
|
||||
state: web::Data<State>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let view = NotificationsView::build(profile.id(), &state).await?;
|
||||
|
||||
crate::rendered(HttpResponse::Ok(), |cursor| {
|
||||
crate::templates::notifications::index(cursor, &view, &nav_state)
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_notifications(_: NavState) -> Result<HttpResponse, Error> {
|
||||
Ok(to_notifications_page())
|
||||
}
|
||||
|
||||
fn to_notifications_page() -> HttpResponse {
|
||||
crate::redirect("/notifications")
|
||||
}
|
|
@ -7,8 +7,7 @@ use actix_web::{
|
|||
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
|
||||
};
|
||||
use event_listener::Event;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::future::{ok, Ready};
|
||||
use futures::future::{ok, LocalBoxFuture, Ready};
|
||||
use hyaenidae_accounts::Authenticated;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
|
|
|
@ -1218,7 +1218,11 @@ async fn profile_buttons(
|
|||
buttons.push(edit);
|
||||
buttons.push(switch);
|
||||
} else if is_follower || is_follow_requested {
|
||||
let unfollow = Button::secondary("Unfollow");
|
||||
let unfollow = if is_follower {
|
||||
Button::secondary("Unfollow")
|
||||
} else {
|
||||
Button::secondary("Remove Request")
|
||||
};
|
||||
let block = Button::secondary("Block");
|
||||
let report = Button::primary_outline("Report");
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use actix_web::{
|
|||
web::{Data, Path},
|
||||
FromRequest, HttpRequest,
|
||||
};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures::future::LocalBoxFuture;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
|
|
75
server/templates/notifications/index.rs.html
Normal file
75
server/templates/notifications/index.rs.html
Normal file
|
@ -0,0 +1,75 @@
|
|||
@use crate::nav::NavState;
|
||||
@use crate::notifications::NotificationsView;
|
||||
@use crate::templates::layouts::home;
|
||||
@use crate::templates::submissions::profile_box;
|
||||
@use hyaenidae_toolkit::templates::button_group;
|
||||
@use hyaenidae_toolkit::{templates::{card, card_body, card_title}, Card};
|
||||
@use hyaenidae_toolkit::templates::link;
|
||||
|
||||
@(view: &NotificationsView, nav_state: &NavState)
|
||||
|
||||
@:home(&format!("Notifications: {}", view.count()), "Notifications on Hyaenidae", nav_state, {}, {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Clear All })
|
||||
@:card_body({
|
||||
<p>This will clear all notifications except for Follow Requests</p>
|
||||
})
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
&view.clear_button(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
@if view.has_follow_requests() {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Follow Requests })
|
||||
@for fr in view.follow_requests() {
|
||||
@:card_body({
|
||||
@:profile_box(fr.profile, None, nav_state.dark(), {
|
||||
@:button_group(&[
|
||||
&fr.accept_button(nav_state.dark()),
|
||||
&fr.reject_button(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
}
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
&view.accept_all_button(nav_state.dark()),
|
||||
&view.reject_all_button(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
}
|
||||
@if view.has_comments() {
|
||||
@:card(Card::full_width().dark(nav_state.dark()), {
|
||||
@:card_title({ Comments })
|
||||
@for c in view.comments() {
|
||||
@:card_body({
|
||||
@:link(&c.author_link(nav_state.dark()), {
|
||||
@c.author_name()
|
||||
})
|
||||
@if let Some(l) = c.submission_link(nav_state.dark()) {
|
||||
commented on your
|
||||
@:link(&l, { submission })
|
||||
}
|
||||
@if let Some(l) = c.reply_to_link(nav_state.dark()) {
|
||||
replied to your
|
||||
@:link(&l, { comment })
|
||||
}
|
||||
<div class="button-section">
|
||||
@:button_group(&[
|
||||
&c.view_button(nav_state.dark()),
|
||||
&c.remove_button(nav_state.dark()),
|
||||
])
|
||||
</div>
|
||||
})
|
||||
}
|
||||
@:card_body({
|
||||
@:button_group(&[
|
||||
&view.clear_comments_button(nav_state.dark()),
|
||||
])
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
|
@ -1,10 +1,11 @@
|
|||
@use crate::templates::profiles::icon;
|
||||
@use crate::{profiles::Profile, submissions::Submission};
|
||||
@use crate::profiles::Profile;
|
||||
@use chrono::{DateTime, Utc};
|
||||
@use hyaenidae_toolkit::{templates::link, Link};
|
||||
@use hyaenidae_toolkit::templates::ago;
|
||||
@use hyaenidae_toolkit::templates::icon as tkicon;
|
||||
|
||||
@(profile: &Profile, submission: &Submission, dark: bool, body: Content)
|
||||
@(profile: &Profile, published: Option<DateTime<Utc>>, dark: bool, body: Content)
|
||||
|
||||
<div class="profile-box">
|
||||
@:tkicon(&profile.view_path(), true, dark, {
|
||||
|
@ -27,7 +28,7 @@
|
|||
@profile.full_handle()
|
||||
})
|
||||
</div>
|
||||
@if let Some(published) = submission.published() {
|
||||
@if let Some(published) = published {
|
||||
<div class="profile-box--meta--date">
|
||||
posted
|
||||
@:ago(published, dark)
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
</div>
|
||||
})
|
||||
@:card_body({
|
||||
@:profile_box(&view.poster, &view.submission, nav_state.dark(), {
|
||||
@:profile_box(&view.poster, view.submission.published(), nav_state.dark(), {
|
||||
@if let Some(description) = view.submission.description() {
|
||||
@description
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue