// ==UserScript== // @name Mastodon Timeline Counter // @version 1.2 // @description Indicates the number of remaining posts on the timeline. // @namespace http://tampermonkey.net/ // @author Bene Laszlo // @match https://mastodon.social/@* // @match https://mastodon.online/@* // @match https://mas.to/@* // @icon https://mastodon.social/packs/media/icons/favicon-16x16-c58fdef40ced38d582d5b8eed9d15c5a.png // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; var list, listContainerEl, boxEl, counterEl, textEl, count, lastCount=0, subCount, placeholdersJustCreated, result, lastResult, total, pack, packSize, packOffset, scrollCount=0, lastPos, firstContentFound, first = true, theme; preInit(); // wait for timeline to load before anything can start function preInit() { var t = setInterval(function() { var coll = document.getElementsByClassName('item-list'); if (coll.length) { listContainerEl = coll[0]; clearTimeout(t); init(); } }, 200); } function init() { // set the colors according to the theme for (const name of ['default','contrast','mastodon-light']) { if (document.body.classList.contains('theme-'+name)) { theme = name; break; } } // source for total number of posts; var totalEl = document.getElementsByClassName('account__header__extra__links')[0].firstElementChild; result = total = totalEl.getAttribute('title').replace(',', ''); // the counter element var navPanel = document.getElementsByClassName('navigation-panel')[0]; var boxX = navPanel.getBoundingClientRect().left; //// counter container boxEl = document.createElement("div"); boxEl.style.position = 'fixed'; boxEl.style.left = (boxX+12)+'px'; boxEl.style.bottom = '10px'; boxEl.style.display = 'flex'; boxEl.style.gap = '4px'; boxEl.style.alignItems = 'flex-end'; //// counter counterEl = document.createElement("div"); counterEl.style.fontSize = '40px'; counterEl.style.lineHeight = '.9em'; counterEl.style.fontWeight = '500'; //// additional text textEl = document.createElement("div"); textEl.innerText = 'total'; var textColor; switch (theme) { case 'default': textColor='#606984'; break; case 'contrast': textColor='#c2cede'; break; case 'mastodon-light': textColor='#444b5d'; break; } textEl.style.color = textColor; boxEl.append(counterEl); boxEl.append(textEl); // featured hashtags should move aside for (var el of document.getElementsByClassName('getting-started__trends')) { el.style.position = 'relative'; el.style.top = '-70px'; el.style.borderBottom = '1px solid #393f4f'; } putContent(total); document.body.appendChild(boxEl); document.addEventListener('scroll', handleScroll); } // scroll event, the main function function handleScroll() { if (window.scrollY <= lastPos) return; // scolling up or not scrolling further down // update the counterEl1 (at every 12th scroll); if (scrollCount == 0) { placeholdersJustCreated = false; list = listContainerEl.getElementsByTagName('article'); count = list.length; if (count != lastCount) { placeholdersJustCreated = true; // get the newly loaded post placeholders packSize = count-lastCount; //console.log(count+" "+lastCount+' → '+packSize); pack = Array.prototype.slice.call(list, -packSize); //console.log(pack[0].getAttribute('aria-posinset')+' "'+pack[0].style.overflow+'"'); lastCount = count; } // there's only info about how many post PLACEHOLDERS are loaded // need to know how many of them are fully loaded if (!placeholdersJustCreated) { firstContentFound = false; for (var i in pack) { var article = pack[i]; var isEmpty = article.style.overflow && article.style.overflow=='hidden'; if (!firstContentFound && isEmpty) continue; // posts scrolled past, which are ALREADY re-unloaded firstContentFound = true; if (isEmpty) break; // first post that is STILL unloaded } subCount = firstContentFound ? i : 0; } else { subCount = 0; } var realCount = count-(packSize-subCount); // console.log(subCount+' :: '+realCount); // The last pack of posts is recognized by having less than 20 posts. (This has a 95% chance of working.) // The last post of this pack makes the counter come to a close. if (packSize<20 && realCount==count-1) {end(result); return;} result = total-realCount; if (result!=total) result++; // post is digested when the NEXT post is loaded and its top is already visible lastResult = result; // on first post scroll, alter the additional text if (first && subCount>1) { textEl.innerText = 'more to go'; first = false; } } scrollCount ++; if (scrollCount==12) scrollCount = 0; lastPos = window.scrollY; putContent(result); } function putContent(n) { // displaying result while fixing kerning of number 1 var resultStr = ''; const digits = Array.from(n.toString()); for (const [j, c] of digits.entries()) { resultStr += (c!=1 || j==digits.length-1) ? c : ''+c+''; } counterEl.innerHTML = resultStr; for (var el of counterEl.getElementsByTagName('span')) { // inline styling of doesn't take effect FSR el.style.position = 'relative'; el.style.left = '1px'; } } // zeroing the counter when the timeline has ended // this userscript relies on the timeline header about the number of posts, which is never exact function end(n) { document.removeEventListener('scroll', handleScroll); var t = setInterval(function() { n--; putContent(n); if (n==0) clearTimeout(t); }, 60); } })();