add: chat pins

This commit is contained in:
trisua 2025-09-03 17:12:26 -04:00
parent 9546c580e7
commit 82eafdadb3
21 changed files with 330 additions and 56 deletions

2
Cargo.lock generated
View file

@ -2998,7 +2998,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tawny" name = "tawny"
version = "1.0.1" version = "1.0.2"
dependencies = [ dependencies = [
"ammonia", "ammonia",
"axum", "axum",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "tawny" name = "tawny"
version = "1.0.1" version = "1.0.2"
edition = "2024" edition = "2024"
authors = ["trisuaso"] authors = ["trisuaso"]
repository = "https://trisua.com/t/tawny" repository = "https://trisua.com/t/tawny"
@ -30,7 +30,10 @@ glob = "0.3.2"
serde_json = "1.0.142" serde_json = "1.0.142"
toml = "0.9.4" toml = "0.9.4"
regex = "1.11.1" regex = "1.11.1"
oiseau = { version = "0.1.2", default-features = false, features = ["postgres", "redis",] } oiseau = { version = "0.1.2", default-features = false, features = [
"postgres",
"redis",
] }
buckets-core = "1.0.4" buckets-core = "1.0.4"
axum-image = "0.1.1" axum-image = "0.1.1"
futures-util = "0.3.31" futures-util = "0.3.31"

View file

@ -326,3 +326,27 @@ function create_direct_chat_with_user(id) {
} }
}); });
} }
function pin_message(e, id) {
fetch(`/api/v1/chats/${STATE.chat_id}/pins/${id}`, {
method: "POST",
})
.then((res) => res.json())
.then((res) => {
show_message(res.message, res.ok);
if (res.ok) {
e.target.remove();
}
});
}
function unpin_message(id) {
fetch(`/api/v1/chats/${STATE.chat_id}/pins/${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((res) => {
show_message(res.message, res.ok);
});
}

View file

@ -116,6 +116,10 @@ article {
height: 100%; height: 100%;
} }
.tabs {
overflow: auto;
}
.fadein { .fadein {
animation: fadein ease-in-out 1 0.5s forwards running; animation: fadein ease-in-out 1 0.5s forwards running;
} }
@ -801,6 +805,7 @@ menu.col {
background: var(--color-surface); background: var(--color-surface);
color: var(--color-text); color: var(--color-text);
border-radius: var(--radius); border-radius: var(--radius);
min-height: 36px;
} }
.message.mine .body { .message.mine .body {
@ -812,3 +817,8 @@ menu.col {
.message:not(.mine) .body { .message:not(.mine) .body {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }
.message:hover .dropdown.hidden,
.message:focus .dropdown.hidden {
display: flex !important;
}

View file

@ -9,7 +9,7 @@
(a (a
("class" "button tab camo") ("class" "button tab camo")
("href" "/chats") ("href" "/chats")
(text "chats") (text "{{ icon \"castle\" }} chats")
(text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}")) (text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}"))
(a (a
("class" "button tab") ("class" "button tab")
@ -18,7 +18,7 @@
(a (a
("class" "button tab camo") ("class" "button tab camo")
("href" "/chats/{{ chat.id }}/manage") ("href" "/chats/{{ chat.id }}/manage")
(text "{{ icon \"settings-2\" }} Manage")))) (text "{{ icon \"settings-2\" }} manage"))))
(div (div
("class" "flex flex_col card_nest reverse") ("class" "flex flex_col card_nest reverse")
("style" "flex: 1 0 auto") ("style" "flex: 1 0 auto")
@ -29,10 +29,6 @@
("class" "card flex flex_rev_col gap_2") ("class" "card flex flex_rev_col gap_2")
("style" "flex: 1 0 auto") ("style" "flex: 1 0 auto")
("id" "messages_stream") ("id" "messages_stream")
(text "{% if messages|length == 0 -%}")
(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!"))
(text "{%- endif %}")
(div ("ui_ident" "data_marker"))) (div ("ui_ident" "data_marker")))
(div ("id" "read_receipt_zone") ("class" "card") ("style" "min-height: 32.5px; position: sticky; bottom: 0"))) (div ("id" "read_receipt_zone") ("class" "card") ("style" "min-height: 32.5px; position: sticky; bottom: 0")))
(form (form
@ -57,5 +53,5 @@
setTimeout(() => { setTimeout(() => {
scroll_bottom(); scroll_bottom();
}, 500);")) }, 1500);"))
(text "{% endblock %}") (text "{% endblock %}")

View file

@ -9,7 +9,7 @@
(a (a
("class" "button tab") ("class" "button tab")
("href" "/chats") ("href" "/chats")
(text "chats") (text "{{ icon \"castle\" }} chats")
(text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}"))) (text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}")))
(button (button
("class" "button square") ("class" "button square")

View file

@ -41,20 +41,27 @@
(text "{%- endif %}") (text "{%- endif %}")
(text "{%- endmacro %}") (text "{%- endmacro %}")
(text "{% macro message(message) -%}") (text "{% macro message(message, is_pinned=false) -%}")
(div (div
("class" "flex w_full gap_ch message {%- if user.id == message.owner %} justify_right mine {%- endif %}") ("class" "flex w_full gap_ch message {%- if user.id == message.owner %} justify_right mine {%- endif %}")
("id" "message_{{ message.id }}") ("id" "message_{{ message.id }}")
(text "{% if message.owner == user.id -%}")
(div (div
("class" "dropdown") ("class" "dropdown hidden")
(button (button
("onclick" "open_dropdown(event)") ("onclick" "open_dropdown(event)")
("exclude" "dropdown") ("exclude" "dropdown")
("class" "button") ("class" "button icon_only big_icon")
(text "{{ icon \"ellipsis\" }}")) (text "{{ icon \"ellipsis\" }}"))
(div (div
("class" "inner surface") ("class" "inner surface")
(text "{% if not is_pinned -%}")
(button
("class" "button surface")
("onclick" "pin_message(event, '{{ message.id }}')")
(text "pin"))
(text "{%- endif %}")
(text "{% if message.owner == user.id -%}")
(button (button
("class" "button surface") ("class" "button surface")
("onclick" "edit_message_ui('{{ message.id }}')") ("onclick" "edit_message_ui('{{ message.id }}')")
@ -62,8 +69,8 @@
(button (button
("class" "button surface red") ("class" "button surface red")
("onclick" "delete_message('{{ message.id }}')") ("onclick" "delete_message('{{ message.id }}')")
(text "delete")))) (text "delete"))
(text "{%- endif %}") (text "{%- endif %}")))
(div (div
("class" "body no_p_margin") ("class" "body no_p_margin")

View file

@ -9,7 +9,7 @@
(a (a
("class" "button tab camo") ("class" "button tab camo")
("href" "/chats") ("href" "/chats")
(text "chats")) (text "{{ icon \"castle\" }} chats"))
(a (a
("class" "button tab camo") ("class" "button tab camo")
("href" "/chats/{{ chat.id }}") ("href" "/chats/{{ chat.id }}")
@ -17,24 +17,31 @@
(a (a
("class" "button tab") ("class" "button tab")
("href" "/chats/{{ chat.id }}/manage") ("href" "/chats/{{ chat.id }}/manage")
(text "{{ icon \"settings-2\" }} Manage")))) (text "{{ icon \"settings-2\" }} manage"))))
(div (div
("class" "flex flex_col gap_4 card") ("class" "flex flex_col gap_4 card")
("style" "flex: 1 0 auto") ("style" "flex: 1 0 auto")
(text "{% if chat.style != \"Direct\" -%}") (div
; gc only ("class" "flex gap_2 flex_wrap")
(button (text "{% if chat.style != \"Direct\" -%}")
("class" "button surface") ; gc only
("onclick" "rename_chat('{{ chat.id }}', GC_INFO)") (button
(text "{{ icon \"pencil\" }} rename chat")) ("class" "button surface")
("onclick" "rename_chat('{{ chat.id }}', GC_INFO)")
(text "{{ icon \"pencil\" }} rename chat"))
(script (script
("type" "application/json") ("type" "application/json")
("id" "gc_info") ("id" "gc_info")
(text "{{ chat.style.Group|json_encode() }}")) (text "{{ chat.style.Group|json_encode() }}"))
(script (script
(text "globalThis.GC_INFO = JSON.parse(document.getElementById(\"gc_info\").innerHTML)")) (text "globalThis.GC_INFO = JSON.parse(document.getElementById(\"gc_info\").innerHTML)"))
(text "{%- endif %}") (text "{%- endif %}")
; every chat
(a
("class" "button surface")
("href" "/chats/{{ chat.id }}/pins")
(text "{{ icon \"pin\" }} view pins")))
(ul (ul
(li (b (text "Chat name: ")) (span (text "{{ components::chat_name(chat=chat, members=members) }}"))) (li (b (text "Chat name: ")) (span (text "{{ components::chat_name(chat=chat, members=members) }}")))

View file

@ -1,6 +1,6 @@
(text "{%- import \"components.lisp\" as components -%}") (text "{%- import \"components.lisp\" as components -%}")
(text "{% for message in messages -%}") (text "{% for message in messages -%}")
(text "{{ components::message(message=message) }}") (text "{{ components::message(message=message, is_pinned=message.id in pins) }}")
(text "{%- endfor %}") (text "{%- endfor %}")
(div (div
@ -12,4 +12,5 @@
(div (div
("class" "hidden") ("class" "hidden")
("id" "msgs_quit_{{ id }}")) ("id" "msgs_quit_{{ id }}"))
(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!"))
(text "{%- endif %}") (text "{%- endif %}")

View file

@ -0,0 +1,36 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "Pins in {{ components::chat_name(chat=chat, members=members) }} — {{ config.name }}"))
(text "{% endblock %} {% block body %}")
(div
("class" "flex w_full gap_2 justify_between items_center")
(div
("class" "tabs short bar flex")
(a
("class" "button tab camo")
("href" "/chats")
(text "{{ icon \"castle\" }} chats"))
(a
("class" "button tab camo")
("href" "/chats/{{ chat.id }}")
(text "{{ components::chat_name(chat=chat, members=members, advanced=true, avatar_size=\"18px\") }}"))
(a
("class" "button tab")
("href" "/chats/{{ chat.id }}/manage")
(text "{{ icon \"settings-2\" }} manage"))))
(div
("class" "flex flex_col gap_4 card")
("style" "flex: 1 0 auto")
(p (text "Using ") (b (text "{{ messages|length }} ")) (text "of ") (b (text "12 ")) (text "pins."))
(text "{% for message in messages -%}")
(hr)
(button
("class" "button surface red")
("onclick" "unpin_message('{{ message.id }}')")
(text "unpin"))
(text "{{ components::message(message=message) }}")
(text "{%- endfor %}"))
(script ("src" "/public/messages.js"))
(script (text "STATE.chat_id = '{{ chat.id }}';"))
(text "{% endblock %}")

View file

@ -134,7 +134,9 @@
} }
.profile .banner { .profile .banner {
background: url(\"{{ config.service_hosts.buckets }}/banners/{{ profile.id }}\") no-repeat center !important; background-image: url(\"{{ config.service_hosts.buckets }}/banners/{{ profile.id }}\") !important;
background-repeat: no-repeat !important;
background-position: center !important;
background-size: cover !important; background-size: cover !important;
border-radius: var(--radius) var(--radius) 0 0; border-radius: var(--radius) var(--radius) 0 0;
height: 225px; height: 225px;

View file

@ -16,6 +16,7 @@ impl DataManager {
members: serde_json::from_str(&get!(x->3(String))).unwrap(), members: serde_json::from_str(&get!(x->3(String))).unwrap(),
last_message_created: get!(x->4(i64)) as usize, last_message_created: get!(x->4(i64)) as usize,
last_message_read_by: serde_json::from_str(&get!(x->5(String))).unwrap(), last_message_read_by: serde_json::from_str(&get!(x->5(String))).unwrap(),
pinned_messages: serde_json::from_str(&get!(x->6(String))).unwrap(),
} }
} }
@ -120,14 +121,15 @@ impl DataManager {
let res = execute!( let res = execute!(
&conn, &conn,
"INSERT INTO t_chats VALUES ($1, $2, $3, $4, $5, $6)", "INSERT INTO t_chats VALUES ($1, $2, $3, $4, $5, $6, $7)",
params![ params![
&(data.id as i64), &(data.id as i64),
&(data.created as i64), &(data.created as i64),
&serde_json::to_string(&data.style).unwrap(), &serde_json::to_string(&data.style).unwrap(),
&serde_json::to_string(&data.members).unwrap(), &serde_json::to_string(&data.members).unwrap(),
&(data.last_message_created as i64), &(data.last_message_created as i64),
&serde_json::to_string(&data.last_message_read_by).unwrap() &serde_json::to_string(&data.last_message_read_by).unwrap(),
&serde_json::to_string(&data.pinned_messages).unwrap(),
] ]
); );
@ -173,6 +175,7 @@ impl DataManager {
auto_method!(update_chat_style(ChatStyle) -> "UPDATE t_chats SET style = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); auto_method!(update_chat_style(ChatStyle) -> "UPDATE t_chats SET style = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}");
auto_method!(update_chat_members(Vec<usize>) -> "UPDATE t_chats SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); auto_method!(update_chat_members(Vec<usize>) -> "UPDATE t_chats SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}");
auto_method!(update_chat_last_message_read_by(Vec<usize>) -> "UPDATE t_chats SET last_message_read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}");
auto_method!(update_chat_last_message_created(i64) -> "UPDATE t_chats SET last_message_created = $1 WHERE id = $2" --cache-key-tmpl="twny.chat:{}"); auto_method!(update_chat_last_message_created(i64) -> "UPDATE t_chats SET last_message_created = $1 WHERE id = $2" --cache-key-tmpl="twny.chat:{}");
auto_method!(update_chat_last_message_read_by(Vec<usize>) -> "UPDATE t_chats SET last_message_read_by = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}");
auto_method!(update_chat_pinned_messages(Vec<usize>) -> "UPDATE t_chats SET pinned_messages = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}");
} }

View file

@ -52,6 +52,10 @@ impl DataManager {
execute!(&conn, sql::CREATE_TABLE_CHATS).unwrap(); execute!(&conn, sql::CREATE_TABLE_CHATS).unwrap();
execute!(&conn, sql::CREATE_TABLE_MESSAGES).unwrap(); execute!(&conn, sql::CREATE_TABLE_MESSAGES).unwrap();
for x in sql::VERSION_MIGRATIONS.split(";") {
execute!(&conn, x).unwrap();
}
Ok(()) Ok(())
} }
} }

View file

@ -1,2 +1,3 @@
pub const CREATE_TABLE_CHATS: &str = include_str!("./create_chats.sql"); pub const CREATE_TABLE_CHATS: &str = include_str!("./create_chats.sql");
pub const CREATE_TABLE_MESSAGES: &str = include_str!("./create_messages.sql"); pub const CREATE_TABLE_MESSAGES: &str = include_str!("./create_messages.sql");
pub const VERSION_MIGRATIONS: &str = include_str!("./version_migrations.sql");

View file

@ -0,0 +1,2 @@
-- chats pinned_messages
ALTER TABLE t_chats ADD COLUMN IF NOT EXISTS pinned_messages TEXT NOT NULL DEFAULT '[]';

View file

@ -32,6 +32,7 @@ pub struct Chat {
/// keep up with if we store by chat instead. This will also declutter /// keep up with if we store by chat instead. This will also declutter
/// the UI and prevent every message showing a read receipt. /// the UI and prevent every message showing a read receipt.
pub last_message_read_by: Vec<usize>, pub last_message_read_by: Vec<usize>,
pub pinned_messages: Vec<usize>,
} }
impl Chat { impl Chat {
@ -44,6 +45,7 @@ impl Chat {
members, members,
last_message_created: 0, last_message_created: 0,
last_message_read_by: Vec::new(), last_message_read_by: Vec::new(),
pinned_messages: Vec::new(),
} }
} }

View file

@ -43,7 +43,7 @@ pub async fn create_request(
req.members.dedup(); req.members.dedup();
if (req.members.len() > 2 && req.style == ChatStyle::Direct) if (req.members.len() > 2 && req.style == ChatStyle::Direct)
| (req.members.len() > GC_MAXIMUM_MEMBERS && req.style != ChatStyle::Direct) | (req.members.len() >= GC_MAXIMUM_MEMBERS && req.style != ChatStyle::Direct)
{ {
return Json(Error::DataTooLong("members list".to_string()).into()); return Json(Error::DataTooLong("members list".to_string()).into());
} else if req.members.len() < 1 { } else if req.members.len() < 1 {
@ -199,7 +199,7 @@ pub async fn add_member_request(
return Json(Error::NotAllowed.into()); return Json(Error::NotAllowed.into());
} }
if chat.style != ChatStyle::Direct && chat.members.len() > GC_MAXIMUM_MEMBERS { if chat.style != ChatStyle::Direct && chat.members.len() >= GC_MAXIMUM_MEMBERS {
// can only have a maximum of GC_MAXIMUM_MEMBERS members in one chat // can only have a maximum of GC_MAXIMUM_MEMBERS members in one chat
return Json(Error::DataTooLong("members list".to_string()).into()); return Json(Error::DataTooLong("members list".to_string()).into());
} }
@ -460,3 +460,83 @@ pub async fn handle_socket(socket: WebSocket, db: DataManager, chat_id: String,
.await; .await;
tracing::info!("socket terminate"); tracing::info!("socket terminate");
} }
pub const MAXIMUM_PINS: usize = 12;
pub async fn add_pin_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path((id, message_id)): Path<(usize, 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()),
};
// ...
let mut chat = match data.get_chat_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if !chat.members.contains(&user.id)
|| chat.pinned_messages.contains(&message_id)
|| chat.pinned_messages.len() >= MAXIMUM_PINS
{
return Json(Error::NotAllowed.into());
}
chat.pinned_messages.push(message_id);
match data
.update_chat_pinned_messages(chat.id, chat.pinned_messages)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn remove_pin_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path((id, message_id)): Path<(usize, 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()),
};
let mut chat = match data.get_chat_by_id(id).await {
Ok(x) => x,
Err(e) => return Json(e.into()),
};
if !chat.members.contains(&user.id) || !chat.pinned_messages.contains(&message_id) {
return Json(Error::NotAllowed.into());
}
chat.pinned_messages.remove(
chat.pinned_messages
.iter()
.position(|x| *x == message_id)
.unwrap(),
);
match data
.update_chat_pinned_messages(chat.id, chat.pinned_messages)
.await
{
Ok(_) => Json(ApiReturn {
ok: true,
message: "Success".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -33,6 +33,11 @@ pub fn routes() -> Router {
"/chats/{id}/read_message", "/chats/{id}/read_message",
post(chats::read_message_request), post(chats::read_message_request),
) )
.route("/chats/{id}/pins/{message}", post(chats::add_pin_request))
.route(
"/chats/{id}/pins/{message}",
delete(chats::remove_pin_request),
)
// messages // messages
.route("/messages/{id}", post(messages::create_request)) .route("/messages/{id}", post(messages::create_request))
.route("/messages/{id}", delete(messages::delete_request)) .route("/messages/{id}", delete(messages::delete_request))

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::{ use crate::{
State, get_user_from_token, State, get_user_from_token,
routes::{ routes::{
@ -50,7 +52,6 @@ pub async fn chat_request(
jar: CookieJar, jar: CookieJar,
Extension(data): Extension<State>, Extension(data): Extension<State>,
Path(id): Path<usize>, Path(id): Path<usize>,
Query(props): Query<PaginatedQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await; let (ref data, ref tera, ref build_code) = *data.read().await;
let user = match get_user_from_token!(jar, data.2) { let user = match get_user_from_token!(jar, data.2) {
@ -75,18 +76,10 @@ pub async fn chat_request(
} }
}; };
let messages = match data.get_messages_by_chat(id, 24, props.page).await {
Ok(x) => x,
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let mut ctx = default_context(&data.0.0, &build_code, &Some(user)); let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
ctx.insert("chat", &chat); ctx.insert("chat", &chat);
ctx.insert("members", &members); ctx.insert("members", &members);
ctx.insert("messages", &messages);
Ok(Html(tera.render("chat.lisp", &ctx).unwrap())) Ok(Html(tera.render("chat.lisp", &ctx).unwrap()))
} }
@ -135,12 +128,47 @@ pub async fn messages_request(
} }
}; };
let chat = match data.get_chat_by_id(id).await {
Ok(x) => x,
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let mut seen_user_blocks: HashMap<usize, bool> = HashMap::new();
let messages = match if before > 0 { let messages = match if before > 0 {
data.get_messages_by_chat_before(id, before, 24, 0).await data.get_messages_by_chat_before(id, before, 24, 0).await
} else { } else {
data.get_messages_by_chat(id, 24, 0).await data.get_messages_by_chat(id, 24, 0).await
} { } {
Ok(x) => x, Ok(x) => {
let mut y = Vec::new();
for z in x {
if let Some(status) = seen_user_blocks.get(&z.owner) {
if *status {
continue;
}
} else {
let is_blocked = data
.2
.get_userblock_by_initiator_receiver(user.id, z.owner)
.await
.is_ok();
seen_user_blocks.insert(z.owner, is_blocked);
if is_blocked {
continue;
}
}
y.push(z);
}
y
}
Err(e) => { Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await); return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
} }
@ -156,6 +184,7 @@ pub async fn messages_request(
}, },
); );
ctx.insert("pins", &chat.pinned_messages);
ctx.insert("messages", &messages); ctx.insert("messages", &messages);
ctx.insert("id", &props.use_id); ctx.insert("id", &props.use_id);
@ -225,3 +254,55 @@ pub async fn manage_chat_request(
Ok(Html(tera.render("manage.lisp", &ctx).unwrap())) Ok(Html(tera.render("manage.lisp", &ctx).unwrap()))
} }
pub async fn chat_pins_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let user = match get_user_from_token!(jar, data.2) {
Some(x) => x,
None => {
return Err(render_error(Error::NotAllowed, tera, data.0.0.clone(), None).await);
}
};
let (chat, members) = match data.get_chat_by_id(id).await {
Ok(x) => {
if !x.members.contains(&user.id) {
return Err(
render_error(Error::NotAllowed, tera, data.0.0.clone(), Some(user)).await,
);
}
data.fill_chat(x).await
}
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
};
let messages = {
let mut x = Vec::new();
for y in &chat.pinned_messages {
x.push(match data.get_message_by_id(*y).await {
Ok(z) => z,
Err(e) => {
return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await);
}
});
}
x
};
let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
ctx.insert("chat", &chat);
ctx.insert("members", &members);
ctx.insert("messages", &messages);
Ok(Html(tera.render("pins.lisp", &ctx).unwrap()))
}

View file

@ -9,7 +9,7 @@ use axum::{
use axum_extra::extract::CookieJar; use axum_extra::extract::CookieJar;
use serde::Deserialize; use serde::Deserialize;
use tera::Tera; use tera::Tera;
use tetratto_core::model::{Error, Result, auth::User}; use tetratto_core::model::{Error, Result, auth::User, permissions::FinePermission};
pub async fn render_error( pub async fn render_error(
e: Error, e: Error,
@ -57,6 +57,20 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension<State>) ->
) )
} }
async fn check_user_is_blocked(user: &User, other_user: &User, data: &DataManager) -> bool {
(data
.2
.get_userblock_by_initiator_receiver(other_user.id, user.id)
.await
.is_ok()
| data
.2
.get_user_stack_blocked_users(other_user.id)
.await
.contains(&user.id))
&& !user.permissions.check(FinePermission::MANAGE_USERS)
}
async fn check_user_blocked_or_private( async fn check_user_blocked_or_private(
user: &Option<User>, user: &Option<User>,
other_user: &User, other_user: &User,
@ -72,12 +86,7 @@ async fn check_user_blocked_or_private(
{ {
// private profile and other_user isn't following user // private profile and other_user isn't following user
return Err(Error::NotAllowed); return Err(Error::NotAllowed);
} else if data } else if check_user_is_blocked(ua, other_user, data).await {
.2
.get_userblock_by_initiator_receiver(other_user.id, ua.id)
.await
.is_ok()
{
// blocked // blocked
return Err(Error::NotAllowed); return Err(Error::NotAllowed);
} }

View file

@ -15,6 +15,7 @@ pub fn routes() -> Router {
// chats // chats
.route("/chats", get(chats::list_request)) .route("/chats", get(chats::list_request))
.route("/chats/{id}", get(chats::chat_request)) .route("/chats/{id}", get(chats::chat_request))
.route("/chats/{id}/pins", get(chats::chat_pins_request))
.route("/chats/{id}/manage", get(chats::manage_chat_request)) .route("/chats/{id}/manage", get(chats::manage_chat_request))
.route( .route(
"/chats/_templates/message/{id}", "/chats/_templates/message/{id}",