GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 08, 2026, 09:47:29 AM Last edit: June 09, 2026, 10:26:11 AM by GhostOfBitcoin Merited by satscraper (5), klarki (3), hugeblack (1) |
|
I would like to suggest adding the new button in Edit Watchlist that allows all entries to be displayed in alphabetical order. I would appreciate it if this could be implemented.
Many people use Bitcointalk's Watchlist feature regularly. They add important topics to the watchlist, and remove old topics when they are no longer needed. But when the watchlist grows large, a common problem arises the entries are randomly placed. As a result, you have to scroll through the entire list to find a specific topic, which is time-consuming and a bit annoying. I'm working on a small userscript to solve this problem: Bitcointalk Watchlist A-Z SorterThe main task of this userscript will be to add a new button to the Edit Watchlist page, like this: Clicking the button will sort all topic entries in the watchlist alphabetically. You will have the option to restore the original order if you wish. Why is this userscript needed?1. It will be easier for those with large watchlists to find specific topics. 2. The hassle of manually scrolling to find topics will be reduced. 3. When removing old entries, users will be able to quickly identify topics. 4. Watchlist management will be cleaner and more organized. 5. No server-side changes to the forum will be required, as all work will be done inside the browser. Basic technical flow: 1. The script will first check if the current page is a Bitcointalk watchlist page. 2. The page URL will be matched 3. Then the watchlist entries from the page will be detected. 4. Each entry usually has a checkbox and a topic link. 5. The script will create an entry object by taking the checkbox and topic link together. 6. The sorting text will be created from the topic title. 7. A-Z sorting will be done using JavaScript localeCompare(). 8. The entries will be reordered in the DOM. 9. The original order will be kept in memory, so that the user can restore it if desired. Sorting logicTopic titles will be normalized for sorting: function normalize(text) { return text.replace(/\s+/g, ' ').trim().toLowerCase(); }
Then it will be compared: titleA.localeCompare(titleB, undefined, { numeric: true, sensitivity: 'base' });
This will reduce the uppercase/lowercase issue and sorting will be relatively better even if there are numbers. On the Edit Watchlist page, there will be a new button next to the Remove checked button:  After clicking:  The user can return to the original order from the sorted view if they wish. How to Install: Step 1: First, install a userscript manager extension in your browser (Tampermonkey). Step 2: Go to the extension dashboard and click on Create a new script or the plus (+) icon. Step 3: Copy and paste the entire code below and save . Step 4: Now refresh the BitcoinTalk forum. You will see a beautiful market ticker on the right side! Userscript version 1.0.0 - June 08, 2026, 09:47:29 AMGreasyFork DownloadUserscript version 1.1.0 - June 08, 2026, 05:00:03 PMGreasyFork DownloadA button has been added. You can now add a custom display title name there, leaving the URL unchanged. If you give a custom name, it will be saved in localStorage. As a result, even if the watchlist page loads later, the saved custom name will be displayed instead of the original title. Userscript version 1.2.0 - 06-09-2026 --> 04:41:52 AMGreasyFork Download- Rename data will usually remain even if you clear browser cache
- Rename data will usually remain even if you clear Brave site data
- Users do not need to export/import
// ==UserScript== // @name Bitcointalk Watchlist Alphabetical Sorter // @namespace bitcointalk-watchlist-tools // @version 1.2.0 // @description Adds Sort A-Z and selected-entry rename buttons on Bitcointalk's edit watchlist page. // @author GhostOfBitcoin // @match https://bitcointalk.org/watchlist.php* // @match http://bitcointalk.org/watchlist.php* // @include https://bitcointalk.org/watchlist.php* // @include http://bitcointalk.org/watchlist.php* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // ==/UserScript==
(function () { 'use strict';
const BUTTON_ID = 'bt-watchlist-sort-az'; const EDIT_BUTTON_ID = 'bt-watchlist-edit-title'; const STATUS_ID = 'bt-watchlist-sort-status'; const STORAGE_KEY = 'bt-watchlist-custom-titles'; const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';
let originalEntries = null; let sorted = false;
function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
function sortText(value) { return cleanText(value).toLocaleLowerCase(); }
function loadCustomTitles() { try { const titles = GM_getValue(STORAGE_KEY, {}); return titles && typeof titles === 'object' ? titles : {}; } catch (error) { return {}; } }
function saveCustomTitles(titles) { GM_setValue(STORAGE_KEY, titles); }
function normalizeHref(href) { try { const url = new URL(href, location.href); url.hash = ''; return url.href; } catch (error) { return String(href || ''); } }
function getEntryLink(entry) { for (const node of entry.nodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.matches && node.matches(LINK_SELECTOR)) return node;
const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; if (link) return link; }
return null; }
function applyCustomTitles() { const titles = loadCustomTitles();
document.querySelectorAll(LINK_SELECTOR).forEach((link) => { const customTitle = titles[normalizeHref(link.href)]; if (customTitle) link.textContent = customTitle; }); }
function isWatchlistPage() { return /\/watchlist\.php/i.test(location.pathname); }
function isControlCheckbox(box) { const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase(); return text.includes('all') || text.includes('checkall') || text.includes('check_all'); }
function getTitleFromNode(node) { const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; return link ? sortText(link.textContent) : ''; }
function nextNodeBelongsToEntry(node) { if (!node) return false; if (node.nodeType === Node.TEXT_NODE) return true; if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.matches('br')) return true; return false; }
function collectTrailingBreaks(startNode, nodes) { let node = startNode.nextSibling; let guard = 0;
while (nextNodeBelongsToEntry(node) && guard < 10) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break; node = node.nextSibling; guard += 1; } }
function buildEntryFromCheckbox(box, index) { if (isControlCheckbox(box)) return null;
const tableRow = box.closest('tr'); if (tableRow && tableRow.querySelector(LINK_SELECTOR)) { return { index, parent: tableRow.parentNode, title: getTitleFromNode(tableRow), nodes: [tableRow] }; }
const label = box.closest('label'); if (label && label.querySelector(LINK_SELECTOR)) { const nodes = [label]; collectTrailingBreaks(label, nodes);
return { index, parent: label.parentNode, title: getTitleFromNode(label), nodes }; }
const parent = box.parentNode; if (!parent) return null;
let link = null; let node = box.nextSibling; let guard = 0;
while (node && node.parentNode === parent && guard < 30) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(LINK_SELECTOR)) { link = node; break; }
link = node.querySelector(LINK_SELECTOR); if (link) break;
if (node.matches('br') || node.matches('input[type="checkbox"]')) break; }
node = node.nextSibling; guard += 1; }
if (!link) return null;
const nodes = []; node = box; guard = 0;
while (node && node.parentNode === parent && guard < 40) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) { break; }
node = next; guard += 1; }
return { index, parent, title: sortText(link.textContent), nodes }; }
function findEntries() { applyCustomTitles();
const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const entries = boxes .map(buildEntryFromCheckbox) .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);
const groups = new Map(); entries.forEach((entry) => { if (!groups.has(entry.parent)) groups.set(entry.parent, []); groups.get(entry.parent).push(entry); });
let bestEntries = []; groups.forEach((groupEntries) => { if (groupEntries.length > bestEntries.length) bestEntries = groupEntries; });
return bestEntries.length > 1 ? bestEntries : []; }
function getSelectedEntries() { return findEntries().filter((entry) => { return entry.nodes.some((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return false;
const checkbox = node.matches('input[type="checkbox"]') ? node : node.querySelector('input[type="checkbox"]');
return checkbox && checkbox.checked && !isControlCheckbox(checkbox); }); }); }
function editSelectedEntryTitle() { const selectedEntries = getSelectedEntries();
if (selectedEntries.length !== 1) { setStatus('Select exactly one entry to rename'); return; }
const link = getEntryLink(selectedEntries[0]); if (!link) { setStatus('Selected entry link was not found'); return; }
const currentTitle = cleanText(link.textContent); const nextTitle = window.prompt('Edit selected watchlist entry name:', currentTitle); if (nextTitle === null) return;
const cleanTitle = cleanText(nextTitle); if (!cleanTitle) { setStatus('Name was not changed'); return; }
const titles = loadCustomTitles(); titles[normalizeHref(link.href)] = cleanTitle; saveCustomTitles(titles);
link.textContent = cleanTitle; setStatus('Selected entry name updated'); }
function moveEntries(entries) { const parent = entries[0].parent; const marker = document.createComment('bt-watchlist-sort-marker'); parent.insertBefore(marker, entries[0].nodes[0]);
entries.forEach((entry) => { entry.nodes.forEach((node) => parent.insertBefore(node, marker)); });
parent.removeChild(marker); }
function sortEntries() { const entries = findEntries(); if (!entries.length) { setStatus('No sortable entries found'); return; }
if (!originalEntries) { originalEntries = entries.map((entry) => ({ parent: entry.parent, title: entry.title, nodes: entry.nodes.slice() })); }
const sortedEntries = entries.slice().sort((a, b) => { return a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }); });
moveEntries(sortedEntries); setStatus(`Sorted ${sortedEntries.length} entries`); }
function restoreEntries() { if (!originalEntries) { setStatus('Original order is not saved yet'); return; }
moveEntries(originalEntries); setStatus('Original order restored'); }
function setStatus(message) { const status = document.getElementById(STATUS_ID); if (status) status.textContent = message; }
function insertButton() { if (document.getElementById(BUTTON_ID)) return;
const entries = findEntries();
const wrapper = document.createElement('div'); wrapper.style.margin = '10px 0';
const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = 'Sort A-Z'; button.style.marginRight = '8px'; button.style.padding = '4px 10px'; button.style.cursor = 'pointer';
const editButton = document.createElement('button'); editButton.id = EDIT_BUTTON_ID; editButton.type = 'button'; editButton.textContent = 'Edit selected name'; editButton.style.marginRight = '8px'; editButton.style.padding = '4px 10px'; editButton.style.cursor = 'pointer';
const status = document.createElement('span'); status.id = STATUS_ID; status.style.color = '#555'; status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';
button.addEventListener('click', () => { if (sorted) { restoreEntries(); button.textContent = 'Sort A-Z'; sorted = false; return; }
sortEntries(); button.textContent = 'Restore original order'; sorted = true; });
editButton.addEventListener('click', editSelectedEntryTitle);
wrapper.appendChild(button); wrapper.appendChild(editButton); wrapper.appendChild(status);
const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => { const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase(); return text.includes('remove checked'); });
if (removeButton && removeButton.parentNode) { removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling); return; }
const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => { return cleanText(element.textContent).toLowerCase() === 'edit watchlist'; });
if (title && title.parentNode) { title.parentNode.insertBefore(wrapper, title.nextSibling); return; }
document.body.insertBefore(wrapper, document.body.firstChild); }
function init() { if (!isWatchlistPage()) return; applyCustomTitles(); insertButton(); }
init(); window.setTimeout(init, 500); window.setTimeout(init, 1500); })();
If you want, I can add some more useful features in the future, such as : - Search / Filter Box so that a specific topic can be found very easily.
- The ability to group Boards and Topics separately so that the watchlist will be more organized.
- Recently Updated Topic First Option so that the most recently updated topics are shown at the top.
|
|
|
|
|
|
Catenaccio
|
 |
