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,
}
}
}