Files

280 lines
9.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* globals setLocalStorage Dragable threadwatcher watchedthread */
class ThreadWatcher {
init() {
//dont bother loading if no footer, must be minimal view
this.footer = document.getElementById('bottom');
if (!this.footer) {
return;
}
//read the watchlist map and minimised state from localstorage
this.watchListMap = new Map(JSON.parse(localStorage.getItem('watchlist')));
this.minimised = localStorage.getItem('threadwatcher-minimise') === 'true';
this.threadMatch = window.location.pathname.match(/^\/(\w+)(?:\/manage)?\/thread\/(\d+)\.html$/);
//call the updatehandler when storage changes in another context
window.addEventListener('storage', e => this.storageEventHandler(e));
//setup the settings menu and "clear" button
this.settingsInput = document.getElementById('watchlist-setting');
this.clearButton = document.getElementById('watchlist-clear');
this.clearButton.addEventListener('click', () => this.clear(), false);
//create and insert the watchlist
this.createList({minimised: this.minimised});
//add events for toggling minimised
this.minimiseButton = this.threadWatcher.firstChild.querySelector('.close');
this.minimiseButton.addEventListener('click', () => this.toggleMinimise());
//check if we are in a thread, and setup events for when the tab is focused/unfocused
this.isFocused = document.hasFocus();
if (this.threadMatch !== null) {
window.addEventListener('focus', () => this.focusHandler(this.threadMatch[1], this.threadMatch[2]));
window.addEventListener('blur', () => this.blurHandler());
if (this.isFocused === true) {
//set unread=0 for the current thread if the tab is focused
this.focusHandler(this.threadMatch[1], this.threadMatch[2]);
}
}
//start refreshing on an interval
this.refreshInterval = setInterval(() => this.refresh(), 60 * 1000);
}
//refresh all the threads in the watchlist map
refresh() {
this.watchListMap.size > 0 && console.log('refreshing watchlist');
for (let t of this.watchListMap.entries()) {
const [board, postId] = t[0].split('-');
const data = t[1];
this.fetchThread(board, postId, data);
}
}
//fetch a thread from the API and check if there are newer posts, and set unread if necessary.
async fetchThread(board, postId, data) {
let res, json;
try {
res = await fetch(`/${board}/thread/${postId}.json`);
json = await res.json();
} catch (e) { /* ignore */ }
if (json && json.replies) {
const newData = {
...data,
subject: (json.subject || json.nomarkup || `#${json.postId}`).substring(0, 25),
};
const updatedDate = new Date(data.updatedDate);
const newPosts = json.replies.filter(r => new Date(r.date) > updatedDate);
if (newPosts.length > 0) {
if (this.isFocused && this.threadMatch
&& this.threadMatch[1] === board && this.threadMatch[2] === postId) {
//unread=0 when fetching from inside a thread that is focused
newData.unread = 0;
} else {
newData.unread += newPosts.length;
//this.notify(newPosts);
}
}
if (newData.subject !== data.subject
|| newData.unread !== data.unread) {
newData.updatedDate = new Date();
const key = `${board}-${postId}`;
this.watchListMap.set(key, newData);
this.updateRow(board, postId, newData);
this.commit();
}
} else if (res && res.status === 404) {
console.log('removing 404 thread from watchlist');
this.remove(board, postId);
}
}
/*send notifications (if enabled and following other settings) for posts fetched by threadwatcher
notify(newPosts) {
if (notificationsEnabled) {
//i dont like fetching and creating the set each time, but it could be updated cross-context so its necessary (for now)
const yous = new Set(JSON.parse(localStorage.getItem('yous')));
for (const reply of newPosts) {
const isYou = yous.has(`${reply.board}-${reply.postId}`);
const quotesYou = reply.quotes.some(q => yous.has(`${reply.board}-${q.postId}`))
if (!isYou && !(notificationYousOnly && !quotesYou)) {
const notificationOptions = formatNotificationOptions(reply);
try {
new Notification(`Post in watched thread: ${document.title}`, notificationOptions);
} catch (e) {
console.log('failed to send notification', e);
}
}
}
}
}*/
//handle event for when storage changes in another tab and update the watcher to be in sync
storageEventHandler(e) {
if (e.storageArea === localStorage
&& e.key === 'watchlist') {
console.log('updating watchlist from another context');
const newMap = new Map(JSON.parse(e.newValue));
this.watchListMap.forEach((data, key) => {
if (!newMap.has(key)) {
const [board, postId] = key.split('-');
this.deleteRow(board, postId);
}
});
//let setOwnUnread = false;
newMap.forEach((data, key) => {
const [board, postId] = key.split('-');
const oldData = this.watchListMap.get(key);
if (!oldData) {
this.addRow(board, postId, data);
} else if (oldData && (oldData.unread !== data.unread
|| oldData.subject !== data.subject)) {
/*if (this.isFocused && this.threadMatch
&& this.threadMatch[1] === board && this.threadMatch[2] === postId) {
setOwnUnread = true;
data.unread = 0;
}*/
this.updateRow(board, postId, data);
}
});
this.watchListMap = new Map(JSON.parse(e.newValue));
this.setVisibility();
/*if (setOwnUnread === true) {
this.commit();
}*/
}
}
//handle event when current page becomes focused (only listens on thread pages) and set unread=0 for this thread
focusHandler(board, postId) {
this.isFocused = true;
const key = `${board}-${postId}`;
const data = this.watchListMap.get(key);
if (data && data.unread !== 0) {
data.unread = 0;
data.updatedDate = new Date();
this.watchListMap.set(key, data);
this.updateRow(board, postId, data);
this.commit();
}
}
//self explanatory
blurHandler() {
this.isFocused = false;
}
//commit any changes to localstorage and settings menu box (readonly)
commit() {
const mapSpread = [...this.watchListMap];
setLocalStorage('watchlist', JSON.stringify(mapSpread));
this.settingsInput.value = mapSpread;
this.setVisibility();
}
toggleMinimise() {
this.minimised = !this.minimised;
this.minimiseButton.textContent = this.minimised ? '[+]' : '[−]';
this.threadWatcher.classList.toggle('minimised');
setLocalStorage('threadwatcher-minimise', this.minimised);
}
//toggles watcher visibility
setVisibility() {
if (this.threadWatcher) {
this.threadWatcher.style.display = (this.watchListMap.size === 0 ? 'none' : null);
}
}
//create the actual thread watcher box and draghandle and insert it into the page
createList() {
const threadWatcherHtml = threadwatcher({ minimised: this.minimised });
this.footer.insertAdjacentHTML('afterend', threadWatcherHtml);
this.threadWatcher = document.getElementById('threadwatcher');
if (this.watchListMap.size === 0) {
this.threadWatcher.style.display = 'none';
}
//reuse the dragable class for something :^)
new Dragable('#threadwatcher-dragHandle', '#threadwatcher');
for (let t of this.watchListMap.entries()) {
const [board, postId] = t[0].split('-');
const data = t[1];
this.addRow(board, postId, data);
}
}
//add a thread to the watchlist map
add(board, postId, data) {
const key = `${board}-${postId}`;
if (this.watchListMap.has(key)) {
//dont add duplicates
return;
}
this.watchListMap.set(key, data);
this.addRow(board, postId, data);
this.commit();
}
//remove a thread from the watchlist map
remove(board, postId) {
this.deleteRow(board, postId);
this.watchListMap.delete(`${board}-${postId}`);
this.commit();
}
//remove all threads from the watchlist
clear() {
for (let t of this.watchListMap.entries()) {
const [board, postId] = t[0].split('-');
const data = t[1];
this.deleteRow(board, postId, data);
}
this.watchListMap = new Map();
this.commit();
}
//add the actual row html to the watcher
addRow(board, postId, data) {
const isCurrentThread = this.threadMatch != null
&& this.threadMatch[1] === board && this.threadMatch[2] === postId;
const watchListItemHtml = watchedthread({ watchedthread: { board, postId, ...data, isCurrentThread } });
this.threadWatcher.insertAdjacentHTML('beforeend', watchListItemHtml);
const watchedThreadElem = this.threadWatcher.lastChild;
const closeButton = watchedThreadElem.querySelector('.close');
//when x button clicked, call remove
closeButton.addEventListener('click', () => this.remove(board, postId));
}
//delete the actual row from the watcher
deleteRow(board, postId) {
const row = this.threadWatcher.querySelector(`[data-id="${board}-${postId}"]`);
row.remove();
}
//update a row in the watcher for new unread count
updateRow(board, postId, data) {
const row = this.threadWatcher.querySelector(`[data-id="${board}-${postId}"]`);
if (data.unread === 0) {
//remove the attribute completely to not show (0), we only want to show (1) or more
row.removeAttribute('data-unread');
} else {
row.setAttribute('data-unread', data.unread);
}
//subject *can* change rarely, if the op was edited
row.children[1].textContent = `/${board}/ - ${data.subject}`;
}
}
const threadWatcher = new ThreadWatcher();
window.addEventListener('settingsReady', () => {
threadWatcher.init();
});