diff options
49 files changed, 1503 insertions, 1510 deletions
@@ -68,7 +68,7 @@ "jsdoc/require-property-description": "off", "jsdoc/require-param-description": "off", "promise/always-return": "off", - "semi": ["error", "always"], + "semi": ["error", "never", { "beforeStatementContinuationChars": "always"}], "semi-style": ["error", "last"], "@typescript-eslint/no-floating-promises": "error" }, diff --git a/package.json b/package.json index 5d0d6ea7..05427f0f 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,13 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "compile": "cross-env NODE_ENV=production webpack", - "lint": "eslint .", + "lint": "eslint src", "vnext:test": "jest", "vnext:build": "webpack -c vnext/webpack.config.js --mode production", "vnext:build:ssr": "npm run vnext:build && webpack -c vnext/server/webpack.config.js --mode production", "vnext:start-ssr": "npm run vnext:build:ssr && cross-env DEBUG=http node --enable-source-maps public/server.js", "vnext:start": "cross-env NODE_ENV=development webpack serve -c vnext/webpack.config.js --mode development", - "vnext:lint": "eslint vnext/src/**/*.js vnext/server/**/*.js" + "vnext:lint": "eslint vnext" }, "repository": { "type": "git", diff --git a/src/main/assets/icon.js b/src/main/assets/icon.js index 6ab3cd81..c94a1663 100644 --- a/src/main/assets/icon.js +++ b/src/main/assets/icon.js @@ -1,40 +1,43 @@ -import evilIcons from 'evil-icons/assets/sprite.svg'; +import evilIcons from 'evil-icons/assets/sprite.svg' function icon(name, { size = '', className = '' }) { - const classes = `icon icon--${name} icon--${size} ${className}`.trim(); + const classes = `icon icon--${name} icon--${size} ${className}`.trim() var icon = '<svg class="icon__cnt">' + `<use xlink:href='${evilIcons}#${name}-icon' />` + - '</svg>'; + '</svg>' var html = '<div class="' + classes + '">' + wrapSpinner(icon, classes) + - '</div>'; + '</div>' - return html; + return html } function wrapSpinner(html, klass) { if (klass.indexOf('spinner') > -1) { - return '<div class="icon__spinner">' + html + '</div>'; + return '<div class="icon__spinner">' + html + '</div>' } else { - return html; + return html } } +/** + * + */ export default function renderIcons() { - var render = true; - var icons = document.querySelectorAll('[data-icon]'); + var render = true + var icons = document.querySelectorAll('[data-icon]') for (var i = 0; i < icons.length; i++) { - var currentIcon = icons[i]; - var name = currentIcon.getAttribute('data-icon'); + var currentIcon = icons[i] + var name = currentIcon.getAttribute('data-icon') var options = { className: currentIcon.className, size: currentIcon.getAttribute('data-size') - }; + } - currentIcon.insertAdjacentHTML('beforebegin', icon(name, options)); - currentIcon.parentNode.removeChild(currentIcon); + currentIcon.insertAdjacentHTML('beforebegin', icon(name, options)) + currentIcon.parentNode.removeChild(currentIcon) } } diff --git a/src/main/assets/scripts.js b/src/main/assets/scripts.js index 4422fdb3..dfd12494 100644 --- a/src/main/assets/scripts.js +++ b/src/main/assets/scripts.js @@ -1,35 +1,33 @@ -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 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' /** * Autosize textarea - * * @param {HTMLTextAreaElement} el textarea element */ function autosize(el) { - let offset = el.offsetHeight - el.clientHeight; + 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'; - }); + 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 `<div class="icon icon--${name}"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/sprite.svg#${name}-icon"></use></svg></div>`; + return `<div class="icon icon--${name}"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/sprite.svg#${name}-icon"></use></svg></div>` } /* eslint-disable only-ascii/only-ascii */ @@ -90,79 +88,78 @@ const translations = { '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; + || window.navigator.language } function i18n(key = '', lang = undefined) { - const fallbackLang = 'ru'; - lang = lang || getLang().split('-')[0]; + const fallbackLang = 'ru' + lang = lang || getLang().split('-')[0] return (translations[lang] && translations[lang][key]) || translations[fallbackLang][key] - || key; + || key } /** @type { EventSource } */ -var es; -var pageTitle; +var es +var pageTitle function initES() { if (!('EventSource' in window)) { - return; + return } - let url = '/api/events'; - let hash = document.getElementById('body').getAttribute('data-hash'); + let url = '/api/events' + let hash = document.getElementById('body').getAttribute('data-hash') if (hash) { - url += '?hash=' + hash; + url += '?hash=' + hash } - es = new EventSource(url); + es = new EventSource(url) es.onopen = function() { - console.log('online'); + 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; + 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); + var jsonMsg = JSON.parse(msg.data) + console.log('data: ' + msg.data) if (jsonMsg.service) { - return; + return } - wsIncomingReply(jsonMsg); + wsIncomingReply(jsonMsg) } catch (err) { - console.log(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; + 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>`; + 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>`; + </div>` let msgContHtml = ` <div class="msg-cont"> <div class="msg-header"> @@ -177,54 +174,54 @@ function wsIncomingReply(msg) { <div class="msg-txt">${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); + </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(); - }); + showCommentForm(msg.mid, msg.rid) + e.preventDefault() + }) - embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a'); + embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a') - document.getElementById('replies').appendChild(li); + document.getElementById('replies').appendChild(li) - updateRepliesCounter(); + updateRepliesCounter() } function newReply(e) { - var li = e.target; - li.classList.remove('reply-new'); - li.removeEventListener('click', e); - li.removeEventListener('mouseover', e); - updateRepliesCounter(); + 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'); + var li = document.querySelector('#replies>li.reply-new') if (li) { - li.classList.remove('reply-new'); - li.removeEventListener('click', this); - li.children[0].scrollIntoView(); - updateRepliesCounter(); + 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'); + 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; + wsthread.textContent = `${replies}` + wsthread.style.display = 'block' + document.title = '[' + replies + '] ' + pageTitle } else { - wsthread.style.display = 'none'; - document.title = pageTitle; + wsthread.style.display = 'none' + document.title = pageTitle } } @@ -234,36 +231,34 @@ function updateRepliesCounter() { /** * 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'); + let form = formEl.closest('form') if (!form.onsubmit || form.submit()) { - /** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]')).click(); + /** @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); + ev = ev || /** @type {KeyboardEvent} */ (window.event) if (ev.keyCode == 27) { - closeDialog(); + closeDialog() } } function handleErrors(response) { if (!response.ok) { - throw Error(response.statusText); + throw Error(response.statusText) } - return response; + return response } /** @@ -272,8 +267,8 @@ function handleErrors(response) { * @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'); + let reply = /** @type { HTMLElement } */ (document.getElementById(rid)) + let formTarget = reply.querySelector('div.msg-cont .msg-comment-target') if (formTarget) { let formHtml = ` <form> @@ -286,34 +281,34 @@ function showCommentForm(mid, rid) { </div> <input type="submit" class="Button" value="OK"> </div> - </form>`; - formTarget.insertAdjacentHTML('afterend', formHtml); - formTarget.remove(); + </form>` + formTarget.insertAdjacentHTML('afterend', formHtml) + formTarget.remove() - let form = /** @type {HTMLFormElement} */ (reply.querySelector('form')); - let submitButton = /** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]')); + 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 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 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 ''; - }; + let len = textarea.value.length + if (len > 4096) { return 'Message is too long' } + return '' + } form.addEventListener('submit', e => { - let validationResult = validateMessage(); + let validationResult = validateMessage() if (validationResult) { - e.preventDefault(); - alert(validationResult); - return false; + e.preventDefault() + alert(validationResult) + return false } - submitButton.disabled = true; - let formData = new FormData(form); + submitButton.disabled = true + let formData = new FormData(form) fetch('/api/comment' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, @@ -323,63 +318,62 @@ function showCommentForm(mid, rid) { if (response.ok) { response.json().then(result => { if (result.newMessage) { - window.location.hash = `#${result.newMessage.rid}`; + window.location.hash = `#${result.newMessage.rid}` } else { - alert(result.text); + alert(result.text) } - window.location.reload(); - }); + window.location.reload() + }) } }).catch(error => { - alert(error.message); - }); - e.preventDefault(); - }); + alert(error.message) + }) + e.preventDefault() + }) } - /** @type {HTMLTextAreaElement} */ (reply.querySelector('.msg-comment textarea')).focus(); + /** @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; + 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'); + let input = div.querySelector('input') if (input) { - input.remove(); - div.classList.remove('attach-photo-active'); + input.remove() + div.classList.remove('attach-photo-active') } else { - let newInput = attachInput(); + let newInput = attachInput() newInput.addEventListener('change', function() { - div.classList.add('attach-photo-active'); - }); - newInput.click(); - div.appendChild(newInput); + 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; + let width = window.innerWidth + let height = window.innerHeight + let minDimension = (width < height) ? width : height if (minDimension < 640) { - return true; // no dialog, open the link + 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; + 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; + openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/p/${fname}"/></a>`, true) + return false } } @@ -393,44 +387,44 @@ function openDialog(html, image) { </div> ${html} </div> - </div>`; - let body = /** @type {HTMLElement} */ (document.querySelector('body')); - body.classList.add('dialog-opened'); - body.insertAdjacentHTML('afterbegin', dialogHtml); + </div>` + 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'); + 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); + 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(); + 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; + style = /** @type {HTMLElement} */ (document.querySelector('#username')).style fetch('/api/users?uname=' + uname) .then(handleErrors) .then(function() { - style.background = '#FFCCCC'; + style.background = '#FFCCCC' }) .catch(function() { - style.background = '#CCFFCC'; - }); + style.background = '#CCFFCC' + }) } /******************************************************************************/ function resultMessage(str) { - var result = document.createElement('p'); - result.textContent = str; - return result; + var result = document.createElement('p') + result.textContent = str + return result } function likeMessage(e, mid) { @@ -443,14 +437,14 @@ function likeMessage(e, mid) { .then(handleErrors) .then(function(response) { if (response.ok) { - e.closest('article').appendChild(resultMessage('OK!')); + e.closest('article').appendChild(resultMessage('OK!')) } }) .catch(function() { - e.closest('article').appendChild(resultMessage(i18n('error.error'))); - }); + e.closest('article').appendChild(resultMessage(i18n('error.error'))) + }) } - return false; + return false } function subscribeMessage(e, mid) { @@ -462,15 +456,15 @@ function subscribeMessage(e, mid) { .then(handleErrors) .then(function(response) { if (response.ok) { - window.location.reload(); + window.location.reload() } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } }) .catch(error => { - alert(error.message); - }); - return false; + alert(error.message) + }) + return false } /******************************************************************************/ @@ -484,13 +478,13 @@ function setPrivacy(e, mid) { .then(handleErrors) .then(function(response) { if (response.ok) { - window.location.reload(); + window.location.reload() } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } }) - .catch(console.err); - return false; + .catch(console.err) + return false } function toggleWL(e, name) { fetch('/api/users/wl?name=' + name @@ -501,13 +495,13 @@ function toggleWL(e, name) { .then(handleErrors) .then(function(response) { if (response.ok) { - window.location.reload(); + window.location.reload() } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } }) - .catch(console.err); - return false; + .catch(console.err) + return false } function getTags() { @@ -516,41 +510,41 @@ function getTags() { }) .then(handleErrors) .then(response => { - return response.json(); + return response.json() }) .then(json => { - let tags = json.map(t => t.tag); - let input = document.getElementById('tags_input'); - }); - return false; + 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; + 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); + let data = new FormData() + data.append('uri', dataUri) fetch('/api/u/', { method: 'POST', body: data }).then(handleErrors) .then(response => { - return response.json(); + return response.json() }) .then(json => { - callback(json); + callback(json) }) .catch(e => { - callback({ uname: dataUri, uri: dataUri }); - }); + callback({ uname: dataUri, uri: dataUri }) + }) } const registerServiceWorker = () => { - const publicKey = 'BPU0LniKKR0QiaUvILPd9AystmSOU8rWDZobxKm7IJN5HYxOSQdktRdc74TZvyRS9_kyUz7LDN6gUAmAVOmObAU'; - navigator.serviceWorker.register('/sw.js', { scope: '/' }); + const publicKey = 'BPU0LniKKR0QiaUvILPd9AystmSOU8rWDZobxKm7IJN5HYxOSQdktRdc74TZvyRS9_kyUz7LDN6gUAmAVOmObAU' + navigator.serviceWorker.register('/sw.js', { scope: '/' }) navigator.serviceWorker.ready.then(reg => { reg.pushManager.subscribe({ userVisibleOnly: true, @@ -562,7 +556,7 @@ const registerServiceWorker = () => { type: 'web', token: JSON.stringify(sub) } - ]; + ] fetch('/api/notifications?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'PUT', headers: { @@ -571,30 +565,30 @@ const registerServiceWorker = () => { credentials: 'same-origin', body: JSON.stringify(body) }).then(response => { - console.log(response.status); - }); + console.log(response.status) + }) }, err => console.error(err) - ); - }); -}; + ) + }) +} function notificationsCheckPermissions(button) { - console.log(Notification.permission); + console.log(Notification.permission) switch (Notification.permission.toLowerCase()) { case 'granted': - button.innerHTML = `${i18n('settings.notifications.granted')}`; - button.disabled = true; - registerServiceWorker(); - break; + button.innerHTML = `${i18n('settings.notifications.granted')}` + button.disabled = true + registerServiceWorker() + break case 'denied': - button.innerHTML = `${i18n('settings.notifications.denied')}`; - button.disabled = true; - break; + button.innerHTML = `${i18n('settings.notifications.denied')}` + button.disabled = true + break case 'default': - button.innerHTML = `${i18n('settings.notifications.request')}`; + button.innerHTML = `${i18n('settings.notifications.request')}` } } @@ -602,77 +596,77 @@ function notificationsCheckPermissions(button) { function ready(fn) { if (document.readyState != 'loading') { - fn(); + fn() } else { - document.addEventListener('DOMContentLoaded', fn); + document.addEventListener('DOMContentLoaded', fn) } } ready(() => { - elementClosest(window); + elementClosest(window) Array.from(document.querySelectorAll('textarea')).forEach((ta) => { - autosize(ta); - }); - svg4everybody(); - renderIcons(); + autosize(ta) + }) + svg4everybody() + renderIcons() var insertPMButtons = function(e) { - e.target.classList.add('narrowpm'); - e.target.parentNode.insertAdjacentHTML('afterend', '<input type="submit" class="Button" value="OK"/>'); - e.target.removeEventListener('click', insertPMButtons); - e.preventDefault(); + e.target.classList.add('narrowpm') + e.target.parentNode.insertAdjacentHTML('afterend', '<input type="submit" class="Button" value="OK"/>') + e.target.removeEventListener('click', insertPMButtons) + e.preventDefault() }; /** @type {HTMLTextAreaElement[]} */ (Array.from(document.querySelectorAll('textarea.replypm'))).forEach(function(e) { - e.addEventListener('click', insertPMButtons); + e.addEventListener('click', insertPMButtons) e.addEventListener('keypress', function(e) { - postformListener(/** @type {HTMLElement} */(e.target), 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); - }); - }); + postformListener(/** @type {HTMLElement} */(e.target), e) + }) + }) - var content = document.getElementById('content'); + var content = document.getElementById('content') if (content) { - var pageMID = +content.getAttribute('data-mid'); + var pageMID = +content.getAttribute('data-mid') if (pageMID > 0) { Array.from(document.querySelectorAll('li.msg')).forEach(li => { - let showReplyFormBtn = li.querySelector('.a-thread-comment'); + let showReplyFormBtn = li.querySelector('.a-thread-comment') if (showReplyFormBtn) { showReplyFormBtn.addEventListener('click', function(e) { - showCommentForm(pageMID, li.id); - e.preventDefault(); - }); + showCommentForm(pageMID, li.id) + e.preventDefault() + }) } - }); - let opMessage = document.querySelector('.msgthread'); + }) + let opMessage = document.querySelector('.msgthread') if (opMessage) { - let replyTextarea = /** @type {HTMLTextAreaElement} */ (opMessage.querySelector('textarea.reply')); + 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)); + replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0)) + replyTextarea.addEventListener('keypress', e => postformListener(/** @type {HTMLElement} */(e.target), e)) if (!window.location.hash) { - replyTextarea.focus(); + replyTextarea.focus() } } } } } - var postmsg = /** @type {HTMLFormElement} */(document.getElementById('postmsg')); + 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)); + let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1)) if (params.has('tag')) { - addTag(params.get('tag')); - e.preventDefault(); + addTag(params.get('tag')) + e.preventDefault() } - }); - }); + }) + }) postmsg.addEventListener('submit', e => { - let formData = new FormData(postmsg); + let formData = new FormData(postmsg) fetch('/api/post' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, @@ -682,23 +676,23 @@ ready(() => { if (response.ok) { response.json().then(result => { if (result.newMessage) { - window.location.href = new URL(`/m/${result.newMessage.mid}`, window.location.href).href; + window.location.href = new URL(`/m/${result.newMessage.mid}`, window.location.href).href } else { - alert(result.text); + alert(result.text) } - }); + }) } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } }).catch(error => { - alert(error.message); - }); - e.preventDefault(); - }); + alert(error.message) + }) + e.preventDefault() + }) } /** @type {HTMLFormElement[]} */ (Array.from(document.querySelectorAll('.pmmsg'))).forEach(pmmsg => { pmmsg.addEventListener('submit', e => { - let formData = new FormData(pmmsg); + let formData = new FormData(pmmsg) fetch('/api/pm' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), { method: 'POST', body: formData, @@ -708,144 +702,144 @@ ready(() => { if (response.ok) { response.json().then(result => { if (result.to) { - window.location.href = new URL('/pm/sent', window.location.href).href; + window.location.href = new URL('/pm/sent', window.location.href).href } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } - }); + }) } else { - alert('Something went wrong :('); + alert('Something went wrong :(') } }).catch(error => { - alert(error.message); - }); - e.preventDefault(); - }); - }); + 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(); - }); - }); + 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(); - }); - }); + 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'); + let fname = /** @type {HTMLElement} */ (e.target).closest('[data-fname]').getAttribute('data-fname') if (!showPhotoDialog(fname)) { - e.preventDefault(); + e.preventDefault() } - }); - }); + }) + }) - var username = document.getElementById('username'); + var username = document.getElementById('username') if (username) { username.addEventListener('blur', function() { - checkUsername(); - }); + 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(); - }); - }); + /** @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'); + 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'; + e.style.display = 'block' }); /** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('#replies .msg-comments'))).forEach(function(e) { - e.style.display = 'none'; - }); - e.preventDefault(); - }); + 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'; + return a.textContent === 'NSFW' } )) { - article.classList.add('nsfw'); + article.classList.add('nsfw') } - }); + }) Array.from(document.querySelectorAll('[data-uri]')).forEach(el => { - let dataUri = el.getAttribute('data-uri') || ''; + let dataUri = el.getAttribute('data-uri') || '' if (dataUri) { setTimeout(() => fetchUserUri(dataUri, user => { - let header = el.closest('.msg-header'); + 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'); + 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); + img.setAttribute('src', user.avatar) + img.setAttribute('alt', user.uname) } - let textNode = a.childNodes[0]; + 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); + let uname = document.createTextNode(user.uname) + a.replaceChild(uname, a.firstChild) } - }); - }), 100); + }) + }), 100) } - }); + }) Array.from(document.querySelectorAll('[data-user-uri]')).forEach(el => { - let dataUri = el.getAttribute('href') || ''; + let dataUri = el.getAttribute('href') || '' if (dataUri) { setTimeout(() => fetchUserUri(dataUri, user => { - let textNode = el.childNodes[0]; + 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); + let uname = document.createTextNode(`@${user.uname}`) + el.replaceChild(uname, el.firstChild) + el.setAttribute('href', user.url || user.uri) } - }), 100); + }), 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'); + el.classList.add('active') + el.setAttribute('disabled', 'disabled') } - }); - initES(); + }) + initES() addEventListener('beforeunload', () => { if (es) { - es.close(); + es.close() } - }); - embedAll(); - const button = document.getElementById('notifications_toggle'); + }) + embedAll() + const button = document.getElementById('notifications_toggle') if (button) { button.addEventListener('click', () => { - Notification.requestPermission(function(result) { notificationsCheckPermissions(button); }); - }); - notificationsCheckPermissions(button); + Notification.requestPermission(function(result) { notificationsCheckPermissions(button) }) + }) + notificationsCheckPermissions(button) } -}); +}) diff --git a/src/main/resources/static/sw.js b/src/main/resources/static/sw.js index 0520bcc1..a64de7cc 100644 --- a/src/main/resources/static/sw.js +++ b/src/main/resources/static/sw.js @@ -2,46 +2,46 @@ /// <reference lib="esnext" /> /// <reference lib="WebWorker" /> -var sw = /** @type {ServiceWorkerGlobalScope & typeof self} */ (self); +var sw = /** @type {ServiceWorkerGlobalScope & typeof self} */ (self) sw.addEventListener('install', function() { - sw.skipWaiting(); -}); + sw.skipWaiting() +}) sw.addEventListener('activate', function() { - sw.clients.claim(); -}); + sw.clients.claim() +}) sw.addEventListener('push', function(evt) { if (evt.data) { - const data = evt.data.json(); - console.log('Push', data); + const data = evt.data.json() + console.log('Push', data) if (data.body) { sw.registration.showNotification(data.title, { body: data.body, icon: data.custom.message.user.avatar, tag: data.tag, data: data.custom.message - }); + }) } } -}); +}) sw.addEventListener('notificationclick', function(evt) { evt.waitUntil( sw.clients.matchAll().then(function(clientList) { - const message = evt.notification.data; + const message = evt.notification.data const url = message.mid ? message.rid ? `https://juick.com/m/${message.mid}#${message.rid}` : `https://juick.com/m/${message.mid}` - : 'https://juick.com/pm/inbox'; + : 'https://juick.com/pm/inbox' if (clientList.length > 0) { for (var i = 0; i < clientList.length; i++) { if (clientList[i].url === url) { - return clientList[i].focus(); + return clientList[i].focus() } } - clientList[0].navigate(url); - return clientList[0].focus(); + clientList[0].navigate(url) + return clientList[0].focus() } - return sw.clients.openWindow(url); + return sw.clients.openWindow(url) }) - ); -}); + ) +}) diff --git a/vnext/server/common/MessageUtils.js b/vnext/server/common/MessageUtils.js index b7ed8ec0..bb3d791f 100644 --- a/vnext/server/common/MessageUtils.js +++ b/vnext/server/common/MessageUtils.js @@ -3,7 +3,7 @@ * @param {import('../../src/api').Message} msg message */ export function isPM(msg) { - return !msg.mid; + return !msg.mid } /** @@ -11,7 +11,7 @@ * @param {import('../../src/api').Message} msg message */ export function isReply(msg) { - return msg.rid && msg.rid > 0; + return msg.rid && msg.rid > 0 } /** @@ -19,7 +19,7 @@ export function isReply(msg) { * @param {import('../../src/api').Message} msg message */ export function isService(msg) { - return msg.service && msg.service; + return msg.service && msg.service } /** @@ -29,11 +29,11 @@ export function isService(msg) { */ export function formatTitle(msg) { if (isReply(msg)) { - return `Reply by ${msg.user.uname}:`; + return `Reply by ${msg.user.uname}:` } else if (isPM(msg)) { - return `Private message from ${msg.user.uname}:`; + return `Private message from ${msg.user.uname}:` } - return `${msg.user.uname}`; + return `${msg.user.uname}` } /** @@ -43,11 +43,11 @@ export function formatTitle(msg) { */ export function formatQuote(msg) { if (isReply(msg)) { - return msg.replyQuote || ''; + return msg.replyQuote || '' } else if (isPM(msg)) { - return ''; + return '' } - return (msg.tags || []).map(t => `*${t}`).join(', '); + return (msg.tags || []).map(t => `*${t}`).join(', ') } /** @@ -56,19 +56,19 @@ export function formatQuote(msg) { * @returns {string} formatted body */ export function formatMessage(msg) { - return msg.body || 'Sent an image'; + return msg.body || 'Sent an image' } -const baseURL = 'https://juick.com'; +const baseURL = 'https://juick.com' /** * @param {import('../api').Message} msg message */ export function formatUrl(msg) { if (isReply(msg)) { - return `${baseURL}/m/${msg.mid}#${msg.rid}`; + return `${baseURL}/m/${msg.mid}#${msg.rid}` } else if (isPM(msg)) { - return `${baseURL}/pm/inbox`; + return `${baseURL}/pm/inbox` } - return `${baseURL}/m/${msg.mid}`; + return `${baseURL}/m/${msg.mid}` } diff --git a/vnext/server/common/MessageUtils.spec.js b/vnext/server/common/MessageUtils.spec.js index 5e6f64c2..33336795 100644 --- a/vnext/server/common/MessageUtils.spec.js +++ b/vnext/server/common/MessageUtils.spec.js @@ -1,4 +1,4 @@ -import { formatTitle, formatMessage, formatQuote } from './MessageUtils'; +import { formatTitle, formatMessage, formatQuote } from './MessageUtils' describe('Message formatting', () => { it('Blog message', () => { @@ -13,11 +13,11 @@ describe('Message formatting', () => { 'people' ], 'body': 'The message' - }; - expect(formatTitle(msg)).toMatchSnapshot(); - expect(formatQuote(msg)).toMatchSnapshot(); - expect(formatMessage(msg)).toMatchSnapshot(); - }); + } + expect(formatTitle(msg)).toMatchSnapshot() + expect(formatQuote(msg)).toMatchSnapshot() + expect(formatMessage(msg)).toMatchSnapshot() + }) it('Reply message', () => { let msg = { 'mid': 1, @@ -28,11 +28,11 @@ describe('Message formatting', () => { }, 'replyQuote': '> The message', 'body': 'The #reply #bla' - }; - expect(formatTitle(msg)).toMatchSnapshot(); - expect(formatQuote(msg)).toMatchSnapshot(); - expect(formatMessage(msg)).toMatchSnapshot(); - }); + } + expect(formatTitle(msg)).toMatchSnapshot() + expect(formatQuote(msg)).toMatchSnapshot() + expect(formatMessage(msg)).toMatchSnapshot() + }) it('PM', () => { let msg = { 'user': { @@ -40,8 +40,8 @@ describe('Message formatting', () => { 'uname': 'ugnich' }, 'body': 'The PM' - }; - expect(formatTitle(msg)).toMatchSnapshot(); - expect(formatMessage(msg)).toMatchSnapshot(); - }); -}); + } + expect(formatTitle(msg)).toMatchSnapshot() + expect(formatMessage(msg)).toMatchSnapshot() + }) +}) diff --git a/vnext/server/durov.js b/vnext/server/durov.js index e9bbdb9a..6ab1831f 100644 --- a/vnext/server/durov.js +++ b/vnext/server/durov.js @@ -1,52 +1,52 @@ -import TelegramBot from 'node-telegram-bot-api'; -import debug from 'debug'; -var log = debug('durov'); -import config from 'config'; +import TelegramBot from 'node-telegram-bot-api' +import debug from 'debug' +var log = debug('durov') +import config from 'config' -import { formatQuote, formatTitle } from './common/MessageUtils'; -import { format } from '../src/utils/embed'; +import { formatQuote, formatTitle } from './common/MessageUtils' +import { format } from '../src/utils/embed' -let sender; -let demouser; -let bot_token; -const durov_token_key = 'service.durov.token'; +let sender +let demouser +let bot_token +const durov_token_key = 'service.durov.token' if (config.has(durov_token_key)) { - bot_token = config.get(durov_token_key); - sender = new TelegramBot(bot_token); - demouser = config.get('service.durov.demouser'); + bot_token = config.get(durov_token_key) + sender = new TelegramBot(bot_token) + demouser = config.get('service.durov.demouser') sender.setWebHook(`${config.get('service.baseURL')}/api/${bot_token}`).then(() => { - log('Webhook is set'); - }).catch(console.log); + log('Webhook is set') + }).catch(console.log) sender.on('message', msg => { - log(`MESSAGE: ${JSON.stringify(msg)}`); - }); + log(`MESSAGE: ${JSON.stringify(msg)}`) + }) } export const webhookPath = () => { - return bot_token; -}; + return bot_token +} export const webhook = (req, res) => { - sender.processUpdate(JSON.parse(req.body)); - res.sendStatus(200); -}; + sender.processUpdate(JSON.parse(req.body)) + res.sendStatus(200) +} /** * @param {import('../src/api').Message} msg * @param {string[]} subscribers */ export const sendTelegramNotification = (msg, subscribers) => { - log(`Telegram event: ${JSON.stringify(msg)}, ${subscribers} ${subscribers.length}`); + log(`Telegram event: ${JSON.stringify(msg)}, ${subscribers} ${subscribers.length}`) if (!msg.service) { if (subscribers && subscribers.includes(demouser)) { - const message = `${formatTitle(msg)}\n${formatQuote(msg)}\n${format(msg.body, msg.uid, false)}`; - log(message); + const message = `${formatTitle(msg)}\n${formatQuote(msg)}\n${format(msg.body, msg.uid, false)}` + log(message) sender.sendMessage(demouser, message, { parse_mode: 'HTML', disable_web_page_preview: true - }).then(log).catch(log); + }).then(log).catch(log) } } -}; +} diff --git a/vnext/server/hms.js b/vnext/server/hms.js index 1f897cef..66bbad82 100644 --- a/vnext/server/hms.js +++ b/vnext/server/hms.js @@ -1,32 +1,32 @@ -import axios from 'axios'; -import config from 'config'; -import debug from 'debug'; -var log = debug('hms'); +import axios from 'axios' +import config from 'config' +import debug from 'debug' +var log = debug('hms') -const { client_id, client_secret } = config.has('service.hms') ? config.get('service.hms') : {}; +const { client_id, client_secret } = config.has('service.hms') ? config.get('service.hms') : {} const refreshToken = async () => { if (client_id) { - const params = new URLSearchParams(); - params.append('grant_type', 'client_credentials'); - params.append('client_id', client_id); - params.append('client_secret', client_secret); - const res = await axios.post('https://oauth-login.cloud.huawei.com/oauth2/v3/token', params).catch(console.log); + const params = new URLSearchParams() + params.append('grant_type', 'client_credentials') + params.append('client_id', client_id) + params.append('client_secret', client_secret) + const res = await axios.post('https://oauth-login.cloud.huawei.com/oauth2/v3/token', params).catch(console.log) try { - log(`HMS response: ${JSON.stringify(res.data)}`); - const access = res.data; - log(`HMS access token: ${access.access_token}`); - return access.access_token; + log(`HMS response: ${JSON.stringify(res.data)}`) + const access = res.data + log(`HMS access token: ${access.access_token}`) + return access.access_token } catch (error) { - log(error); - return ''; + log(error) + return '' } } - return ''; -}; + return '' +} export const send = async (msg, tokenList = []) => { - const adminToken = await refreshToken(); + const adminToken = await refreshToken() if (adminToken) { const response = await axios.post(`https://push-api.cloud.huawei.com/v1/${client_id}/messages:send`, { headers: { @@ -52,7 +52,7 @@ export const send = async (msg, tokenList = []) => { 'token': tokenList } } - }).catch(log); - log(`hcm: ${response.status}`); + }).catch(log) + log(`hcm: ${response.status}`) } -}; +} diff --git a/vnext/server/http.js b/vnext/server/http.js index 0ffa8bfe..a90afef3 100644 --- a/vnext/server/http.js +++ b/vnext/server/http.js @@ -1,7 +1,7 @@ -import axios from 'axios'; -import config from 'config'; -import debug from 'debug'; -var log = debug('http'); +import axios from 'axios' +import config from 'config' +import debug from 'debug' +var log = debug('http') /** @@ -10,7 +10,6 @@ var log = debug('http'); */ /** * fetch message subscribers - * * @param {URLSearchParams} params - request params * @returns {Promise<import('../client').SecureUser[]>} subscribers list */ @@ -18,26 +17,25 @@ export function subscribers(params) { return new Promise((resolve, reject) => { client.get(`/notifications?${params.toString()}`) .then(response => { - log(`CODE: ${response.status}`); - resolve(response.data); + log(`CODE: ${response.status}`) + resolve(response.data) }) - .catch(reason => { reject(reason); }); - }); + .catch(reason => { reject(reason) }) + }) } /** * delete invalid tokens - * * @param {import('../client').Token[]} tokens tokens */ export const deleteSubscribers = async (tokens) => { - const response = await client.post('/notifications/delete', tokens); - return response.data; -}; + const response = await client.post('/notifications/delete', tokens) + return response.data +} -const baseURL = config.get('service.baseURL') + '/api'; -const user = config.get('service.user') || process.env.JUICK_SERVICE_USER; -const password = config.get('service.password') || process.env.JUICK_SERVICE_PASSWORD; +const baseURL = config.get('service.baseURL') + '/api' +const user = config.get('service.user') || process.env.JUICK_SERVICE_USER +const password = config.get('service.password') || process.env.JUICK_SERVICE_PASSWORD /** @type { import('axios').AxiosInstance } */ let client = axios.create({ @@ -45,5 +43,5 @@ let client = axios.create({ headers: user ? { 'Authorization': 'Basic ' + Buffer.from(user + ':' + password).toString('base64') } : {} -}); -log(`HTTP client initialized with base URL ${baseURL} ${ user? `and ${user} user` : ''}`); +}) +log(`HTTP client initialized with base URL ${baseURL} ${ user? `and ${user} user` : ''}`) diff --git a/vnext/server/index.js b/vnext/server/index.js index 5bfa9070..8eb86981 100644 --- a/vnext/server/index.js +++ b/vnext/server/index.js @@ -1,53 +1,53 @@ -import express from 'express'; -import { raw } from 'body-parser'; -import cors from 'cors'; -import config from 'config'; -import debug from 'debug'; -const log = debug('http'); - -import serverRenderer from './middleware/renderer'; -import event from './middleware/event'; -import oembed from './middleware/oembed'; -import urlExpand from './middleware/urlexpand'; - -const PORT = 8081; -import path from 'path'; -import { webhook, webhookPath } from './durov'; +import express from 'express' +import { raw } from 'body-parser' +import cors from 'cors' +import config from 'config' +import debug from 'debug' +const log = debug('http') + +import serverRenderer from './middleware/renderer' +import event from './middleware/event' +import oembed from './middleware/oembed' +import urlExpand from './middleware/urlexpand' + +const PORT = 8081 +import path from 'path' +import { webhook, webhookPath } from './durov' // initialize the application and create the routes -const app = express(); -app.use(raw()); -app.use(cors()); -const router = express.Router(); +const app = express() +app.use(raw()) +app.use(cors()) +const router = express.Router() -router.post('/api/sender', event); -router.get('/api/oembed', oembed); -router.get('/api/urlexpand', urlExpand); +router.post('/api/sender', event) +router.get('/api/oembed', oembed) +router.get('/api/urlexpand', urlExpand) -const durov_webhook = webhookPath(); +const durov_webhook = webhookPath() if (durov_webhook) { - router.post(`/api/${durov_webhook}`, webhook); + router.post(`/api/${durov_webhook}`, webhook) } -router.use('^/$', serverRenderer); +router.use('^/$', serverRenderer) -const STATIC_ROOT = config.get('service.static_root') || path.resolve(__dirname, 'public'); +const STATIC_ROOT = config.get('service.static_root') || path.resolve(__dirname, 'public') // other static resources should just be served as they are router.use(express.static( STATIC_ROOT, { maxAge: '30d' }, -)); +)) -router.use('*', serverRenderer); +router.use('*', serverRenderer) -app.use(router); +app.use(router) // start the app app.listen(PORT, (error) => { if (error) { - return console.log('something bad happened', error); + return console.log('something bad happened', error) } - log('listening on ' + PORT + '...'); -}); + log('listening on ' + PORT + '...') +}) diff --git a/vnext/server/middleware/event.js b/vnext/server/middleware/event.js index 5681eb59..59f36950 100644 --- a/vnext/server/middleware/event.js +++ b/vnext/server/middleware/event.js @@ -1,54 +1,54 @@ -import { simpleParser } from 'mailparser'; -import { isPM, isReply, isService } from '../common/MessageUtils'; -import { sendTelegramNotification } from '../durov'; -import { subscribers } from '../http'; -import { sendNotification, buildNotification } from '../sender'; -import debug from 'debug'; -import { send } from '../hms'; -var log = debug('event'); +import { simpleParser } from 'mailparser' +import { isPM, isReply, isService } from '../common/MessageUtils' +import { sendTelegramNotification } from '../durov' +import { subscribers } from '../http' +import { sendNotification, buildNotification } from '../sender' +import debug from 'debug' +import { send } from '../hms' +var log = debug('event') /** @type {number[]} */ -const allSandboxIds = []; +const allSandboxIds = [] /** * handle message event * @param {import('../../client').Message} msg message */ function processMessageEvent(msg) { - let params = {}; - params.uid = isPM(msg) ? msg.to.uid : msg.user.uid; + let params = {} + params.uid = isPM(msg) ? msg.to.uid : msg.user.uid if (isReply(msg)) { - params.mid = msg.mid; - params.rid = msg.rid; + params.mid = msg.mid + params.rid = msg.rid } else if (!isPM(msg) && !isService(msg)) { - params.mid = msg.mid; + params.mid = msg.mid } subscribers(new URLSearchParams(JSON.parse(JSON.stringify(params)))).then(users => { users.forEach(user => { - log(`${user.uname}: ${user.unreadCount}`); + log(`${user.uname}: ${user.unreadCount}`) let [sandboxTokens, productionTokens] = (user.tokens || []) .filter(t => ['mpns', 'apns', 'fcm', 'web'].includes(t.type)) .map(t => t.type === 'web' ? JSON.parse(t.token) : t.token) .reduce((result, element, i) => { allSandboxIds.includes(user.uid) ? result[0].push(element) - : result[1].push(element); - return result; - }, [[], []]); - sendNotification(productionTokens, sandboxTokens, buildNotification(user, msg)); + : result[1].push(element) + return result + }, [[], []]) + sendNotification(productionTokens, sandboxTokens, buildNotification(user, msg)) let durovIds = (user.tokens || []) .filter(t => ['durov'].includes(t.type)) - .map(t => t.token); - sendTelegramNotification(msg, durovIds); + .map(t => t.token) + sendTelegramNotification(msg, durovIds) let hmsIds = (user.tokens || []) .filter(t => t.type === 'hcm') - .map(t => t.token); - log(`${user.uname}: ${hmsIds}`); + .map(t => t.token) + log(`${user.uname}: ${hmsIds}`) send(msg, hmsIds).then(() => { // TODO: handle results - }).catch(log); - }); - }).catch(log); + }).catch(log) + }) + }).catch(log) } /** @@ -58,30 +58,30 @@ function processMessageEvent(msg) { export default function handleMessage(req, res) { return simpleParser(req.body, {}) .then(parsed => { - const new_version = parsed.headers.get('x-event-version') == '1.0'; - log(`New event: ${parsed.text}, new version: ${new_version}`); + const new_version = parsed.headers.get('x-event-version') == '1.0' + log(`New event: ${parsed.text}, new version: ${new_version}`) if (new_version) { /** @type {import('../../client').SystemEvent} */ - const event = JSON.parse(parsed.text || ''); + const event = JSON.parse(parsed.text || '') if (event.type === 'message' && event.message) { if (event.message.service) { // TODO: remove - let msg = { ...event.message }; + let msg = { ...event.message } if (event.from) { - msg.user = event.from; + msg.user = event.from } - processMessageEvent(msg); + processMessageEvent(msg) } else { - processMessageEvent(event.message); + processMessageEvent(event.message) } } } else { /** @type {import('../../client').Message} */ - const msg = JSON.parse(parsed.text || ''); - processMessageEvent(msg); + const msg = JSON.parse(parsed.text || '') + processMessageEvent(msg) } - res.end(); + res.end() }) - .catch(err => { log(err); res.status(400).send('Invalid request'); }); + .catch(err => { log(err); res.status(400).send('Invalid request') }) } diff --git a/vnext/server/middleware/oembed.js b/vnext/server/middleware/oembed.js index 8b1ed61d..5d062228 100644 --- a/vnext/server/middleware/oembed.js +++ b/vnext/server/middleware/oembed.js @@ -1,19 +1,19 @@ -import { embeddedTweet } from '../../src/api'; +import { embeddedTweet } from '../../src/api' /** * Return content for embedding * @type {import('express').RequestParamHandler} */ const oembed = async (req, res) => { - let url = (req.query.url || '').toString(); + let url = (req.query.url || '').toString() return embeddedTweet(url).then(result => { - res.send(result); - res.end(); + res.send(result) + res.end() }).catch(err => { - console.log(`HTTP ${err.response ? err.response.status : err.code}: ${url}`); - res.sendStatus(err.response ? err.response.status : 500); - res.end(); - }); -}; + console.log(`HTTP ${err.response ? err.response.status : err.code}: ${url}`) + res.sendStatus(err.response ? err.response.status : 500) + res.end() + }) +} -export default oembed; +export default oembed diff --git a/vnext/server/middleware/renderer.js b/vnext/server/middleware/renderer.js index debba758..e7e0324e 100644 --- a/vnext/server/middleware/renderer.js +++ b/vnext/server/middleware/renderer.js @@ -1,54 +1,54 @@ -import * as ReactDOMServer from 'react-dom/server'; -import cookie from 'cookie'; -import config from 'config'; +import * as ReactDOMServer from 'react-dom/server' +import cookie from 'cookie' +import config from 'config' // import our main App component -import App from '../../src/App'; +import App from '../../src/App' -import { getLinks } from '../sape'; -import { StaticRouter } from 'react-router-dom/server'; -import { VisitorProvider } from '../../src/ui/VisitorContext'; +import { getLinks } from '../sape' +import { StaticRouter } from 'react-router-dom/server' +import { VisitorProvider } from '../../src/ui/VisitorContext' -import path from 'path'; -import fs from 'fs'; +import path from 'path' +import fs from 'fs' // convert a Unicode string to a string in which // each 16-bit unit occupies only one byte function toBinary(string) { - const codeUnits = new Uint16Array(string.length); + const codeUnits = new Uint16Array(string.length) for (let i = 0; i < codeUnits.length; i++) { - codeUnits[i] = string.charCodeAt(i); + codeUnits[i] = string.charCodeAt(i) } - return Buffer.from(String.fromCharCode(...new Uint8Array(codeUnits.buffer))).toString('base64'); + return Buffer.from(String.fromCharCode(...new Uint8Array(codeUnits.buffer))).toString('base64') } -const STATIC_ROOT = config.get('service.static_root') || path.resolve(__dirname, 'public'); +const STATIC_ROOT = config.get('service.static_root') || path.resolve(__dirname, 'public') const serverRenderer = async (req, res) => { // point to the html file created by CRA's build tool - const filePath = path.resolve(STATIC_ROOT, 'index.html'); + const filePath = path.resolve(STATIC_ROOT, 'index.html') // links - const cookies = cookie.parse(req.headers.cookie || ''); + const cookies = cookie.parse(req.headers.cookie || '') - const links = await getLinks(req.originalUrl, cookies['sape_cookie']); + const links = await getLinks(req.originalUrl, cookies['sape_cookie']) fs.readFile(filePath, 'utf8', (err, htmlData) => { if (err) { - console.error('err', err); - return res.status(404).end(); + console.error('err', err) + return res.status(404).end() } - const routerContext = {}; + const routerContext = {} const props = { footer: links.join(' ') - }; + } - const marker = '<div id="app">'; - const data = htmlData.split(marker); - const propsData = `<script>window.__PROPS__="${toBinary(JSON.stringify(props))}";</script>${marker}`; - let didError = false; + const marker = '<div id="app">' + const data = htmlData.split(marker) + const propsData = `<script>window.__PROPS__="${toBinary(JSON.stringify(props))}";</script>${marker}` + let didError = false const { pipe } = ReactDOMServer.renderToPipeableStream( <VisitorProvider> <StaticRouter location={req.baseUrl} context={routerContext}> @@ -57,33 +57,33 @@ const serverRenderer = async (req, res) => { </VisitorProvider> , { onShellReady() { - res.statusCode = didError ? 500 : 200; - res.setHeader('Content-type', 'text/html'); - res.write(data[0]); - res.write(propsData); - pipe(res, { end: false }); + res.statusCode = didError ? 500 : 200 + res.setHeader('Content-type', 'text/html') + res.write(data[0]) + res.write(propsData) + pipe(res, { end: false }) }, onShellError() { - didError = true; - res.statusCode = 500; - res.setHeader('Content-type', 'text/html'); + didError = true + res.statusCode = 500 + res.setHeader('Content-type', 'text/html') res.send( '<h1>Something went wrong :(</h1>' - ); - res.end(); + ) + res.end() }, onAllReady() { if (!didError) { - res.write(data[1]); + res.write(data[1]) } - res.end(); + res.end() }, onError(err) { - didError = true; - console.log(err); + didError = true + console.log(err) } - }); - }); -}; + }) + }) +} -export default serverRenderer; +export default serverRenderer diff --git a/vnext/server/middleware/urlexpand.js b/vnext/server/middleware/urlexpand.js index f78d1cc0..a99f80a7 100644 --- a/vnext/server/middleware/urlexpand.js +++ b/vnext/server/middleware/urlexpand.js @@ -1,4 +1,4 @@ -import { expandShortenedLink } from '../../src/api'; +import { expandShortenedLink } from '../../src/api' /** * Expand URLs @@ -6,8 +6,8 @@ import { expandShortenedLink } from '../../src/api'; * @param {import("next").NextApiResponse} res */ export default function urlExpand(req, res) { - let url = (req.query.url || '').toString(); + let url = (req.query.url || '').toString() return expandShortenedLink(url).then(result => { - res.json(result); - }); + res.json(result) + }) } diff --git a/vnext/server/sape.js b/vnext/server/sape.js index ad374f2b..cff9b48a 100644 --- a/vnext/server/sape.js +++ b/vnext/server/sape.js @@ -1,9 +1,9 @@ -import { parseStringPromise } from 'xml2js'; -import axios from 'axios'; -import { setupCache } from 'axios-cache-interceptor'; -import config from 'config'; +import { parseStringPromise } from 'xml2js' +import axios from 'axios' +import { setupCache } from 'axios-cache-interceptor' +import config from 'config' -const token = config.get('service.sape.token') || process.env.SAPE_TOKEN; +const token = config.get('service.sape.token') || process.env.SAPE_TOKEN /** @external Promise */ @@ -14,19 +14,19 @@ const token = config.get('service.sape.token') || process.env.SAPE_TOKEN; */ export const getLinks = async (uri, sapeCookie) => { if (!token) { - console.warn('Sape is not configured'); - return []; + console.warn('Sape is not configured') + return [] } - const response = await sape.get(`http://dispencer-01.sape.ru/code.php?user=${token}&host=juick.com&charset=UTF-8&as_xml=true`); - const data = await parseStringPromise(response.data); - const showCode = token === sapeCookie; - const requestURI = showCode ? '*' : uri; + const response = await sape.get(`http://dispencer-01.sape.ru/code.php?user=${token}&host=juick.com&charset=UTF-8&as_xml=true`) + const data = await parseStringPromise(response.data) + const showCode = token === sapeCookie + const requestURI = showCode ? '*' : uri const page = data.sape.page.filter(page => { - const uri = page['$']['uri']; - return uri === requestURI; - }); - return page.length > 0 ? showCode ? [page[0]._] : page[0].link : []; -}; + const uri = page['$']['uri'] + return uri === requestURI + }) + return page.length > 0 ? showCode ? [page[0]._] : page[0].link : [] +} /** @type { import('axios-cache-interceptor').AxiosCacheInstance } */ let sape = setupCache( @@ -36,4 +36,4 @@ let sape = setupCache( } }), { ttl: 3600 * 1000 } -); +) diff --git a/vnext/server/sender.js b/vnext/server/sender.js index 7c72cbf3..48b5fb78 100644 --- a/vnext/server/sender.js +++ b/vnext/server/sender.js @@ -1,11 +1,11 @@ -import PushNotifications from 'node-pushnotifications'; -import debug from 'debug'; -const log = debug('sender'); -import { deleteSubscribers } from './http'; -import { formatMessage, formatTitle, formatQuote } from './common/MessageUtils'; -import config from 'config'; +import PushNotifications from 'node-pushnotifications' +import debug from 'debug' +const log = debug('sender') +import { deleteSubscribers } from './http' +import { formatMessage, formatTitle, formatQuote } from './common/MessageUtils' +import config from 'config' -let cfg = /** @type { import('node-pushnotifications').Settings } */ (config); +let cfg = /** @type { import('node-pushnotifications').Settings } */ (config) const apnConfig = (production = true) => { const apn = { @@ -16,98 +16,96 @@ const apnConfig = (production = true) => { teamId: cfg.apn?.token?.teamId || process.env.JUICK_APN_TEAM_ID }, production: production - }; - return apn; -}; + } + return apn +} const gcmConfig = { ...cfg.gcm, id: cfg.gcm?.id || process.env.JUICK_GCM_ID -}; +} const push = new PushNotifications({ ...config, apn: apnConfig(true), gcm: gcmConfig, -}); +}) const sandbox = new PushNotifications({ ...config, apn: apnConfig(false), gcm: gcmConfig -}); +}) /** @type {string} */ -const application = config.get('service.application') || process.env.JUICK_APN_APPLICATION || ''; +const application = config.get('service.application') || process.env.JUICK_APN_APPLICATION || '' /** * send notification - * * @param {PushNotifications.RegistrationId[]} productionIds * @param {PushNotifications.RegistrationId[]} sandboxIds * @param {PushNotifications.Data} data */ export function sendNotification(productionIds, sandboxIds, data) { [productionIds, sandboxIds].map((registrationIds, index) => { - let sender = index == 0 ? push : sandbox; + let sender = index == 0 ? push : sandbox if (registrationIds && registrationIds.length) { sender.send(registrationIds, data) .then((results) => { results.forEach(result => { - log(`${result.method}: ${result.success} success, ${result.failure} failure`); + log(`${result.method}: ${result.success} success, ${result.failure} failure`) if (result.failure) { - console.error(`${result.method} failure: ${JSON.stringify(result)}`); - console.error(`Failed data: ${JSON.stringify(data)}`); + console.error(`${result.method} failure: ${JSON.stringify(result)}`) + console.error(`Failed data: ${JSON.stringify(data)}`) } - }); + }) results.filter(r => r.method === 'apn') .forEach(async r => { - log(`Response message: ${JSON.stringify(r.message)}`); + log(`Response message: ${JSON.stringify(r.message)}`) let badTokens = r.message.filter(m => m.errorMsg === 'BadDeviceToken').map(m => { - return { 'type': 'apns', 'token': m.regId }; - }); + return { 'type': 'apns', 'token': m.regId } + }) if (badTokens.length > 0) { - await deleteSubscribers(badTokens); - log(`${badTokens.length} APNS tokens deleted`); + await deleteSubscribers(badTokens) + log(`${badTokens.length} APNS tokens deleted`) } - }); + }) results.filter(r => r.method === 'gcm') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'NotRegistered' || m.errorMsg === 'MismatchSenderId' || m.errorMsg === 'InvalidRegistration').map(m => { - return { 'type': 'fcm', 'token': m.regId }; - }); + return { 'type': 'fcm', 'token': m.regId } + }) if (badTokens.length > 0) { - await deleteSubscribers(badTokens); - log(`${badTokens.length} GCM tokens deleted`); + await deleteSubscribers(badTokens) + log(`${badTokens.length} GCM tokens deleted`) } - }); + }) results.filter(r => r.method === 'mpns') .forEach(async r => { let badTokens = r.message.filter(m => m.errorMsg === 'The channel expired.').map(m => { - return { 'type': 'mpns', 'token': m.regId }; - }); + return { 'type': 'mpns', 'token': m.regId } + }) if (badTokens.length > 0) { - await deleteSubscribers(badTokens); - log(`${badTokens.length} MPNS tokens deleted`); + await deleteSubscribers(badTokens) + log(`${badTokens.length} MPNS tokens deleted`) } - }); + }) results.filter(r => r.method === 'webPush') .forEach(async r => { let badTokens = r.message.filter(m => m.error && m.error['statusCode'] === 410).map(m => { - return { 'type': 'web', 'token': JSON.stringify(m.regId) }; - }); + return { 'type': 'web', 'token': JSON.stringify(m.regId) } + }) if (badTokens.length > 0) { - await deleteSubscribers(badTokens); - log(`${badTokens.length} WebPush tokens deleted`); + await deleteSubscribers(badTokens) + log(`${badTokens.length} WebPush tokens deleted`) } - }); + }) }) - .catch((err) => { console.error(JSON.stringify(err)); }); + .catch((err) => { console.error(JSON.stringify(err)) }) } - }); + }) } /** * builds notification object - * * @param {import('../client').SecureUser} user user * @param {import('../client').Message} msg message * @returns {PushNotifications.Data} notification template @@ -119,33 +117,33 @@ export function buildNotification(user, msg) { message: msg }, timeToLive: 0 - }; - let { tokens, ...subscriber } = user; + } + let { tokens, ...subscriber } = user if (msg.service) { - template.contentAvailable = true; - template.custom.service = true; - template.custom.user = subscriber; + template.contentAvailable = true + template.custom.service = true + template.custom.user = subscriber } else { - const avatar = `https://i.juick.com/a/${msg.user.uid}.png`; - const title = formatTitle(msg); - const body = `${formatQuote(msg)}\n${formatMessage(msg)}`; - template.custom.mid = msg.mid; - template.custom.rid = msg.rid; - template.custom.uname = msg.user.uname; - template.custom.avatarUrl = avatar; - template.image1src = avatar; - template.text1 = title; - template.text2 = body; - template.title = title; - template.body = body; - template.badge = user.unreadCount || 0; - template.mutableContent = 1; - template.color = '#3c77aa'; - template.icon = 'ic_notification'; - template.clickAction = 'com.juick.NEW_EVENT_ACTION'; - const tag = msg.mid == 0 ? msg.user.uname : msg.mid; - template.tag = `${tag}`; - template.android_channel_id = 'default'; + const avatar = `https://i.juick.com/a/${msg.user.uid}.png` + const title = formatTitle(msg) + const body = `${formatQuote(msg)}\n${formatMessage(msg)}` + template.custom.mid = msg.mid + template.custom.rid = msg.rid + template.custom.uname = msg.user.uname + template.custom.avatarUrl = avatar + template.image1src = avatar + template.text1 = title + template.text2 = body + template.title = title + template.body = body + template.badge = user.unreadCount || 0 + template.mutableContent = 1 + template.color = '#3c77aa' + template.icon = 'ic_notification' + template.clickAction = 'com.juick.NEW_EVENT_ACTION' + const tag = msg.mid == 0 ? msg.user.uname : msg.mid + template.tag = `${tag}` + template.android_channel_id = 'default' } - return template; + return template } diff --git a/vnext/server/webpack.config.js b/vnext/server/webpack.config.js index 61f00d4d..77a86abd 100644 --- a/vnext/server/webpack.config.js +++ b/vnext/server/webpack.config.js @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const ESLintPlugin = require('eslint-webpack-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); +const ESLintPlugin = require('eslint-webpack-plugin') +const TerserPlugin = require('terser-webpack-plugin') module.exports = () => { - const node_env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; - const dev = node_env !== 'production'; + const node_env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development' + const dev = node_env !== 'production' const config = { mode: node_env, devtool: dev ? 'cheap-module-source-map' : false, @@ -37,7 +37,7 @@ module.exports = () => { symlinks: false, extensions: ['.js'] } - }; + } if (dev) { config.plugins.push( new ESLintPlugin({ @@ -46,7 +46,7 @@ module.exports = () => { failOnWarning: false, failOnError: true, fix: false - })); + })) config.devServer = { hot: true, historyApiFallback: true, @@ -55,7 +55,7 @@ module.exports = () => { runtimeErrors: true } } - }; + } } config.optimization = { minimize: !dev, @@ -67,6 +67,6 @@ module.exports = () => { terserOptions: {}, }), ] - }; - return config; -}; + } + return config +} diff --git a/vnext/src/App.js b/vnext/src/App.js index 125b6fda..e723fe9c 100644 --- a/vnext/src/App.js +++ b/vnext/src/App.js @@ -1,25 +1,25 @@ -import { useState, useEffect, useRef, Fragment, useCallback } from 'react'; -import { Route, Link, Routes, useSearchParams } from 'react-router-dom'; +import { useState, useEffect, useRef, Fragment, useCallback } from 'react' +import { Route, Link, Routes, useSearchParams } from 'react-router-dom' -import svg4everybody from 'svg4everybody'; +import svg4everybody from 'svg4everybody' -import Icon from './ui/Icon'; -import { Discover, Discussions, Blog, Tag, Home } from './ui/Feeds'; -import { Friends, Readers } from './ui/Users'; -import Settings from './ui/Settings'; -import Contacts from './ui/Contacts'; -import Chat from './ui/Chat'; -import Header from './ui/Header'; -import Post from './ui/Post'; -import Thread from './ui/Thread'; -import Login from './ui/Login'; +import Icon from './ui/Icon' +import { Discover, Discussions, Blog, Tag, Home } from './ui/Feeds' +import { Friends, Readers } from './ui/Users' +import Settings from './ui/Settings' +import Contacts from './ui/Contacts' +import Chat from './ui/Chat' +import Header from './ui/Header' +import Post from './ui/Post' +import Thread from './ui/Thread' +import Login from './ui/Login' -import { useCookies } from 'react-cookie'; +import { useCookies } from 'react-cookie' -import { me, trends } from './api'; -import { useVisitor } from './ui/VisitorContext'; -import Avatar from './ui/Avatar'; -import { Toaster } from 'react-hot-toast'; +import { me, trends } from './api' +import { useVisitor } from './ui/VisitorContext' +import Avatar from './ui/Avatar' +import { Toaster } from 'react-hot-toast' /** * @@ -29,34 +29,34 @@ import { Toaster } from 'react-hot-toast'; */ export default function App({ footer }) { - let contentRef = useRef(null); - const [cookie, setCookie] = useCookies(['hash']); + let contentRef = useRef(null) + const [cookie, setCookie] = useCookies(['hash']) - const [allTrends, setAllTrends] = useState([]); + const [allTrends, setAllTrends] = useState([]) - const [visitor, setVisitor] = useVisitor(); + const [visitor, setVisitor] = useVisitor() - const params = useSearchParams(); + const params = useSearchParams() useEffect(() => { - svg4everybody(); + svg4everybody() if (params['hash']) { - setCookie('hash', params['hash'], { path: '/' }); - let retpath = params['retpath'] || `${window.location.protocol}//${window.location.host}${window.location.pathname}`; - window.history.replaceState({}, document.title, retpath); + setCookie('hash', params['hash'], { path: '/' }) + let retpath = params['retpath'] || `${window.location.protocol}//${window.location.host}${window.location.pathname}` + window.history.replaceState({}, document.title, retpath) } - }, [setCookie, footer, params]); + }, [setCookie, footer, params]) let updateStatus = useCallback(() => { // refresh server visitor state (unread counters) me().then(visitor => { - setVisitor(visitor); - }).catch(console.error); - }, [setVisitor]); + setVisitor(visitor) + }).catch(console.error) + }, [setVisitor]) - const [hash, setHash] = useState(cookie.hash); + const [hash, setHash] = useState(cookie.hash) - const [eventSource, setEventSource] = /** @param EventSource? */ useState({}); + const [eventSource, setEventSource] = /** @param EventSource? */ useState({}) /** * @param {import("./api").SecureUser} visitor @@ -64,53 +64,53 @@ export default function App({ footer }) { let auth = useCallback((visitor) => { setVisitor(prevState => { if (visitor.hash != prevState.hash) { - setHash(visitor.hash); + setHash(visitor.hash) } - return visitor; - }); - }, [setVisitor]); + return visitor + }) + }, [setVisitor]) useEffect(() => { - let es; + let es const anonymousUser = { uid: 0 - }; + } if (hash) { me().then(visitor => auth(visitor)) - .catch(() => setVisitor(anonymousUser)); + .catch(() => setVisitor(anonymousUser)) if ('EventSource' in window) { - const eventParams = new URLSearchParams({ hash: hash }); - let url = new URL(`https://juick.com/api/events?${eventParams.toString()}`); - console.log(url.toString()); - es = new EventSource(url.toString()); + const eventParams = new URLSearchParams({ hash: hash }) + let url = new URL(`https://juick.com/api/events?${eventParams.toString()}`) + console.log(url.toString()) + es = new EventSource(url.toString()) es.onopen = () => { - console.log('online'); - }; + console.log('online') + } es.onerror = () => { - es.removeEventListener('read', updateStatus); - es.removeEventListener('msg', updateStatus); - }; - es.addEventListener('read', updateStatus); - es.addEventListener('msg', updateStatus); - setEventSource(es); + es.removeEventListener('read', updateStatus) + es.removeEventListener('msg', updateStatus) + } + es.addEventListener('read', updateStatus) + es.addEventListener('msg', updateStatus) + setEventSource(es) } } else { - setVisitor(anonymousUser); + setVisitor(anonymousUser) } return (() => { if (es && es.removeEventListener) { - es.removeEventListener('read', updateStatus); - es.removeEventListener('msg', updateStatus); + es.removeEventListener('read', updateStatus) + es.removeEventListener('msg', updateStatus) } - }); - }, [auth, hash, setVisitor, updateStatus]); + }) + }, [auth, hash, setVisitor, updateStatus]) useEffect(() => { const getTrends = async () => { - setAllTrends(await trends()); - }; - getTrends().catch(console.error); - }, []); + setAllTrends(await trends()) + } + getTrends().catch(console.error) + }, []) return ( <> @@ -200,5 +200,5 @@ export default function App({ footer }) { </Routes> </section> </> - ); + ) } diff --git a/vnext/src/api/index.js b/vnext/src/api/index.js index ef753eaf..566478a9 100644 --- a/vnext/src/api/index.js +++ b/vnext/src/api/index.js @@ -1,7 +1,7 @@ -import axios from 'axios'; -import Cookies from 'universal-cookie'; +import axios from 'axios' +import Cookies from 'universal-cookie' -const apiBaseUrl = 'https://juick.com'; +const apiBaseUrl = 'https://juick.com' /** * @typedef {object} Token @@ -73,17 +73,17 @@ const apiBaseUrl = 'https://juick.com'; const client = axios.create({ baseURL: apiBaseUrl -}); +}) client.interceptors.request.use(config => { if (config.url.startsWith('/')) { // only local URLs - let cookies = new Cookies(); + let cookies = new Cookies() config.params = Object.assign(config.params || {}, { hash: cookies.get('hash') - }); + }) } - return config; -}); + return config +}) /** * fetch my info @@ -92,28 +92,28 @@ client.interceptors.request.use(config => { * @returns {Promise<SecureUser, Error>} me object */ export function me(username = '', password = '') { - let cookies = new Cookies(); + let cookies = new Cookies() return new Promise((resolve, reject) => { client.get('/api/me', { headers: username ? { 'Authorization': 'Basic ' + window.btoa(unescape(encodeURIComponent(username + ':' + password))) } : {} }).then(response => { - let visitor = response.data; - cookies.set('hash', visitor.hash, { path: '/' }); - resolve(visitor); + let visitor = response.data + cookies.set('hash', visitor.hash, { path: '/' }) + resolve(visitor) }).catch(reason => { - cookies.remove('hash', { path: '/' }); - reject(reason); - }); - }); + cookies.remove('hash', { path: '/' }) + reject(reason) + }) + }) } /** * @param {string} username */ export function info(username) { - return client.get(`/api/info/${username}`); + return client.get(`/api/info/${username}`) } @@ -121,7 +121,7 @@ export function info(username) { * */ export function getChats() { - return client.get('/api/groups_pms'); + return client.get('/api/groups_pms') } /** @@ -132,7 +132,7 @@ export function getChat(userName) { params: { 'uname': userName } - }); + }) } /** @@ -140,10 +140,10 @@ export function getChat(userName) { * @param {string} body */ export function pm(userName, body) { - let form = new FormData(); - form.set('uname', userName); - form.set('body', body); - return client.post('/api/pm', form); + let form = new FormData() + form.set('uname', userName) + form.set('body', body) + return client.post('/api/pm', form) } /** @@ -153,7 +153,7 @@ export function pm(userName, body) { export function getMessages(path, params) { return client.get(path, { params: params - }); + }) } /** @@ -161,10 +161,10 @@ export function getMessages(path, params) { * @param {string} attach */ export function post(body, attach) { - let form = new FormData(); - form.append('attach', attach); - form.append('body', body); - return client.post('/api/post', form); + let form = new FormData() + form.append('attach', attach) + form.append('body', body) + return client.post('/api/post', form) } /** @@ -174,12 +174,12 @@ export function post(body, attach) { * @param {string} attach */ export function comment(mid, rid, body, attach) { - let form = new FormData(); - form.append('mid', mid.toString()); - form.append('rid', rid.toString()); - form.append('body', body); - form.append('attach', attach); - return client.post('/api/comment', form); + let form = new FormData() + form.append('mid', mid.toString()) + form.append('rid', rid.toString()) + form.append('body', body) + form.append('attach', attach) + return client.post('/api/comment', form) } /** * Edit message @@ -188,48 +188,48 @@ export function comment(mid, rid, body, attach) { * @param {string?} body */ export function update(mid, rid, body) { - let form = new FormData(); - form.append('mid', mid); - form.append('rid', rid); - form.append('body', body); - return client.post('/api/update', form); + let form = new FormData() + form.append('mid', mid) + form.append('rid', rid) + form.append('body', body) + return client.post('/api/update', form) } /** * Update user avatar * @param {string} newAvatar */ export function updateAvatar(newAvatar) { - let form = new FormData(); - form.append('avatar', newAvatar); - return client.post('/api/me/upload', form); + let form = new FormData() + form.append('avatar', newAvatar) + return client.post('/api/me/upload', form) } /** * @param {string} network */ function socialLink(network) { - return `${apiBaseUrl}/api/_${network}login?state=${window.location.protocol}//${window.location.host}${window.location.pathname}`; + return `${apiBaseUrl}/api/_${network}login?state=${window.location.protocol}//${window.location.host}${window.location.pathname}` } /** * */ export function facebookLink() { - return socialLink('fb'); + return socialLink('fb') } /** * */ export function vkLink() { - return socialLink('vk'); + return socialLink('vk') } /** * */ export function appleLink() { - return socialLink('apple'); + return socialLink('apple') } /** @@ -237,10 +237,10 @@ export function appleLink() { * @param {SecureUser} visitor */ export function markReadTracker(msg, visitor) { - return `${apiBaseUrl}/api/thread/mark_read/${msg.mid}-${msg.rid || 0}.gif?hash=${visitor.hash}`; + return `${apiBaseUrl}/api/thread/mark_read/${msg.mid}-${msg.rid || 0}.gif?hash=${visitor.hash}` } -let profileCache = {}; +let profileCache = {} /** * Fetch user profile @@ -249,18 +249,18 @@ let profileCache = {}; export function fetchUserUri(profileUrl) { return new Promise((resolve, reject) => { if (profileCache[profileUrl]) { - resolve(profileCache[profileUrl]); + resolve(profileCache[profileUrl]) } else { client.get(profileUrl, { headers: { 'Accept': 'application/ld+json' } }).then(response => { - profileCache[profileUrl] = response.data; - resolve(response.data); - }).catch(reject); + profileCache[profileUrl] = response.data + resolve(response.data) + }).catch(reject) } - }); + }) } /** @@ -269,13 +269,13 @@ export function fetchUserUri(profileUrl) { */ export const trends = async () => { try { - const response = await client.get('/api/tags'); - return response.data; + const response = await client.get('/api/tags') + return response.data } catch (e) { - console.error(e); - return []; + console.error(e) + return [] } -}; +} /** * Fetch Tweet content @@ -289,9 +289,9 @@ export function fetchUserUri(profileUrl) { 'omit_script': true, 'url': url } - }); - return response.data; -}; + }) + return response.data +} /** * Checks if HTTP error code is redirection code @@ -299,7 +299,7 @@ export function fetchUserUri(profileUrl) { * @returns {boolean} is HTTP request redirected or not */ function isHttpRedirected(code = 200) { - return [301, 302].includes(code); + return [301, 302].includes(code) } /** @@ -308,7 +308,7 @@ function isHttpRedirected(code = 200) { * @returns {boolean} is HTTP request successful or not */ function isHttpSuccessful(code = 200) { - return code >= 200 && code < 300; + return code >= 200 && code < 300 } /** @@ -323,25 +323,25 @@ function expandShortenedLink(url = '') { }).then(response => { if (isHttpSuccessful(response.status)) { // URL is not redirected - resolve(url); - return; + resolve(url) + return } if (isHttpRedirected(response.status)) { - resolve(/** @type { string } */ (response.headers['Location'])); - return; + resolve(/** @type { string } */ (response.headers['Location'])) + return } // Error case - reject('Invalid response'); + reject('Invalid response') }).catch(error => { - reject(error); - }); - }); + reject(error) + }) + }) } export { embeddedTweet, expandShortenedLink -}; +} diff --git a/vnext/src/index.js b/vnext/src/index.js index 11a78a48..5ab543c4 100644 --- a/vnext/src/index.js +++ b/vnext/src/index.js @@ -1,32 +1,32 @@ -import 'core-js/modules/es.array.map'; -import 'core-js/modules/es.map'; -import 'core-js/modules/es.object.create'; -import 'core-js/modules/es.object.define-property'; -import 'core-js/modules/es.object.set-prototype-of'; -import 'core-js/modules/es.promise'; -import 'core-js/modules/es.set'; -import 'core-js/modules/es.symbol'; -import 'core-js/modules/web.dom-collections.iterator'; -import 'url-polyfill'; -import { StrictMode, lazy } from 'react'; -import { createRoot, hydrateRoot } from 'react-dom/client'; -import { BrowserRouter } from 'react-router-dom'; -import { CookiesProvider } from 'react-cookie'; +import 'core-js/modules/es.array.map' +import 'core-js/modules/es.map' +import 'core-js/modules/es.object.create' +import 'core-js/modules/es.object.define-property' +import 'core-js/modules/es.object.set-prototype-of' +import 'core-js/modules/es.promise' +import 'core-js/modules/es.set' +import 'core-js/modules/es.symbol' +import 'core-js/modules/web.dom-collections.iterator' +import 'url-polyfill' +import { StrictMode, lazy } from 'react' +import { createRoot, hydrateRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import { CookiesProvider } from 'react-cookie' -import { VisitorProvider } from './ui/VisitorContext'; +import { VisitorProvider } from './ui/VisitorContext' -const Juick = lazy(() => import('./App')); +const Juick = lazy(() => import('./App')) function fromBinary(encoded) { - const binary = window.atob(encoded); - const bytes = new Uint8Array(binary.length); + const binary = window.atob(encoded) + const bytes = new Uint8Array(binary.length) for (let i = 0; i < bytes.length; i++) { - bytes[i] = binary.charCodeAt(i); + bytes[i] = binary.charCodeAt(i) } - return String.fromCharCode(...new Uint16Array(bytes.buffer)); + return String.fromCharCode(...new Uint16Array(bytes.buffer)) } -const props = window.__PROPS__ ? JSON.parse(fromBinary(window.__PROPS__)) : {}; +const props = window.__PROPS__ ? JSON.parse(fromBinary(window.__PROPS__)) : {} const JuickApp = () => ( <StrictMode> @@ -38,11 +38,11 @@ const JuickApp = () => ( </CookiesProvider> </VisitorProvider> </StrictMode> -); +) -let root = document.getElementById('app'); +let root = document.getElementById('app') if (window.__PROPS__) { - hydrateRoot(root, <JuickApp />); + hydrateRoot(root, <JuickApp />) } else { - createRoot(root).render(<JuickApp />); + createRoot(root).render(<JuickApp />) } diff --git a/vnext/src/ui/Avatar.js b/vnext/src/ui/Avatar.js index 9d93521f..1a8db0c3 100644 --- a/vnext/src/ui/Avatar.js +++ b/vnext/src/ui/Avatar.js @@ -1,7 +1,7 @@ -import { memo } from 'react'; -import { Link } from 'react-router-dom'; +import { memo } from 'react' +import { Link } from 'react-router-dom' -import Icon from './Icon'; +import Icon from './Icon' /** * @typedef {object} AvatarProps @@ -38,7 +38,7 @@ function Avatar({ user, style, link, children }) { {children} </div> </div> - ); + ) } -export default memo(Avatar); +export default memo(Avatar) diff --git a/vnext/src/ui/Button.js b/vnext/src/ui/Button.js index 2c315e46..dd425021 100644 --- a/vnext/src/ui/Button.js +++ b/vnext/src/ui/Button.js @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import { memo } from 'react' /** * @param {import('react').ClassAttributes<HTMLButtonElement> & import('react').ButtonHTMLAttributes<HTMLButtonElement>} props @@ -6,7 +6,7 @@ import { memo } from 'react'; function Button(props) { return ( <button className="Button" {...props} /> - ); + ) } -export default memo(Button); +export default memo(Button) diff --git a/vnext/src/ui/Chat.js b/vnext/src/ui/Chat.js index 4fdeaac7..55b89d09 100644 --- a/vnext/src/ui/Chat.js +++ b/vnext/src/ui/Chat.js @@ -1,17 +1,17 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useParams } from 'react-router-dom'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -dayjs.extend(utc); +import { useEffect, useState, useCallback } from 'react' +import { useParams } from 'react-router-dom' +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +dayjs.extend(utc) -import PM from './PM'; -import MessageInput from './MessageInput'; -import UserInfo from './UserInfo'; +import PM from './PM' +import MessageInput from './MessageInput' +import UserInfo from './UserInfo' -import { getChat, pm } from '../api'; +import { getChat, pm } from '../api' -import { useVisitor } from './VisitorContext'; -import { Helmet } from 'react-helmet'; +import { useVisitor } from './VisitorContext' +import { Helmet } from 'react-helmet' /** * @@ -24,48 +24,48 @@ import { Helmet } from 'react-helmet'; * @param {ChatProps} props */ export default function Chat(props) { - const [visitor] = useVisitor(); - const [messages, setMessages] = useState([]); - const params = useParams(); + const [visitor] = useVisitor() + const [messages, setMessages] = useState([]) + const params = useParams() let loadChat = useCallback((uname) => { - const { hash } = visitor; + const { hash } = visitor if (hash && uname) { getChat(uname) .then(response => { - setMessages(response.data); - }).catch(console.log); + setMessages(response.data) + }).catch(console.log) } - }, [visitor]); + }, [visitor]) let onMessage = useCallback((json) => { - const msg = JSON.parse(json.data); + const msg = JSON.parse(json.data) if (msg.user.uname === params.user) { setMessages((oldChat) => { - return [msg, ...oldChat]; - }); + return [msg, ...oldChat] + }) } - }, [params.user]); + }, [params.user]) let onSend = async ({ body }) => { - let result = false; - let res = await pm(params.user, body).catch(console.error); - result = res.status == 200; - return result; - }; + let result = false + let res = await pm(params.user, body).catch(console.error) + result = res.status == 200 + return result + } useEffect(() => { if (props.connection.addEventListener) { - props.connection.addEventListener('msg', onMessage); + props.connection.addEventListener('msg', onMessage) } - loadChat(params.user); - console.log(props.connection); + loadChat(params.user) + console.log(props.connection) return () => { if (props.connection.removeEventListener) { - props.connection.removeEventListener('msg', onMessage); + props.connection.removeEventListener('msg', onMessage) } - }; - }, [props.connection, onMessage, loadChat, params.user]); - const uname = params.user; + } + }, [props.connection, onMessage, loadChat, params.user]) + const uname = params.user return ( <div className="msg-cont"> <Helmet> @@ -90,5 +90,5 @@ export default function Chat(props) { ) } </div> - ); + ) } diff --git a/vnext/src/ui/Comment.js b/vnext/src/ui/Comment.js index e8fb2afb..6ba34ff0 100644 --- a/vnext/src/ui/Comment.js +++ b/vnext/src/ui/Comment.js @@ -1,14 +1,14 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState } from 'react' -import MessageInput from './MessageInput'; -import Avatar from './Avatar'; -import { UserLink } from './UserInfo'; -import Button from './Button'; +import MessageInput from './MessageInput' +import Avatar from './Avatar' +import { UserLink } from './UserInfo' +import Button from './Button' -import { format, embedUrls } from '../utils/embed'; +import { format, embedUrls } from '../utils/embed' -import { chatItemStyle } from './helpers/BubbleStyle'; -import { useVisitor } from './VisitorContext'; +import { chatItemStyle } from './helpers/BubbleStyle' +import { useVisitor } from './VisitorContext' /** * @param {{ @@ -23,20 +23,20 @@ import { useVisitor } from './VisitorContext'; * @returns import('react').ReactElement */ export default function Comment({ msg, draft, active, setActive, onStartEditing, postComment }) { - const [visitor] = useVisitor(); + const [visitor] = useVisitor() /** @type {import('react').MutableRefObject<HTMLDivElement?>} */ - const embedRef = useRef(null); + const embedRef = useRef(null) /** @type {import('react').MutableRefObject<HTMLDivElement?>} */ - const msgRef = useRef(null); - const [author] = useState(msg.user); + const msgRef = useRef(null) + const [author] = useState(msg.user) useEffect(() => { if (msgRef.current && embedRef.current) { - embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current); + embedUrls(msgRef.current.querySelectorAll('a'), embedRef.current) if (!embedRef.current.hasChildNodes()) { - embedRef.current.style.display = 'none'; + embedRef.current.style.display = 'none' } } - }, []); + }, []) return ( <div style={chatItemStyle(visitor, msg)}> <div className="msg-header"> @@ -94,11 +94,11 @@ export default function Comment({ msg, draft, active, setActive, onStartEditing, </div> } </div> - ); + ) } /** * @type {import('react').CSSProperties} */ const linkStyle = { cursor: 'pointer' -}; +} diff --git a/vnext/src/ui/Contact.js b/vnext/src/ui/Contact.js index 75c80332..d7bb12c8 100644 --- a/vnext/src/ui/Contact.js +++ b/vnext/src/ui/Contact.js @@ -1,6 +1,6 @@ -import { memo } from 'react'; +import { memo } from 'react' -import Avatar from './Avatar'; +import Avatar from './Avatar' /** * @typedef {object} ContactProps @@ -18,7 +18,7 @@ function Contact({ user, style }) { {user.unreadCount && <span className="badge">{user.unreadCount}</span>} <div className="msg-ts">{user.lastMessageText}</div> </Avatar> - ); + ) } -export default memo(Contact); +export default memo(Contact) diff --git a/vnext/src/ui/Contacts.js b/vnext/src/ui/Contacts.js index b1f87723..1c23f042 100644 --- a/vnext/src/ui/Contacts.js +++ b/vnext/src/ui/Contacts.js @@ -1,23 +1,23 @@ -import { useEffect, useState } from 'react'; -import { Helmet } from 'react-helmet'; +import { useEffect, useState } from 'react' +import { Helmet } from 'react-helmet' -import { getChats } from '../api'; +import { getChats } from '../api' -import Contact from './Contact.js'; -import { ChatSpinner } from './Spinner'; +import Contact from './Contact.js' +import { ChatSpinner } from './Spinner' /** * */ export default function Contacts() { - const [pms, setPms] = useState([]); + const [pms, setPms] = useState([]) useEffect(() => { getChats() .then(response => { - setPms(response.data.pms); - }).catch(console.log); - }, []); + setPms(response.data.pms) + }).catch(console.log) + }, []) return ( <div className="msg-cont"> <Helmet> @@ -31,7 +31,7 @@ export default function Contacts() { } </div> </div> - ); + ) } const chatListStyle = { @@ -39,7 +39,7 @@ const chatListStyle = { flexDirection: 'column', width: '100%', padding: '12px' -}; +} const chatTitleStyle = { width: '100%', @@ -48,4 +48,4 @@ const chatTitleStyle = { background: 'var(--main-background-color)', color: 'var(--text-color)', borderBottom: '1px solid var(--border-color)' -}; +} diff --git a/vnext/src/ui/Feeds.js b/vnext/src/ui/Feeds.js index 48da52e3..086a910e 100644 --- a/vnext/src/ui/Feeds.js +++ b/vnext/src/ui/Feeds.js @@ -1,18 +1,18 @@ -import { useState, useEffect } from 'react'; -import { Link, useLocation, useParams, Navigate, useSearchParams } from 'react-router-dom'; +import { useState, useEffect } from 'react' +import { Link, useLocation, useParams, Navigate, useSearchParams } from 'react-router-dom' -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -dayjs.extend(utc); +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +dayjs.extend(utc) -import Message from './Message'; -import Spinner from './Spinner'; +import Message from './Message' +import Spinner from './Spinner' -import UserInfo from './UserInfo'; +import UserInfo from './UserInfo' -import { getMessages } from '../api'; -import { useVisitor } from './VisitorContext'; -import { Helmet } from 'react-helmet'; +import { getMessages } from '../api' +import { useVisitor } from './VisitorContext' +import { Helmet } from 'react-helmet' /** * @typedef {object} Query @@ -28,29 +28,29 @@ import { Helmet } from 'react-helmet'; */ function RequireAuth({ children }) { - let location = useLocation(); - let [visitor] = useVisitor(); + let location = useLocation() + let [visitor] = useVisitor() if (!visitor.hash) { // Redirect them to the /login page, but save the current location they were // trying to go to when they were redirected. This allows us to send them // along to that page after they login, which is a nicer user experience // than dropping them off on the home page. - return <Navigate to="/login" state={{ from: location }} />; + return <Navigate to="/login" state={{ from: location }} /> } - return children; + return children } /** * */ export function Discover() { - const [search] = useSearchParams(); + const [search] = useSearchParams() const query = { baseUrl: '/api/messages', search: search, pageParam: search.search ? 'page' : 'before_mid' - }; + } return ( <> <Helmet> @@ -58,7 +58,7 @@ export function Discover() { </Helmet> <Feed query={query} /> </> - ); + ) } /** @@ -68,7 +68,7 @@ export function Discussions() { const query = { baseUrl: '/api/messages/discussions', pageParam: 'to' - }; + } return ( <> <Helmet> @@ -76,26 +76,26 @@ export function Discussions() { </Helmet> <Feed query={query} /> </> - ); + ) } /** * */ export function Blog() { - const { user } = useParams(); - const [params] = useSearchParams(); + const { user } = useParams() + const [params] = useSearchParams() const search = { ...params, uname: user - }; + } const query = { baseUrl: '/api/messages', search: search, pageParam: search.search ? 'page' : 'before_mid' - }; - const blogTitle = `${user} blog`; - const pageTitle = search.tag ? `${blogTitle}: #${search.tag}` : blogTitle; + } + const blogTitle = `${user} blog` + const pageTitle = search.tag ? `${blogTitle}: #${search.tag}` : blogTitle return ( <> <Helmet> @@ -106,22 +106,22 @@ export function Blog() { </div> <Feed query={query} /> </> - ); + ) } /** * */ export function Tag() { - const params = useParams(); - const { tag } = params; + const params = useParams() + const { tag } = params const query = { baseUrl: '/api/messages', search: { tag: tag }, pageParam: 'before_mid' - }; + } return ( <> <Helmet> @@ -129,7 +129,7 @@ export function Tag() { </Helmet> <Feed query={query} /> </> - ); + ) } /** @@ -139,12 +139,12 @@ export function Home() { const query = { baseUrl: '/api/home', pageParam: 'before_mid' - }; + } return ( <RequireAuth> <Feed query={query} /> </RequireAuth> - ); + ) } /** @@ -157,52 +157,52 @@ export function Home() { * @param {FeedState} props */ function Feed({ query }) { - const location = useLocation(); - const [visitor] = useVisitor(); + const location = useLocation() + const [visitor] = useVisitor() const [state, setState] = useState({ hash: visitor.hash, msgs: [], nextpage: null, error: false, tag: '' - }); - const [loading, setLoading] = useState(true); - const [filter] = useSearchParams(); + }) + const [loading, setLoading] = useState(true) + const [filter] = useSearchParams() useEffect(() => { - setLoading(true); + setLoading(true) let getPageParam = (pageParam, lastMessage, /** @type { URLSearchParams } */ filterParams) => { - const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : dayjs.utc(lastMessage.updated).valueOf(); - filterParams.append(pageParam, pageValue); - return `?${filterParams.toString()}`; - }; - let params = { ...Object.fromEntries(filter), ...query.search }; - let url = query.baseUrl; + const pageValue = pageParam === 'before_mid' ? lastMessage.mid : pageParam === 'page' ? (Number(filterParams.page) || 0) + 1 : dayjs.utc(lastMessage.updated).valueOf() + filterParams.append(pageParam, pageValue) + return `?${filterParams.toString()}` + } + let params = { ...Object.fromEntries(filter), ...query.search } + let url = query.baseUrl getMessages(url, params) .then(response => { - const { data } = response; - const { pageParam } = query; - const lastMessage = data.slice(-1)[0] || {}; - const nextpage = getPageParam(pageParam, lastMessage, new URLSearchParams(params)); - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; + const { data } = response + const { pageParam } = query + const lastMessage = data.slice(-1)[0] || {} + const nextpage = getPageParam(pageParam, lastMessage, new URLSearchParams(params)) + document.body.scrollTop = 0 + document.documentElement.scrollTop = 0 setState((prevState) => { return { ...prevState, msgs: data, nextpage: nextpage, tag: filter['tag'] || '' - }; - }); - setLoading(false); + } + }) + setLoading(false) }).catch(() => { setState((prevState) => { return { ...prevState, error: true - }; - }); - }); - }, [query, filter]); + } + }) + }) + }, [query, filter]) return (state.msgs.length > 0 ? ( <div className="msgs"> { @@ -227,5 +227,5 @@ function Feed({ query }) { } </div> ) : state.error ? <div>error</div> : loading ? <div className="msgs"><Spinner /><Spinner /><Spinner /><Spinner /></div> : <div>No more messages</div> - ); + ) } diff --git a/vnext/src/ui/Header.js b/vnext/src/ui/Header.js index 6cf07844..cee24c67 100644 --- a/vnext/src/ui/Header.js +++ b/vnext/src/ui/Header.js @@ -1,19 +1,19 @@ -import { memo, useCallback } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { memo, useCallback } from 'react' +import { Link, useNavigate } from 'react-router-dom' -import SearchBox from './SearchBox'; +import SearchBox from './SearchBox' function Header() { - const navigate = useNavigate(); + const navigate = useNavigate() /** * @param {string} searchString */ let searchAll = useCallback((searchString) => { - let location = {}; - location.pathname = '/discover'; - location.search = `?search=${searchString}`; - navigate(location); - }, [navigate]); + let location = {} + location.pathname = '/discover' + location.search = `?search=${searchString}` + navigate(location) + }, [navigate]) return ( <div id="header"> <div id="header_wrapper"> @@ -25,7 +25,7 @@ function Header() { </div> </div> </div> - ); + ) } -export default memo(Header); +export default memo(Header) diff --git a/vnext/src/ui/Icon.js b/vnext/src/ui/Icon.js index 6d10df16..bc5ce08a 100644 --- a/vnext/src/ui/Icon.js +++ b/vnext/src/ui/Icon.js @@ -1,7 +1,7 @@ -import { createElement, memo } from 'react'; -import PropTypes from 'prop-types'; +import { createElement, memo } from 'react' +import PropTypes from 'prop-types' -import evilIcons from 'evil-icons/assets/sprite.svg'; +import evilIcons from 'evil-icons/assets/sprite.svg' /** * @typedef {object} IconProps @@ -16,18 +16,18 @@ import evilIcons from 'evil-icons/assets/sprite.svg'; * @param {IconProps} props - icon props */ function IconElement(props) { - var size = props.size ? ' icon--' + props.size : ''; - var className = props.className ? ' ' + props.className : ''; - var klass = 'icon' + (!props.noFill ? ' icon--' + props.name : '') + size + className; + var size = props.size ? ' icon--' + props.size : '' + var className = props.className ? ' ' + props.className : '' + var klass = 'icon' + (!props.noFill ? ' icon--' + props.name : '') + size + className - var name = '#' + props.name + '-icon'; - var useTag = `<use xlink:href='${evilIcons}${name}' />`; - var Icon = createElement('svg', { className: 'icon__cnt', dangerouslySetInnerHTML: { __html: useTag } }); + var name = '#' + props.name + '-icon' + var useTag = `<use xlink:href='${evilIcons}${name}' />` + var Icon = createElement('svg', { className: 'icon__cnt', dangerouslySetInnerHTML: { __html: useTag } }) return createElement( 'div', { className: klass }, wrapSpinner(Icon, klass) - ); + ) } /** @@ -40,17 +40,17 @@ function wrapSpinner(Html, klass) { 'div', { className: 'icon__spinner' }, Html - ); + ) } else { - return Html; + return Html } } -export default memo(IconElement); +export default memo(IconElement) IconElement.propTypes = { size: PropTypes.string.isRequired, name: PropTypes.string.isRequired, className: PropTypes.string, noFill: PropTypes.bool -}; +} diff --git a/vnext/src/ui/Login.js b/vnext/src/ui/Login.js index 73da49c8..5d9908cb 100644 --- a/vnext/src/ui/Login.js +++ b/vnext/src/ui/Login.js @@ -1,13 +1,13 @@ -import { useEffect } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useEffect } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' -import Icon from './Icon'; -import Button from './Button'; -import { useForm } from 'react-hook-form'; +import Icon from './Icon' +import Button from './Button' +import { useForm } from 'react-hook-form' -import { me, facebookLink, vkLink, appleLink } from '../api'; +import { me, facebookLink, vkLink, appleLink } from '../api' -import { useVisitor } from './VisitorContext'; +import { useVisitor } from './VisitorContext' /** * @typedef {object} LoginProps @@ -19,29 +19,29 @@ import { useVisitor } from './VisitorContext'; * @param {LoginProps} props */ function Login({ onAuth }) { - const location = useLocation(); - const navigate = useNavigate(); - const [visitor] = useVisitor(); + const location = useLocation() + const navigate = useNavigate() + const [visitor] = useVisitor() useEffect(() => { if (visitor.hash) { - const {retpath } = location.state || '/'; - console.log(retpath); - navigate(retpath); + const {retpath } = location.state || '/' + console.log(retpath) + navigate(retpath) } - }, [navigate, location.state, visitor]); + }, [navigate, location.state, visitor]) - const { register, handleSubmit } = useForm(); + const { register, handleSubmit } = useForm() /** @type { import('react-hook-form').SubmitHandler<import('react-hook-form').FieldValues> } */ let onSubmit = (values) => { me(values.username, values.password) .then(response => { - onAuth(response); + onAuth(response) } ).catch(ex => { - console.log(ex); - }); - }; + console.log(ex) + }) + } return ( <div className="msg-cont"> <div className="dialoglogin"> @@ -64,25 +64,25 @@ function Login({ onAuth }) { </form> </div> </div> - ); + ) } -export default Login; +export default Login const socialButtonsStyle = { display: 'flex', justifyContent: 'space-evenly', padding: '4px' -}; +} const facebookButtonStyle = { color: '#fff', padding: '2px 14px', background: '#3b5998' -}; +} const vkButtonStyle = { color: '#fff', padding: '2px 14px', background: '#4c75a3' -}; +} diff --git a/vnext/src/ui/Message.js b/vnext/src/ui/Message.js index c5ad175d..e4135700 100644 --- a/vnext/src/ui/Message.js +++ b/vnext/src/ui/Message.js @@ -1,18 +1,18 @@ -import React, { Fragment, memo, useEffect, useRef } from 'react'; +import React, { Fragment, memo, useEffect, useRef } from 'react' -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -dayjs.extend(utc); -import relativeTime from 'dayjs/plugin/relativeTime'; -dayjs.extend(relativeTime); +import dayjs from 'dayjs' +import utc from 'dayjs/plugin/utc' +dayjs.extend(utc) +import relativeTime from 'dayjs/plugin/relativeTime' +dayjs.extend(relativeTime) -import { Link } from 'react-router-dom'; -import Icon from './Icon'; -import Avatar from './Avatar'; -import { UserLink } from './UserInfo'; +import { Link } from 'react-router-dom' +import Icon from './Icon' +import Avatar from './Avatar' +import { UserLink } from './UserInfo' -import { format, embedUrls } from '../utils/embed'; -import { useVisitor } from './VisitorContext'; +import { format, embedUrls } from '../utils/embed' +import { useVisitor } from './VisitorContext' /** * @callback ToggleSubscriptionCallback @@ -31,30 +31,30 @@ import { useVisitor } from './VisitorContext'; * @param {React.PropsWithChildren<{}> & MessageProps} props props */ export default function Message({ data, isThread, onToggleSubscription, children }) { - const [visitor] = useVisitor(); - const isCode = (data.tags || []).indexOf('code') >= 0; - const likesSummary = data.likes ? `${data.likes}` : 'Recommend'; - const commentsSummary = data.replies ? `${data.replies}` : 'Comment'; + const [visitor] = useVisitor() + const isCode = (data.tags || []).indexOf('code') >= 0 + const likesSummary = data.likes ? `${data.likes}` : 'Recommend' + const commentsSummary = data.replies ? `${data.replies}` : 'Comment' /** * @type {React.MutableRefObject<HTMLDivElement?>} */ - const embedRef = useRef(null); + const embedRef = useRef(null) /** * @type {React.MutableRefObject<HTMLDivElement?>} */ - const msgRef = useRef(null); + const msgRef = useRef(null) useEffect(() => { - const msg = msgRef.current; - const embed = embedRef.current; + const msg = msgRef.current + const embed = embedRef.current if (msg && embed) { - embedUrls(msg.querySelectorAll('a'), embed); + embedUrls(msg.querySelectorAll('a'), embed) if (!embed.hasChildNodes()) { - embed.style.display = 'none'; + embed.style.display = 'none' } } - }, []); + }, []) const canComment = data.user && visitor.uid === data.user.uid || !data.ReadOnly && visitor.uid > 0 - || !data.ReadOnly && !isThread; + || !data.ReadOnly && !isThread return ( <div className="msg-cont"> <Recommendations forMessage={data} /> @@ -120,7 +120,7 @@ export default function Message({ data, isThread, onToggleSubscription, children { data.user && canComment && (( isThread ? ( - <a className="msg-button" onClick={() => { onToggleSubscription(data); }}> + <a className="msg-button" onClick={() => { onToggleSubscription(data) }}> { data.subscribed ? (<> <Icon name="ei-check" size="s" /> @@ -143,14 +143,14 @@ export default function Message({ data, isThread, onToggleSubscription, children } {children} </div > - ); + ) } /** * @param {{isCode: boolean, data: {__html: string}}} props props */ function MessageContainer({ isCode, data }) { - return isCode ? (<pre dangerouslySetInnerHTML={data} />) : (<span dangerouslySetInnerHTML={data} />); + return isCode ? (<pre dangerouslySetInnerHTML={data} />) : (<span dangerouslySetInnerHTML={data} />) } /** @@ -171,18 +171,18 @@ function Tags({ data, user }) { )) } </span> - ) : null; + ) : null } -const TagsList = memo(Tags); +const TagsList = memo(Tags) /** * * @param {{forMessage: import('../client').Message}} props props */ function Recommends({ forMessage }) { - const { recommendations } = forMessage; - const likes = forMessage.likes || 0; + const { recommendations } = forMessage + const likes = forMessage.likes || 0 return recommendations && recommendations.length > 0 && ( <div className="msg-recomms">{'♡ by '} { @@ -197,7 +197,7 @@ function Recommends({ forMessage }) { likes > recommendations.length && (<span> and {likes - recommendations.length} others</span>) } </div> - ) || null; + ) || null } -const Recommendations = memo(Recommends); +const Recommendations = memo(Recommends) diff --git a/vnext/src/ui/MessageInput.js b/vnext/src/ui/MessageInput.js index 3d24e728..6ebf4361 100644 --- a/vnext/src/ui/MessageInput.js +++ b/vnext/src/ui/MessageInput.js @@ -1,10 +1,10 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react' -import Icon from './Icon'; -import Button from './Button'; +import Icon from './Icon' +import Button from './Button' -import UploadButton from './UploadButton'; -import toast from 'react-hot-toast'; +import UploadButton from './UploadButton' +import toast from 'react-hot-toast' /** @@ -13,13 +13,13 @@ import toast from 'react-hot-toast'; */ function moveCaretToEnd(el) { if (typeof el.selectionStart == 'number') { - el.selectionStart = el.selectionEnd = el.value.length; + el.selectionStart = el.selectionEnd = el.value.length } else if (typeof el.createTextRange != 'undefined') { // Internet Explorer - el.focus(); - var range = el.createTextRange(); - range.collapse(false); - range.select(); + el.focus() + var range = el.createTextRange() + range.collapse(false) + range.select() } } @@ -42,74 +42,74 @@ export default function MessageInput({ text, rows, placeholder, onSend }) { /** * @type {React.MutableRefObject<HTMLTextAreaElement?>} */ - let textareaRef = useRef(null); + let textareaRef = useRef(null) /** * @type {React.MutableRefObject<HTMLInputElement?>} */ - let fileinput = useRef(null); + let fileinput = useRef(null) let updateFocus = () => { - const isDesktop = window.matchMedia('(min-width: 62.5rem)'); + const isDesktop = window.matchMedia('(min-width: 62.5rem)') if (isDesktop.matches) { - const textarea = textareaRef.current; + const textarea = textareaRef.current if (textarea) { - textarea.focus(); - moveCaretToEnd(textarea); + textarea.focus() + moveCaretToEnd(textarea) } } - }; + } useEffect(() => { textChanged({ target: { value: text } - }); - updateFocus(); - }, [text]); + }) + updateFocus() + }, [text]) - let [body, setBody] = useState(text); + let [body, setBody] = useState(text) let handleCtrlEnter = (event) => { if (event.ctrlKey && (event.charCode == 10 || event.charCode == 13)) { - onSubmit({}); + onSubmit({}) } - }; + } const textChanged = (event) => { - setBody(event.target.value); - const el = textareaRef.current; + setBody(event.target.value) + const el = textareaRef.current if (el) { - const offset = el.offsetHeight - el.clientHeight; - const height = el.scrollHeight + offset; - el.style.height = `${height + offset}px`; + const offset = el.offsetHeight - el.clientHeight + const height = el.scrollHeight + offset + el.style.height = `${height + offset}px` } - }; - const [attach, setAttach] = useState(''); + } + const [attach, setAttach] = useState('') let uploadValueChanged = (attach) => { - setAttach(attach); - }; + setAttach(attach) + } let onSubmit = (event) => { if (event.preventDefault) { - event.preventDefault(); + event.preventDefault() } - const input = fileinput.current; + const input = fileinput.current if (input && input.files) { onSend({ body: body, attach: attach ? input.files[0] : '' }).then((success) => { if (success) { - setAttach(''); - setBody(''); + setAttach('') + setBody('') if (textareaRef.current) { - textareaRef.current.style.height = ''; + textareaRef.current.style.height = '' } - updateFocus(); + updateFocus() } else { - toast('Can not update this message'); + toast('Can not update this message') } - }).catch(console.log); + }).catch(console.log) } - }; + } return ( <form className="msg-comment-target" style={{ padding: '12px', width: '100%' }} onSubmit={onSubmit}> <div style={commentStyle}> @@ -122,7 +122,7 @@ export default function MessageInput({ text, rows, placeholder, onSend }) { </div> </div> </form> - ); + ) } /** @@ -133,7 +133,7 @@ const commentStyle = { flexDirection: 'column', width: '100%', marginTop: '10px' -}; +} /** * @type {React.CSSProperties} @@ -143,7 +143,7 @@ const inputBarStyle = { alignItems: 'center', justifyContent: 'space-between', padding: '3px' -}; +} /** * @type {React.CSSProperties} @@ -156,4 +156,4 @@ const textInputStyle = { border: 0, outline: 'none', padding: '4px' -}; +} diff --git a/vnext/src/ui/PM.js b/vnext/src/ui/PM.js index d5a7eff1..3aa877b1 100644 --- a/vnext/src/ui/PM.js +++ b/vnext/src/ui/PM.js @@ -1,13 +1,13 @@ -import { memo } from 'react'; +import { memo } from 'react' -import Avatar from './Avatar'; -import { format } from '../utils/embed'; -import { chatItemStyle } from './helpers/BubbleStyle'; -import { useVisitor } from './VisitorContext'; +import Avatar from './Avatar' +import { format } from '../utils/embed' +import { chatItemStyle } from './helpers/BubbleStyle' +import { useVisitor } from './VisitorContext' function PM(props) { - const { chat } = props; - const [visitor] = useVisitor(); + const { chat } = props + const [visitor] = useVisitor() return ( <li> <div style={chatItemStyle(visitor, chat)}> @@ -17,10 +17,10 @@ function PM(props) { </div> </div> </li> - ); + ) } -export default memo(PM); +export default memo(PM) /* PM.propTypes = { chat: MessageType.isRequired diff --git a/vnext/src/ui/Post.js b/vnext/src/ui/Post.js index 45eceb35..6f7d1e93 100644 --- a/vnext/src/ui/Post.js +++ b/vnext/src/ui/Post.js @@ -1,42 +1,42 @@ -import { useState } from 'react'; -import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { useState } from 'react' +import { useLocation, useNavigate, useSearchParams } from 'react-router-dom' -import Button from './Button'; -import MessageInput from './MessageInput'; +import Button from './Button' +import MessageInput from './MessageInput' -import { post, update } from '../api'; -import { useVisitor } from './VisitorContext'; -import { Helmet } from 'react-helmet'; +import { post, update } from '../api' +import { useVisitor } from './VisitorContext' +import { Helmet } from 'react-helmet' /** * */ export default function Post() { - const location = useLocation(); - const navigate = useNavigate(); - const [visitor] = useVisitor(); - let draftMessage = (location.state || {}).data || {}; - let [draft, setDraft] = useState(draftMessage.body); - let [params] = useSearchParams(); + const location = useLocation() + const navigate = useNavigate() + const [visitor] = useVisitor() + let draftMessage = (location.state || {}).data || {} + let [draft, setDraft] = useState(draftMessage.body) + let [params] = useSearchParams() let postMessage = async ({ attach, body }) => { try { - const res = draftMessage.mid ? await update(draftMessage.mid, 0, body) : await post(body, attach); - let result = res.status == 200; + const res = draftMessage.mid ? await update(draftMessage.mid, 0, body) : await post(body, attach) + let result = res.status == 200 if (result) { - const msg = res.data.newMessage; - navigate(`/${visitor.uname}/${msg.mid}`); + const msg = res.data.newMessage + navigate(`/${visitor.uname}/${msg.mid}`) } - return result; + return result } catch (e) { - console.log(e); + console.log(e) } - return false; - }; + return false + } let appendTag = (tag) => { setDraft(prevDraft => { - return `${prevDraft || ''} *${tag} `; - }); - }; + return `${prevDraft || ''} *${tag} ` + }) + } return ( <div className="msg-cont"> <Helmet> @@ -51,12 +51,12 @@ export default function Post() { <p>Tags:</p> { visitor.tagStats.map(t => { - return (<Button key={t.tag} onClick={() => { appendTag(t.tag); }}>{t.tag}</Button>); + return (<Button key={t.tag} onClick={() => { appendTag(t.tag) }}>{t.tag}</Button>) }) } </div> } </div> - ); + ) } diff --git a/vnext/src/ui/SearchBox.js b/vnext/src/ui/SearchBox.js index 636967b1..e63a19ee 100644 --- a/vnext/src/ui/SearchBox.js +++ b/vnext/src/ui/SearchBox.js @@ -1,4 +1,4 @@ -import { useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form' /** * @typedef {object} SearchBoxPropsFields @@ -13,18 +13,18 @@ import { useForm } from 'react-hook-form'; * @param {SearchBoxProps} props */ function SearchBox({ onSearch }) { - const { register, handleSubmit } = useForm(); + const { register, handleSubmit } = useForm() /** @type { import('react-hook-form').SubmitHandler<import('react-hook-form').FieldValues> } */ let onSubmit = ( values ) => { - onSearch(values.search); - }; + onSearch(values.search) + } return ( <form onSubmit={handleSubmit(onSubmit)}> <input className="text" type="text" placeholder="Search..." {...register('search')} /> <input data-testid="submit" type="submit" hidden /> </form> - ); + ) } -export default SearchBox; +export default SearchBox diff --git a/vnext/src/ui/Settings.js b/vnext/src/ui/Settings.js index 380d8ff6..1205cd49 100644 --- a/vnext/src/ui/Settings.js +++ b/vnext/src/ui/Settings.js @@ -1,46 +1,46 @@ -import { Fragment, useState, useRef } from 'react'; +import { Fragment, useState, useRef } from 'react' -import { me, updateAvatar } from '../api'; +import { me, updateAvatar } from '../api' -import Button from './Button'; -import Icon from './Icon'; -import UploadButton from './UploadButton'; -import Avatar from './Avatar'; -import { useVisitor } from './VisitorContext'; -import { Helmet } from 'react-helmet'; +import Button from './Button' +import Icon from './Icon' +import UploadButton from './UploadButton' +import Avatar from './Avatar' +import { useVisitor } from './VisitorContext' +import { Helmet } from 'react-helmet' /** * @param {{ onChange: Function }} props */ function ChangeAvatarForm({ onChange }) { - const [visitor] = useVisitor(); - const [avatar, setAvatar] = useState(''); - const [preview, setPreview] = useState(); - const avatarInput = useRef(); + const [visitor] = useVisitor() + const [avatar, setAvatar] = useState('') + const [preview, setPreview] = useState() + const avatarInput = useRef() let avatarChanged = (newAvatar) => { - setAvatar(newAvatar); - setPreview(''); + setAvatar(newAvatar) + setPreview('') if (newAvatar) { - let reader = new FileReader(); + let reader = new FileReader() reader.onloadend = (preview) => { - setPreview(preview.target.result); - }; - reader.readAsDataURL(avatarInput.current.files[0]); + setPreview(preview.target.result) + } + reader.readAsDataURL(avatarInput.current.files[0]) } - }; - let previewUser = { ...visitor, uname: '<preview>' }; + } + let previewUser = { ...visitor, uname: '<preview>' } if (preview) { - previewUser = { ...visitor, avatar: preview, uname: '<preview>' }; + previewUser = { ...visitor, avatar: preview, uname: '<preview>' } } let onSubmitAvatar = async (event) => { if (event.preventDefault) { - event.preventDefault(); + event.preventDefault() } - await updateAvatar(avatarInput.current.files[0]); - avatarChanged(''); - let visitor = await me(); - onChange(visitor); - }; + await updateAvatar(avatarInput.current.files[0]) + avatarChanged('') + let visitor = await me() + onChange(visitor) + } return ( <form> <small>Recommendations: PNG, 96x96, <50Kb. Also, JPG and GIF supported.</small> @@ -50,7 +50,7 @@ function ChangeAvatarForm({ onChange }) { <Avatar user={previewUser} /> <Button onClick={onSubmitAvatar}>Update</Button> </form> - ); + ) } /** @@ -58,47 +58,47 @@ function ChangeAvatarForm({ onChange }) { */ export default function Settings({ onChange }) { - const [visitor] = useVisitor(); + const [visitor] = useVisitor() let passwordChanged = () => { - console.log('password changed'); - }; + console.log('password changed') + } let onSubmitPassword = (event) => { if (event.preventDefault) { - event.preventDefault(); + event.preventDefault() } - console.log('password update'); - }; + console.log('password update') + } let emailChanged = () => { - console.log('email update'); - }; + console.log('email update') + } let disableTelegram = () => { - console.log('telegram disable'); - }; + console.log('telegram disable') + } let disableFacebook = (event) => { if (event.preventDefault) { - event.preventDefault(); + event.preventDefault() } - console.log('facebook disable'); - }; + console.log('facebook disable') + } let enableFacebook = (event) => { if (event.preventDefault) { - event.preventDefault(); + event.preventDefault() } - console.log('facebook enable'); - }; + console.log('facebook enable') + } let disableTwitter = () => { - console.log('twitter disable'); - }; + console.log('twitter disable') + } let deleteJid = () => { // TODO - }; + } let addEmail = () => { // TODO - }; + } let deleteEmail = () => { // TODO - }; + } return ( <div className="msg-cont"> <Helmet> @@ -249,7 +249,7 @@ export default function Settings({ onChange }) { </fieldset> </div> - ); + ) } diff --git a/vnext/src/ui/Spinner.js b/vnext/src/ui/Spinner.js index 3e38571e..b611abb8 100644 --- a/vnext/src/ui/Spinner.js +++ b/vnext/src/ui/Spinner.js @@ -1,5 +1,5 @@ -import { memo } from 'react'; -import ContentLoader from 'react-content-loader'; +import { memo } from 'react' +import ContentLoader from 'react-content-loader' function Spinner(props) { return ( @@ -20,10 +20,10 @@ function Spinner(props) { </ContentLoader> </div> </div> - ); + ) } -export default memo(Spinner); +export default memo(Spinner) /** * @@ -42,5 +42,5 @@ export function ChatSpinner(props) { <rect x="56" y="20" rx="0" ry="0" width="85" height="6.4" /> <rect x="0" y="0" rx="0" ry="0" width="48" height="48" /> </ContentLoader> - ); + ) } diff --git a/vnext/src/ui/Thread.js b/vnext/src/ui/Thread.js index a3136c35..b727b73d 100644 --- a/vnext/src/ui/Thread.js +++ b/vnext/src/ui/Thread.js @@ -1,128 +1,128 @@ -import { useEffect, useState, useCallback } from 'react'; -import { useLocation, useParams } from 'react-router-dom'; +import { useEffect, useState, useCallback } from 'react' +import { useLocation, useParams } from 'react-router-dom' -import Comment from './Comment'; -import Message from './Message'; -import MessageInput from './MessageInput'; -import Spinner from './Spinner'; +import Comment from './Comment' +import Message from './Message' +import MessageInput from './MessageInput' +import Spinner from './Spinner' -import { getMessages, comment, update, post } from '../api'; -import { useVisitor } from './VisitorContext'; -import { Helmet } from 'react-helmet'; +import { getMessages, comment, update, post } from '../api' +import { useVisitor } from './VisitorContext' +import { Helmet } from 'react-helmet' /** * @type { import('../api').Message } */ -const emptyMessage = {}; +const emptyMessage = {} /** * Thread component * @param {import('react').PropsWithChildren<{}>} props */ export default function Thread(props) { - const location = useLocation(); - const params = useParams(); - const [message, setMessage] = useState((location.state || {}).data || {}); - const [replies, setReplies] = useState([]); - const [loading, setLoading] = useState(false); - const [active, setActive] = useState(0); + const location = useLocation() + const params = useParams() + const [message, setMessage] = useState((location.state || {}).data || {}) + const [replies, setReplies] = useState([]) + const [loading, setLoading] = useState(false) + const [active, setActive] = useState(0) - const [editing, setEditing] = useState(emptyMessage); - const [visitor] = useVisitor(); - const [hash] = useState(visitor.hash); - const { mid } = params; + const [editing, setEditing] = useState(emptyMessage) + const [visitor] = useVisitor() + const [hash] = useState(visitor.hash) + const { mid } = params let loadReplies = useCallback(() => { - document.body.scrollTop = 0; - document.documentElement.scrollTop = 0; - setReplies([]); - setLoading(true); + document.body.scrollTop = 0 + document.documentElement.scrollTop = 0 + setReplies([]) + setLoading(true) let params = { mid: mid - }; - params.hash = hash; + } + params.hash = hash getMessages('/api/thread', params) .then(response => { - let updatedMessage = response.data.shift(); + let updatedMessage = response.data.shift() if (!message.mid) { - setMessage(updatedMessage); + setMessage(updatedMessage) } - setReplies(response.data); - setLoading(false); - setActive(0); + setReplies(response.data) + setLoading(false) + setActive(0) } ).catch(ex => { - console.log(ex); - }); - }, [hash, message.mid, mid]); + console.log(ex) + }) + }, [hash, message.mid, mid]) let postComment = async ({ body, attach }) => { try { let res = editing.rid ? await update(mid, editing.rid, body) - : await comment(mid, active, body, attach); - let result = res.status == 200; + : await comment(mid, active, body, attach) + let result = res.status == 200 if (result) { - setEditing(emptyMessage); + setEditing(emptyMessage) } - return result; + return result } catch (e) { - console.error(e); + console.error(e) } - return false; - }; + return false + } let startEditing = (reply) => { - setActive(reply.to.rid || 0); - setEditing(reply); - }; + setActive(reply.to.rid || 0) + setEditing(reply) + } useEffect(() => { - setActive(0); - loadReplies(); - }, [loadReplies]); + setActive(0) + loadReplies() + }, [loadReplies]) useEffect(() => { let onReply = (json) => { - const msg = JSON.parse(json.data); + const msg = JSON.parse(json.data) if (msg.mid == message.mid) { setReplies(oldReplies => { - return [...oldReplies, msg]; - }); + return [...oldReplies, msg] + }) setActive(prev => { - return prev + 1; - }); + return prev + 1 + }) } - }; + } if (props.connection.addEventListener && message.mid) { - props.connection.addEventListener('msg', onReply); + props.connection.addEventListener('msg', onReply) } return () => { if (props.connection.removeEventListener && message.mid) { - props.connection.removeEventListener('msg', onReply); + props.connection.removeEventListener('msg', onReply) } - }; - }, [props.connection, message.mid]); + } + }, [props.connection, message.mid]) - const loaders = Math.min(message.replies || 0, 10); - const pageTitle = `${params.user} ${message && message.tags || 'thread'}`; + const loaders = Math.min(message.replies || 0, 10) + const pageTitle = `${params.user} ${message && message.tags || 'thread'}` /** @type { import('./Message').ToggleSubscriptionCallback } */ const handleSubsciptionToggle = (message) => { if (message.subscribed) { if (confirm('Unsubscribe?')) { post(`U #${message.mid}`).then((response) => { if (response.status === 200) { - setMessage({...message, subscribed: false}); + setMessage({...message, subscribed: false}) } - }).catch(console.error); + }).catch(console.error) } } else { if (confirm('Subscribe?')) { post(`S #${message.mid}`).then((response) => { if (response.status === 200) { - setMessage({...message, subscribed: true}); + setMessage({...message, subscribed: true}) } - }).catch(console.error); + }).catch(console.error) } } - }; + } return ( <> <Helmet> @@ -155,5 +155,5 @@ export default function Thread(props) { </ul> } </> - ); + ) } diff --git a/vnext/src/ui/UploadButton.js b/vnext/src/ui/UploadButton.js index b652e522..5ef2fd94 100644 --- a/vnext/src/ui/UploadButton.js +++ b/vnext/src/ui/UploadButton.js @@ -1,4 +1,4 @@ -import Icon from './Icon'; +import Icon from './Icon' /** * @typedef {object} UploadButtonProps @@ -13,20 +13,20 @@ import Icon from './Icon'; */ export default function UploadButton(props) { let openfile = () => { - const input = props.inputRef.current; + const input = props.inputRef.current if (props.value) { - props.onChange(''); + props.onChange('') } else { - input.click(); + input.click() } - }; + } /** * @param {import('react').ChangeEvent<HTMLInputElement>} event */ let attachmentChanged = (event) => { - props.onChange(event.target.value); - }; + props.onChange(event.target.value) + } return ( <div style={props.value ? activeStyle : inactiveStyle} onClick={openfile}> @@ -35,14 +35,14 @@ export default function UploadButton(props) { style={{ display: 'none' }} ref={props.inputRef} value={props.value} onChange={attachmentChanged} /> </div> - ); + ) } const inactiveStyle = { cursor: 'pointer', color: '#888' -}; +} const activeStyle = { cursor: 'pointer', color: 'green' -}; +} diff --git a/vnext/src/ui/UserInfo.js b/vnext/src/ui/UserInfo.js index 2ca8c431..f71dfcdc 100644 --- a/vnext/src/ui/UserInfo.js +++ b/vnext/src/ui/UserInfo.js @@ -1,13 +1,13 @@ -import { memo, useState, useEffect, useRef } from 'react'; -import { Link } from 'react-router-dom'; +import { memo, useState, useEffect, useRef } from 'react' +import { Link } from 'react-router-dom' -import { info, fetchUserUri } from '../api'; +import { info, fetchUserUri } from '../api' -import Avatar from './Avatar'; -import Icon from './Icon'; -import defaultAvatar from '../assets/av-96.png'; +import Avatar from './Avatar' +import Icon from './Icon' +import defaultAvatar from '../assets/av-96.png' -let isMounted; +let isMounted /** * User info component @@ -17,19 +17,19 @@ export default function UserInfo({ uname, onUpdate, children }) { const [user, setUser] = useState({ uname: uname, uid: 0 - }); + }) useEffect(() => { - isMounted = true; + isMounted = true info(uname).then(response => { if (isMounted) { - onUpdate && onUpdate(response.data); - setUser(response.data); + onUpdate && onUpdate(response.data) + setUser(response.data) } - }).catch(console.log); + }).catch(console.log) return () => { - isMounted = false; - }; - }, [onUpdate, uname]); + isMounted = false + } + }, [onUpdate, uname]) return ( <> <div className="userinfo"> @@ -59,7 +59,7 @@ export default function UserInfo({ uname, onUpdate, children }) { </div> {children} </> - ); + ) } /** @@ -67,22 +67,22 @@ export default function UserInfo({ uname, onUpdate, children }) { * @param {{user: import('../api').User}} props */ function Summary({ user }) { - const readUrl = `/${user.uname}/friends`; - const readersUrl = `/${user.uname}/readers`; - const blUrl = `/${user.uname}/bl`; - let read = user.read && <Link key={readUrl} to={readUrl}>I read: {user.read.length}</Link>; - let readers = user.readers && <Link key={readersUrl} to={readersUrl}>My readers: {user.readers.length}</Link>; - let mybl = user.statsMyBL && <Link key={blUrl} to={blUrl}>My blacklist: {user.statsMyBL}</Link>; - let presentItems = [read, readers, mybl].filter(Boolean); + const readUrl = `/${user.uname}/friends` + const readersUrl = `/${user.uname}/readers` + const blUrl = `/${user.uname}/bl` + let read = user.read && <Link key={readUrl} to={readUrl}>I read: {user.read.length}</Link> + let readers = user.readers && <Link key={readersUrl} to={readersUrl}>My readers: {user.readers.length}</Link> + let mybl = user.statsMyBL && <Link key={blUrl} to={blUrl}>My blacklist: {user.statsMyBL}</Link> + let presentItems = [read, readers, mybl].filter(Boolean) return ( <div className="msg-summary"> {presentItems.length > 0 && presentItems.reduce((prev, curr) => [prev, ' ', curr])} </div> - ); + ) } -const UserSummary = memo(Summary); +const UserSummary = memo(Summary) /** @@ -90,10 +90,10 @@ const UserSummary = memo(Summary); * @param {{ user: import('../api').User}} props */ export function UserLink(props) { - const [user, setUser] = useState(props.user); - const userRef = useRef(user); + const [user, setUser] = useState(props.user) + const userRef = useRef(user) useEffect(() => { - isMounted = true; + isMounted = true if (userRef.current.uri) { fetchUserUri(userRef.current.uri).then(remote_user => { if (isMounted) { @@ -102,7 +102,7 @@ export function UserLink(props) { uname: remote_user.preferredUsername, avatar: remote_user.icon && remote_user.icon.url, uri: userRef.current.uri - }); + }) } }).catch(() => { setUser({ @@ -110,13 +110,13 @@ export function UserLink(props) { uname: userRef.current.uri, uri: userRef.current.uri, avatar: defaultAvatar - }); - }); + }) + }) } return () => { - isMounted = false; - }; - }, [props.user]); + isMounted = false + } + }, [props.user]) return ( user.uid ? <Link key={user.uid} to={`/${user.uname}/`} className="info-avatar"> @@ -125,5 +125,5 @@ export function UserLink(props) { : <a href={user.uri} className="info-avatar"> <img src={user.avatar || defaultAvatar} />{user.uname} </a> - ); + ) } diff --git a/vnext/src/ui/Users.js b/vnext/src/ui/Users.js index 32ff2d03..e4fcba1f 100644 --- a/vnext/src/ui/Users.js +++ b/vnext/src/ui/Users.js @@ -1,15 +1,15 @@ -import { useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useState } from 'react' +import { useParams } from 'react-router-dom' -import UserInfo from './UserInfo'; -import Avatar from './Avatar'; -import { Helmet } from 'react-helmet'; +import UserInfo from './UserInfo' +import Avatar from './Avatar' +import { Helmet } from 'react-helmet' /** * Friends feed */ export function Friends() { - const params = useParams(); + const params = useParams() return ( <> <Helmet> @@ -17,14 +17,14 @@ export function Friends() { </Helmet> <Users uname={params.user} prop='read' /> </> - ); + ) } /** * Readers feed */ export function Readers() { - const params = useParams(); + const params = useParams() return ( <> <Helmet> @@ -32,7 +32,7 @@ export function Readers() { </Helmet> <Users uname={params.user} prop='readers' /> </> - ); + ) } /** @@ -40,7 +40,7 @@ export function Readers() { * @param {{uname: string, prop: string}} props */ function Users({ uname, prop }) { - const [user, setUser] = useState({ uid: 0, uname: uname }); + const [user, setUser] = useState({ uid: 0, uname: uname }) return ( <UserInfo uname={uname} onUpdate={setUser}> <div style={{ display: 'flex', flexWrap: 'wrap', flexDirection: 'row' }}> @@ -52,5 +52,5 @@ function Users({ uname, prop }) { } </div> </UserInfo> - ); + ) } diff --git a/vnext/src/ui/VisitorContext.js b/vnext/src/ui/VisitorContext.js index 240b709b..9740f9ca 100644 --- a/vnext/src/ui/VisitorContext.js +++ b/vnext/src/ui/VisitorContext.js @@ -1,18 +1,18 @@ -import { createContext, useContext, useState } from 'react'; +import { createContext, useContext, useState } from 'react' -const Visitor = createContext(); +const Visitor = createContext() /** @type {import('../api').SecureUser} */ const unknownUser = { uid: -1 -}; +} /** * @param { import('react').PropsWithChildren<{}> } props */ export function VisitorProvider({ children }) { - const state = useState(unknownUser); - return <Visitor.Provider value={state}>{children}</Visitor.Provider>; + const state = useState(unknownUser) + return <Visitor.Provider value={state}>{children}</Visitor.Provider> } /** @@ -23,9 +23,9 @@ export function VisitorProvider({ children }) { * ]} visitor hook */ export function useVisitor() { - const visitor = useContext(Visitor); + const visitor = useContext(Visitor) if (visitor === undefined) { - throw new Error('useVisitor must be used within a VisitorProvider'); + throw new Error('useVisitor must be used within a VisitorProvider') } - return visitor; + return visitor }
\ No newline at end of file diff --git a/vnext/src/ui/__tests__/Avatar.test.js b/vnext/src/ui/__tests__/Avatar.test.js index f454f6c7..7aea804c 100644 --- a/vnext/src/ui/__tests__/Avatar.test.js +++ b/vnext/src/ui/__tests__/Avatar.test.js @@ -1,14 +1,14 @@ -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom' -import Avatar from '../Avatar'; -import renderer from 'react-test-renderer'; +import Avatar from '../Avatar' +import renderer from 'react-test-renderer' test('Avatar renders correctly', () => { const component = renderer.create( <MemoryRouter> <Avatar user={{ uid: 1, uname: 'ugnich', avatar: 'https://juick.com/i/a/1-deadbeef.png' }} /> </MemoryRouter> - ); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); + ) + let tree = component.toJSON() + expect(tree).toMatchSnapshot() +}) diff --git a/vnext/src/ui/__tests__/MessageInput.test.js b/vnext/src/ui/__tests__/MessageInput.test.js index 0bfe2569..4af36b71 100644 --- a/vnext/src/ui/__tests__/MessageInput.test.js +++ b/vnext/src/ui/__tests__/MessageInput.test.js @@ -1,46 +1,46 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react' -import MessageInput from '../MessageInput'; +import MessageInput from '../MessageInput' test('Gives immediate focus on to textarea on load', async () => { - let draft = 'draft'; - render(<MessageInput text={draft} onSend={async () => { return true; }} />); - expect(screen.getByText(draft)).toHaveFocus(); -}); + let draft = 'draft' + render(<MessageInput text={draft} onSend={async () => { return true }} />) + expect(screen.getByText(draft)).toHaveFocus() +}) test('Submits on ctrl-enter and pass validation', async () => { - let result = false; - const onSend = jest.fn(async ({ body }) => { result = body === 'YO'; return result; }); - let draft = 'draft'; - render(<MessageInput onSend={onSend} text={draft} />); - let textarea = screen.getByText(draft); - fireEvent.change(textarea, { target: { value: 'HI' } }); + let result = false + const onSend = jest.fn(async ({ body }) => { result = body === 'YO'; return result }) + let draft = 'draft' + render(<MessageInput onSend={onSend} text={draft} />) + let textarea = screen.getByText(draft) + fireEvent.change(textarea, { target: { value: 'HI' } }) // this event should not submit fireEvent.keyPress(textarea, { charCode: 13, which: 13, keyCode: 13 - }); + }) // this event should submit fireEvent.keyPress(textarea, { charCode: 13, which: 13, keyCode: 13, ctrlKey: true - }); - expect(onSend).toHaveBeenCalledTimes(1); - expect(result).toBe(false); - fireEvent.change(textarea, { target: { value: 'YO' } }); + }) + expect(onSend).toHaveBeenCalledTimes(1) + expect(result).toBe(false) + fireEvent.change(textarea, { target: { value: 'YO' } }) fireEvent.keyPress(textarea, { charCode: 13, which: 13, keyCode: 13, ctrlKey: true - }); - expect(onSend).toHaveBeenCalledTimes(2); - expect(result).toBe(true); - await waitFor(() => expect(textarea).toHaveTextContent('')); - textarea.focus(); - expect(textarea).toHaveFocus(); -}); + }) + expect(onSend).toHaveBeenCalledTimes(2) + expect(result).toBe(true) + await waitFor(() => expect(textarea).toHaveTextContent('')) + textarea.focus() + expect(textarea).toHaveFocus() +}) diff --git a/vnext/src/ui/__tests__/UserLink.test.js b/vnext/src/ui/__tests__/UserLink.test.js index 6bb4da29..99ca42ce 100644 --- a/vnext/src/ui/__tests__/UserLink.test.js +++ b/vnext/src/ui/__tests__/UserLink.test.js @@ -1,10 +1,10 @@ -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom' -import { UserLink } from '../UserInfo'; -import renderer, { act } from 'react-test-renderer'; +import { UserLink } from '../UserInfo' +import renderer, { act } from 'react-test-renderer' test('UserLink renders correctly', async () => { - let component = null; + let component = null act(() => { component = renderer.create( <MemoryRouter> @@ -14,8 +14,8 @@ test('UserLink renders correctly', async () => { <UserLink user={{ uid: 0, uname: '', uri: 'https://example.com/u/test' }} /> </> </MemoryRouter> - ); - }); - let tree = component.toJSON(); - expect(tree).toMatchSnapshot(); -}); + ) + }) + let tree = component.toJSON() + expect(tree).toMatchSnapshot() +}) diff --git a/vnext/src/ui/helpers/BubbleStyle.js b/vnext/src/ui/helpers/BubbleStyle.js index d2886e1e..def60b62 100644 --- a/vnext/src/ui/helpers/BubbleStyle.js +++ b/vnext/src/ui/helpers/BubbleStyle.js @@ -4,9 +4,9 @@ * @returns { import('react').CSSProperties} CSS properties */ export function chatItemStyle(me, msg) { - const user = msg.user; - const isMe = me.uid === user.uid; - const alignment = isMe ? 'flex-end' : 'flex-start'; + const user = msg.user + const isMe = me.uid === user.uid + const alignment = isMe ? 'flex-end' : 'flex-start' return { padding: '3px 6px', listStyle: 'none', @@ -14,5 +14,5 @@ export function chatItemStyle(me, msg) { display: 'flex', flexDirection: 'column', alignItems: alignment - }; + } } diff --git a/vnext/src/utils/embed.js b/vnext/src/utils/embed.js index f16342cf..afec71b4 100644 --- a/vnext/src/utils/embed.js +++ b/vnext/src/utils/embed.js @@ -2,21 +2,21 @@ function htmlEscape(html) { return html.replace(/&/g, '&') .replace(/"/g, '"') .replace(/</g, '<') - .replace(/>/g, '>'); + .replace(/>/g, '>') } function insertAfter(newNode, referenceNode) { - referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling) } function setContent(containerNode, ...newNodes) { - removeAllFrom(containerNode); - newNodes.forEach(n => containerNode.appendChild(n)); - return containerNode; + removeAllFrom(containerNode) + newNodes.forEach(n => containerNode.appendChild(n)) + return containerNode } function removeAllFrom(fromNode) { - fromNode.innerHTML = ''; + fromNode.innerHTML = '' } // rules :: [{pr: number, re: RegExp, with: string}] @@ -24,96 +24,96 @@ function removeAllFrom(fromNode) { // 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++; } + 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)]; }) + 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)); + .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 [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); + ? (() => { 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 txt } - return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped + return ft(htmlEscape(txt), rules) // idStr above relies on the fact the text is escaped } function fixWwwLink(url) { - return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//'); + return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//') } function makeNewNode(embedType, aNode, reResult) { const withClasses = el => { if (embedType.className) { - el.classList.add(...embedType.className.split(' ')); + el.classList.add(...embedType.className.split(' ')) } - return el; - }; - return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div'))); + 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; + 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.setAttribute('data-ratio', ratio); - makeResizable(element, w => w * element.getAttribute('data-ratio')); + element.setAttribute('data-ratio', ratio) + makeResizable(element, w => w * element.getAttribute('data-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'; + el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px' } - }; - window.addEventListener('resize', () => setHeight(element)); - setHeight(element); + } + window.addEventListener('resize', () => setHeight(element)) + setHeight(element) } function extractDomain(url) { - const domainRe = /^(?:https?:\/\/)?(?:[^@/\n]+@)?(?:www\.)?([^:/\n]+)/i; - let result = domainRe.exec(url) || []; + const domainRe = /^(?:https?:\/\/)?(?:[^@/\n]+@)?(?:www\.)?([^:/\n]+)/i + let result = domainRe.exec(url) || [] if (result.length > 0) { - return result[1]; + return result[1] } } function urlReplace(match, p1, p2, p3) { - let isBrackets = (p1 !== undefined); + let isBrackets = (p1 !== undefined) return (isBrackets) ? `<a href="${fixWwwLink(p2 || p3)}">${p1}</a>` - : `<a href="${fixWwwLink(match)}">${extractDomain(match)}</a>`; + : `<a href="${fixWwwLink(match)}">${extractDomain(match)}</a>` } function urlReplaceInCode(match, p1, p2, p3) { - let isBrackets = (p1 !== undefined); + let isBrackets = (p1 !== undefined) return (isBrackets) ? `<a href="${fixWwwLink(p2 || p3)}">${match}</a>` - : `<a href="${fixWwwLink(match)}">${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="/m/${mid || messageId}${replyPart}">${match}</a>`; - }; + let replyPart = (rid && rid != '0') ? '#' + rid : '' + return `<a href="/m/${mid || messageId}${replyPart}">${match}</a>` + } } /** @@ -125,8 +125,8 @@ function messageReplyReplace(messageId) { * @returns {string} formatted message */ function juickFormat(txt, messageId, isCode) { - const urlRe = /(?:\[([^\][]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))/gi; - const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, ''); + const urlRe = /(?:\[([^\][]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))/gi + const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, '') return (isCode) ? formatText(txt, [ { pr: 1, re: urlRe, with: urlReplaceInCode }, @@ -142,7 +142,7 @@ function juickFormat(txt, messageId, isCode) { { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<i>', '</i>'] }, { pr: 2, re: /\b_([^\n]+?)_((?=\s)|(?=$)|(?=[!"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<u>', '</u>'] }, { pr: 3, re: /\n/g, with: '<br/>' }, - ]); + ]) } /** * @external RegExpExecArray @@ -179,9 +179,9 @@ function getEmbeddableLinkTypes() { re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;?=]*)?$/i, makeNode: function(aNode, reResult, div) { // dirty fix for dropbox urls - let url = aNode.href.endsWith('dl=0') ? aNode.href.replace('dl=0', 'raw=1') : aNode.href; - div.innerHTML = `<a href="${url}"><img src="${url}"></a>`; - return div; + let url = aNode.href.endsWith('dl=0') ? aNode.href.replace('dl=0', 'raw=1') : aNode.href + div.innerHTML = `<a href="${url}"><img src="${url}"></a>` + return div } }, { @@ -190,8 +190,8 @@ function getEmbeddableLinkTypes() { className: 'picture compact', 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; + div.innerHTML = `<a href="${aNode.href}"><img src="${aNode.href}"></a>` + return div } }, { @@ -200,8 +200,8 @@ function getEmbeddableLinkTypes() { className: 'video compact', 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; + div.innerHTML = `<video src="${aNode.href}" title="${aNode.href}" controls></video>` + return div } }, { @@ -210,8 +210,8 @@ function getEmbeddableLinkTypes() { className: 'audio singleColumn', 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; + div.innerHTML = `<audio src="${aNode.href}" title="${aNode.href}" controls></audio>` + return div } }, { @@ -220,37 +220,37 @@ function getEmbeddableLinkTypes() { className: 'youtube resizableV singleColumn', 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 [, v, args, plist] = reResult; - let iframeUrl; + let [, v, args, plist] = reResult + let iframeUrl if (plist) { - iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + 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]); + .forEach(z => pp[z[0]] = z[1]) let embedArgs = { rel: '0', enablejsapi: '1', origin: `${window.location.protocol}//${window.location.host}` - }; + } 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))); + 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; + embedArgs['list'] = pp.list } - v = v || pp.v; + v = v || pp.v let argsStr = Object.keys(embedArgs) .map(k => `${k}=${embedArgs[k]}`) - .join('&'); - iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`; + .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); + let iframe = makeIframe(iframeUrl, '100%', '360px') + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0) + return setContent(div, iframe) } }, { @@ -259,9 +259,9 @@ function getEmbeddableLinkTypes() { className: 'vimeo resizableV', 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); + let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px') + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0) + return setContent(div, iframe) } }, { @@ -273,9 +273,9 @@ function getEmbeddableLinkTypes() { fetch('https://beta.juick.com/api/oembed?url=' + reResult[0]) .then(response => response.json()) .then(json => { - div.innerHTML = json.html; - }).catch(console.log); - return div; + div.innerHTML = json.html + }).catch(console.log) + return div } }, { @@ -284,10 +284,10 @@ function getEmbeddableLinkTypes() { className: 'picture compact', re: /https?:\/\/www\.?instagram\.com(\/p\/\w+)\/?/i, makeNode: function(aNode, reResult, div) { - let [, postId] = reResult; - let mediaUrl = `https://instagr.am${postId}/media`; - div.innerHTML = `<a href="${aNode.href}"><img src="${mediaUrl}"></a>`; - return div; + let [, postId] = reResult + let mediaUrl = `https://instagr.am${postId}/media` + div.innerHTML = `<a href="${aNode.href}"><img src="${mediaUrl}"></a>` + return div } }, { @@ -296,19 +296,19 @@ function getEmbeddableLinkTypes() { className: 'tg compact', re: /https?:\/\/t\.me\/(\S+)/i, makeNode: function(aNode, reResult, div) { - let [, post] = reResult; + let [, post] = reResult // innerHTML cannot insert scripts, so... - let script = document.createElement('script'); - script.src = 'https://telegram.org/js/telegram-widget.js?18'; - script.setAttribute('data-telegram-post', post); - script.setAttribute('data-tme-mode', 'data-tme-mode'); - script.setAttribute('data-width', '100%'); - script.charset = 'utf-8'; - div.appendChild(script); - return div; + let script = document.createElement('script') + script.src = 'https://telegram.org/js/telegram-widget.js?18' + script.setAttribute('data-telegram-post', post) + script.setAttribute('data-tme-mode', 'data-tme-mode') + script.setAttribute('data-width', '100%') + script.charset = 'utf-8' + div.appendChild(script) + return div } }, - ]; + ] } /** @@ -320,38 +320,38 @@ function getEmbeddableLinkTypes() { * @returns { boolean } `true` when some link was embedded */ function embedLink(aNode, linkTypes, container, afterNode = false) { - 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 + 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) { anyEmbed = linkTypes.some((linkType) => { - let reResult = linkType.re.exec(aNode.href); + 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 (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); + insertAfter(newNode, afterNode) } else { - container.appendChild(newNode); + container.appendChild(newNode) } - aNode.classList.add('embedLink'); - return true; + aNode.classList.add('embedLink') + return true } - }); + }) } - return anyEmbed; + return anyEmbed } function embedLinks(aNodes, container) { - let anyEmbed = false; - let embeddableLinkTypes = getEmbeddableLinkTypes(); + let anyEmbed = false + let embeddableLinkTypes = getEmbeddableLinkTypes() Array.from(aNodes).forEach(aNode => { - let isEmbedded = embedLink(aNode, embeddableLinkTypes, container); - anyEmbed = anyEmbed || isEmbedded; - }); - return anyEmbed; + let isEmbedded = embedLink(aNode, embeddableLinkTypes, container) + anyEmbed = anyEmbed || isEmbedded + }) + return anyEmbed } /** @@ -364,19 +364,19 @@ function embedLinks(aNodes, container) { * @param {string} allLinksSelector */ export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { - let allLinks = x.querySelectorAll(allLinksSelector); + let allLinks = x.querySelectorAll(allLinksSelector) - let existingContainer = x.querySelector('div.embedContainer'); + let existingContainer = x.querySelector('div.embedContainer') if (existingContainer) { - embedLinks(allLinks, existingContainer); + embedLinks(allLinks, existingContainer) } else { - let embedContainer = document.createElement('div'); - embedContainer.className = 'embedContainer'; + let embedContainer = document.createElement('div') + embedContainer.className = 'embedContainer' - let anyEmbed = embedLinks(allLinks, embedContainer); + let anyEmbed = embedLinks(allLinks, embedContainer) if (anyEmbed) { - let beforeNode = x.querySelector(beforeNodeSelector); - x.insertBefore(embedContainer, beforeNode); + let beforeNode = x.querySelector(beforeNodeSelector) + x.insertBefore(embedContainer, beforeNode) } } } @@ -385,11 +385,11 @@ export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { * Embed all the links in all messages/replies on the page. */ export function embedAll() { - let beforeNodeSelector = '.msg-txt + *'; - let allLinksSelector = '.msg-txt a'; + let beforeNodeSelector = '.msg-txt + *' + let allLinksSelector = '.msg-txt a' Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => { - embedLinksToX(msg, beforeNodeSelector, allLinksSelector); - }); + embedLinksToX(msg, beforeNodeSelector, allLinksSelector) + }) } /** * Embed URLs to container @@ -397,8 +397,8 @@ export function embedAll() { * @param {HTMLDivElement} embedContainer */ export function embedUrls(urls, embedContainer) { - embedLinks(urls, embedContainer); + embedLinks(urls, embedContainer) } -export const format = juickFormat; +export const format = juickFormat diff --git a/vnext/webpack.config.js b/vnext/webpack.config.js index 65f2aad6..241cc431 100644 --- a/vnext/webpack.config.js +++ b/vnext/webpack.config.js @@ -1,13 +1,13 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const ESLintPlugin = require('eslint-webpack-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const TerserPlugin = require('terser-webpack-plugin'); +const ESLintPlugin = require('eslint-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const TerserPlugin = require('terser-webpack-plugin') module.exports = () => { - const node_env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development'; - const dev = node_env !== 'production'; + const node_env = process.env.NODE_ENV ? process.env.NODE_ENV : 'development' + const dev = node_env !== 'production' const config = { mode: node_env, devtool: dev ? 'source-map' : false, @@ -75,7 +75,7 @@ module.exports = () => { symlinks: false, extensions: ['.js'] } - }; + } if (dev) { config.plugins.push( new ESLintPlugin({ @@ -84,14 +84,14 @@ module.exports = () => { failOnWarning: false, failOnError: true, fix: false - })); + })) config.devServer = { hot: true, historyApiFallback: true, client: { overlay: true } - }; + } } config.optimization = { minimize: !dev, @@ -103,6 +103,6 @@ module.exports = () => { terserOptions: {}, }), ] - }; - return config; -}; + } + return config +} |