June 08, 2026, 10:15:48 AM |
|
Actually with time, people will have to clean their watchlists, and large watchlists will be reduce in number of topics. I agree that your userscript is useful for people who have need of sorting watched topics but I feel differently.
If I consider a thread is helpful, I will either bookmark it on my browser or add it to my documents which can be a word file or an excel file or anything document format which can help me finding such useful threads later quickly and easily.
Personally I don't have need of sorting A-Z topics in my watchlist because it's actually not too large.
|
|
|
|
|
|
| R |
▀▀▀▀▀▀▀██████▄▄ ████████████████ ▀▀▀▀█████▀▀▀█████ ████████▌███▐████ ▄▄▄▄█████▄▄▄█████ ████████████████ ▄▄▄▄▄▄▄██████▀▀ | LLBIT | | | 4,000+ GAMES███████████████████ ██████████▀▄▀▀▀████ ████████▀▄▀██░░░███ ██████▀▄███▄▀█▄▄▄██ ███▀▀▀▀▀▀█▀▀▀▀▀▀███ ██░░░░░░░░█░░░░░░██ ██▄░░░░░░░█░░░░░▄██ ███▄░░░░▄█▄▄▄▄▄████ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ | █████████ ▀████████ ░░▀██████ ░░░░▀████ ░░░░░░███ ▄░░░░░███ ▀█▄▄▄████ ░░▀▀█████ ▀▀▀▀▀▀▀▀▀ | █████████ ░░░▀▀████ ██▄▄▀░███ █░░█▄░░██ ░████▀▀██ █░░█▀░░██ ██▀▀▄░███ ░░░▄▄████ ▀▀▀▀▀▀▀▀▀ |
| | | | | | | | | ▄▄████▄▄ ▀█▀▄▀▀▄▀█▀ ▄▄░░▄█░██░█▄░░▄▄ ▄▄█░▄▀█░▀█▄▄█▀░█▀▄░█▄▄ ▀▄█░███▄█▄▄█▄███░█▄▀ ▀▀█░░░▄▄▄▄░░░█▀▀ █░░██████░░█ █░░░░▀▀░░░░█ █▀▄▀▄▀▄▀▄▀▄█ ▄░█████▀▀█████░▄ ▄███████░██░███████▄ ▀▀██████▄▄██████▀▀ ▀▀████████▀▀ | . ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ░▀▄░▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄░▄▀ ███▀▄▀█████████████████▀▄▀ █████▀▄░▄▄▄▄▄███░▄▄▄▄▄▄▀ ███████▀▄▀██████░█▄▄▄▄▄▄▄▄ █████████▀▄▄░███▄▄▄▄▄▄░▄▀ ████████████░███████▀▄▀ ████████████░██▀▄▄▄▄▀ ████████████░▀▄▀ ████████████▄▀ ███████████▀ | ▄▄███████▄▄ ▄████▀▀▀▀▀▀▀████▄ ▄███▀▄▄███████▄▄▀███▄ ▄██▀▄█▀▀▀█████▀▀▀█▄▀██▄ ▄██▀▄███░░░▀████░███▄▀██▄ ███░████░░░░░▀██░████░███ ███░████░█▄░░░░▀░████░███ ███░████░███▄░░░░████░███ ▀██▄▀███░█████▄░░███▀▄██▀ ▀██▄▀█▄▄▄██████▄██▀▄██▀ ▀███▄▀▀███████▀▀▄███▀ ▀████▄▄▄▄▄▄▄████▀ ▀▀███████▀▀ | | OFFICIAL PARTNERSHIP SOUTHAMPTON FC FAZE CLAN SSC NAPOLI |
|
|
|
satscraper
Legendary

Activity: 1540
Merit: 2856
|
~
Thanks for implementing this. I have one more suggestion regarding it. A lot of entries in the watch list begin with insignificant words like ANN, INFO, etc., which add unnecessary distraction. Could you add the button to edit the name of selected entry while preserving its URL? This would make the list more clear and easier to read.
|
| EARNBET | | | ⚽ 🏀 🏈 🏓 🎯 🥊 |
| ⚾ 🎾 ⛳ 🏐 🏏 🏎️ | | |
███████▄▄███████████ ████▄██████████████████ ██▄▀▀███████████████▀▀███ █▄████████████████████████ ▄▄████████▀▀▀▀▀████████▄▄██ ███████████████████████████ █████████▌████▀████████████ ███████████████████████████ ▀▀███████▄▄▄▄▄█████████▀▀██ █▀█████████████████████▀██ ██▀▄▄███████████████▄▄███ ████▀██████████████████ ███████▀▀███████████ | ....HIGHEST.... VIP REWARDS ✔ G U A R A N T E E D
| | | 🜲 | KING OF THE CASTLE $200K in prizes | | | ..PLAY NOW.. |
|
|
|
GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 08, 2026, 05:00:03 PM |
|
A lot of entries in the watch list begin with insignificant words like ANN, INFO, etc., which add unnecessary distraction.
Good suggestion. Many topic titles in Watchlist actually start with prefixes like [ANN], [INFO], [BOUNTY], [DISCUSSION], etc., so when sorting A-Z, sometimes the sorting is done by prefix instead of useful title. Could you add the button to edit the name of selected entry while preserving its URL?
A button has been added as per your suggestion. You can now add a custom display title name there, leaving the URL unchanged. If you give a custom name, it will be saved in localStorage. As a result, even if the watchlist page loads later, the saved custom name will be displayed instead of the original title. GreasyFork DownloadUserscript version 1.1.0 - 06-09-2026 --> 05:00:03 PM// ==UserScript== // @name Bitcointalk Watchlist Alphabetical Sorter // @namespace bitcointalk-watchlist-tools // @version 1.1.0 // @description Adds Sort A-Z and selected-entry rename buttons on Bitcointalk's edit watchlist page. // @author GhostOfBitcoin // @match https://bitcointalk.org/watchlist.php* // @match http://bitcointalk.org/watchlist.php* // @include https://bitcointalk.org/watchlist.php* // @include http://bitcointalk.org/watchlist.php* // @run-at document-idle // @grant none // ==/UserScript==
(function () { 'use strict';
const BUTTON_ID = 'bt-watchlist-sort-az'; const EDIT_BUTTON_ID = 'bt-watchlist-edit-title'; const STATUS_ID = 'bt-watchlist-sort-status'; const STORAGE_KEY = 'bt-watchlist-custom-titles'; const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';
let originalEntries = null; let sorted = false;
function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
function sortText(value) { return cleanText(value).toLocaleLowerCase(); }
function loadCustomTitles() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}') || {}; } catch (error) { return {}; } }
function saveCustomTitles(titles) { localStorage.setItem(STORAGE_KEY, JSON.stringify(titles)); }
function normalizeHref(href) { try { const url = new URL(href, location.href); url.hash = ''; return url.href; } catch (error) { return String(href || ''); } }
function getEntryLink(entry) { for (const node of entry.nodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.matches && node.matches(LINK_SELECTOR)) return node;
const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; if (link) return link; }
return null; }
function applyCustomTitles() { const titles = loadCustomTitles();
document.querySelectorAll(LINK_SELECTOR).forEach((link) => { const customTitle = titles[normalizeHref(link.href)]; if (customTitle) link.textContent = customTitle; }); }
function isWatchlistPage() { return /\/watchlist\.php/i.test(location.pathname); }
function isControlCheckbox(box) { const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase(); return text.includes('all') || text.includes('checkall') || text.includes('check_all'); }
function getTitleFromNode(node) { const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; return link ? sortText(link.textContent) : ''; }
function nextNodeBelongsToEntry(node) { if (!node) return false; if (node.nodeType === Node.TEXT_NODE) return true; if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.matches('br')) return true; return false; }
function collectTrailingBreaks(startNode, nodes) { let node = startNode.nextSibling; let guard = 0;
while (nextNodeBelongsToEntry(node) && guard < 10) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break; node = node.nextSibling; guard += 1; } }
function buildEntryFromCheckbox(box, index) { if (isControlCheckbox(box)) return null;
const tableRow = box.closest('tr'); if (tableRow && tableRow.querySelector(LINK_SELECTOR)) { return { index, parent: tableRow.parentNode, title: getTitleFromNode(tableRow), nodes: [tableRow] }; }
const label = box.closest('label'); if (label && label.querySelector(LINK_SELECTOR)) { const nodes = [label]; collectTrailingBreaks(label, nodes);
return { index, parent: label.parentNode, title: getTitleFromNode(label), nodes }; }
const parent = box.parentNode; if (!parent) return null;
let link = null; let node = box.nextSibling; let guard = 0;
while (node && node.parentNode === parent && guard < 30) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(LINK_SELECTOR)) { link = node; break; }
link = node.querySelector(LINK_SELECTOR); if (link) break;
if (node.matches('br') || node.matches('input[type="checkbox"]')) break; }
node = node.nextSibling; guard += 1; }
if (!link) return null;
const nodes = []; node = box; guard = 0;
while (node && node.parentNode === parent && guard < 40) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) { break; }
node = next; guard += 1; }
return { index, parent, title: sortText(link.textContent), nodes }; }
function findEntries() { applyCustomTitles();
const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const entries = boxes .map(buildEntryFromCheckbox) .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);
const groups = new Map(); entries.forEach((entry) => { if (!groups.has(entry.parent)) groups.set(entry.parent, []); groups.get(entry.parent).push(entry); });
let bestEntries = []; groups.forEach((groupEntries) => { if (groupEntries.length > bestEntries.length) bestEntries = groupEntries; });
return bestEntries.length > 1 ? bestEntries : []; }
function getSelectedEntries() { return findEntries().filter((entry) => { return entry.nodes.some((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return false;
const checkbox = node.matches('input[type="checkbox"]') ? node : node.querySelector('input[type="checkbox"]');
return checkbox && checkbox.checked && !isControlCheckbox(checkbox); }); }); }
function editSelectedEntryTitle() { const selectedEntries = getSelectedEntries();
if (selectedEntries.length !== 1) { setStatus('Select exactly one entry to rename'); return; }
const link = getEntryLink(selectedEntries[0]); if (!link) { setStatus('Selected entry link was not found'); return; }
const currentTitle = cleanText(link.textContent); const nextTitle = window.prompt('Edit selected watchlist entry name:', currentTitle); if (nextTitle === null) return;
const cleanTitle = cleanText(nextTitle); if (!cleanTitle) { setStatus('Name was not changed'); return; }
const titles = loadCustomTitles(); titles[normalizeHref(link.href)] = cleanTitle; saveCustomTitles(titles);
link.textContent = cleanTitle; setStatus('Selected entry name updated'); }
function moveEntries(entries) { const parent = entries[0].parent; const marker = document.createComment('bt-watchlist-sort-marker'); parent.insertBefore(marker, entries[0].nodes[0]);
entries.forEach((entry) => { entry.nodes.forEach((node) => parent.insertBefore(node, marker)); });
parent.removeChild(marker); }
function sortEntries() { const entries = findEntries(); if (!entries.length) { setStatus('No sortable entries found'); return; }
if (!originalEntries) { originalEntries = entries.map((entry) => ({ parent: entry.parent, title: entry.title, nodes: entry.nodes.slice() })); }
const sortedEntries = entries.slice().sort((a, b) => { return a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }); });
moveEntries(sortedEntries); setStatus(`Sorted ${sortedEntries.length} entries`); }
function restoreEntries() { if (!originalEntries) { setStatus('Original order is not saved yet'); return; }
moveEntries(originalEntries); setStatus('Original order restored'); }
function setStatus(message) { const status = document.getElementById(STATUS_ID); if (status) status.textContent = message; }
function insertButton() { if (document.getElementById(BUTTON_ID)) return;
const entries = findEntries();
const wrapper = document.createElement('div'); wrapper.style.margin = '10px 0';
const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = 'Sort A-Z'; button.style.marginRight = '8px'; button.style.padding = '4px 10px'; button.style.cursor = 'pointer';
const editButton = document.createElement('button'); editButton.id = EDIT_BUTTON_ID; editButton.type = 'button'; editButton.textContent = 'Edit selected name'; editButton.style.marginRight = '8px'; editButton.style.padding = '4px 10px'; editButton.style.cursor = 'pointer';
const status = document.createElement('span'); status.id = STATUS_ID; status.style.color = '#555'; status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';
button.addEventListener('click', () => { if (sorted) { restoreEntries(); button.textContent = 'Sort A-Z'; sorted = false; return; }
sortEntries(); button.textContent = 'Restore original order'; sorted = true; });
editButton.addEventListener('click', editSelectedEntryTitle);
wrapper.appendChild(button); wrapper.appendChild(editButton); wrapper.appendChild(status);
const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => { const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase(); return text.includes('remove checked'); });
if (removeButton && removeButton.parentNode) { removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling); return; }
const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => { return cleanText(element.textContent).toLowerCase() === 'edit watchlist'; });
if (title && title.parentNode) { title.parentNode.insertBefore(wrapper, title.nextSibling); return; }
document.body.insertBefore(wrapper, document.body.firstChild); }
function init() { if (!isWatchlistPage()) return; applyCustomTitles(); insertButton(); }
init(); window.setTimeout(init, 500); window.setTimeout(init, 1500); })();
Actually with time, people will have to clean their watchlists, and large watchlists will be reduce in number of topics. I agree that your userscript is useful for people who have need of sorting watched topics but I feel differently.
If I consider a thread is helpful, I will either bookmark it on my browser or add it to my documents which can be a word file or an excel file or anything document format which can help me finding such useful threads later quickly and easily.
Personally I don't have need of sorting A-Z topics in my watchlist because it's actually not too large.
Your point is valid. A-Z sorting may not be necessary for small watchlists, and it is a good habit to bookmark/document important threads. This script is mainly for those who use watchlist as a temporary tracking tool and follow many topics at once. A-Z sorting can be useful when searching for specific topics from a large watchlist, removing them or reviewing them. It is not necessary for all users, but it can be a small convenience tool for heavy watchlist users. From your feedback I got the idea for an export/bookmark style feature for a future version.
|
|
|
|
|
satscraper
Legendary

