feature: file uploads and audio embedding

This commit is contained in:
Andy Burke 2025-08-20 11:48:16 -07:00
parent 2224e1abe0
commit 4c0a8bb700
7 changed files with 105 additions and 22 deletions

View file

@ -136,7 +136,7 @@
const body = new FormData();
body.append("file", avatar, encodeURIComponent(avatar.name));
const avatar_path = `/files/users/${user.id}/avatars/${avatar.name}`;
const avatar_path = `/files/users/${user.id}/avatars/${encodeURIComponent(avatar.name)}`;
const avatar_upload_response = await api.fetch(avatar_path, {
method: "PUT",

View file

@ -90,10 +90,24 @@
padding: 0.75rem;
}
#talk #room-chat-entry-container form button {
#talk #room-chat-entry-container form input[type="file"] {
opacity: 0;
display: none;
}
#talk #room-chat-entry-container form button,
#talk #room-chat-entry-container form label {
position: relative;
top: inherit;
font-size: inherit;
transition: inherit;
width: 50px;
padding: inherit;
margin: 0 1rem;
cursor: pointer;
align-content: center;
border-radius: var(--border-radius);
border: 1px solid var(--text);
}
#talk #room-chat-entry-container form textarea {
@ -101,6 +115,7 @@
flex-grow: 1;
background: inherit;
color: inherit;
border-radius: var(--border-radius);
}
#talk .message-container {
@ -109,7 +124,7 @@
background: rgba(255, 255, 255, 0.03);
margin-top: 0.75rem;
padding: 2px;
border-radius: 4px;
border-radius: var(--border-radius);
}
#talk .message-container.user-tick.time-tick + .message-container.user-tick.time-tick,
@ -250,12 +265,17 @@
opacity: 0;
}
#talk .embed-container audio {
width: 100%;
}
#talk .embed-container.rounded {
border-radius: 6px;
}
#talk .embed-container.short {
height: 0;
min-height: 40px;
overflow: hidden;
overflow-y: auto;
padding-bottom: 7.5%;
@ -347,7 +367,8 @@
padding: 0.25rem;
}
#talk #room-chat-container #room-chat-entry-container form button {
#talk #room-chat-container #room-chat-entry-container form button,
#talk #room-chat-container #room-chat-entry-container form label {
margin: 0 0.5rem;
}
}

View file

@ -89,9 +89,16 @@
<div id="room-chat-content"></div>
<div id="room-chat-entry-container">
<form id="room-chat-entry" action="" data-smart="true" data-method="POST">
<button aria-label="Attach file">
<i class="icon attachment"></i>
</button>
<input
id="file-upload-and-share-input"
aria-label="Upload and share file"
type="file"
name="file-upload-and-share"
multiple
/>
<label for="file-upload-and-share-input">
<div class="icon attachment"></div>
</label>
<textarea
id="room-chat-input"
class="room-chat-input"
@ -101,10 +108,12 @@
<button id="room-chat-send" class="primary" aria-label="Send a message">
<i class="icon send"></i>
</button>
<script>
{
const form = document.currentScript.closest("form");
const file_input = document.querySelector(
'input[name="file-upload-and-share"]',
);
const chat_input = document.getElementById("room-chat-input");
const room_chat_container =
document.getElementById("room-chat-container");
@ -118,18 +127,48 @@
}
});
form.on_submit = (event) => {
const message = chat_input.value.trim();
if (message.length === 0) {
return false;
}
form.on_submit = async (event) => {
const user = JSON.parse(document.body.dataset.user);
const room_id = room_chat_container.dataset.room_id;
if (!room_id) {
alert("Failed to get room_id!");
return false;
}
form.uploaded_urls = [];
form.errors = [];
for await (const file of file_input.files) {
const body = new FormData();
body.append("file", file, encodeURIComponent(file.name));
const file_path = `/files/users/${user.id}/${encodeURIComponent(file.name)}`;
const file_upload_response = await api.fetch(file_path, {
method: "PUT",
body,
});
if (!file_upload_response.ok) {
const error = await file_upload_response.json();
form.errors.push(error?.error?.message ?? "Unknown error.");
continue;
}
const file_url = `${window.location.protocol}//${window.location.host}${file_path}`;
form.uploaded_urls.push(file_url);
}
if (form.errors.length) {
const errors = form.errors.join("\n\n");
alert(errors);
return false;
}
const message = chat_input.value.trim();
if (form.uploaded_urls.length === 0 && message.length === 0) {
return false;
}
form.action = `/api/rooms/${room_id}/events`;
};
@ -147,6 +186,15 @@
updated: now,
};
if (form.uploaded_urls.length) {
json.data = json.data ?? {};
json.data.message =
(typeof json.data.message === "string" &&
json.data.message.trim().length
? json.data.message.trim() + "\n"
: "") + form.uploaded_urls.join("\n");
}
const user = JSON.parse(document.body.dataset.user);
render_text_event(room_chat_content, json, user);
document

View file

@ -2,7 +2,7 @@
// watch out for places we need to set `lastIndex` ... :frown:
const URL_MATCHING_REGEX =
/(?:(?<protocol>[a-zA-Z]+):\/\/)?(?:(?<auth>(?<username>\S.+)\:(?<password>.+))\@)?(?<host>(?:(?<hostname>[-a-zA-Z0-9\.]+)\.)?(?<domain>[-a-zA-Z0-9]+?\.(?<tld>[-a-zA-Z0-9]{2,64}))(?:\:(?<port>[0-9]{1,6}))?)\b(?<path>[-a-zA-Z0-9@:%_{}\[\]<>\(\)\+.~&\/="]*?(?<extension>\.[^\.?/#"]+)?)(?:\?(?<query>[a-zA-Z0-9!$%&<>()*+,-\.\/\:\;\=\?\@_~"]+))?(?:#(?<hash>[a-zA-Z0-9!$&'()*+,-\.\/\:\;\=\?\@_~"]*?))?(?:$|\s)/gim;
/(?:(?<protocol>[a-zA-Z]+):)?(?:\/\/)?(?:(?<auth>(?<username>\S.+)\:(?<password>.+))\@)?(?<host>(?:(?<hostname>[-a-zA-Z0-9\.]+)\.)?(?<domain>(?:[-a-zA-Z0-9]+?\.(?<tld>[-a-zA-Z0-9]{2,64}))|localhost)(?:\:(?<port>[0-9]{1,6}))?)\b(?<path>[-a-zA-Z0-9@:%_{}\[\]<>\(\)\+.~&\/="]*?(?<extension>\.[^\.?/#"\n<>]+)?)(?:\?(?<query>[a-zA-Z0-9!$%&<>()*+,-\.\/\:\;\=\?\@_~"]+))?(?:#(?<hash>[a-zA-Z0-9!$&'()*+,-\.\/\:\;\=\?\@_~"]*?))?(?:$|\s)/gim;
const VIDEO_ID_EXTRACTOR =
/(?<video_domain>vimeo\.com|youtu(?:be\.com|\.be|be\.googleapis\.com))(?:\/(?<action>video|embed|watch|shorts|v))?.*(?:(?:\/|v=)(?<video_id>[A-Za-z0-9._%-]*))\S*/gi;
@ -159,8 +159,14 @@ const URL_MATCH_HANDLERS = [
// const punycode = get_punycode();
// const punycoded_url = punycode.encode(match[0]);
console.dir({
link_info,
});
if (typeof link_info.extension === "string") {
const mime_types = get_mime_types(link_info.extension);
console.dir({
mime_types,
});
if (mime_types.length) {
if (mime_types.includes("image/gif")) {
return `<div class="embed-container image gif"><img src="${link_info.url}" alt="A gif from ${link_info.domain}" /></div>`;
@ -173,6 +179,16 @@ const URL_MATCH_HANDLERS = [
if (mime_types[0].indexOf("image") === 0) {
return `<div class="embed-container image"><img src="${link_info.url}" alt="An image from ${link_info.domain}" /></div>`;
}
if (mime_types[0].indexOf("audio") === 0) {
return `
<div class="embed-container short">
<audio controls>
<source src="${link_info.url}" type="${mime_types[0].indexOf("audio/mpeg") === 0 ? "audio/mpeg" : mime_types[0]}">
Your browser does not support the audio element.
</audio>
</div>`;
}
}
}
@ -181,7 +197,7 @@ const URL_MATCH_HANDLERS = [
];
function message_text_to_html(input) {
let html_message = input;
let html_message = (input ?? "").replace(/\n/g, "<br/>");
let match;
URL_MATCHING_REGEX.lastIndex = 0;