import elementClosest from 'element-closest' import 'formdata-polyfill' import 'classlist.js' import 'whatwg-fetch' import 'core-js/stable' import { embedLinksToX, embedAll, format } from '../../../vnext/src/utils/embed' import renderIcons from './icon' import svg4everybody from 'svg4everybody' import { createRoot } from 'react-dom/client' import { useState, useEffect } from 'react' import Message from '../../../vnext/src/ui/Message' import { VisitorProvider } from '../../../vnext/src/ui/VisitorContext' import { CookiesProvider } from 'react-cookie' /** * 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() } 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 => 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', method: 'POST' }) .then(handleErrors) .then(function(response) { if (response.ok) { window.location.reload() } else { alert('Something went wrong :(') } }) .catch(console.err) return false } function toggleWL(e, name) { fetch('/api/users/wl?name=' + name + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { credentials: 'same-origin', method: 'POST' }) .then(handleErrors) .then(function(response) { if (response.ok) { window.location.reload() } else { alert('Something went wrong :(') } }) .catch(console.err) return false } /* function getTags() { fetch('/api/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), { credentials: 'omit' }) .then(handleErrors) .then(response => response.json()) .then(json => { let tags = json.map(t => t.tag) let input = document.getElementById('tags_input') }).catch(console.error) return false } */ function addTag(tag) { document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value return false } function fetchUserUri(dataUri) { let data = new FormData() data.append('uri', dataUri) return new Promise((resolve) => { fetch('/api/u/', { method: 'POST', body: data }).then(handleErrors) .then(response => { return response.json() }) .then(json => { resolve(json) }) .catch(() => { resolve({ uname: dataUri, uri: dataUri }) }) }) } const registerServiceWorker = () => { const publicKey = 'BPU0LniKKR0QiaUvILPd9AystmSOU8rWDZobxKm7IJN5HYxOSQdktRdc74TZvyRS9_kyUz7LDN6gUAmAVOmObAU' navigator.serviceWorker.register('/sw.js', { scope: '/' }) navigator.serviceWorker.ready.then(reg => { return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: publicKey }) }).then( sub => { return [ { type: 'web', token: JSON.stringify(sub) } ] }, err => console.error(err) ).then(body => { return fetch('/api/notifications?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'same-origin', body: JSON.stringify(body) }) }).then(response => { console.log(response.status) }).catch(console.error) } function notificationsCheckPermissions(button) { console.log(Notification.permission) switch (Notification.permission.toLowerCase()) { case 'granted': button.innerHTML = `${i18n('settings.notifications.granted')}` button.disabled = true registerServiceWorker() break case 'denied': button.innerHTML = `${i18n('settings.notifications.denied')}` button.disabled = true break case 'default': button.innerHTML = `${i18n('settings.notifications.request')}` } } const Feed = () => { const [messages, setMessages] = useState(/** @type { import('../../../vnext/src/api').Message[]? } */ (null)) useEffect(() => { let url = new URL(window.location.href) fetch(url, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(response => response.json()) .then(json => { setMessages(json) }) .catch(console.error) }, []) return messages ? ( <> { messages.map(message => {}} /> ) } ) : 'Loading...' } /******************************************************************************/ function ready(fn) { if (document.readyState != 'loading') { fn() } else { document.addEventListener('DOMContentLoaded', fn) } } ready(() => { 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) { embedAll() Array.from(document.querySelectorAll('textarea')).forEach((ta) => { autosize(ta) }) 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', () => 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 => response.json()) .then(result => { if (result.newMessage) { window.location.href = new URL(`/m/${result.newMessage.mid}`, window.location.href).href } else { alert(result.text) } }).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 => response.json()) .then(result => { if (result.to) { window.location.href = new URL('/pm/sent', window.location.href).href } 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) { setPrivacy( e.target, document.getElementById('content').getAttribute('data-mid')) e.preventDefault() }) }) Array.from(document.querySelectorAll('.a-vip')).forEach(function(e) { e.addEventListener('click', function(e) { toggleWL( e.target, e.target.closest('[data-name]').getAttribute('data-name')) 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).then(user => { let header = el.closest('.msg-header') Array.from(header.querySelectorAll('.a-username')).forEach(a => { a.setAttribute('href', user.url || 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).then(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) el.setAttribute('href', user.url || user.uri) } }), 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() } }) const button = document.getElementById('notifications_toggle') if (button) { button.addEventListener('click', () => { Notification.requestPermission(() => { notificationsCheckPermissions(button) }) }) notificationsCheckPermissions(button) } let root = document.getElementById('feed') createRoot(root).render( ) })