- cca085e60090ca21edf0dee6aa012fc4c949809a start of import/export - f816da146c32f010476872d15b58ec8301b9fdf2 start of changing stuff until I can get a bundle - c92adde147792356ff206107b2311590e8b2c054 first bundle without errors - e652dd2b785e355e0ac33566da7eaaaa19c7c539 Bundling works with ts files - 60fdb2539a757ca2f66258b21adf81246873893f meta info in compilation - 8ccae783cbf65ac186d5669dedd9f945f7608694 new build doesn't cause errors on page load as userscript - 6fa11c42a05572779870f94b7ef4ea8dac373450 work in progress: load userscript in browser and fix bugs - b15c557d483de544a38a28cb78f25139d1d8421f migrated yotsuba templates to plain js the old templates caused some variable be in a wrong scope after decaffeinate, causing them to be unreadable from the old template the old templates caused some variable be in a wrong scope after decaffeinate, causing them to be unreadable from the old template - 9d763e852fde74808ca14d5a8d6be45f51ae2765 update readme - 924eda8268bcfc4f1c0a83062ecd1d0d65bd92aa added more imports, and now the circular dependencies are haunting me - ddd2d23315d801c7deaa28313833e667698aadd3 jsx templates for escaped strings, more bug fixed from circular dependencies - fee484dd447820d908c77b1e9d31235ab95a481c some fixes, clarify jsx - e1d01d02eba5db2f604a5df786c525e95f32a2f9 Unpacked extension more fixes - 97d9090b712d20f7d851c82af84c65060f1a9c6e fixed class on post that caused catalog to appear empty - 96a2c7b4a1e69f5812d1e53b2e4c90f6d8447b02 A child class that's not supposed to run the parents constructor? That needs a workaround in es6 classes. - fc06b4e1b2769550d4c69377b84d3ccacdb2e013 changed jsx to make the tests pass - 7b317b2a0feabe8caa547c76baf0c908b21592f1 revert archive and banners to json
344 lines
10 KiB
JavaScript
344 lines
10 KiB
JavaScript
import Callbacks from "../classes/Callbacks";
|
|
import DataBoard from "../classes/DataBoard";
|
|
import RandomAccessList from "../classes/RandomAccessList";
|
|
import Get from "../General/Get";
|
|
import Header from "../General/Header";
|
|
import { g, Conf, d } from "../globals/globals";
|
|
import $ from "../platform/$";
|
|
import { debounce, SECOND } from "../platform/helpers";
|
|
import QuoteYou from "../Quotelinks/QuoteYou";
|
|
import Favicon from "./Favicon";
|
|
import ThreadWatcher from "./ThreadWatcher";
|
|
|
|
/*
|
|
* decaffeinate suggestions:
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/main/docs/suggestions.md
|
|
*/
|
|
var Unread = {
|
|
init() {
|
|
if ((g.VIEW !== 'thread') || (
|
|
!Conf['Unread Count'] &&
|
|
!Conf['Unread Favicon'] &&
|
|
!Conf['Unread Line'] &&
|
|
!Conf['Remember Last Read Post'] &&
|
|
!Conf['Desktop Notifications'] &&
|
|
!Conf['Quote Threading']
|
|
)) { return; }
|
|
|
|
if (Conf['Remember Last Read Post']) {
|
|
$.sync('Remember Last Read Post', enabled => Conf['Remember Last Read Post'] = enabled);
|
|
this.db = new DataBoard('lastReadPosts', this.sync);
|
|
}
|
|
|
|
this.hr = $.el('hr', {
|
|
id: 'unread-line',
|
|
className: 'unread-line'
|
|
}
|
|
);
|
|
this.posts = new Set();
|
|
this.postsQuotingYou = new Set();
|
|
this.order = new RandomAccessList();
|
|
this.position = null;
|
|
|
|
Callbacks.Thread.push({
|
|
name: 'Unread',
|
|
cb: this.node
|
|
});
|
|
|
|
return Callbacks.Post.push({
|
|
name: 'Unread',
|
|
cb: this.addPost
|
|
});
|
|
},
|
|
|
|
node() {
|
|
Unread.thread = this;
|
|
Unread.title = d.title;
|
|
Unread.lastReadPost = Unread.db?.get({
|
|
boardID: this.board.ID,
|
|
threadID: this.ID
|
|
}) || 0;
|
|
Unread.readCount = 0;
|
|
for (var ID of this.posts.keys) { if (+ID <= Unread.lastReadPost) { Unread.readCount++; } }
|
|
$.one(d, '4chanXInitFinished', Unread.ready);
|
|
$.on(d, 'PostsInserted', Unread.onUpdate);
|
|
$.on(d, 'ThreadUpdate', function(e) { if (e.detail[404]) { return Unread.update(); } });
|
|
const resetLink = $.el('a', {
|
|
href: 'javascript:;',
|
|
className: 'unread-reset',
|
|
textContent: 'Mark all unread'
|
|
}
|
|
);
|
|
$.on(resetLink, 'click', Unread.reset);
|
|
return Header.menu.addEntry({
|
|
el: resetLink,
|
|
order: 70
|
|
});
|
|
},
|
|
|
|
ready() {
|
|
if (Conf['Remember Last Read Post'] && Conf['Scroll to Last Read Post']) { Unread.scroll(); }
|
|
Unread.setLine(true);
|
|
Unread.read();
|
|
Unread.update();
|
|
$.on(d, 'scroll visibilitychange', Unread.read);
|
|
if (Conf['Unread Line']) { return $.on(d, 'visibilitychange', Unread.setLine); }
|
|
},
|
|
|
|
positionPrev() {
|
|
if (Unread.position) { return Unread.position.prev; } else { return Unread.order.last; }
|
|
},
|
|
|
|
scroll() {
|
|
// Let the header's onload callback handle it.
|
|
let hash;
|
|
if ((hash = location.hash.match(/\d+/)) && hash[0] in Unread.thread.posts) { return; }
|
|
|
|
let position = Unread.positionPrev();
|
|
while (position) {
|
|
var {bottom} = position.data.nodes;
|
|
if (!bottom.getBoundingClientRect().height) {
|
|
// Don't try to scroll to posts with display: none
|
|
position = position.prev;
|
|
} else {
|
|
Header.scrollToIfNeeded(bottom, true);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
reset() {
|
|
if (Unread.lastReadPost == null) { return; }
|
|
|
|
Unread.posts = new Set();
|
|
Unread.postsQuotingYou = new Set();
|
|
Unread.order = new RandomAccessList();
|
|
Unread.position = null;
|
|
Unread.lastReadPost = 0;
|
|
Unread.readCount = 0;
|
|
Unread.thread.posts.forEach(post => Unread.addPost.call(post));
|
|
|
|
$.forceSync('Remember Last Read Post');
|
|
if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {
|
|
Unread.db.set({
|
|
boardID: Unread.thread.board.ID,
|
|
threadID: Unread.thread.ID,
|
|
val: 0
|
|
});
|
|
}
|
|
|
|
Unread.updatePosition();
|
|
Unread.setLine();
|
|
return Unread.update();
|
|
},
|
|
|
|
sync() {
|
|
if (Unread.lastReadPost == null) { return; }
|
|
const lastReadPost = Unread.db.get({
|
|
boardID: Unread.thread.board.ID,
|
|
threadID: Unread.thread.ID,
|
|
defaultValue: 0
|
|
});
|
|
if (Unread.lastReadPost >= lastReadPost) { return; }
|
|
Unread.lastReadPost = lastReadPost;
|
|
|
|
const postIDs = Unread.thread.posts.keys;
|
|
for (let i = Unread.readCount, end = postIDs.length; i < end; i++) {
|
|
var ID = +postIDs[i];
|
|
if (!Unread.thread.posts.get(ID).isFetchedQuote) {
|
|
if (ID > Unread.lastReadPost) { break; }
|
|
Unread.posts.delete(ID);
|
|
Unread.postsQuotingYou.delete(ID);
|
|
}
|
|
Unread.readCount++;
|
|
}
|
|
|
|
Unread.updatePosition();
|
|
Unread.setLine();
|
|
return Unread.update();
|
|
},
|
|
|
|
addPost() {
|
|
if (this.isFetchedQuote || this.isClone) { return; }
|
|
Unread.order.push(this);
|
|
if ((this.ID <= Unread.lastReadPost) || this.isHidden || QuoteYou.isYou(this)) { return; }
|
|
Unread.posts.add((Unread.posts.last = this.ID));
|
|
Unread.addPostQuotingYou(this);
|
|
return Unread.position != null ? Unread.position : (Unread.position = Unread.order[this.ID]);
|
|
},
|
|
|
|
addPostQuotingYou(post) {
|
|
for (var quotelink of post.nodes.quotelinks) {
|
|
if (QuoteYou.db?.get(Get.postDataFromLink(quotelink))) {
|
|
Unread.postsQuotingYou.add((Unread.postsQuotingYou.last = post.ID));
|
|
Unread.openNotification(post);
|
|
return;
|
|
}
|
|
}
|
|
},
|
|
|
|
openNotification(post, predicate=' replied to you') {
|
|
if (!Header.areNotificationsEnabled) { return; }
|
|
const notif = new Notification(`${post.info.nameBlock}${predicate}`, {
|
|
body: post.commentDisplay(),
|
|
icon: Favicon.logo
|
|
}
|
|
);
|
|
notif.onclick = function() {
|
|
Header.scrollToIfNeeded(post.nodes.bottom, true);
|
|
return window.focus();
|
|
};
|
|
return notif.onshow = () => setTimeout(() => notif.close()
|
|
, 7 * SECOND);
|
|
},
|
|
|
|
onUpdate() {
|
|
return $.queueTask(function() { // ThreadUpdater may scroll immediately after inserting posts
|
|
Unread.setLine();
|
|
Unread.read();
|
|
return Unread.update();
|
|
});
|
|
},
|
|
|
|
readSinglePost(post) {
|
|
const {ID} = post;
|
|
if (!Unread.posts.has(ID)) { return; }
|
|
Unread.posts.delete(ID);
|
|
Unread.postsQuotingYou.delete(ID);
|
|
Unread.updatePosition();
|
|
Unread.saveLastReadPost();
|
|
return Unread.update();
|
|
},
|
|
|
|
read: debounce(100, function(e) {
|
|
// Update the lastReadPost when hidden posts are added to the thread.
|
|
if (!Unread.posts.size && (Unread.readCount !== Unread.thread.posts.keys.length)) {
|
|
Unread.saveLastReadPost();
|
|
}
|
|
|
|
if (d.hidden || !Unread.posts.size) { return; }
|
|
|
|
let count = 0;
|
|
while (Unread.position) {
|
|
var {ID, data} = Unread.position;
|
|
var {bottom} = data.nodes;
|
|
if (!!bottom.getBoundingClientRect().height && // post has been hidden
|
|
(Header.getBottomOf(bottom) <= -1)) { break; } // post is completely read
|
|
count++;
|
|
Unread.posts.delete(ID);
|
|
Unread.postsQuotingYou.delete(ID);
|
|
Unread.position = Unread.position.next;
|
|
}
|
|
|
|
if (!count) { return; }
|
|
Unread.updatePosition();
|
|
Unread.saveLastReadPost();
|
|
if (e) { return Unread.update(); }
|
|
}),
|
|
|
|
updatePosition() {
|
|
while (Unread.position && !Unread.posts.has(Unread.position.ID)) {
|
|
Unread.position = Unread.position.next;
|
|
}
|
|
},
|
|
|
|
saveLastReadPost: debounce(2 * SECOND, function() {
|
|
let ID;
|
|
$.forceSync('Remember Last Read Post');
|
|
if (!Conf['Remember Last Read Post'] || !Unread.db) { return; }
|
|
const postIDs = Unread.thread.posts.keys;
|
|
for (let i = Unread.readCount, end = postIDs.length; i < end; i++) {
|
|
ID = +postIDs[i];
|
|
if (!Unread.thread.posts.get(ID).isFetchedQuote) {
|
|
if (Unread.posts.has(ID)) { break; }
|
|
Unread.lastReadPost = ID;
|
|
}
|
|
Unread.readCount++;
|
|
}
|
|
if (Unread.thread.isDead && !Unread.thread.isArchived) { return; }
|
|
return Unread.db.set({
|
|
boardID: Unread.thread.board.ID,
|
|
threadID: Unread.thread.ID,
|
|
val: Unread.lastReadPost
|
|
});
|
|
}),
|
|
|
|
setLine(force) {
|
|
if (!Conf['Unread Line']) { return; }
|
|
if (Unread.hr.hidden || d.hidden || (force === true)) {
|
|
const oldPosition = Unread.linePosition;
|
|
if (Unread.linePosition = Unread.positionPrev()) {
|
|
if (Unread.linePosition !== oldPosition) {
|
|
let node = Unread.linePosition.data.nodes.bottom;
|
|
if (node.nextSibling?.tagName === 'BR') { node = node.nextSibling; }
|
|
$.after(node, Unread.hr);
|
|
}
|
|
} else {
|
|
$.rm(Unread.hr);
|
|
}
|
|
}
|
|
return Unread.hr.hidden = Unread.linePosition === Unread.order.last;
|
|
},
|
|
|
|
update() {
|
|
const count = Unread.posts.size;
|
|
const countQuotingYou = Unread.postsQuotingYou.size;
|
|
|
|
if (Conf['Unread Count']) {
|
|
const titleQuotingYou = Conf['Quoted Title'] && countQuotingYou ? '(!) ' : '';
|
|
const titleCount = count || !Conf['Hide Unread Count at (0)'] ? `(${count}) ` : '';
|
|
const titleDead = Unread.thread.isDead ?
|
|
Unread.title.replace('-', (Unread.thread.isArchived ? '- Archived -' : '- 404 -'))
|
|
:
|
|
Unread.title;
|
|
d.title = `${titleQuotingYou}${titleCount}${titleDead}`;
|
|
}
|
|
|
|
Unread.saveThreadWatcherCount();
|
|
|
|
if (Conf['Unread Favicon'] && (g.SITE.software === 'yotsuba')) {
|
|
const {isDead} = Unread.thread;
|
|
return Favicon.set((
|
|
countQuotingYou ?
|
|
(isDead ? 'unreadDeadY' : 'unreadY')
|
|
: count ?
|
|
(isDead ? 'unreadDead' : 'unread')
|
|
:
|
|
(isDead ? 'dead' : 'default')
|
|
)
|
|
);
|
|
}
|
|
},
|
|
|
|
saveThreadWatcherCount: debounce(2 * SECOND, function() {
|
|
$.forceSync('Remember Last Read Post');
|
|
if (Conf['Remember Last Read Post'] && (!Unread.thread.isDead || Unread.thread.isArchived)) {
|
|
let posts;
|
|
const quotingYou = !Conf['Require OP Quote Link'] && QuoteYou.isYou(Unread.thread.OP) ? Unread.posts : Unread.postsQuotingYou;
|
|
if (!quotingYou.size) {
|
|
quotingYou.last = 0;
|
|
} else if (!quotingYou.has(quotingYou.last)) {
|
|
quotingYou.last = 0;
|
|
posts = Unread.thread.posts.keys;
|
|
for (let i = posts.length - 1; i >= 0; i--) {
|
|
if (quotingYou.has(+posts[i])) {
|
|
quotingYou.last = posts[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return ThreadWatcher.update(g.SITE.ID, Unread.thread.board.ID, Unread.thread.ID, {
|
|
last: Unread.thread.lastPost,
|
|
isDead: Unread.thread.isDead,
|
|
isArchived: Unread.thread.isArchived,
|
|
unread: Unread.posts.size,
|
|
quotingYou: (quotingYou.last || 0)
|
|
}
|
|
);
|
|
}
|
|
})
|
|
};
|
|
export default Unread;
|