generated from t/malachite
add: images in messages for supporters
This commit is contained in:
parent
1c1eb3be5d
commit
dfa1abe2d9
7 changed files with 116 additions and 4 deletions
|
@ -3,3 +3,7 @@
|
||||||
messaging service :)
|
messaging service :)
|
||||||
|
|
||||||
<https://tetratto.com/post/217811578144161792>
|
<https://tetratto.com/post/217811578144161792>
|
||||||
|
|
||||||
|
## Usage notes
|
||||||
|
|
||||||
|
- For message uploads to work properly, you must symlink the `buckets` directory into the app's CWD
|
||||||
|
|
1
app/.gitignore
vendored
1
app/.gitignore
vendored
|
@ -6,3 +6,4 @@ public/favicon.svg
|
||||||
.env
|
.env
|
||||||
app.toml
|
app.toml
|
||||||
tetratto.toml
|
tetratto.toml
|
||||||
|
buckets
|
||||||
|
|
|
@ -132,6 +132,14 @@ function create_message(e) {
|
||||||
|
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
|
|
||||||
|
// attach images
|
||||||
|
if (e.target.images) {
|
||||||
|
for (const file of e.target.images.files) {
|
||||||
|
body.append(file.name, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add json body
|
||||||
body.append(
|
body.append(
|
||||||
"body",
|
"body",
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
|
@ -139,11 +147,13 @@ function create_message(e) {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// send request
|
||||||
fetch(`/api/v1/messages/${STATE.chat_id}`, { method: "POST", body })
|
fetch(`/api/v1/messages/${STATE.chat_id}`, { method: "POST", body })
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
e.target.reset();
|
e.target.reset();
|
||||||
|
document.getElementById("images_zone").classList.add("hidden");
|
||||||
} else {
|
} else {
|
||||||
show_message(res.message, res.ok);
|
show_message(res.message, res.ok);
|
||||||
}
|
}
|
||||||
|
@ -350,3 +360,38 @@ function unpin_message(id) {
|
||||||
show_message(res.message, res.ok);
|
show_message(res.message, res.ok);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function display_pending_images(e) {
|
||||||
|
document.getElementById("images_zone").innerHTML = "";
|
||||||
|
document.getElementById("images_zone").classList.remove("hidden");
|
||||||
|
|
||||||
|
if (e.target.files.length < 1) {
|
||||||
|
document.getElementById("images_zone").classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (const file of e.target.files) {
|
||||||
|
document.getElementById("images_zone").innerHTML +=
|
||||||
|
`<button class="button surface" onclick="remove_file_from_picker('images', ${idx})">${file.name}</button>`;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_file_from_picker(input_id, idx) {
|
||||||
|
const input = document.getElementById(input_id);
|
||||||
|
const files = Array.from(input.files);
|
||||||
|
files.splice(idx - 1, 1);
|
||||||
|
|
||||||
|
// update files
|
||||||
|
const list = new DataTransfer();
|
||||||
|
|
||||||
|
for (item of files) {
|
||||||
|
list.items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.files = list.files;
|
||||||
|
|
||||||
|
// render
|
||||||
|
display_pending_images({ target: input });
|
||||||
|
}
|
||||||
|
|
|
@ -822,3 +822,18 @@ menu.col {
|
||||||
.message:focus .dropdown.hidden {
|
.message:focus .dropdown.hidden {
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message img {
|
||||||
|
max-width: 50dvw;
|
||||||
|
max-height: 25dvh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .upload {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size)
|
||||||
|
var(--color-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message .body p:last-of-type {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
|
@ -30,10 +30,38 @@
|
||||||
("style" "flex: 1 0 auto")
|
("style" "flex: 1 0 auto")
|
||||||
("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")))
|
||||||
(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)")
|
||||||
|
(text "{% if user.permissions|has_supporter -%}")
|
||||||
|
(div
|
||||||
|
("class" "dropdown")
|
||||||
|
(button
|
||||||
|
("onclick" "open_dropdown(event)")
|
||||||
|
("exclude" "dropdown")
|
||||||
|
("class" "button icon_only big_icon")
|
||||||
|
("type" "button")
|
||||||
|
(text "{{ icon \"plus\" }}"))
|
||||||
|
(div
|
||||||
|
("class" "inner left")
|
||||||
|
(button
|
||||||
|
("class" "button")
|
||||||
|
("onclick" "document.getElementById('images').click()")
|
||||||
|
("type" "button")
|
||||||
|
(text "attach image"))))
|
||||||
|
(text "{%- endif %}")
|
||||||
|
|
||||||
|
(input
|
||||||
|
("type" "file")
|
||||||
|
("class" "hidden")
|
||||||
|
("accept" "image/*")
|
||||||
|
("id" "images")
|
||||||
|
("name" "images")
|
||||||
|
("multiple" "")
|
||||||
|
("onchange" "display_pending_images(event)"))
|
||||||
|
|
||||||
(input
|
(input
|
||||||
("type" "text")
|
("type" "text")
|
||||||
("class" "w_full")
|
("class" "w_full")
|
||||||
|
|
|
@ -75,7 +75,15 @@
|
||||||
(div
|
(div
|
||||||
("class" "body no_p_margin")
|
("class" "body no_p_margin")
|
||||||
("id" "{{ message.id }}_body")
|
("id" "{{ message.id }}_body")
|
||||||
(text "{{ message.content|markdown|safe }}"))
|
(text "{{ message.content|markdown|safe }}")
|
||||||
|
(div
|
||||||
|
("class" "flex flex_col gap_1 {% if message.uploads|length == 0 -%} hidden {%- endif %}")
|
||||||
|
(text "{% for upload in message.uploads -%}")
|
||||||
|
(a
|
||||||
|
("href" "{{ config.service_hosts.buckets }}/message_media/{{ upload }}")
|
||||||
|
("target" "_blank")
|
||||||
|
(img ("class" "upload") ("src" "{{ config.service_hosts.buckets }}/message_media/{{ upload }}") ("alt" "Media upload")))
|
||||||
|
(text "{%- endfor %}")))
|
||||||
(form
|
(form
|
||||||
("class" "body hidden flex flex_row gap_ch")
|
("class" "body hidden flex flex_row gap_ch")
|
||||||
("id" "{{ message.id }}_edit_area")
|
("id" "{{ message.id }}_edit_area")
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{State, get_user_from_token, markdown::render_markdown, model::Message};
|
use crate::{State, get_user_from_token, markdown::render_markdown, model::Message};
|
||||||
use axum::{Extension, Json, body::Bytes, extract::Path, response::IntoResponse};
|
use axum::{Extension, Json, body::Bytes, extract::Path, response::IntoResponse};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
|
use axum_image::{encode::save_webp_buffer, extract::JsonMultipart};
|
||||||
use buckets_core::model::{MediaType, MediaUpload};
|
use buckets_core::model::{MediaType, MediaUpload};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tetratto_core::model::{ApiReturn, Error};
|
use tetratto_core::model::{ApiReturn, Error, permissions::FinePermission};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct CreateMessage {
|
pub struct CreateMessage {
|
||||||
|
@ -45,8 +47,16 @@ pub async fn create_request(
|
||||||
// create uploads
|
// create uploads
|
||||||
let mut uploads: Vec<(MediaUpload, Bytes)> = Vec::new();
|
let mut uploads: Vec<(MediaUpload, Bytes)> = Vec::new();
|
||||||
|
|
||||||
|
if !user.permissions.check(FinePermission::SUPPORTER) && !byte_parts.is_empty() {
|
||||||
|
return Json(Error::RequiresSupporter.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if byte_parts.len() > 4 {
|
||||||
|
return Json(Error::MiscError("Too many images".to_string()).into());
|
||||||
|
}
|
||||||
|
|
||||||
for part in &byte_parts {
|
for part in &byte_parts {
|
||||||
if part.len() < MAXIMUM_UPLOAD_SIZE {
|
if part.len() > MAXIMUM_UPLOAD_SIZE {
|
||||||
return Json(Error::FileTooLarge.into());
|
return Json(Error::FileTooLarge.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,6 +66,7 @@ pub async fn create_request(
|
||||||
MediaUpload::new(MediaType::Webp, user.id, "message_media".to_string()),
|
MediaUpload::new(MediaType::Webp, user.id, "message_media".to_string()),
|
||||||
part,
|
part,
|
||||||
));
|
));
|
||||||
|
tokio::time::sleep(Duration::from_millis(150)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create message
|
// create message
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue