feature: file uploads and audio embedding
This commit is contained in:
parent
2224e1abe0
commit
4c0a8bb700
7 changed files with 105 additions and 22 deletions
|
@ -17,10 +17,9 @@
|
||||||
"include": ["**/*.ts"],
|
"include": ["**/*.ts"],
|
||||||
"options": {
|
"options": {
|
||||||
"useTabs": true,
|
"useTabs": true,
|
||||||
"lineWidth": 140,
|
"lineWidth": 180,
|
||||||
"indentWidth": 4,
|
"indentWidth": 4,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"proseWrap": "preserve",
|
|
||||||
"trailingCommas": "never"
|
"trailingCommas": "never"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
--border-normal: #888;
|
--border-normal: #888;
|
||||||
--border-highlight: #bbb;
|
--border-highlight: #bbb;
|
||||||
--icon-scale: 1.25;
|
--icon-scale: 1.25;
|
||||||
|
--border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
@ -184,7 +185,7 @@ button {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid var(--text);
|
border: 1px solid var(--text);
|
||||||
border-radius: 4px;
|
border-radius: var(--border-radius);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
body.append("file", avatar, encodeURIComponent(avatar.name));
|
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, {
|
const avatar_upload_response = await api.fetch(avatar_path, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
|
|
@ -90,10 +90,24 @@
|
||||||
padding: 0.75rem;
|
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;
|
width: 50px;
|
||||||
padding: inherit;
|
padding: inherit;
|
||||||
margin: 0 1rem;
|
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 {
|
#talk #room-chat-entry-container form textarea {
|
||||||
|
@ -101,6 +115,7 @@
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background: inherit;
|
background: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
#talk .message-container {
|
#talk .message-container {
|
||||||
|
@ -109,7 +124,7 @@
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border-radius: 4px;
|
border-radius: var(--border-radius);
|
||||||
}
|
}
|
||||||
|
|
||||||
#talk .message-container.user-tick.time-tick + .message-container.user-tick.time-tick,
|
#talk .message-container.user-tick.time-tick + .message-container.user-tick.time-tick,
|
||||||
|
@ -250,12 +265,17 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#talk .embed-container audio {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
#talk .embed-container.rounded {
|
#talk .embed-container.rounded {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#talk .embed-container.short {
|
#talk .embed-container.short {
|
||||||
height: 0;
|
height: 0;
|
||||||
|
min-height: 40px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-bottom: 7.5%;
|
padding-bottom: 7.5%;
|
||||||
|
@ -347,7 +367,8 @@
|
||||||
padding: 0.25rem;
|
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;
|
margin: 0 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,9 +89,16 @@
|
||||||
<div id="room-chat-content"></div>
|
<div id="room-chat-content"></div>
|
||||||
<div id="room-chat-entry-container">
|
<div id="room-chat-entry-container">
|
||||||
<form id="room-chat-entry" action="" data-smart="true" data-method="POST">
|
<form id="room-chat-entry" action="" data-smart="true" data-method="POST">
|
||||||
<button aria-label="Attach file">
|
<input
|
||||||
<i class="icon attachment"></i>
|
id="file-upload-and-share-input"
|
||||||
</button>
|
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
|
<textarea
|
||||||
id="room-chat-input"
|
id="room-chat-input"
|
||||||
class="room-chat-input"
|
class="room-chat-input"
|
||||||
|
@ -101,10 +108,12 @@
|
||||||
<button id="room-chat-send" class="primary" aria-label="Send a message">
|
<button id="room-chat-send" class="primary" aria-label="Send a message">
|
||||||
<i class="icon send"></i>
|
<i class="icon send"></i>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
{
|
{
|
||||||
const form = document.currentScript.closest("form");
|
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 chat_input = document.getElementById("room-chat-input");
|
||||||
const room_chat_container =
|
const room_chat_container =
|
||||||
document.getElementById("room-chat-container");
|
document.getElementById("room-chat-container");
|
||||||
|
@ -118,18 +127,48 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
form.on_submit = (event) => {
|
form.on_submit = async (event) => {
|
||||||
const message = chat_input.value.trim();
|
const user = JSON.parse(document.body.dataset.user);
|
||||||
if (message.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const room_id = room_chat_container.dataset.room_id;
|
const room_id = room_chat_container.dataset.room_id;
|
||||||
if (!room_id) {
|
if (!room_id) {
|
||||||
alert("Failed to get room_id!");
|
alert("Failed to get room_id!");
|
||||||
return false;
|
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`;
|
form.action = `/api/rooms/${room_id}/events`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -147,6 +186,15 @@
|
||||||
updated: now,
|
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);
|
const user = JSON.parse(document.body.dataset.user);
|
||||||
render_text_event(room_chat_content, json, user);
|
render_text_event(room_chat_content, json, user);
|
||||||
document
|
document
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
// watch out for places we need to set `lastIndex` ... :frown:
|
// watch out for places we need to set `lastIndex` ... :frown:
|
||||||
|
|
||||||
const URL_MATCHING_REGEX =
|
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 =
|
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;
|
/(?<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 punycode = get_punycode();
|
||||||
// const punycoded_url = punycode.encode(match[0]);
|
// const punycoded_url = punycode.encode(match[0]);
|
||||||
|
|
||||||
|
console.dir({
|
||||||
|
link_info,
|
||||||
|
});
|
||||||
if (typeof link_info.extension === "string") {
|
if (typeof link_info.extension === "string") {
|
||||||
const mime_types = get_mime_types(link_info.extension);
|
const mime_types = get_mime_types(link_info.extension);
|
||||||
|
console.dir({
|
||||||
|
mime_types,
|
||||||
|
});
|
||||||
if (mime_types.length) {
|
if (mime_types.length) {
|
||||||
if (mime_types.includes("image/gif")) {
|
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>`;
|
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) {
|
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>`;
|
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) {
|
function message_text_to_html(input) {
|
||||||
let html_message = input;
|
let html_message = (input ?? "").replace(/\n/g, "<br/>");
|
||||||
let match;
|
let match;
|
||||||
|
|
||||||
URL_MATCHING_REGEX.lastIndex = 0;
|
URL_MATCHING_REGEX.lastIndex = 0;
|
||||||
|
|
|
@ -19,9 +19,7 @@ export async function get_session(request: Request, meta: Record<string, any>):
|
||||||
meta.valid_session = !!meta.session && meta.now < new Date(meta.session.timestamps.expires).valueOf();
|
meta.valid_session = !!meta.session && meta.now < new Date(meta.session.timestamps.expires).valueOf();
|
||||||
|
|
||||||
meta.request_totp = request.headers.get(`x-${TOTP_TOKEN}`) ?? meta.cookies[TOTP_TOKEN] ?? '';
|
meta.request_totp = request.headers.get(`x-${TOTP_TOKEN}`) ?? meta.cookies[TOTP_TOKEN] ?? '';
|
||||||
meta.valid_totp = meta.valid_session && meta.session && meta.request_totp
|
meta.valid_totp = meta.valid_session && meta.session && meta.request_totp ? await verifyTotp(meta.request_totp, meta.session.secret) : false;
|
||||||
? await verifyTotp(meta.request_totp, meta.session.secret)
|
|
||||||
: false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_user(request: Request, meta: Record<string, any>): Promise<undefined> {
|
export async function get_user(request: Request, meta: Record<string, any>): Promise<undefined> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue