Untitled
unknown
plain_text
6 months ago
10 kB
111
No Index
// ==UserScript==
// @name Custom Reddit notifications UI2
// @namespace https://reddit.com/
// @version Release 2
// @description Custom Reddit notifications UI with dark/light mode toggle and full functions
// @author RaidPrincess
// @match *://*.reddit.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
const baseUrl = 'https://www.reddit.com';
let open = false;
let visibleItems = [];
let lightMode = false;
let toggleBtn;
const container = document.createElement('div');
container.className = 'rnqv-container';
container.style.display = 'none';
document.body.appendChild(container);
const style = document.createElement('style');
style.textContent = `
.rnqv-container {
position: fixed;
top: 55px;
right: 10px;
width: 360px;
max-height: 420px;
background: #1a1a1b;
color: #d7dadc;
border: 1px solid #343536;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.4);
z-index: 9999;
padding: 12px;
overflow-y: auto;
font-size: 14px;
}
.rnqv-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.rnqv-actions {
display: flex;
gap: 8px;
}
.rnqv-btn {
background-color: transparent;
border: 1px solid #343536;
border-radius: 4px;
padding: 4px 10px;
color: inherit;
cursor: pointer;
font-size: 13px;
}
.rnqv-btn:hover {
background-color: #272729;
}
.rnqv-container ul {
list-style: none;
margin: 0;
padding: 0;
}
.rnqv-container li {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.rnqv-container a {
color: #ffffff;
text-decoration: none;
flex: 1;
}
.rnqv-container a:hover {
text-decoration: underline;
}
.rnqv-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
flex-shrink: 0;
}
.rnqv-dismiss {
cursor: pointer;
font-size: 16px;
color: #888;
}
.rnqv-dismiss:hover {
color: red;
}
.rnqv-fake-ping {
position: absolute;
top: 6px;
right: 8px;
width: 12px;
height: 12px;
background: #ff2200;
border-radius: 50%;
z-index: 10;
color: white;
font-size: 9px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
[data-testid="unread-indicator"],
#notifications-inbox-button span[class*="icon"] > span {
opacity: 0 !important;
pointer-events: none !important;
}
`;
document.head.appendChild(style);
function toggleLightMode() {
lightMode = !lightMode;
container.style.background = lightMode ? '#ffffff' : '#1a1a1b';
container.style.color = lightMode ? '#000000' : '#d7dadc';
container.querySelectorAll('a').forEach(a => a.style.color = lightMode ? '#000000' : '#ffffff');
if (toggleBtn) toggleBtn.textContent = lightMode ? '☾' : '☀︎';
}
function replaceInboxButton() {
const btn = document.querySelector('#notifications-inbox-button');
if (!btn || btn.dataset.rnqv === '1') return;
const clone = btn.cloneNode(true);
clone.id = 'notifications-inbox-button';
clone.style.position = 'relative';
clone.dataset.rnqv = '1';
clone.addEventListener('click', (e) => {
e.preventDefault();
e.stopImmediatePropagation();
e.stopPropagation();
open = !open;
container.style.display = open ? 'block' : 'none';
if (open) {
loadNotifications();
setSeenCookie();
}
}, true);
btn.replaceWith(clone);
refreshNotificationCount();
}
function updateFakePing(count) {
const btn = document.querySelector('#notifications-inbox-button');
if (!btn) return;
let dot = btn.querySelector('.rnqv-fake-ping');
if (count > 0) {
if (!dot) {
dot = document.createElement('span');
dot.className = 'rnqv-fake-ping';
btn.appendChild(dot);
}
dot.textContent = count;
} else if (dot) {
dot.remove();
}
}
function setSeenCookie() {
document.cookie = `rnqv_seen=true; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
}
function getHiddenMessageIDs() {
const match = document.cookie.match(/(?:^|; )rnqv_hidden=([^;]*)/);
return match ? decodeURIComponent(match[1]).split(',') : [];
}
function setHiddenMessageIDs(ids) {
const encoded = encodeURIComponent([...new Set(ids)].join(','));
document.cookie = `rnqv_hidden=${encoded}; path=/; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
}
async function markMessageAsRead(id) {
try {
await fetch(`${baseUrl}/api/read_message`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'id=' + encodeURIComponent(id)
});
} catch {}
}
async function refreshNotificationCount() {
const hiddenIDs = getHiddenMessageIDs();
try {
const res = await fetch(`${baseUrl}/message/unread.json`, { credentials: 'include' });
const items = (await res.json())?.data?.children || [];
visibleItems = items.filter(i => !hiddenIDs.includes(i.data.name));
updateFakePing(visibleItems.length);
} catch {}
}
async function loadNotifications() {
container.innerHTML = '';
const header = document.createElement('div');
header.className = 'rnqv-header';
const title = document.createElement('strong');
title.textContent = 'Notifications';
header.appendChild(title);
const fullBtn = document.createElement('button');
fullBtn.textContent = '🌐︎';
fullBtn.className = 'rnqv-btn';
fullBtn.onclick = () => window.open('https://www.reddit.com/notifications', '_blank');
toggleBtn = document.createElement('button');
toggleBtn.textContent = lightMode ? '☾' : '☀︎';
toggleBtn.className = 'rnqv-btn';
toggleBtn.onclick = toggleLightMode;
const actions = document.createElement('div');
actions.className = 'rnqv-actions';
actions.appendChild(fullBtn);
actions.appendChild(toggleBtn);
header.appendChild(actions);
container.appendChild(header);
const list = document.createElement('ul');
container.appendChild(list);
const hiddenIDs = getHiddenMessageIDs();
try {
const res = await fetch(`${baseUrl}/message/unread.json`, { credentials: 'include' });
const items = (await res.json())?.data?.children?.slice(0, 10) || [];
visibleItems = items.filter(i => !hiddenIDs.includes(i.data.name));
updateFakePing(visibleItems.length);
if (!visibleItems.length) {
const li = document.createElement('li');
li.textContent = 'No new notifications.';
list.appendChild(li);
return;
}
for (const item of visibleItems) {
const d = item.data;
const li = document.createElement('li');
const avatar = document.createElement('img');
avatar.className = 'rnqv-avatar';
avatar.src = 'https://www.redditstatic.com/avatars/avatar_default_02_24A0ED.png';
li.appendChild(avatar);
const a = document.createElement('a');
a.textContent = d.was_comment
? `u/${d.author}: ${(d.body || '').replace(/\s+/g, ' ').trim().slice(0, 80)}`
: `${d.subject || '(no subject)'} (from u/${d.author})`;
a.href = d.context ? baseUrl + d.context : baseUrl + '/message/messages/' + d.id;
a.target = '_blank';
const dismiss = document.createElement('span');
dismiss.className = 'rnqv-dismiss';
dismiss.textContent = '🅧';
dismiss.title = 'Dismiss & mark as read';
function removeItem() {
li.remove();
visibleItems = visibleItems.filter(v => v.data.name !== d.name);
hiddenIDs.push(d.name);
setHiddenMessageIDs(hiddenIDs);
markMessageAsRead(d.name);
updateFakePing(visibleItems.length);
}
a.addEventListener('click', removeItem);
dismiss.addEventListener('click', removeItem);
li.appendChild(a);
li.appendChild(dismiss);
list.appendChild(li);
fetch(`${baseUrl}/user/${d.author}/about.json`, { credentials: 'include' })
.then(r => r.json())
.then(user => {
const icon = user.data?.icon_img?.split('?')[0];
if (icon) avatar.src = icon;
}).catch(() => {});
}
} catch {
const li = document.createElement('li');
li.textContent = 'Error loading notifications.';
list.appendChild(li);
}
}
new MutationObserver(() => replaceInboxButton()).observe(document.body, { childList: true, subtree: true });
document.addEventListener('click', (e) => {
if (open && !container.contains(e.target) && !e.target.closest('#notifications-inbox-button')) {
container.style.display = 'none';
open = false;
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && open) {
container.style.display = 'none';
open = false;
}
});
container.addEventListener('click', (e) => e.stopPropagation());
function removeDynamicBadge() {
const badge = document.querySelector('dynamic-badge[data-id="notification-count-element"]');
if (badge) badge.remove();
}
removeDynamicBadge();
new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
removeDynamicBadge();
}
}
}).observe(document.body, { childList: true, subtree: true });
setInterval(removeDynamicBadge, 3000);
setInterval(refreshNotificationCount, 10000);
})();
Editor is loading...
Leave a Comment