diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 4bc2a12..c36bcb9 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -600,6 +600,10 @@ nav .button:not(.title):not(.active):hover { top: unset; } + body { + padding-bottom: 72px; + } + nav button:not(.dropdown *), nav .button:not(.dropdown *) { font-size: 12px; diff --git a/crates/app/src/public/html/communities/base.html b/crates/app/src/public/html/communities/base.html index 5bd84fd..3779ff3 100644 --- a/crates/app/src/public/html/communities/base.html +++ b/crates/app/src/public/html/communities/base.html @@ -25,18 +25,6 @@ {{ community.title }} - - {% if user %} -
- {{ components::likes(id=community.id, - asset_type="Community", likes=community.likes, - dislikes=community.dislikes) }} -
- {% endif %} @@ -69,7 +57,51 @@
+ > +
+
+ Read access +
+ +
+ +
+
+ +
+
+ Write access +
+ +
+ +
+
+
@@ -96,7 +128,7 @@ document.getElementById("manage_fields"), [ [ - ["display_name", "Title"], + ["display_name", "Display title"], "{{ community.context.display_name }}", "input", ], @@ -111,7 +143,7 @@ window.save_context = () => { fetch( - `/api/v1/communities/{{ community.id }}/context`, + "/api/v1/communities/{{ community.id }}/context", { method: "POST", headers: { @@ -131,6 +163,31 @@ ]); }); }; + + window.save_access = (event, mode) => { + const selected = + event.target.selectedOptions[0]; + fetch( + `/api/v1/communities/{{ community.id }}/access/${mode}`, + { + method: "POST", + headers: { + "Content-Type": + "application/json", + }, + body: JSON.stringify({ + access: selected.value, + }), + }, + ) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "success" : "error", + res.message, + ]); + }); + }; }, 250); {% endif %} @@ -159,6 +216,28 @@ Created {{ community.created }} + +
+ Score +
+ {{ community.likes - community.dislikes + }} + {% if user %} +
+ {{ components::likes(id=community.id, + asset_type="Community", + likes=community.likes, + dislikes=community.dislikes) }} +
+ {% endif %} +
+
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 09ae7d7..4a435f6 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -1,5 +1,6 @@ pub mod auth; pub mod communities; +pub mod notifications; pub mod reactions; use axum::{ @@ -120,6 +121,16 @@ pub fn routes() -> Router { "/auth/profile/find/{id}", get(auth::profile::redirect_from_id), ) + // notifications + .route( + "/notifications/my", + delete(notifications::delete_all_request), + ) + .route("/notifications/{id}", delete(notifications::delete_request)) + .route( + "/notifications/{id}/read", + delete(notifications::update_read_status_request), + ) } #[derive(Deserialize)] @@ -182,3 +193,8 @@ pub struct CreateReaction { pub struct UpdateUserIsVerified { pub is_verified: bool, } + +#[derive(Deserialize)] +pub struct UpdateNotificationRead { + pub read: bool, +} diff --git a/crates/app/src/routes/api/v1/notifications.rs b/crates/app/src/routes/api/v1/notifications.rs new file mode 100644 index 0000000..e1df7db --- /dev/null +++ b/crates/app/src/routes/api/v1/notifications.rs @@ -0,0 +1,70 @@ +use axum::{Extension, Json, extract::Path, response::IntoResponse}; +use axum_extra::extract::CookieJar; +use tetratto_core::model::{ApiReturn, Error}; + +use crate::{State, get_user_from_token}; + +use super::UpdateNotificationRead; + +pub async fn delete_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.delete_notification(id, &user).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Notification deleted".to_string(), + payload: (), + }), + Err(e) => return Json(e.into()), + } +} + +pub async fn delete_all_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.delete_all_notifications(&user).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Notifications deleted".to_string(), + payload: (), + }), + Err(e) => return Json(e.into()), + } +} + +pub async fn update_read_status_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.update_notification_read(id, req.read, &user).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "Notification updated".to_string(), + payload: (), + }), + Err(e) => return Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/reactions.rs b/crates/app/src/routes/api/v1/reactions.rs index dcc4767..1c1a3fc 100644 --- a/crates/app/src/routes/api/v1/reactions.rs +++ b/crates/app/src/routes/api/v1/reactions.rs @@ -61,12 +61,10 @@ pub async fn create_request( // create reaction match data - .create_reaction(Reaction::new( - user.id, - asset_id, - req.asset_type, - req.is_like, - )) + .create_reaction( + Reaction::new(user.id, asset_id, req.asset_type, req.is_like), + &user, + ) .await { Ok(_) => Json(ApiReturn { diff --git a/crates/core/src/database/drivers/sql/create_notifications.sql b/crates/core/src/database/drivers/sql/create_notifications.sql index 7be3b27..01fdc62 100644 --- a/crates/core/src/database/drivers/sql/create_notifications.sql +++ b/crates/core/src/database/drivers/sql/create_notifications.sql @@ -3,5 +3,6 @@ CREATE TABLE IF NOT EXISTS notifications ( created INTEGER NOT NULL, title TEXT NOT NULL, content TEXT NOT NULL, - owner INTEGER NOT NULL + owner INTEGER NOT NULL, + read INTEGER NOT NULL ) diff --git a/crates/core/src/database/notifications.rs b/crates/core/src/database/notifications.rs index 117af17..b6820ac 100644 --- a/crates/core/src/database/notifications.rs +++ b/crates/core/src/database/notifications.rs @@ -21,6 +21,7 @@ impl DataManager { title: get!(x->2(String)), content: get!(x->3(String)), owner: get!(x->4(isize)) as usize, + read: if get!(x->5(i8)) == 1 { true } else { false }, } } @@ -59,13 +60,14 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO reactions VALUES ($1, $2, $3, $4, $5)", + "INSERT INTO notifications VALUES ($1, $2, $3, $4, $5, $6)", &[ &data.id.to_string().as_str(), &data.created.to_string().as_str(), &data.title.to_string().as_str(), &data.content.to_string().as_str(), - &data.owner.to_string().as_str() + &data.owner.to_string().as_str(), + &(if data.read { 1 } else { 0 }).to_string().as_str() ] ); @@ -80,7 +82,7 @@ impl DataManager { Ok(()) } - pub async fn delete_notification(&self, id: usize, user: User) -> Result<()> { + pub async fn delete_notification(&self, id: usize, user: &User) -> Result<()> { let notification = self.get_notification_by_id(id).await?; if user.id != notification.owner { @@ -114,4 +116,61 @@ impl DataManager { // return Ok(()) } + + pub async fn delete_all_notifications(&self, user: &User) -> Result<()> { + let notifications = self.get_notifications_by_owner(user.id).await?; + + for notification in notifications { + if user.id != notification.owner { + if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); + } + } + + self.delete_notification(notification.id, user).await? + } + + Ok(()) + } + + pub async fn update_notification_read( + &self, + id: usize, + new_read: bool, + user: &User, + ) -> Result<()> { + let y = self.get_notification_by_id(id).await?; + + if y.owner != user.id { + if !user.permissions.check(FinePermission::MANAGE_NOTIFICATIONS) { + return Err(Error::NotAllowed); + } + } + + // ... + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "UPDATE notifications SET read = $1 WHERE id = $2", + &[&(if new_read { 1 } else { 0 }).to_string(), &id.to_string()] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.notification:{}", id)).await; + + if (y.read == true) && (new_read == false) { + self.incr_user_notifications(user.id).await?; + } else if (y.read == false) && (new_read == true) { + self.decr_user_notifications(user.id).await?; + } + + Ok(()) + } } diff --git a/crates/core/src/database/reactions.rs b/crates/core/src/database/reactions.rs index e14b103..0827a3c 100644 --- a/crates/core/src/database/reactions.rs +++ b/crates/core/src/database/reactions.rs @@ -2,7 +2,7 @@ use super::*; use crate::cache::Cache; use crate::model::{ Error, Result, - auth::User, + auth::{Notification, User}, permissions::FinePermission, reactions::{AssetType, Reaction}, }; @@ -61,7 +61,7 @@ impl DataManager { /// /// # Arguments /// * `data` - a mock [`Reaction`] object to insert - pub async fn create_reaction(&self, data: Reaction) -> Result<()> { + pub async fn create_reaction(&self, data: Reaction, user: &User) -> Result<()> { let conn = match self.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), @@ -95,6 +95,24 @@ impl DataManager { } } { return Err(e); + } else if data.is_like { + let community = self.get_community_by_id(data.asset).await.unwrap(); + + if community.owner != user.id { + if let Err(e) = self + .create_notification(Notification::new( + "Your community has received a like!".to_string(), + format!( + "[@{}](/api/v1/auth/profile/find/{}) has liked your community!", + user.username, user.id + ), + community.owner, + )) + .await + { + return Err(e); + } + } } } AssetType::Post => { @@ -106,6 +124,24 @@ impl DataManager { } } { return Err(e); + } else if data.is_like { + let post = self.get_post_by_id(data.asset).await.unwrap(); + + if post.owner != user.id { + if let Err(e) = self + .create_notification(Notification::new( + "Your post has received a like!".to_string(), + format!( + "[@{}](/api/v1/auth/profile/find/{}) has liked your post!", + user.username, user.id + ), + post.owner, + )) + .await + { + return Err(e); + } + } } } }; diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 91b5aef..831e774 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -114,6 +114,7 @@ pub struct Notification { pub title: String, pub content: String, pub owner: usize, + pub read: bool, } impl Notification { @@ -128,6 +129,7 @@ impl Notification { title, content, owner, + read: false, } } }