mirror of
https://github.com/afoim/fuwari.git
synced 2026-01-31 00:53:19 +08:00
557 lines
26 KiB
Plaintext
557 lines
26 KiB
Plaintext
<style>
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background-color: var(--primary);
|
|
border-radius: 20px;
|
|
opacity: 0.5;
|
|
}
|
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background-color: var(--primary);
|
|
opacity: 0.8;
|
|
}
|
|
/* Firefox */
|
|
.custom-scrollbar {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--primary) transparent;
|
|
}
|
|
</style>
|
|
|
|
<div id="new-post-notification" class="fixed bottom-4 right-4 z-50 flex flex-col items-end pointer-events-none">
|
|
<!-- Minimized State (Bell Icon) -->
|
|
<button id="notification-minimized" class="pointer-events-auto bg-[var(--card-bg)] border border-[var(--primary)] text-[var(--primary)] p-3 rounded-full shadow-lg transform translate-y-20 opacity-0 transition-all duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)] hover:scale-110 active:scale-95 flex items-center justify-center relative group">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path></svg>
|
|
<span id="notification-dot" class="absolute top-0 right-0 w-3 h-3 bg-red-500 rounded-full border-2 border-[var(--card-bg)] hidden animate-pulse"></span>
|
|
</button>
|
|
|
|
<!-- Expanded State (Panel) -->
|
|
<div id="notification-panel" class="pointer-events-auto bg-[var(--card-bg)] border border-[var(--primary)] rounded-xl shadow-lg p-4 max-w-[90vw] w-80 transform translate-y-4 opacity-0 scale-95 origin-bottom-right transition-all duration-300 hidden absolute bottom-16 right-0 sm:bottom-14">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<div class="flex items-center gap-2 text-[var(--primary)]">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
|
<h3 class="font-bold text-black dark:text-white">发现新文章</h3>
|
|
</div>
|
|
<div class="flex items-center gap-1">
|
|
<button id="clear-notification" class="text-black/50 dark:text-white/50 hover:text-red-500 transition-colors p-1 rounded-md hover:bg-red-500/10" title="清空通知">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"></path><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path></svg>
|
|
</button>
|
|
<button id="minimize-notification" class="text-black/50 dark:text-white/50 hover:text-[var(--primary)] transition-colors p-1 rounded-md hover:bg-[var(--primary)]/10" title="隐藏">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="new-post-list" class="text-sm text-black/80 dark:text-white/80 transition-colors space-y-1 max-h-[60vh] overflow-y-auto overflow-x-hidden custom-scrollbar"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
import * as Diff from 'diff';
|
|
|
|
(async function() {
|
|
const DB_NAME = 'fuwari-rss-store';
|
|
const DB_VERSION = 2;
|
|
const STORE_NAME = 'posts';
|
|
const LOCAL_STORAGE_KEY = 'blog-posts-cache';
|
|
const NOTIFICATION_ID = 'new-post-notification';
|
|
const LIST_ID = 'new-post-list';
|
|
|
|
// Compute a context-aware ID for the current site/path
|
|
const SCOPE_ID = window.location.pathname.split('/')[1] || 'root';
|
|
|
|
// IndexedDB Helpers
|
|
function openDB() {
|
|
return new Promise((resolve, reject) => {
|
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
request.onerror = () => reject(request.error);
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onupgradeneeded = (event) => {
|
|
const db = event.target.result;
|
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
// We use 'id' as the keyPath which will be a composite key
|
|
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function generateId(guid) {
|
|
// Create a unique ID combining the scope (pathname root) and the article guid
|
|
return `${SCOPE_ID}:${guid}`;
|
|
}
|
|
|
|
function getStoredPosts(db) {
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], 'readonly');
|
|
const store = transaction.objectStore(STORE_NAME);
|
|
const request = store.getAll();
|
|
request.onsuccess = () => resolve(request.result);
|
|
request.onerror = () => reject(request.error);
|
|
});
|
|
}
|
|
|
|
function savePosts(db, posts) {
|
|
return new Promise((resolve, reject) => {
|
|
const transaction = db.transaction([STORE_NAME], 'readwrite');
|
|
const store = transaction.objectStore(STORE_NAME);
|
|
|
|
posts.forEach(post => {
|
|
// Ensure we save with the scoped ID
|
|
const itemToSave = { ...post, id: generateId(post.guid) };
|
|
store.put(itemToSave);
|
|
});
|
|
|
|
transaction.oncomplete = () => resolve();
|
|
transaction.onerror = () => reject(transaction.error);
|
|
});
|
|
}
|
|
|
|
// Helper to get current RSS items
|
|
async function fetchRSS() {
|
|
try {
|
|
const response = await fetch('/rss.xml', { cache: 'no-store' });
|
|
const text = await response.text();
|
|
const parser = new DOMParser();
|
|
const xml = parser.parseFromString(text, 'text/xml');
|
|
const items = Array.from(xml.querySelectorAll('item'));
|
|
|
|
return items.map(item => {
|
|
const title = item.querySelector('title')?.textContent || '';
|
|
const link = item.querySelector('link')?.textContent || '';
|
|
const guid = item.querySelector('guid')?.textContent || link;
|
|
const pubDate = new Date(item.querySelector('pubDate')?.textContent || '').getTime();
|
|
const description = item.querySelector('description')?.textContent || '';
|
|
|
|
// Try to get content from multiple possible sources to ensure we capture it
|
|
const contentEncoded = item.getElementsByTagNameNS('http://purl.org/rss/1.0/modules/content/', 'encoded')[0]?.textContent;
|
|
const content = contentEncoded ||
|
|
item.getElementsByTagName('content:encoded')[0]?.textContent ||
|
|
item.querySelector('content')?.textContent || '';
|
|
|
|
return {
|
|
title,
|
|
link,
|
|
guid,
|
|
pubDate,
|
|
description,
|
|
content // Full content is stored here
|
|
};
|
|
});
|
|
} catch (e) {
|
|
console.error('Failed to fetch RSS:', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Helper to compute text diff
|
|
function computeDiff(oldText, newText) {
|
|
if (!oldText || !newText) return null;
|
|
|
|
// Strip HTML tags for cleaner comparison (optional, but usually better for text posts)
|
|
const stripHtml = (html) => {
|
|
const tmp = document.createElement("DIV");
|
|
tmp.innerHTML = html;
|
|
return tmp.textContent || tmp.innerText || "";
|
|
};
|
|
|
|
const cleanOld = stripHtml(oldText);
|
|
const cleanNew = stripHtml(newText);
|
|
|
|
// Use 'diff' library to compute line-by-line diff
|
|
const diffs = Diff.diffLines(cleanOld, cleanNew);
|
|
|
|
// Filter to only show relevant changes (context)
|
|
// We don't want to show the whole file if only one line changed.
|
|
// Simple logic: return chunks that are added or removed, plus a bit of context.
|
|
|
|
// But for simplicity in UI rendering, let's just return the diff structure
|
|
// and let the UI render it with colors.
|
|
// We only return if there are actual changes.
|
|
const hasChanges = diffs.some(part => part.added || part.removed);
|
|
|
|
if (!hasChanges) return null;
|
|
|
|
return diffs;
|
|
}
|
|
|
|
// Helper to show notification
|
|
function showNotification(newPosts, timestamp, isFresh, initTime) {
|
|
const minimizedBtn = document.getElementById('notification-minimized');
|
|
const panel = document.getElementById('notification-panel');
|
|
const list = document.getElementById(LIST_ID);
|
|
const dot = document.getElementById('notification-dot');
|
|
const minimizeBtn = document.getElementById('minimize-notification');
|
|
const clearBtn = document.getElementById('clear-notification');
|
|
const NOTIFICATION_STATE_KEY = 'fuwari-notification-state';
|
|
const INIT_TIME_KEY = 'fuwari-notification-init-time';
|
|
|
|
if (!minimizedBtn || !panel || !list) return;
|
|
|
|
// Show the minimized bell with animation
|
|
// Add a small delay to make the entrance noticeable after page load
|
|
requestAnimationFrame(() => {
|
|
minimizedBtn.classList.remove('translate-y-20', 'opacity-0');
|
|
});
|
|
|
|
const initTimeStr = new Date(initTime).toLocaleString();
|
|
const checkTimeStr = new Date(timestamp).toLocaleString();
|
|
|
|
// Logic for "No updates"
|
|
if (newPosts.length === 0) {
|
|
list.innerHTML = `<div class="text-center text-gray-500 dark:text-gray-400 py-4">
|
|
<p class="text-sm font-medium mb-2">暂无文章更新</p>
|
|
<div class="text-xs opacity-70 bg-gray-100 dark:bg-gray-800 rounded px-2 py-1 inline-block">
|
|
${initTimeStr} - ${checkTimeStr}
|
|
</div>
|
|
</div>`;
|
|
dot?.classList.add('hidden');
|
|
|
|
// Setup event listeners even if no posts (so it can be opened)
|
|
setupEventListeners();
|
|
return;
|
|
}
|
|
|
|
// Show timestamp header
|
|
let html = `
|
|
<div class="text-xs text-gray-500 dark:text-gray-400 mb-2 px-1 flex flex-col gap-0.5">
|
|
<div class="font-medium">发现更新</div>
|
|
<div class="opacity-70 text-[10px]">${initTimeStr} - ${checkTimeStr}</div>
|
|
</div>`;
|
|
|
|
newPosts.forEach(post => {
|
|
const isUpdated = post.isUpdated;
|
|
const badge = isUpdated
|
|
? '<span class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-600 dark:text-blue-300 px-1.5 py-0.5 rounded ml-2">更新</span>'
|
|
: '<span class="text-xs bg-green-100 dark:bg-green-900 text-green-600 dark:text-green-300 px-1.5 py-0.5 rounded ml-2">新文章</span>';
|
|
|
|
// Generate Diff View button if updated
|
|
let diffButton = '';
|
|
const safeId = 'diff-' + post.guid.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
|
|
if (isUpdated && post.diff) {
|
|
diffButton = `
|
|
<button onclick="window.toggleDiff('${safeId}')" class="ml-auto text-xs text-[var(--primary)] hover:underline focus:outline-none pointer-events-auto">
|
|
查看变更
|
|
</button>`;
|
|
}
|
|
|
|
html += `
|
|
<div class="mb-2 last:mb-0">
|
|
<div class="flex items-center justify-between p-2 rounded-lg hover:bg-[var(--primary)]/5 transition-colors">
|
|
<a href="${post.link}" class="font-medium truncate pr-2 hover:text-[var(--primary)] transition-colors text-black dark:text-white block flex-1" target="_blank">
|
|
${post.title}
|
|
</a>
|
|
<div class="flex items-center shrink-0">
|
|
${diffButton}
|
|
${badge}
|
|
</div>
|
|
</div>
|
|
${isUpdated && post.diff ? `
|
|
<div id="${safeId}" class="hidden mt-2 p-2 bg-gray-50 dark:bg-gray-800 rounded text-xs overflow-x-auto border border-gray-200 dark:border-gray-700 max-h-60 overflow-y-auto">
|
|
${post.diff.map(part => {
|
|
const colorClass = part.added ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 block my-1 p-1 rounded break-all whitespace-pre-wrap' :
|
|
part.removed ? 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 block my-1 p-1 rounded break-all whitespace-pre-wrap' :
|
|
'text-gray-500 dark:text-gray-400 block my-1 p-1 break-all whitespace-pre-wrap';
|
|
return `<div class="${colorClass}">${part.value}</div>`;
|
|
}).join('')}
|
|
</div>
|
|
` : ''}
|
|
</div>`;
|
|
});
|
|
|
|
list.innerHTML = html;
|
|
|
|
// Show red dot ONLY if it is a fresh update
|
|
if (isFresh) {
|
|
dot?.classList.remove('hidden');
|
|
// Auto-open panel on first load if there are updates
|
|
setTimeout(() => {
|
|
openPanel();
|
|
}, 1500);
|
|
} else {
|
|
dot?.classList.add('hidden');
|
|
}
|
|
|
|
setupEventListeners();
|
|
|
|
function setupEventListeners() {
|
|
// State management functions
|
|
const openPanel = () => {
|
|
panel.classList.remove('hidden');
|
|
// Small delay to allow display:block to apply before transition
|
|
requestAnimationFrame(() => {
|
|
panel.classList.remove('translate-y-4', 'opacity-0', 'scale-95');
|
|
});
|
|
dot?.classList.add('hidden'); // Hide dot when opened
|
|
};
|
|
|
|
const closePanel = () => {
|
|
panel.classList.add('translate-y-4', 'opacity-0', 'scale-95');
|
|
setTimeout(() => {
|
|
panel.classList.add('hidden');
|
|
}, 300); // Match transition duration
|
|
};
|
|
|
|
// Event Listeners
|
|
// Remove old listeners to prevent duplicates if called multiple times?
|
|
// Actually, in this script structure, showNotification is called once per page load.
|
|
minimizedBtn.onclick = () => {
|
|
if (panel.classList.contains('hidden')) {
|
|
openPanel();
|
|
} else {
|
|
closePanel();
|
|
}
|
|
};
|
|
|
|
minimizeBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
closePanel();
|
|
// Completely hide the bell after closing the panel via the 'X' button
|
|
// Wait for panel closing animation to finish
|
|
setTimeout(() => {
|
|
minimizedBtn.classList.add('translate-y-20', 'opacity-0');
|
|
minimizedBtn.classList.add('pointer-events-none'); // Prevent clicks while hidden
|
|
}, 300);
|
|
};
|
|
|
|
if (clearBtn) {
|
|
clearBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
// Clear state from localStorage
|
|
localStorage.removeItem(NOTIFICATION_STATE_KEY);
|
|
|
|
// Update init time to current time since we are "resetting" the baseline
|
|
const now = Date.now();
|
|
localStorage.setItem(INIT_TIME_KEY, now.toString());
|
|
|
|
// Refresh view with new init time
|
|
showNotification([], now, false, now);
|
|
};
|
|
}
|
|
|
|
// Add event delegation for toggles
|
|
// Ensure we don't add duplicate listeners to list
|
|
// A simple way is to re-create the listener or check via a flag.
|
|
// But since this function runs once, it's fine.
|
|
if (!list.hasAttribute('data-listening')) {
|
|
list.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('[data-diff-toggle]');
|
|
if (btn) {
|
|
const targetId = btn.getAttribute('data-diff-toggle');
|
|
const target = document.getElementById(targetId);
|
|
if (target) {
|
|
target.classList.toggle('hidden');
|
|
}
|
|
}
|
|
});
|
|
list.setAttribute('data-listening', 'true');
|
|
}
|
|
}
|
|
|
|
// Helper to trigger open from outside (used by auto-open logic above)
|
|
// We need to expose openPanel or move the auto-open logic inside setupEventListeners or keep it here
|
|
// The previous code called openPanel() which was defined below.
|
|
// We need to make sure openPanel is available.
|
|
function openPanel() {
|
|
panel.classList.remove('hidden');
|
|
requestAnimationFrame(() => {
|
|
panel.classList.remove('translate-y-4', 'opacity-0', 'scale-95');
|
|
});
|
|
dot?.classList.add('hidden');
|
|
}
|
|
|
|
const closePanel = () => {
|
|
panel.classList.add('translate-y-4', 'opacity-0', 'scale-95');
|
|
setTimeout(() => {
|
|
panel.classList.add('hidden');
|
|
}, 300); // Match transition duration
|
|
};
|
|
|
|
// Event Listeners
|
|
minimizedBtn.onclick = () => {
|
|
if (panel.classList.contains('hidden')) {
|
|
openPanel();
|
|
} else {
|
|
closePanel();
|
|
}
|
|
};
|
|
|
|
minimizeBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
closePanel();
|
|
};
|
|
|
|
// Add event delegation for toggles
|
|
// Remove and re-add to ensure freshness
|
|
// Or better, just use one listener on the container that always works
|
|
// The previous check if (!list.hasAttribute('data-listening')) might be persisting across re-renders improperly if Astro keeps DOM
|
|
// Let's make it robust:
|
|
|
|
// Remove existing listener if possible? No easy way without reference.
|
|
// Instead, let's use a unique attribute or just replace the node (expensive)
|
|
// Or simpler: assign onclick handler to the list container which bubbles up
|
|
|
|
// Use a window-level handler for maximum robustness, filtering by ID
|
|
window.toggleDiff = function(safeId) {
|
|
const target = document.getElementById(safeId);
|
|
if (target) {
|
|
target.classList.toggle('hidden');
|
|
}
|
|
};
|
|
|
|
// Auto-open panel on first load if it's high priority?
|
|
// Or keep it minimized to be less intrusive?
|
|
// Let's auto-open it briefly to catch attention, then user can minimize
|
|
}
|
|
|
|
// Main logic
|
|
try {
|
|
// Developer Mode Check
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const isDevMode = urlParams.has('debug-notification');
|
|
|
|
if (isDevMode) {
|
|
console.log('[Notification] Developer mode active');
|
|
const mockDiff = [
|
|
{ value: "This is some unchanged text context.\n", added: undefined, removed: undefined },
|
|
{ value: "This line was removed.\n", added: undefined, removed: true },
|
|
{ value: "This line is new.\n", added: true, removed: undefined },
|
|
{ value: "More unchanged text.\n", added: undefined, removed: undefined }
|
|
];
|
|
|
|
const mockPosts = [
|
|
{ title: 'Test New Post 1', link: '#', guid: 'test-1', isUpdated: false },
|
|
{
|
|
title: 'Test Updated Post',
|
|
link: '#',
|
|
guid: 'test-2',
|
|
isUpdated: true,
|
|
diff: mockDiff
|
|
},
|
|
];
|
|
showNotification(mockPosts, Date.now(), true);
|
|
return;
|
|
}
|
|
|
|
const currentPosts = await fetchRSS();
|
|
if (currentPosts.length === 0) return;
|
|
|
|
const db = await openDB();
|
|
const storedPosts = await getStoredPosts(db);
|
|
const NOTIFICATION_STATE_KEY = 'fuwari-notification-state';
|
|
const INIT_TIME_KEY = 'fuwari-notification-init-time';
|
|
|
|
// Helper to get or set init time
|
|
let initTimestamp = localStorage.getItem(INIT_TIME_KEY);
|
|
if (!initTimestamp) {
|
|
initTimestamp = Date.now().toString();
|
|
localStorage.setItem(INIT_TIME_KEY, initTimestamp);
|
|
}
|
|
const initTime = parseInt(initTimestamp);
|
|
|
|
// Migration from localStorage (Old cache)
|
|
const localCache = localStorage.getItem(LOCAL_STORAGE_KEY);
|
|
if (storedPosts.length === 0 && localCache) {
|
|
console.log('[Notification] Migrating from localStorage...');
|
|
// Just init DB and clear old cache
|
|
await savePosts(db, currentPosts);
|
|
localStorage.removeItem(LOCAL_STORAGE_KEY);
|
|
// Treat as first visit (no updates)
|
|
showNotification([], Date.now(), false, initTime);
|
|
return;
|
|
}
|
|
|
|
if (storedPosts.length === 0) {
|
|
// First visit
|
|
await savePosts(db, currentPosts);
|
|
// Clear any stale notification state
|
|
localStorage.removeItem(NOTIFICATION_STATE_KEY);
|
|
showNotification([], Date.now(), false, initTime);
|
|
} else {
|
|
// Compare
|
|
const storedMap = new Map(
|
|
storedPosts
|
|
.filter(p => p.id && p.id.startsWith(`${SCOPE_ID}:`))
|
|
.map(p => [p.guid, p])
|
|
);
|
|
|
|
const detectedChanges = [];
|
|
|
|
currentPosts.forEach(post => {
|
|
const stored = storedMap.get(post.guid);
|
|
if (!stored) {
|
|
// New post
|
|
detectedChanges.push({ ...post, isUpdated: false });
|
|
} else {
|
|
// Check for ANY change (Title, Date, Description, Content)
|
|
const titleChanged = post.title !== stored.title;
|
|
const dateChanged = post.pubDate !== stored.pubDate;
|
|
const descriptionChanged = post.description !== stored.description;
|
|
const contentChanged = post.content !== stored.content;
|
|
|
|
if (titleChanged || dateChanged || descriptionChanged || contentChanged) {
|
|
// Calculate diff for content
|
|
let diff = null;
|
|
|
|
if (contentChanged) {
|
|
diff = computeDiff(stored.content, post.content);
|
|
} else if (descriptionChanged) {
|
|
// If content is same but description changed, show description diff
|
|
diff = computeDiff(stored.description, post.description);
|
|
// Add a label to indicate this is description diff?
|
|
// The UI doesn't support labels inside diff yet, but at least user sees the text change.
|
|
} else if (titleChanged) {
|
|
diff = computeDiff(stored.title, post.title);
|
|
}
|
|
|
|
detectedChanges.push({
|
|
...post,
|
|
isUpdated: true,
|
|
diff
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
if (detectedChanges.length > 0) {
|
|
// CASE 1: New updates detected
|
|
const timestamp = Date.now();
|
|
// Persist notification state
|
|
const state = {
|
|
timestamp: timestamp,
|
|
items: detectedChanges
|
|
};
|
|
localStorage.setItem(NOTIFICATION_STATE_KEY, JSON.stringify(state));
|
|
|
|
// Update DB so we don't detect them again as "fresh" next time
|
|
await savePosts(db, currentPosts);
|
|
|
|
// Show with Red Dot + Auto Open
|
|
showNotification(detectedChanges, timestamp, true, initTime);
|
|
} else {
|
|
// CASE 2: No new updates
|
|
// Check if we have a persisted notification state
|
|
const persistedStateStr = localStorage.getItem(NOTIFICATION_STATE_KEY);
|
|
if (persistedStateStr) {
|
|
try {
|
|
const state = JSON.parse(persistedStateStr);
|
|
// Show persisted items, NO Red Dot, NO Auto Open
|
|
showNotification(state.items, state.timestamp, false, initTime);
|
|
} catch (e) {
|
|
console.error('Failed to parse notification state', e);
|
|
showNotification([], Date.now(), false, initTime);
|
|
}
|
|
} else {
|
|
// No updates and no history -> "No updates"
|
|
showNotification([], Date.now(), false, initTime);
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error('New post check failed:', e);
|
|
}
|
|
})();
|
|
</script> |