diff options
Diffstat (limited to 'static/js')
| -rw-r--r-- | static/js/pages/chat_init.js | 13 | ||||
| -rw-r--r-- | static/js/pages/directory_init.js | 11 | ||||
| -rw-r--r-- | static/js/pages/expandable_login_init.js | 11 | ||||
| -rw-r--r-- | static/js/pages/frontpage_refresh.js | 11 | ||||
| -rw-r--r-- | static/js/pages/fullscreen_front_init.js | 18 | ||||
| -rw-r--r-- | static/js/pages/fullscreen_front_run.js | 7 | ||||
| -rw-r--r-- | static/js/pages/fullscreen_init.js | 55 | ||||
| -rw-r--r-- | static/js/pages/log_favs_init.js | 7 | ||||
| -rw-r--r-- | static/js/pages/log_init.js | 58 | ||||
| -rw-r--r-- | static/js/pages/scroll_pane_init.js | 13 | ||||
| -rw-r--r-- | static/js/pages/search_files_init.js | 13 | ||||
| -rwxr-xr-x | static/js/pichat.butt.js | 104 | ||||
| -rwxr-xr-x | static/js/pichat.js | 106 | ||||
| -rwxr-xr-x | static/js/src/text.js | 104 | ||||
| -rwxr-xr-x | static/js/src/youtube.js | 2 |
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] |
