diff --git a/Cargo.lock b/Cargo.lock index cde7139..3a62069 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2998,7 +2998,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tawny" -version = "1.0.2" +version = "1.0.3" dependencies = [ "ammonia", "axum", diff --git a/Cargo.toml b/Cargo.toml index 47d7b7f..08e1f57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tawny" -version = "1.0.2" +version = "1.0.3" edition = "2024" authors = ["trisuaso"] repository = "https://trisua.com/t/tawny" diff --git a/app/public/messages.js b/app/public/messages.js index ff00834..e1e57ae 100644 --- a/app/public/messages.js +++ b/app/public/messages.js @@ -5,6 +5,7 @@ const STATE = { is_loading: false, stream_element: null, last_message_time: 0, + replying_to: undefined, last_read_receipt_load: 0, }; @@ -144,6 +145,7 @@ function create_message(e) { "body", JSON.stringify({ content: e.target.content.value, + replying_to: STATE.replying_to, }), ); @@ -154,6 +156,7 @@ function create_message(e) { if (res.ok) { e.target.reset(); document.getElementById("images_zone").classList.add("hidden"); + clear_replying_to(); } else { show_message(res.message, res.ok); } @@ -395,3 +398,25 @@ function remove_file_from_picker(input_id, idx) { // render display_pending_images({ target: input }); } + +function reply_to_message(id) { + STATE.replying_to = id; + document.getElementById("replying_to_zone").classList.remove("hidden"); + document.getElementById(`message_${id}`).classList.add("card"); + document.getElementById(`message_${id}`).classList.add("surface"); + scroll_bottom(); +} + +function clear_replying_to() { + if (STATE.replying_to) { + document + .getElementById(`message_${STATE.replying_to}`) + .classList.remove("card"); + document + .getElementById(`message_${STATE.replying_to}`) + .classList.remove("surface"); + } + + STATE.replying_to = undefined; + document.getElementById("replying_to_zone").classList.add("hidden"); +} diff --git a/app/templates_src/chat.lisp b/app/templates_src/chat.lisp index f276a60..98f006a 100644 --- a/app/templates_src/chat.lisp +++ b/app/templates_src/chat.lisp @@ -31,7 +31,13 @@ ("id" "messages_stream") (div ("ui_ident" "data_marker"))) (div ("id" "read_receipt_zone") ("class" "card") ("style" "min-height: 32.5px; position: sticky; bottom: 0")) - (div ("id" "images_zone") ("class" "card hidden flex gap_2 flex_wrap"))) + (div ("id" "images_zone") ("class" "card hidden flex gap_2 flex_wrap")) + (div + ("id" "replying_to_zone") ("class" "card hidden flex flex_col gap_1") + (div + ("class" "flex items_center gap_ch") + (text "{{ icon \"reply\" }} Replying to message")) + (button ("class" "button surface red") ("onclick" "clear_replying_to()") (text "cancel")))) (form ("class" "card flex flex_row items_center gap_2") ("onsubmit" "create_message(event)") diff --git a/app/templates_src/components.lisp b/app/templates_src/components.lisp index 186d91b..293ad86 100644 --- a/app/templates_src/components.lisp +++ b/app/templates_src/components.lisp @@ -41,10 +41,11 @@ (text "{%- endif %}") (text "{%- endmacro %}") -(text "{% macro message(message, is_pinned=false) -%}") +(text "{% macro message(message, replying_to=false, is_pinned=false, hide_actions=false) -%}") (div ("class" "flex w_full gap_ch message {%- if user.id == message.owner %} justify_right mine {%- endif %}") - ("id" "message_{{ message.id }}") + ("id" "{% if not hide_actions -%} message_{{ message.id }} {%- endif %}") + (text "{% if not hide_actions -%}") (div ("class" "dropdown hidden") (button @@ -61,6 +62,11 @@ (text "pin")) (text "{%- endif %}") + (button + ("class" "button surface") + ("onclick" "reply_to_message('{{ message.id }}')") + (text "reply")) + (text "{% if message.owner == user.id -%}") (button ("class" "button surface") @@ -71,6 +77,7 @@ ("onclick" "delete_message('{{ message.id }}')") (text "delete")) (text "{%- endif %}"))) + (text "{%- endif %}") (div ("class" "body no_p_margin") @@ -101,6 +108,12 @@ ("href" "/@{{ message.owner }}?redirect=true") ("target" "_blank") (text "{{ self::avatar(id=message.owner) }}"))) + +(text "{% if replying_to -%}") +(div + ("style" "transform: scale(0.8); opacity: 75%; width: 110%") + (text "{{ self::message(message=replying_to, hide_actions=true) }}")) +(text "{%- endif %}") (text "{%- endmacro %}") (text "{% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}") diff --git a/app/templates_src/message.lisp b/app/templates_src/message.lisp index fdc4e30..a88efdb 100644 --- a/app/templates_src/message.lisp +++ b/app/templates_src/message.lisp @@ -1,2 +1,2 @@ (text "{%- import \"components.lisp\" as components -%}") -(text "{{ components::message(message=message) }}") +(text "{{ components::message(message=message, replying_to=replying_to) }}") diff --git a/app/templates_src/messages.lisp b/app/templates_src/messages.lisp index 0402357..f897170 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, is_pinned=message.id in pins) }}") +(text "{{ components::message(message=message[0], replying_to=message[1], is_pinned=message[0].id in pins) }}") (text "{%- endfor %}") (div diff --git a/src/database/messages.rs b/src/database/messages.rs index 7639e3d..2134d1d 100644 --- a/src/database/messages.rs +++ b/src/database/messages.rs @@ -22,6 +22,7 @@ impl DataManager { chat: get!(x->4(i64)) as usize, content: get!(x->5(String)), uploads: serde_json::from_str(&get!(x->6(String))).unwrap(), + replying_to: get!(x->7(i64)) as usize, } } @@ -91,7 +92,7 @@ impl DataManager { /// * `data` - a mock [`Message`] object to insert pub async fn create_message(&self, data: Message) -> Result { // check values - if data.content.trim().len() < 2 { + if data.content.trim().len() < 2 && data.uploads.is_empty() { return Err(Error::DataTooShort("content".to_string())); } else if data.content.len() > 2048 { return Err(Error::DataTooLong("content".to_string())); @@ -105,7 +106,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO t_messages VALUES ($1, $2, $3, $4, $5, $6, $7)", + "INSERT INTO t_messages VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", params![ &(data.id as i64), &(data.created as i64), @@ -114,6 +115,7 @@ impl DataManager { &(data.chat as i64), &data.content, &serde_json::to_string(&data.uploads).unwrap(), + &(data.replying_to as i64) ] ); diff --git a/src/database/sql/create_messages.sql b/src/database/sql/create_messages.sql index 7b1c04d..75e9cb6 100644 --- a/src/database/sql/create_messages.sql +++ b/src/database/sql/create_messages.sql @@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS t_messages ( owner BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, chat BIGINT NOT NULL, content TEXT NOT NULL, - uploads TEXT NOT NULL + uploads TEXT NOT NULL, + replying_to BIGINT NOT NULL ); diff --git a/src/database/sql/version_migrations.sql b/src/database/sql/version_migrations.sql index 627dc0a..b17b4a6 100644 --- a/src/database/sql/version_migrations.sql +++ b/src/database/sql/version_migrations.sql @@ -1,2 +1,5 @@ -- chats pinned_messages ALTER TABLE t_chats ADD COLUMN IF NOT EXISTS pinned_messages TEXT NOT NULL DEFAULT '[]'; + +-- messages replying_to +ALTER TABLE t_messages ADD COLUMN IF NOT EXISTS replying_to BIGINT NOT NULL DEFAULT 0; diff --git a/src/model.rs b/src/model.rs index 75604c4..da784ee 100644 --- a/src/model.rs +++ b/src/model.rs @@ -68,6 +68,7 @@ pub struct Message { pub chat: usize, pub content: String, pub uploads: Vec, + pub replying_to: usize, } impl Message { @@ -83,6 +84,7 @@ impl Message { chat, content, uploads, + replying_to: 0, } } } diff --git a/src/routes/api/messages.rs b/src/routes/api/messages.rs index 7926e0e..8bfd877 100644 --- a/src/routes/api/messages.rs +++ b/src/routes/api/messages.rs @@ -11,6 +11,7 @@ use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission}; #[derive(Deserialize)] pub struct CreateMessage { pub content: String, + pub replying_to: Option, } const MAXIMUM_UPLOAD_SIZE: usize = 4_194_304; // 4 MiB @@ -28,7 +29,7 @@ pub async fn create_request( }; // check fields - if req.content.trim().len() < 2 { + if req.content.trim().len() < 2 && byte_parts.is_empty() { return Json(Error::DataTooShort("content".to_string()).into()); } else if req.content.len() > 2048 { return Json(Error::DataTooLong("content".to_string()).into()); @@ -69,16 +70,29 @@ pub async fn create_request( tokio::time::sleep(Duration::from_millis(150)).await; } + // ... + let mut msg = Message::new( + user.id, + chat.id, + req.content, + uploads.iter().map(|x| x.0.id).collect(), + ); + + if let Some(replying_to) = req.replying_to { + let replying_to: usize = replying_to.parse().unwrap_or(0); + + if replying_to != 0 { + let replying_to_msg = match data.get_message_by_id(replying_to).await { + Ok(x) => x, + Err(e) => return Json(e.into()), + }; + + msg.replying_to = replying_to_msg.id; + } + } + // create message - match data - .create_message(Message::new( - user.id, - chat.id, - req.content, - uploads.iter().map(|x| x.0.id).collect(), - )) - .await - { + match data.create_message(msg).await { Ok(x) => { // store uploads for (upload, part) in uploads { diff --git a/src/routes/pages/chats.rs b/src/routes/pages/chats.rs index 38b2477..8f60167 100644 --- a/src/routes/pages/chats.rs +++ b/src/routes/pages/chats.rs @@ -104,8 +104,22 @@ pub async fn single_message_request( } }; + let replying_to = if message.replying_to != 0 { + match data.get_message_by_id(message.replying_to).await { + Ok(x) => Some(x), + Err(e) => { + return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await); + } + } + } else { + None + }; + let mut ctx = default_context(&data.0.0, &build_code, &Some(user)); + ctx.insert("message", &message); + ctx.insert("replying_to", &replying_to); + Ok(Html(tera.render("message.lisp", &ctx).unwrap())) } @@ -164,7 +178,18 @@ pub async fn messages_request( } } - y.push(z); + let replying_to = if z.replying_to != 0 { + match data.get_message_by_id(z.replying_to).await { + Ok(x) => Some(x), + Err(e) => { + return Err(render_error(e, tera, data.0.0.clone(), Some(user)).await); + } + } + } else { + None + }; + + y.push((z, replying_to)); } y @@ -179,7 +204,7 @@ pub async fn messages_request( ctx.insert( "last_message_time", &match messages.last() { - Some(x) => x.created, + Some(x) => x.0.created, None => 0, }, );