import elementClosest from 'element-closest'
import 'formdata-polyfill'
import 'classlist.js'
import 'whatwg-fetch'
import 'core-js/stable'
import { embedLinksToX, embedAll, format } from '../../../vnext/src/utils/embed'
import renderIcons from './icon'
import svg4everybody from 'svg4everybody'
import { createRoot } from 'react-dom/client'
import { useState, useEffect } from 'react'
import Message from '../../../vnext/src/ui/Message'
import { VisitorProvider } from '../../../vnext/src/ui/VisitorContext'
import { CookiesProvider } from 'react-cookie'
/**
* Autosize textarea
* @param {HTMLTextAreaElement} el textarea element
*/
function autosize(el) {
let offset = el.offsetHeight - el.clientHeight
el.addEventListener('input', (ev) => {
const textarea = /** @type {HTMLTextAreaElement} */ (ev.target)
textarea.style.height = 'auto'
textarea.style.height = (textarea.scrollHeight + offset) + 'px'
})
}
/**
* Display an icon from the evil-icons set
* @param {string} name Icon name from the iconset
* @returns {string} HTML markup for the selected icon
*/
function evilIcon(name) {
return `
`
}
/* eslint-disable only-ascii/only-ascii */
const translations = {
'en': {
'message.inReplyTo': 'in reply to',
'message.reply': 'Reply',
'message.likeThisMessage?': 'Recommend this message?',
'postForm.pleaseInputMessageText': 'Please input message text',
'postForm.upload': 'Upload',
'postForm.newMessage': 'New message...',
'postForm.imageLink': 'Link to image',
'postForm.imageFormats': 'JPG/PNG, up to 100 MB',
'postForm.or': 'or',
'postForm.tags': 'Tags (space separated)',
'postForm.submit': 'Send',
'comment.writeComment': 'Write a comment...',
'shareDialog.linkToMessage': 'Link to message',
'shareDialog.messageNumber': 'Message number',
'shareDialog.share': 'Share',
'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself',
'loginDialog.registeredAlready': 'Registered already?',
'loginDialog.username': 'Username',
'loginDialog.password': 'Password',
'loginDialog.facebook': 'Login with Facebook',
'loginDialog.vk': 'Login with VK',
'loginDialog.email': 'Registration',
'error.error': 'Error',
'settings.notifications.granted': 'Enabled',
'settings.notifications.denied': 'Denied',
'settings.notifications.request': 'Enable',
},
'ru': {
'message.inReplyTo': 'в ответ на',
'message.reply': 'Ответить',
'message.likeThisMessage?': 'Рекомендовать это сообщение?',
'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения',
'postForm.upload': 'загрузить',
'postForm.newMessage': 'Новое сообщение...',
'postForm.imageLink': 'Ссылка на изображение',
'postForm.imageFormats': 'JPG/PNG, до 100 Мб',
'postForm.or': 'или',
'postForm.tags': 'Теги (через пробел)',
'postForm.submit': 'Отправить',
'comment.writeComment': 'Написать комментарий...',
'shareDialog.linkToMessage': 'Ссылка на сообщение',
'shareDialog.messageNumber': 'Номер сообщения',
'shareDialog.share': 'Поделиться',
'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь',
'loginDialog.registeredAlready': 'Уже зарегистрированы?',
'loginDialog.username': 'Имя пользователя',
'loginDialog.password': 'Пароль',
'loginDialog.facebook': 'Войти через Facebook',
'loginDialog.vk': 'Войти через ВКонтакте',
'loginDialog.email': 'Регистрация',
'error.error': 'Ошибка',
'settings.notifications.granted': 'Подключено',
'settings.notifications.denied': 'Запрещено',
'settings.notifications.request': 'Подключить',
}
}
/* eslint-enable only-ascii/only-ascii */
/**
* Detect window language
* @returns {string} Detected language
*/
function getLang() {
return (window.navigator.languages && window.navigator.languages[0])
|| window.navigator['userLanguage']
|| window.navigator.language
}
function i18n(key = '', lang = undefined) {
const fallbackLang = 'ru'
lang = lang || getLang().split('-')[0]
return (translations[lang] && translations[lang][key])
|| translations[fallbackLang][key]
|| key
}
/** @type { EventSource } */
var es
var pageTitle
function initES() {
if (!('EventSource' in window)) {
return
}
let url = '/api/events'
let hash = document.getElementById('body').getAttribute('data-hash')
if (hash) {
url += '?hash=' + hash
}
es = new EventSource(url)
es.onopen = function() {
console.log('online')
if (!document.querySelector('#wsthread')) {
var d = document.createElement('div')
d.id = 'wsthread'
d.addEventListener('click', nextReply)
document.querySelector('body').appendChild(d)
pageTitle = document.title
}
}
es.addEventListener('msg', msg => {
try {
var jsonMsg = JSON.parse(msg.data)
console.log('data: ' + msg.data)
if (jsonMsg.service) {
return
}
wsIncomingReply(jsonMsg)
} catch (err) {
console.log(err)
}
})
}
function wsIncomingReply(msg) {
let content = document.getElementById('content')
if (!content) { return }
let pageMID = content.getAttribute('data-mid')
if (!pageMID || pageMID != msg.mid) { return }
let msgNum = '/' + msg.rid
if (msg.replyto > 0) {
msgNum += ` ${i18n('message.inReplyTo')} /${msg.replyto}`
}
let photoDiv = (msg.attach == null) ? '' : `
`
let msgContHtml = `
${format(msg.body, msg.mid, false)}
${photoDiv}
`
let li = document.createElement('li')
li.setAttribute('class', 'msg reply-new')
li.setAttribute('id', msg.rid)
li.innerHTML = msgContHtml
li.addEventListener('click', newReply)
li.addEventListener('mouseover', newReply)
li.querySelector('a.msg-reply-link').addEventListener('click', function(e) {
showCommentForm(msg.mid, msg.rid)
e.preventDefault()
})
embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a')
document.getElementById('replies').appendChild(li)
updateRepliesCounter()
}
function newReply(e) {
var li = e.target
li.classList.remove('reply-new')
li.removeEventListener('click', e)
li.removeEventListener('mouseover', e)
updateRepliesCounter()
}
function nextReply() {
var li = document.querySelector('#replies>li.reply-new')
if (li) {
li.classList.remove('reply-new')
li.removeEventListener('click', this)
li.children[0].scrollIntoView()
updateRepliesCounter()
}
}
function updateRepliesCounter() {
var replies = Array.from(document.querySelectorAll('#replies>li.reply-new')).length
var wsthread = document.getElementById('wsthread')
if (replies) {
wsthread.textContent = `${replies}`
wsthread.style.display = 'block'
document.title = '[' + replies + '] ' + pageTitle
} else {
wsthread.style.display = 'none'
document.title = pageTitle
}
}
/******************************************************************************/
/******************************************************************************/
/******************************************************************************/
/**
* Submit form on Ctrl+Enter
* @param {HTMLElement} formEl Target form element
* @param {KeyboardEvent} ev Keyboard event
*/
function postformListener(formEl, ev) {
if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) {
let form = formEl.closest('form')
if (!form.onsubmit || form.submit()) {
/** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]')).click()
}
}
}
/**
* Close dialog on Esc
* @param {KeyboardEvent} ev Keyboard event
*/
function closeDialogListener(ev) {
ev = ev || /** @type {KeyboardEvent} */ (window.event)
if (ev.keyCode == 27) {
closeDialog()
}
}
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText)
}
return response
}
/**
*
* @param {number} mid message id
* @param {string} rid reply id
*/
function showCommentForm(mid, rid) {
let reply = /** @type { HTMLElement } */ (document.getElementById(rid))
let formTarget = reply.querySelector('div.msg-cont .msg-comment-target')
if (formTarget) {
let formHtml = `
`
formTarget.insertAdjacentHTML('afterend', formHtml)
formTarget.remove()
let form = /** @type {HTMLFormElement} */ (reply.querySelector('form'))
let submitButton = /** @type {HTMLInputElement} */ (form.querySelector('input[type="submit"]'))
let attachButton = /** @type {HTMLInputElement} */ (form.querySelector('.msg-comment .attach-photo'))
attachButton.addEventListener('click', e => attachCommentPhoto(/** @type {HTMLDivElement} */(e.target)))
let textarea = /** @type {HTMLTextAreaElement} */ (form.querySelector('.msg-comment textarea'))
textarea.addEventListener('keypress', e => postformListener(/** @type {HTMLElement} */(e.target), e))
autosize(textarea)
let validateMessage = () => {
let len = textarea.value.length
if (len > 4096) { return 'Message is too long' }
return ''
}
form.addEventListener('submit', e => {
let validationResult = validateMessage()
if (validationResult) {
e.preventDefault()
alert(validationResult)
return false
}
submitButton.disabled = true
let formData = new FormData(form)
fetch('/api/comment' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'POST',
body: formData,
credentials: 'omit'
}).then(handleErrors)
.then(response => response.json())
.then(result => {
if (result.newMessage) {
window.location.hash = `#${result.newMessage.rid}`
} else {
alert(result.text)
}
window.location.reload()
}).catch(error => {
alert(error.message)
})
e.preventDefault()
})
}
/** @type {HTMLTextAreaElement} */ (reply.querySelector('.msg-comment textarea')).focus()
}
function attachInput() {
let inp = document.createElement('input')
inp.setAttribute('type', 'file')
inp.setAttribute('name', 'attach')
inp.setAttribute('accept', 'image/jpeg,image/png')
inp.style.visibility = 'hidden'
return inp
}
/**
* "Attach" button
* @param {HTMLDivElement} div element attach to
*/
function attachCommentPhoto(div) {
let input = div.querySelector('input')
if (input) {
input.remove()
div.classList.remove('attach-photo-active')
} else {
let newInput = attachInput()
newInput.addEventListener('change', function() {
div.classList.add('attach-photo-active')
})
newInput.click()
div.appendChild(newInput)
}
}
function showPhotoDialog(fname) {
let width = window.innerWidth
let height = window.innerHeight
let minDimension = (width < height) ? width : height
if (minDimension < 640) {
return true // no dialog, open the link
} else if (minDimension < 1280) {
openDialog(``, true)
return false
} else {
openDialog(``, true)
return false
}
}
function openDialog(html, image) {
var dialogHtml = `
`
let body = /** @type {HTMLElement} */ (document.querySelector('body'))
body.classList.add('dialog-opened')
body.insertAdjacentHTML('afterbegin', dialogHtml)
if (image) {
let header = /** @type {HTMLElement} */ (document.querySelector('#dialog_header'))
header.classList.add('header_image')
}
document.addEventListener('keydown', closeDialogListener)
document.querySelector('#dialogb').addEventListener('click', closeDialog)
document.querySelector('#dialogc').addEventListener('click', closeDialog)
}
function closeDialog() {
document.querySelector('body').classList.remove('dialog-opened')
document.querySelector('#dialogb').remove()
document.querySelector('#dialogt').remove()
}
function checkUsername() {
var uname = document.querySelector('#username').textContent,
style = /** @type {HTMLElement} */ (document.querySelector('#username')).style
fetch('/api/users?uname=' + uname)
.then(handleErrors)
.then(function() {
style.background = '#FFCCCC'
})
.catch(function() {
style.background = '#CCFFCC'
})
}
/******************************************************************************/
function resultMessage(str) {
var result = document.createElement('p')
result.textContent = str
return result
}
function likeMessage(e, mid) {
if (confirm(i18n('message.likeThisMessage?'))) {
fetch('/api/like?mid=' + mid
+ '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'POST',
credentials: 'omit'
})
.then(handleErrors)
.then(function(response) {
if (response.ok) {
e.closest('article').appendChild(resultMessage('OK!'))
}
})
.catch(function() {
e.closest('article').appendChild(resultMessage(i18n('error.error')))
})
}
return false
}
function subscribeMessage(e, mid) {
fetch('/api/subscribe?mid=' + mid
+ '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'POST',
credentials: 'omit'
})
.then(handleErrors)
.then(function(response) {
if (response.ok) {
window.location.reload()
} else {
alert('Something went wrong :(')
}
})
.catch(error => {
alert(error.message)
})
return false
}
/******************************************************************************/
function setPrivacy(e, mid) {
fetch('/api/messages/set_privacy?mid=' + mid
+ '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
credentials: 'same-origin',
method: 'POST'
})
.then(handleErrors)
.then(function(response) {
if (response.ok) {
window.location.reload()
} else {
alert('Something went wrong :(')
}
})
.catch(console.err)
return false
}
function toggleWL(e, name) {
fetch('/api/users/wl?name=' + name
+ '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
credentials: 'same-origin',
method: 'POST'
})
.then(handleErrors)
.then(function(response) {
if (response.ok) {
window.location.reload()
} else {
alert('Something went wrong :(')
}
})
.catch(console.err)
return false
}
/*
function getTags() {
fetch('/api/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), {
credentials: 'omit'
})
.then(handleErrors)
.then(response => response.json())
.then(json => {
let tags = json.map(t => t.tag)
let input = document.getElementById('tags_input')
}).catch(console.error)
return false
}
*/
function addTag(tag) {
document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value
return false
}
function fetchUserUri(dataUri) {
let data = new FormData()
data.append('uri', dataUri)
return new Promise((resolve) => {
fetch('/api/u/', {
method: 'POST',
body: data
}).then(handleErrors)
.then(response => {
return response.json()
})
.then(json => {
resolve(json)
})
.catch(() => {
resolve({ uname: dataUri, uri: dataUri })
})
})
}
const registerServiceWorker = () => {
const publicKey = 'BPU0LniKKR0QiaUvILPd9AystmSOU8rWDZobxKm7IJN5HYxOSQdktRdc74TZvyRS9_kyUz7LDN6gUAmAVOmObAU'
navigator.serviceWorker.register('/sw.js', { scope: '/' })
navigator.serviceWorker.ready.then(reg => {
return reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
})
}).then(
sub => {
return [
{
type: 'web',
token: JSON.stringify(sub)
}
]
},
err => console.error(err)
).then(body => {
return fetch('/api/notifications?hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify(body)
})
}).then(response => {
console.log(response.status)
}).catch(console.error)
}
function notificationsCheckPermissions(button) {
console.log(Notification.permission)
switch (Notification.permission.toLowerCase()) {
case 'granted':
button.innerHTML = `${i18n('settings.notifications.granted')}`
button.disabled = true
registerServiceWorker()
break
case 'denied':
button.innerHTML = `${i18n('settings.notifications.denied')}`
button.disabled = true
break
case 'default':
button.innerHTML = `${i18n('settings.notifications.request')}`
}
}
const Feed = () => {
const [messages, setMessages] = useState(/** @type { import('../../../vnext/src/api').Message[]? } */ (null))
useEffect(() => {
let url = new URL(window.location.href)
fetch(url, {
headers: {
'Accept': 'application/json'
},
credentials: 'same-origin'
})
.then(response => response.json())
.then(json => {
setMessages(json)
})
.catch(console.error)
}, [])
return messages ? (
<>
{
messages.map(message => {}} /> )
}
>
) : 'Loading...'
}
/******************************************************************************/
function ready(fn) {
if (document.readyState != 'loading') {
fn()
} else {
document.addEventListener('DOMContentLoaded', fn)
}
}
ready(() => {
var insertPMButtons = function(e) {
e.target.classList.add('narrowpm')
e.target.parentNode.insertAdjacentHTML('afterend', '')
e.target.removeEventListener('click', insertPMButtons)
e.preventDefault()
};
/** @type {HTMLTextAreaElement[]} */ (Array.from(document.querySelectorAll('textarea.replypm'))).forEach(function(e) {
e.addEventListener('click', insertPMButtons)
e.addEventListener('keypress', function(e) {
postformListener(/** @type {HTMLElement} */(e.target), e)
})
});
/** @type {HTMLTextAreaElement[]} */ (Array.from(document.querySelectorAll('#postmsg textarea'))).forEach(function(e) {
e.addEventListener('keypress', function(e) {
postformListener(/** @type {HTMLElement} */(e.target), e)
})
})
var content = document.getElementById('content')
if (content) {
var pageMID = +content.getAttribute('data-mid')
if (pageMID > 0) {
embedAll()
Array.from(document.querySelectorAll('textarea')).forEach((ta) => {
autosize(ta)
})
Array.from(document.querySelectorAll('li.msg')).forEach(li => {
let showReplyFormBtn = li.querySelector('.a-thread-comment')
if (showReplyFormBtn) {
showReplyFormBtn.addEventListener('click', function(e) {
showCommentForm(pageMID, li.id)
e.preventDefault()
})
}
})
let opMessage = document.querySelector('.msgthread')
if (opMessage) {
let replyTextarea = /** @type {HTMLTextAreaElement} */ (opMessage.querySelector('textarea.reply'))
if (replyTextarea) {
replyTextarea.addEventListener('focus', () => showCommentForm(pageMID, 0))
replyTextarea.addEventListener('keypress', e => postformListener(/** @type {HTMLElement} */(e.target), e))
if (!window.location.hash) {
replyTextarea.focus()
}
}
}
}
}
var postmsg = /** @type {HTMLFormElement} */(document.getElementById('postmsg'))
if (postmsg) {
Array.from(document.querySelectorAll('a')).filter(t => t.href.indexOf('?') >= 0).forEach(t => {
t.addEventListener('click', e => {
let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1))
if (params.has('tag')) {
addTag(params.get('tag'))
e.preventDefault()
}
})
})
postmsg.addEventListener('submit', e => {
let formData = new FormData(postmsg)
fetch('/api/post' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'POST',
body: formData,
credentials: 'omit'
}).then(handleErrors)
.then(response => response.json())
.then(result => {
if (result.newMessage) {
window.location.href = new URL(`/m/${result.newMessage.mid}`, window.location.href).href
} else {
alert(result.text)
}
}).catch(error => {
alert(error.message)
})
e.preventDefault()
})
}
/** @type {HTMLFormElement[]} */ (Array.from(document.querySelectorAll('.pmmsg'))).forEach(pmmsg => {
pmmsg.addEventListener('submit', e => {
let formData = new FormData(pmmsg)
fetch('/api/pm' + '?hash=' + document.getElementById('body').getAttribute('data-hash'), {
method: 'POST',
body: formData,
credentials: 'omit'
}).then(handleErrors)
.then(response => response.json())
.then(result => {
if (result.to) {
window.location.href = new URL('/pm/sent', window.location.href).href
} else {
alert('Something went wrong :(')
}
})
.catch(error => {
alert(error.message)
})
e.preventDefault()
})
})
Array.from(document.querySelectorAll('.l .a-privacy')).forEach(function(e) {
e.addEventListener('click', function(e) {
setPrivacy(
e.target,
document.getElementById('content').getAttribute('data-mid'))
e.preventDefault()
})
})
Array.from(document.querySelectorAll('.a-vip')).forEach(function(e) {
e.addEventListener('click', function(e) {
toggleWL(
e.target,
e.target.closest('[data-name]').getAttribute('data-name'))
e.preventDefault()
})
})
Array.from(document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]')).forEach(function(el) {
el.addEventListener('click', function(e) {
let fname = /** @type {HTMLElement} */ (e.target).closest('[data-fname]').getAttribute('data-fname')
if (!showPhotoDialog(fname)) {
e.preventDefault()
}
})
})
var username = document.getElementById('username')
if (username) {
username.addEventListener('blur', function() {
checkUsername()
})
}
Array.from(document.querySelectorAll('.l .a-like')).forEach(function(e) {
e.addEventListener('click', function(e) {
likeMessage(
e.target,
/** @type {HTMLElement} */(e.target).closest('article').getAttribute('data-mid'))
e.preventDefault()
})
})
Array.from(document.querySelectorAll('.l .a-sub')).forEach(function(e) {
e.addEventListener('click', function(e) {
subscribeMessage(
e.target,
document.getElementById('content').getAttribute('data-mid'))
e.preventDefault()
})
})
var unfoldall = document.getElementById('unfoldall')
if (unfoldall) {
unfoldall.addEventListener('click', function(e) {
/** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('#replies>li'))).forEach(function(e) {
e.style.display = 'block'
});
/** @type {HTMLElement[]} */ (Array.from(document.querySelectorAll('#replies .msg-comments'))).forEach(function(e) {
e.style.display = 'none'
})
e.preventDefault()
})
}
Array.from(document.querySelectorAll('article')).forEach(function(article) {
if (Array.prototype.some.call(
Array.from(article.querySelectorAll('.msg-tags a')),
function(a) {
return a.textContent === 'NSFW'
}
)) {
article.classList.add('nsfw')
}
})
Array.from(document.querySelectorAll('[data-uri]')).forEach(el => {
let dataUri = el.getAttribute('data-uri') || ''
if (dataUri) {
setTimeout(() => fetchUserUri(dataUri).then(user => {
let header = el.closest('.msg-header')
Array.from(header.querySelectorAll('.a-username')).forEach(a => {
a.setAttribute('href', user.url || user.uri)
let img = a.querySelector('img')
if (img && user.avatar) {
img.setAttribute('src', user.avatar)
img.setAttribute('alt', user.uname)
}
let textNode = a.childNodes[0]
if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) {
let uname = document.createTextNode(user.uname)
a.replaceChild(uname, a.firstChild)
}
})
}), 100)
}
})
Array.from(document.querySelectorAll('[data-user-uri]')).forEach(el => {
let dataUri = el.getAttribute('href') || ''
if (dataUri) {
setTimeout(() => fetchUserUri(dataUri).then(user => {
let textNode = el.childNodes[0]
if (textNode.nodeType === Node.TEXT_NODE && textNode.nodeValue.trim().length > 0) {
let uname = document.createTextNode(`@${user.uname}`)
el.replaceChild(uname, el.firstChild)
el.setAttribute('href', user.url || user.uri)
}
}), 100)
}
})
let location = window.location.href;
/** @type {HTMLLinkElement[]} */ (Array.from(document.querySelectorAll('#header_wrapper a'))).forEach(el => {
if (el.href === location) {
el.classList.add('active')
el.setAttribute('disabled', 'disabled')
}
})
initES()
addEventListener('beforeunload', () => {
if (es) {
es.close()
}
})
const button = document.getElementById('notifications_toggle')
if (button) {
button.addEventListener('click', () => {
Notification.requestPermission(() => { notificationsCheckPermissions(button) })
})
notificationsCheckPermissions(button)
}
let root = document.getElementById('feed')
createRoot(root).render(
)
})