|
|
|
let liveEnabled = localStorage.getItem('live') == 'true';
|
|
|
|
let scrollEnabled = localStorage.getItem('scroll') == 'true';
|
|
|
|
let socket;
|
|
|
|
let socketPingInterval;
|
|
|
|
let forceUpdate;
|
|
|
|
let newPost;
|
|
|
|
|
|
|
|
window.addEventListener('settingsReady', function(event) { //after domcontentloaded
|
|
|
|
|
|
|
|
let supportsWebSockets = 'WebSocket' in window || 'MozWebSocket' in window;
|
|
|
|
const livecolor = document.getElementById('livecolor');
|
|
|
|
const liveElem = document.getElementById('livetext');
|
|
|
|
const livetext = (isThread || isRecent) && liveElem ? liveElem.childNodes[1] : null;
|
|
|
|
let room = liveElem && liveElem.dataset.room;
|
|
|
|
const viewRawIp = liveElem && liveElem.dataset.viewRawIp === 'true';
|
|
|
|
const updateButton = document.getElementById('updatepostsbutton');
|
|
|
|
const updateLive = (message, color, showRelativeTime) => {
|
|
|
|
livecolor.style.backgroundColor = color;
|
|
|
|
livetext.nodeValue = `${message}`;
|
|
|
|
}
|
|
|
|
let lastPostIds = {};
|
|
|
|
let liveTimeout;
|
|
|
|
const postContainers = document.getElementsByClassName('post-container');
|
|
|
|
for (let i = 0; i < postContainers.length; i++) {
|
|
|
|
const postContainer = postContainers[i];
|
|
|
|
const { board, postId } = postContainer.dataset;
|
|
|
|
lastPostIds[board] = Math.max((lastPostIds[board] || 0), postId);
|
|
|
|
}
|
|
|
|
|
|
|
|
//add text before post-info to show posts deleted, moved, etc
|
|
|
|
const markPost = (data) => {
|
|
|
|
console.log('got mark post message', data);
|
|
|
|
const anchor = document.getElementById(data.postId);
|
|
|
|
const postContainer = anchor.nextSibling;
|
|
|
|
postContainer.classList.add('marked');
|
|
|
|
postContainer.setAttribute('data-mark', data.mark);
|
|
|
|
//handle any special cases for different marks
|
|
|
|
switch (data.type) {
|
|
|
|
case "delete":
|
|
|
|
case "move":
|
|
|
|
if (postContainer.classList.contains('op')) {
|
|
|
|
//moved or delete OPs then apply to whole thread
|
|
|
|
const postContainers = document.getElementsByClassName('post-container');
|
|
|
|
Array.from(postContainers).forEach(e => {
|
|
|
|
e.classList.add('marked')
|
|
|
|
e.setAttribute('data-mark', data.mark);
|
|
|
|
});
|
|
|
|
//remove new reply buttons and postform
|
|
|
|
document.getElementById('postform').remove();
|
|
|
|
const postButtons = document.getElementsByClassName('post-button');
|
|
|
|
Array.from(postButtons).forEach(e => e.remove());
|
|
|
|
//and disconnect socket
|
|
|
|
if (socket.connected === true) {
|
|
|
|
socket.disconnect();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
//nothing special
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
newPost = (data, options = {}) => {
|
|
|
|
//insert at end of thread, but insert at top for globalmanage
|
|
|
|
//console.log('got new post', data);
|
|
|
|
const postData = data;
|
|
|
|
lastPostIds[postData.board] = postData.postId;
|
|
|
|
//create a new post
|
|
|
|
const postHtml = post({
|
|
|
|
viewRawIp,
|
|
|
|
post: postData,
|
|
|
|
modview: isModView,
|
|
|
|
manage: (isRecent && !isGlobalRecent),
|
|
|
|
globalmanage: isGlobalRecent,
|
|
|
|
upLevel: isThread,
|
|
|
|
...extraLocals,
|
|
|
|
});
|
|
|
|
let insertPoint;
|
|
|
|
if (options.insertPoint) {
|
|
|
|
insertPoint = options.insertPoint;
|
|
|
|
} else if (isRecent) {
|
|
|
|
const firstHr = document.querySelector('hr');
|
|
|
|
const newHr = document.createElement('hr');
|
|
|
|
const threadWrapper = document.createElement('div');
|
|
|
|
threadWrapper.classList.add('thread');
|
|
|
|
insertPoint = threadWrapper;
|
|
|
|
firstHr.insertAdjacentElement('beforebegin', newHr);
|
|
|
|
newHr.insertAdjacentElement('afterend', threadWrapper);
|
|
|
|
} else {
|
|
|
|
insertPoint = document.querySelector('.thread');
|
|
|
|
}
|
|
|
|
insertPoint.insertAdjacentHTML(options.insertPosition || 'beforeend', postHtml);
|
|
|
|
if (isRecent) {
|
|
|
|
//cap the recent pages to 20 posts so they dont grow to infinity
|
|
|
|
Array.from(document.querySelectorAll('.thread'))
|
|
|
|
.slice(20)
|
|
|
|
.forEach(el => {
|
|
|
|
el.previousSibling.remove();
|
|
|
|
el.remove();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
//add backlink to quoted posts
|
|
|
|
for (let j = 0; j < postData.quotes.length; j++) {
|
|
|
|
const quoteData = postData.quotes[j];
|
|
|
|
const quotedPost = document.querySelector(`.post-container[data-post-id="${quoteData.postId}"][data-board="${postData.board}"]`);
|
|
|
|
if (quotedPost) { //check if exists, it wont necessarily, e.g. global/manage recent pages
|
|
|
|
let replies = quotedPost.querySelector('.replies');
|
|
|
|
if (!replies) {
|
|
|
|
const quotedPostData = quotedPost.querySelector('.post-data');
|
|
|
|
const newRepliesDiv = document.createElement('div');
|
|
|
|
newRepliesDiv.textContent = 'Replies: ';
|
|
|
|
newRepliesDiv.classList.add('replies', 'mt-5', 'ml-5');
|
|
|
|
quotedPostData.appendChild(newRepliesDiv);
|
|
|
|
replies = newRepliesDiv;
|
|
|
|
}
|
|
|
|
if (new RegExp(`>>${postData.postId}(\\s|$)`).test(replies.innerText)) {
|
|
|
|
//reply link already exists (probably from a late catch up)
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
const newReply = document.createElement('a');
|
|
|
|
const space = document.createTextNode(' ');
|
|
|
|
newReply.href = `/${postData.board}/${(isModView || isRecent) ? 'manage/' : ''}thread/${postData.thread || postData.postId}.html#${postData.postId}`;
|
|
|
|
newReply.textContent = `>>${postData.postId}`;
|
|
|
|
newReply.classList.add('quote');
|
|
|
|
replies.appendChild(newReply);
|
|
|
|
replies.appendChild(space);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const newPostAnchor = document.getElementById(postData.postId);
|
|
|
|
const newPostElement = newPostAnchor.nextSibling;
|
|
|
|
if (scrollEnabled) {
|
|
|
|
if (isGlobalRecent) {
|
|
|
|
window.scrollTo(0, 0); //recent pages are reverse sort, so just go to top
|
|
|
|
} else {
|
|
|
|
newPostAnchor.scrollIntoView(); //scroll to post if enabled;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const newPostEvent = new CustomEvent('addPost', {
|
|
|
|
detail: {
|
|
|
|
nonotify: options.nonotify,
|
|
|
|
post: newPostElement,
|
|
|
|
postId: postData.postId,
|
|
|
|
json: postData
|
|
|
|
}
|
|
|
|
});
|
|
|
|
//dispatch the event so quote click handlers, image expand, etc can be added in separate scripts by listening to the event
|
|
|
|
setTimeout(() => {
|
|
|
|
window.dispatchEvent(newPostEvent);
|
|
|
|
}, 50);
|
|
|
|
}
|
|
|
|
|
|
|
|
let jsonParts = window.location.pathname.replace(/\.html$/, '.json').split('/');
|
|
|
|
let jsonPath;
|
|
|
|
if (isModView) {
|
|
|
|
jsonParts.splice(2,1); //remove manage from json url
|
|
|
|
}
|
|
|
|
jsonPath = jsonParts.join('/');
|
|
|
|
const fetchNewPosts = async () => {
|
|
|
|
console.log('fetching posts from api');
|
|
|
|
updateLive('Fetching posts...', 'yellow');
|
|
|
|
let json;
|
|
|
|
let newPosts = [];
|
|
|
|
try {
|
|
|
|
json = await fetch(jsonPath).then(res => res.json());
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
const postsList = (json && json.replies) ? json.replies : json;
|
|
|
|
if (postsList && Array.isArray(postsList) && postsList.length > 0) {
|
|
|
|
newPosts = postsList.filter(r => {
|
|
|
|
return r.postId > (lastPostIds[r.board] || 0);
|
|
|
|
}); //filter to only newer posts
|
|
|
|
if (newPosts.length > 0) {
|
|
|
|
for (let i = 0; i < newPosts.length; i++) {
|
|
|
|
newPost(newPosts[i]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
updateLive('Updated', 'green');
|
|
|
|
return newPosts.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
let interval = 5000;
|
|
|
|
let intervalStart;
|
|
|
|
forceUpdate = async () => {
|
|
|
|
updateButton.disabled = true;
|
|
|
|
clearTimeout(liveTimeout);
|
|
|
|
if ((await fetchNewPosts()) > 0) {
|
|
|
|
interval = 5000;
|
|
|
|
} else {
|
|
|
|
interval = Math.min(interval*2, 90000);
|
|
|
|
}
|
|
|
|
setTimeout(() => {
|
|
|
|
updateButton.disabled = false;
|
|
|
|
}, 10000);
|
|
|
|
if (liveEnabled) {
|
|
|
|
intervalStart = Date.now();
|
|
|
|
liveTimeout = setTimeout(forceUpdate, interval);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
if (liveEnabled && intervalStart) {
|
|
|
|
const remaining = Math.abs((interval - (Date.now() - intervalStart))/1000);
|
|
|
|
updateButton.value = `Update (${remaining.toFixed(0)}s)`;
|
|
|
|
}
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
const enableLive = () => {
|
|
|
|
if (supportsWebSockets) {
|
|
|
|
updateButton.style.display = 'none';
|
|
|
|
if (!room) {
|
|
|
|
const roomParts = window.location.pathname.replace(/\.html$/, '').split('/');
|
|
|
|
room = `${roomParts[1]}-${roomParts[roomParts.length-1]}`;
|
|
|
|
}
|
|
|
|
socket = io({
|
|
|
|
transports: ['websocket'],
|
|
|
|
reconnectionAttempts: 3,
|
|
|
|
reconnectionDelay: 3000,
|
|
|
|
reconnectionDelayMax: 15000,
|
|
|
|
});
|
|
|
|
const socketPing = () => {
|
|
|
|
const pingStart = Date.now();
|
|
|
|
socket.volatile.emit('ping', () => {
|
|
|
|
const latency = Date.now() - pingStart;
|
|
|
|
updateLive(`Connected for live posts (${latency}ms)`, '#0de600');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
socket.on('connect', async () => {
|
|
|
|
console.log('socket connected');
|
|
|
|
await fetchNewPosts();
|
|
|
|
socket.emit('room', room);
|
|
|
|
clearInterval(socketPingInterval);
|
|
|
|
socketPingInterval = setInterval(socketPing, 20000);
|
|
|
|
});
|
|
|
|
socket.on('message', (message) => {
|
|
|
|
console.log(message, room);
|
|
|
|
if (message === 'joined') {
|
|
|
|
updateLive('Connected for live posts', '#0de600');
|
|
|
|
socketPing();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
socket.on('reconnect_attempt', () => {
|
|
|
|
updateLive('Attempting to reconnect...', 'yellow');
|
|
|
|
});
|
|
|
|
socket.on('disconnect', () => {
|
|
|
|
console.log('lost connection to room');
|
|
|
|
updateLive('Disconnected', 'red');
|
|
|
|
});
|
|
|
|
socket.on('reconnect', () => {
|
|
|
|
console.log('reconnected to room');
|
|
|
|
fetchNewPosts();
|
|
|
|
});
|
|
|
|
socket.on('error', (e) => {
|
|
|
|
updateLive('Socket error', 'orange');
|
|
|
|
console.error(e);
|
|
|
|
});
|
|
|
|
socket.on('connect_error', (e) => {
|
|
|
|
updateLive('Error connecting', 'orange');
|
|
|
|
console.error(e);
|
|
|
|
});
|
|
|
|
socket.on('reconnect_error', (e) => {
|
|
|
|
updateLive('Error reconnecting', 'orange');
|
|
|
|
console.error(e);
|
|
|
|
});
|
|
|
|
socket.on('reconnect_failed', (e) => {
|
|
|
|
updateLive('Failed reconnecting', 'orange');
|
|
|
|
console.error(e);
|
|
|
|
console.log('failed to reconnnect, falling back to polling')
|
|
|
|
socket.close();
|
|
|
|
supportsWebSockets = false;
|
|
|
|
enableLive();
|
|
|
|
});
|
|
|
|
socket.on('newPost', newPost);
|
|
|
|
socket.on('markPost', markPost);
|
|
|
|
} else {
|
|
|
|
//websocket not supported, update with polling to api
|
|
|
|
updateButton.removeAttribute('style');
|
|
|
|
forceUpdate();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const disableLive = () => {
|
|
|
|
updateButton.value = 'Update';
|
|
|
|
updateButton.removeAttribute('style');
|
|
|
|
clearTimeout(liveTimeout);
|
|
|
|
if (socket && supportsWebSockets) {
|
|
|
|
socket.disconnect();
|
|
|
|
}
|
|
|
|
updateLive('Live posts off', 'darkgray');
|
|
|
|
};
|
|
|
|
|
|
|
|
const liveSetting = document.getElementById('live-setting');
|
|
|
|
const toggleLive = () => {
|
|
|
|
liveEnabled = !liveEnabled;
|
|
|
|
liveEnabled ? enableLive() : disableLive();
|
|
|
|
console.log('toggling live posts', liveEnabled);
|
|
|
|
setLocalStorage('live', liveEnabled);
|
|
|
|
}
|
|
|
|
liveSetting.checked = liveEnabled;
|
|
|
|
liveSetting.addEventListener('change', toggleLive, false);
|
|
|
|
|
|
|
|
const scrollSetting = document.getElementById('scroll-setting');
|
|
|
|
const toggleScroll = () => {
|
|
|
|
scrollEnabled = !scrollEnabled;
|
|
|
|
console.log('toggling post scrolling', scrollEnabled);
|
|
|
|
setLocalStorage('scroll', scrollEnabled);
|
|
|
|
}
|
|
|
|
scrollSetting.checked = scrollEnabled;
|
|
|
|
scrollSetting.addEventListener('change', toggleScroll, false);
|
|
|
|
|
|
|
|
if ((isThread || isRecent) && updateButton) {
|
|
|
|
updateButton.addEventListener('click', forceUpdate);
|
|
|
|
liveEnabled ? enableLive() : disableLive();
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|