Meguca Unread

On Meguca/shamichan imageboards marks last read post and scrolls to it

// ==UserScript==
// @name         Meguca Unread
// @namespace    meguca.shamichan.ext
// @version      1.0.5
// @description  On Meguca/shamichan imageboards marks last read post and scrolls to it 
// @author       SaddestPanda
// @license      GNU GPLv3
// @match        https://2chen.moe/*
// @match        https://sturdychan.help/*
// @match        https://shamik.ooo/*
// @match        https://shamiko.org/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    'use strict';

    let threadPosts = document.querySelectorAll("#threads #thread-container article");
    if (threadPosts?.length < 10) {
        //disable if there are less than 10 posts
        return;
    }

    addMyStyle("meguca-extended-css", `
    .lastRead {
        border-top: 8px solid #1cb9d2;
    }
    `);

    let db;
    let retries = 0;
    dbStart();

    function dbStart() {
        let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
        let DBOpenRequest = indexedDB.open('meguca');
        DBOpenRequest.onsuccess = (event) => {
            db = event.target.result;
            dbContinue();
        }
        DBOpenRequest.onerror = (event) => {
            //retry db access
            if (retries < 5) {
                retries++;
                setTimeout(() => {
                    dbStart();
                }, 150);
            }
        }
    }

    async function dbContinue() {
        let transaction = db.transaction("seenPost", "readonly");
        let objectStore = transaction.objectStore("seenPost");
        let getAll = objectStore.getAll();
        getAll.onsuccess = (event) => {
            //Close db early
            db.close();

            let allData = event.target.result;
            let threadID = document.querySelector("#thread-container").dataset.id;

            //Find "first unread post"
            let postIDs = new Map();
            threadPosts.forEach(element => {
                let id = parseInt(element.id.split("p")[1]);
                postIDs.set(id, true);
            });

            allData.forEach(obj => {
                if (obj.op == threadID) {
                    postIDs.delete(obj.id)
                }
            });

            if (postIDs.size == 0) {
                //No unread posts. Scroll to bottom.
                document.querySelector("html").scrollIntoView(false);
            } else {
                //Scroll to first unread post
                const iterator = postIDs.keys();
                let firstUnreadID = iterator.next().value;
                let firstUnreadElem = document.querySelector(`article[id="p${firstUnreadID}"]`);
                if (firstUnreadElem) {
                    //Mark as read (add styling)
                    firstUnreadElem.classList.add("lastRead");
                    //Do scroll (top of next elem)
                    let firstUnreadPos = findPos(firstUnreadElem?.nextElementSibling || firstUnreadElem);
                    window.scroll(0, firstUnreadPos.top - window.innerHeight);
                }
            }

            /*
            //Find "last read post"
            //This method doesn't work as hovered backlinks are set to read as well

            let filteredData = allData.filter(obj => obj.op == threadID);
            let lastObj = filteredData[filteredData.length - 1];
            let lastReadElem = document.querySelector(`article[id="p${lastObj.id}"]`);
            //Mark as read (add styling)
            lastReadElem.classList.add("lastRead");
            //Scroll one screen height above last read (don't show last read)
            let lastReadPos = findPos(lastReadElem);
            window.scroll(0, lastReadPos.top - window.innerHeight + 150); //+N is to show last read post and part of the next post 
            */

        };
        getAll.onerror = event => {
            console.error("🚀 ~ dbContinue ~ event ~ onerror:", event);
            db.close();
        };
    }

    function addMyStyle(newID, newStyle) {
        let myStyle = document.createElement('style');
        //myStyle.type = 'text/css';
        myStyle.id = newID;
        myStyle.textContent = newStyle;
        document.querySelector("head").appendChild(myStyle);
    }

    function findPos(obj) {
        const rect = obj.getBoundingClientRect();
        return {
            left: rect.left + window.scrollX,
            top: rect.top + window.scrollY
        }
    }
})();