add: notifications for likes
TODO: notifications ui
This commit is contained in:
parent
6413ed09fb
commit
9dc75d7095
9 changed files with 292 additions and 27 deletions
|
@ -600,6 +600,10 @@ nav .button:not(.title):not(.active):hover {
|
||||||
top: unset;
|
top: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding-bottom: 72px;
|
||||||
|
}
|
||||||
|
|
||||||
nav button:not(.dropdown *),
|
nav button:not(.dropdown *),
|
||||||
nav .button:not(.dropdown *) {
|
nav .button:not(.dropdown *) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
@ -25,18 +25,6 @@
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<span class="fade">{{ community.title }}</span>
|
<span class="fade">{{ community.title }}</span>
|
||||||
|
|
||||||
{% if user %}
|
|
||||||
<div
|
|
||||||
class="flex gap-1 reactions_box"
|
|
||||||
hook="check_reactions"
|
|
||||||
hook-arg:id="{{ community.id }}"
|
|
||||||
>
|
|
||||||
{{ components::likes(id=community.id,
|
|
||||||
asset_type="Community", likes=community.likes,
|
|
||||||
dislikes=community.dislikes) }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -69,7 +57,51 @@
|
||||||
<div
|
<div
|
||||||
id="manage_fields"
|
id="manage_fields"
|
||||||
class="flex flex-col gap-2"
|
class="flex flex-col gap-2"
|
||||||
></div>
|
>
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card small">
|
||||||
|
<b>Read access</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<select
|
||||||
|
onchange="save_access(event, 'read')"
|
||||||
|
>
|
||||||
|
<option value="Everybody">
|
||||||
|
Everybody
|
||||||
|
</option>
|
||||||
|
<option value="Unlisted">
|
||||||
|
Unlisted
|
||||||
|
</option>
|
||||||
|
<option value="Private">
|
||||||
|
Private
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-nest">
|
||||||
|
<div class="card small">
|
||||||
|
<b>Write access</b>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<select
|
||||||
|
onchange="save_access(event, 'write')"
|
||||||
|
>
|
||||||
|
<option value="Everybody">
|
||||||
|
Everybody
|
||||||
|
</option>
|
||||||
|
<option value="Joined">
|
||||||
|
Joined
|
||||||
|
</option>
|
||||||
|
<option value="Owner">
|
||||||
|
Owner only
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="margin" />
|
<hr class="margin" />
|
||||||
|
|
||||||
|
@ -96,7 +128,7 @@
|
||||||
document.getElementById("manage_fields"),
|
document.getElementById("manage_fields"),
|
||||||
[
|
[
|
||||||
[
|
[
|
||||||
["display_name", "Title"],
|
["display_name", "Display title"],
|
||||||
"{{ community.context.display_name }}",
|
"{{ community.context.display_name }}",
|
||||||
"input",
|
"input",
|
||||||
],
|
],
|
||||||
|
@ -111,7 +143,7 @@
|
||||||
|
|
||||||
window.save_context = () => {
|
window.save_context = () => {
|
||||||
fetch(
|
fetch(
|
||||||
`/api/v1/communities/{{ community.id }}/context`,
|
"/api/v1/communities/{{ community.id }}/context",
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
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);
|
}, 250);
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -159,6 +216,28 @@
|
||||||
<span class="notification chip">Created</span>
|
<span class="notification chip">Created</span>
|
||||||
<span class="date">{{ community.created }}</span>
|
<span class="date">{{ community.created }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full flex justify-between items-center">
|
||||||
|
<span class="notification chip">Score</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<b
|
||||||
|
>{{ community.likes - community.dislikes
|
||||||
|
}}</b
|
||||||
|
>
|
||||||
|
{% if user %}
|
||||||
|
<div
|
||||||
|
class="flex gap-1 reactions_box"
|
||||||
|
hook="check_reactions"
|
||||||
|
hook-arg:id="{{ community.id }}"
|
||||||
|
>
|
||||||
|
{{ components::likes(id=community.id,
|
||||||
|
asset_type="Community",
|
||||||
|
likes=community.likes,
|
||||||
|
dislikes=community.dislikes) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod communities;
|
pub mod communities;
|
||||||
|
pub mod notifications;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
@ -120,6 +121,16 @@ pub fn routes() -> Router {
|
||||||
"/auth/profile/find/{id}",
|
"/auth/profile/find/{id}",
|
||||||
get(auth::profile::redirect_from_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)]
|
#[derive(Deserialize)]
|
||||||
|
@ -182,3 +193,8 @@ pub struct CreateReaction {
|
||||||
pub struct UpdateUserIsVerified {
|
pub struct UpdateUserIsVerified {
|
||||||
pub is_verified: bool,
|
pub is_verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateNotificationRead {
|
||||||
|
pub read: bool,
|
||||||
|
}
|
||||||
|
|
70
crates/app/src/routes/api/v1/notifications.rs
Normal file
70
crates/app/src/routes/api/v1/notifications.rs
Normal file
|
@ -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<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> 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<State>,
|
||||||
|
) -> 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<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
Json(req): Json<UpdateNotificationRead>,
|
||||||
|
) -> 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()),
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,12 +61,10 @@ pub async fn create_request(
|
||||||
|
|
||||||
// create reaction
|
// create reaction
|
||||||
match data
|
match data
|
||||||
.create_reaction(Reaction::new(
|
.create_reaction(
|
||||||
user.id,
|
Reaction::new(user.id, asset_id, req.asset_type, req.is_like),
|
||||||
asset_id,
|
&user,
|
||||||
req.asset_type,
|
)
|
||||||
req.is_like,
|
|
||||||
))
|
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(_) => Json(ApiReturn {
|
Ok(_) => Json(ApiReturn {
|
||||||
|
|
|
@ -3,5 +3,6 @@ CREATE TABLE IF NOT EXISTS notifications (
|
||||||
created INTEGER NOT NULL,
|
created INTEGER NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
owner INTEGER NOT NULL
|
owner INTEGER NOT NULL,
|
||||||
|
read INTEGER NOT NULL
|
||||||
)
|
)
|
||||||
|
|
|
@ -21,6 +21,7 @@ impl DataManager {
|
||||||
title: get!(x->2(String)),
|
title: get!(x->2(String)),
|
||||||
content: get!(x->3(String)),
|
content: get!(x->3(String)),
|
||||||
owner: get!(x->4(isize)) as usize,
|
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!(
|
let res = execute!(
|
||||||
&conn,
|
&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.id.to_string().as_str(),
|
||||||
&data.created.to_string().as_str(),
|
&data.created.to_string().as_str(),
|
||||||
&data.title.to_string().as_str(),
|
&data.title.to_string().as_str(),
|
||||||
&data.content.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(())
|
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?;
|
let notification = self.get_notification_by_id(id).await?;
|
||||||
|
|
||||||
if user.id != notification.owner {
|
if user.id != notification.owner {
|
||||||
|
@ -114,4 +116,61 @@ impl DataManager {
|
||||||
// return
|
// return
|
||||||
Ok(())
|
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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use super::*;
|
||||||
use crate::cache::Cache;
|
use crate::cache::Cache;
|
||||||
use crate::model::{
|
use crate::model::{
|
||||||
Error, Result,
|
Error, Result,
|
||||||
auth::User,
|
auth::{Notification, User},
|
||||||
permissions::FinePermission,
|
permissions::FinePermission,
|
||||||
reactions::{AssetType, Reaction},
|
reactions::{AssetType, Reaction},
|
||||||
};
|
};
|
||||||
|
@ -61,7 +61,7 @@ impl DataManager {
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `data` - a mock [`Reaction`] object to insert
|
/// * `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 {
|
let conn = match self.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||||
|
@ -95,6 +95,24 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
} {
|
} {
|
||||||
return Err(e);
|
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 => {
|
AssetType::Post => {
|
||||||
|
@ -106,6 +124,24 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
} {
|
} {
|
||||||
return Err(e);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -114,6 +114,7 @@ pub struct Notification {
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub owner: usize,
|
pub owner: usize,
|
||||||
|
pub read: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Notification {
|
impl Notification {
|
||||||
|
@ -128,6 +129,7 @@ impl Notification {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
owner,
|
owner,
|
||||||
|
read: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue