Files
steveseguin c9380616de merge fix
2025-05-12 21:45:08 -04:00

1034 lines
38 KiB
JavaScript

document.addEventListener('DOMContentLoaded', () => {
initializeApp();
});
async function initializeApp() {
try {
const initialized = await window.notificationManager.initialize();
console.log('Notification manager initialized:', initialized);
handleUrlParameters();
initTabSystem();
initNotificationBadge();
checkNotificationPermission();
setupUrlDetection();
enhanceTopicInput();
setupEventListeners();
setupParseUrlButton();
checkSavedSubscription();
updateExampleUrl();
window.notificationManager.subscribe(handleNotificationEvent);
const statusCheckInterval = setInterval(() => {
if (navigator.serviceWorker.controller && localStorage.getItem('notifyTopic')) {
window.notificationManager.checkStatus()
.then(status => {
updateSubscriptionStatusPanel();
});
}
}, 30000);
const renewalCheckInterval = setInterval(() => {
if (navigator.serviceWorker.controller && localStorage.getItem('notifyTopic')) {
window.notificationManager.checkSubscriptionRenewal();
}
}, 3600000);
setTimeout(() => {
if (navigator.serviceWorker.controller && localStorage.getItem('notifyTopic')) {
window.notificationManager.checkSubscriptionRenewal();
}
}, 10000);
const subscriptionHeartbeatInterval = setInterval(() => {
if (navigator.serviceWorker.controller && localStorage.getItem('notifyTopic')) {
const topic = localStorage.getItem('notifyTopic');
// Ping the server to keep subscription active
fetch('https://notify.vdo.ninja/ping', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
topic: topic
})
}).catch(err => console.warn('Heartbeat error:', err));
// Also periodically test the push subscription
window.notificationManager.testPushSubscription()
.then(result => {
if (!result.success) {
console.log('Push subscription test failed:', result);
window.notificationManager.startSubscription(topic);
}
});
}
}, 24 * 60 * 60 * 1000); // Daily check
const validityCheckInterval = setInterval(() => {
if (navigator.serviceWorker.controller && localStorage.getItem('notifyTopic')) {
window.notificationManager.checkSubscriptionValidity().then(isValid => {
if (!isValid) {
const topic = localStorage.getItem('notifyTopic');
if (topic) {
console.log('Subscription became invalid, resubscribing...');
window.notificationManager.startSubscription(topic);
}
}
});
}
}, 3600000);
updateSubscriptionStatusPanel();
console.log('App initialization complete');
} catch (error) {
console.error('Error initializing app:', error);
document.getElementById('notification-status').innerHTML = `
<div class="status warning">
<p>Error initializing notification system. Please refresh the page or try again later.</p>
</div>
`;
}
}
async function handleVdoUrlParameter(vdoUrlParam, subscribeParam) {
try {
const decodedUrl = decodeURIComponent(vdoUrlParam);
const result = await parseVdoNinjaUrl(decodedUrl);
if (result && result.topic) {
const topicInput = document.getElementById('topic-input');
if (topicInput) {
topicInput.value = result.topic;
updateExampleUrl();
}
const urlInput = document.getElementById('vdo-url-input');
if (urlInput) {
urlInput.value = generateSafeUrl(decodedUrl);
}
if (subscribeParam === 'true' || subscribeParam === '1') {
setTimeout(() => {
requestNotificationPermission().then(permission => {
if (permission === 'granted') {
startNotificationSubscription(result.topic);
displayStatusMessage(`✅ Automatically subscribed to: <strong>${result.topic}</strong>`, 'active');
}
});
}, 1000);
}
}
} catch (error) {
console.error('Error handling VDO URL parameter:', error);
}
}
function handleTopicParameter(topicParam) {
if (!topicParam){return;}
const topicInput = document.getElementById('topic-input');
if (topicInput) {
topicInput.value = topicParam;
}
setTimeout(() => {
requestNotificationPermission().then(permission => {
if (permission === 'granted') {
startNotificationSubscription(topicParam);
displayStatusMessage(`✅ Automatically subscribed to topic: <strong>${topicParam}</strong>`, 'active');
}
});
}, 1000);
}
function handleUrlParameters() {
try {
const urlParams = new URLSearchParams(window.location.search);
const subscribeParam = urlParams.get('subscribe');
const topicParam = urlParams.get('topic');
const vdoUrlParam = urlParams.get('url');
if (vdoUrlParam) {
handleVdoUrlParameter(vdoUrlParam, subscribeParam);
}
else if (subscribeParam === 'true' || subscribeParam === '1') {
if (topicParam) {
handleTopicParameter(topicParam);
}
}
history.replaceState(null, '', window.location.pathname);
} catch (e) {
console.error('Error handling URL parameters:', e);
displayStatusMessage('Error processing URL parameters', 'warning');
}
}
function extractVdoNinjaUrl(text) {
if (!text) return null;
const regex = /(https?:\/\/)?([\w.-]+\.)?(vdo\.ninja|obs\.ninja)(\S*)/gi;
const match = regex.exec(text);
if (match) {
let url = match[0];
if (!url.startsWith('http')) {
url = 'https://' + url;
}
return url;
}
return null;
}
function hashTopic(text) {
if (!text || typeof text !== 'string') {
return generateRandomTopic();
}
try {
const salt1 = "VDO_24x89jf3mwqpz";
const salt2 = "Ninja_7fhGj2P9wq";
let saltedText = salt1 + text + salt2 + text.split('').reverse().join('');
let hash = 0;
for (let i = 0; i < saltedText.length; i++) {
const char = saltedText.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
let hash2 = 0;
for (let i = 0; i < saltedText.length; i++) {
hash2 = ((hash2 << 7) + hash2) + saltedText.charCodeAt(i);
hash2 = hash2 & hash2;
}
const combinedHash = Math.abs(hash).toString(36) + Math.abs(hash2).toString(36);
if (combinedHash.length < 8) {
return combinedHash + Math.random().toString(36).substring(2, 10);
}
return combinedHash.substring(0, 16);
} catch (e) {
console.error('Error in hashTopic:', e);
return generateRandomTopic();
}
}
function addParamToUrl(url, param, value) {
try {
const urlObj = new URL(url);
urlObj.searchParams.set(param, value);
return urlObj.toString();
} catch (e) {
console.error('Error adding param to URL:', e);
return url;
}
}
async function parseVdoNinjaUrl(url) {
try {
if (typeof url !== 'string') {
throw new Error('Invalid URL: must be a string');
}
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
if (!url.includes('vdo.ninja') && !url.includes('obs.ninja') && !url.includes('versus.cam') && !url.includes('comms.cam')) {
throw new Error('Not a valid VDO.Ninja URL');
}
const urlObj = new URL(url);
const params = new URLSearchParams(urlObj.search);
const existingTopic = params.get('poke');
if (existingTopic) {
return {
topic: existingTopic,
notificationUrl: url,
originalUrl: url,
isExistingTopic: true
};
}
const roomId = params.get('room') || params.get('roomid') || params.get('r') || '';
const directorId = params.get('director') || params.get('dir') || '';
const pushId = params.get('push') || params.get('id') || params.get('permaid') || '';
const viewId = params.get('streamid') || params.get('view') || params.get('v') || params.get('pull') || '';
const password = params.get('password') || '';
const hash = params.get('hash') || '';
const scene = params.get('scene') || params.get('scn') || '';
const components = {
room: roomId,
director: directorId,
push: pushId,
view: viewId,
scene: scene,
domain: urlObj.hostname.replace(/\./g, '_')
};
let sensitiveData = Object.entries(components)
.filter(([_, value]) => value)
.map(([key, value]) => `${key}:${value}`)
.join('_');
if (!sensitiveData) {
sensitiveData = 'vdo_' + Math.random().toString(36).substring(2, 10);
}
const secureTopicHash = hashTopic(sensitiveData);
const finalPrefix = components.domain || 'vdo_ninja';
const finalTopic = `${finalPrefix}_${secureTopicHash.substring(0, 12)}`;
const notificationUrl = addParamToUrl(url, 'poke', finalTopic);
return {
topic: finalTopic,
notificationUrl: notificationUrl,
originalUrl: url,
roomId: components.room || '',
directorId: components.director || '',
viewId: components.view || '',
pushId: components.push || ''
};
} catch (e) {
console.error('Error parsing VDO.Ninja URL:', e);
throw e;
}
}
function toHexString(buffer) {
return Array.prototype.map.call(
buffer,
b => b.toString(16).padStart(2, '0')
).join('');
}
function setupUrlDetection() {
const topicInput = document.getElementById('topic-input');
const urlInput = document.getElementById('vdo-url-input');
if (!urlInput) {
const subscribeCard = document.querySelector('.notification-card');
if (subscribeCard) {
const newFormGroup = document.createElement('div');
newFormGroup.className = 'form-group';
newFormGroup.innerHTML = `
<label for="vdo-url-input">VDO.Ninja URL (Optional):</label>
<input type="text" id="vdo-url-input" placeholder="Paste your VDO.Ninja URL">
<small>We'll extract the notification topic from your URL automatically</small>
`;
const topicFormGroup = subscribeCard.querySelector('.form-group');
if (topicFormGroup) {
subscribeCard.insertBefore(newFormGroup, topicFormGroup);
} else {
subscribeCard.appendChild(newFormGroup);
}
const urlInput = document.getElementById('vdo-url-input');
addUrlInputListeners(urlInput, topicInput);
}
} else {
addUrlInputListeners(urlInput, topicInput);
}
}
function displayStatusMessage(message, statusType = 'active', duration = 5000) {
const statusEl = document.getElementById('notification-status');
if (!statusEl) return;
statusEl.innerHTML = `
<div class="status ${statusType}">
<p>${message}</p>
</div>
`;
if (duration > 0) {
setTimeout(() => {
if (statusEl.innerHTML.includes(message)) {
statusEl.innerHTML = '';
}
}, duration);
}
}
async function processVdoUrl(url, topicInput) {
try {
const result = await parseVdoNinjaUrl(url);
if (result && result.topic) {
topicInput.value = result.topic;
updateExampleUrl();
displayStatusMessage(`✅ Topic extracted: <strong>${result.topic}</strong>`, 'active');
}
} catch (error) {
console.error('Error processing VDO URL:', error);
displayStatusMessage('⚠️ Could not extract topic from URL', 'warning');
}
}
function addUrlInputListeners(urlInput, topicInput) {
if (!urlInput || !topicInput) return;
urlInput.addEventListener('input', async () => {
const url = urlInput.value.trim();
if (url && (url.includes('vdo.ninja') || url.includes('obs.ninja'))) {
processVdoUrl(url, topicInput);
}
});
urlInput.addEventListener('paste', async (e) => {
setTimeout(async () => {
const url = urlInput.value.trim();
if (url && (url.includes('vdo.ninja') || url.includes('obs.ninja'))) {
processVdoUrl(url, topicInput);
}
}, 10);
});
}
function enhanceTopicInput() {
const topicInput = document.getElementById('topic-input');
if (topicInput) {
topicInput.addEventListener('paste', async (e) => {
const clipboardData = e.clipboardData || window.clipboardData;
const pastedText = clipboardData.getData('text');
if (pastedText && (pastedText.includes('://') ||
pastedText.includes('vdo.ninja') ||
pastedText.includes('obs.ninja'))) {
e.preventDefault();
const topic = await extractTopicFromUrl(pastedText);
if (topic) {
topicInput.value = topic;
updateExampleUrl();
displayStatusMessage(`✅ Topic extracted from URL: <strong>${topic}</strong>`, 'active');
} else {
topicInput.value = pastedText;
displayStatusMessage('⚠️ Could not extract topic from text', 'warning');
}
}
});
}
}
async function extractTopicFromUrl(url) {
if (!url) return null;
if (/^[a-zA-Z0-9_-]+$/.test(url) && !url.includes('.') && !url.includes('/')) {
return url;
}
const vdoUrl = extractVdoNinjaUrl(url);
if (vdoUrl) {
const result = await parseVdoNinjaUrl(vdoUrl);
if (result && result.topic) {
return result.topic;
}
}
return null;
}
function generateSafeUrl(url) {
try {
if (!url) return '';
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'https://' + url;
}
const urlObj = new URL(url);
const params = new URLSearchParams(urlObj.search);
const sensitiveParams = [
'password', 'pass', 'pwd', 'hash', 'secret', 'token', 'key', 'apikey',
'api_key', 'auth', 'credentials', 'cred', 'signin', 'login'
];
sensitiveParams.forEach(param => {
if (params.has(param)) {
params.delete(param);
}
});
urlObj.search = params.toString();
return urlObj.toString();
} catch (e) {
console.error("Error generating safe URL:", e);
return '';
}
}
function updateSubscriptionStatusPanel() {
const statusContainer = document.getElementById('subscription-status-panel');
if (!statusContainer) {
const panel = document.createElement('div');
panel.id = 'subscription-status-panel';
panel.className = 'notification-card';
panel.innerHTML = `
<h2>Subscription Status</h2>
<div id="subscription-status-details">
<p>Not currently monitoring any topics</p>
</div>
`;
const firstCard = document.querySelector('.notification-card');
if (firstCard && firstCard.parentNode) {
firstCard.parentNode.insertBefore(panel, firstCard.nextSibling);
}
}
const topic = localStorage.getItem('notifyTopic');
const statusDetails = document.getElementById('subscription-status-details');
if (!statusDetails) return;
if (topic) {
checkConnectionStatus().then(statusInfo => {
updateStatusDisplay(statusInfo);
}).catch(err => {
console.error('Error checking connection status:', err);
updateStatusDisplay({ status: 'disconnected', text: 'Connection error' });
});
const lastPollTime = localStorage.getItem('lastPollTime') || 'Never';
const lastPollDate = lastPollTime !== 'Never' ? new Date(parseInt(lastPollTime)).toLocaleString() : 'Never';
const pushEnabled = localStorage.getItem('pushSubscription') ? 'Enabled' : 'Disabled';
statusDetails.innerHTML = `
<div class="status active">
<p><strong>Currently subscribed to:</strong> ${topic}</p>
<p><strong>Connection status:</strong> <span id="sw-connection-status">Checking...</span></p>
<p><strong>Push notifications:</strong> <span id="push-status">${pushEnabled}</span></p>
<p><strong>Last activity:</strong> <span id="last-activity-display">${lastPollDate}</span></p>
<button id="unsubscribe-button" class="danger">Unsubscribe from "${topic}"</button>
</div>
`;
document.getElementById('unsubscribe-button')?.addEventListener('click', () => {
stopNotificationSubscription();
updateSubscriptionStatusPanel();
});
} else {
statusDetails.innerHTML = `
<div class="status inactive">
<p>Not currently monitoring any topics</p>
<p>Enter a topic above and click "Subscribe to Notifications"</p>
</div>
`;
}
}
async function checkConnectionStatus() {
if (!navigator.serviceWorker.controller) {
return { status: 'disconnected', text: 'Service worker not active' };
}
try {
const response = await window.notificationManager.sendToServiceWorker({ action: 'checkStatus' });
if (response?.sse?.connected) {
return { status: 'connected', text: 'Using real-time updates' };
} else if (response?.polling?.isPolling) {
return { status: 'polling', text: 'Using fallback polling' };
} else {
return { status: 'disconnected', text: 'Not connected' };
}
} catch (error) {
console.error('Error checking status:', error);
return { status: 'disconnected', text: 'Status check failed' };
}
}
function updateStatusDisplay(statusInfo) {
const statusElement = document.getElementById('sw-connection-status');
if (statusElement) {
statusElement.textContent = statusInfo.text;
statusElement.className = statusInfo.status;
}
}
function handleNotificationEvent(event, data) {
switch (event) {
case 'connectionStatus':
updateConnectionStatus(data.connected, data.topic);
updateSubscriptionStatusPanel();
break;
case 'sseStatus':
updateConnectionStatusDisplay(data.status, data.attempt, data.delay);
updateSubscriptionStatusPanel();
break;
case 'notification':
createNotificationPopup(data.notification);
createFlashEffect();
updateLastActivityTime();
updateSubscriptionStatusPanel();
break;
case 'historyUpdated':
updateNotificationHistoryUI();
updateNotificationBadge();
break;
case 'statusUpdate':
if (data && data.topic) {
document.getElementById('topic-input').value = data.topic;
}
if (data && data.sseConnected) {
updateConnectionStatusDisplay('connected');
} else if (data && data.isPolling) {
updateConnectionStatusDisplay('polling');
}
updateSubscriptionStatusPanel();
break;
}
}
function checkNotificationPermission() {
if ('Notification' in window) {
if (Notification.permission !== 'granted' && Notification.permission !== 'denied') {
document.getElementById('notification-status').innerHTML = `
<div class="status inactive">
<p>Browser notifications are not enabled. <button id="request-permission">Enable Notifications</button></p>
</div>
`;
document.getElementById('request-permission')?.addEventListener('click', requestNotificationPermission);
}
}
}
async function requestNotificationPermission() {
// First check current permission
if (Notification.permission === 'denied') {
console.warn('Notifications are blocked. User must change browser settings.');
document.getElementById('notification-status').innerHTML = `
<div class="status warning">
<p>Notifications are blocked in your browser settings. Please enable them to receive notifications.</p>
</div>
`;
return 'denied';
}
try {
const permission = await window.notificationManager.requestPermission();
if (permission === 'granted') {
document.getElementById('notification-status').innerHTML = `
<div class="status active">
<p>Browser notifications are enabled!</p>
</div>
`;
// Test if push subscription is working
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.getSubscription();
if (subscription) {
console.log('Testing existing push subscription');
await window.notificationManager.testPushSubscription();
}
}
return permission;
} catch (error) {
console.error('Error requesting permission:', error);
return 'error';
}
}
function checkSavedSubscription() {
const savedTopic = localStorage.getItem('notifyTopic');
const topicInput = document.getElementById('topic-input');
if (savedTopic) {
topicInput.value = savedTopic;
if (navigator.serviceWorker.controller) {
window.notificationManager.checkStatus().then(hasActiveSubscription => {
if (!hasActiveSubscription) {
console.log('No active subscription found, starting new subscription');
startNotificationSubscription(savedTopic);
} else {
console.log('Active subscription found and restored');
}
updateSubscriptionStatusPanel();
});
} else {
console.log('Waiting for service worker before restoring subscription');
navigator.serviceWorker.ready.then(() => {
if (navigator.serviceWorker.controller) {
window.notificationManager.checkStatus().then(hasActiveSubscription => {
if (!hasActiveSubscription) {
startNotificationSubscription(savedTopic);
}
updateSubscriptionStatusPanel();
});
} else {
startNotificationSubscription(savedTopic);
updateSubscriptionStatusPanel();
}
});
}
} else {
const randomTopic = generateRandomTopic();
topicInput.value = randomTopic;
updateExampleUrl();
updateSubscriptionStatusPanel();
}
}
function generateRandomTopic() {
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
const length = 8;
let result = '';
for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
result += characters.charAt(randomIndex);
}
return result;
}
function setupEventListeners() {
const subscribeBtn = document.getElementById('subscribe-btn');
if (subscribeBtn) {
subscribeBtn.addEventListener('click', () => {
let topic = document.getElementById('topic-input').value.trim();
if (!topic) {
topic = generateRandomTopic();
document.getElementById('topic-input').value = topic;
}
requestNotificationPermission().then(permission => {
if (permission === 'granted') {
startNotificationSubscription(topic);
}
});
});
}
const clearHistoryBtn = document.getElementById('clear-history');
if (clearHistoryBtn) {
clearHistoryBtn.addEventListener('click', clearNotificationHistory);
}
const unsubscribeBtn = document.getElementById('unsubscribe-all');
if (unsubscribeBtn) {
unsubscribeBtn.addEventListener('click', stopNotificationSubscription);
}
const topicInput = document.getElementById('topic-input');
if (topicInput) {
topicInput.addEventListener('input', updateExampleUrl);
}
const soundSelect = document.getElementById('notification-sound');
if (soundSelect) {
soundSelect.value = localStorage.getItem('notificationSound') || 'default';
soundSelect.addEventListener('change', e => {
localStorage.setItem('notificationSound', e.target.value);
});
}
const desktopToggle = document.getElementById('desktop-notifications');
if (desktopToggle) {
desktopToggle.checked = localStorage.getItem('desktopNotifications') !== 'false';
desktopToggle.addEventListener('change', e => {
localStorage.setItem('desktopNotifications', e.target.checked);
});
}
const fakeNotificationBtn = document.getElementById('fake-notification-btn');
if (fakeNotificationBtn) {
fakeNotificationBtn.addEventListener('click', () => {
let topic = document.getElementById('topic-input').value.trim();
if (!topic) {
topic = generateRandomTopic();
document.getElementById('topic-input').value = topic;
}
const testNotification = {
title: 'Test Notification',
body: 'This is a test notification for VDO.Ninja',
url: window.location.href,
timestamp: Date.now()
};
if (navigator.serviceWorker.controller) {
window.notificationManager.sendToServiceWorker({
action: 'testNotification',
notification: testNotification
});
} else {
window.notificationManager.showNotification(testNotification);
}
});
}
setInterval(updateSubscriptionStatusPanel, 30000);
const testNotificationBtn = document.getElementById('test-notification-btn');
if (testNotificationBtn) {
testNotificationBtn.addEventListener('click', () => {
let topic = document.getElementById('topic-input').value.trim();
if (!topic) {
topic = generateRandomTopic();
document.getElementById('topic-input').value = topic;
}
const button = testNotificationBtn;
const originalText = button.textContent;
button.textContent = 'Sending...';
button.disabled = true;
window.notificationManager.sendTestNotification(topic)
.then(success => {
if (!success) {
alert('Failed to send test notification');
}
})
.catch(error => {
console.error('Error sending notification:', error);
alert('Error sending notification');
})
.finally(() => {
button.textContent = originalText;
button.disabled = false;
});
});
}
}
function startNotificationSubscription(topic) {
if (!topic) return;
window.notificationManager.startSubscription(topic)
.then(success => {
updateSubscriptionStatusPanel();
});
}
function stopNotificationSubscription() {
window.notificationManager.stopSubscription()
.then(() => {
updateSubscriptionStatusPanel();
});
}
function updateConnectionStatus(connected, topic) {
if (connected) {
updateStatusDisplay({ status: 'connected', text: 'Using real-time updates' });
localStorage.setItem('lastPollTime', Date.now().toString());
} else {
updateStatusDisplay({ status: 'disconnected', text: 'Disconnected' });
}
}
function updateConnectionStatusDisplay(status, attempt, delay) {
const statusEl = document.getElementById('connection-status');
if (!statusEl) return;
switch (status) {
case 'connected':
statusEl.textContent = 'Using real-time updates';
statusEl.style.color = '#64b5f6';
break;
case 'reconnecting':
statusEl.textContent = `Connection lost, reconnecting${attempt ? ` (attempt ${attempt})` : ''}...`;
statusEl.style.color = '#ff9800';
break;
case 'polling':
statusEl.textContent = 'Using fallback polling';
statusEl.style.color = '#ff9800';
break;
case 'disconnected':
statusEl.textContent = 'Disconnected';
statusEl.style.color = '#f44336';
break;
default:
statusEl.textContent = 'Connection status unknown';
statusEl.style.color = '#999';
}
}
function updateLastActivityTime() {
const lastActivityEl = document.getElementById('last-activity-display');
if (!lastActivityEl) return;
const lastPollTime = parseInt(localStorage.getItem('lastPollTime') || '0');
const now = Date.now();
const diff = now - lastPollTime;
if (diff < 60000) {
lastActivityEl.textContent = 'Just now';
} else if (diff < 3600000) {
const minutes = Math.floor(diff / 60000);
lastActivityEl.textContent = `${minutes} minute${minutes !== 1 ? 's' : ''} ago`;
} else {
const hours = Math.floor(diff / 3600000);
lastActivityEl.textContent = `${hours} hour${hours !== 1 ? 's' : ''} ago`;
}
}
function getBaseUrl() {
const url = window.location;
const path = url.pathname;
// Extract everything before "/notifications/" in the path
const notificationsIndex = path.indexOf('/notifications/');
if (notificationsIndex !== -1) {
// Get everything up to the notifications segment
const basePath = path.substring(0, notificationsIndex + 1); // Keep trailing slash
return url.origin + basePath;
} else {
// Notifications not in path, return the origin + path
// Ensure path ends with trailing slash
const normalizedPath = path.endsWith('/') ? path : path + '/';
return url.origin + normalizedPath;
}
}
function updateExampleUrl() {
const topicInput = document.getElementById('topic-input');
const exampleUrl = document.getElementById('example-url');
if (!topicInput || !exampleUrl) return;
const topic = topicInput.value.trim() || 'your-topic';
exampleUrl.value = getBaseUrl() + `?room=example&poke=${topic}`;
}
function createNotificationPopup(notification) {
const notificationId = 'notification-' + Date.now();
const notificationElement = document.createElement('div');
notificationElement.className = 'notification-popup';
notificationElement.id = notificationId;
const timeAgo = window.notificationManager.getTimeAgo(notification.timestamp || Date.now());
notificationElement.innerHTML = `
<div class="notification-popup-header">
<h3 class="notification-popup-title">🔔 ${notification.title || 'VDO.Ninja Notification'}</h3>
<button class="notification-popup-close">&times;</button>
</div>
<div class="notification-popup-body">
<p>${notification.body || 'Someone joined your room'}</p>
<div class="time"><strong>${timeAgo}</strong> (${new Date(notification.timestamp || Date.now()).toLocaleTimeString()})</div>
</div>
<div class="notification-popup-actions">
<button class="secondary dismiss-btn">Dismiss</button>
<button class="open-url-btn">Open Room</button>
</div>
`;
document.body.appendChild(notificationElement);
notificationElement.querySelector('.notification-popup-close').addEventListener('click', () => {
if (document.body.contains(notificationElement)) {
document.body.removeChild(notificationElement);
}
});
notificationElement.querySelector('.dismiss-btn').addEventListener('click', () => {
if (document.body.contains(notificationElement)) {
document.body.removeChild(notificationElement);
}
});
notificationElement.querySelector('.open-url-btn').addEventListener('click', () => {
if (notification.url) {
window.open(notification.url, '_blank');
}
if (document.body.contains(notificationElement)) {
document.body.removeChild(notificationElement);
}
});
setTimeout(() => {
if (document.body.contains(notificationElement)) {
notificationElement.style.opacity = '0';
notificationElement.style.transition = 'opacity 0.5s ease';
setTimeout(() => {
if (document.body.contains(notificationElement)) {
document.body.removeChild(notificationElement);
}
}, 500);
}
}, 15000);
}
function createFlashEffect() {
const flashElement = document.createElement('div');
flashElement.className = 'notification-flash';
document.body.appendChild(flashElement);
setTimeout(() => {
if (document.body.contains(flashElement)) {
document.body.removeChild(flashElement);
}
}, 500);
document.body.classList.add('shake');
setTimeout(() => {
document.body.classList.remove('shake');
}, 500);
}
function clearNotificationHistory() {
window.notificationManager.clearNotificationHistory();
}
function updateNotificationHistoryUI() {
const historyEl = document.getElementById('notification-history');
if (!historyEl) return;
const notificationHistory = window.notificationManager.getNotificationHistory();
if (notificationHistory.length === 0) {
historyEl.innerHTML = '<p>No notifications yet.</p>';
return;
}
historyEl.innerHTML = '';
notificationHistory.forEach((notification, index) => {
const notificationEl = document.createElement('div');
notificationEl.className = 'notification-item';
if (notification.isNew) {
notificationEl.classList.add('new');
}
const timestamp = notification.timestamp || notification.receivedAt || Date.now();
const timeAgo = window.notificationManager.getTimeAgo(timestamp);
notificationEl.innerHTML = `
<div class="notification-icon">🔔</div>
<div class="content">
<h3>${notification.title || 'VDO.Ninja Notification'}</h3>
<p>${notification.body || 'Notification received'}</p>
<div class="time"><strong>${timeAgo}</strong> (${new Date(timestamp).toLocaleString()})</div>
</div>
<div class="actions">
<button class="open-url" data-index="${index}">Open</button>
</div>
`;
historyEl.appendChild(notificationEl);
});
document.querySelectorAll('.open-url').forEach(button => {
button.addEventListener('click', (e) => {
const index = parseInt(e.target.getAttribute('data-index'));
const notification = notificationHistory[index];
if (notification && notification.url) {
window.open(notification.url, '_blank');
}
});
});
}
function setupParseUrlButton() {
const parseUrlBtn = document.getElementById('parse-url-btn');
const vdoUrlInput = document.getElementById('vdo-url-input');
const parsedUrlOutput = document.getElementById('parsed-url-output');
const generatedTopic = document.getElementById('generated-topic');
const notificationUrl = document.getElementById('notification-url');
const copyUrlBtn = document.getElementById('copy-url-btn');
const useGeneratedTopicBtn = document.getElementById('use-generated-topic');
if (!parseUrlBtn || !vdoUrlInput) return;
parseUrlBtn.addEventListener('click', async () => {
const url = vdoUrlInput.value.trim();
if (!url) {
displayStatusMessage('⚠️ Please enter a VDO.Ninja URL', 'warning');
return;
}
try {
parseUrlBtn.textContent = 'Processing...';
parseUrlBtn.disabled = true;
const result = await parseVdoNinjaUrl(url);
if (result && result.topic) {
if (generatedTopic) generatedTopic.value = result.topic;
if (notificationUrl) notificationUrl.value = result.notificationUrl;
if (parsedUrlOutput) parsedUrlOutput.style.display = 'block';
if (copyUrlBtn) {
copyUrlBtn.addEventListener('click', () => {
notificationUrl.select();
document.execCommand('copy');
copyUrlBtn.textContent = 'Copied!';
setTimeout(() => {
copyUrlBtn.textContent = 'Copy URL';
}, 2000);
});
}
if (useGeneratedTopicBtn) {
useGeneratedTopicBtn.addEventListener('click', () => {
const topicInput = document.getElementById('topic-input');
if (topicInput) {
topicInput.value = result.topic;
const subscribeBtn = document.getElementById('subscribe-btn');
if (subscribeBtn) {
subscribeBtn.scrollIntoView({ behavior: 'smooth' });
subscribeBtn.classList.add('shake');
setTimeout(() => {
subscribeBtn.classList.remove('shake');
}, 1000);
}
}
});
}
displayStatusMessage('✅ Successfully parsed URL', 'active');
} else {
displayStatusMessage('⚠️ Could not generate a topic from this URL', 'warning');
}
} catch (error) {
console.error('Error parsing URL:', error);
displayStatusMessage('⚠️ Error parsing URL: ' + error.message, 'warning');
} finally {
parseUrlBtn.textContent = 'Create Notification Topic';
parseUrlBtn.disabled = false;
}
});
}
function initNotificationBadge() {
const historyTab = document.querySelector('.tab[data-tab="history"]');
if (!historyTab) return;
let badge = document.getElementById('notification-badge');
if (!badge) {
badge = document.createElement('span');
badge.id = 'notification-badge';
badge.style.display = 'none';
badge.style.backgroundColor = '#f44336';
badge.style.color = 'white';
badge.style.borderRadius = '50%';
badge.style.padding = '2px 6px';
badge.style.fontSize = '11px';
badge.style.marginLeft = '5px';
badge.style.fontWeight = 'bold';
historyTab.appendChild(badge);
}
updateNotificationBadge();
}
function updateNotificationBadge() {
const badge = document.getElementById('notification-badge');
if (!badge) return;
const notificationHistory = window.notificationManager.getNotificationHistory();
const unreadCount = notificationHistory.filter(n => n.isNew).length;
if (unreadCount > 0) {
badge.textContent = unreadCount;
badge.style.display = 'inline-block';
} else {
badge.style.display = 'none';
}
}
function initTabSystem() {
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const tabId = tab.getAttribute('data-tab');
const contentEl = document.getElementById(`${tabId}-tab`);
if (contentEl) {
contentEl.classList.add('active');
}
if (tabId === 'history') {
const notificationHistory = window.notificationManager.getNotificationHistory();
notificationHistory.forEach(n => n.isNew = false);
localStorage.setItem('notificationHistory', JSON.stringify(notificationHistory));
updateNotificationHistoryUI();
updateNotificationBadge();
}
});
});
}
document.addEventListener('visibilitychange', () => {
const topic = localStorage.getItem('notifyTopic');
if (!topic) return;
if (document.visibilityState === 'visible') {
window.notificationManager.checkStatus();
}
});
window.addEventListener('online', () => {
const topic = localStorage.getItem('notifyTopic');
if (topic) {
console.log('Back online, reconnecting to notification service');
window.notificationManager.startSubscription(topic);
}
});