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', mode: 'cors', credentials: 'omit' }) .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; })); } });