From 4621010bd7b0c5f55bbfaf8b104ae7b7dec1a9a8 Mon Sep 17 00:00:00 2001 From: Michelle Date: Thu, 11 Jun 2026 09:27:34 +0200 Subject: [PATCH] feat: add dark mode support for meeting join and start processes --- server/config/bbb.js | 7 ++++++- server/routes/rooms.js | 8 ++++---- src/components/RoomCard.jsx | 6 ++++-- src/pages/GuestJoin.jsx | 2 ++ src/pages/RoomDetail.jsx | 8 +++++--- src/themes/index.js | 12 ++++++++++++ 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/server/config/bbb.js b/server/config/bbb.js index 80b1850..c787761 100644 --- a/server/config/bbb.js +++ b/server/config/bbb.js @@ -130,7 +130,7 @@ export async function createMeeting(room, logoutURL, loginURL = null, presentati return apiCall('create', params, xmlBody); } -export async function joinMeeting(uid, name, isModerator = false, avatarURL = null) { +export async function joinMeeting(uid, name, isModerator = false, avatarURL = null, darkMode = false) { const { moderatorPW, attendeePW } = getRoomPasswords(uid); const params = { meetingID: uid, @@ -141,6 +141,11 @@ export async function joinMeeting(uid, name, isModerator = false, avatarURL = nu if (avatarURL) { params.avatarURL = avatarURL; } + if (darkMode) { + // Per-user userdata override: start the client in dark theme for users who + // have a dark theme active in Redlight. Read by BBB as bbb_prefer_dark_theme. + params['userdata-bbb_prefer_dark_theme'] = 'true'; + } return buildUrl('join', params); } diff --git a/server/routes/rooms.js b/server/routes/rooms.js index 18874bd..c341049 100644 --- a/server/routes/rooms.js +++ b/server/routes/rooms.js @@ -519,7 +519,7 @@ router.post('/:uid/start', authenticateToken, async (req, res) => { await createMeeting(room, baseUrl, loginURL, presentationUrl, analyticsCallbackURL, owner?.language || 'en'); const avatarURL = getUserAvatarURL(req, req.user); const displayName = req.user.display_name || req.user.name; - const joinUrl = await joinMeeting(room.uid, displayName, true, avatarURL); + const joinUrl = await joinMeeting(room.uid, displayName, true, avatarURL, !!req.body.dark_mode); res.json({ joinUrl }); } catch (err) { log.rooms.error(`Start meeting error: ${err.message}`); @@ -553,7 +553,7 @@ router.post('/:uid/join', authenticateToken, async (req, res) => { const isShared = !isOwner && await db.get('SELECT id FROM room_shares WHERE room_id = ? AND user_id = ?', [room.id, req.user.id]); const isModerator = isOwner || !!isShared || room.all_join_moderator; const avatarURL = getUserAvatarURL(req, req.user); - const joinUrl = await joinMeeting(room.uid, req.user.display_name || req.user.name, isModerator, avatarURL); + const joinUrl = await joinMeeting(room.uid, req.user.display_name || req.user.name, isModerator, avatarURL, !!req.body.dark_mode); res.json({ joinUrl }); } catch (err) { log.rooms.error(`Join meeting error: ${err.message}`); @@ -629,7 +629,7 @@ router.get('/:uid/public', async (req, res) => { // POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed) router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => { try { - const { name, access_code, moderator_code, avatar_image, avatar_color } = req.body; + const { name, access_code, moderator_code, avatar_image, avatar_color, dark_mode } = req.body; if (!name || name.trim().length === 0) { return res.status(400).json({ error: 'Name is required' }); @@ -691,7 +691,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => { } else { guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`; } - const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL); + const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL, !!dark_mode); res.json({ joinUrl }); } catch (err) { log.rooms.error(`Guest join error: ${err.message}`); diff --git a/src/components/RoomCard.jsx b/src/components/RoomCard.jsx index 1b14ee2..d990ff3 100644 --- a/src/components/RoomCard.jsx +++ b/src/components/RoomCard.jsx @@ -2,6 +2,7 @@ import { Users, Play, Trash2, Radio, Loader2, Share2, Copy, Link } from 'lucide- import { useNavigate } from 'react-router-dom'; import { useState, useEffect, useRef } from 'react'; import api from '../services/api'; +import { isCurrentThemeDark } from '../themes'; import { useLanguage } from '../contexts/LanguageContext'; import toast from 'react-hot-toast'; @@ -107,11 +108,12 @@ export default function RoomCard({ room, onDelete }) { setStarting(true); try { if (status.running) { - const data = room.access_code ? { access_code: prompt(t('room.enterAccessCode')) } : {}; + const data = { dark_mode: isCurrentThemeDark() }; + if (room.access_code) data.access_code = prompt(t('room.enterAccessCode')); const res = await api.post(`/rooms/${room.uid}/join`, data); if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank'); } else { - const res = await api.post(`/rooms/${room.uid}/start`); + const res = await api.post(`/rooms/${room.uid}/start`, { dark_mode: isCurrentThemeDark() }); if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank'); toast.success(t('room.meetingStarted')); setTimeout(() => { diff --git a/src/pages/GuestJoin.jsx b/src/pages/GuestJoin.jsx index 5d9968a..1c266a0 100644 --- a/src/pages/GuestJoin.jsx +++ b/src/pages/GuestJoin.jsx @@ -3,6 +3,7 @@ import { useParams, Link, useSearchParams, useNavigate } from 'react-router-dom' import { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle, FileText, Clock, X } from 'lucide-react'; import BrandLogo from '../components/BrandLogo'; import api from '../services/api'; +import { isCurrentThemeDark } from '../themes'; import toast from 'react-hot-toast'; import { useLanguage } from '../contexts/LanguageContext'; import { useAuth } from '../contexts/AuthContext'; @@ -35,6 +36,7 @@ export default function GuestJoin() { name: name.trim(), access_code: accessCode || undefined, moderator_code: moderatorCode || undefined, + dark_mode: isCurrentThemeDark(), }; // If logged in, send avatar data if (isLoggedIn && user) { diff --git a/src/pages/RoomDetail.jsx b/src/pages/RoomDetail.jsx index d1e7fb3..e08e604 100644 --- a/src/pages/RoomDetail.jsx +++ b/src/pages/RoomDetail.jsx @@ -8,6 +8,7 @@ import { } from 'lucide-react'; import Modal from '../components/Modal'; import api from '../services/api'; +import { isCurrentThemeDark } from '../themes'; import { useAuth } from '../contexts/AuthContext'; import { useLanguage } from '../contexts/LanguageContext'; import RecordingList from '../components/RecordingList'; @@ -124,7 +125,7 @@ export default function RoomDetail() { toast.success(t('room.meetingStarted')); setWaitingToJoin(false); setActionLoading('join'); - api.post(`/rooms/${uid}/join`, {}) + api.post(`/rooms/${uid}/join`, { dark_mode: isCurrentThemeDark() }) .then(res => { if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank'); }) .catch(err => toast.error(err.response?.data?.error || t('room.joinFailed'))) .finally(() => setActionLoading(null)); @@ -135,7 +136,7 @@ export default function RoomDetail() { const handleStart = async () => { setActionLoading('start'); try { - const res = await api.post(`/rooms/${uid}/start`); + const res = await api.post(`/rooms/${uid}/start`, { dark_mode: isCurrentThemeDark() }); if (res.data.joinUrl) { window.open(res.data.joinUrl, '_blank'); } @@ -157,7 +158,8 @@ export default function RoomDetail() { setWaitingToJoin(false); setActionLoading('join'); try { - const data = room.access_code ? { access_code: prompt(t('room.enterAccessCode')) } : {}; + const data = { dark_mode: isCurrentThemeDark() }; + if (room.access_code) data.access_code = prompt(t('room.enterAccessCode')); const res = await api.post(`/rooms/${uid}/join`, data); if (res.data.joinUrl) { window.open(res.data.joinUrl, '_blank'); diff --git a/src/themes/index.js b/src/themes/index.js index fd7cd89..9b85a8b 100644 --- a/src/themes/index.js +++ b/src/themes/index.js @@ -173,6 +173,18 @@ export function getThemeById(id) { return themes.find(t => t.id === id) || themes[1]; // default to dark } +// Whether the currently applied theme is a dark one. Reads the resolved theme +// from the attribute (set by ThemeContext) with a localStorage +// fallback, so it works without React context — handy for one-off API payloads +// such as the BBB join call that forwards dark mode to the meeting. +export function isCurrentThemeDark() { + const id = (typeof document !== 'undefined' + && document.documentElement.getAttribute('data-theme')) + || (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) + || 'dark'; + return getThemeById(id).type === 'dark'; +} + export function getThemeGroups() { const groups = {}; themes.forEach(theme => {