generated from t/malachite
add: message replies
This commit is contained in:
parent
dfa1abe2d9
commit
ca1eca967c
13 changed files with 113 additions and 22 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -2998,7 +2998,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tawny"
|
name = "tawny"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"axum",
|
"axum",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tawny"
|
name = "tawny"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["trisuaso"]
|
authors = ["trisuaso"]
|
||||||
repository = "https://trisua.com/t/tawny"
|
repository = "https://trisua.com/t/tawny"
|
||||||
|
|
|
@ -5,6 +5,7 @@ const STATE = {
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
stream_element: null,
|
stream_element: null,
|
||||||
last_message_time: 0,
|
last_message_time: 0,
|
||||||
|
replying_to: undefined,
|
||||||
last_read_receipt_load: 0,
|
last_read_receipt_load: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,6 +145,7 @@ function create_message(e) {
|
||||||
"body",
|
"body",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
content: e.target.content.value,
|
content: e.target.content.value,
|
||||||
|
replying_to: STATE.replying_to,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -154,6 +156,7 @@ function create_message(e) {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
e.target.reset();
|
e.target.reset();
|
||||||
document.getElementById("images_zone").classList.add("hidden");
|
document.getElementById("images_zone").classList.add("hidden");
|
||||||
|
clear_replying_to();
|
||||||
} else {
|
} else {
|
||||||
show_message(res.message, res.ok);
|
show_message(res.message, res.ok);
|
||||||
}
|
}
|
||||||
|
@ -395,3 +398,25 @@ function remove_file_from_picker(input_id, idx) {
|
||||||
// render
|
// render
|
||||||
display_pending_images({ target: input });
|
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");
|
||||||
|
}
|
||||||
|
|
|
@ -31,7 +31,13 @@
|
||||||
("id" "messages_stream")
|
("id" "messages_stream")
|
||||||
(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"))
|
||||||
(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
|
(form
|
||||||
("class" "card flex flex_row items_center gap_2")
|
("class" "card flex flex_row items_center gap_2")
|
||||||
("onsubmit" "create_message(event)")
|
("onsubmit" "create_message(event)")
|
||||||
|
|
|
@ -41,10 +41,11 @@
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
(text "{%- endmacro %}")
|
(text "{%- endmacro %}")
|
||||||
|
|
||||||
(text "{% macro message(message, is_pinned=false) -%}")
|
(text "{% macro message(message, replying_to=false, is_pinned=false, hide_actions=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" "{% if not hide_actions -%} message_{{ message.id }} {%- endif %}")
|
||||||
|
(text "{% if not hide_actions -%}")
|
||||||
(div
|
(div
|
||||||
("class" "dropdown hidden")
|
("class" "dropdown hidden")
|
||||||
(button
|
(button
|
||||||
|
@ -61,6 +62,11 @@
|
||||||
(text "pin"))
|
(text "pin"))
|
||||||
(text "{%- endif %}")
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(button
|
||||||
|
("class" "button surface")
|
||||||
|
("onclick" "reply_to_message('{{ message.id }}')")
|
||||||
|
(text "reply"))
|
||||||
|
|
||||||
(text "{% if message.owner == user.id -%}")
|
(text "{% if message.owner == user.id -%}")
|
||||||
(button
|
(button
|
||||||
("class" "button surface")
|
("class" "button surface")
|
||||||
|
@ -71,6 +77,7 @@
|
||||||
("onclick" "delete_message('{{ message.id }}')")
|
("onclick" "delete_message('{{ message.id }}')")
|
||||||
(text "delete"))
|
(text "delete"))
|
||||||
(text "{%- endif %}")))
|
(text "{%- endif %}")))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
(div
|
(div
|
||||||
("class" "body no_p_margin")
|
("class" "body no_p_margin")
|
||||||
|
@ -101,6 +108,12 @@
|
||||||
("href" "/@{{ message.owner }}?redirect=true")
|
("href" "/@{{ message.owner }}?redirect=true")
|
||||||
("target" "_blank")
|
("target" "_blank")
|
||||||
(text "{{ self::avatar(id=message.owner) }}")))
|
(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 "{%- endmacro %}")
|
||||||
|
|
||||||
(text "{% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
|
(text "{% macro theme(user, theme_preference) -%} {% if user %} {% if user.settings.theme_hue -%}")
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
(text "{%- import \"components.lisp\" as components -%}")
|
(text "{%- import \"components.lisp\" as components -%}")
|
||||||
(text "{{ components::message(message=message) }}")
|
(text "{{ components::message(message=message, replying_to=replying_to) }}")
|
||||||
|
|
|
@ -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, 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 %}")
|
(text "{%- endfor %}")
|
||||||
|
|
||||||
(div
|
(div
|
||||||
|
|
|
@ -22,6 +22,7 @@ impl DataManager {
|
||||||
chat: get!(x->4(i64)) as usize,
|
chat: get!(x->4(i64)) as usize,
|
||||||
content: get!(x->5(String)),
|
content: get!(x->5(String)),
|
||||||
uploads: serde_json::from_str(&get!(x->6(String))).unwrap(),
|
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
|
/// * `data` - a mock [`Message`] object to insert
|
||||||
pub async fn create_message(&self, data: Message) -> Result<Message> {
|
pub async fn create_message(&self, data: Message) -> Result<Message> {
|
||||||
// check values
|
// 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()));
|
return Err(Error::DataTooShort("content".to_string()));
|
||||||
} else if data.content.len() > 2048 {
|
} else if data.content.len() > 2048 {
|
||||||
return Err(Error::DataTooLong("content".to_string()));
|
return Err(Error::DataTooLong("content".to_string()));
|
||||||
|
@ -105,7 +106,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&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![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
@ -114,6 +115,7 @@ impl DataManager {
|
||||||
&(data.chat as i64),
|
&(data.chat as i64),
|
||||||
&data.content,
|
&data.content,
|
||||||
&serde_json::to_string(&data.uploads).unwrap(),
|
&serde_json::to_string(&data.uploads).unwrap(),
|
||||||
|
&(data.replying_to as i64)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS t_messages (
|
||||||
owner BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
owner BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
chat BIGINT NOT NULL,
|
chat BIGINT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
uploads TEXT NOT NULL
|
uploads TEXT NOT NULL,
|
||||||
|
replying_to BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
-- chats pinned_messages
|
-- chats pinned_messages
|
||||||
ALTER TABLE t_chats ADD COLUMN IF NOT EXISTS pinned_messages TEXT NOT NULL DEFAULT '[]';
|
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;
|
||||||
|
|
|
@ -68,6 +68,7 @@ pub struct Message {
|
||||||
pub chat: usize,
|
pub chat: usize,
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub uploads: Vec<usize>,
|
pub uploads: Vec<usize>,
|
||||||
|
pub replying_to: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Message {
|
impl Message {
|
||||||
|
@ -83,6 +84,7 @@ impl Message {
|
||||||
chat,
|
chat,
|
||||||
content,
|
content,
|
||||||
uploads,
|
uploads,
|
||||||
|
replying_to: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission};
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateMessage {
|
pub struct CreateMessage {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
|
pub replying_to: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAXIMUM_UPLOAD_SIZE: usize = 4_194_304; // 4 MiB
|
const MAXIMUM_UPLOAD_SIZE: usize = 4_194_304; // 4 MiB
|
||||||
|
@ -28,7 +29,7 @@ pub async fn create_request(
|
||||||
};
|
};
|
||||||
|
|
||||||
// check fields
|
// 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());
|
return Json(Error::DataTooShort("content".to_string()).into());
|
||||||
} else if req.content.len() > 2048 {
|
} else if req.content.len() > 2048 {
|
||||||
return Json(Error::DataTooLong("content".to_string()).into());
|
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;
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create message
|
// ...
|
||||||
match data
|
let mut msg = Message::new(
|
||||||
.create_message(Message::new(
|
|
||||||
user.id,
|
user.id,
|
||||||
chat.id,
|
chat.id,
|
||||||
req.content,
|
req.content,
|
||||||
uploads.iter().map(|x| x.0.id).collect(),
|
uploads.iter().map(|x| x.0.id).collect(),
|
||||||
))
|
);
|
||||||
.await
|
|
||||||
{
|
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(msg).await {
|
||||||
Ok(x) => {
|
Ok(x) => {
|
||||||
// store uploads
|
// store uploads
|
||||||
for (upload, part) in uploads {
|
for (upload, part) in uploads {
|
||||||
|
|
|
@ -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));
|
let mut ctx = default_context(&data.0.0, &build_code, &Some(user));
|
||||||
|
|
||||||
ctx.insert("message", &message);
|
ctx.insert("message", &message);
|
||||||
|
ctx.insert("replying_to", &replying_to);
|
||||||
|
|
||||||
Ok(Html(tera.render("message.lisp", &ctx).unwrap()))
|
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
|
y
|
||||||
|
@ -179,7 +204,7 @@ pub async fn messages_request(
|
||||||
ctx.insert(
|
ctx.insert(
|
||||||
"last_message_time",
|
"last_message_time",
|
||||||
&match messages.last() {
|
&match messages.last() {
|
||||||
Some(x) => x.created,
|
Some(x) => x.0.created,
|
||||||
None => 0,
|
None => 0,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue