diff options
Diffstat (limited to 'static')
| -rwxr-xr-x | static/css/dump.css | 57 | ||||
| -rwxr-xr-x | static/css/dumpnewuser.css | 3 | ||||
| -rwxr-xr-x | static/css/front.css | 62 | ||||
| -rw-r--r-- | static/css/frontpage_legacy.css | 5 | ||||
| -rw-r--r-- | static/css/frontpage_profile.css | 3 | ||||
| -rw-r--r-- | static/css/meme_pages.css | 6 | ||||
| -rwxr-xr-x | static/css/mgmt.css | 12 | ||||
| -rw-r--r-- | static/css/newlog.css | 1 | ||||
| -rw-r--r-- | static/css/req_reset.css | 1 | ||||
| -rw-r--r-- | static/css/rooms_vortex.css | 32 | ||||
| -rw-r--r-- | static/css/simplerlog.css | 2 | ||||
| -rw-r--r-- | static/js/pages/chat_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 |
16 files changed, 445 insertions, 68 deletions
diff --git a/static/css/dump.css b/static/css/dump.css index f8cfbe2..c7d2217 100755 --- a/static/css/dump.css +++ b/static/css/dump.css @@ -106,7 +106,7 @@ a.img_rolldis:hover { } #dumplist { - top: 0px:padding:0px; + padding: 0px; font-size: 88%; text-transform: uppercase; text-decoration: none; @@ -1031,7 +1031,6 @@ a:active { #binfo { font-family: Arial, Helvetica, sans-serif; font-size: 11px; - bottom } #preload { @@ -1044,6 +1043,51 @@ a:active { display: none !important; } +.is-hidden { + display: none; +} + +.is-relative { + position: relative; +} + +.loading-bar { + position: absolute; + left: 50%; + top: 50%; +} +.loading-bar-label { + position: absolute; + font-family: arial; + font-size: 10px; + color: #ccc; + background: #000; + left: -50px; + top: -18px; + padding: 1px; +} +.loading-bar-track { + position: absolute; + left: -50px; + top: -5px; + font-size: 1px; + width: 100px; + height: 10px; + background: #333; +} +.loading-bar-fill { + position: absolute; + font-size: 1px; + width: 0px; + height: 10px; + background: #eee; +} + +.embed-frame { + border: 7px inset #ccc; + margin: 0; +} + /* profile shit */ @@ -2061,6 +2105,10 @@ a.msgbtn:active { #fblike { margin-top: 6px; margin-left: 6px; + border: none; + overflow: hidden; + width: 100px; + height: 21px; } #logo { float: left; @@ -2512,6 +2560,11 @@ a#disregister:hover { -webkit-background-clip: text; } +.rainbow-link { + border-bottom: 1px solid rgba(0, 255, 255, 0.3); + font-weight: bold; +} + .nick_aids_enoch a:link, .nick_aids_enoch .content, .nick_aids_enoch .msg-hover { color: transparent; text-shadow: 0 0 5px #857E96; } .nick_aids_enoch { opacity: 0.3; } .nick_aids_enoch img { -webkit-filter: blur(4px); } diff --git a/static/css/dumpnewuser.css b/static/css/dumpnewuser.css index 1c92e62..9a6ff6c 100755 --- a/static/css/dumpnewuser.css +++ b/static/css/dumpnewuser.css @@ -498,7 +498,6 @@ left:0; #binfo { font-family: Arial, Helvetica, sans-serif; font-size: 11px; - bottom } #preload { @@ -507,4 +506,4 @@ left:0; top: 0px; } -.invisible { display: none !important; }
\ No newline at end of file +.invisible { display: none !important; } diff --git a/static/css/front.css b/static/css/front.css index ef0ee70..39e7700 100755 --- a/static/css/front.css +++ b/static/css/front.css @@ -119,6 +119,68 @@ filter: progid:DXImageTransform.Microsoft.dropShadow(color=#ccc, offX=3, offY=4, #signin_menu p a { color:#27B!important; } + +.is-relative { + position: relative; +} +.is-hidden { + display: none; +} + +.loading-bar { + position: absolute; + left: 50%; + top: 50%; +} +.loading-bar-label { + position: absolute; + font-family: arial; + font-size: 10px; + color: #ccc; + background: #000; + left: -50px; + top: -18px; + padding: 1px; +} +.loading-bar-track { + position: absolute; + left: -50px; + top: -5px; + font-size: 1px; + width: 100px; + height: 10px; + background: #333; +} +.loading-bar-fill { + position: absolute; + font-size: 1px; + width: 0px; + height: 10px; + background: #eee; +} + +#fblike { + border: none; + overflow: hidden; + width: 100px; + height: 21px; +} +.fullscreen-thumb { + overflow: hidden; + width: 240px; + height: 200px; + border: 0px; +} +.fullscreen-bg { + overflow: hidden; + width: 100%; + height: 100%; + border: 0px; + z-index: -1; + position: fixed; + top: 0px; + left: 0px; +} #signin-submit { display:inline-block; width:90px; diff --git a/static/css/frontpage_legacy.css b/static/css/frontpage_legacy.css index c667daa..21c0f94 100644 --- a/static/css/frontpage_legacy.css +++ b/static/css/frontpage_legacy.css @@ -17,7 +17,7 @@ img{border:0px;} #posts a{text-decoration:none; font-family:Arial, Helvetica, sans-serif;letter-spacing:0px;color:#f0e;} #posts a:hover{color:#333;text-decoration:none;} #sideinfo{ color:#333; padding:12px;margin:7px;font-size:47px;float:left;min-height:210px;width:240px;border:1px inset #f1f1f1; border-radius:3px;-webkit-border-radius:3px;-moz-border-radius: 3px;background-color:white;font-family: 'HelveticaNeue-Light','Helvetica Neue Light','Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:bold; line-height:55px;} -.alttxt{font-size:18px;line-height:20px;} +.alttxt{font-size:18px;line-height:20px;text-transform:uppercase;text-align:left;} .logged-dump{border:1px inset #f1f1f1; border-radius:3px;-webkit-border-radius:3px;-moz-border-radius: 3px;font-family:Arial, Helvetica, sans-serif;float:left;max-width:250px;min-width:230px;padding:7px;margin:7px;background-color:white} .logged-dump img{max-width:240px;max-height:240px;padding-top:3px; } .buttons img{display:none;z-index:4;float:left;padding-top:5px;cursor:pointer;} @@ -42,7 +42,7 @@ z-index:4; #pgbuttons{position:fixed;bottom:6px;right:0px;/*border-top:1px ridge #333;*/min-width:70px;padding:5px;} #pgbuttons a{text-decoration:none;} #footer{word-spacing:20px;font-size:11px;font-family: Monaco, "Courier New", Courier, monospace;text-align:center;margin:7px;color:#333;width:900px;display:block;height:18px;overflow:hidden;} -#footer a{text-decoration:none;color:#333;} +#footer a{text-decoration:none;color:#333;word-spacing:normal;} #footer a:hover{color:#000;} #pgbuttons input{color:#eee;letter-spacing:1px; text-decoration:none; @@ -64,6 +64,7 @@ z-index:4; .hallscore{margin-right:-20px;color:#fff;text-align:center;margin-top:-20px;float:right;font-size:40px;z-index:1;padding:6px;background-image:url('/static/img/hallheart.png');background-position:top left;font-style: oblique;font-weight:100;height:50px;width:50px;text-shadow:-1px 1px 0px #000;line-height:37px;text-indent:4px;} body.logged-in .hallscore{cursor:pointer;} .hallnick{font-size:16px;} +#hallnick iframe{overflow:hidden;width:240px;height:200px;border:0px;} a#disregister { background: #FCF0AD; color: #fe1409; diff --git a/static/css/frontpage_profile.css b/static/css/frontpage_profile.css index 247160e..2267600 100644 --- a/static/css/frontpage_profile.css +++ b/static/css/frontpage_profile.css @@ -25,7 +25,7 @@ img{border:0px;} #pgbuttons{position:fixed;bottom:6px;right:0px;/*border-top:1px ridge #333;*/min-width:70px;padding:5px;} #pgbuttons a{text-decoration:none;} #footer{word-spacing:20px;font-size:11px;font-family: Monaco, "Courier New", Courier, monospace;text-align:center;margin:7px;color:#333;width:900px;display:block;height:18px;overflow:hidden;} -#footer a{text-decoration:none;color:#333;} +#footer a{text-decoration:none;color:#333;word-spacing:normal;} #footer a:hover{color:#000;} #pgbuttons input{color:#eee;letter-spacing:1px; text-decoration:none; @@ -47,6 +47,7 @@ img{border:0px;} #hallscore{right:-11px;color:#fff;text-align:center;top:-13px;position:absolute;font-size:40px;z-index:1;padding:6px;background-image:url('/static/img/hallheart.png');background-position:top left;font-family: 'HelveticaNeue-Light','Helvetica Neue Light','Helvetica Neue',Arial,Helvetica,sans-serif;font-weight:100;height:50px;width:50px; text-shadow:-1px 1px 0px #000;line-height:37px;text-indent:5px; } #hallnick{font-size:16px;} +#hallnick iframe{overflow:hidden;width:240px;height:200px;border:0px;} a#disregister { background: #FCF0AD; color: #fe1409; diff --git a/static/css/meme_pages.css b/static/css/meme_pages.css index c53d420..22d5a2a 100644 --- a/static/css/meme_pages.css +++ b/static/css/meme_pages.css @@ -10,6 +10,12 @@ body { overflow: auto; } +.preload-img { + visibility: hidden; + width: 0px; + height: 0px; +} + #rapper { background-color: #f2f9fe; width: 800px; diff --git a/static/css/mgmt.css b/static/css/mgmt.css index 2ff6d8e..4360ad3 100755 --- a/static/css/mgmt.css +++ b/static/css/mgmt.css @@ -259,10 +259,10 @@ position:absolute; top:21px;right:0px; } #dumplist{ -top:0px:width:100%;padding:0px;background-color:#fff; -color:#000;font-size:60%;text-transform:uppercase;text-decoration: none; -border-bottom:1px solid #999;text-align:left;text-indent:350px; -padding-right:44px;line-height:1.1;background-color:#eff5fb;overflow:hidden;position:fixed;height:10px;width:100%; +top:0px;width:100%;padding:0px;background-color:#fff; + color:#000;font-size:60%;text-transform:uppercase;text-decoration: none; + border-bottom:1px solid #999;text-align:left;text-indent:350px; + padding-right:44px;line-height:1.1;background-color:#eff5fb;overflow:hidden;position:fixed;height:10px;width:100%; /*margin-left:40%; border-bottom-left-radius:5px; -webkit-border-bottom-left-radius:5px; @@ -291,6 +291,7 @@ display:none; .no-cursor { cursor: none; } .invisible { display: none !important; } +.preload-img { visibility: hidden; width: 0px; height: 0px; } #cursor-big { position: absolute; z-index: 1000; } .thumb { @@ -1186,7 +1187,6 @@ left:0; #binfo { font-family: Arial, Helvetica, sans-serif; font-size: 11px; - bottom } #preload { @@ -1682,5 +1682,3 @@ top:50px; cursor: pointer; display: none; } - - diff --git a/static/css/newlog.css b/static/css/newlog.css index f0e0277..8cc8032 100644 --- a/static/css/newlog.css +++ b/static/css/newlog.css @@ -206,6 +206,7 @@ img { #footer a { text-decoration: none; color: #333; + word-spacing: normal; } #footer a:hover { diff --git a/static/css/req_reset.css b/static/css/req_reset.css index 0807ec7..65d1c3c 100644 --- a/static/css/req_reset.css +++ b/static/css/req_reset.css @@ -1,3 +1,4 @@ #main { padding: 10px 2em 0px 2em; } label { float: left; width: 150px; } .error { border: 1px solid red; } +#feedback { color: red; margin-bottom: 2em; } diff --git a/static/css/rooms_vortex.css b/static/css/rooms_vortex.css index 7403454..f13c797 100644 --- a/static/css/rooms_vortex.css +++ b/static/css/rooms_vortex.css @@ -62,3 +62,35 @@ text-decoration:none; font-family: Arial, Helvetica, sans-serif; color:blue; } + +.loading-bar { + position: absolute; + left: 50%; + top: 50%; +} +.loading-bar-label { + position: absolute; + font-family: arial; + font-size: 10px; + color: #ccc; + background: #000; + left: -50px; + top: -18px; + padding: 1px; +} +.loading-bar-track { + position: absolute; + left: -50px; + top: -5px; + font-size: 1px; + width: 100px; + height: 10px; + background: #333; +} +.loading-bar-fill { + position: absolute; + font-size: 1px; + width: 0px; + height: 10px; + background: #eee; +} diff --git a/static/css/simplerlog.css b/static/css/simplerlog.css index 784253d..a158fd8 100644 --- a/static/css/simplerlog.css +++ b/static/css/simplerlog.css @@ -202,6 +202,7 @@ img { #footer a { text-decoration: none; color: #333; + word-spacing: normal; } #footer a:hover { @@ -301,6 +302,7 @@ a#disregister:hover { text-align: center; left: 0; z-index: 1000; + background-color: white; } #inpage-search-results #search-results-images { 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/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] |
