aboutsummaryrefslogtreecommitdiff
path: root/juick-www/src/main/assets
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2018-03-27 22:13:16 +0300
committerGravatar Vitaly Takmazov2018-03-27 22:22:35 +0300
commit13237626f3956d93a91a94bee6fee6aa86134a06 (patch)
tree50634fdfb2fddc924d60be18886184f49b2f7115 /juick-www/src/main/assets
parentcc551432bf80e4466b92c42a77a094f31408abeb (diff)
www: spring boot autoconfigured static resources
+ cache busting using Spring
Diffstat (limited to 'juick-www/src/main/assets')
-rw-r--r--juick-www/src/main/assets/embed.js336
-rw-r--r--juick-www/src/main/assets/logo.pngbin0 -> 2447 bytes
-rw-r--r--juick-www/src/main/assets/logo@2x.pngbin0 -> 4822 bytes
-rw-r--r--juick-www/src/main/assets/scripts.js736
-rw-r--r--juick-www/src/main/assets/style.css907
5 files changed, 1979 insertions, 0 deletions
diff --git a/juick-www/src/main/assets/embed.js b/juick-www/src/main/assets/embed.js
new file mode 100644
index 00000000..25c37142
--- /dev/null
+++ b/juick-www/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(/^(?:>|&gt;)\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: /((?:^(?:>|&gt;)\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-www/src/main/assets/logo.png b/juick-www/src/main/assets/logo.png
new file mode 100644
index 00000000..4e0f6d56
--- /dev/null
+++ b/juick-www/src/main/assets/logo.png
Binary files differ
diff --git a/juick-www/src/main/assets/logo@2x.png b/juick-www/src/main/assets/logo@2x.png
new file mode 100644
index 00000000..6febeaf9
--- /dev/null
+++ b/juick-www/src/main/assets/logo@2x.png
Binary files differ
diff --git a/juick-www/src/main/assets/scripts.js b/juick-www/src/main/assets/scripts.js
new file mode 100644
index 00000000..9da9ce3c
--- /dev/null
+++ b/juick-www/src/main/assets/scripts.js
@@ -0,0 +1,736 @@
+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',
+ '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': 'Войти через ВКонтакте',
+ '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);
+ 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 === undefined) ? '' : `
+ <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="/${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} &middot; <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="/_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('//juick.com/like?mid=' + mid, {
+ 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();
+});
diff --git a/juick-www/src/main/assets/style.css b/juick-www/src/main/assets/style.css
new file mode 100644
index 00000000..ce80e650
--- /dev/null
+++ b/juick-www/src/main/assets/style.css
@@ -0,0 +1,907 @@
+/* #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: 50px;
+}
+#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;
+}
+body > header {
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.28);
+ background: #fff;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+}
+#header_wrapper {
+ margin: 0 auto;
+ width: 1000px;
+ display: flex;
+}
+#footer {
+ clear: both;
+ color: #999;
+ font-size: 10pt;
+ margin: 40px;
+ padding: 10px 0;
+}
+
+@media screen and (max-width: 850px) {
+ body {
+ -moz-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-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;
+ margin: 7px 25px 0 20px;
+ 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 {
+ flex-grow: 1;
+ display: flex;
+}
+#global a {
+ color: #888;
+ display: inline-block;
+ font-size: 13pt;
+ padding: 14px 6px;
+}
+#global li {
+ display: inline-block;
+}
+#global li: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 {
+ margin: 12px 20px 12px 0;
+}
+#search input {
+ background: #FFF;
+ border: 1px solid #DDDDD5;
+ outline: none !important;
+ padding: 4px;
+}
+@media screen and (max-width: 850px) {
+ #logo {
+ display: none;
+ }
+ #search {
+ display: inline-block;
+ float: none;
+ margin: 10px 10px;
+ }
+}
+
+/* #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 p {
+ font-size: 10pt;
+ line-height: 140%;
+}
+#column .tags {
+ text-align: justify;
+}
+#column .inp {
+ background: #fff;
+ border: 1px solid #ddddd5;
+ outline: none !important;
+ padding: 4px;
+ width: 222px;
+}
+#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,
+.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) {
+ 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) {
+ .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;
+}
+#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 */