refactor: clean up chat and split up embed handling

This commit is contained in:
Andy Burke 2025-09-16 12:25:11 -07:00
parent 03751c6d00
commit 7e4ab72fe6
14 changed files with 352 additions and 274 deletions

48
public/js/embeds/audio.js Normal file
View file

@ -0,0 +1,48 @@
function embed_audio(link_info) {
if (typeof link_info.extension !== "string") {
return;
}
const mime_types = get_mime_types(link_info.extension);
const is_audio = mime_types[0]?.indexOf("audio") === 0;
if (!is_audio) {
return;
}
return `
<div class="audio-container" tabindex="-1">
<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 class="enhanced-audio-player-container">
<div class="audio-player-display-container">
<canvas class="audio-player-display"></canvas>
</div>
<div class="audio-controls-container">
<div class="progress-container">
<div class="time-container"><span class="current">00:00</span></div>
<div class="slider-container">
<input type="range" name="progress" title="" min="0" max="1000" step="1" value="0" />
<label class="time-container" for="progress"><span class="current">00:00</span></label>
</div>
<div class="time-container"><span class="duration">00:00</span></div>
</div>
<div class="buttons-container">
<div class="audio-control blank">
<a href="${link_info.url}" download>
<div class="icon download"></div>
</a>
</div>
<div class="audio-control skip-back"><div class="icon skip-back"></div></div>
<div class="audio-control play-pause-toggle"><div class="icon play"></div><div class="icon pause"></div></div>
<div class="audio-control skip-forward"><div class="icon skip-forward"></div></div>
<div class="audio-control volume">
<input type="range" name="volume" title="Volume" min="0" max="100" step="1" value="" />
</div>
</div>
</div>
</div>
</div>`;
}

13
public/js/embeds/gif.js Normal file
View file

@ -0,0 +1,13 @@
function embed_gif(link_info) {
if (typeof link_info.extension !== "string") {
return;
}
const is_gif = get_mime_types(link_info.extension).includes("image/gif");
if (!is_gif) {
return;
}
return `<div class="embed-container image gif"><img src="${link_info.url}" alt="A gif from ${link_info.domain}" /></div>`;
}

13
public/js/embeds/image.js Normal file
View file

@ -0,0 +1,13 @@
function embed_image(link_info) {
if (typeof link_info.extension !== "string") {
return;
}
const is_image = get_mime_types(link_info.extension).shift()?.indexOf("image") === 0;
if (!is_image) {
return;
}
return `<div class="embed-container image"><img src="${link_info.url}" alt="An image from ${link_info.domain}" /></div>`;
}

3
public/js/embeds/link.js Normal file
View file

@ -0,0 +1,3 @@
function embed_link(link_info) {
return `<a href="${link_info.url}">${link_info.url}</a>`;
}

13
public/js/embeds/mp4.js Normal file
View file

@ -0,0 +1,13 @@
function embed_mp4(link_info) {
if (typeof link_info.extension !== "string") {
return;
}
const is_mp4 = get_mime_types(link_info.extension).includes("video/mp4");
if (!is_mp4) {
return;
}
return `<div class="embed-container image gif letterbox"><video autoplay="true" muted="true" loop="true" playsinline="true"><source src="${link_info.url}" type="video/mp4"><a href="${link_info.url}">${link_info.url}</a></video></div>`;
}

View file

@ -0,0 +1,33 @@
const SPOTIFY_EXTRACTOR =
/^\/(?<item_type>(?:album|artist|episode|playlist|tracks?))\/?(?<item_id>[a-zA-Z0-9]{22})/gi;
function embed_spotify(link_info) {
const is_spotify_link = ["spotify.com"].includes(link_info.domain?.toLowerCase());
if (!is_spotify_link) {
return;
}
SPOTIFY_EXTRACTOR.lastIndex = 0;
const {
groups: { item_type, item_id },
} = SPOTIFY_EXTRACTOR.exec(link_info.path ?? "") ?? { groups: {} };
if (!item_id) {
return;
}
return `
<div class="embed-container iframe ${!item_type || item_type.toLowerCase().indexOf("track") === 0 ? "short" : "square"} spotify rounded">
<div class="embed-actions-container">
<button class="icon plus" onclick="console.log(\"close\");"/>
<button class="icon pause" onclick="console.log(\"stop\");"/>
</div>
<iframe
src="https://open.spotify.com/embed/${item_type ?? "track"}/${item_id}"
allowfullscreen
allow="clipboard-write; encrypted-media; fullscreen; picture-in-picture"
loading="lazy"></iframe>
</div>`;
}

35
public/js/embeds/tidal.js Normal file
View file

