// ==UserScript==
// @name Reddit Activity Monitor
// @namespace https://justanotherenemy.com
// @description Get notified when specific Reddit activities occur
// @match https://*.reddit.com/*
// @grant GM_notification
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @version 1.0
// ==/UserScript==
// Initial configuration
let CONFIG = GM_getValue('redditMonitorConfig', {
monitorSubreddits: ['insertsubredditname'],
monitorUsers: ['INSERT-USER-NAME'],
monitorThreads: [],
refreshInterval: 60000, // 1 minute in ms
maxNotifications: 5,
enabled: true
});
// Store for previously seen content
let seenContent = GM_getValue('seenContent', {
posts: {},
comments: {}
});
// Add styles for the UI - IMPROVED COLORS FOR BETTER VISIBILITY
GM_addStyle(`
#reddit-monitor-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 350px;
background-color: #1a1a1a;
border: 1px solid #444;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.4);
z-index: 9999;
font-family: Arial, sans-serif;
max-height: 500px;
overflow: hidden;
color: #e0e0e0;
}
#reddit-monitor-panel.collapsed {
width: 200px;
height: 40px;
overflow: hidden;
}
#reddit-monitor-header {
background-color: #ff4500;
color: white;
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
user-select: none;
font-weight: bold;
}
#reddit-monitor-content {
padding: 15px;
max-height: 450px;
overflow-y: auto;
background-color: #2a2a2a;
}
.tab-buttons {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #444;
flex-wrap: wrap;
}
.tab-button {
padding: 8px 15px;
cursor: pointer;
background: #444;
color: #e0e0e0;
border: none;
outline: none;
margin-right: 2px;
margin-bottom: 2px;
border-radius: 4px 4px 0 0;
}
.tab-button:hover {
background: #555;
}
.tab-button.active {
background: #ff4500;
color: white;
font-weight: bold;
}
.tab-content {
display: none;
margin-top: 10px;
}
.tab-content.active {
display: block;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #e0e0e0;
}
.input-group input, .input-group select {
width: 100%;
padding: 8px;
border: 1px solid #555;
border-radius: 4px;
box-sizing: border-box;
margin-bottom: 5px;
background-color: #333;
color: #e0e0e0;
}
.input-group input:focus, .input-group select:focus {
border-color: #ff4500;
outline: none;
}
.monitor-list {
margin-top: 15px;
max-height: 200px;
overflow-y: auto;
border: 1px solid #444;
padding: 5px;
background-color: #333;
border-radius: 4px;
}
.list-item {
display: flex;
justify-content: space-between;
padding: 8px;
border-bottom: 1px solid #444;
align-items: center;
}
.list-item:last-child {
border-bottom: none;
}
.list-item button {
background: none;
border: none;
color: #ff7555;
cursor: pointer;
font-size: 16px;
font-weight: bold;
padding: 0 5px;
}
.list-item button:hover {
color: #ff4500;
}
.save-settings, .add-item-btn {
background-color: #ff4500;
color: white;
padding: 8px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 5px;
display: inline-block;
width: auto;
font-weight: bold;
}
.save-settings:hover, .add-item-btn:hover {
background-color: #ff6a33;
}
.status-toggle {
margin-top: 15px;
display: flex;
align-items: center;
color: #e0e0e0;
}
.status-toggle-switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
margin-left: 10px;
}
.status-toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #555;
transition: .4s;
border-radius: 24px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: #5cb85c;
}
input:checked + .toggle-slider:before {
transform: translateX(26px);
}
.notification-log {
max-height: 200px;
overflow-y: auto;
border: 1px solid #444;
padding: 10px;
margin-top: 10px;
background-color: #333;
border-radius: 4px;
}
.notification-item {
padding: 8px 0;
border-bottom: 1px solid #444;
font-size: 13px;
}
.notification-item:last-child {
border-bottom: none;
}
.notification-item a {
color: #4da3ff;
text-decoration: none;
}
.notification-item a:hover {
text-decoration: underline;
color: #7ab9ff;
}
.notification-item .time {
color: #aaa;
font-size: 11px;
margin-top: 3px;
}
`);
// Create log storage if it doesn't exist
let notificationLog = GM_getValue('notificationLog', []);
// Create the UI panel
function createUI() {
// Remove any existing panel first (for updates/reloads)
const existingPanel = document.getElementById('reddit-monitor-panel');
if (existingPanel) {
existingPanel.remove();
}
const panel = document.createElement('div');
panel.id = 'reddit-monitor-panel';
// Create header
const header = document.createElement('div');
header.id = 'reddit-monitor-header';
header.innerHTML = `<span>Reddit Activity Monitor</span><span id="panel-toggle">−</span>`;
// Create content area as a div (not innerHTML)
const content = document.createElement('div');
content.id = 'reddit-monitor-content';
// Create tabs container
const tabButtons = document.createElement('div');
tabButtons.className = 'tab-buttons';
// Create individual tab buttons
const tabs = ['settings', 'subreddits', 'users', 'threads', 'log'];
tabs.forEach(tab => {
const button = document.createElement('button');
button.className = 'tab-button';
if (tab === 'settings') button.classList.add('active');
button.setAttribute('data-tab', tab);
button.textContent = tab.charAt(0).toUpperCase() + tab.slice(1);
button.addEventListener('click', (e) => {
// Update active button
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
e.target.classList.add('active');
// Update active tab
document.querySelectorAll('.tab-content').forEach(tabContent => tabContent.classList.remove('active'));
document.getElementById(`${tab}-tab`).classList.add('active');
});
tabButtons.appendChild(button);
});
// Add tab buttons to content
content.appendChild(tabButtons);
// Create Settings Tab
const settingsTab = document.createElement('div');
settingsTab.className = 'tab-content active';
settingsTab.id = 'settings-tab';
settingsTab.innerHTML = `
<div class="input-group">
<label for="refresh-interval">Refresh Interval (seconds)</label>
<input type="number" id="refresh-interval" min="30" value="${CONFIG.refreshInterval/1000}">
</div>
<div class="input-group">
<label for="max-notifications">Max Notifications Per Check</label>
<input type="number" id="max-notifications" min="1" max="20" value="${CONFIG.maxNotifications}">
</div>
<div class="status-toggle">
<span>Monitor Status:</span>
<label class="status-toggle-switch">
<input type="checkbox" id="monitor-enabled" ${CONFIG.enabled ? 'checked' : ''}>
<span class="toggle-slider"></span>
</label>
</div>
<button class="save-settings" id="save-settings-btn">Save Settings</button>
`;
// Create Subreddits Tab
const subredditsTab = document.createElement('div');
subredditsTab.className = 'tab-content';
subredditsTab.id = 'subreddits-tab';
subredditsTab.innerHTML = `
<div class="input-group">
<label for="new-subreddit">Add Subreddit (without r/)</label>
<input type="text" id="new-subreddit" placeholder="Enter subreddit name">
<button class="add-item-btn" id="add-subreddit-btn">Add</button>
</div>
<div class="monitor-list" id="subreddit-list">
${generateListHTML(CONFIG.monitorSubreddits, 'subreddit')}
</div>
`;
// Create Users Tab
const usersTab = document.createElement('div');
usersTab.className = 'tab-content';
usersTab.id = 'users-tab';
usersTab.innerHTML = `
<div class="input-group">
<label for="new-user">Add User (without u/)</label>
<input type="text" id="new-user" placeholder="Enter username">
<button class="add-item-btn" id="add-user-btn">Add</button>
</div>
<div class="monitor-list" id="user-list">
${generateListHTML(CONFIG.monitorUsers, 'user')}
</div>
`;
// Create Threads Tab
const threadsTab = document.createElement('div');
threadsTab.className = 'tab-content';
threadsTab.id = 'threads-tab';
threadsTab.innerHTML = `
<div class="input-group">
<label for="new-thread">Add Thread ID or URL</label>
<input type="text" id="new-thread" placeholder="Enter thread ID or Reddit URL">
<button class="add-item-btn" id="add-thread-btn">Add</button>
</div>
<div class="monitor-list" id="thread-list">
${generateListHTML(CONFIG.monitorThreads, 'thread')}
</div>
`;
// Create Log Tab
const logTab = document.createElement('div');
logTab.className = 'tab-content';
logTab.id = 'log-tab';
logTab.innerHTML = `
<h3>Recent Notifications</h3>
<div class="notification-log" id="notification-log">
${generateLogHTML()}
</div>
<button class="save-settings" id="clear-log-btn">Clear Log</button>
`;
// Add tabs to content
content.appendChild(settingsTab);
content.appendChild(subredditsTab);
content.appendChild(usersTab);
content.appendChild(threadsTab);
content.appendChild(logTab);
// Add elements to panel
panel.appendChild(header);
panel.appendChild(content);
// Add panel to page
document.body.appendChild(panel);
// Header click event listener
header.addEventListener('click', function(e) {
// Only toggle if clicking on the header itself or the toggle button
if (e.target === header || e.target.id === 'panel-toggle') {
togglePanel();
}
});
// Settings save button
document.getElementById('save-settings-btn').addEventListener('click', saveSettings);
// Add item buttons
document.getElementById('add-subreddit-btn').addEventListener('click', () => addItem('subreddit'));
document.getElementById('add-user-btn').addEventListener('click', () => addItem('user'));
document.getElementById('add-thread-btn').addEventListener('click', () => addItem('thread'));
// Add event listeners to remove buttons
addRemoveButtonListeners();
// Clear log button
document.getElementById('clear-log-btn').addEventListener('click', clearLog);
// Monitor toggle
document.getElementById('monitor-enabled').addEventListener('change', toggleMonitor);
}
// Add event listeners to all remove buttons
function addRemoveButtonListeners() {
document.querySelectorAll('.remove-item-btn').forEach(button => {
button.addEventListener('click', (e) => {
const type = e.target.getAttribute('data-type');
const value = e.target.getAttribute('data-value');
removeItem(type, value);
});
});
}
// Helper function to generate list HTML
function generateListHTML(items, type) {
if (!items || items.length === 0) {
return '<p>No items added yet.</p>';
}
let html = '';
items.forEach(item => {
html += `
<div class="list-item">
<span>${item}</span>
<button class="remove-item-btn" data-type="${type}" data-value="${item}">×</button>
</div>
`;
});
return html;
}
// Helper function to generate log HTML
function generateLogHTML() {
if (!notificationLog || notificationLog.length === 0) {
return '<p>No notifications yet.</p>';
}
let html = '';
notificationLog.slice(0, 50).forEach(item => {
html += `
<div class="notification-item">
<div><a href="${item.url}" target="_blank">${item.title}</a></div>
<div>${item.text}</div>
<div class="time">${new Date(item.time).toLocaleString()}</div>
</div>
`;
});
return html;
}
// Toggle panel expanded/collapsed
function togglePanel() {
const panel = document.getElementById('reddit-monitor-panel');
const toggle = document.getElementById('panel-toggle');
if (panel.classList.contains('collapsed')) {
panel.classList.remove('collapsed');
toggle.textContent = '−';
} else {
panel.classList.add('collapsed');
toggle.textContent = '+';
}
}
// Save settings
function saveSettings() {
const refreshIntervalInput = document.getElementById('refresh-interval');
const maxNotificationsInput = document.getElementById('max-notifications');
// Validate inputs
if (!refreshIntervalInput.value || isNaN(refreshIntervalInput.value) ||
!maxNotificationsInput.value || isNaN(maxNotificationsInput.value)) {
alert('Please enter valid numbers for the settings.');
return;
}
const refreshInterval = Math.max(30, parseInt(refreshIntervalInput.value)) * 1000;
const maxNotifications = Math.min(20, Math.max(1, parseInt(maxNotificationsInput.value)));
CONFIG.refreshInterval = refreshInterval;
CONFIG.maxNotifications = maxNotifications;
saveConfig();
// Restart the monitor with new settings
if (CONFIG.enabled) {
stopMonitoring();
startMonitoring();
}
alert('Settings saved!');
}
// Add item to a list
function addItem(type) {
const input = document.getElementById(`new-${type}`);
let value = input.value.trim();
if (!value) {
alert(`Please enter a ${type} to add.`);
return;
}
// Process thread URLs to extract ID
if (type === 'thread' && value.includes('reddit.com')) {
const match = value.match(/reddit\.com\/r\/[^/]+\/comments\/([^/]+)/);
if (match && match[1]) {
value = match[1];
}
}
// Determine which config array to update
let targetArray;
switch(type) {
case 'subreddit':
// Remove r/ prefix if present
value = value.replace(/^r\//, '').toLowerCase();
targetArray = CONFIG.monitorSubreddits;
break;
case 'user':
// Remove u/ prefix if present
value = value.replace(/^u\//, '');
targetArray = CONFIG.monitorUsers;
break;
case 'thread':
// Add t3_ prefix if not present
value = value.startsWith('t3_') ? value : `t3_${value}`;
targetArray = CONFIG.monitorThreads;
break;
}
// Check if item already exists
if (targetArray.includes(value)) {
alert(`This ${type} is already being monitored.`);
return;
}
// Add item
targetArray.push(value);
// Update UI
document.getElementById(`${type}-list`).innerHTML = generateListHTML(targetArray, type);
// Re-add event listeners for remove buttons
addRemoveButtonListeners();
// Clear input
input.value = '';
// Save config
saveConfig();
}
// Remove item from a list
function removeItem(type, value) {
let targetArray;
switch(type) {
case 'subreddit':
targetArray = CONFIG.monitorSubreddits;
break;
case 'user':
targetArray = CONFIG.monitorUsers;
break;
case 'thread':
targetArray = CONFIG.monitorThreads;
break;
}
// Find and remove item
const index = targetArray.indexOf(value);
if (index !== -1) {
targetArray.splice(index, 1);
}
// Update UI
document.getElementById(`${type}-list`).innerHTML = generateListHTML(targetArray, type);
// Re-add event listeners for remove buttons
addRemoveButtonListeners();
// Save config
saveConfig();
}
// Clear notification log
function clearLog() {
notificationLog = [];
GM_setValue('notificationLog', notificationLog);
document.getElementById('notification-log').innerHTML = '<p>No notifications yet.</p>';
}
// Toggle monitoring on/off
function toggleMonitor() {
CONFIG.enabled = document.getElementById('monitor-enabled').checked;
saveConfig();
if (CONFIG.enabled) {
startMonitoring();
} else {
stopMonitoring();
}
}
// Save config to GM storage
function saveConfig() {
GM_setValue('redditMonitorConfig', CONFIG);
}
// Monitor timer reference
let monitorTimer = null;
// Start the monitoring process
function startMonitoring() {
if (monitorTimer) {
clearTimeout(monitorTimer);
}
checkForNewContent();
}
// Stop the monitoring process
function stopMonitoring() {
if (monitorTimer) {
clearTimeout(monitorTimer);
monitorTimer = null;
}
}
// Main function to periodically check for new content
function checkForNewContent() {
if (!CONFIG.enabled) return;
// Check subreddits for new posts
CONFIG.monitorSubreddits.forEach(sub => {
fetchSubredditPosts(sub);
});
// Check user activity
CONFIG.monitorUsers.forEach(user => {
fetchUserActivity(user);
});
// Check specific threads for new comments
CONFIG.monitorThreads.forEach(threadId => {
fetchThreadComments(threadId);
});
// Schedule next check
monitorTimer = setTimeout(checkForNewContent, CONFIG.refreshInterval);
}
// Example function to fetch subreddit posts using Reddit JSON API
function fetchSubredditPosts(subreddit) {
GM_xmlhttpRequest({
method: "GET",
url: `https://www.reddit.com/r/${subreddit}/new.json?limit=10`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
const posts = data.data.children;
let newPosts = [];
for(const post of posts) {
const postId = post.data.id;
if(!seenContent.posts[postId]) {
seenContent.posts[postId] = true;
newPosts.push(post.data);
}
}
// Show notifications for new posts (limited by maxNotifications)
newPosts.slice(0, CONFIG.maxNotifications).forEach(post => {
const title = `New post in r/${subreddit}`;
const text = `${post.title.substring(0, 50)}${post.title.length > 50 ? '...' : ''}`;
const url = `https://www.reddit.com${post.permalink}`;
showNotification(title, text, url);
// Add to log
addToLog(title, text, url);
});
// Save updated seen content
GM_setValue('seenContent', seenContent);
// Update log in UI if log tab is open
updateLogUI();
} catch(e) {
console.error("Error processing subreddit posts:", e);
}
}
});
}
// Function to fetch user activity
function fetchUserActivity(username) {
GM_xmlhttpRequest({
method: "GET",
url: `https://www.reddit.com/user/${username}/overview.json?limit=10`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
const activities = data.data.children;
let newActivities = [];
for(const activity of activities) {
const activityId = activity.data.id;
const activityType = activity.data.hasOwnProperty('link_title') ? 'comment' : 'post';
const storageKey = activityType + 's';
if(!seenContent[storageKey]) {
seenContent[storageKey] = {};
}
if(!seenContent[storageKey][activityId]) {
seenContent[storageKey][activityId] = true;
newActivities.push({data: activity.data, type: activityType});
}
}
// Show notifications for new activities
newActivities.slice(0, CONFIG.maxNotifications).forEach(activity => {
const isComment = activity.type === 'comment';
const title = `New ${isComment ? 'comment' : 'post'} by u/${username}`;
const text = isComment ?
`On post: ${activity.data.link_title.substring(0, 40)}...` :
`${activity.data.title.substring(0, 50)}${activity.data.title.length > 50 ? '...' : ''}`;
const url = `https://www.reddit.com${activity.data.permalink}`;
showNotification(title, text, url);
// Add to log
addToLog(title, text, url);
});
// Save updated seen content
GM_setValue('seenContent', seenContent);
// Update log in UI if log tab is open
updateLogUI();
} catch(e) {
console.error("Error processing user activity:", e);
}
}
});
}
// Function to fetch thread comments
function fetchThreadComments(threadId) {
// Extract the actual ID if it starts with t3_
const id = threadId.startsWith('t3_') ? threadId.substring(3) : threadId;
GM_xmlhttpRequest({
method: "GET",
url: `https://www.reddit.com/comments/${id}.json`,
onload: function(response) {
try {
const data = JSON.parse(response.responseText);
// The second element of the array contains the comments
const comments = data[1].data.children;
const newComments = [];
processComments(comments, newComments, threadId, data[0].data.children[0].data.title);
// Save updated seen content
GM_setValue('seenContent', seenContent);
// Update log in UI if log tab is open
updateLogUI();
} catch(e) {
console.error("Error processing thread comments:", e);
}
}
});
}
// Recursive function to process comments including replies
function processComments(comments, newComments, threadId, threadTitle) {
for(const comment of comments) {
// Skip non-comment items like "more" links
if(comment.kind !== 't1') continue;
const commentId = comment.data.id;
if(!seenContent.comments) {
seenContent.comments = {};
}
if(!seenContent.comments[commentId]) {
seenContent.comments[commentId] = true;
newComments.push(comment.data);
// Limit notifications
if(newComments.length <= CONFIG.maxNotifications) {
const title = `New comment in thread`;
const text = `u/${comment.data.author} on "${threadTitle.substring(0, 30)}...": ${comment.data.body.substring(0, 40)}${comment.data.body.length > 40 ? '...' : ''}`;
const url = `https://www.reddit.com/comments/${threadId.replace('t3_', '')}/comment/${commentId}/`;
showNotification(title, text, url);
// Add to log
addToLog(title, text, url);
}
}
// Process replies if any
if(comment.data.replies && comment.data.replies.data && comment.data.replies.data.children) {
processComments(comment.data.replies.data.children, newComments, threadId, threadTitle);
}
}
}
// Function to show a notification
function showNotification(title, text, url) {
GM_notification({
title: title,
text: text,
timeout: 10000,
onclick: function() {
window.open(url, '_blank');
}
});
}
// Add to notification log
function addToLog(title, text, url) {
notificationLog.unshift({
title: title,
text: text,
url: url,
time: Date.now()
});
// Limit log size
if (notificationLog.length > 100) {
notificationLog = notificationLog.slice(0, 100);
}
GM_setValue('notificationLog', notificationLog);
}
// Update log UI if visible
function updateLogUI() {
const logElement = document.getElementById('notification-log');
if (logElement) {
logElement.innerHTML = generateLogHTML();
}
}
// Initialize - wait for page to fully load
window.addEventListener('load', function() {
// Short delay to ensure Reddit's UI is fully loaded
setTimeout(function() {
// Create UI
createUI();
// Start monitoring if enabled
if (CONFIG.enabled) {
startMonitoring();
}
}, 1000);
});