diff --git a/server/routes/federation.js b/server/routes/federation.js index ff80809..12c9857 100644 --- a/server/routes/federation.js +++ b/server/routes/federation.js @@ -83,8 +83,11 @@ router.post('/invite', authenticateToken, async (req, res) => { } // Build guest join URL for the remote user + // If the room has an access code, embed it so the recipient can join without manual entry const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`; - const joinUrl = `${baseUrl}/join/${room.uid}`; + const joinUrl = room.access_code + ? `${baseUrl}/join/${room.uid}?ac=${encodeURIComponent(room.access_code)}` + : `${baseUrl}/join/${room.uid}`; // Build invitation payload const inviteId = uuidv4(); diff --git a/server/routes/notifications.js b/server/routes/notifications.js index 6bb7464..4bc7afd 100644 --- a/server/routes/notifications.js +++ b/server/routes/notifications.js @@ -45,4 +45,30 @@ router.post('/:id/read', authenticateToken, async (req, res) => { } }); +// DELETE /api/notifications/all — Delete all notifications for current user +// NOTE: Declared before /:id to avoid routing collision +router.delete('/all', authenticateToken, async (req, res) => { + try { + const db = getDb(); + await db.run('DELETE FROM notifications WHERE user_id = ?', [req.user.id]); + res.json({ success: true }); + } catch { + res.status(500).json({ error: 'Failed to delete notifications' }); + } +}); + +// DELETE /api/notifications/:id — Delete a single notification +router.delete('/:id', authenticateToken, async (req, res) => { + try { + const db = getDb(); + await db.run( + 'DELETE FROM notifications WHERE id = ? AND user_id = ?', + [req.params.id, req.user.id], + ); + res.json({ success: true }); + } catch { + res.status(500).json({ error: 'Failed to delete notification' }); + } +}); + export default router; diff --git a/src/components/NotificationBell.jsx b/src/components/NotificationBell.jsx index 440a016..6a03e93 100644 --- a/src/components/NotificationBell.jsx +++ b/src/components/NotificationBell.jsx @@ -1,5 +1,5 @@ import { useRef, useState, useEffect } from 'react'; -import { Bell, BellOff, CheckCheck, ExternalLink } from 'lucide-react'; +import { Bell, BellOff, CheckCheck, ExternalLink, Trash2, X } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; import { useNotifications } from '../contexts/NotificationContext'; import { useLanguage } from '../contexts/LanguageContext'; @@ -48,7 +48,7 @@ function notificationSubtitle(n, t, lang) { } export default function NotificationBell() { - const { notifications, unreadCount, markRead, markAllRead } = useNotifications(); + const { notifications, unreadCount, markRead, markAllRead, deleteNotification, clearAll } = useNotifications(); const { t, language } = useLanguage(); const navigate = useNavigate(); const [open, setOpen] = useState(false); @@ -70,6 +70,11 @@ export default function NotificationBell() { setOpen(false); }; + const handleDelete = async (e, id) => { + e.stopPropagation(); + await deleteNotification(id); + }; + const recent = notifications.slice(0, 20); return ( @@ -102,16 +107,28 @@ export default function NotificationBell() { )} - {unreadCount > 0 && ( - - )} +
+ {unreadCount > 0 && ( + + )} + {notifications.length > 0 && ( + + )} +
{/* List */} @@ -127,7 +144,7 @@ export default function NotificationBell() {
  • handleNotificationClick(n)} - className={`flex items-start gap-3 px-4 py-3 cursor-pointer transition-colors border-b border-th-border/50 last:border-0 + className={`group flex items-start gap-3 px-4 py-3 cursor-pointer transition-colors border-b border-th-border/50 last:border-0 ${n.read ? 'hover:bg-th-hover' : 'bg-th-accent/5 hover:bg-th-accent/10'}`} > {/* Icon */} @@ -146,7 +163,7 @@ export default function NotificationBell() {

    - {/* Unread dot + link indicator */} + {/* Right side: unread dot, link icon, delete button */}
    {!n.read && ( @@ -154,6 +171,13 @@ export default function NotificationBell() { {n.link && ( )} +
  • ))} diff --git a/src/contexts/NotificationContext.jsx b/src/contexts/NotificationContext.jsx index f450d50..643ed98 100644 --- a/src/contexts/NotificationContext.jsx +++ b/src/contexts/NotificationContext.jsx @@ -71,8 +71,29 @@ export function NotificationProvider({ children }) { } catch { /* silent */ } }; + const deleteNotification = async (id) => { + try { + await api.delete(`/notifications/${id}`); + setNotifications(prev => { + const removed = prev.find(n => n.id === id); + if (removed && !removed.read) setUnreadCount(c => Math.max(0, c - 1)); + return prev.filter(n => n.id !== id); + }); + seenIds.current.delete(id); + } catch { /* silent */ } + }; + + const clearAll = async () => { + try { + await api.delete('/notifications/all'); + setNotifications([]); + setUnreadCount(0); + seenIds.current = new Set(); + } catch { /* silent */ } + }; + return ( - + {children} ); diff --git a/src/i18n/de.json b/src/i18n/de.json index 8d21a7b..71a23af 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -370,7 +370,9 @@ }, "notifications": { "bell": "Benachrichtigungen", - "markAllRead": "Alle als gelesen markieren", + "markAllRead": "Alle gelesen", + "clearAll": "Alle löschen", + "delete": "Löschen", "noNotifications": "Keine Benachrichtigungen", "roomShareAdded": "Raum wurde mit dir geteilt", "roomShareRemoved": "Raumzugriff wurde entfernt", diff --git a/src/i18n/en.json b/src/i18n/en.json index d963206..df6833c 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -370,7 +370,9 @@ }, "notifications": { "bell": "Notifications", - "markAllRead": "Mark all as read", + "markAllRead": "Mark all read", + "clearAll": "Clear all", + "delete": "Delete", "noNotifications": "No notifications yet", "roomShareAdded": "Room shared with you", "roomShareRemoved": "Room access removed", diff --git a/src/pages/GuestJoin.jsx b/src/pages/GuestJoin.jsx index 70e6078..400a5c4 100644 --- a/src/pages/GuestJoin.jsx +++ b/src/pages/GuestJoin.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useParams, Link } from 'react-router-dom'; +import { useParams, Link, useSearchParams } from 'react-router-dom'; import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle, FileText } from 'lucide-react'; import BrandLogo from '../components/BrandLogo'; import api from '../services/api'; @@ -10,6 +10,7 @@ import { useBranding } from '../contexts/BrandingContext'; export default function GuestJoin() { const { uid } = useParams(); + const [searchParams] = useSearchParams(); const { t } = useLanguage(); const { user } = useAuth(); const { imprintUrl, privacyUrl } = useBranding(); @@ -19,7 +20,7 @@ export default function GuestJoin() { const [error, setError] = useState(null); const [joining, setJoining] = useState(false); const [name, setName] = useState(user?.name || ''); - const [accessCode, setAccessCode] = useState(''); + const [accessCode, setAccessCode] = useState(searchParams.get('ac') || ''); const [moderatorCode, setModeratorCode] = useState(''); const [status, setStatus] = useState({ running: false }); const [recordingConsent, setRecordingConsent] = useState(false);