Activity: 1540
Merit: 2856
|
 |
June 08, 2026, 07:33:34 PM Last edit: June 08, 2026, 07:50:54 PM by satscraper Merited by GhostOfBitcoin (1) |
|
~
Well, I have updated to the new script version. Both "Sort A–Z" and "Edit selected name" buttons are in place and functioning as they should. However, the only problem at least for me, since I prefer the browser to clear data on exit is that local storage data is also deleted when browser closes. As a result, the changes made don’t persist, and there’s seems to be no way to add an exclusion for Bitcointalk in Brave, which I tend to use. This makes the "Edit selected name" button useless in this case because it requires some tine to rename which is painfull task when the list is big. The "Sort A–Z" button remains useful, because even though relevant data doesn’t survive after exit, it still only takes one click to reorder the full list. The workaround could be to save the relevant key and value for local storage and add them manually after the browser is opened, but that would also take some time. I’ll probably think about this though I’m not sure yet. For those users who dont clear browsers on exit "Edit selected name" button remains useful.
|
| EARNBET | | | ⚽ 🏀 🏈 🏓 🎯 🥊 |
| ⚾ 🎾 ⛳ 🏐 🏏 🏎️ | | |
███████▄▄███████████ ████▄██████████████████ ██▄▀▀███████████████▀▀███ █▄████████████████████████ ▄▄████████▀▀▀▀▀████████▄▄██ ███████████████████████████ █████████▌████▀████████████ ███████████████████████████ ▀▀███████▄▄▄▄▄█████████▀▀██ █▀█████████████████████▀██ ██▀▄▄███████████████▄▄███ ████▀██████████████████ ███████▀▀███████████ | ....HIGHEST.... VIP REWARDS ✔ G U A R A N T E E D
| | | 🜲 | KING OF THE CASTLE $200K in prizes | | | ..PLAY NOW.. |
|
|
|
GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 09, 2026, 04:41:52 AM |
|
~
Well, I have updated to the new script version. Both "Sort A–Z" and "Edit selected name" buttons are in place and functioning as they should. However, the only problem at least for me, since I prefer the browser to clear data on exit is that local storage data is also deleted when browser closes. As a result, the changes made don’t persist, and there’s seems to be no way to add an exclusion for Bitcointalk in Brave, which I tend to use. This makes the "Edit selected name" button useless in this case because it requires some tine to rename which is painfull task when the list is big. The "Sort A–Z" button remains useful, because even though relevant data doesn’t survive after exit, it still only takes one click to reorder the full list. The workaround could be to save the relevant key and value for local storage and add them manually after the browser is opened, but that would also take some time. I’ll probably think about this though I’m not sure yet. For those users who dont clear browsers on exit "Edit selected name" button remains useful. Previously, the script would store the renamed name in the browser's localStorage. Now, it stores it in the userscript manager's storage, using GM_setValue() and GM_getValue(). localStorage is part of the browser's site data. So if "Clear site data on exit" is enabled in Brave or another browser, then the localStorage of bitcointalk.org could be deleted. Then the renamed name would also be lost. When saved with GM_setValue(), the data is usually stored in the userscript manager's storage. It is not the website's cache/localStorage. So Brave's "Clear site data on exit" settings usually have no effect. In the loadCustomTitles() function, GM_getValue() has been used instead of localStorage.getItem(). In the saveCustomTitles() function, GM_setValue() has been used instead of localStorage.setItem(). Rename data will usually remain even if you clear browser cache Rename data will usually remain even if you clear Brave site data Users do not need to export/import Userscript Version 1.2.0: 06-09-2026 --> 04:41:52 AM// ==UserScript== // @name Bitcointalk Watchlist Alphabetical Sorter // @namespace bitcointalk-watchlist-tools // @version 1.2.0 // @description Adds Sort A-Z and selected-entry rename buttons on Bitcointalk's edit watchlist page. // @author GhostOfBitcoin // @match https://bitcointalk.org/watchlist.php* // @match http://bitcointalk.org/watchlist.php* // @include https://bitcointalk.org/watchlist.php* // @include http://bitcointalk.org/watchlist.php* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // ==/UserScript==
(function () { 'use strict';
const BUTTON_ID = 'bt-watchlist-sort-az'; const EDIT_BUTTON_ID = 'bt-watchlist-edit-title'; const STATUS_ID = 'bt-watchlist-sort-status'; const STORAGE_KEY = 'bt-watchlist-custom-titles'; const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';
let originalEntries = null; let sorted = false;
function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
function sortText(value) { return cleanText(value).toLocaleLowerCase(); }
function loadCustomTitles() { try { const titles = GM_getValue(STORAGE_KEY, {}); return titles && typeof titles === 'object' ? titles : {}; } catch (error) { return {}; } }
function saveCustomTitles(titles) { GM_setValue(STORAGE_KEY, titles); }
function normalizeHref(href) { try { const url = new URL(href, location.href); url.hash = ''; return url.href; } catch (error) { return String(href || ''); } }
function getEntryLink(entry) { for (const node of entry.nodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.matches && node.matches(LINK_SELECTOR)) return node;
const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; if (link) return link; }
return null; }
function applyCustomTitles() { const titles = loadCustomTitles();
document.querySelectorAll(LINK_SELECTOR).forEach((link) => { const customTitle = titles[normalizeHref(link.href)]; if (customTitle) link.textContent = customTitle; }); }
function isWatchlistPage() { return /\/watchlist\.php/i.test(location.pathname); }
function isControlCheckbox(box) { const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase(); return text.includes('all') || text.includes('checkall') || text.includes('check_all'); }
function getTitleFromNode(node) { const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; return link ? sortText(link.textContent) : ''; }
function nextNodeBelongsToEntry(node) { if (!node) return false; if (node.nodeType === Node.TEXT_NODE) return true; if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.matches('br')) return true; return false; }
function collectTrailingBreaks(startNode, nodes) { let node = startNode.nextSibling; let guard = 0;
while (nextNodeBelongsToEntry(node) && guard < 10) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break; node = node.nextSibling; guard += 1; } }
function buildEntryFromCheckbox(box, index) { if (isControlCheckbox(box)) return null;
const tableRow = box.closest('tr'); if (tableRow && tableRow.querySelector(LINK_SELECTOR)) { return { index, parent: tableRow.parentNode, title: getTitleFromNode(tableRow), nodes: [tableRow] }; }
const label = box.closest('label'); if (label && label.querySelector(LINK_SELECTOR)) { const nodes = [label]; collectTrailingBreaks(label, nodes);
return { index, parent: label.parentNode, title: getTitleFromNode(label), nodes }; }
const parent = box.parentNode; if (!parent) return null;
let link = null; let node = box.nextSibling; let guard = 0;
while (node && node.parentNode === parent && guard < 30) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(LINK_SELECTOR)) { link = node; break; }
link = node.querySelector(LINK_SELECTOR); if (link) break;
if (node.matches('br') || node.matches('input[type="checkbox"]')) break; }
node = node.nextSibling; guard += 1; }
if (!link) return null;
const nodes = []; node = box; guard = 0;
while (node && node.parentNode === parent && guard < 40) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) { break; }
node = next; guard += 1; }
return { index, parent, title: sortText(link.textContent), nodes }; }
function findEntries() { applyCustomTitles();
const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const entries = boxes .map(buildEntryFromCheckbox) .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);
const groups = new Map(); entries.forEach((entry) => { if (!groups.has(entry.parent)) groups.set(entry.parent, []); groups.get(entry.parent).push(entry); });
let bestEntries = []; groups.forEach((groupEntries) => { if (groupEntries.length > bestEntries.length) bestEntries = groupEntries; });
return bestEntries.length > 1 ? bestEntries : []; }
function getSelectedEntries() { return findEntries().filter((entry) => { return entry.nodes.some((node) => { if (node.nodeType !== Node.ELEMENT_NODE) return false;
const checkbox = node.matches('input[type="checkbox"]') ? node : node.querySelector('input[type="checkbox"]');
return checkbox && checkbox.checked && !isControlCheckbox(checkbox); }); }); }
function editSelectedEntryTitle() { const selectedEntries = getSelectedEntries();
if (selectedEntries.length !== 1) { setStatus('Select exactly one entry to rename'); return; }
const link = getEntryLink(selectedEntries[0]); if (!link) { setStatus('Selected entry link was not found'); return; }
const currentTitle = cleanText(link.textContent); const nextTitle = window.prompt('Edit selected watchlist entry name:', currentTitle); if (nextTitle === null) return;
const cleanTitle = cleanText(nextTitle); if (!cleanTitle) { setStatus('Name was not changed'); return; }
const titles = loadCustomTitles(); titles[normalizeHref(link.href)] = cleanTitle; saveCustomTitles(titles);
link.textContent = cleanTitle; setStatus('Selected entry name updated'); }
function moveEntries(entries) { const parent = entries[0].parent; const marker = document.createComment('bt-watchlist-sort-marker'); parent.insertBefore(marker, entries[0].nodes[0]);
entries.forEach((entry) => { entry.nodes.forEach((node) => parent.insertBefore(node, marker)); });
parent.removeChild(marker); }
function sortEntries() { const entries = findEntries(); if (!entries.length) { setStatus('No sortable entries found'); return; }
if (!originalEntries) { originalEntries = entries.map((entry) => ({ parent: entry.parent, title: entry.title, nodes: entry.nodes.slice() })); }
const sortedEntries = entries.slice().sort((a, b) => { return a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }); });
moveEntries(sortedEntries); setStatus(`Sorted ${sortedEntries.length} entries`); }
function restoreEntries() { if (!originalEntries) { setStatus('Original order is not saved yet'); return; }
moveEntries(originalEntries); setStatus('Original order restored'); }
function setStatus(message) { const status = document.getElementById(STATUS_ID); if (status) status.textContent = message; }
function insertButton() { if (document.getElementById(BUTTON_ID)) return;
const entries = findEntries();
const wrapper = document.createElement('div'); wrapper.style.margin = '10px 0';
const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = 'Sort A-Z'; button.style.marginRight = '8px'; button.style.padding = '4px 10px'; button.style.cursor = 'pointer';
const editButton = document.createElement('button'); editButton.id = EDIT_BUTTON_ID; editButton.type = 'button'; editButton.textContent = 'Edit selected name'; editButton.style.marginRight = '8px'; editButton.style.padding = '4px 10px'; editButton.style.cursor = 'pointer';
const status = document.createElement('span'); status.id = STATUS_ID; status.style.color = '#555'; status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';
button.addEventListener('click', () => { if (sorted) { restoreEntries(); button.textContent = 'Sort A-Z'; sorted = false; return; }
sortEntries(); button.textContent = 'Restore original order'; sorted = true; });
editButton.addEventListener('click', editSelectedEntryTitle);
wrapper.appendChild(button); wrapper.appendChild(editButton); wrapper.appendChild(status);
const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => { const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase(); return text.includes('remove checked'); });
if (removeButton && removeButton.parentNode) { removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling); return; }
const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => { return cleanText(element.textContent).toLowerCase() === 'edit watchlist'; });
if (title && title.parentNode) { title.parentNode.insertBefore(wrapper, title.nextSibling); return; }
document.body.insertBefore(wrapper, document.body.firstChild); }
function init() { if (!isWatchlistPage()) return; applyCustomTitles(); insertButton(); }
init(); window.setTimeout(init, 500); window.setTimeout(init, 1500); })();
|
|
|
|
|
satscraper
Legendary

