Bitcoin Forum
July 02, 2026, 01:24:46 AM *
News: Latest Bitcoin Core release: 31.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: [Userscript] A Tool to Save Posts, Organize and Revisit Forum Posts  (Read 111 times)
MisFoxie (OP)
Full Member
***
Offline

Activity: 173
Merit: 106


View Profile
June 28, 2026, 07:08:05 AM
 #1

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 desktop

1. 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 / phone

You 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
Code:
// ==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.

IF MONEY BECOMES CODE
WHO REALLY HOLDS THE POWER?
dkbit98
Legendary
*
Offline

Activity: 3010
Merit: 8734


AntiSwap.io - NO AML/KYC EXCHANGER MONITORING


View Profile WWW
June 29, 2026, 02:46:18 PM
 #2

This can be useful tool but for saving posts I want to read later, but I don't think you should mix it with sMerits thing.
I also don't like position of Saved posts button, on the right bottom side, and I think it would work better on top of the page or added in forum menu.


Code:
[center][table][tr][td][font=Arial Black][size=24pt][glow=#222,1][nbsp][url=https://en.antiswap.io/?utm_source=bitcointalk_s3][size=5pt][sup][size=21pt][b][color=#03adfd]🛡[/b][/sup][/size][size=13pt][nbsp][/size][size=5pt][sup][size=18pt][color=#fff]Anti[color=#3b82f6]Swap[/sup][/size][nbsp][nbsp][size=14pt][sup][size=8pt][i][color=#fff]NO[nbsp]AML/KYC—EXCHANGER[nbsp]MONITORING[/sup][/size][nbsp][nbsp][size=6pt][sup][size=16pt][glow=#03adfd,1][nbsp][font=Impact][color=#fff]900+[/font][nbsp][/glow][/size][/sup][/size][size=6pt][sup][size=16pt][glow=#3b82f6,1][nbsp][size=8pt][sup][size=8pt][color=#fff]EXCHANGERS[/size][/sup][/size][nbsp][/glow][/size][/sup][/size][/url][nbsp][nbsp][font=Arial][b][size=14pt][sup][size=8pt][url=https://bitcointalk.org/index.php?topic=5568680.msg66184227#msg66184227][color=#fff]BITCOINTALK[/url][/size][/sup][/size][/font][nbsp][size=9pt][sup][size=18pt][color=#3b82f6]│[/size][/sup][/size][nbsp][font=Arial][b][size=14pt][sup][size=8pt][url=https://t.me/+qGCCD6ncnctiZTli][color=#fff]TELEGRAM[/url][/size][/sup][/size][/font][nbsp][nbsp][/td][/tr][/table][/center]
Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!