import elementClosest from 'element-closest'; import 'formdata-polyfill'; import 'classlist.js'; import 'whatwg-fetch'; import 'core-js/stable'; import { embedLinksToX, embedAll, format } from './embed'; import renderIcons from './icon'; import svg4everybody from 'svg4everybody'; /** * Autosize textarea * * @param {HTMLTextAreaElement} el textarea element */ function autosize(el) { let offset = el.offsetHeight - el.clientHeight; el.addEventListener('input', (ev) => { const textarea = /** @type {HTMLTextAreaElement} */ (ev.target); textarea.style.height = 'auto'; textarea.style.height = (textarea.scrollHeight + offset) + 'px'; }); } /** * Display an icon from the evil-icons set * * @param {string} name Icon name from the iconset * @returns {string} HTML markup for the selected icon */ function evilIcon(name) { return `
`; } /* 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 100 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', 'settings.notifications.granted': 'Enabled', 'settings.notifications.denied': 'Denied', 'settings.notifications.request': 'Enable', }, 'ru': { 'message.inReplyTo': 'в ответ на', 'message.reply': 'Ответить', 'message.likeThisMessage?': 'Рекомендовать это сообщение?', 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения', 'postForm.upload': 'загрузить', 'postForm.newMessage': 'Новое сообщение...', 'postForm.imageLink': 'Ссылка на изображение', 'postForm.imageFormats': 'JPG/PNG, до 100 Мб', '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': 'Ошибка', 'settings.notifications.granted': 'Подключено', 'settings.notifications.denied': 'Запрещено', 'settings.notifications.request': 'Подключить', } }; /* eslint-enable only-ascii/only-ascii */ /** * Detect window language * * @returns {string} Detected language */ 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; } /** @type { EventSource } */ var es; var pageTitle; function initES() { if (!('EventSource' in window)) { return; } let url = '/api/events'; let hash = document.getElementById('body').getAttribute('data-hash'); if (hash) { url += '?hash=' + hash; } es = new EventSource(url); es.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; } }; es.addEventListener('msg', msg => { try { var jsonMsg = JSON.parse(msg.data); console.log('data: ' + msg.data); if (jsonMsg.service) { return; } wsIncomingReply(jsonMsg); } catch (err) { console.log(err); } }); } 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')} /${msg.replyto}`; } let photoDiv = (msg.attach == null) ? '' : `
`; let msgContHtml = `
${msg.user.uname}:
${msg.user.uname}
${format(msg.body, msg.mid, false)}
${photoDiv}
`; 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(); }); embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a'); document.getElementById('replies').appendChild(li); updateRepliesCounter(); var notify = new Notification(msg.user.uname, { body: `${msg.replyQuote}\n${msg.body}`, icon: msg.user.avatar }); notify.onerror = console.log; } 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 = Array.from(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; } } /******************************************************************************/ /******************************************************************************/ /******************************************************************************/ /** * Submit form on Ctrl+Enter * * @param {HTMLElement} formEl Target form element * @param {KeyboardEvent} ev Keyboard event */ function postformListener(formEl, ev) { if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) { let form = formEl.closest('form'); if (!form.onsubmit || form.submit()) { /** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]')).click(); } } } /** * Close dialog on Esc * * @param {KeyboardEvent} ev Keyboard event */ function closeDialogListener(ev) { ev = ev || /** @type {KeyboardEvent} */ (window.event); if (ev.keyCode == 27) { closeDialog(); } } function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } /** * * @param {number} mid message id * @param {string} rid reply id */ function showCommentForm(mid, rid) { let reply = /** @type { HTMLElement } */ (document.getElementById(rid)); let formTarget = reply.querySelector('div.msg-cont .msg-comment-target'); if (formTarget) { let formHtml = `
${evilIcon('ei-camera')}
`; formTarget.insertAdjacentHTML('afterend', formHtml); formTarget.remove(); let form = /** @type {HTMLFormElement} */ (reply.querySelector('form')); let submitButton = /** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]')); let attachButton = /** @type {HTMLInputElement} */ (form.querySelector('.msg-comment .attach-photo')); attachButton.addEventListener('click', e => attachCommentPhoto(/** @type {HTMLDivElement} */(e.target))); let textarea = /** @type {HTMLTextAreaElement} */ (form.querySelector('.msg-comment textarea')); textarea.addEventListener('keypress', e => postformListener(/** @type {HTMLElement} */(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; let formData = new FormData(form); fetch('/api/comment' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, credentials: 'omit' }).then(handleErrors) .then(response => { if (response.ok) { response.json().then(result => { if (result.newMessage) { window.location.hash = `#${result.newMessage.rid}`; } else { alert(result.text); } window.location.reload(); }); } }).catch(error => { alert(error.message); }); e.preventDefault(); }); } /** @type {HTMLTextAreaElement} */ (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; } /** * "Attach" button * * @param {HTMLDivElement} div element attach to */ 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 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(``, true); return false; } else { openDialog(``, true); return false; } } function openDialog(html, image) { var dialogHtml = `
${evilIcon('ei-close')}
${html}
`; let body = /** @type {HTMLElement} */ (document.querySelector('body')); body.classList.add('dialog-opened'); body.insertAdjacentHTML('afterbegin', dialogHtml); if (image) { let header = /** @type {HTMLElement} */ (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() { document.querySelector('body').classList.remove('dialog-opened'); document.querySelector('#dialogb').remove(); document.querySelector('#dialogt').remove(); } function checkUsername() { var uname = document.querySelector('#username').textContent, style = /** @type {HTMLElement} */ (document.querySelector('#username')).style; fetch('/api/users?uname=' + uname) .then(handleErrors) .then(function() { style.background = '#FFCCCC'; }) .catch(function() { style.background = '#CCFFCC'; }); } /******************************************************************************/ function resultMessage(str) { var result = document.createElement('p'); result.textContent = str; return result; } function likeMessage(e, mid) { if (confirm(i18n('message.likeThisMessage?'))) { fetch('/api/like?mid=' + mid + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', credentials: 'omit' }) .then(handleErrors) .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 subscribeMessage(e, mid) { fetch('/api/subscribe?mid=' + mid + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', credentials: 'omit' }) .then(handleErrors) .then(function(response) { if (response.ok) { window.location.reload(); } else { alert('Something went wrong :('); } }) .catch(error => { alert(error.message); }); return false; } /******************************************************************************/ function setPrivacy(e, mid) { fetch('/api/messages/set_privacy?mid=' + mid + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { credentials: 'same-origin' }) .then(handleErrors) .then(function() { e.closest('article').append(resultMessage('OK!')); }); return false; } function getTags() { fetch('/api/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), { credentials: 'omit' }) .then(handleErrors) .then(response => { return response.json(); }) .then(json => { let tags = json.map(t => t.tag); let input = document.getElementById('tags_input'); }); return false; } function addTag(tag) { document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value; return false; } function fetchUserUri(dataUri, callback) { let data = new FormData(); data.append('uri', dataUri); fetch('/api/u/', { method: 'POST', body: data }).then(handleErrors) .then(response => { return response.json(); }) .then(json => { callback(json); }) .catch(e => { callback({ uname: dataUri, uri: dataUri }); }); } function notificationsCheckPermissions(button) { console.log(Notification.permission); switch (Notification.permission.toLowerCase()) { case 'granted': button.innerHTML = `${i18n('settings.notifications.granted')}`; button.disabled = true; break; case 'denied': button.innerHTML = `${i18n('settings.notifications.denied')}`; button.disabled = true; break; case 'default': button.innerHTML = `${i18n('settings.notifications.request')}`; } } /******************************************************************************/ function ready(fn) { if (document.readyState != 'loading') { fn(); } else { document.addEventListener('DOMContentLoaded', fn); } } ready(() => { elementClosest(window); Array.from(document.querySelectorAll('textarea')).forEach((ta) => { autosize(ta); }); svg4everybody(); renderIcons(); var insertPMButtons = function(e) { e.target.classList.add('narrowpm'); e.target.parentNode.insertAdjacentHTML('afterend', ''); e.target.removeEventListener('click', insertPMButtons); e.preventDefault(); }; /** @type {HTMLTextAreaElement[]} */ (Array.from(document.querySelectorAll('textarea.replypm'))).forEach(function(e) { e.addEventListener('click', insertPMButtons); e.addEventListener('keypress', function(e) { postformListener(/** @type {HTMLElement} */(e.target), e); }); }); /** @type {HTMLTextAreaElement[]} */ (Array.from(document.querySelectorAll('#postmsg textarea'))).forEach(function(e) { e.addEventListener('keypress', function(e) { postformListener(/** @type {HTMLElement} */(e.target), e); }); }); var content = document.getElementById('content'); if (content) { var pageMID = +content.getAttribute('data-mid'); if (pageMID > 0) { Array.from(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 = /** @type {HTMLTextAreaElement} */ (opMessage.querySelector('textarea.reply')); if (replyTextarea) { replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0)); replyTextarea.addEventListener('keypress', e => postformListener(/** @type {HTMLElement} */(e.target), e)); if (!window.location.hash) { replyTextarea.focus(); } } } } } var postmsg = /** @type {HTMLFormElement} */(document.getElementById('postmsg')); if (postmsg) { Array.from(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(); } }); }); postmsg.addEventListener('submit', e => { let formData = new FormData(postmsg); fetch('/api/post' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, credentials: 'omit' }).then(handleErrors) .then(response => { if (response.ok) { response.json().then(result => { if (result.newMessage) { window.location.href = new URL(`/m/${result.newMessage.mid}`, window.location.href).href; } else { alert(result.text); } }); } else { alert('Something went wrong :('); } }).catch(error => { alert(error.message); }); e.preventDefault(); }); } /** @type {HTMLFormElement[]} */ (Array.from(document.querySelectorAll('.pmmsg'))).forEach(pmmsg => { pmmsg.addEventListener('submit', e => { let formData = new FormData(pmmsg); fetch('/api/pm' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, credentials: 'omit' }).then(handleErrors) .then(response => { if (response.ok) { response.json().then(result => { if (result.to) { window.location.href = new URL('/pm/sent', window.location.href).href; } else { alert('Something went wrong :('); } }); } else { alert('Something went wrong :('); } }).catch(error => { alert(error.message); }); e.preventDefault(); }); }); Array.from(document.querySelectorAll('.l .a-privacy')).forEach(function(e) { e.addEventListener('click', function(e) { const article = /** @type {HTMLElement} */((e.target).closest('article')); setPrivacy( e.target, article.getAttribute('data-mid')); e.preventDefault(); }); }); Array.from(document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]')).forEach(function(el) { el.addEventListener('click', function(e) { let fname = /** @type {HTMLElement} */ (e.target).closest('[data-fname]').getAttribute('data-fname'); if (!showPhotoDialog(fname)) { e.preventDefault(); } }); }); var username = document.getElementById('username'); if (username) { username.addEventListener('blur', function() { checkUsername(); }); } Array.from(document.querySelectorAll('.l .a-like')).forEach(function(e) { e.addEventListener('click', function(e) { likeMessage( e.target, /** @type {HTMLElement} */(e.target).closest('article').getAttribute('data-mid')); e.preventDefault(); }); }); Array.from(document.querySelectorAll('.l .a-sub')).forEach(function(e) { e.addEventListener('click', function(e) { subscribeMessage( e.target, document.getElementById('content').getAttribute('data-mid')); e.preventDefault(); }); }); var unfoldall = document.getElementById('unfoldall'); if (unfoldall) { unfoldall.addEventListener('click', function(e) { /** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('#replies>li'))).forEach(function(e) { e.style.display = 'block'; }); /** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('#replies .msg-comments'))).forEach(function(e) { e.style.display = 'none'; }); e.preventDefault(); }); } Array.from(document.querySelectorAll('article')).forEach(function(article) { if (Array.prototype.some.call( Array.from(article.querySelectorAll('.msg-tags a')), function(a) { return a.textContent === 'NSFW'; } )) { article.classList.add('nsfw'); } }); Array.from(document.querySelectorAll('[data-uri]')).forEach(el => { let dataUri = el.getAttribute('data-uri') || ''; if (dataUri) { setTimeout(() => fetchUserUri(dataUri, user => { let header = el.closest('.msg-header'); Array.from(header.querySelectorAll('.a-username')).forEach(a => { a.setAttribute('href', user.uri); let img = a.querySelector('img'); if (img && user.avatar) { img.setAttribute('src', user.avatar); img.setAttribute('alt', user.uname); } let textNode = a.childNodes[0]; if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) { let uname = document.createTextNode(user.uname); a.replaceChild(uname, a.firstChild); } }); }), 100); } }); Array.from(document.querySelectorAll('[data-user-uri]')).forEach(el => { let dataUri = el.getAttribute('href') || ''; if (dataUri) { setTimeout(() => fetchUserUri(dataUri, user => { let textNode = el.childNodes[0]; if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) { let uname = document.createTextNode(`@${user.uname}`); el.replaceChild(uname, el.firstChild); } }), 100); } }); let location = window.location.href; /** @type {HTMLLinkElement[]} */ (Array.from(document.querySelectorAll('#header_wrapper a'))).forEach(el => { if (el.href === location) { el.classList.add('active'); el.setAttribute('disabled', 'disabled'); } }); initES(); addEventListener('beforeunload', () => { if (es) { es.close(); } }); embedAll(); const button = document.getElementById('notifications_toggle'); if (button) { button.addEventListener('click', () => { Notification.requestPermission(function(result) { notificationsCheckPermissions(button); }); }); notificationsCheckPermissions(button); } });