Activity: 1540
Merit: 2856
|
 |
June 09, 2026, 08:19:45 AM Last edit: June 09, 2026, 08:38:36 AM by satscraper |
|
~
I have decided not to store anything locally and have reverted to the first version of the script with a single A–Z button, because I'm afraid if Bitcointalk ever suffered an XSS attack my machine could become vulnerable via methods introduced in the last two versions. So my Tampermonkey keeps only the first script active; it will do its task without undermining my machine. P.S. I recommend keeping this first version unchanged on OP so everyone can choose their way.
|
| EARNBET | | | ⚽ 🏀 🏈 🏓 🎯 🥊 |
| ⚾ 🎾 ⛳ 🏐 🏏 🏎️ | | |
███████▄▄███████████ ████▄██████████████████ ██▄▀▀███████████████▀▀███ █▄████████████████████████ ▄▄████████▀▀▀▀▀████████▄▄██ ███████████████████████████ █████████▌████▀████████████ ███████████████████████████ ▀▀███████▄▄▄▄▄█████████▀▀██ █▀█████████████████████▀██ ██▀▄▄███████████████▄▄███ ████▀██████████████████ ███████▀▀███████████ | ....HIGHEST.... VIP REWARDS ✔ G U A R A N T E E D
| | | 🜲 | KING OF THE CASTLE $200K in prizes | | | ..PLAY NOW.. |
|
|
|
GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 09, 2026, 12:07:55 PM Merited by satscraper (1) |
|
~
I have decided not to store anything locally and have reverted to the first version of the script with a single A–Z button, because I'm afraid if Bitcointalk ever suffered an XSS attack my machine could become vulnerable via methods introduced in the last two versions. So my Tampermonkey keeps only the first script active; it will do its task without undermining my machine. P.S. I recommend keeping this first version unchanged on OP so everyone can choose their way. I understand your concern. The new version only stores custom display names for watchlist links. For example, a topic URL can be associated with a custom title of the user's choice. It does not store any passwords, cookies, private keys, login data, and does not send any information anywhere. In the latest version, I have used GM_getValue/GM_setValue instead of localStorage, so the information is stored in the userscript manager's storage, not in the website's localStorage. Userscript version 1.0.0 - June 08, 2026, 09:47:29 AMGreasyFork DownloadUserscript version 1.1.0 - June 08, 2026, 05:00:03 PMGreasyFork DownloadUserscript version 1.2.0 - 06-09-2026 --> 04:41:52 AMGreasyFork DownloadStill, I agree that the user should have the freedom to choose. So I'll leave the original A-Z only version available in the OP and point out that the version with the rename feature is completely optional, only for those who want to use a permanent custom name.
|
|
|
|
|
|
Dareo
|
 |
