diff --git a/Cargo.lock b/Cargo.lock index 640e373..cde7139 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2998,7 +2998,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tawny" -version = "1.0.1" +version = "1.0.2" dependencies = [ "ammonia", "axum", diff --git a/Cargo.toml b/Cargo.toml index 0e8192f..47d7b7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tawny" -version = "1.0.1" +version = "1.0.2" edition = "2024" authors = ["trisuaso"] repository = "https://trisua.com/t/tawny" @@ -30,7 +30,10 @@ glob = "0.3.2" serde_json = "1.0.142" toml = "0.9.4" 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" axum-image = "0.1.1" futures-util = "0.3.31" diff --git a/app/public/messages.js b/app/public/messages.js index 154e5ed..603992b 100644 --- a/app/public/messages.js +++ b/app/public/messages.js @@ -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); + }); +} diff --git a/app/public/style.css b/app/public/style.css index 57bfe34..a18f77d 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -116,6 +116,10 @@ article { height: 100%; } +.tabs { + overflow: auto; +} + .fadein { animation: fadein ease-in-out 1 0.5s forwards running; } @@ -801,6 +805,7 @@ menu.col { background: var(--color-surface); color: var(--color-text); border-radius: var(--radius); + min-height: 36px; } .message.mine .body { @@ -812,3 +817,8 @@ menu.col { .message:not(.mine) .body { border-bottom-left-radius: 0; } + +.message:hover .dropdown.hidden, +.message:focus .dropdown.hidden { + display: flex !important; +} diff --git a/app/templates_src/chat.lisp b/app/templates_src/chat.lisp index 791405f..248d12c 100644 --- a/app/templates_src/chat.lisp +++ b/app/templates_src/chat.lisp @@ -9,7 +9,7 @@ (a ("class" "button tab camo") ("href" "/chats") - (text "chats") + (text "{{ icon \"castle\" }} chats") (text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}")) (a ("class" "button tab") @@ -18,7 +18,7 @@ (a ("class" "button tab camo") ("href" "/chats/{{ chat.id }}/manage") - (text "{{ icon \"settings-2\" }} Manage")))) + (text "{{ icon \"settings-2\" }} manage")))) (div ("class" "flex flex_col card_nest reverse") ("style" "flex: 1 0 auto") @@ -29,10 +29,6 @@ ("class" "card flex flex_rev_col gap_2") ("style" "flex: 1 0 auto") ("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 ("id" "read_receipt_zone") ("class" "card") ("style" "min-height: 32.5px; position: sticky; bottom: 0"))) (form @@ -57,5 +53,5 @@ setTimeout(() => { scroll_bottom(); - }, 500);")) + }, 1500);")) (text "{% endblock %}") diff --git a/app/templates_src/chats.lisp b/app/templates_src/chats.lisp index 092a0b8..2a79eb7 100644 --- a/app/templates_src/chats.lisp +++ b/app/templates_src/chats.lisp @@ -9,7 +9,7 @@ (a ("class" "button tab") ("href" "/chats") - (text "chats") + (text "{{ icon \"castle\" }} chats") (text "{% if user.missed_messages_count > 0 -%}") (b (text "({{ user.missed_messages_count }})")) (text "{%- endif %}"))) (button ("class" "button square") diff --git a/app/templates_src/components.lisp b/app/templates_src/components.lisp index acab48f..81c6a61 100644 --- a/app/templates_src/components.lisp +++ b/app/templates_src/components.lisp @@ -41,20 +41,27 @@ (text "{%- endif %}") (text "{%- endmacro %}") -(text "{% macro message(message) -%}") +(text "{% macro message(message, is_pinned=false) -%}") (div ("class" "flex w_full gap_ch message {%- if user.id == message.owner %} justify_right mine {%- endif %}") ("id" "message_{{ message.id }}") - (text "{% if message.owner == user.id -%}") (div - ("class" "dropdown") + ("class" "dropdown hidden") (button ("onclick" "open_dropdown(event)") ("exclude" "dropdown") - ("class" "button") + ("class" "button icon_only big_icon") (text "{{ icon \"ellipsis\" }}")) (div ("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 ("class" "button surface") ("onclick" "edit_message_ui('{{ message.id }}')") @@ -62,8 +69,8 @@ (button ("class" "button surface red") ("onclick" "delete_message('{{ message.id }}')") - (text "delete")))) - (text "{%- endif %}") + (text "delete")) + (text "{%- endif %}"))) (div ("class" "body no_p_margin") diff --git a/app/templates_src/manage.lisp b/app/templates_src/manage.lisp index b7a43d0..006a1c9 100644 --- a/app/templates_src/manage.lisp +++ b/app/templates_src/manage.lisp @@ -9,7 +9,7 @@ (a ("class" "button tab camo") ("href" "/chats") - (text "chats")) + (text "{{ icon \"castle\" }} chats")) (a ("class" "button tab camo") ("href" "/chats/{{ chat.id }}") @@ -17,24 +17,31 @@ (a ("class" "button tab") ("href" "/chats/{{ chat.id }}/manage") - (text "{{ icon \"settings-2\" }} Manage")))) + (text "{{ icon \"settings-2\" }} manage")))) (div ("class" "flex flex_col gap_4 card") ("style" "flex: 1 0 auto") - (text "{% if chat.style != \"Direct\" -%}") - ; gc only - (button - ("class" "button surface") - ("onclick" "rename_chat('{{ chat.id }}', GC_INFO)") - (text "{{ icon \"pencil\" }} rename chat")) + (div + ("class" "flex gap_2 flex_wrap") + (text "{% if chat.style != \"Direct\" -%}") + ; gc only + (button + ("class" "button surface") + ("onclick" "rename_chat('{{ chat.id }}', GC_INFO)") + (text "{{ icon \"pencil\" }} rename chat")) - (script - ("type" "application/json") - ("id" "gc_info") - (text "{{ chat.style.Group|json_encode() }}")) - (script - (text "globalThis.GC_INFO = JSON.parse(document.getElementById(\"gc_info\").innerHTML)")) - (text "{%- endif %}") + (script + ("type" "application/json") + ("id" "gc_info") + (text "{{ chat.style.Group|json_encode() }}")) + (script + (text "globalThis.GC_INFO = JSON.parse(document.getElementById(\"gc_info\").innerHTML)")) + (text "{%- endif %}") + ; every chat + (a + ("class" "button surface") + ("href" "/chats/{{ chat.id }}/pins") + (text "{{ icon \"pin\" }} view pins"))) (ul (li (b (text "Chat name: ")) (span (text "{{ components::chat_name(chat=chat, members=members) }}"))) diff --git a/app/templates_src/messages.lisp b/app/templates_src/messages.lisp index e90786c..0402357 100644 --- a/app/templates_src/messages.lisp +++ b/app/templates_src/messages.lisp @@ -1,6 +1,6 @@ (text "{%- import \"components.lisp\" as components -%}") (text "{% for message in messages -%}") -(text "{{ components::message(message=message) }}") +(text "{{ components::message(message=message, is_pinned=message.id in pins) }}") (text "{%- endfor %}") (div @@ -12,4 +12,5 @@ (div ("class" "hidden") ("id" "msgs_quit_{{ id }}")) +(i ("class" "flex gap_ch items_center fade") (text "{{ icon \"star\" }} This is the start of the chat!")) (text "{%- endif %}") diff --git a/app/templates_src/pins.lisp b/app/templates_src/pins.lisp new file mode 100644 index 0000000..e6bd3eb --- /dev/null +++ b/app/templates_src/pins.lisp @@ -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 %}") diff --git a/app/templates_src/profile.lisp b/app/templates_src/profile.lisp index a644c9c..8fa811a 100644 --- a/app/templates_src/profile.lisp +++ b/app/templates_src/profile.lisp @@ -134,7 +134,9 @@ } .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; border-radius: var(--radius) var(--radius) 0 0; height: 225px; diff --git a/src/database/chats.rs b/src/database/chats.rs index e3b2424..ae4ab01 100644 --- a/src/database/chats.rs +++ b/src/database/chats.rs @@ -16,6 +16,7 @@ impl DataManager { members: serde_json::from_str(&get!(x->3(String))).unwrap(), last_message_created: get!(x->4(i64)) as usize, 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!( &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![ &(data.id as i64), &(data.created as i64), &serde_json::to_string(&data.style).unwrap(), &serde_json::to_string(&data.members).unwrap(), &(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_members(Vec) -> "UPDATE t_chats SET members = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); - auto_method!(update_chat_last_message_read_by(Vec) -> "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_read_by(Vec) -> "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) -> "UPDATE t_chats SET pinned_messages = $1 WHERE id = $2" --serde --cache-key-tmpl="twny.chat:{}"); } diff --git a/src/database/mod.rs b/src/database/mod.rs index a7e6bd4..3f9ad34 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -52,6 +52,10 @@ impl DataManager { execute!(&conn, sql::CREATE_TABLE_CHATS).unwrap(); execute!(&conn, sql::CREATE_TABLE_MESSAGES).unwrap(); + for x in sql::VERSION_MIGRATIONS.split(";") { + execute!(&conn, x).unwrap(); + } + Ok(()) } } diff --git a/src/database/sql/mod.rs b/src/database/sql/mod.rs index 0f6e946..28bf53b 100644 --- a/src/database/sql/mod.rs +++ b/src/database/sql/mod.rs @@ -1,2 +1,3 @@ pub const CREATE_TABLE_CHATS: &str = include_str!("./create_chats.sql"); pub const CREATE_TABLE_MESSAGES: &str = include_str!("./create_messages.sql"); +pub const VERSION_MIGRATIONS: &str = include_str!("./version_migrations.sql"); diff --git a/src/database/sql/version_migrations.sql b/src/database/sql/version_migrations.sql new file mode 100644 index 0000000..627dc0a --- /dev/null +++ b/src/database/sql/version_migrations.sql @@ -0,0 +1,2 @@ +-- chats pinned_messages +ALTER TABLE t_chats ADD COLUMN IF NOT EXISTS pinned_messages TEXT NOT NULL DEFAULT '[]'; diff --git a/src/model.rs b/src/model.rs index f49c97c..75604c4 100644 --- a/src/model.rs +++ b/src/model.rs @@ -32,6 +32,7 @@ pub struct Chat { /// keep up with if we store by chat instead. This will also declutter /// the UI and prevent every message showing a read receipt. pub last_message_read_by: Vec, + pub pinned_messages: Vec, } impl Chat { @@ -44,6 +45,7 @@ impl Chat { members, last_message_created: 0, last_message_read_by: Vec::new(), + pinned_messages: Vec::new(), } } diff --git a/src/routes/api/chats.rs b/src/routes/api/chats.rs index 170832b..1e39e85 100644 --- a/src/routes/api/chats.rs +++ b/src/routes/api/chats.rs @@ -43,7 +43,7 @@ pub async fn create_request( req.members.dedup(); 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()); } else if req.members.len() < 1 { @@ -199,7 +199,7 @@ pub async fn add_member_request( 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 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; tracing::info!("socket terminate"); } + +pub const MAXIMUM_PINS: usize = 12; + +pub async fn add_pin_request( + jar: CookieJar, + Extension(data): Extension, + 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, + 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()), + } +} diff --git a/src/routes/api/mod.rs b/src/routes/api/mod.rs index a48b844..35329eb 100644 --- a/src/routes/api/mod.rs +++ b/src/routes/api/mod.rs @@ -33,6 +33,11 @@ pub fn routes() -> Router { "/chats/{id}/read_message", 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 .route("/messages/{id}", post(messages::create_request)) .route("/messages/{id}", delete(messages::delete_request)) diff --git a/src/routes/pages/chats.rs b/src/routes/pages/chats.rs index 1521bae..38b2477 100644 --- a/src/routes/pages/chats.rs +++ b/src/routes/pages/chats.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{ State, get_user_from_token, routes::{ @@ -50,7 +52,6 @@ pub async fn chat_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, - Query(props): Query, ) -> impl IntoResponse { let (ref data, ref tera, ref build_code) = *data.read().await; 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)); ctx.insert("chat", &chat); ctx.insert("members", &members); - ctx.insert("messages", &messages); 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 = HashMap::new(); + let messages = match if before > 0 { data.get_messages_by_chat_before(id, before, 24, 0).await } else { 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) => { 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("id", &props.use_id); @@ -225,3 +254,55 @@ pub async fn manage_chat_request( Ok(Html(tera.render("manage.lisp", &ctx).unwrap())) } + +pub async fn chat_pins_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> 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())) +} diff --git a/src/routes/pages/misc.rs b/src/routes/pages/misc.rs index cf8033c..35e1a77 100644 --- a/src/routes/pages/misc.rs +++ b/src/routes/pages/misc.rs @@ -9,7 +9,7 @@ use axum::{ use axum_extra::extract::CookieJar; use serde::Deserialize; 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( e: Error, @@ -57,6 +57,20 @@ pub async fn login_request(jar: CookieJar, Extension(data): Extension) -> ) } +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( user: &Option, other_user: &User, @@ -72,12 +86,7 @@ async fn check_user_blocked_or_private( { // private profile and other_user isn't following user return Err(Error::NotAllowed); - } else if data - .2 - .get_userblock_by_initiator_receiver(other_user.id, ua.id) - .await - .is_ok() - { + } else if check_user_is_blocked(ua, other_user, data).await { // blocked return Err(Error::NotAllowed); } diff --git a/src/routes/pages/mod.rs b/src/routes/pages/mod.rs index e5618ec..81dd4e8 100644 --- a/src/routes/pages/mod.rs +++ b/src/routes/pages/mod.rs @@ -15,6 +15,7 @@ pub fn routes() -> Router { // chats .route("/chats", get(chats::list_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/_templates/message/{id}",