summaryrefslogtreecommitdiff
path: root/static/js
diff options
context:
space:
mode:
Diffstat (limited to 'static/js')
-rw-r--r--static/js/pages/chat_init.js13
-rw-r--r--static/js/pages/directory_init.js11
-rw-r--r--static/js/pages/expandable_login_init.js11
-rw-r--r--static/js/pages/frontpage_refresh.js11
-rw-r--r--static/js/pages/fullscreen_front_init.js18
-rw-r--r--static/js/pages/fullscreen_front_run.js7
-rw-r--r--static/js/pages/fullscreen_init.js55
-rw-r--r--static/js/pages/log_favs_init.js7
-rw-r--r--static/js/pages/log_init.js58
-rw-r--r--static/js/pages/scroll_pane_init.js13
-rw-r--r--static/js/pages/search_files_init.js13
-rwxr-xr-xstatic/js/pichat.butt.js104
-rwxr-xr-xstatic/js/pichat.js106
-rwxr-xr-xstatic/js/src/text.js104
-rwxr-xr-xstatic/js/src/youtube.js2
15 files changed, 479 insertions, 54 deletions
diff --git a/static/js/pages/chat_init.js b/static/js/pages/chat_init.js
index 58dba07..7149291 100644
--- a/static/js/pages/chat_init.js
+++ b/static/js/pages/chat_init.js
@@ -23,9 +23,17 @@
if (typeof window.Recips === 'undefined') { window.Recips = []; }
var hasMessageList = document.getElementById('messageList') !== null;
+ var hasChatInput = document.getElementById('msgInput') !== null;
+
+ if (hasMessageList && hasChatInput && typeof window.initChat === 'function') { window.initChat(); }
+ if (hasMessageList && hasChatInput && typeof window.initChatMsgs === 'function') { window.initChatMsgs(); }
+
+ // Some room pages render dumps in `.logged-dump` containers without the full chat UI.
+ // In that case, run the log initializer so URLs become hotlinked images.
+ if (!hasChatInput && typeof window.initLog === 'function' && jQuery('.logged-dump .content').length) {
+ window.initLog(window.Recips);
+ }
- if (hasMessageList && typeof window.initChat === 'function') { window.initChat(); }
- if (hasMessageList && typeof window.initChatMsgs === 'function') { window.initChatMsgs(); }
if (typeof window.Away !== 'undefined' && typeof window.Away.startTitleUpdater === 'function') { window.Away.startTitleUpdater(); }
if (window.Nick && typeof window.setupUpload === 'function' && typeof window.Room !== 'undefined') {
@@ -33,4 +41,3 @@
}
});
})(jQuery);
-
diff --git a/static/js/pages/directory_init.js b/static/js/pages/directory_init.js
new file mode 100644
index 0000000..70f2017
--- /dev/null
+++ b/static/js/pages/directory_init.js
@@ -0,0 +1,11 @@
+// Directory page initializer.
+// Keeps templates mostly data-only.
+
+(function() {
+ if (typeof jQuery !== 'function') { return; }
+
+ jQuery(function() {
+ if (typeof initDirectory === 'function') { initDirectory(); }
+ });
+})();
+
diff --git a/static/js/pages/expandable_login_init.js b/static/js/pages/expandable_login_init.js
new file mode 100644
index 0000000..669836f
--- /dev/null
+++ b/static/js/pages/expandable_login_init.js
@@ -0,0 +1,11 @@
+// Expandable login-form initializer (used by $form_login()$).
+// Keeps templates mostly data-only.
+
+(function() {
+ if (typeof jQuery !== 'function') { return; }
+
+ jQuery(function() {
+ if (typeof initExpandableLoginForm === 'function') { initExpandableLoginForm(); }
+ });
+})();
+
diff --git a/static/js/pages/frontpage_refresh.js b/static/js/pages/frontpage_refresh.js
new file mode 100644
index 0000000..aa31cc2
--- /dev/null
+++ b/static/js/pages/frontpage_refresh.js
@@ -0,0 +1,11 @@
+// Frontpage-only helper.
+// The legacy frontpage HTML calls `refreshing()` via <body onload="refreshing()">.
+
+if (typeof window.refreshing !== 'function') {
+ window.refreshing = function refreshing() {
+ var el = document.getElementsByName('posts')[0];
+ if (el) { el.src = el.src; }
+ setTimeout(refreshing, 300000);
+ };
+}
+
diff --git a/static/js/pages/fullscreen_front_init.js b/static/js/pages/fullscreen_front_init.js
new file mode 100644
index 0000000..a1a87a5
--- /dev/null
+++ b/static/js/pages/fullscreen_front_init.js
@@ -0,0 +1,18 @@
+// Shared initializer for fullscreen feed pages.
+// Keeps template inline JS mostly data-only (LoggedIn, Timestamp).
+
+(function($){
+ function defaultPop(url) {
+ var newwindow = window.open(
+ url,
+ 'name',
+ 'height=50,width=400,left=20,top=20,location=0,status=0,scrollbar=0,resizable=0'
+ );
+ if (window.focus && newwindow) { newwindow.focus(); }
+ return newwindow;
+ }
+
+ if (typeof window.pop !== 'function') { window.pop = defaultPop; }
+
+ if (typeof window.startChatUpdater === 'function') { $(window.startChatUpdater); }
+})(jQuery);
diff --git a/static/js/pages/fullscreen_front_run.js b/static/js/pages/fullscreen_front_run.js
new file mode 100644
index 0000000..1116b7a
--- /dev/null
+++ b/static/js/pages/fullscreen_front_run.js
@@ -0,0 +1,7 @@
+// Runs `initFullscreen()` for `template/fullscreen_front.st` without inline JS.
+(function() {
+ if (typeof window.initFullscreen === 'function') {
+ window.initFullscreen();
+ }
+})();
+
diff --git a/static/js/pages/fullscreen_init.js b/static/js/pages/fullscreen_init.js
new file mode 100644
index 0000000..a7f8bb7
--- /dev/null
+++ b/static/js/pages/fullscreen_init.js
@@ -0,0 +1,55 @@
+// Initializer for `/fullscreen` (login/register overlays + fullscreen feed).
+// Goal: reduce template inline JS without changing behavior.
+
+(function($){
+ function choice(a) { return a[Math.floor(Math.random() * a.length)]; }
+
+ function defaultPop(url) {
+ var newwindow = window.open(
+ url,
+ 'name',
+ 'height=50,width=400,left=20,top=20,location=0,status=0,scrollbar=0,resizable=0'
+ );
+ if (window.focus && newwindow) { newwindow.focus(); }
+ return newwindow;
+ }
+
+ if (typeof window.pop !== 'function') { window.pop = defaultPop; }
+
+ var urls = [
+ 'https://archive.hump.fm/images/20100601/1275428508049-dumpfm-foot-oie_oie_overlay-1.gif',
+ 'https://archive.hump.fm/images/20100928/1285728674225-dumpfm-timb-running.unicorn.gif',
+ 'https://archive.hump.fm/images/20100726/1280119193796-dumpfm-enso-human-condition.gif',
+ 'https://archive.hump.fm/images/20100521/1274415795577-dumpfm-ucnv-mx.gif',
+ 'https://archive.hump.fm/images/20100912/1284315873224-dumpfm-Neontoast-1283990707508-dumpfm-crunkus-crabtoon.gif',
+ 'https://archive.hump.fm/images/20110927/1317105622918-dumpfm-peachfist-test8scam.gif',
+ 'https://archive.hump.fm/images/20110323/1300915179773-dumpfm-blingscience-fishtank.gif',
+ 'https://archive.hump.fm/images/20110418/1303108538834-dumpfm-LAVARLAMAR-lettuce_lavarlamar.gif',
+ 'https://s3.amazonaws.com/i.asdf.us/im/84/gradient_horse_1318306378_1322355741_ryz_1337322355_ryz.gif',
+ 'https://archive.hump.fm/images/20110724/1311552093462-dumpfm-hologrampa-1291586335941-dumpfm-jeeeelings-cat_face_wink_hologrampa-lettuce.gif',
+ 'https://s3.amazonaws.com/i.asdf.us/im/be/tt7620731fltt_1315431978.gif'
+ ];
+
+ // Legacy behavior: register these on DOM-ready (historically done via `jQuery(initLogin)`).
+ if (typeof window.initLogin === 'function') { $(window.initLogin); }
+ if (typeof window.startChatUpdater === 'function') { $(window.startChatUpdater); }
+
+ if (window.location && window.location.href.indexOf('nologin') !== -1) {
+ $('#loginbox').hide();
+ }
+
+ var bigImage = document.getElementById('big-image');
+ if (bigImage) { bigImage.innerHTML = "<img src='" + choice(urls) + "'>"; }
+
+ if (typeof window.initFullscreen === 'function') { window.initFullscreen(); }
+ if (typeof window.initRegister === 'function') { window.initRegister(); }
+
+ $('#reglink').click(function(e){
+ e.preventDefault();
+ $('#loginbox').hide();
+ $('#registerbox').show().addClass('b');
+ return false;
+ });
+
+ $('#nickInput').focus();
+})(jQuery);
diff --git a/static/js/pages/log_favs_init.js b/static/js/pages/log_favs_init.js
new file mode 100644
index 0000000..47565a9
--- /dev/null
+++ b/static/js/pages/log_favs_init.js
@@ -0,0 +1,7 @@
+// Log template helper: kick off the permalink fav-list loader (defined in pichat.js).
+(function() {
+ if (typeof window.load_favs === 'function') {
+ window.load_favs();
+ }
+})();
+
diff --git a/static/js/pages/log_init.js b/static/js/pages/log_init.js
new file mode 100644
index 0000000..93d27b0
--- /dev/null
+++ b/static/js/pages/log_init.js
@@ -0,0 +1,58 @@
+// Shared initializer for log-like pages (frontpage/log views) that include pichat.js.
+// Keeps templates mostly data-only (Recips, MasonryColumnWidth).
+
+(function($){
+ function initLogIfPresent() {
+ if (typeof window.Recips === 'undefined') { window.Recips = []; }
+ if (typeof window.initLog === 'function') { window.initLog(window.Recips); }
+ }
+
+ function initMasonryIfPresent() {
+ var $posts = $('#posts');
+ if (!$posts.length) { return; }
+ if (typeof $posts.masonry !== 'function') { return; }
+
+ var colWidth = typeof window.MasonryColumnWidth !== 'undefined' ? window.MasonryColumnWidth : 275;
+
+ $posts.masonry({ columnWidth: colWidth });
+ $posts.masonry({ singleMode: true });
+ $posts.masonry({ resizeable: true });
+ $posts.masonry({ animate: true });
+ }
+
+ $(initLogIfPresent);
+ $(window).load(initMasonryIfPresent);
+})(jQuery);
+
+if (typeof window.images_loading_bar !== 'function') {
+ window.images_loading_bar = function images_loading_bar() {
+ try {
+ var imgs = document.getElementsByTagName('img');
+ var total = imgs.length;
+
+ if (!total) {
+ var lb0 = document.getElementById('LB0');
+ if (lb0) { lb0.style.display = 'none'; }
+ return;
+ }
+
+ var loaded = 0;
+ for (var i = 0; i < total; i++) {
+ loaded += imgs[i].complete ? 1 : 0;
+ }
+
+ var lb1 = document.getElementById('LB1');
+ if (lb1) { lb1.style.width = Math.round((loaded / total) * 100) + 'px'; }
+
+ if (loaded === total) {
+ setTimeout(function() {
+ var lb0Done = document.getElementById('LB0');
+ if (lb0Done) { lb0Done.style.display = 'none'; }
+ }, 128);
+ } else {
+ setTimeout(images_loading_bar, 64);
+ }
+ } catch (e) {}
+ };
+}
+
diff --git a/static/js/pages/scroll_pane_init.js b/static/js/pages/scroll_pane_init.js
new file mode 100644
index 0000000..1508418
--- /dev/null
+++ b/static/js/pages/scroll_pane_init.js
@@ -0,0 +1,13 @@
+// Optional jScrollPane initializer for legacy pages that use `.scroll-pane`.
+// Keep it defensive: some pages may not load the plugin.
+
+(function() {
+ if (typeof jQuery !== 'function') { return; }
+
+ jQuery(function() {
+ if (jQuery.fn && typeof jQuery.fn.jScrollPane === 'function') {
+ jQuery('.scroll-pane').jScrollPane();
+ }
+ });
+})();
+
diff --git a/static/js/pages/search_files_init.js b/static/js/pages/search_files_init.js
new file mode 100644
index 0000000..661da63
--- /dev/null
+++ b/static/js/pages/search_files_init.js
@@ -0,0 +1,13 @@
+// Full-page search initializer.
+// Keeps templates mostly data-only.
+
+(function() {
+ if (typeof jQuery !== 'function') { return; }
+
+ jQuery(function() {
+ if (window.Search && typeof window.Search.initFullpage === 'function') {
+ window.Search.initFullpage();
+ }
+ });
+})();
+
diff --git a/static/js/pichat.butt.js b/static/js/pichat.butt.js
index 220e873..f0baae5 100755
--- a/static/js/pichat.butt.js
+++ b/static/js/pichat.butt.js
@@ -65,18 +65,68 @@ function normalizeUrl(url) {
}
URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
-PicRegex = /\.(jpg|jpeg|png|gif|bmp)$/i;
+PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|webp|fid)$/i;
+VideoRegex = /\.(mp4|webm|mov|m4v|gifv)$/i;
+
+function splitTrailingPunctuation(url) {
+ if (!url) return { "url": url, "suffix": "" };
+ var match = url.match(/[)\]\}\.,!\?;:'"]+$/);
+ if (!match) return { "url": url, "suffix": "" };
+ return { "url": url.slice(0, -match[0].length), "suffix": match[0] };
+}
+
+function imgurIdFromUri(uri) {
+ var host = parseDomain(uri.host || "");
+ if (!host || !host.match(/(^|\\.)imgur\\.com$/i)) return null;
+ if (!uri.file) return null;
+
+ var fileLower = uri.file.toLowerCase();
+ if (PicRegex.test(fileLower) || VideoRegex.test(fileLower)) return null;
+
+ if (!uri.file.match(/^[A-Za-z0-9]+$/)) return null;
+ return uri.file;
+}
+
+function imgurCandidateUrls(id) {
+ var base = "https://i.imgur.com/" + id;
+ return [base + ".jpg", base + ".png", base + ".gif", base + ".jpeg"];
+}
+
+function imgurHotlinkFallback(img) {
+ try {
+ var candidates = (img.getAttribute("data-imgur-candidates") || "").split("|");
+ if (!candidates.length || !candidates[0]) return;
+
+ var idx = parseInt(img.getAttribute("data-imgur-idx") || "0", 10);
+ var next = idx + 1;
+ if (next >= candidates.length) {
+ var link = img.parentNode;
+ if (link && link.tagName == "A") {
+ var href = link.getAttribute("href") || "";
+ while (link.firstChild) link.removeChild(link.firstChild);
+ link.appendChild(document.createTextNode(href));
+ }
+ return;
+ }
+
+ img.setAttribute("data-imgur-idx", "" + next);
+ img.src = candidates[next];
+ } catch (e) {}
+}
function getImagesAsArray(text) {
var imgs = []
var urls = text.match(URLRegex)
if (urls === null) return imgs
for (var i = 0; i<urls.length; i++){
- var url = urls[i]
- var normalized = normalizeUrl(url);
- var urlWithoutParams = normalized.replace(/[?#].*$/i, "");
- if (PicRegex.test(urlWithoutParams))
- imgs.push(normalized)
+ var split = splitTrailingPunctuation(urls[i]);
+ var normalized = normalizeUrl(split.url);
+ var uri = parseUri(normalized);
+ var imgurId = imgurIdFromUri(uri);
+ var candidate = imgurId ? imgurCandidateUrls(imgurId)[0] : normalized;
+
+ var urlWithoutParams = candidate.replace(/[?#].*$/i, "");
+ if (PicRegex.test(urlWithoutParams)) imgs.push(candidate)
}
return imgs
}
@@ -89,22 +139,35 @@ function linkify(text) {
// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/
var LastMsgContainsImage = false
function linkReplace(url) {
- //var urlWithoutParams = url.replace(/\?.*$/i, "");
-
- linkUrl = normalizeUrl(url);
-
- var uri = parseUri(url)
+ var split = splitTrailingPunctuation(url);
+ linkUrl = normalizeUrl(split.url);
+ var uri = parseUri(linkUrl);
+
+ var imgurId = imgurIdFromUri(uri);
+ if (imgurId) {
+ LastMsgContainsImage = true;
+ var candidates = imgurCandidateUrls(imgurId);
+ var first = candidates[0];
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\">"
+ + "<img src=\"" + first + "\" class=\"imgur-hotlink\" data-imgur-idx=\"0\" data-imgur-candidates=\"" + candidates.join('|') + "\" onerror=\"imgurHotlinkFallback(this)\">"
+ + "</a>" + split.suffix;
+ }
+
switch(getUriType(uri)) {
case 'image':
LastMsgContainsImage = true;
- return "<a target='_blank' href='" + linkUrl + "'><img src='" + linkUrl + "'></a>"; break;
- case 'youtube':
+ return "<a target='_blank' href='" + linkUrl + "'><img src='" + linkUrl + "'></a>" + split.suffix;
+ case 'video':
+ LastMsgContainsImage = true;
+ var videoUrl = linkUrl.replace(/\.gifv([?#].*)?$/i, '.mp4$1');
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\"><video src=\"" + videoUrl + "\" autoplay loop muted controls playsinline></video></a>" + split.suffix;
+ case 'youtube':
Youtube.startAnimation();
- return "<a target='_blank' class='youtube' href='" + linkUrl + "'>" +
- "<img class='youtube-thumb' width='130' height='97' src='"+Youtube.nextThumbUrl(uri.queryKey.v)+"'>" +
- "<img class='youtube-controls' src='/static/img/youtube.controls.png'></a>"; break;
+ return "<a target=\"_blank\" class=\"youtube\" href=\"" + linkUrl + "\">" +
+ "<img class=\"youtube-thumb\" width=\"130\" height=\"97\" src=\"" + Youtube.nextThumbUrl(uri.queryKey.v) + "\">" +
+ "<img class=\"youtube-controls\" src=\"/static/img/youtube.controls.png\"></a>" + split.suffix;
default:
- return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>";
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\">" + split.url + "</a>" + split.suffix;
}
}
@@ -130,7 +193,7 @@ Youtube = {
var img = $(this);
// yt thumb url example: https://i.ytimg.com/vi/0123456789A/1.jpg
var src = img.attr("src") || ""
- var match = src.match(/\\/vi\\/([^/]{11})\\/(\\d)\\.jpg/i)
+ var match = src.match(/\/vi\/([^/]{11})\/(\d)\.jpg/i)
if (!match) return
var v = match[1]
var num = match[2]
@@ -149,8 +212,11 @@ Youtube = {
function getUriType(uri){
if (PicRegex.test(uri.file.toLowerCase()))
return "image";
+
+ if (VideoRegex.test(uri.file.toLowerCase()))
+ return "video";
- if (parseDomain(uri.host) == "youtube.com" && 'v' in uri.queryKey || uri.anchor.indexOf('v') != -1)
+ if (parseDomain(uri.host) == "youtube.com" && ('v' in uri.queryKey || uri.anchor.indexOf('v') != -1))
return "youtube";
return "link";
diff --git a/static/js/pichat.js b/static/js/pichat.js
index 2283baf..ad50fae 100755
--- a/static/js/pichat.js
+++ b/static/js/pichat.js
@@ -2114,21 +2114,74 @@ function escapeHtml(txt) {
URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
-PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|fid)$/i;
+PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|webp|fid)$/i;
+VideoRegex = /\.(mp4|webm|mov|m4v|gifv)$/i;
RecipRegex = /(^|\s)@\w+/g;
TopicRegex = /(^|\s)#\w+/g;
+function splitTrailingPunctuation(url) {
+ if (!url) return { "url": url, "suffix": "" };
+ // Common punctuation that follows pasted links in chat.
+ var match = url.match(/[)\]\}\.,!\?;:'"]+$/);
+ if (!match) return { "url": url, "suffix": "" };
+ return { "url": url.slice(0, -match[0].length), "suffix": match[0] };
+}
+
+function imgurIdFromUri(uri) {
+ var host = (uri.host || "").toLowerCase();
+ if (!host || !host.match(/(^|\.)imgur\.com$/i)) return null;
+ if (!uri.file) return null;
+
+ var fileLower = uri.file.toLowerCase();
+ if (PicRegex.test(fileLower) || VideoRegex.test(fileLower)) return null;
+
+ if (!uri.file.match(/^[A-Za-z0-9]+$/)) return null;
+ return uri.file;
+}
+
+function imgurCandidateUrls(id) {
+ var base = "https://i.imgur.com/" + id;
+ return [base + ".jpg", base + ".png", base + ".gif", base + ".jpeg"];
+}
+
+function imgurHotlinkFallback(img) {
+ try {
+ var candidates = (img.getAttribute("data-imgur-candidates") || "").split("|");
+ if (!candidates.length || !candidates[0]) return;
+
+ var idx = parseInt(img.getAttribute("data-imgur-idx") || "0", 10);
+ var next = idx + 1;
+ if (next >= candidates.length) {
+ var link = img.parentNode;
+ if (link && link.tagName == "A") {
+ var href = link.getAttribute("href") || "";
+ while (link.firstChild) link.removeChild(link.firstChild);
+ link.appendChild(document.createTextNode(href));
+ }
+ return;
+ }
+
+ img.setAttribute("data-imgur-idx", "" + next);
+ img.src = candidates[next];
+ } catch (e) {}
+}
+
function getImagesAsArray(text) {
var imgs = []
var urls = text.match(URLRegex)
if (urls === null) return imgs
for (var i = 0; i<urls.length; i++){
- var url = urls[i]
- var normalized = normalizeUrl(url);
- var urlWithoutParams = normalized.replace(/[?#].*$/i, "");
- if (PicRegex.test(urlWithoutParams))
- imgs.push(normalized)
+ var split = splitTrailingPunctuation(urls[i]);
+ var normalized = normalizeUrl(split.url);
+ var uri = parseUri(normalized);
+
+ // Try to turn common "page" links into direct image URLs (best-effort).
+ var imgurId = imgurIdFromUri(uri);
+ var candidate = imgurId ? imgurCandidateUrls(imgurId)[0] : normalized;
+
+ var urlWithoutParams = candidate.replace(/[?#].*$/i, "");
+ if (PicRegex.test(urlWithoutParams)) imgs.push(candidate)
}
return imgs
}
@@ -2204,25 +2257,43 @@ function imgClickHandler() {
// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/
var LastMsgContainsImage = false
function linkReplace(url) {
- linkUrl = normalizeUrl(url);
+ var split = splitTrailingPunctuation(url);
+ var hrefUrl = normalizeUrl(split.url);
+ var uri = parseUri(hrefUrl);
+
+ // Best-effort support for Imgur page links (e.g. https://imgur.com/<id>).
+ var imgurId = imgurIdFromUri(uri);
+ if (imgurId) {
+ LastMsgContainsImage = true;
+ var candidates = imgurCandidateUrls(imgurId);
+ var first = candidates[0];
+ return "<a target=\"_blank\" href=\"" + hrefUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\">"
+ + "<img src=\"" + first + "\" class=\"unbound imgur-hotlink\" data-imgur-idx=\"0\" data-imgur-candidates=\"" + candidates.join("|") + "\" onerror=\"imgurHotlinkFallback(this)\">"
+ + "</a>" + split.suffix;
+ }
+
+ linkUrl = hrefUrl;
- var uri = parseUri(url)
var type = getUriType(uri)
if (type == 'image') {
LastMsgContainsImage = true;
- return "<a target='_blank' href='" + linkUrl + "' class='img-wrapper' onclick='return imgClickHandler()'><img src='" + linkUrl + "' class='unbound'></a>";
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\"><img src=\"" + linkUrl + "\" class=\"unbound\"></a>" + split.suffix;
+ } else if (type == 'video') {
+ LastMsgContainsImage = true;
+ var videoUrl = linkUrl.replace(/\.gifv([?#].*)?$/i, ".mp4$1");
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"video-wrapper\" onclick=\"return imgClickHandler()\"><video src=\"" + videoUrl + "\" autoplay loop muted controls playsinline></video></a>" + split.suffix;
} else if (type == 'youtube') {
Youtube.startAnimation();
- return "<a target='_blank' class='youtube' href='" + linkUrl + "'>" +
- "<img class='youtube-thumb' width='130' height='97' src='"+Youtube.nextThumbUrl(uri.queryKey.v)+"'>" +
- "<img class='youtube-controls' src='/static/img/youtube.controls.png'></a>"
+ return "<a target=\"_blank\" class=\"youtube\" href=\"" + linkUrl + "\">" +
+ "<img class=\"youtube-thumb\" width=\"130\" height=\"97\" src=\"" + Youtube.nextThumbUrl(uri.queryKey.v) + "\">" +
+ "<img class=\"youtube-controls\" src=\"/static/img/youtube.controls.png\"></a>" + split.suffix;
} else if (type == 'midi') {
- return '<embed src="'+linkUrl+'" autostart="false" loop="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="'+linkUrl+'">'+uri.file+'</a>'
+ return '<embed src="' + linkUrl + '" autostart="false" loop="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
} else if (type == 'wav') {
- return '<audio src="'+linkUrl+'" controls volume="80" width="150" height="20" style="vertical-align:bottom"></audio> <a href="'+linkUrl+'">'+uri.file+'</a>'
+ return '<audio src="' + linkUrl + '" controls volume="80" width="150" height="20" style="vertical-align:bottom"></audio> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
} else
- return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>";
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\">" + split.url + "</a>" + split.suffix;
}
@@ -2230,6 +2301,9 @@ function linkReplace(url) {
function getUriType(uri){
if (PicRegex.test(uri.file.toLowerCase()))
return "image";
+
+ if (VideoRegex.test(uri.file.toLowerCase()))
+ return "video";
var domain = parseDomain(uri.host)
@@ -2443,7 +2517,7 @@ Youtube = {
var img = $(this);
// yt thumb url example: https://i.ytimg.com/vi/0123456789A/1.jpg
var src = img.attr("src") || ""
- var match = src.match(/\\/vi\\/([^/]{11})\\/(\\d)\\.jpg/i)
+ var match = src.match(/\/vi\/([^/]{11})\/(\d)\.jpg/i)
if (!match) return
var v = match[1]
var num = match[2]
diff --git a/static/js/src/text.js b/static/js/src/text.js
index 6fe5c3c..d99efbb 100755
--- a/static/js/src/text.js
+++ b/static/js/src/text.js
@@ -14,21 +14,74 @@ function escapeHtml(txt) {
URLRegex = /((\b(http\:\/\/|https\:\/\/|ftp\:\/\/)|(www\.))+(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
-PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|fid)$/i;
+PicRegex = /\.(jpg|jpeg|png|gif|bmp|svg|webp|fid)$/i;
+VideoRegex = /\.(mp4|webm|mov|m4v|gifv)$/i;
RecipRegex = /(^|\s)@\w+/g;
TopicRegex = /(^|\s)#\w+/g;
+function splitTrailingPunctuation(url) {
+ if (!url) return { "url": url, "suffix": "" };
+ // Common punctuation that follows pasted links in chat.
+ var match = url.match(/[)\]\}\.,!\?;:'"]+$/);
+ if (!match) return { "url": url, "suffix": "" };
+ return { "url": url.slice(0, -match[0].length), "suffix": match[0] };
+}
+
+function imgurIdFromUri(uri) {
+ var host = (uri.host || "").toLowerCase();
+ if (!host || !host.match(/(^|\.)imgur\.com$/i)) return null;
+ if (!uri.file) return null;
+
+ var fileLower = uri.file.toLowerCase();
+ if (PicRegex.test(fileLower) || VideoRegex.test(fileLower)) return null;
+
+ if (!uri.file.match(/^[A-Za-z0-9]+$/)) return null;
+ return uri.file;
+}
+
+function imgurCandidateUrls(id) {
+ var base = "https://i.imgur.com/" + id;
+ return [base + ".jpg", base + ".png", base + ".gif", base + ".jpeg"];
+}
+
+function imgurHotlinkFallback(img) {
+ try {
+ var candidates = (img.getAttribute("data-imgur-candidates") || "").split("|");
+ if (!candidates.length || !candidates[0]) return;
+
+ var idx = parseInt(img.getAttribute("data-imgur-idx") || "0", 10);
+ var next = idx + 1;
+ if (next >= candidates.length) {
+ var link = img.parentNode;
+ if (link && link.tagName == "A") {
+ var href = link.getAttribute("href") || "";
+ while (link.firstChild) link.removeChild(link.firstChild);
+ link.appendChild(document.createTextNode(href));
+ }
+ return;
+ }
+
+ img.setAttribute("data-imgur-idx", "" + next);
+ img.src = candidates[next];
+ } catch (e) {}
+}
+
function getImagesAsArray(text) {
var imgs = []
var urls = text.match(URLRegex)
if (urls === null) return imgs
for (var i = 0; i<urls.length; i++){
- var url = urls[i]
- var normalized = normalizeUrl(url);
- var urlWithoutParams = normalized.replace(/[?#].*$/i, "");
- if (PicRegex.test(urlWithoutParams))
- imgs.push(normalized)
+ var split = splitTrailingPunctuation(urls[i]);
+ var normalized = normalizeUrl(split.url);
+ var uri = parseUri(normalized);
+
+ // Try to turn common "page" links into direct image URLs (best-effort).
+ var imgurId = imgurIdFromUri(uri);
+ var candidate = imgurId ? imgurCandidateUrls(imgurId)[0] : normalized;
+
+ var urlWithoutParams = candidate.replace(/[?#].*$/i, "");
+ if (PicRegex.test(urlWithoutParams)) imgs.push(candidate)
}
return imgs
}
@@ -104,25 +157,43 @@ function imgClickHandler() {
// durty hack to use a global to check this... but otherwise i'd have to rewrite the String.replace function? :/
var LastMsgContainsImage = false
function linkReplace(url) {
- linkUrl = normalizeUrl(url);
+ var split = splitTrailingPunctuation(url);
+ var hrefUrl = normalizeUrl(split.url);
+ var uri = parseUri(hrefUrl);
+
+ // Best-effort support for Imgur page links (e.g. https://imgur.com/<id>).
+ var imgurId = imgurIdFromUri(uri);
+ if (imgurId) {
+ LastMsgContainsImage = true;
+ var candidates = imgurCandidateUrls(imgurId);
+ var first = candidates[0];
+ return "<a target=\"_blank\" href=\"" + hrefUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\">"
+ + "<img src=\"" + first + "\" class=\"unbound imgur-hotlink\" data-imgur-idx=\"0\" data-imgur-candidates=\"" + candidates.join("|") + "\" onerror=\"imgurHotlinkFallback(this)\">"
+ + "</a>" + split.suffix;
+ }
+
+ linkUrl = hrefUrl;
- var uri = parseUri(url)
var type = getUriType(uri)
if (type == 'image') {
LastMsgContainsImage = true;
- return "<a target='_blank' href='" + linkUrl + "' class='img-wrapper' onclick='return imgClickHandler()'><img src='" + linkUrl + "' class='unbound'></a>";
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"img-wrapper\" onclick=\"return imgClickHandler()\"><img src=\"" + linkUrl + "\" class=\"unbound\"></a>" + split.suffix;
+ } else if (type == 'video') {
+ LastMsgContainsImage = true;
+ var videoUrl = linkUrl.replace(/\.gifv([?#].*)?$/i, ".mp4$1");
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\" class=\"video-wrapper\" onclick=\"return imgClickHandler()\"><video src=\"" + videoUrl + "\" autoplay loop muted controls playsinline></video></a>" + split.suffix;
} else if (type == 'youtube') {
Youtube.startAnimation();
- return "<a target='_blank' class='youtube' href='" + linkUrl + "'>" +
- "<img class='youtube-thumb' width='130' height='97' src='"+Youtube.nextThumbUrl(uri.queryKey.v)+"'>" +
- "<img class='youtube-controls' src='/static/img/youtube.controls.png'></a>"
+ return "<a target=\"_blank\" class=\"youtube\" href=\"" + linkUrl + "\">" +
+ "<img class=\"youtube-thumb\" width=\"130\" height=\"97\" src=\"" + Youtube.nextThumbUrl(uri.queryKey.v) + "\">" +
+ "<img class=\"youtube-controls\" src=\"/static/img/youtube.controls.png\"></a>" + split.suffix;
} else if (type == 'midi') {
- return '<embed src="'+linkUrl+'" autostart="false" loop="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="'+linkUrl+'">'+uri.file+'</a>'
+ return '<embed src="' + linkUrl + '" autostart="false" loop="false" volume="80" width="150" height="20" style="vertical-align:bottom"> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
} else if (type == 'wav') {
- return '<audio src="'+linkUrl+'" controls volume="80" width="150" height="20" style="vertical-align:bottom"></audio> <a href="'+linkUrl+'">'+uri.file+'</a>'
+ return '<audio src="' + linkUrl + '" controls volume="80" width="150" height="20" style="vertical-align:bottom"></audio> <a href="' + linkUrl + '">' + uri.file + '</a>' + split.suffix;
} else
- return "<a target='_blank' href='" + linkUrl + "'>" + url + "</a>";
+ return "<a target=\"_blank\" href=\"" + linkUrl + "\">" + split.url + "</a>" + split.suffix;
}
@@ -130,6 +201,9 @@ function linkReplace(url) {
function getUriType(uri){
if (PicRegex.test(uri.file.toLowerCase()))
return "image";
+
+ if (VideoRegex.test(uri.file.toLowerCase()))
+ return "video";
var domain = parseDomain(uri.host)
diff --git a/static/js/src/youtube.js b/static/js/src/youtube.js
index 2b6a977..ccb2cd4 100755
--- a/static/js/src/youtube.js
+++ b/static/js/src/youtube.js
@@ -19,7 +19,7 @@ Youtube = {
var img = $(this);
// yt thumb url example: https://i.ytimg.com/vi/0123456789A/1.jpg
var src = img.attr("src") || ""
- var match = src.match(/\\/vi\\/([^/]{11})\\/(\\d)\\.jpg/i)
+ var match = src.match(/\/vi\/([^/]{11})\/(\d)\.jpg/i)
if (!match) return
var v = match[1]
var num = match[2]