June 09, 2026, 08:36:12 PM |
|
Seeing this thing, I found it very useful, but the problem is that I visit the forum most of the time from my mobile browser, so in that case, this script cannot be used on my mobile Google Chrome browser, because I am not able to install any extension, so I want to know if there is any way to use this script on my mobile, if there is, I will definitely use it and it will be very useful for me.
|
|
|
|
GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 10, 2026, 08:22:24 AM |
|
Seeing this thing, I found it very useful, but the problem is that I visit the forum most of the time from my mobile browser, so in that case, this script cannot be used on my mobile Google Chrome browser, because I am not able to install any extension, so I want to know if there is any way to use this script on my mobile, if there is, I will definitely use it and it will be very useful for me.
Hey My friend @Dareo, This script cannot be used directly in mobile Google Chrome, because Chrome on Android does not support userscript managers like Tampermonkey or Violentmonkey. However, there are some alternatives: 1. Firefox for Android: A userscript manager extension is available there, the script can be installed and used. Step 1: Install Mozilla Firefox on Android. Step 2: Open Firefox and go to the Add-ons section. Step 3: Install the Tampermonkey or Violentmonkey add-on. Step 4: Open the Tampermonkey Extension's Dashboard. Step 5: Click "Create New Script". Step 6: Paste the Userscript code. Step 7: Save and open Bitcointalk to test. 2. Kiwi Browser: Kiwi Browser on Android supports many Chrome extensions, so Tampermonkey can usually be installed there. Step 1: Install Kiwi Browser on Android phone. Step 2: Open the browser and go to the Chrome Web Store. Step 3: Install Tampermonkey from the Chrome Web Store. Step 4: Open the Tampermonkey Extension's Dashboard. Step 5: Click "Create New Script". Step 6: Paste the Userscript code. Step 7: Save and open Bitcointalk to test. If you want stability and good extension support --> Firefox + Violentmonkey Or If you want a Chrome-like experience --> Kiwi Browser + Tampermonkey
|
|
|
|
|
satscraper
Legendary

Activity: 1540
Merit: 2856
|
 |
June 11, 2026, 07:05:25 AM Last edit: June 11, 2026, 09:56:19 AM by satscraper Merited by GhostOfBitcoin (2) |
|
~ I understand your concern.
BTW, I thought I could have my cake and eat it too if you add to A-Z script a function to simultaneously and automatically strip off the sorted entries both the decorations (like 🟠🛡️) and unnecessary leading prefixes like New, ANN, INFO, BOUNTY etc, leaving just the core. This would remove the need to store anything locally. Just press A-Z button whenever you need it, and shazam! Would it be possible? UPD. I did it by myself with help of Claude code. // ==UserScript== // @name Bitcointalk Watchlist Alphabetical Sorter and Stripper // @namespace bitcointalk-watchlist-tools // @version 4.0.0 // @description Adds a Sort A-Z button on Bitcointalk's edit watchlist page. Strips decorations and noise prefixes from titles when sorted. // @author You // @match https://bitcointalk.org/watchlist.php* // @match http://bitcointalk.org/watchlist.php* // @include https://bitcointalk.org/watchlist.php* // @include http://bitcointalk.org/watchlist.php* // @run-at document-idle // @grant none // ==/UserScript==
(function () { 'use strict';
const BUTTON_ID = 'bt-watchlist-sort-az'; const STATUS_ID = 'bt-watchlist-sort-status'; const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]';
// Prefixes to strip (case-insensitive, including common bracket forms) const NOISE_PREFIX_RE = /^[\s\[\]|:–—-]*(?:new|ann|announcement|discussion|info|bounty|airdrop|ico|ieo|ido|presale|pre-sale|wts|wtb|wtt|sell|buy|trade|hot|tutorial|pin|pinned|sticky|closed|moved|locked|official|update|urgent|important)\b[\s\[\]|:–—-]*/i;
// Decorations: emoji and other Unicode symbols that commonly appear as topic decorations // Covers: emoji (U+1F000–U+1FFFF), dingbats (U+2600–U+27BF), enclosed alphanumerics, etc. const DECORATION_RE = /[\u{1F000}-\u{1FFFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{FE00}-\u{FE0F}\u{20D0}-\u{20FF}]+/gu;
// Stray punctuation left over after stripping, at the start of the string const LEADING_JUNK_RE = /^[\s\[\](){}|:;.!?–—★●▶►◆◇♦•*#@~^_/\\,'"<>]+/;
let originalEntries = null; let sorted = false;
// Map: link element → original text content (populated on first sort) const originalLinkTexts = new WeakMap();
function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
function sortText(value) { return cleanText(value).toLocaleLowerCase(); }
/** * Derive a clean display title from a raw link text. * Removes decorative Unicode symbols and leading noise-word prefixes, * then trims any leftover junk characters from the start. */ function cleanDisplayTitle(raw) { let s = raw; // 1. Strip emoji / Unicode decorations (multiple passes for clusters) s = s.replace(DECORATION_RE, ' '); // 2. Strip leading noise-word prefixes (may repeat: "[ ANN ] NEW Foo" → strip twice) let prev; do { prev = s; s = s.replace(NOISE_PREFIX_RE, ''); } while (s !== prev); // 3. Strip leftover leading junk punctuation s = s.replace(LEADING_JUNK_RE, ''); // 4. Normalise internal whitespace s = s.replace(/\s+/g, ' ').trim(); return s || raw.trim(); // fall back to original if we stripped everything }
function isWatchlistPage() { return /\/watchlist\.php/i.test(location.pathname); }
function isControlCheckbox(box) { const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase(); return text.includes('all') || text.includes('checkall') || text.includes('check_all'); }
function getTitleFromNode(node) { const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; return link ? sortText(link.textContent) : ''; }
function nextNodeBelongsToEntry(node) { if (!node) return false; if (node.nodeType === Node.TEXT_NODE) return true; if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.matches('br')) return true; return false; }
function collectTrailingBreaks(startNode, nodes) { let node = startNode.nextSibling; let guard = 0;
while (nextNodeBelongsToEntry(node) && guard < 10) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break; node = node.nextSibling; guard += 1; } }
function buildEntryFromCheckbox(box, index) { if (isControlCheckbox(box)) return null;
const tableRow = box.closest('tr'); if (tableRow && tableRow.querySelector(LINK_SELECTOR)) { return { index, parent: tableRow.parentNode, title: getTitleFromNode(tableRow), nodes: [tableRow] }; }
const label = box.closest('label'); if (label && label.querySelector(LINK_SELECTOR)) { const nodes = [label]; collectTrailingBreaks(label, nodes);
return { index, parent: label.parentNode, title: getTitleFromNode(label), nodes }; }
const parent = box.parentNode; if (!parent) return null;
let link = null; let node = box.nextSibling; let guard = 0;
while (node && node.parentNode === parent && guard < 30) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(LINK_SELECTOR)) { link = node; break; }
link = node.querySelector(LINK_SELECTOR); if (link) break;
if (node.matches('br') || node.matches('input[type="checkbox"]')) break; }
node = node.nextSibling; guard += 1; }
if (!link) return null;
const nodes = []; node = box; guard = 0;
while (node && node.parentNode === parent && guard < 40) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) { break; }
node = next; guard += 1; }
return { index, parent, title: sortText(link.textContent), nodes }; }
function findEntries() { const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const entries = boxes .map(buildEntryFromCheckbox) .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);
const groups = new Map(); entries.forEach((entry) => { if (!groups.has(entry.parent)) groups.set(entry.parent, []); groups.get(entry.parent).push(entry); });
let bestEntries = []; groups.forEach((groupEntries) => { if (groupEntries.length > bestEntries.length) bestEntries = groupEntries; });
return bestEntries.length > 1 ? bestEntries : []; }
function moveEntries(entries) { const parent = entries[0].parent; const marker = document.createComment('bt-watchlist-sort-marker'); parent.insertBefore(marker, entries[0].nodes[0]);
entries.forEach((entry) => { entry.nodes.forEach((node) => parent.insertBefore(node, marker)); });
parent.removeChild(marker); }
/** * For each entry's nodes, find every watchlist link and replace its * visible text with a cleaned version, saving the original in a WeakMap. */ function applyCleanTitles(entries) { entries.forEach((entry) => { entry.nodes.forEach((node) => { const links = node.querySelectorAll ? node.querySelectorAll(LINK_SELECTOR) : []; links.forEach((link) => { if (!originalLinkTexts.has(link)) { originalLinkTexts.set(link, link.textContent); } const cleaned = cleanDisplayTitle(link.textContent); if (cleaned !== link.textContent) { link.textContent = cleaned; } }); // Handle the case where the node itself is the link if (node.matches && node.matches(LINK_SELECTOR)) { if (!originalLinkTexts.has(node)) { originalLinkTexts.set(node, node.textContent); } const cleaned = cleanDisplayTitle(node.textContent); if (cleaned !== node.textContent) { node.textContent = cleaned; } } }); }); }
/** * Restore every link's text content from the WeakMap originals. */ function restoreCleanTitles(entries) { entries.forEach((entry) => { entry.nodes.forEach((node) => { const links = node.querySelectorAll ? node.querySelectorAll(LINK_SELECTOR) : []; links.forEach((link) => { if (originalLinkTexts.has(link)) { link.textContent = originalLinkTexts.get(link); } }); if (node.matches && node.matches(LINK_SELECTOR) && originalLinkTexts.has(node)) { node.textContent = originalLinkTexts.get(node); } }); }); }
function sortEntries() { const entries = findEntries(); if (!entries.length) { setStatus('No sortable entries found'); return; }
if (!originalEntries) { originalEntries = entries.map((entry) => ({ parent: entry.parent, title: entry.title, nodes: entry.nodes.slice() })); }
// Clean display text before sorting so the sorted list shows clean titles applyCleanTitles(entries);
// Re-read titles after cleaning so sort order reflects cleaned text const enriched = entries.map((entry) => { const link = entry.nodes.reduce((found, node) => { if (found) return found; if (node.querySelectorAll) { const l = node.querySelector(LINK_SELECTOR); if (l) return l; } if (node.matches && node.matches(LINK_SELECTOR)) return node; return null; }, null); return { ...entry, sortTitle: link ? sortText(link.textContent) : entry.title }; });
const sortedEntries = enriched.slice().sort((a, b) => { return a.sortTitle.localeCompare(b.sortTitle, undefined, { numeric: true, sensitivity: 'base' }); });
moveEntries(sortedEntries); setStatus(`Sorted ${sortedEntries.length} entries`); }
function restoreEntries() { if (!originalEntries) { setStatus('Original order is not saved yet'); return; }
restoreCleanTitles(originalEntries); moveEntries(originalEntries); setStatus('Original order restored'); }
function setStatus(message) { const status = document.getElementById(STATUS_ID); if (status) status.textContent = message; }
function insertButton() { if (document.getElementById(BUTTON_ID)) return;
const entries = findEntries();
const wrapper = document.createElement('div'); wrapper.style.margin = '10px 0';
const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = 'Sort A-Z'; button.style.marginRight = '8px'; button.style.padding = '4px 10px'; button.style.cursor = 'pointer';
const status = document.createElement('span'); status.id = STATUS_ID; status.style.color = '#555'; status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';
button.addEventListener('click', () => { if (sorted) { restoreEntries(); button.textContent = 'Sort A-Z'; sorted = false; return; }
sortEntries(); button.textContent = 'Restore original order'; sorted = true; });
wrapper.appendChild(button); wrapper.appendChild(status);
const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => { const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase(); return text.includes('remove checked'); });
if (removeButton && removeButton.parentNode) { removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling); return; }
const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => { return cleanText(element.textContent).toLowerCase() === 'edit watchlist'; });
if (title && title.parentNode) { title.parentNode.insertBefore(wrapper, title.nextSibling); return; }
document.body.insertBefore(wrapper, document.body.firstChild); }
function init() { if (!isWatchlistPage()) return; insertButton(); }
init(); window.setTimeout(init, 500); window.setTimeout(init, 1500); })();
Claude comments: "The key addition is a cleanDisplayTitle function that strips decorations and noise prefixes from the link's visible text (not its href) when sorting is applied, and a restoreDisplayTitle that puts the original text back on restore." I have tested it. Works like a charm. From now on my Tampermonkey keeps active this version. Watch list is cleaned from noise-words and decorations and A-Z ordered. Nothing is stored locally. Finally I have achieved what I wanted to.
|
| EARNBET | | | ⚽ 🏀 🏈 🏓 🎯 🥊 |
| ⚾ 🎾 ⛳ 🏐 🏏 🏎️ | | |
███████▄▄███████████ ████▄██████████████████ ██▄▀▀███████████████▀▀███ █▄████████████████████████ ▄▄████████▀▀▀▀▀████████▄▄██ ███████████████████████████ █████████▌████▀████████████ ███████████████████████████ ▀▀███████▄▄▄▄▄█████████▀▀██ █▀█████████████████████▀██ ██▀▄▄███████████████▄▄███ ████▀██████████████████ ███████▀▀███████████ | ....HIGHEST.... VIP REWARDS ✔ G U A R A N T E E D
| | | 🜲 | KING OF THE CASTLE $200K in prizes | | | ..PLAY NOW.. |
|
|
|
GhostOfBitcoin (OP)
Member


