diff options
Diffstat (limited to 'juick-server/src/main/assets')
-rw-r--r-- | juick-server/src/main/assets/embed.js | 336 | ||||
-rw-r--r-- | juick-server/src/main/assets/logo.png | bin | 0 -> 2447 bytes | |||
-rw-r--r-- | juick-server/src/main/assets/logo@2x.png | bin | 0 -> 4822 bytes | |||
-rw-r--r-- | juick-server/src/main/assets/scripts.js | 805 | ||||
-rw-r--r-- | juick-server/src/main/assets/style.css | 952 |
5 files changed, 2093 insertions, 0 deletions
diff --git a/juick-server/src/main/assets/embed.js b/juick-server/src/main/assets/embed.js new file mode 100644 index 00000000..25c37142 --- /dev/null +++ b/juick-server/src/main/assets/embed.js @@ -0,0 +1,336 @@ + +function insertAfter(newNode, referenceNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); +} + +function setContent(containerNode, ...newNodes) { + removeAllFrom(containerNode); + newNodes.forEach(n => containerNode.appendChild(n)); + return containerNode; +} + +function removeAllFrom(fromNode) { + for (let c; c = fromNode.lastChild; ) { fromNode.removeChild(c); } +} + +function htmlEscape(html) { + let textarea = document.createElement('textarea'); + textarea.textContent = html; + return textarea.innerHTML; +} + +// rules :: [{pr: number, re: RegExp, with: string}] +// rules :: [{pr: number, re: RegExp, with: Function}] +// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string]}] +// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string, Function]}] +function formatText(txt, rules) { + let idCounter = 0; + function nextId() { return idCounter++; } + function ft(txt, rules) { + let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; }) + .filter(([,m]) => m !== null) + .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index)); + if (matches && matches.length > 0) { + let [rule, match] = matches[0]; + let subsequentRules = rules.filter(r => r.pr >= rule.pr); + let idStr = `<>(${nextId()})<>`; + let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex); + let innerStr = (rule.brackets) + ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })() + : match[0].replace(rule.re, rule.with); + return ft(outerStr, subsequentRules).replace(idStr, innerStr); + } + return txt; + } + return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped +} + +function fixWwwLink(url) { + return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//'); +} + +function makeNewNode(embedType, aNode, reResult) { + const withClasses = el => { + if (embedType.className) { + el.classList.add(...embedType.className.split(' ')); + } + return el; + }; + return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div'))); +} + +function makeIframe(src, w, h, scrolling='no') { + let iframe = document.createElement('iframe'); + iframe.style.width = w; + iframe.style.height = h; + iframe.frameBorder = 0; + iframe.scrolling = scrolling; + iframe.setAttribute('allowFullScreen', ''); + iframe.src = src; + iframe.innerHTML = 'Cannot show iframes.'; + return iframe; +} + +function makeResizableToRatio(element, ratio) { + element.dataset['ratio'] = ratio; + makeResizable(element, w => w * element.dataset['ratio']); +} + +// calcHeight :: Number -> Number -- calculate element height for a given width +function makeResizable(element, calcHeight) { + const setHeight = el => { + if (document.body.contains(el) && (el.offsetWidth > 0)) { + el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px'; + } + }; + window.addEventListener('resize', () => setHeight(element)); + setHeight(element); +} + +function extractDomain(url) { + const domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i; + return domainRe.exec(url)[1]; +} + +function urlReplace(match, p1, p2, p3) { + let isBrackets = (p1 !== undefined); + return (isBrackets) + ? `<a href="${fixWwwLink(p2 || p3)}">${p1}</a>` + : `<a href="${fixWwwLink(match)}">${extractDomain(match)}</a>`; +} + +function urlReplaceInCode(match, p1, p2, p3) { + let isBrackets = (p1 !== undefined); + return (isBrackets) + ? `<a href="${fixWwwLink(p2 || p3)}">${match}</a>` + : `<a href="${fixWwwLink(match)}">${match}</a>`; +} + +function messageReplyReplace(messageId) { + return function(match, mid, rid) { + let replyPart = (rid && rid != '0') ? '#' + rid : ''; + return `<a href="/${mid || messageId}${replyPart}">${match}</a>`; + }; +} + +/** + * Given "txt" message in unescaped plaintext with Juick markup, this function + * returns escaped formatted HTML string. + * + * @param {string} txt + * @param {string} messageId - current message id + * @param {boolean} isCode + * @returns {string} + */ +function juickFormat(txt, messageId, isCode) { + const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi; + const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, ''); + return (isCode) + ? formatText(txt, [ + { pr: 1, re: urlRe, with: urlReplaceInCode }, + { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) }, + { pr: 1, re: /\B@([\w-]+)\b/gi, with: '<a href="/$1">@$1</a>' }, + ]) + : formatText(txt, [ + { pr: 0, re: /((?:^(?:>|>)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['<q>', '</q>', bqReplace] }, + { pr: 1, re: urlRe, with: urlReplace }, + { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) }, + { pr: 1, re: /\B@([\w-]+)\b/gi, with: '<a href="/$1">@$1</a>' }, + { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<b>', '</b>'] }, + { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<i>', '</i>'] }, + { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<span class="u">', '</span>'] }, + { pr: 3, re: /\n/g, with: '<br/>' }, + ]); +} + +function getEmbeddableLinkTypes() { + return [ + { + name: 'Jpeg and png images', + id: 'embed_jpeg_and_png_images', + className: 'picture compact', + ctsDefault: false, + re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = `<a href="${aNode.href}"><img src="${aNode.href}"></a>`; + return div; + } + }, + { + name: 'Gif images', + id: 'embed_gif_images', + className: 'picture compact', + ctsDefault: true, + re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = `<a href="${aNode.href}"><img src="${aNode.href}"></a>`; + return div; + } + }, + { + name: 'Video (webm, mp4, ogv)', + id: 'embed_webm_and_mp4_videos', + className: 'video compact', + ctsDefault: false, + re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = `<video src="${aNode.href}" title="${aNode.href}" controls></video>`; + return div; + } + }, + { + name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)', + id: 'embed_sound_files', + className: 'audio singleColumn', + ctsDefault: false, + re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = `<audio src="${aNode.href}" title="${aNode.href}" controls></audio>`; + return div; + } + }, + { + name: 'YouTube videos (and playlists)', + id: 'embed_youtube_videos', + className: 'youtube resizableV singleColumn', + ctsDefault: false, + re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i, + makeNode: function(aNode, reResult, div) { + let [url, v, args, plist] = reResult; + let iframeUrl; + if (plist) { + iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist; + } else { + let pp = {}; args.replace(/^\?/, '') + .split('&') + .map(s => s.split('=')) + .forEach(z => pp[z[0]] = z[1]); + let embedArgs = { rel: '0' }; + if (pp.t) { + const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i; + let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t); + embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0))); + } + if (pp.list) { + embedArgs['list'] = pp.list; + } + v = v || pp.v; + let argsStr = Object.keys(embedArgs) + .map(k => `${k}=${embedArgs[k]}`) + .join('&'); + iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`; + } + let iframe = makeIframe(iframeUrl, '100%', '360px'); + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0); + return setContent(div, iframe); + } + }, + { + name: 'Vimeo videos', + id: 'embed_vimeo_videos', + className: 'vimeo resizableV', + ctsDefault: false, + re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i, + makeNode: function(aNode, reResult, div) { + let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px'); + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0); + return setContent(div, iframe); + } + } + ]; +} + +function embedLink(aNode, linkTypes, container, afterNode) { + let anyEmbed = false; + let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi,'')); + let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice + if (sameEmbed === null) { + anyEmbed = [].some.call(linkTypes, function(linkType) { + let reResult = linkType.re.exec(aNode.href); + if (reResult) { + if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; } + let newNode = makeNewNode(linkType, aNode, reResult); + if (!newNode) { return false; } + newNode.setAttribute('data-linkid', linkId); + if (afterNode) { + insertAfter(newNode, afterNode); + } else { + container.appendChild(newNode); + } + aNode.classList.add('embedLink'); + return true; + } + }); + } + return anyEmbed; +} + +function embedLinks(aNodes, container) { + let anyEmbed = false; + let embeddableLinkTypes = getEmbeddableLinkTypes(); + Array.from(aNodes).forEach(aNode => { + let isEmbedded = embedLink(aNode, embeddableLinkTypes, container); + anyEmbed = anyEmbed || isEmbedded; + }); + return anyEmbed; +} + +/** + * Embed all the links inside element "x" that match to "allLinksSelector". + * All the embedded media is placed inside "div.embedContainer". + * "div.embedContainer" is inserted before an element matched by "beforeNodeSelector" + * if not present. Existing container is used otherwise. + * + * @param {Element} x + * @param {string} beforeNodeSelector + * @param {string} allLinksSelector + */ +function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { + let isCtsPost = false; + let allLinks = x.querySelectorAll(allLinksSelector); + + let existingContainer = x.querySelector('div.embedContainer'); + if (existingContainer) { + embedLinks(allLinks, existingContainer); + } else { + let embedContainer = document.createElement('div'); + embedContainer.className = 'embedContainer'; + + let anyEmbed = embedLinks(allLinks, embedContainer); + if (anyEmbed) { + let beforeNode = x.querySelector(beforeNodeSelector); + x.insertBefore(embedContainer, beforeNode); + } + } +} + +function embedLinksToArticles() { + let beforeNodeSelector = 'nav.l'; + let allLinksSelector = 'p:not(.ir) a, pre a'; + Array.from(document.querySelectorAll('#content article')).forEach(article => { + embedLinksToX(article, beforeNodeSelector, allLinksSelector); + }); +} + +function embedLinksToPost() { + let beforeNodeSelector = '.msg-txt + *'; + let allLinksSelector = '.msg-txt a'; + Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => { + embedLinksToX(msg, beforeNodeSelector, allLinksSelector); + }); +} + +/** + * Embed all the links in all messages/replies on the page. + */ +function embedAll() { + if (document.querySelector('#content article[data-mid]')) { + embedLinksToArticles(); + } else { + embedLinksToPost(); + } +} + +exports.embedAll = embedAll; +exports.embedLinksToX = embedLinksToX; +exports.format = juickFormat; diff --git a/juick-server/src/main/assets/logo.png b/juick-server/src/main/assets/logo.png Binary files differnew file mode 100644 index 00000000..4e0f6d56 --- /dev/null +++ b/juick-server/src/main/assets/logo.png diff --git a/juick-server/src/main/assets/logo@2x.png b/juick-server/src/main/assets/logo@2x.png Binary files differnew file mode 100644 index 00000000..6febeaf9 --- /dev/null +++ b/juick-server/src/main/assets/logo@2x.png diff --git a/juick-server/src/main/assets/scripts.js b/juick-server/src/main/assets/scripts.js new file mode 100644 index 00000000..54e7958e --- /dev/null +++ b/juick-server/src/main/assets/scripts.js @@ -0,0 +1,805 @@ +require('whatwg-fetch'); +require('element-closest'); +require('classlist.js'); +require('url-search-params-polyfill'); +let Awesomplete = require('awesomplete'); +import * as killy from './embed'; + +if (!('remove' in Element.prototype)) { // Firefox <23 + Element.prototype.remove = function () { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; +} + +NodeList.prototype.forEach = Array.prototype.forEach; +HTMLCollection.prototype.forEach = Array.prototype.forEach; + +NodeList.prototype.filter = Array.prototype.filter; +HTMLCollection.prototype.filter = Array.prototype.filter; + +Element.prototype.selectText = function () { + let d = document; + if (d.body.createTextRange) { + let range = d.body.createTextRange(); + range.moveToElementText(this); + range.select(); + } else if (window.getSelection) { + let selection = window.getSelection(); + let rangeSel = d.createRange(); + rangeSel.selectNodeContents(this); + selection.removeAllRanges(); + selection.addRange(rangeSel); + } +}; + +function autosize(el) { + let offset = (!window.opera) + ? (el.offsetHeight - el.clientHeight) + : (el.offsetHeight + parseInt(window.getComputedStyle(el, null).getPropertyValue('border-top-width'))); + + let resize = function (el) { + el.style.height = 'auto'; + el.style.height = (el.scrollHeight + offset) + 'px'; + }; + + if (el.addEventListener) { + el.addEventListener('input', () => resize(el)); + } else if (el.attachEvent) { + el.attachEvent('onkeyup', () => resize(el)); + } +} + +function evilIcon(name) { + return `<div class="icon icon--${name}"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#${name}-icon"></use></svg></div>`; +} + +/* eslint-disable only-ascii/only-ascii */ +const translations = { + 'en': { + 'message.inReplyTo': 'in reply to', + 'message.reply': 'Reply', + 'message.likeThisMessage?': 'Recommend this message?', + 'postForm.pleaseInputMessageText': 'Please input message text', + 'postForm.upload': 'Upload', + 'postForm.newMessage': 'New message...', + 'postForm.imageLink': 'Link to image', + 'postForm.imageFormats': 'JPG/PNG, up to 10 MB', + 'postForm.or': 'or', + 'postForm.tags': 'Tags (space separated)', + 'postForm.submit': 'Send', + 'comment.writeComment': 'Write a comment...', + 'shareDialog.linkToMessage': 'Link to message', + 'shareDialog.messageNumber': 'Message number', + 'shareDialog.share': 'Share', + 'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself', + 'loginDialog.registeredAlready': 'Registered already?', + 'loginDialog.username': 'Username', + 'loginDialog.password': 'Password', + 'loginDialog.facebook': 'Login with Facebook', + 'loginDialog.vk': 'Login with VK', + 'loginDialog.email': 'Registration', + 'error.error': 'Error' + }, + 'ru': { + 'message.inReplyTo': 'в ответ на', + 'message.reply': 'Ответить', + 'message.likeThisMessage?': 'Рекомендовать это сообщение?', + 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения', + 'postForm.upload': 'загрузить', + 'postForm.newMessage': 'Новое сообщение...', + 'postForm.imageLink': 'Ссылка на изображение', + 'postForm.imageFormats': 'JPG/PNG, до 10Мб', + 'postForm.or': 'или', + 'postForm.tags': 'Теги (через пробел)', + 'postForm.submit': 'Отправить', + 'comment.writeComment': 'Написать комментарий...', + 'shareDialog.linkToMessage': 'Ссылка на сообщение', + 'shareDialog.messageNumber': 'Номер сообщения', + 'shareDialog.share': 'Поделиться', + 'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь', + 'loginDialog.registeredAlready': 'Уже зарегистрированы?', + 'loginDialog.username': 'Имя пользователя', + 'loginDialog.password': 'Пароль', + 'loginDialog.facebook': 'Войти через Facebook', + 'loginDialog.vk': 'Войти через ВКонтакте', + 'loginDialog.email': 'Регистрация', + 'error.error': 'Ошибка' + } +}; +/* eslint-enable only-ascii/only-ascii */ + +function getLang() { + return (window.navigator.languages && window.navigator.languages[0]) + || window.navigator.userLanguage + || window.navigator.language; +} +function i18n(key, lang = undefined) { + const fallbackLang = 'ru'; + lang = lang || getLang().split('-')[0]; + return (translations[lang] && translations[lang][key]) + || translations[fallbackLang][key] + || key; +} + +var ws, + pageTitle; + +function initWS() { + let url = (window.location.protocol === 'https:' ? 'wss' : 'ws') + ':' + + '//api.juick.com/ws/'; + let hash = document.getElementById('body').getAttribute('data-hash'); + if (hash) { + url += '?hash=' + hash; + } else { + let content = document.getElementById('content'); + if (content) { + let pageMID = content.getAttribute('data-mid'); + if (pageMID) { + url += pageMID; + } + } + } + + ws = new WebSocket(url); + ws.onopen = function () { + console.log('online'); + if (!document.querySelector('#wsthread')) { + var d = document.createElement('div'); + d.id = 'wsthread'; + d.addEventListener('click', nextReply); + document.querySelector('body').appendChild(d); + pageTitle = document.title; + } + }; + ws.onclose = function () { + console.log('offline'); + ws = false; + setTimeout(function () { + initWS(); + }, 2000); + }; + ws.onmessage = function (msg) { + if (msg.data == ' ') { + ws.send(' '); + } else { + try { + var jsonMsg = JSON.parse(msg.data); + console.log('data: ' + msg.data); + if (jsonMsg.service) { + return; + } + wsIncomingReply(jsonMsg); + } catch (err) { + console.log(err); + } + } + }; + setInterval(wsSendKeepAlive, 90000); +} + +function wsSendKeepAlive() { + if (ws) { + ws.send(' '); + } +} + +function wsShutdown() { + if (ws) { + ws.onclose = function () { }; + ws.close(); + } +} + +function wsIncomingReply(msg) { + let content = document.getElementById('content'); + if (!content) { return; } + let pageMID = content.getAttribute('data-mid'); + if (!pageMID || pageMID != msg.mid) { return; } + let msgNum = '/' + msg.rid; + if (msg.replyto > 0) { + msgNum += ` ${i18n('message.inReplyTo')} <a href="#${msg.replyto}">/${msg.replyto}</a>`; + } + let photoDiv = (msg.attach == null) ? '' : ` + <div class="msg-media"><a href="//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}"> + <img src="//i.juick.com/photos-512/${msg.mid}-${msg.rid}.${msg.attach}"/></a> + </div>`; + let msgContHtml = ` + <div class="msg-cont"> + <div class="msg-header"> + <a href="/${msg.user.uname}/">${msg.user.uname}</a>: + <div class="msg-avatar"> + <a href="/${msg.user.uname}/"><img src="//i.juick.com/a/${msg.user.uid}.png" alt="${msg.user.uname}"/></a> + </div> + <div class="msg-ts"> + <a href="/m/${msg.mid}#${msg.rid}" title="${msg.timestamp}GMT">${msg.timestamp}</a> + </div> + </div> + <div class="msg-txt">${killy.format(msg.body, msg.mid, false)}</div>${photoDiv} + <div class="msg-links">${msgNum} · <a class="msg-reply-link" href="#">${i18n('message.reply')}</a></div> + <div class="msg-comment-target msg-comment-hidden"></div> + </div>`; + + let li = document.createElement('li'); + li.setAttribute('class', 'msg reply-new'); + li.setAttribute('id', msg.rid); + li.innerHTML = msgContHtml; + li.addEventListener('click', newReply); + li.addEventListener('mouseover', newReply); + li.querySelector('a.msg-reply-link').addEventListener('click', function (e) { + showCommentForm(msg.mid, msg.rid); + e.preventDefault(); + }); + + killy.embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a'); + + document.getElementById('replies').appendChild(li); + + updateRepliesCounter(); +} + +function newReply(e) { + var li = e.target; + li.classList.remove('reply-new'); + li.removeEventListener('click', e); + li.removeEventListener('mouseover', e); + updateRepliesCounter(); +} + +function nextReply() { + var li = document.querySelector('#replies>li.reply-new'); + if (li) { + li.classList.remove('reply-new'); + li.removeEventListener('click', this); + li.children[0].scrollIntoView(); + updateRepliesCounter(); + } +} + +function updateRepliesCounter() { + var replies = document.querySelectorAll('#replies>li.reply-new').length; + var wsthread = document.getElementById('wsthread'); + if (replies) { + wsthread.textContent = replies; + wsthread.style.display = 'block'; + document.title = '[' + replies + '] ' + pageTitle; + } else { + wsthread.style.display = 'none'; + document.title = pageTitle; + } +} + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +function postformListener(formEl, ev) { + if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) { + let form = formEl.closest('form'); + if (!form.onsubmit || form.onsubmit()) { + form.submit(); + } + } +} +function closeDialogListener(ev) { + ev = ev || window.event; + if (ev.keyCode == 27) { + closeDialog(); + } +} + +function newMessage(evt) { + document.querySelectorAll('#newmessage .dialogtxt').forEach(t => { + t.remove(); + }); + if (document.querySelector('#newmessage textarea').value.length == 0 + && document.querySelector('#newmessage .img').value.length == 0 + && !document.querySelector('#newmessage input[type="file"]')) { + document.querySelector('#newmessage').insertAdjacentHTML('afterbegin', `<p class="dialogtxt">${i18n('postForm.pleaseInputMessageText')}</p>`); + evt.preventDefault(); + } +} + +function showCommentForm(mid, rid) { + let reply = document.getElementById(rid); + let formTarget = reply.querySelector('div.msg-cont .msg-comment-target'); + if (formTarget) { + let formHtml = ` + <form action="/comment" method="POST" enctype="multipart/form-data"> + <input type="hidden" name="mid" value="${mid}"> + <input type="hidden" name="rid" value="${rid}"> + <div class="msg-comment"> + <div class="ta-wrapper"> + <textarea name="body" rows="1" class="reply" placeholder="${i18n('comment.writeComment')}"></textarea> + <div class="attach-photo">${evilIcon('ei-camera')}</div> + </div> + <input type="submit" value="OK"> + </div> + </form>`; + formTarget.insertAdjacentHTML('afterend', formHtml); + formTarget.remove(); + + let form = reply.querySelector('form'); + let submitButton = form.querySelector('input[type="submit"]'); + + let attachButton = form.querySelector('.msg-comment .attach-photo'); + attachButton.addEventListener('click', e => attachCommentPhoto(e.target)); + + let textarea = form.querySelector('.msg-comment textarea'); + textarea.addEventListener('keypress', e => postformListener(e.target, e)); + autosize(textarea); + + let validateMessage = () => { + let len = textarea.value.length; + if (len > 4096) { return 'Message is too long'; } + return ''; + }; + form.addEventListener('submit', e => { + let validationResult = validateMessage(); + if (validationResult) { + e.preventDefault(); + alert(validationResult); + return false; + } + submitButton.disabled = true; + }); + } + reply.querySelector('.msg-comment textarea').focus(); +} + +function attachInput() { + let inp = document.createElement('input'); + inp.setAttribute('type', 'file'); + inp.setAttribute('name', 'attach'); + inp.setAttribute('accept', 'image/jpeg,image/png'); + inp.style.visibility = 'hidden'; + return inp; +} + +function attachCommentPhoto(div) { + let input = div.querySelector('input'); + if (input) { + input.remove(); + div.classList.remove('attach-photo-active'); + } else { + let newInput = attachInput(); + newInput.addEventListener('change', function () { + div.classList.add('attach-photo-active'); + }); + newInput.click(); + div.appendChild(newInput); + } +} + +function attachMessagePhoto(div) { + var f = div.closest('form'), + finput = f.querySelector('input[type="file"]'); + if (!finput) { + var inp = attachInput(); + inp.style.float = 'left'; + inp.style.width = 0; + inp.style.height = 0; + inp.addEventListener('change', function () { + div.textContent = i18n('postForm.upload') + ' (✓)'; + }); + f.appendChild(inp); + inp.click(); + } else { + finput.remove(); + div.textContent = i18n('postForm.upload'); + } +} + +function showMessageLinksDialog(mid, rid) { + let hlink = window.location.protocol + '//juick.com/' + mid; + let mlink = '#' + mid; + if (rid > 0) { + hlink += '#' + rid; + mlink += '/' + rid; + } + let hlinkenc = encodeURIComponent(hlink); + let html = ` + <div class="dialogshare"> + ${i18n('shareDialog.linkToMessage')}: <div onclick="this.selectText()" class="dialogl">${hlink}</div> + ${i18n('shareDialog.messageNumber')}: <div onclick="this.selectText()" class="dialogl">${mlink}</div> + ${i18n('shareDialog.share')}: + <ul> + <li><a href="https://www.facebook.com/sharer/sharer.php?u=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-facebook')}</a></li> + <li><a href="https://twitter.com/intent/tweet?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-twitter')}</a></li> + <li><a href="https://vk.com/share.php?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-vk')}</a></li> + </ul> + </div>`; + + openDialog(html); +} + +function showPhotoDialog(fname) { + let width = window.innerWidth; + let height = window.innerHeight; + let minDimension = (width < height) ? width : height; + if (minDimension < 640) { + return true; // no dialog, open the link + } else if (minDimension < 1280) { + openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/photos-1024/${fname}"/></a>`, true); + return false; + } else { + openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/p/${fname}"/></a>`, true); + return false; + } +} + +function openPostDialog() { + let newmessageTemplate = ` + <form id="newmessage" action="/post" method="post" enctype="multipart/form-data"> + <textarea name="body" placeholder="${i18n('postForm.newMessage')}"></textarea> + <div> + <input class="img" name="img" placeholder="${i18n('postForm.imageLink')} (${i18n('postForm.imageFormats')})"/> + ${i18n('postForm.or')} <a href="#">${i18n('postForm.upload')}</a><br/> + <input id="tags_input" class="tags" name="tags" placeholder="${i18n('postForm.tags')}"/><br/> + <input type="submit" class="subm" value="${i18n('postForm.submit')}"/> + </div> + </form> + `; + return openDialog(newmessageTemplate); +} + +function openDialog(html, image) { + var dialogHtml = ` + <div id="dialogt"> + <div id="dialogb"></div> + <div id="dialogw"> + <div id="dialog_header"> + <div id="dialogc">${evilIcon('ei-close')}</div> + </div> + ${html} + </div> + </div>`; + let body = document.querySelector('body'); + body.classList.add('dialog-opened'); + body.insertAdjacentHTML('afterbegin', dialogHtml); + if (image) { + let header = document.querySelector('#dialog_header'); + header.classList.add('header_image'); + } + document.addEventListener('keydown', closeDialogListener); + document.querySelector('#dialogb').addEventListener('click', closeDialog); + document.querySelector('#dialogc').addEventListener('click', closeDialog); +} + +function closeDialog() { + let draft = document.querySelector('#newmessage textarea'); + if (draft) { + window.draft = draft.value; + } + document.querySelector('body').classList.remove('dialog-opened'); + document.querySelector('#dialogb').remove(); + document.querySelector('#dialogt').remove(); +} + +function openSocialWindow(a) { + var w = window.open(a.href, 'juickshare', 'width=640,height=400'); + if (window.focus) { w.focus(); } + return false; +} + +function checkUsername() { + var uname = document.querySelector('#username').textContent, + style = document.querySelector('#username').style; + fetch('//api.juick.com/users?uname=' + uname) + .then(function () { + style.background = '#FFCCCC'; + }) + .catch(function () { + style.background = '#CCFFCC'; + }); +} + +/******************************************************************************/ + +function openDialogLogin() { + let html = ` + <div class="dialoglogin"> + <p>${i18n('loginDialog.pleaseIntroduceYourself')}:</p> + <a href="mailto:juick@juick.com?subject=LOGIN" id="signemail">${evilIcon('ei-envelope')}${i18n('loginDialog.email')}</a> + <a href="/_fblogin" id="signfb">${evilIcon('ei-sc-facebook')}${i18n('loginDialog.facebook')}</a> + <a href="/_vklogin" id="signvk">${evilIcon('ei-sc-vk')}${i18n('loginDialog.vk')}</a> + <p>${i18n('loginDialog.registeredAlready')}</p> + <form action="/login" method="POST"> + <input class="signinput" type="text" name="username" placeholder="${i18n('loginDialog.username')}"/><br/> + <input class="signinput" type="password" name="password" placeholder="${i18n('loginDialog.password')}"/><br/> + <input class="signsubmit" type="submit" value="OK"/> + </form> + </div>`; + openDialog(html); + return false; +} + +/******************************************************************************/ + +function resultMessage(str) { + var result = document.createElement('p'); + result.textContent = str; + return result; +} + +function likeMessage(e, mid) { + if (confirm(i18n('message.likeThisMessage?'))) { + fetch('//api.juick.com/like?mid=' + mid + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + method: 'POST', + credentials: 'same-origin' + }) + .then(function (response) { + if (response.ok) { + e.closest('article').appendChild(resultMessage('OK!')); + } + }) + .catch(function () { + e.closest('article').appendChild(resultMessage(i18n('error.error'))); + }); + } + return false; +} + +/******************************************************************************/ + +function setPopular(e, mid, popular) { + fetch('//api.juick.com/messages/set_popular?mid=' + mid + + '&popular=' + popular + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function setPrivacy(e, mid) { + fetch('//api.juick.com/messages/set_privacy?mid=' + mid + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function getTags() { + fetch('//api.juick.com/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(response => { + return response.json(); + }) + .then(json => { + let tags = json.map(t => t.tag); + let input = document.getElementById('tags_input'); + new Awesomplete(input, { list: tags }); + }); + return false; +} + +function addTag(tag) { + document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value; + return false; +} + +/******************************************************************************/ + +function ready(fn) { + if (document.readyState != 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } +} + +ready(function () { + document.querySelectorAll('textarea').forEach((ta) => { + autosize(ta); + }); + + var insertPMButtons = function (e) { + e.target.classList.add('narrowpm'); + e.target.parentNode.insertAdjacentHTML('afterend', '<input type="submit" value="OK"/>'); + e.target.removeEventListener('click', insertPMButtons); + e.preventDefault(); + }; + document.querySelectorAll('textarea.replypm').forEach(function (e) { + e.addEventListener('click', insertPMButtons); + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + document.querySelectorAll('#postmsg textarea').forEach(function (e) { + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + + var content = document.getElementById('content'); + if (content) { + var pageMID = content.getAttribute('data-mid'); + if (pageMID > 0) { + document.querySelectorAll('li.msg').forEach(li => { + let showReplyFormBtn = li.querySelector('.a-thread-comment'); + if (showReplyFormBtn) { + showReplyFormBtn.addEventListener('click', function (e) { + showCommentForm(pageMID, li.id); + e.preventDefault(); + }); + } + }); + let opMessage = document.querySelector('.msgthread'); + if (opMessage) { + let replyTextarea = opMessage.querySelector('textarea.reply'); + if (replyTextarea) { + replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0)); + replyTextarea.addEventListener('keypress', e => postformListener(e.target, e)); + if (!window.location.hash) { + replyTextarea.focus(); + } + } + } + } + } + + var postmsg = document.getElementById('postmsg'); + if (postmsg) { + document.querySelectorAll('a').filter(t => t.href.indexOf('?') >= 0).forEach(t => { + t.addEventListener('click', e => { + let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1)); + if (params.has('tag')) { + addTag(params.get('tag')); + e.preventDefault(); + } + }); + }); + } + + document.querySelectorAll('.msg-menu').forEach(function (el) { + el.addEventListener('click', function (e) { + var reply = e.target.closest('li'); + var rid = reply ? parseInt(reply.id) : 0; + var message = e.target.closest('section'); + var mid = message.getAttribute('data-mid') || e.target.closest('article').getAttribute('data-mid'); + showMessageLinksDialog(mid, rid); + e.preventDefault(); + }); + }); + document.querySelectorAll('.l .a-privacy').forEach(function (e) { + e.addEventListener('click', function (e) { + setPrivacy( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]').forEach(function (el) { + el.addEventListener('click', function (e) { + let fname = e.target.closest('[data-fname]').getAttribute('data-fname'); + if (!showPhotoDialog(fname)) { + e.preventDefault(); + } + }); + }); + document.querySelectorAll('.social a').forEach(function (e) { + e.addEventListener('click', function (e) { + openSocialWindow(e.target); + e.preventDefault(); + }); + }); + var username = document.getElementById('username'); + if (username) { + username.addEventListener('blur', function () { + checkUsername(); + }); + } + + document.querySelectorAll('.l .a-like').forEach(function (e) { + e.addEventListener('click', function (e) { + likeMessage( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.a-login').forEach(function (el) { + el.addEventListener('click', function (e) { + openDialogLogin(); + e.preventDefault(); + }); + }); + var unfoldall = document.getElementById('unfoldall'); + if (unfoldall) { + unfoldall.addEventListener('click', function (e) { + document.querySelectorAll('#replies>li').forEach(function (e) { + e.style.display = 'block'; + }); + document.querySelectorAll('#replies .msg-comments').forEach(function (e) { + e.style.display = 'none'; + }); + e.preventDefault(); + }); + } + document.querySelectorAll('article').forEach(function (article) { + if (Array.prototype.some.call( + article.querySelectorAll('.msg-tags a'), + function (a) { + return a.textContent === 'NSFW'; + } + )) { + article.classList.add('nsfw'); + } + }); + initWS(); + + window.addEventListener('pagehide', wsShutdown); + + killy.embedAll(); + var elSelector = 'header', + elClassHidden = 'header--hidden', + elClassBackground = 'header--background', + throttleTimeout = 500, + element = document.querySelector(elSelector); + + if (element) { + + var dHeight = 0, + wHeight = 0, + wScrollCurrent = 0, + wScrollBefore = 0, + wScrollDiff = 0, + + throttle = function (delay, fn) { + var last, deferTimer; + return function () { + var context = this, args = arguments, now = +new Date; + if (last && now < last + delay) { + clearTimeout(deferTimer); + deferTimer = setTimeout( + function () { + last = now; + fn.apply(context, args); + }, + delay); + } else { + last = now; + fn.apply(context, args); + } + }; + }; + + window.addEventListener('scroll', throttle(throttleTimeout, function () { + dHeight = document.body.offsetHeight; + wHeight = window.innerHeight; + wScrollCurrent = window.pageYOffset; + wScrollDiff = wScrollBefore - wScrollCurrent; + + if (wScrollCurrent <= 0) { + // scrolled to the very top; element sticks to the top + element.classList.remove(elClassHidden); + element.classList.remove(elClassBackground); + } else if (wScrollDiff > 0 && element.classList.contains(elClassHidden)) { + // scrolled up; element slides in + element.classList.remove(elClassHidden); + element.classList.add(elClassBackground); + } else if (wScrollDiff < 0) { + // scrolled down + if (wScrollCurrent + wHeight >= dHeight && element.classList.contains(elClassHidden)) { + // scrolled to the very bottom; element slides in + element.classList.remove(elClassHidden); + element.classList.add(elClassBackground); + } else { + // scrolled down; element slides out + element.classList.add(elClassHidden); + } + } + + wScrollBefore = wScrollCurrent; + })); + } +}); diff --git a/juick-server/src/main/assets/style.css b/juick-server/src/main/assets/style.css new file mode 100644 index 00000000..d7cd2223 --- /dev/null +++ b/juick-server/src/main/assets/style.css @@ -0,0 +1,952 @@ +/* #region generic */ + +html, +body, +div, +h1, +h2, +ul, +li, +p, +form, +input, +textarea, +pre { + margin: 0; + padding: 0; +} +html, +input, +textarea { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-size: 12pt; + -webkit-font-smoothing: subpixel-antialiased; +} +h1, +h2 { + font-weight: normal; +} +ul { + list-style-type: none; +} +a { + color: #069; + text-decoration: none; +} +img, +hr { + border: none; +} +hr { + background: #CCC; + height: 1px; + margin: 10px 0; +} +pre { + background: #222; + color: #0f0; + overflow-x: auto; + padding: 6px; + white-space: pre; +} +pre::selection { + background: #0f0; + color: #222; +} +pre::-moz-selection { + background: #0f0; + color: #222; +} +.u { + text-decoration: underline; +} + +/* #endregion */ + +/* #region overall layout */ + +html { + background: #f8f8f8; + color: #222; +} +#wrapper { + margin: 0 auto; + width: 1000px; + margin-top: 52px; +} +#column { + float: left; + margin-left: 10px; + overflow: hidden; + padding-top: 10px; + width: 240px; +} +#content { + float: right; + margin: 12px 0 0 0; + width: 728px; +} +#minimal_content { + margin: 0 auto; + min-width: 310px; + width: auto; +} +*::selection { + background: #006699; + color: #fff; +} +body > header { + position: fixed; + top: 0; + width: 100%; + z-index: 10; + transition-duration: 0.5s; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-property: transform; +} +@supports (backdrop-filter: blur(10px)) { + body > header--background { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(10px); + } +} +#header_wrapper { + margin: 0 auto; + width: 1000px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + padding: 4px; +} +.header--background { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.28); + background: #fff; +} +.header--hidden { + transform: translateY(-100%); +} +#footer { + clear: both; + color: #999; + font-size: 10pt; + margin: 40px; + padding: 10px 0; +} + +@media screen and (max-width: 850px) { + body { + text-size-adjust: 100%; + } + body, + #wrapper, + #topwrapper, + #content, + #footer { + float: none; + margin: 0 auto; + min-width: 310px; + width: auto; + } + #wrapper { + margin-top: 50px; + } + body > header { + margin-bottom: 15px; + } + #column { + float: none; + margin: 0 10px; + padding-top: 0; + width: auto; + } +} + +/* #endregion */ + +/* #region header internals */ + +#logo { + height: 36px; + width: 110px; +} +#logo a { + background: url("logo@2x.png") no-repeat; + background-size: cover; + border: 0; + display: block; + height: 36px; + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + width: 110px; +} +#global { + display: flex; +} +#global a { + color: #888; + display: inline-block; + font-size: 13pt; + padding: 14px 6px; +} +#global li { + display: inline-block; +} +#ctitle a { + padding: 14px; +} +#global li:hover, +#ctitle a:hover, +.l a:hover { + background-color: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + cursor: pointer; + transition: box-shadow 0.2s ease-in; +} +#search input { + background: #FFF; + border: 1px solid #ccc; + outline: none !important; + padding: 4px; + -webkit-appearance: none; + border-radius: 0; +} + +/* #endregion */ + +/* #region left column internals */ + +.toolbar { + border-top: 1px solid #CCC; +} + +#column ul, +#column p, +#column hr { + margin: 10px 0; +} +#column li > a { + display: block; + height: 100%; + padding: 6px; +} +#column li > a:hover { + background-color: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + transition: background-color 0.2s ease-in; +} +#column .margtop { + margin-top: 15px; +} + +#column .tags { + background: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + padding: 6px; + text-align: justify; +} +#column .inp { + background: #fff; + border: 1px solid #ddddd5; + outline: none !important; + padding: 4px; + width: 222px; +} +#column .tags h4 { + background: #eee; + border: 1px solid #eee; + color: #888; + display: block; + text-align: center; +} +#ctitle { + font-size: 14pt; +} +#ctitle img { + margin-right: 5px; + vertical-align: middle; + width: 48px; +} +#ustats li { + font-size: 10pt; + margin: 3px 0; +} +#column table.iread { + width: 100%; +} +#column table.iread td { + text-align: center; +} +#column table.iread img { + height: 48px; + width: 48px; +} + +/* #endregion */ + +/* #region main content */ +#content > p, +#content > h1, +#content > h2, +#minimal_content > p, +#minimal_content > h1, +#minimal_content > h2 { + margin: 1em 0; +} +.page { + background: #eee; + padding: 6px; + text-align: center; +} + +.page a { + color: #888; +} + +/* #endregion */ + +/* #region article, message internals */ + +article { + background: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + margin-bottom: 10px; + padding: 20px; +} +article time { + color: #999; + font-size: 10pt; +} +article p { + clear: left; + margin: 5px 0 15px 0; + word-wrap: break-word; + overflow-wrap: break-word; +} +article .ir { + text-align: center; +} +article .ir a { + cursor: zoom-in; + display: block; +} +article .ir img { + max-width: 100%; +} +article > nav.l, +.msg-cont > nav.l { + border-top: 1px solid #eee; + display: flex; + justify-content: space-around; + font-size: 10pt; +} +article > nav.l a, +.msg-cont > nav.l a { + color: #888; + margin-right: 15px; +} +article .likes { + padding-left: 20px; +} +article .replies { + margin-left: 18px; +} +article .tags { + margin-top: 3px; +} +.msg-tags { + margin-top: 12px; + min-height: 1px; +} +article .tags > a, +.badge, +.msg-tags > a { + background: #eee; + border: 1px solid #eee; + color: #888; + display: inline-block; + font-size: 10pt; + margin-bottom: 5px; + margin-right: 5px; + padding: 0 10px; +} +.l .msg-button { + align-items: center; + display: flex; + flex-basis: 0; + flex-direction: column; + flex-grow: 1; + padding-top: 12px; +} +.l .msg-button-icon { + font-weight: bold; +} +.msgthread { + margin-bottom: 0; +} +.msg-avatar { + float: left; + height: 48px; + margin-right: 10px; + width: 48px; +} +.msg-avatar img { + height: 48px; + vertical-align: top; + width: 48px; +} +.msg-cont { + background: #FFF; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + margin-bottom: 12px; + padding: 20px; + width: 640px; +} +.reply-new .msg-cont { + border-right: 5px solid #0C0; +} +.msg-ts { + font-size: small; + vertical-align: top; +} +.msg-ts, +.msg-ts > a { + color: #999; +} +.msg-txt { + clear: both; + margin: 0 0 12px; + padding-top: 10px; + word-wrap: break-word; + overflow-wrap: break-word; +} +.msg-media { + text-align: center; +} +.msg-links { + color: #999; + font-size: small; + margin: 5px 0 0 0; +} +.msg-comments { + color: #AAA; + font-size: small; + margin-top: 10px; + overflow: hidden; + text-indent: 10px; +} +.ta-wrapper { + border: 1px solid #DDD; + display: flex; + flex-grow: 1; +} +.msg-comment { + display: flex; + width: 100%; + margin-top: 10px; +} +.msg-comment-hidden { + display: none; +} +.msg-comment textarea { + border: 0; + flex-grow: 1; + outline: none !important; + padding: 4px; + resize: vertical; + vertical-align: top; +} +.attach-photo { + cursor: pointer; +} +.attach-photo-active { + color: green; +} +.msg-comment input { + align-self: flex-start; + background: #EEE; + border: 1px solid #CCC; + color: #999; + margin: 0 0 0 6px; + position: -webkit-sticky; + position: sticky; + top: 70px; + vertical-align: top; + width: 50px; +} +.msg-recomms { + color: #AAA; + font-size: small; + margin-top: 10px; + overflow: hidden; + text-indent: 10px; +} +#replies .msg-txt, +#private-messages .msg-txt { + margin: 0; +} +.title2 { + background: #fff; + margin: 20px 0; + padding: 10px 20px; + width: 640px; +} +.title2-right { + float: right; + line-height: 24px; +} +#content .title2 h2 { + font-size: x-large; + margin: 0; +} + +@media screen and (max-width: 850px) { + #header_wrapper { + width: auto; + } + #global { + justify-content: space-around; + flex-grow: 1; + } + #search { + padding: 4px; + } + article { + overflow: auto; + } + article p { + margin: 10px 0 8px 0; + } + .msg, + .msg-cont { + min-width: 280px; + width: auto; + } + .msg-cont { + margin: 8px 0; + } + .msg-media { + overflow: auto; + } + .title2 h2 { + font-size: large; + } + .msg-comment { + flex-direction: column; + } + .msg-comment input { + align-self: flex-end; + margin: 6px 0 0 0; + width: 100px; + } +} + +@media screen and (max-width: 480px) { + #wrapper { + margin-top: 104px; + } + #search { + display: none; + } + #global a { + padding: 14px 2px; + font-size: 11pt; + } + .msg-cont > nav.l, + article > nav.l { + font-size: 9pt; + } + .msg-txt { + padding-top: 5px; + } + .title2 { + font-size: 11pt; + width: auto; + } + #content .title2 h2 { + font-size: 11pt; + } + .title2-right { + line-height: initial; + } +} + +/* #endregion */ + +/* #region user-generated texts */ + +q:before, +q:after { + content: ""; +} +q, +blockquote { + border-left: 3px solid #CCC; + color: #666; + display: block; + margin: 10px 0 10px 10px; + padding-left: 10px; +} + +/* #endregion */ + +/* #region new message form internals */ + +#newmessage { + background: #E5E5E0; + margin-bottom: 20px; + padding: 15px; +} +#newmessage textarea { + border: 1px solid #CCC; + box-sizing: border-box; + margin: 0 0 5px 0; + margin-top: 20px; + max-height: 6em; + min-width: 280px; + padding: 4px; + width: 100%; +} +#newmessage input { + border: 1px solid #CCC; + margin: 5px 0; + padding: 2px 4px; +} +#newmessage .img { + width: 500px; +} +#newmessage .tags { + width: 500px; +} +#newmessage .subm { + background: #EEEEE5; + width: 150px; +} +@media screen and (max-width: 850px) { + #newmessage .img, + #newmessage .tags { + width: 100%; + } +} + +/* #endregion */ + +/* #region user lists */ + +.users { + margin: 10px 0; + width: 100%; + display: flex; + flex-wrap: wrap; +} +.users > span { + overflow: hidden; + padding: 6px 0; + width: 200px; +} +.users img { + height: 32px; + margin-right: 6px; + vertical-align: middle; + width: 32px; +} + +/* #endregion */ + +/* #region signup form */ + +.signup-h1 > img { + margin-right: 10px; + vertical-align: middle; +} +.signup-h1 { + font-size: x-large; + margin: 20px 0 10px 0; +} +.signup-h2 { + font-size: large; + margin: 10px 0 5px 0; +} +.signup-hr { + margin: 20px 0; +} + +/* #endregion */ + +/* #region PM */ + +.newpm { + margin: 20px 60px 30px 60px; +} +.newpm textarea { + resize: vertical; + width: 100%; +} +.newpm-send input { + width: 100px; +} + +/* #endregion */ + +/* #region popup dialog (lightbox) */ + +#dialogb { + background: #222; + height: 100%; + left: 0; + opacity: 0.6; + position: fixed; + top: 0; + width: 100%; + z-index: 10; +} +#dialogt { + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; +} +#dialogw { + z-index: 11; + max-width: 96%; + max-height: calc(100% - 100px); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +#dialogw a { + display: block; +} +#dialogw img { + max-height: 100%; + max-height: 90vh; + max-width: 100%; +} +#dialog_header { + width: 100%; + height: 44px; + position: fixed; + display: flex; + flex-direction: row-reverse; + align-items: center; +} +.header_image { + background: rgba(0, 0, 0, 0.28); +} +#dialogc { + cursor: pointer; + color: #ccc; + padding-right: 6px; +} +.dialoglogin { + background: #fff; + padding: 25px; + width: 300px; +} +.dialog-opened { + overflow: hidden; +} +#signemail, +#signfb, +#signvk { + display: block; + line-height: 32px; + margin: 10px 0; + text-decoration: none; + width: 100%; +} +#signvk { + margin-bottom: 30px; +} +.dialoglogin form { + margin-top: 7px; +} +.signinput, +.signsubmit { + border: 1px solid #CCC; + margin: 3px 0; + padding: 3px; +} +.signinput { + width: 292px; +} +.signsubmit { + width: 70px; +} +.dialogshare { + background: #fff; + min-width: 300px; + overflow: auto; + padding: 20px; +} +.dialogl { + background: #fff; + border: 1px solid #DDD; + margin: 3px 0 20px; + padding: 5px; +} +.dialogshare li { + float: left; + margin: 5px 10px 0 0; +} +.dialogshare a { + display: block; +} +.dialogtxt { + background: #fff; + padding: 20px; +} + +@media screen and (max-width: 480px) { + .dialog-opened { + position: fixed; + width: 100%; + } +} + +/* #endregion */ + +/* #region misc */ + +#wsthread { + background: #CCC; + bottom: 20px; + cursor: pointer; + display: none; + padding: 5px 10px; + position: fixed; + right: 20px; +} +.sharenew { + display: inline-block; + line-height: 32px; + min-height: 32px; + min-width: 200px; + padding: 0 12px 0 37px; +} +.icon { + margin-top: -2px; + vertical-align: middle; +} +.icon--ei-link { + margin-top: -1px; +} +.icon--ei-comment { + margin-top: -5px; +} +.newmessage { + /* textarea on the /post page */ + border: 1px solid #DDD; + padding: 2px; + resize: vertical; + width: 100%; +} + +/* #endregion */ + +/* #region footer internals */ + +#footer-social { + float: left; +} +#footer-social a { + border: 0; + display: inline-block; +} +#footer-left { + margin-left: 286px; + margin-right: 350px; +} +#footer-right { + float: right; +} + +@media screen and (max-width: 850px) { + #footer { + margin: 0 10px; + } + #footer div { + float: none; + margin: 10px 0; + } +} + +/* #endregion */ + +/* #region settings */ + +fieldset { + border: 1px dotted #ccc; + margin-top: 25px; +} + +/* #endregion */ + +/* #region embeds */ + +.embedContainer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding: 0; + margin: 30px -3px 15px -3px; +} +.embedContainer > * { + box-sizing: border-box; + flex-grow: 1; + margin: 3px; + min-width: 49%; +} +.embedContainer > .compact { + flex-grow: 0; +} +.embedContainer .picture img { + display: block; +} +.embedContainer img, +.embedContainer video { + max-width: 100%; + max-height: 80vh; +} +.embedContainer > .audio, +.embedContainer > .youtube { + min-width: 90%; +} +.embedContainer audio { + width: 100%; +} +.embedContainer iframe { + overflow: hidden; + resize: vertical; + display: block; +} + +/* #endregion */ + +/* #region nsfw */ + +article.nsfw .embedContainer img, +article.nsfw .embedContainer video, +article.nsfw .embedContainer iframe, +article.nsfw .ir img { + opacity: 0.1; +} +article.nsfw .embedContainer img:hover, +article.nsfw .embedContainer video:hover, +article.nsfw .embedContainer iframe:hover, +article.nsfw .ir img:hover { + opacity: 1; +} + +/* #endregion */ |