mirror of
https://github.com/luxfi/pics.git
synced 2026-10-30 15:22:15 +00:00
280 lines
9.2 KiB
JavaScript
280 lines
9.2 KiB
JavaScript
/* 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();
|
||
|
||
});
|