feat: add dark mode support for meeting join and start processes
Build & Push Docker Image / build (push) Successful in 4m14s
Build & Push Docker Image / build (push) Successful in 4m14s
This commit is contained in:
@@ -130,7 +130,7 @@ export async function createMeeting(room, logoutURL, loginURL = null, presentati
|
|||||||
return apiCall('create', params, xmlBody);
|
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 { moderatorPW, attendeePW } = getRoomPasswords(uid);
|
||||||
const params = {
|
const params = {
|
||||||
meetingID: uid,
|
meetingID: uid,
|
||||||
@@ -141,6 +141,11 @@ export async function joinMeeting(uid, name, isModerator = false, avatarURL = nu
|
|||||||
if (avatarURL) {
|
if (avatarURL) {
|
||||||
params.avatarURL = 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);
|
return buildUrl('join', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -519,7 +519,7 @@ router.post('/:uid/start', authenticateToken, async (req, res) => {
|
|||||||
await createMeeting(room, baseUrl, loginURL, presentationUrl, analyticsCallbackURL, owner?.language || 'en');
|
await createMeeting(room, baseUrl, loginURL, presentationUrl, analyticsCallbackURL, owner?.language || 'en');
|
||||||
const avatarURL = getUserAvatarURL(req, req.user);
|
const avatarURL = getUserAvatarURL(req, req.user);
|
||||||
const displayName = req.user.display_name || req.user.name;
|
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 });
|
res.json({ joinUrl });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.rooms.error(`Start meeting error: ${err.message}`);
|
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 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 isModerator = isOwner || !!isShared || room.all_join_moderator;
|
||||||
const avatarURL = getUserAvatarURL(req, req.user);
|
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 });
|
res.json({ joinUrl });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.rooms.error(`Join meeting error: ${err.message}`);
|
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)
|
// POST /api/rooms/:uid/guest-join - Join meeting as guest (no auth needed)
|
||||||
router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
|
router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
|
||||||
try {
|
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) {
|
if (!name || name.trim().length === 0) {
|
||||||
return res.status(400).json({ error: 'Name is required' });
|
return res.status(400).json({ error: 'Name is required' });
|
||||||
@@ -691,7 +691,7 @@ router.post('/:uid/guest-join', guestJoinLimiter, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`;
|
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 });
|
res.json({ joinUrl });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.rooms.error(`Guest join error: ${err.message}`);
|
log.rooms.error(`Guest join error: ${err.message}`);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Users, Play, Trash2, Radio, Loader2, Share2, Copy, Link } from 'lucide-
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { isCurrentThemeDark } from '../themes';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
|
|
||||||
@@ -107,11 +108,12 @@ export default function RoomCard({ room, onDelete }) {
|
|||||||
setStarting(true);
|
setStarting(true);
|
||||||
try {
|
try {
|
||||||
if (status.running) {
|
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);
|
const res = await api.post(`/rooms/${room.uid}/join`, data);
|
||||||
if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank');
|
if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank');
|
||||||
} else {
|
} 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');
|
if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank');
|
||||||
toast.success(t('room.meetingStarted'));
|
toast.success(t('room.meetingStarted'));
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -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 { Video, User, Lock, Shield, ArrowRight, Loader2, Users, Radio, AlertCircle, FileText, Clock, X } from 'lucide-react';
|
||||||
import BrandLogo from '../components/BrandLogo';
|
import BrandLogo from '../components/BrandLogo';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { isCurrentThemeDark } from '../themes';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
@@ -35,6 +36,7 @@ export default function GuestJoin() {
|
|||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
access_code: accessCode || undefined,
|
access_code: accessCode || undefined,
|
||||||
moderator_code: moderatorCode || undefined,
|
moderator_code: moderatorCode || undefined,
|
||||||
|
dark_mode: isCurrentThemeDark(),
|
||||||
};
|
};
|
||||||
// If logged in, send avatar data
|
// If logged in, send avatar data
|
||||||
if (isLoggedIn && user) {
|
if (isLoggedIn && user) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Modal from '../components/Modal';
|
import Modal from '../components/Modal';
|
||||||
import api from '../services/api';
|
import api from '../services/api';
|
||||||
|
import { isCurrentThemeDark } from '../themes';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
import RecordingList from '../components/RecordingList';
|
import RecordingList from '../components/RecordingList';
|
||||||
@@ -124,7 +125,7 @@ export default function RoomDetail() {
|
|||||||
toast.success(t('room.meetingStarted'));
|
toast.success(t('room.meetingStarted'));
|
||||||
setWaitingToJoin(false);
|
setWaitingToJoin(false);
|
||||||
setActionLoading('join');
|
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'); })
|
.then(res => { if (res.data.joinUrl) window.open(res.data.joinUrl, '_blank'); })
|
||||||
.catch(err => toast.error(err.response?.data?.error || t('room.joinFailed')))
|
.catch(err => toast.error(err.response?.data?.error || t('room.joinFailed')))
|
||||||
.finally(() => setActionLoading(null));
|
.finally(() => setActionLoading(null));
|
||||||
@@ -135,7 +136,7 @@ export default function RoomDetail() {
|
|||||||
const handleStart = async () => {
|
const handleStart = async () => {
|
||||||
setActionLoading('start');
|
setActionLoading('start');
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`/rooms/${uid}/start`);
|
const res = await api.post(`/rooms/${uid}/start`, { dark_mode: isCurrentThemeDark() });
|
||||||
if (res.data.joinUrl) {
|
if (res.data.joinUrl) {
|
||||||
window.open(res.data.joinUrl, '_blank');
|
window.open(res.data.joinUrl, '_blank');
|
||||||
}
|
}
|
||||||
@@ -157,7 +158,8 @@ export default function RoomDetail() {
|
|||||||
setWaitingToJoin(false);
|
setWaitingToJoin(false);
|
||||||
setActionLoading('join');
|
setActionLoading('join');
|
||||||
try {
|
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);
|
const res = await api.post(`/rooms/${uid}/join`, data);
|
||||||
if (res.data.joinUrl) {
|
if (res.data.joinUrl) {
|
||||||
window.open(res.data.joinUrl, '_blank');
|
window.open(res.data.joinUrl, '_blank');
|
||||||
|
|||||||
@@ -173,6 +173,18 @@ export function getThemeById(id) {
|
|||||||
return themes.find(t => t.id === id) || themes[1]; // default to dark
|
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 <html data-theme> 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() {
|
export function getThemeGroups() {
|
||||||
const groups = {};
|
const groups = {};
|
||||||
themes.forEach(theme => {
|
themes.forEach(theme => {
|
||||||
|
|||||||
Reference in New Issue
Block a user