@ -0,0 +1,35 @@
const TIDAL_EXTRACTOR =
/^\/(?:(?<action>.*?)\/)?(?<item_type>(?:album|artist|episode|playlist|tracks?))\/(?<item_id>[0-9]+)/gi;
function embed_tidal(link_info) {
const is_tidal_link = ["tidal.com", "tidalhi.fi"].includes(link_info.domain?.toLowerCase());
if (!is_tidal_link) {
return;
}
TIDAL_EXTRACTOR.lastIndex = 0;
const {
groups: { action, item_type, item_id },
} = TIDAL_EXTRACTOR.exec(link_info.path ?? "") ?? { groups: {} };
if (!(item_type && item_id)) {
return;
}
return `
<div class="embed-container iframe ${item_type.toLowerCase().indexOf("track") === 0 ? "short" : "square"} tidal">
<div class="embed-actions-container">
<button class="icon plus" onclick="console.log(\"close\");"/>
<button class="icon pause" onclick="console.log(\"stop\");"/>
</div>
<iframe
src="https://embed.tidal.com/${item_type.at(-1) === "s" ? item_type : `${item_type}s`}/${item_id}"
allow="encrypted-media"
sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
title="TIDAL Embed Player"
loading="lazy"
></iframe>
</div>`;
}

35
public/js/embeds/vimeo.js Normal file
View file

@ -0,0 +1,35 @@
const VIMEO_ID_EXTRACTOR =
/(?<video_domain>vimeo\.com)(?:\/(?<action>video|embed|watch|shorts|v))?.*(?:(?:\/|v=)(?<video_id>[A-Za-z0-9._%-]*))\S*/gi;
function embed_vimeo(link_info) {
const is_vimeo_link = ["vimeo.com"].includes(link_info.domain?.toLowerCase());
if (!is_vimeo_link) {
return;
}
VIMEO_ID_EXTRACTOR.lastIndex = 0;
const {
groups: { video_domain, action, video_id },
} = VIMEO_ID_EXTRACTOR.exec(link_info.url) ?? { groups: {} };
if (!video_id) {
return;
}
return `
<div class="embed-container iframe letterbox vimeo">
<div class="embed-actions-container">
<button class="icon plus" onclick="console.log(\"close\");"/>
<button class="icon pause" onclick="console.log(\"stop\");"/>
</div>
<iframe
src="https://player.vimeo.com/video/${video_id}"
frameborder="0"
allow="fullscreen; picture-in-picture; clipboard-write; encrypted-media; web-share"
referrerpolicy="strict-origin-when-cross-origin"
title="Star Trek: Legacy"
loading="lazy"></iframe>
</div>`;
}

View file

@ -0,0 +1,38 @@
const YOUTUBE_ID_EXTRACTOR =
/(?<video_domain>youtu(?:be\.com|\.be|be\.googleapis\.com))(?:\/(?<action>video|embed|watch|shorts|v))?.*(?:(?:\/|v=)(?<video_id>[A-Za-z0-9._%-]*))\S*/gi;
function embed_youtube(link_info) {
const is_youtube_link = ["youtube.com", "youtu.be", "youtube.googleapis.com"].includes(
link_info.domain?.toLowerCase(),
);
if (!is_youtube_link) {
return;
}
YOUTUBE_ID_EXTRACTOR.lastIndex = 0;
const {
groups: { video_domain, action, video_id },
} = YOUTUBE_ID_EXTRACTOR.exec(link_info.url) ?? { groups: {} };
if (!video_id) {
return;
}
return `
<div class="embed-container iframe ${action === "shorts" ? "vertical" : "letterbox"} youtube">
<div class="embed-actions-container">
<button class="icon plus" onclick="console.log(\"close\");"/>
<button class="icon pause" onclick="console.log(\"stop\");"/>
</div>
<iframe
src="https://www.youtube.com/embed/${video_id}"
title="YouTube video player"
allow="clipboard-write; encrypted-media; picture-in-picture; web-share;"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen
loading="lazy"
></iframe>
</div>`;
}

55
public/js/htmlify.js Normal file
View file

@ -0,0 +1,55 @@
// wow https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time
// watch out for places we need to set `lastIndex` ... :frown:
const EMBEDS = [
embed_tidal,
embed_spotify,
embed_youtube,
embed_vimeo,
embed_audio,
embed_gif,
embed_mp4,
embed_image,
embed_link,
];
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}))|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;
function htmlify(content) {
let html_content = (content ?? "").replace(/\n/g, "<br/>");
let match;
URL_MATCHING_REGEX.lastIndex = 0;
while ((match = URL_MATCHING_REGEX.exec(content)) !== null) {
const url = match[0];
const {
groups: { protocol, host, hostname, domain, tld, path, extension, query, hash },
} = match;
const link_info = {
url,
protocol,
host,
hostname,
domain,
tld,
path,
extension,
query,
hash,
};
for (const embed of EMBEDS) {
const result = embed(link_info);
if (typeof result === "string") {
html_content = html_content.replace(url, result);
break;
}
}
}
return html_content;
}