Last active
December 2, 2025 09:38
-
-
Save ALERTua/e555998e9c47d99d2bf470f5c9afb3fc to your computer and use it in GitHub Desktop.
Block users on DOU.ua forums and hide their comments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name DOU.ua Forum User Blocker | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0.20 | |
| // @description Block users on DOU.ua forums and hide their comments | |
| // @author You | |
| // @homepage https://gist.github.com/ALERTua/e555998e9c47d99d2bf470f5c9afb3fc | |
| // @downloadURL https://gist.githubusercontent.com/ALERTua/e555998e9c47d99d2bf470f5c9afb3fc/raw | |
| // @updateURL https://gist.githubusercontent.com/ALERTua/e555998e9c47d99d2bf470f5c9afb3fc/raw | |
| // @match *.dou.ua/forums/topic/* | |
| // @match *.dou.ua/lenta/*/* | |
| // @match *.dou.ua/blogs/* | |
| // @match *.dou.ua/articles/* | |
| // @grant none | |
| // @run-at document-start | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| // Configuration | |
| const STORAGE_KEY = 'douForumBlockedUsers'; | |
| const HIDDEN_TEXT = 'Hidden'; | |
| const DEBUG_MODE = false; // Set to false to disable logging | |
| const PERF_MODE = false; // Set to false to disable logging | |
| // Modern CSS styles | |
| const styles = ` | |
| .dou-user-blocker-btn { | |
| background: linear-gradient(135deg, #bcbcbc 0%, #a8a8a8 100%); | |
| color: #333; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 4px 8px; | |
| font-size: 0.75em; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin-left: 8px; | |
| font-weight: 500; | |
| } | |
| .dou-user-blocker-btn:hover { | |
| transform: translateY(-1px); | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.2); | |
| } | |
| .dou-user-blocker-btn.blocked { | |
| background: linear-gradient(135deg, #a0a0a0 0%, #8c8c8c 100%); | |
| } | |
| .dou-user-blocker-btn.blocked:hover { | |
| background: linear-gradient(135deg, #8c8c8c 0%, #a0a0a0 100%); | |
| } | |
| .dou-user-blocker-management { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| padding: 12px 16px; | |
| cursor: pointer; | |
| z-index: 10000; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 6px 20px rgba(0,0,0,0.3); | |
| font-weight: 600; | |
| font-size: 0.9em; | |
| } | |
| .dou-user-blocker-management:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.4); | |
| } | |
| .dou-user-blocker-management::before { | |
| content: "🚫"; | |
| margin-right: 8px; | |
| } | |
| .dou-user-blocker-panel { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%); | |
| background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); | |
| border-radius: 16px; | |
| padding: 24px; | |
| z-index: 10001; | |
| width: 90%; | |
| max-width: 400px; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.3); | |
| border: 1px solid rgba(255,255,255,0.2); | |
| } | |
| .dou-user-blocker-panel.dark-theme { | |
| background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
| color: white; | |
| } | |
| .dou-user-blocker-title { | |
| margin: 0 0 16px 0; | |
| font-size: 1.4em; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-title { | |
| background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .dou-user-blocker-list { | |
| list-style: none; | |
| padding: 0; | |
| margin: 0; | |
| } | |
| .dou-user-blocker-item { | |
| background: rgba(102, 126, 234, 0.1); | |
| margin-bottom: 12px; | |
| padding: 16px; | |
| border-radius: 8px; | |
| border-left: 4px solid #667eea; | |
| transition: all 0.3s ease; | |
| } | |
| .dou-user-blocker-item:hover { | |
| background: rgba(102, 126, 234, 0.15); | |
| transform: translateX(4px); | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-item { | |
| background: rgba(116, 185, 255, 0.1); | |
| border-left-color: #74b9ff; | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-item:hover { | |
| background: rgba(116, 185, 255, 0.15); | |
| } | |
| .dou-user-blocker-nickname { | |
| font-weight: 600; | |
| font-size: 1.1em; | |
| margin-bottom: 4px; | |
| color: #2c3e50; | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-nickname { | |
| color: #ecf0f1; | |
| } | |
| .dou-user-blocker-profile { | |
| font-size: 0.85em; | |
| color: #7f8c8d; | |
| margin-bottom: 8px; | |
| word-break: break-all; | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-profile { | |
| color: #bdc3c7; | |
| } | |
| .dou-user-blocker-unblock { | |
| background: linear-gradient(135deg, #c0c0c0 0%, #a0a0a0 100%); | |
| color: #333; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 6px 12px; | |
| font-size: 0.85em; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| float: right; | |
| } | |
| .dou-user-blocker-unblock:hover { | |
| background: linear-gradient(135deg, #a0a0a0 0%, #c0c0c0 100%); | |
| transform: translateY(-1px); | |
| } | |
| .dou-user-blocker-empty { | |
| text-align: center; | |
| color: #7f8c8d; | |
| font-style: italic; | |
| padding: 20px; | |
| } | |
| .dou-user-blocker-panel.dark-theme .dou-user-blocker-empty { | |
| color: #bdc3c7; | |
| } | |
| .dou-user-blocker-close { | |
| background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); | |
| color: white; | |
| border: none; | |
| border-radius: 6px; | |
| padding: 8px 16px; | |
| font-size: 0.9em; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin-top: 16px; | |
| width: 100%; | |
| } | |
| .dou-user-blocker-close:hover { | |
| background: linear-gradient(135deg, #7f8c8d 0%, #95a5a6 100%); | |
| } | |
| .dou-user-blocker-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| z-index: 10000; | |
| backdrop-filter: blur(4px); | |
| } | |
| .dou-user-blocker-spoiler { | |
| border-collapse: collapse; | |
| list-style: none; | |
| font: .625em Roboto,Arial,sans-serif; | |
| font-size: 13px; | |
| margin: 0; | |
| padding: 0; | |
| border: 0; | |
| vertical-align: top; | |
| cursor: pointer; | |
| outline: 0; | |
| border-bottom: 1px dashed #606983; | |
| text-decoration: none; | |
| margin-right: 4px; | |
| color: #9e9e9e; | |
| border-color: #9e9e9e; | |
| } | |
| .dou-user-blocker-spoiler:hover { | |
| border-color: #7f8c8d; | |
| color: #7f8c8d; | |
| } | |
| .dou-user-blocker-hidden { | |
| background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%); | |
| color: #ffffff; | |
| padding: 8px 12px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| font-style: italic; | |
| border: 2px dashed rgba(255, 255, 255, 0.3); | |
| } | |
| .dou-user-blocker-hidden:hover { | |
| background: linear-gradient(135deg, #7f8c8d 0%, #95a5a6 100%); | |
| transform: scale(1.02); | |
| } | |
| @media (prefers-color-scheme: dark) { | |
| .dou-user-blocker-panel { | |
| background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%); | |
| color: white; | |
| } | |
| .dou-user-blocker-title { | |
| background: linear-gradient(135deg, #74b9ff 0%, #0984e3 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .dou-user-blocker-nickname { | |
| color: #ecf0f1; | |
| } | |
| .dou-user-blocker-profile { | |
| color: #bdc3c7; | |
| } | |
| } | |
| `; | |
| // Logging utility | |
| function log(message, data = null) { | |
| if (DEBUG_MODE) { | |
| const timestamp = new Date().toISOString(); | |
| if (data) { | |
| console.log(`[DOU-Blocker:${timestamp}] ${message}`, data); | |
| } else { | |
| console.log(`[DOU-Blocker:${timestamp}] ${message}`); | |
| } | |
| } | |
| } | |
| function logError(message, error = null) { | |
| if (DEBUG_MODE) { | |
| const timestamp = new Date().toISOString(); | |
| console.error(`[DOU-Blocker:${timestamp}] ERROR: ${message}`, error); | |
| } | |
| } | |
| function logPerformance(label, startTime) { | |
| if (DEBUG_MODE && PERF_MODE) { | |
| const duration = performance.now() - startTime; | |
| console.log(`[DOU-Blocker:PERF] ${label} took ${duration.toFixed(2)}ms`); | |
| } | |
| } | |
| // Initialize blocked users list from browser storage | |
| let blockedUsers = []; | |
| try { | |
| const stored = localStorage.getItem(STORAGE_KEY); | |
| blockedUsers = stored ? JSON.parse(stored) : []; | |
| log(`Loaded ${blockedUsers.length} blocked users from storage`, blockedUsers); | |
| } catch (error) { | |
| logError('Failed to load blocked users from storage', error); | |
| blockedUsers = []; | |
| } | |
| // Add styles to page | |
| function addStyles() { | |
| const startTime = performance.now(); | |
| try { | |
| if (document.getElementById('dou-user-blocker-styles')) { | |
| log('Styles already exist, skipping'); | |
| return; | |
| } | |
| const styleElement = document.createElement('style'); | |
| styleElement.id = 'dou-user-blocker-styles'; | |
| styleElement.textContent = styles; | |
| if (!document.head) { | |
| logError('Document head not available for style injection'); | |
| return; | |
| } | |
| document.head.appendChild(styleElement); | |
| logPerformance('addStyles', startTime); | |
| log('Styles added successfully'); | |
| } catch (error) { | |
| logError('Failed to add styles', error); | |
| logPerformance('addStyles (failed)', startTime); | |
| } | |
| } | |
| // Save blocked users to browser storage | |
| function saveBlockedUsers() { | |
| const startTime = performance.now(); | |
| try { | |
| const beforeCount = blockedUsers.length; | |
| const uniqueUsers = blockedUsers.filter((user, index, self) => | |
| index === self.findIndex(u => u.profileUrl === user.profileUrl) | |
| ); | |
| const afterCount = uniqueUsers.length; | |
| const duplicatesRemoved = beforeCount - afterCount; | |
| try { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(uniqueUsers)); | |
| blockedUsers = uniqueUsers; | |
| log(`Saved ${afterCount} blocked users to storage (${duplicatesRemoved} duplicates removed)`); | |
| logPerformance('saveBlockedUsers', startTime); | |
| } catch (storageError) { | |
| logError('Failed to save to localStorage (quota exceeded?)', storageError); | |
| logPerformance('saveBlockedUsers (failed)', startTime); | |
| } | |
| } catch (error) { | |
| logError('Error in saveBlockedUsers', error); | |
| logPerformance('saveBlockedUsers (error)', startTime); | |
| } | |
| } | |
| // Extract user info from profile URL | |
| function getUserInfo(profileUrl) { | |
| if (!profileUrl) return null; | |
| const cleanUrl = profileUrl.split('?')[0].trim(); | |
| const match = cleanUrl.match(/dou\.ua\/users\/([^\/]+)\//); | |
| if (match) { | |
| return { | |
| username: match[1], | |
| profileUrl: cleanUrl | |
| }; | |
| } | |
| return null; | |
| } | |
| // Restore all comments for a specific user (remove spoilers) | |
| function restoreUserComments(profileUrl) { | |
| const startTime = performance.now(); | |
| let restoredCount = 0; | |
| try { | |
| log(`Restoring comments for user: ${profileUrl}`); | |
| // Find all comments that have spoilers and belong to this user | |
| const spoiledComments = document.querySelectorAll('.dou-user-blocker-has-spoiler'); | |
| log(`Found ${spoiledComments.length} spoiled comments to check`); | |
| spoiledComments.forEach(commentElement => { | |
| try { | |
| // Check if this comment belongs to the user we're restoring | |
| const userLinks = commentElement.closest('[class*="comment"], tr, div')?.querySelectorAll('a[href*="/users/"]'); | |
| if (userLinks) { | |
| for (const link of userLinks) { | |
| const userInfo = getUserInfo(link.href); | |
| if (userInfo && userInfo.profileUrl === profileUrl) { | |
| // This is a comment by the user we're unblocking - remove spoiler | |
| const contentDiv = commentElement.querySelector('.dou-user-blocker-content'); | |
| const spoilerBtn = commentElement.querySelector('.dou-user-blocker-spoiler'); | |
| if (contentDiv && spoilerBtn) { | |
| commentElement.innerHTML = contentDiv.innerHTML; | |
| commentElement.classList.remove('dou-user-blocker-has-spoiler'); | |
| restoredCount++; | |
| log(`Removed spoiler from comment by ${userInfo.username}`); | |
| } | |
| break; // Found the user, no need to check more links | |
| } | |
| } | |
| } | |
| } catch (commentError) { | |
| logError('Error processing spoiled comment', commentError); | |
| } | |
| }); | |
| log(`restoreUserComments: restored ${restoredCount} comments for ${profileUrl}`); | |
| logPerformance('restoreUserComments', startTime); | |
| } catch (error) { | |
| logError('Error in restoreUserComments', error); | |
| logPerformance('restoreUserComments (error)', startTime); | |
| } | |
| return restoredCount; | |
| } | |
| // Update all block buttons for a specific user | |
| function updateUserButtons(profileUrl, isBlocked) { | |
| const startTime = performance.now(); | |
| let updatedCount = 0; | |
| try { | |
| // Find all block buttons for this user | |
| document.querySelectorAll('.dou-user-blocker-btn').forEach(button => { | |
| try { | |
| // Find the associated user info by looking at nearby user links | |
| const postAuthorDiv = button.closest('.b-post-author'); | |
| if (postAuthorDiv) { | |
| const userLink = postAuthorDiv.querySelector('a[href*="/users/"].avatar'); | |
| if (userLink) { | |
| const userInfo = getUserInfo(userLink.href); | |
| if (userInfo && userInfo.profileUrl === profileUrl) { | |
| // Update this button | |
| button.textContent = isBlocked ? 'Unblock' : 'Block'; | |
| button.className = `dou-user-blocker-btn ${isBlocked ? 'blocked' : ''}`; | |
| button.title = isBlocked ? 'Unblock user' : 'Block user'; | |
| updatedCount++; | |
| } | |
| } | |
| } | |
| } catch (buttonError) { | |
| logError('Error updating individual button', buttonError); | |
| } | |
| }); | |
| log(`updateUserButtons: updated ${updatedCount} buttons for ${profileUrl} (blocked: ${isBlocked})`); | |
| logPerformance('updateUserButtons', startTime); | |
| } catch (error) { | |
| logError('Error in updateUserButtons', error); | |
| logPerformance('updateUserButtons (error)', startTime); | |
| } | |
| return updatedCount; | |
| } | |
| // Hide comments from blocked users | |
| function hideBlockedComments() { | |
| const startTime = performance.now(); | |
| let processedCount = 0; | |
| let hiddenCount = 0; | |
| let skippedAlreadyHidden = 0; | |
| try { | |
| // Find all comment containers directly | |
| const commentContainers = document.querySelectorAll('.comment'); | |
| log(`Found ${commentContainers.length} comment containers to process`); | |
| commentContainers.forEach((commentContainer, index) => { | |
| try { | |
| // Extract user info from the user link within this comment | |
| const userLink = commentContainer.querySelector('a[href*="/users/"].avatar'); | |
| if (!userLink) { | |
| log(`Comment ${index} has no user link, skipping`); | |
| return; | |
| } | |
| const userInfo = getUserInfo(userLink.href); | |
| if (!userInfo) { | |
| log(`Comment ${index} has invalid user link: ${userLink.href}`); | |
| return; | |
| } | |
| processedCount++; | |
| const isBlocked = blockedUsers.some(user => user.profileUrl === userInfo.profileUrl); | |
| // Hide the comment content - prioritize finding the text container with paragraph content | |
| let commentText = commentContainer.querySelector('.comment_text'); | |
| if (!commentText) { | |
| commentText = commentContainer.querySelector('.comment-text, .text, p'); | |
| } | |
| if (!commentText) { | |
| commentText = commentContainer.querySelector('[class*="comment"], [class*="text"] p'); | |
| } | |
| // If we found a p element, use its parent instead to wrap the whole text block | |
| if (commentText && commentText.tagName === 'P' && commentText.parentElement) { | |
| commentText = commentText.parentElement; | |
| } | |
| log(`Processing comment ${index} by ${userInfo.username}${isBlocked ? ' (blocked)' : ' (not blocked)'}`); | |
| log(`Found comment text element: ${commentText?.tagName} with classes: ${commentText?.className}`); | |
| if (commentText) { | |
| log(`Comment text has content: ${commentText.textContent?.substring(0, 50)}...`); | |
| } else { | |
| log('No comment text element found, checking container structure:'); | |
| log(`Container classes: ${commentContainer.className}`); | |
| const paragraphs = commentContainer.querySelectorAll('p'); | |
| log(`Found ${paragraphs.length} paragraph elements in container`); | |
| } | |
| if (isBlocked) { | |
| hiddenCount++; | |
| if (commentText && !commentText.classList.contains('dou-user-blocker-has-spoiler')) { | |
| // Wrap the content in a spoiler | |
| const originalContent = commentText.innerHTML; | |
| commentText.innerHTML = ` | |
| <span class="dou-user-blocker-spoiler">${HIDDEN_TEXT}</span> | |
| <span class="dou-user-blocker-content" style="display: none;">${originalContent}</span> | |
| `; | |
| commentText.classList.add('dou-user-blocker-has-spoiler'); | |
| // Add click handler to reveal content temporarily | |
| const spoilerBtn = commentText.querySelector('.dou-user-blocker-spoiler'); | |
| const contentDiv = commentText.querySelector('.dou-user-blocker-content'); | |
| let autoHideTimer = null; | |
| spoilerBtn.addEventListener('click', function() { | |
| if (contentDiv.style.display === 'none') { | |
| // Reveal content | |
| contentDiv.style.display = 'inline'; | |
| this.style.display = 'none'; // Hide the spoiler button | |
| // Auto-hide after 5 seconds | |
| autoHideTimer = setTimeout(() => { | |
| if (contentDiv.style.display !== 'none') { | |
| contentDiv.style.display = 'none'; | |
| this.style.display = 'inline'; // Show spoiler again | |
| this.textContent = HIDDEN_TEXT; | |
| autoHideTimer = null; | |
| } | |
| }, 5000); | |
| } | |
| }); | |
| log(`Added spoiler to comment by blocked user ${userInfo.username}`); | |
| } else if (commentText && commentText.classList.contains('dou-user-blocker-has-spoiler')) { | |
| skippedAlreadyHidden++; | |
| } | |
| } else { | |
| // Make sure unblocked users' comments are shown | |
| if (commentText && commentText.classList.contains('dou-user-blocker-has-spoiler')) { | |
| const contentDiv = commentText.querySelector('.dou-user-blocker-content'); | |
| const spoilerBtn = commentText.querySelector('.dou-user-blocker-spoiler'); | |
| if (contentDiv && spoilerBtn) { | |
| commentText.innerHTML = contentDiv.innerHTML; | |
| commentText.classList.remove('dou-user-blocker-has-spoiler'); | |
| log(`Removed spoiler from comment by unblocked user ${userInfo.username}`); | |
| } | |
| } | |
| } | |
| } catch (commentError) { | |
| logError(`Error processing comment ${index}`, commentError); | |
| } | |
| }); | |
| log(`hideBlockedComments: processed ${processedCount} comments, hidden ${hiddenCount}, skipped ${skippedAlreadyHidden} already hidden`); | |
| logPerformance('hideBlockedComments', startTime); | |
| } catch (error) { | |
| logError('Error in hideBlockedComments', error); | |
| logPerformance('hideBlockedComments (error)', startTime); | |
| } | |
| } | |
| // Add block/unblock buttons to user profiles in comments | |
| function addBlockButtons() { | |
| const startTime = performance.now(); | |
| let processedCount = 0; | |
| let addedCount = 0; | |
| try { | |
| // Process only .b-post-author elements within .comment containers | |
| const postAuthorElements = document.querySelectorAll('.comment .b-post-author'); | |
| log(`Found ${postAuthorElements.length} post author elements in comments`); | |
| postAuthorElements.forEach((postAuthorDiv, index) => { | |
| try { | |
| // Skip if button already exists in this post author div | |
| if (postAuthorDiv.querySelector('.dou-user-blocker-btn')) { | |
| return; | |
| } | |
| // Find the user avatar link | |
| const avatarLink = postAuthorDiv.querySelector('a[href*="/users/"].avatar'); | |
| if (!avatarLink || avatarLink.closest('.sup-users')) { | |
| return; | |
| } | |
| const userInfo = getUserInfo(avatarLink.href); | |
| if (!userInfo) return; | |
| processedCount++; | |
| const blockBtn = document.createElement('button'); | |
| // Update button state dynamically based on current blocked status | |
| function updateButtonState() { | |
| const currentlyBlocked = blockedUsers.some(user => user.profileUrl === userInfo.profileUrl); | |
| blockBtn.className = `dou-user-blocker-btn ${currentlyBlocked ? 'blocked' : ''}`; | |
| blockBtn.textContent = currentlyBlocked ? 'Unblock' : 'Block'; | |
| blockBtn.title = currentlyBlocked ? 'Unblock user' : 'Block user'; | |
| } | |
| updateButtonState(); | |
| blockBtn.addEventListener('click', function(e) { | |
| const clickTime = performance.now(); | |
| try { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const currentlyBlocked = blockedUsers.some(user => user.profileUrl === userInfo.profileUrl); | |
| log(`Block button clicked for user: ${userInfo.username} (currently ${currentlyBlocked ? 'blocked' : 'unblocked'})`); | |
| if (currentlyBlocked) { | |
| // Remove from blocked list | |
| blockedUsers = blockedUsers.filter(user => user.profileUrl !== userInfo.profileUrl); | |
| log(`Unblocked user: ${userInfo.username}`); | |
| // Restore all comments for this user | |
| const restoredComments = restoreUserComments(userInfo.profileUrl); | |
| log(`Restored ${restoredComments} comments for user: ${userInfo.username}`); | |
| // Ensure any remaining hidden comments are properly handled | |
| hideBlockedComments(); | |
| } else { | |
| // Get full name from the avatar link text | |
| const avatarLink = postAuthorDiv.querySelector('a[href*="/users/"].avatar'); | |
| const fullName = avatarLink ? avatarLink.textContent.trim() : userInfo.username; | |
| // Add to blocked list | |
| blockedUsers.push({ | |
| username: userInfo.username, | |
| fullName: fullName, | |
| profileUrl: userInfo.profileUrl, | |
| blockedAt: new Date().toISOString() | |
| }); | |
| log(`Blocked user: ${fullName} (${userInfo.username})`); | |
| } | |
| saveBlockedUsers(); | |
| hideBlockedComments(); | |
| updateUserButtons(userInfo.profileUrl, !currentlyBlocked); | |
| updateManagementButton(); | |
| updateButtonState(); // Update this button's state | |
| logPerformance('block button click handler', clickTime); | |
| } catch (clickError) { | |
| logError('Error in block button click handler', clickError); | |
| logPerformance('block button click handler (error)', clickTime); | |
| } | |
| }); | |
| // Insert before the comment-link (timestamp) | |
| const commentLink = postAuthorDiv.querySelector('.comment-link'); | |
| if (commentLink) { | |
| commentLink.insertAdjacentHTML('beforebegin', ' '); | |
| commentLink.parentNode.insertBefore(blockBtn, commentLink); | |
| addedCount++; | |
| //log(`Added button before comment-link for ${userInfo.username}`); | |
| } | |
| } catch (error) { | |
| logError(`Error processing post author element ${index}`, error); | |
| } | |
| }); | |
| log(`addBlockButtons: processed ${processedCount} post authors, added ${addedCount} buttons`); | |
| logPerformance('addBlockButtons', startTime); | |
| } catch (error) { | |
| logError('Error in addBlockButtons', error); | |
| logPerformance('addBlockButtons (error)', startTime); | |
| } | |
| } | |
| // Update management button count | |
| function updateManagementButton() { | |
| const startTime = performance.now(); | |
| try { | |
| const existingBtn = document.getElementById('dou-user-blocker-management'); | |
| if (existingBtn) { | |
| const oldText = existingBtn.textContent; | |
| existingBtn.textContent = `Blocked: ${blockedUsers.length}`; | |
| log(`Updated management button: "${oldText}" → "${existingBtn.textContent}"`); | |
| } else { | |
| log('Management button not found for update'); | |
| } | |
| logPerformance('updateManagementButton', startTime); | |
| } catch (error) { | |
| logError('Error in updateManagementButton', error); | |
| logPerformance('updateManagementButton (error)', startTime); | |
| } | |
| } | |
| // Create management panel | |
| function createManagementPanel() { | |
| // Remove existing panel if open | |
| const existingPanel = document.getElementById('dou-user-blocker-panel'); | |
| if (existingPanel) { | |
| existingPanel.remove(); | |
| } | |
| const overlay = document.createElement('div'); | |
| overlay.className = 'dou-user-blocker-overlay'; | |
| overlay.addEventListener('click', function() { | |
| document.body.removeChild(overlay); | |
| const panel = document.getElementById('dou-user-blocker-panel'); | |
| if (panel) panel.remove(); | |
| }); | |
| const panel = document.createElement('div'); | |
| panel.className = 'dou-user-blocker-panel'; | |
| panel.id = 'dou-user-blocker-panel'; | |
| // Detect dark theme preference | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| panel.classList.add('dark-theme'); | |
| } | |
| const title = document.createElement('h3'); | |
| title.className = 'dou-user-blocker-title'; | |
| title.textContent = 'Blocked Users'; | |
| panel.appendChild(title); | |
| if (blockedUsers.length === 0) { | |
| const emptyMsg = document.createElement('div'); | |
| emptyMsg.className = 'dou-user-blocker-empty'; | |
| emptyMsg.textContent = 'No blocked users'; | |
| panel.appendChild(emptyMsg); | |
| } else { | |
| const list = document.createElement('ul'); | |
| list.className = 'dou-user-blocker-list'; | |
| blockedUsers.forEach(user => { | |
| const item = document.createElement('li'); | |
| item.className = 'dou-user-blocker-item'; | |
| const nickname = document.createElement('div'); | |
| nickname.className = 'dou-user-blocker-nickname'; | |
| nickname.textContent = user.fullName || user.username; | |
| const profile = document.createElement('div'); | |
| profile.className = 'dou-user-blocker-profile'; | |
| profile.textContent = user.profileUrl; | |
| const unblockBtn = document.createElement('button'); | |
| unblockBtn.className = 'dou-user-blocker-unblock'; | |
| unblockBtn.textContent = 'Unblock'; | |
| unblockBtn.addEventListener('click', function() { | |
| const unblockTime = performance.now(); | |
| try { | |
| log(`Management panel: unblocking user ${user.username}`); | |
| // Remove from blocked list | |
| blockedUsers = blockedUsers.filter(u => u.profileUrl !== user.profileUrl); | |
| // Restore all comments for this user | |
| const restoredComments = restoreUserComments(user.profileUrl); | |
| log(`Management panel: restored ${restoredComments} comments for user: ${user.username}`); | |
| // Update all buttons for this user | |
| const updatedButtons = updateUserButtons(user.profileUrl, false); | |
| log(`Management panel: updated ${updatedButtons} buttons for user: ${user.username}`); | |
| saveBlockedUsers(); | |
| hideBlockedComments(); | |
| // Close panel and refresh | |
| document.body.removeChild(overlay); | |
| document.body.removeChild(panel); | |
| createManagementPanel(); // Refresh the panel | |
| logPerformance('management panel unblock handler', unblockTime); | |
| } catch (unblockError) { | |
| logError('Error in management panel unblock handler', unblockError); | |
| logPerformance('management panel unblock handler (error)', unblockTime); | |
| } | |
| }); | |
| item.appendChild(nickname); | |
| item.appendChild(profile); | |
| item.appendChild(unblockBtn); | |
| list.appendChild(item); | |
| }); | |
| panel.appendChild(list); | |
| } | |
| const closeBtn = document.createElement('button'); | |
| closeBtn.className = 'dou-user-blocker-close'; | |
| closeBtn.textContent = 'Close'; | |
| closeBtn.addEventListener('click', function() { | |
| document.body.removeChild(overlay); | |
| document.body.removeChild(panel); | |
| }); | |
| panel.appendChild(closeBtn); | |
| document.body.appendChild(overlay); | |
| document.body.appendChild(panel); | |
| } | |
| // Add management button | |
| function addManagementButton() { | |
| const existingBtn = document.getElementById('dou-user-blocker-management'); | |
| if (existingBtn) existingBtn.remove(); | |
| const managementBtn = document.createElement('button'); | |
| managementBtn.className = 'dou-user-blocker-management'; | |
| managementBtn.id = 'dou-user-blocker-management'; | |
| managementBtn.textContent = `Blocked: ${blockedUsers.length}`; | |
| managementBtn.addEventListener('click', createManagementPanel); | |
| document.body.appendChild(managementBtn); | |
| } | |
| // Initialize the script | |
| function init() { | |
| const startTime = performance.now(); | |
| log('Initializing DOU.ua User Blocker script'); | |
| try { | |
| addStyles(); | |
| log('Styles initialization completed'); | |
| hideBlockedComments(); | |
| log('Initial comment hiding completed'); | |
| addBlockButtons(); | |
| log('Initial button addition completed'); | |
| addManagementButton(); | |
| log('Management button addition completed'); | |
| // Set up observer for dynamically loaded content with debouncing | |
| let observerTimeout; | |
| const observer = new MutationObserver(function(mutations) { | |
| // Clear any pending observer callback | |
| if (observerTimeout) { | |
| clearTimeout(observerTimeout); | |
| } | |
| // Debounce the observer callback to prevent loops | |
| observerTimeout = setTimeout(() => { | |
| try { | |
| const addedNodes = mutations.reduce((count, mutation) => { | |
| return count + mutation.addedNodes.length; | |
| }, 0); | |
| if (addedNodes > 0) { | |
| log(`Observer triggered: ${mutations.length} mutations, ${addedNodes} new nodes`); | |
| // Check if any new nodes contain user links (comments/post authors) before processing | |
| let hasUserContent = false; | |
| mutations.forEach(mutation => { | |
| mutation.addedNodes.forEach(node => { | |
| if (node.nodeType === Node.ELEMENT_NODE) { | |
| const userLinks = node.querySelectorAll ? node.querySelectorAll('a[href*="/users/"]') : []; | |
| if (userLinks.length > 0 || node.matches && node.matches('a[href*="/users/"]')) { | |
| hasUserContent = true; | |
| } | |
| } | |
| }); | |
| }); | |
| // Only process if we detected new user content | |
| if (hasUserContent) { | |
| log('Detected new user content, processing comments and buttons'); | |
| hideBlockedComments(); | |
| addBlockButtons(); | |
| } else { | |
| log('No new user content detected, skipping processing'); | |
| } | |
| } | |
| } catch (observerError) { | |
| logError('Error in observer callback', observerError); | |
| } | |
| }, 100); // 100ms debounce | |
| }); | |
| // Observe the entire document body comprehensively to catch all dynamic content | |
| // This ensures expand-thread and other AJAX-loaded content is detected | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| log('Observing entire document body for all dynamic content changes'); | |
| log('Observer setup completed, watching entire document body for all dynamic content changes'); | |
| logPerformance('init', startTime); | |
| log('DOU.ua User Blocker initialization completed successfully'); | |
| } catch (error) { | |
| logError('Error during script initialization', error); | |
| logPerformance('init (error)', startTime); | |
| } | |
| } | |
| // Start the script when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment