refactor: clean up chat and split up embed handling
This commit is contained in:
parent
03751c6d00
commit
7e4ab72fe6
14 changed files with 352 additions and 274 deletions
|
|
@ -16,8 +16,9 @@ const DEFAULT_USER_PERMISSIONS: string[] = [
|
||||||
'topics.read',
|
'topics.read',
|
||||||
'topics.chat.write',
|
'topics.chat.write',
|
||||||
'topics.chat.read',
|
'topics.chat.read',
|
||||||
'topics.threads.write',
|
'topics.posts.create',
|
||||||
'topics.threads.read',
|
'topics.posts.write',
|
||||||
|
'topics.posts.read',
|
||||||
'users.read'
|
'users.read'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,9 +254,9 @@ body[data-perms*="topics.write"] [data-requires-permission="topics.write"],
|
||||||
body[data-perms*="topics.chat.create"] [data-requires-permission="topics.chat.create"],
|
body[data-perms*="topics.chat.create"] [data-requires-permission="topics.chat.create"],
|
||||||
body[data-perms*="topics.chat.read"] [data-requires-permission="topics.chat.read"],
|
body[data-perms*="topics.chat.read"] [data-requires-permission="topics.chat.read"],
|
||||||
body[data-perms*="topics.chat.write"] [data-requires-permission="topics.chat.write"],
|
body[data-perms*="topics.chat.write"] [data-requires-permission="topics.chat.write"],
|
||||||
body[data-perms*="topics.threads.create"] [data-requires-permission="topics.threads.create"],
|
body[data-perms*="topics.posts.create"] [data-requires-permission="topics.posts.create"],
|
||||||
body[data-perms*="topics.threads.read"] [data-requires-permission="topics.threads.read"],
|
body[data-perms*="topics.posts.read"] [data-requires-permission="topics.posts.read"],
|
||||||
body[data-perms*="topics.threads.write"] [data-requires-permission="topics.threads.write"],
|
body[data-perms*="topics.posts.write"] [data-requires-permission="topics.posts.write"],
|
||||||
body[data-perms*="users.read"] [data-requires-permission="users.read"],
|
body[data-perms*="users.read"] [data-requires-permission="users.read"],
|
||||||
body[data-perms*="users.write"] [data-requires-permission="users.write"],
|
body[data-perms*="users.write"] [data-requires-permission="users.write"],
|
||||||
body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] {
|
body[data-perms*="files.write.own"] [data-requires-permission="files.write.own"] {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,19 @@
|
||||||
|
|
||||||
<script src="./js/audioplayer.js" type="text/javascript"></script>
|
<script src="./js/audioplayer.js" type="text/javascript"></script>
|
||||||
<script src="./js/datetimeutils.js" type="text/javascript"></script>
|
<script src="./js/datetimeutils.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<script src="./js/embeds/audio.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/gif.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/image.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/link.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/mp4.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/spotify.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/tidal.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/vimeo.js" type="text/javascript"></script>
|
||||||
|
<script src="./js/embeds/youtube.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<script src="./js/htmlify.js" type="text/javascript"></script>
|
||||||
|
|
||||||
<script src="./js/locationchange.js" type="text/javascript"></script>
|
<script src="./js/locationchange.js" type="text/javascript"></script>
|
||||||
<script src="./js/notifications.js" type="text/javascript"></script>
|
<script src="./js/notifications.js" type="text/javascript"></script>
|
||||||
<script src="./js/totp.js" type="text/javascript"></script>
|
<script src="./js/totp.js" type="text/javascript"></script>
|
||||||
|
|
@ -29,6 +42,40 @@
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
<script>
|
<script>
|
||||||
|
/* globals - sue me */
|
||||||
|
const USERS = {
|
||||||
|
_evict_timeouts: {},
|
||||||
|
_update_timeouts: {},
|
||||||
|
get: async (id, force) => {
|
||||||
|
if ( force || !USERS[ id ] ) {
|
||||||
|
USERS[ id ] = (await (await api.fetch(`/api/users/${id}`)).json());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !USERS._update_timeouts[ id ] ) {
|
||||||
|
USERS._update_timeouts[ id ] = setInterval( () => {
|
||||||
|
USERS.get( id, true );
|
||||||
|
}, 1 * 60_000 );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !force ) {
|
||||||
|
if ( USERS._evict_timeouts[ id ] ) {
|
||||||
|
clearTimeout( USERS._evict_timeouts[ id ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
USERS._evict_timeouts[id] = setTimeout( () => {
|
||||||
|
if ( USERS._update_timeouts[ id ] ) {
|
||||||
|
clearTimeout( USERS._update_timeouts[ id ] );
|
||||||
|
delete USERS._update_timeouts[ id ];
|
||||||
|
}
|
||||||
|
|
||||||
|
delete USERS[ id ];
|
||||||
|
}, 10 * 60_000 );
|
||||||
|
}
|
||||||
|
|
||||||
|
return USERS[ id ];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const HASH_EXTRACTOR = /^\#\/topic\/(?<topic_id>[A-Za-z\-]+)\/?(?<view>\w+)/gm;
|
const HASH_EXTRACTOR = /^\#\/topic\/(?<topic_id>[A-Za-z\-]+)\/?(?<view>\w+)/gm;
|
||||||
|
|
||||||
function extract_url_hash_info() {
|
function extract_url_hash_info() {
|
||||||
|
|
|
||||||
48
public/js/embeds/audio.js
Normal file
48
public/js/embeds/audio.js
Normal 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
13
public/js/embeds/gif.js
Normal 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
13
public/js/embeds/image.js
Normal 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
3
public/js/embeds/link.js
Normal 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
13
public/js/embeds/mp4.js
Normal 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>`;
|
||||||
|
}
|
||||||
33
public/js/embeds/spotify.js
Normal file
33
public/js/embeds/spotify.js
Normal 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
35
public/js/embeds/tidal.js
Normal 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
35
public/js/embeds/vimeo.js
Normal 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>`;
|
||||||
|
}
|
||||||
38
public/js/embeds/youtube.js
Normal file
38
public/js/embeds/youtube.js
Normal 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
55
public/js/htmlify.js
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,263 +1,10 @@
|
||||||
// wow https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time
|
|
||||||
// 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}))|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;
|
|
||||||
const SPOTIFY_EXTRACTOR =
|
|
||||||
/^\/(?<item_type>(?:album|artist|episode|playlist|tracks?))\/?(?<item_id>[a-zA-Z0-9]{22})/gi;
|
|
||||||
const TIDAL_EXTRACTOR =
|
|
||||||
/^\/(?:(?<action>.*?)\/)?(?<item_type>(?:album|artist|episode|playlist|tracks?))\/(?<item_id>[0-9]+)/gi;
|
|
||||||
|
|
||||||
const URL_MATCH_HANDLERS = [
|
|
||||||
// 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>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 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>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// YouTube
|
|
||||||
(link_info) => {
|
|
||||||
const is_youtube_link = ["youtube.com", "youtu.be", "youtube.googleapis.com"].includes(
|
|
||||||
link_info.domain?.toLowerCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!is_youtube_link) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VIDEO_ID_EXTRACTOR.lastIndex = 0;
|
|
||||||
|
|
||||||
const {
|
|
||||||
groups: { video_domain, action, video_id },
|
|
||||||
} = VIDEO_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>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Vimeo
|
|
||||||
(link_info) => {
|
|
||||||
const is_vimeo_link = ["vimeo.com"].includes(link_info.domain?.toLowerCase());
|
|
||||||
|
|
||||||
if (!is_vimeo_link) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
VIDEO_ID_EXTRACTOR.lastIndex = 0;
|
|
||||||
|
|
||||||
const {
|
|
||||||
groups: { video_domain, action, video_id },
|
|
||||||
} = VIDEO_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>`;
|
|
||||||
},
|
|
||||||
|
|
||||||
// linkify generic url
|
|
||||||
(link_info) => {
|
|
||||||
// TODO: punycoding if something has unicode?
|
|
||||||
// const punycode = get_punycode();
|
|
||||||
// const punycoded_url = punycode.encode(match[0]);
|
|
||||||
|
|
||||||
if (typeof link_info.extension === "string") {
|
|
||||||
const mime_types = get_mime_types(link_info.extension);
|
|
||||||
|
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mime_types.includes("video/mp4")) {
|
|
||||||
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>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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="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>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<a href="${link_info.url}">${link_info.url}</a>`;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function message_text_to_html(input) {
|
|
||||||
let html_message = (input ?? "").replace(/\n/g, "<br/>");
|
|
||||||
let match;
|
|
||||||
|
|
||||||
URL_MATCHING_REGEX.lastIndex = 0;
|
|
||||||
while ((match = URL_MATCHING_REGEX.exec(input)) !== null) {
|
|
||||||
const url = match[0];
|
|
||||||
|
|
||||||
const {
|
|
||||||
groups: { protocol, host, hostname, domain, tld, path, extension, query, hash },
|
|
||||||
} = match;
|
|
||||||
|
|
||||||
for (const handler of URL_MATCH_HANDLERS) {
|
|
||||||
const result = handler({
|
|
||||||
url,
|
|
||||||
protocol,
|
|
||||||
host,
|
|
||||||
hostname,
|
|
||||||
domain,
|
|
||||||
tld,
|
|
||||||
path,
|
|
||||||
extension,
|
|
||||||
query,
|
|
||||||
hash,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof result === "string") {
|
|
||||||
html_message = html_message.replace(url, result);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return html_message;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time_tick_tock_timeout = 60 * 1000; // 1 minute
|
const time_tick_tock_timeout = 60 * 1000; // 1 minute
|
||||||
let last_event_datetime_value = 0;
|
let last_event_datetime_value = 0;
|
||||||
let time_tick_tock_class = "time-tock";
|
let time_tick_tock_class = "time-tock";
|
||||||
|
|
||||||
let last_creator_id = null;
|
let last_creator_id = null;
|
||||||
let user_tick_tock_class = "user-tock";
|
let user_tick_tock_class = "user-tock";
|
||||||
function render_text_event(topic_chat_content, event, creator, existing_element) {
|
function render_chat_message(topic_chat_content, event, creator, existing_element) {
|
||||||
const event_datetime = datetime_to_local(event.timestamps.created);
|
const event_datetime = datetime_to_local(event.timestamps.created);
|
||||||
|
|
||||||
if (event_datetime.value - last_event_datetime_value > time_tick_tock_timeout) {
|
if (event_datetime.value - last_event_datetime_value > time_tick_tock_timeout) {
|
||||||
|
|
@ -299,7 +46,7 @@ function render_text_event(topic_chat_content, event, creator, existing_element)
|
||||||
<span class="short">${event_datetime.short}</span>
|
<span class="short">${event_datetime.short}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-content-container">${message_text_to_html(event.data.content)}</div>
|
<div class="message-content-container">${htmlify(event.data.content)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
if (existing_element) {
|
if (existing_element) {
|
||||||
|
|
@ -311,8 +58,7 @@ function render_text_event(topic_chat_content, event, creator, existing_element)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = {};
|
async function append_chat_events(events) {
|
||||||
async function append_topic_events(events) {
|
|
||||||
const topic_chat_content = document.getElementById("topic-chat-content");
|
const topic_chat_content = document.getElementById("topic-chat-content");
|
||||||
let last_message_id = topic_chat_content.dataset.last_message_id ?? "";
|
let last_message_id = topic_chat_content.dataset.last_message_id ?? "";
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
|
|
@ -328,16 +74,14 @@ async function append_topic_events(events) {
|
||||||
topic_chat_content.dataset.last_message_id = last_message_id;
|
topic_chat_content.dataset.last_message_id = last_message_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
users[event.creator_id] =
|
const creator = await USERS.get(event.creator_id);
|
||||||
users[event.creator_id] ??
|
|
||||||
(await (await api.fetch(`/api/users/${event.creator_id}`)).json());
|
|
||||||
|
|
||||||
const existing_element =
|
const existing_element =
|
||||||
document.getElementById(`chat-${event.id.substring(0, 49)}`) ??
|
document.getElementById(`chat-${event.id.substring(0, 49)}`) ??
|
||||||
(event.meta?.temp_id
|
(event.meta?.temp_id
|
||||||
? document.getElementById(`chat-${event.meta.temp_id}`)
|
? document.getElementById(`chat-${event.meta.temp_id}`)
|
||||||
: undefined);
|
: undefined);
|
||||||
render_text_event(topic_chat_content, event, users[event.creator_id], existing_element);
|
render_chat_message(topic_chat_content, event, creator, existing_element);
|
||||||
}
|
}
|
||||||
|
|
||||||
topic_chat_content.scrollTop = topic_chat_content.scrollHeight;
|
topic_chat_content.scrollTop = topic_chat_content.scrollHeight;
|
||||||
|
|
@ -347,7 +91,7 @@ async function append_topic_events(events) {
|
||||||
// similar for when we change topics, this is the most basic
|
// similar for when we change topics, this is the most basic
|
||||||
// first pass outline
|
// first pass outline
|
||||||
let topic_polling_request_abort_controller = null;
|
let topic_polling_request_abort_controller = null;
|
||||||
async function poll_for_new_events() {
|
async function poll_for_new_chat_events() {
|
||||||
const topic_chat_content = document.getElementById("topic-chat-content");
|
const topic_chat_content = document.getElementById("topic-chat-content");
|
||||||
const topic_id = document.body.dataset.topic;
|
const topic_id = document.body.dataset.topic;
|
||||||
const last_message_id = topic_chat_content.dataset.last_message_id;
|
const last_message_id = topic_chat_content.dataset.last_message_id;
|
||||||
|
|
@ -366,8 +110,8 @@ async function poll_for_new_events() {
|
||||||
})
|
})
|
||||||
.then(async (new_events_response) => {
|
.then(async (new_events_response) => {
|
||||||
const new_events = ((await new_events_response.json()) ?? []).reverse();
|
const new_events = ((await new_events_response.json()) ?? []).reverse();
|
||||||
await append_topic_events(new_events.toReversed());
|
await append_chat_events(new_events.toReversed());
|
||||||
poll_for_new_events(topic_id);
|
poll_for_new_chat_events(topic_id);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// TODO: poll again? back off?
|
// TODO: poll again? back off?
|
||||||
|
|
@ -375,7 +119,7 @@ async function poll_for_new_events() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load_active_topic() {
|
async function load_active_topic_for_chat() {
|
||||||
const topic_id = document.body.dataset.topic;
|
const topic_id = document.body.dataset.topic;
|
||||||
if (!topic_id) return;
|
if (!topic_id) return;
|
||||||
|
|
||||||
|
|
@ -420,8 +164,8 @@ async function load_active_topic() {
|
||||||
|
|
||||||
const events = (await events_response.json()).reverse();
|
const events = (await events_response.json()).reverse();
|
||||||
|
|
||||||
await append_topic_events(events);
|
await append_chat_events(events);
|
||||||
poll_for_new_events();
|
poll_for_new_chat_events();
|
||||||
}
|
}
|
||||||
document.addEventListener("topic_changed", load_active_topic);
|
document.addEventListener("topic_changed", load_active_topic_for_chat);
|
||||||
document.addEventListener("user_logged_in", load_active_topic);
|
document.addEventListener("user_logged_in", load_active_topic_for_chat);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue