add: message notifications

This commit is contained in:
trisua 2025-09-07 10:44:14 -04:00
parent ca1eca967c
commit 361d3d8e30
9 changed files with 100 additions and 13 deletions

8
Cargo.lock generated
View file

@ -2998,7 +2998,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tawny"
version = "1.0.3"
version = "1.0.4"
dependencies = [
"ammonia",
"axum",
@ -3015,7 +3015,7 @@ dependencies = [
"serde",
"serde_json",
"tera",
"tetratto-core 16.0.2",
"tetratto-core 16.0.3",
"tetratto-shared",
"tokio",
"toml 0.9.5",
@ -3098,9 +3098,9 @@ dependencies = [
[[package]]
name = "tetratto-core"
version = "16.0.2"
version = "16.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "380eed8dec18b0dcda3440d47375a1bacf94e42fdcd93d464e27682d005bf356"
checksum = "9e3e81378d7f02a6f7d18bf9ca58e3885c6cb8611ca4d0536c76b320e6e4017a"
dependencies = [
"async-recursion",
"base16ct",

View file

@ -1,6 +1,6 @@
[package]
name = "tawny"
version = "1.0.3"
version = "1.0.4"
edition = "2024"
authors = ["trisuaso"]
repository = "https://trisua.com/t/tawny"
@ -8,7 +8,7 @@ license = "AGPL-3.0-or-later"
homepage = "https://tawny.cc"
[dependencies]
tetratto-core = "16.0.2"
tetratto-core = "16.0.3"
tetratto-shared = "12.0.6"
tokio = { version = "1.47.1", features = ["macros", "rt-multi-thread"] }
pathbufd = "0.1.4"

View file

@ -12,6 +12,7 @@ const STATE = {
function create_streamer(chat_id, hook_element) {
STATE.chat_id = chat_id;
STATE.stream_element = hook_element.parentElement;
clear_notifications();
STATE.observer = new IntersectionObserver(
(entries) => {
@ -112,6 +113,10 @@ function sock_con() {
if (msg.method === "MessageCreate") {
render_message(msg.body);
setTimeout(() => {
clear_notifications();
}, 150);
} else if (msg.method === "MessageDelete") {
if (document.getElementById(`message_${msg.body}`)) {
document.getElementById(`message_${msg.body}`).remove();
@ -420,3 +425,15 @@ function clear_replying_to() {
STATE.replying_to = undefined;
document.getElementById("replying_to_zone").classList.add("hidden");
}
function clear_notifications() {
fetch(`/api/v1/chats/${STATE.chat_id}/notifications`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
if (!res.ok) {
show_message(res.message, res.ok);
}
});
}

View file

@ -837,3 +837,22 @@ menu.col {
.message .body p:last-of-type {
margin: 0 !important;
}
.message_reply_wrapper .message {
opacity: 75%;
padding: 0 4px;
& .body {
min-height: unset;
height: 26px !important;
overflow: hidden;
}
& p {
font-size: 10px;
}
& .avatar {
--size: 18px !important;
}
}

View file

@ -111,7 +111,7 @@
(text "{% if replying_to -%}")
(div
("style" "transform: scale(0.8); opacity: 75%; width: 110%")
("class" "message_reply_wrapper")
(text "{{ self::message(message=replying_to, hide_actions=true) }}"))
(text "{%- endif %}")
(text "{%- endmacro %}")

View file

@ -58,7 +58,9 @@
("class" "card_nest w_full")
("style" "max-width: 25rem")
(div
("class" "card banner"))
("class" "card banner")
(img
("src" "{{ config.service_hosts.buckets }}/banners/{{ profile.id }}")))
(div
("class" "card flex flex_col gap_ch")
(text "{{ components::avatar(id=profile.id, size=\"160px\") }}")
@ -134,12 +136,17 @@
}
.profile .banner {
background-image: url(\"{{ config.service_hosts.buckets }}/banners/{{ profile.id }}\") !important;
background-repeat: no-repeat !important;
background-position: center !important;
background-size: cover !important;
border-radius: var(--radius) var(--radius) 0 0;
height: 225px;
overflow: hidden;
padding: 0 !important;
}
.profile .banner img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.card_nest .card:nth-child(2) {

View file

@ -540,3 +540,29 @@ pub async fn remove_pin_request(
Err(e) => Json(e.into()),
}
}
pub async fn clear_chat_notifications(
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.2) {
Some(x) => x,
None => return Json(Error::NotAllowed.into()),
};
if let Err(e) = data
.2
.delete_all_notifications_by_tag(&user, &id.to_string())
.await
{
return Json(e.into());
}
Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: (),
})
}

View file

@ -6,7 +6,7 @@ use axum_extra::extract::CookieJar;
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
use buckets_core::model::{MediaType, MediaUpload};
use serde::Deserialize;
use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission};
use tetratto_core::model::{ApiReturn, Error, auth::Notification, permissions::FinePermission};
#[derive(Deserialize)]
pub struct CreateMessage {
@ -121,6 +121,20 @@ pub async fn create_request(
if let Err(e) = data.2.incr_user_missed_messages(member).await {
return Json(e.into());
}
let mut notif = Notification::new(
"You've received a new message".to_string(),
format!(
"[@{}](/api/v1/auth/user/find/{}) has sent you a message in a [chat]({}/chats/{})",
user.username, user.id, data.0.0.host, chat.id
),
member,
);
notif.tag = chat.id.to_string();
if let Err(e) = data.2.create_notification(notif).await {
return Json(e.into());
}
}
// ...

View file

@ -38,6 +38,10 @@ pub fn routes() -> Router {
"/chats/{id}/pins/{message}",
delete(chats::remove_pin_request),
)
.route(
"/chats/{id}/notifications",
delete(chats::clear_chat_notifications),
)
// messages
.route("/messages/{id}", post(messages::create_request))
.route("/messages/{id}", delete(messages::delete_request))