Activity: 70
Merit: 41
|
 |
June 11, 2026, 09:51:51 AM Last edit: June 11, 2026, 10:26:55 AM by GhostOfBitcoin Merited by satscraper (1) |
|
~ I understand your concern.
BTW, I thought I could have my cake and eat it too if you add to A-Z script a function to simultaneously and automatically strip off the sorted entries both the decorations (like 🟠🛡️) and unnecessary leading prefixes like New, ANN, INFO, BOUNTY etc, leaving just the core. This would remove the need to store anything locally. Just press A-Z button whenever you need it, and shazam! Would it be possible? This has been done as per your request. Unnecessary prefixes, spaces, emoji, symbols, bracket blocks and separators at the beginning of the topic have been removed. The prefix in a Watchlist entry can be in different forms, such as: 🟠🛡️ [ANN] Project Name [ANN][ICO] Token Name (INFO) Wallet Guide {SERVICE} Escrow Thread --- [BOUNTY] Campaign Name
After removing: Project Name Token Name Wallet Guide Escrow Thread Campaign Name
Now everything is cleaned using regex and the prefix-related parts are removed until the original title starts. As a result, while sorting, the prefix is removed and the sorting is done based on the original title. Also, the script does not save any data anywhere. It only cleans the visible title of the current page and sorts according to that cleaned title. Userscript version 1.3.0 - GreasyFork DownloadUserscript version 1.3.0: // ==UserScript== // @name Bitcointalk Watchlist Alphabetical Sorter // @namespace bitcointalk-watchlist-tools // @version 1.3.0 // @description Adds a Sort A-Z button that removes common watchlist title prefixes on Bitcointalk's edit watchlist page. // @author GhostOfBitcoin // @match https://bitcointalk.org/watchlist.php* // @match http://bitcointalk.org/watchlist.php* // @include https://bitcointalk.org/watchlist.php* // @include http://bitcointalk.org/watchlist.php* // @run-at document-idle // @grant none // ==/UserScript==
(function () { 'use strict';
const BUTTON_ID = 'bt-watchlist-sort-az'; const STATUS_ID = 'bt-watchlist-sort-status'; const LINK_SELECTOR = 'a[href*="topic="], a[href*="board="]'; const PREFIX_PATTERN = /^(?:\s|[\u{1F000}-\u{1FAFF}\u2600-\u27BF]\uFE0F?|[\[\(\{][^\]\)\}]{0,80}[\]\)\}]|[|:;,\-\u2013\u2014~\u2022*]+)+/gu;
let originalEntries = null; let sorted = false;
function cleanText(value) { return String(value || '').replace(/\s+/g, ' ').trim(); }
function sortText(value) { return cleanText(value).replace(PREFIX_PATTERN, '').toLocaleLowerCase(); }
function getEntryLink(entry) { for (const node of entry.nodes) { if (node.nodeType !== Node.ELEMENT_NODE) continue;
if (node.matches && node.matches(LINK_SELECTOR)) return node;
const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; if (link) return link; }
return null; }
function isWatchlistPage() { return /\/watchlist\.php/i.test(location.pathname); }
function isControlCheckbox(box) { const text = `${box.name || ''} ${box.id || ''} ${box.value || ''}`.toLowerCase(); return text.includes('all') || text.includes('checkall') || text.includes('check_all'); }
function getTitleFromNode(node) { const link = node.querySelector ? node.querySelector(LINK_SELECTOR) : null; return link ? sortText(link.textContent) : ''; }
function nextNodeBelongsToEntry(node) { if (!node) return false; if (node.nodeType === Node.TEXT_NODE) return true; if (node.nodeType !== Node.ELEMENT_NODE) return false; if (node.matches('br')) return true; return false; }
function collectTrailingBreaks(startNode, nodes) { let node = startNode.nextSibling; let guard = 0;
while (nextNodeBelongsToEntry(node) && guard < 10) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break; node = node.nextSibling; guard += 1; } }
function buildEntryFromCheckbox(box, index) { if (isControlCheckbox(box)) return null;
const tableRow = box.closest('tr'); if (tableRow && tableRow.querySelector(LINK_SELECTOR)) { return { index, parent: tableRow.parentNode, title: getTitleFromNode(tableRow), nodes: [tableRow] }; }
const label = box.closest('label'); if (label && label.querySelector(LINK_SELECTOR)) { const nodes = [label]; collectTrailingBreaks(label, nodes);
return { index, parent: label.parentNode, title: getTitleFromNode(label), nodes }; }
const parent = box.parentNode; if (!parent) return null;
let link = null; let node = box.nextSibling; let guard = 0;
while (node && node.parentNode === parent && guard < 30) { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches(LINK_SELECTOR)) { link = node; break; }
link = node.querySelector(LINK_SELECTOR); if (link) break;
if (node.matches('br') || node.matches('input[type="checkbox"]')) break; }
node = node.nextSibling; guard += 1; }
if (!link) return null;
const nodes = []; node = box; guard = 0;
while (node && node.parentNode === parent && guard < 40) { nodes.push(node); if (node.nodeType === Node.ELEMENT_NODE && node.matches('br')) break;
const next = node.nextSibling; if (next && next.nodeType === Node.ELEMENT_NODE && next.matches('input[type="checkbox"]')) { break; }
node = next; guard += 1; }
return { index, parent, title: sortText(link.textContent), nodes }; }
function findEntries() { const boxes = Array.from(document.querySelectorAll('input[type="checkbox"]')); const entries = boxes .map(buildEntryFromCheckbox) .filter((entry) => entry && entry.parent && entry.title && entry.nodes.length > 0);
const groups = new Map(); entries.forEach((entry) => { if (!groups.has(entry.parent)) groups.set(entry.parent, []); groups.get(entry.parent).push(entry); });
let bestEntries = []; groups.forEach((groupEntries) => { if (groupEntries.length > bestEntries.length) bestEntries = groupEntries; });
return bestEntries.length > 1 ? bestEntries : []; }
function moveEntries(entries) { const parent = entries[0].parent; const marker = document.createComment('bt-watchlist-sort-marker'); parent.insertBefore(marker, entries[0].nodes[0]);
entries.forEach((entry) => { entry.nodes.forEach((node) => parent.insertBefore(node, marker)); });
parent.removeChild(marker); }
function sortEntries() { const entries = findEntries(); if (!entries.length) { setStatus('No sortable entries found'); return; }
if (!originalEntries) { originalEntries = entries.map((entry) => ({ parent: entry.parent, title: entry.title, nodes: entry.nodes.slice() })); }
entries.forEach((entry) => { const link = getEntryLink(entry); if (!link) return;
const cleanedTitle = cleanText(link.textContent).replace(PREFIX_PATTERN, ''); if (!cleanedTitle) return;
link.textContent = cleanedTitle; entry.title = sortText(cleanedTitle); });
const sortedEntries = entries.slice().sort((a, b) => { return a.title.localeCompare(b.title, undefined, { numeric: true, sensitivity: 'base' }); });
moveEntries(sortedEntries); setStatus(`Sorted ${sortedEntries.length} entries`); }
function restoreEntries() { if (!originalEntries) { setStatus('Original order is not saved yet'); return; }
moveEntries(originalEntries); setStatus('Original order restored'); }
function setStatus(message) { const status = document.getElementById(STATUS_ID); if (status) status.textContent = message; }
function insertButton() { if (document.getElementById(BUTTON_ID)) return;
const entries = findEntries();
const wrapper = document.createElement('div'); wrapper.style.margin = '10px 0';
const button = document.createElement('button'); button.id = BUTTON_ID; button.type = 'button'; button.textContent = 'Sort A-Z'; button.style.marginRight = '8px'; button.style.padding = '4px 10px'; button.style.cursor = 'pointer';
const status = document.createElement('span'); status.id = STATUS_ID; status.style.color = '#555'; status.textContent = entries.length ? `Found ${entries.length} entries` : 'Sorter loaded';
button.addEventListener('click', () => { if (sorted) { restoreEntries(); button.textContent = 'Sort A-Z'; sorted = false; return; }
sortEntries(); button.textContent = 'Restore original order'; sorted = true; });
wrapper.appendChild(button); wrapper.appendChild(status);
const removeButton = Array.from(document.querySelectorAll('input, button')).find((element) => { const text = `${element.value || ''} ${element.textContent || ''}`.toLowerCase(); return text.includes('remove checked'); });
if (removeButton && removeButton.parentNode) { removeButton.parentNode.insertBefore(wrapper, removeButton.nextSibling); return; }
const title = Array.from(document.querySelectorAll('b, h1, h2, h3, td, div')).find((element) => { return cleanText(element.textContent).toLowerCase() === 'edit watchlist'; });
if (title && title.parentNode) { title.parentNode.insertBefore(wrapper, title.nextSibling); return; }
document.body.insertBefore(wrapper, document.body.firstChild); }
function init() { if (!isWatchlistPage()) return; insertButton(); }
init(); window.setTimeout(init, 500); window.setTimeout(init, 1500); })();
-snip- UPD. I did it by myself with help of Claude code.
Nice work, brother, and congratulations but Currently, your script has this regex to remove the noise prefix: const NOISE_PREFIX_RE = /^[\s\[\]|:–—-]*(?:new|ann|announcement|discussion|info|bounty|airdrop|ico|ieo|ido|presale|pre-sale|wts|wtb|wtt|sell|buy|trade|hot|pin|pinned|sticky|closed|moved|locked|official|update|urgent|important)\b[\s\[\]|:–—-]*/i; This will remove: ANN Bitcoin INFO Bitcoin BOUNTY Bitcoin [ANN] Bitcoin [INFO] Bitcoin
But will not remove: [GUIDE] Bitcoin [TUTORIAL] Bitcoin [SERVICE] Bitcoin [PROJECT] Bitcoin [TOKEN] Bitcoin [COIN] Bitcoin [AMA] Bitcoin [NEWS] Bitcoin
Because GUIDE, TUTORIAL, etc. are not in the regex list. Suppose it is on the watchlist: [GUIDE] Bitcoin Wallet Bitcoin Discussion [TUTORIAL] Lightning Network
The current sort order will be: Bitcoin Discussion [GUIDE] Bitcoin Wallet [TUTORIAL] Lightning Network
But you actually want: Bitcoin Discussion Bitcoin Wallet Lightning Network
GUIDE or TUTORIAL will not affect sort.
|
|
|
|
|
satscraper
Legendary

Activity: 1540
Merit: 2856
|
 |
June 11, 2026, 11:08:52 AM |
|
~
Nice work, brother, and congratulations
Thanks! ~
Currently, your script has this regex to remove the noise prefix:
Yeah, I know this. I intend to add other prefixes when needed by editing NOISE_PREFIX_RE. It takes only a few seconds to add additional noise words to the relevant script handled by my Tampermonkey extension.
|
| EARNBET | | | ⚽ 🏀 🏈 🏓 🎯 🥊 |
| ⚾ 🎾 ⛳ 🏐 🏏 🏎️ | | |
███████▄▄███████████ ████▄██████████████████ ██▄▀▀███████████████▀▀███ █▄████████████████████████ ▄▄████████▀▀▀▀▀████████▄▄██ ███████████████████████████ █████████▌████▀████████████ ███████████████████████████ ▀▀███████▄▄▄▄▄█████████▀▀██ █▀█████████████████████▀██ ██▀▄▄███████████████▄▄███ ████▀██████████████████ ███████▀▀███████████ | ....HIGHEST.... VIP REWARDS ✔ G U A R A N T E E D
| | | 🜲 | KING OF THE CASTLE $200K in prizes | | | ..PLAY NOW.. |
|
|
|
|