|
MisFoxie (OP)
|
 |
June 28, 2026, 07:08:05 AM |
|
While visiting [Merit] Share your best posts/threads with Fillippone to be merit assessed this thread made by Fillippone I notice an interesting fact. As everyone know Fillippone help users by giving merit to quality posts but the interesting fact is Fillippone gives 1 merit extra in the submitted form which i think to keep track of his activity like mark it as reviewed or maybe I'm wrong. So I made a userscript to help track his activity in that thread without giving extra merits. What this tool does?This tool allow users to Save multiple post in a thread, the saved post got highlighted and the recent saved post in a thread can be open through Saved Posts button which located at the right bottom side. It also shows total smerit available to give. Why people should trust this tool? - It runs locally in Tampermonkey.
- Saved posts remain on the user’s own device.
- It does not use any external server or analytics.
- It does not collect passwords, cookies, private messages, or personal information.
- Network access is restricted to Bitcointalk.org only, mainly for checking the logged-in user’s sMerit balance.
- The entire source code is visible, so users can review it before installation.
- Users can disable or uninstall it anytime from Tampermonkey.
Some photos for reference: How to install on desktop1. Install Tampermonkey extension in your browser. 2. Go to chrome://extensions/ click Details in Tampermonkey> scroll down and Allow User Scripts [on] 3. Go to Greasyfork and click [install this script] then install 6. Open or refresh Bitcointalk. How to install on Android / phoneYou need a browser that supports userscripts. For example: - Kiwi Browser + Tampermonkey
- mises Browser + Tampermonkey
- Any other Android browser that supports userscript extensions
Script code box // ==UserScript== // @name Bitcointalk Saved Posts // @namespace https://bitcointalk.org/ // @version 1.1.0 // @description Save Bitcointalk posts locally and automatically show your remaining sMerit. // @author Misfoxie // @match https://bitcointalk.org/* // @match http://bitcointalk.org/* // @match https://www.bitcointalk.org/* // @match http://www.bitcointalk.org/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @connect bitcointalk.org // @connect www.bitcointalk.org // @run-at document-end // ==/UserScript==
(function () { 'use strict';
const STORAGE_KEY = 'btt_saved_posts_v1'; const POST_SELECTOR = '.subject[id^="subject_"]'; const SMERIT_CACHE_KEY = 'btt_remaining_smerit_cache'; const SMERIT_CACHE_TIME_KEY = 'btt_remaining_smerit_cache_time'; const SMERIT_CACHE_DURATION = 5 * 60 * 1000;
const css = ` #bttsp-launcher { position:fixed; right:18px; bottom:18px; z-index:99998; border:0; border-radius:999px; background:#263b55; color:#fff; padding:11px 15px; box-shadow:0 4px 18px #0005; cursor:pointer; font:600 13px Arial,sans-serif; } #bttsp-launcher:hover { background:#345273; } #bttsp-panel { position:fixed; right:18px; bottom:68px; z-index:99999; width:min(390px,calc(100vw - 28px)); max-height:min(650px,calc(100vh - 90px)); display:none; flex-direction:column; overflow:hidden; border:1px solid #8da0b4; border-radius:10px; background:#f5f7fa; color:#1d2936; box-shadow:0 10px 36px #0007; font:13px Arial,sans-serif; text-align:left; } #bttsp-panel.bttsp-open { display:flex; } .bttsp-head { display:flex; align-items:center; justify-content:space-between; padding:13px 14px; background:#263b55; color:#fff; } .bttsp-head strong { font-size:15px; } .bttsp-close { border:0; background:transparent; color:#fff; font-size:22px; line-height:18px; cursor:pointer; } #bttsp-list { overflow:auto; padding:8px; } .bttsp-empty { padding:28px 15px; color:#627181; text-align:center; line-height:1.5; } .bttsp-thread { margin-bottom:8px; overflow:hidden; border:1px solid #c8d1db; border-radius:7px; background:#fff; } .bttsp-thread summary { position:relative; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; align-items:center; padding:10px 11px; cursor:pointer; list-style:none; } .bttsp-thread summary::-webkit-details-marker { display:none; } .bttsp-summary-text { min-width:0; } .bttsp-title { display:block; overflow:hidden; color:#203f64; font-weight:700; text-overflow:ellipsis; white-space:nowrap; } .bttsp-preview { display:block; margin-top:5px; overflow:hidden; color:#596978; font-size:12px; text-overflow:ellipsis; white-space:nowrap; } .bttsp-meta { display:block; margin-top:5px; color:#8995a1; font-size:11px; } .bttsp-open-latest { display:inline-block; align-self:center; border:1px solid #315a83; border-radius:5px; background:#345f89; color:#fff !important; padding:7px 10px; font-weight:700; text-decoration:none !important; white-space:nowrap; } .bttsp-open-latest:hover { background:#264b70; } .bttsp-posts { border-top:1px solid #dce2e8; } .bttsp-item { display:grid; grid-template-columns:1fr auto; gap:8px; padding:9px 11px; border-bottom:1px solid #edf0f3; } .bttsp-item:last-child { border-bottom:0; } .bttsp-link { min-width:0; color:#294f79; text-decoration:none; } .bttsp-link:hover { text-decoration:underline; } .bttsp-link span { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .bttsp-link small { display:block; margin-top:3px; color:#7a8793; } .bttsp-delete { align-self:center; border:0; border-radius:4px; background:#eceff2; color:#8b3030; padding:4px 7px; cursor:pointer; } .bttsp-save { position:relative !important; top:2px !important; margin-left:7px !important; border:1px solid #526b85 !important; border-radius:4px !important; background:#edf2f6 !important; color:#27435f !important; padding:3px 7px !important; cursor:pointer !important; font:bold 11px Arial,sans-serif !important; vertical-align:middle !important; } .bttsp-save:hover { background:#dce7ef !important; } .bttsp-save.bttsp-is-saved { border-color:#a56d08 !important; background:#ffe7a8 !important; color:#634200 !important; } .bttsp-smerit { display:inline-block; margin-left:7px; border:1px solid #9abe92; border-radius:11px; background:#dcebd8; color:#17601d; padding:3px 9px; font:bold 12px Arial,sans-serif; line-height:1.25; vertical-align:middle; white-space:nowrap; } .bttsp-smerit.bttsp-smerit-error { background:#eee; color:#777; } .bttsp-saved-row { box-shadow:inset 5px 0 #e0a11d !important; } .bttsp-saved-cell { background-image:linear-gradient(90deg,rgba(255,224,132,.30),rgba(255,255,255,0) 55%) !important; } .bttsp-saved-cell, .bttsp-saved-cell * { color:#71869a !important; } .bttsp-saved-cell a, .bttsp-saved-cell a * { color:#4f789f !important; } .bttsp-saved-row .bttsp-poster-text { color:#71869a !important; } .bttsp-saved-row .poster_info a .bttsp-poster-text { color:#4f789f !important; } .bttsp-saved-cell .bttsp-save { border-color:#a56d08 !important; background:#ffe7a8 !important; color:#634200 !important; opacity:1 !important; } .bttsp-saved-cell .bttsp-smerit { border-color:#9abe92 !important; background:#dcebd8 !important; color:#17601d !important; opacity:1 !important; } .bttsp-saved-cell .bttsp-saved-label { background:#e0a11d !important; color:#302000 !important; } .bttsp-saved-label { display:inline-block; margin-left:7px; border-radius:10px; background:#e0a11d; color:#302000; padding:2px 7px; font:bold 10px Arial,sans-serif; vertical-align:middle; } #bttsp-toast { position:fixed; left:50%; bottom:24px; z-index:100000; transform:translateX(-50%); border-radius:6px; background:#182536; color:#fff; padding:9px 14px; box-shadow:0 3px 12px #0006; font:13px Arial,sans-serif; opacity:0; pointer-events:none; transition:opacity .2s; } #bttsp-toast.bttsp-show { opacity:1; } @media (max-width:600px) { #bttsp-launcher { right:10px; bottom:10px; } #bttsp-panel { right:7px; bottom:58px; } } `;
let saved = loadSaved(); let toastTimer;
function loadSaved() { const value = GM_getValue(STORAGE_KEY, []); if (!Array.isArray(value)) return []; return value.filter(item => item && item.postId && item.topicId && item.url); }
function persist() { GM_setValue(STORAGE_KEY, saved); }
function getCachedSmerit() { const value = localStorage.getItem(SMERIT_CACHE_KEY); const time = Number(localStorage.getItem(SMERIT_CACHE_TIME_KEY)); if (value === null || !time || Date.now() - time > SMERIT_CACHE_DURATION) return null; return value; }
function cacheSmerit(value) { localStorage.setItem(SMERIT_CACHE_KEY, String(value)); localStorage.setItem(SMERIT_CACHE_TIME_KEY, String(Date.now())); }
function findMeritLinks() { return [...document.querySelectorAll('a[href*="action=merit"]')]; }
function extractSmerit(html) { const doc = new DOMParser().parseFromString(html, 'text/html'); const text = (doc.body ? doc.body.textContent : html).replace(/\s+/g, ' ').trim(); const patterns = [ /You have\s+(\d+)\s+sendable merits?/i, /You have\s+(\d+)\s+sMerits?/i, /You have\s+(\d+)\s+available merits?/i, /You can send\s+(\d+)\s+merits?/i, /sendable merits?\s*[:\-]?\s*(\d+)/i, /sMerits?\s*[:\-]?\s*(\d+)/i ]; for (const pattern of patterns) { const match = text.match(pattern); if (match) return match[1]; } return null; }
function showSmerit(value, failed = false) { findMeritLinks().forEach(link => { let badge = link.parentElement && link.parentElement.querySelector(`.bttsp-smerit[data-for="${link.href}"]`); if (!badge) { badge = el('span', 'bttsp-smerit'); badge.dataset.for = link.href; link.insertAdjacentElement('afterend', badge); } badge.textContent = `sMerit: ${value}`; badge.classList.toggle('bttsp-smerit-error', failed); badge.title = failed ? 'Could not read the current sMerit balance' : 'Remaining sendable merit (cached for five minutes)'; }); }
function initSmerit() { const meritLinks = findMeritLinks(); if (!meritLinks.length) return;
const cached = getCachedSmerit(); if (cached !== null) { showSmerit(cached); return; }
GM_xmlhttpRequest({ method: 'GET', url: meritLinks[0].href, headers: { Accept: 'text/html' }, onload(response) { const value = extractSmerit(response.responseText || ''); if (value !== null) { cacheSmerit(value); showSmerit(value); } else { showSmerit('?', true); console.warn('[Bitcointalk Saved Posts] Could not detect remaining sMerit.'); } }, onerror() { showSmerit('?', true); console.warn('[Bitcointalk Saved Posts] Failed to load the Merit page.'); } }); }
function normalizeText(value, maxLength) { const text = String(value || '').replace(/\s+/g, ' ').trim(); return text.length > maxLength ? `${text.slice(0, maxLength - 1).trimEnd()}\u2026` : text; }
function parseTopicId(url) { const match = String(url).match(/[?;&]topic=(\d+)/i); return match ? match[1] : ''; }
function getPostData(subject) { const postId = subject.id.replace('subject_', ''); const contentCell = subject.closest('.td_headerandpost') || subject.closest('td'); const postTable = contentCell && contentCell.closest('table'); const permalink = (contentCell && contentCell.querySelector(`a[href*="#msg${postId}"]`)) || subject.querySelector('a'); const postBody = contentCell && contentCell.querySelector('.post'); const authorLink = postTable && postTable.querySelector('.poster_info a[href*="action=profile"]'); const numberLink = contentCell && contentCell.querySelector('.message_number'); const topicId = parseTopicId(permalink ? permalink.href : location.href); if (!postId || !topicId || !permalink) return null;
return { postId, topicId, threadTitle: normalizeText((subject.querySelector('a') || subject).textContent.replace(/^Re:\s*/i, ''), 180) || `Topic ${topicId}`, postTitle: normalizeText((subject.querySelector('a') || subject).textContent, 180), author: normalizeText(authorLink && authorLink.textContent, 80) || 'Unknown member', postNumber: normalizeText(numberLink && numberLink.textContent, 20) || `#${postId}`, snippet: normalizeText(postBody && postBody.textContent, 240) || 'Saved Bitcointalk post', url: new URL(permalink.href, location.href).href, savedAt: Date.now() }; }
function findSaved(postId) { return saved.find(item => item.postId === String(postId)); }
function savePost(data) { if (findSaved(data.postId)) { saved = saved.filter(item => item.postId !== data.postId); showToast('Post removed from saved posts'); } else { saved.push(data); showToast('Post saved on this device'); } persist(); decoratePosts(); renderPanel(); }
function preparePosterText(postTable) { const poster = postTable && postTable.querySelector('.poster_info'); if (!poster || poster.dataset.bttspTextPrepared) return; poster.dataset.bttspTextPrepared = '1';
const walker = document.createTreeWalker(poster, NodeFilter.SHOW_TEXT); const textNodes = []; while (walker.nextNode()) { if (walker.currentNode.nodeValue.trim()) textNodes.push(walker.currentNode); } textNodes.forEach(textNode => { const wrapper = el('span', 'bttsp-poster-text'); textNode.parentNode.insertBefore(wrapper, textNode); wrapper.appendChild(textNode); }); }
function removePost(postId) { saved = saved.filter(item => item.postId !== String(postId)); persist(); decoratePosts(); renderPanel(); showToast('Saved post removed'); }
function decoratePosts() { document.querySelectorAll(POST_SELECTOR).forEach(subject => { const data = getPostData(subject); if (!data) return; const contentCell = subject.closest('.td_headerandpost') || subject.closest('td'); const postTable = contentCell && contentCell.closest('table'); const buttonArea = contentCell && contentCell.querySelector('.td_buttons > div, .td_buttons'); if (!buttonArea) return; preparePosterText(postTable);
let button = buttonArea.querySelector(`.bttsp-save[data-post-id="${data.postId}"]`); if (!button) { button = document.createElement('button'); button.type = 'button'; button.className = 'bttsp-save'; button.dataset.postId = data.postId; button.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); savePost(getPostData(subject)); }); buttonArea.appendChild(button); }
const isSaved = Boolean(findSaved(data.postId)); button.textContent = isSaved ? '\u2605 Saved' : '\u2606 Save'; button.title = isSaved ? 'Remove this saved post' : 'Save this post locally'; button.classList.toggle('bttsp-is-saved', isSaved); if (postTable) postTable.classList.toggle('bttsp-saved-row', isSaved); if (contentCell) contentCell.classList.toggle('bttsp-saved-cell', isSaved);
let label = subject.querySelector('.bttsp-saved-label'); if (isSaved && !label) { label = document.createElement('span'); label.className = 'bttsp-saved-label'; label.textContent = 'SAVED'; subject.appendChild(label); } else if (!isSaved && label) { label.remove(); } }); }
function groupByThread() { const groups = new Map(); saved.forEach(post => { if (!groups.has(post.topicId)) groups.set(post.topicId, []); groups.get(post.topicId).push(post); }); return [...groups.values()] .map(posts => posts.sort((a, b) => b.savedAt - a.savedAt)) .sort((a, b) => b[0].savedAt - a[0].savedAt); }
function formatDate(timestamp) { try { return new Date(timestamp).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }); } catch (_) { return new Date(timestamp).toLocaleString(); } }
function el(tag, className, text) { const node = document.createElement(tag); if (className) node.className = className; if (text !== undefined) node.textContent = text; return node; }
function renderPanel() { const list = document.getElementById('bttsp-list'); if (!list) return; list.replaceChildren();
const groups = groupByThread(); if (!groups.length) { list.appendChild(el('div', 'bttsp-empty', 'No saved posts yet. Open a thread and use the \u2606 Save button beside a post.')); return; }
groups.forEach(posts => { const latest = posts[0]; const details = el('details', 'bttsp-thread'); const summary = document.createElement('summary'); const summaryText = el('span', 'bttsp-summary-text'); summaryText.appendChild(el('span', 'bttsp-title', latest.threadTitle)); summaryText.appendChild(el('span', 'bttsp-preview', latest.snippet)); summaryText.appendChild(el('span', 'bttsp-meta', `${posts.length} saved post${posts.length === 1 ? '' : 's'} \u00b7 latest ${formatDate(latest.savedAt)}`)); const openLatest = el('a', 'bttsp-open-latest', 'Open'); openLatest.href = latest.url; openLatest.title = `Open the most recently saved post (${latest.postNumber})`; openLatest.addEventListener('click', event => event.stopPropagation()); summary.append(summaryText, openLatest); details.appendChild(summary);
const postList = el('div', 'bttsp-posts'); posts.forEach(post => { const item = el('div', 'bttsp-item'); const link = el('a', 'bttsp-link'); link.href = post.url; link.title = post.snippet; link.appendChild(el('span', '', `${post.postNumber} \u2014 ${post.snippet}`)); link.appendChild(el('small', '', `${post.author} \u00b7 ${formatDate(post.savedAt)}`)); const remove = el('button', 'bttsp-delete', '\u2715'); remove.type = 'button'; remove.title = 'Remove saved post'; remove.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); removePost(post.postId); }); item.append(link, remove); postList.appendChild(item); }); details.appendChild(postList); list.appendChild(details); }); }
function showToast(message) { const toast = document.getElementById('bttsp-toast'); if (!toast) return; toast.textContent = message; toast.classList.add('bttsp-show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('bttsp-show'), 1800); }
function buildUi() { const style = document.createElement('style'); style.textContent = css;
const launcher = el('button', '', 'Saved posts'); launcher.id = 'bttsp-launcher'; launcher.type = 'button';
const panel = el('aside'); panel.id = 'bttsp-panel'; panel.setAttribute('aria-label', 'Saved Bitcointalk posts'); const head = el('div', 'bttsp-head'); head.appendChild(el('strong', '', 'Saved threads')); const close = el('button', 'bttsp-close', '\u00d7'); close.type = 'button'; close.title = 'Close'; head.appendChild(close); const list = el('div'); list.id = 'bttsp-list'; panel.append(head, list);
const toast = el('div'); toast.id = 'bttsp-toast'; document.head.appendChild(style); document.body.append(launcher, panel, toast);
launcher.addEventListener('click', () => panel.classList.toggle('bttsp-open')); close.addEventListener('click', () => panel.classList.remove('bttsp-open')); document.addEventListener('keydown', event => { if (event.key === 'Escape') panel.classList.remove('bttsp-open'); }); }
buildUi(); decoratePosts(); renderPanel(); initSmerit(); })();
Well, this tool can also be helpful for those who what want to track thread and quickly access them. Just two click and it will lead you to you favorite thread.
|