Skip to content

Commit 818f5d2

Browse files
committed
Fix host moderation controls and desktop viewer signal auth
1 parent 570cd13 commit 818f5d2

14 files changed

Lines changed: 590 additions & 82 deletions

File tree

apps/desktop/src/renderer/components/ParticipantList.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,7 @@ describe('ParticipantList', () => {
508508
expect(screen.queryByTitle('Mute participant')).not.toBeInTheDocument();
509509
});
510510

511-
it('does not show mute button for participant with null user_id', () => {
511+
it('shows mute button for participant with null user_id using participant id fallback', () => {
512512
const participants = [
513513
createMockParticipant({
514514
id: 'p-1',
@@ -530,7 +530,7 @@ describe('ParticipantList', () => {
530530
/>
531531
);
532532

533-
expect(screen.queryByTitle('Mute participant')).not.toBeInTheDocument();
533+
expect(screen.getByTitle('Mute participant')).toBeInTheDocument();
534534
});
535535
});
536536
});

apps/desktop/src/renderer/components/ParticipantList.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,13 @@ export function ParticipantList({
188188
<div className="flex gap-1">
189189
{/* Mute/Unmute button */}
190190
{onMuteParticipant &&
191-
participant.user_id &&
192191
(() => {
193-
const userId = participant.user_id;
194-
const isMutedNow = mutedParticipants?.has(userId) ?? false;
192+
const targetId = participant.user_id ?? participant.id;
193+
const isMutedNow = mutedParticipants?.has(targetId) ?? false;
195194
return (
196195
<button
197196
onClick={() => {
198-
onMuteParticipant(userId, !isMutedNow);
197+
onMuteParticipant(targetId, !isMutedNow);
199198
}}
200199
disabled={loadingAction !== null}
201200
className={`rounded p-1.5 transition-colors disabled:opacity-50 ${

apps/desktop/src/renderer/components/capture/CapturePreview.tsx

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ export function CapturePreview({
202202
stopHosting,
203203
publishStream: hostPublishStream,
204204
unpublishStream: hostUnpublishStream,
205+
grantControl,
206+
revokeControl,
207+
kickViewer,
205208
muteViewer,
206209
micEnabled: hostMicEnabled,
207210
hasMic: hostHasMic,
@@ -446,6 +449,24 @@ export function CapturePreview({
446449
return headers;
447450
}, []);
448451

452+
const resolveViewerTargetId = useCallback(
453+
(participantId: string): string | null => {
454+
const participant = participants.find((p) => p.id === participantId);
455+
if (!participant) return null;
456+
457+
const candidates = [participant.user_id, participant.id].filter(
458+
(value): value is string => typeof value === 'string' && value.length > 0
459+
);
460+
461+
for (const candidate of candidates) {
462+
if (hostedViewers.has(candidate)) return candidate;
463+
}
464+
465+
return candidates[0] ?? null;
466+
},
467+
[participants, hostedViewers]
468+
);
469+
449470
const handleGrantControl = useCallback(
450471
async (participantId: string) => {
451472
if (!session) return;
@@ -460,12 +481,22 @@ export function CapturePreview({
460481
);
461482
if (!response.ok) {
462483
console.error('Failed to grant control');
484+
return;
485+
}
486+
487+
const viewerId = resolveViewerTargetId(participantId);
488+
if (viewerId) {
489+
grantControl(viewerId);
490+
} else {
491+
console.warn('[CapturePreview] Could not resolve viewer target for grant control', {
492+
participantId,
493+
});
463494
}
464495
} catch (err) {
465496
console.error('Error granting control:', err);
466497
}
467498
},
468-
[session, getAuthHeaders]
499+
[session, getAuthHeaders, resolveViewerTargetId, grantControl]
469500
);
470501

471502
const handleRevokeControl = useCallback(
@@ -482,12 +513,22 @@ export function CapturePreview({
482513
);
483514
if (!response.ok) {
484515
console.error('Failed to revoke control');
516+
return;
517+
}
518+
519+
const viewerId = resolveViewerTargetId(participantId);
520+
if (viewerId) {
521+
revokeControl(viewerId);
522+
} else {
523+
console.warn('[CapturePreview] Could not resolve viewer target for revoke control', {
524+
participantId,
525+
});
485526
}
486527
} catch (err) {
487528
console.error('Error revoking control:', err);
488529
}
489530
},
490-
[session, getAuthHeaders]
531+
[session, getAuthHeaders, resolveViewerTargetId, revokeControl]
491532
);
492533

493534
const handleKickParticipant = useCallback(
@@ -503,12 +544,22 @@ export function CapturePreview({
503544
);
504545
if (!response.ok) {
505546
console.error('Failed to kick participant');
547+
return;
548+
}
549+
550+
const viewerId = resolveViewerTargetId(participantId);
551+
if (viewerId) {
552+
kickViewer(viewerId);
553+
} else {
554+
console.warn('[CapturePreview] Could not resolve viewer target for kick', {
555+
participantId,
556+
});
506557
}
507558
} catch (err) {
508559
console.error('Error kicking participant:', err);
509560
}
510561
},
511-
[session, getAuthHeaders]
562+
[session, getAuthHeaders, resolveViewerTargetId, kickViewer]
512563
);
513564

514565
const handleStartRecording = useCallback(async () => {
@@ -556,14 +607,14 @@ export function CapturePreview({
556607
}, [isPaused, pauseRecording, resumeRecording]);
557608

558609
const handleMuteParticipant = useCallback(
559-
(participantUserId: string, muted: boolean) => {
560-
muteViewer(participantUserId, muted);
610+
(participantIdentity: string, muted: boolean) => {
611+
muteViewer(participantIdentity, muted);
561612
setMutedParticipants((prev) => {
562613
const next = new Set(prev);
563614
if (muted) {
564-
next.add(participantUserId);
615+
next.add(participantIdentity);
565616
} else {
566-
next.delete(participantUserId);
617+
next.delete(participantIdentity);
567618
}
568619
return next;
569620
});
@@ -959,6 +1010,22 @@ export function CapturePreview({
9591010
<ChatPanel
9601011
sessionId={session.id}
9611012
currentUserId={currentUserId}
1013+
isHost={true}
1014+
mutedParticipants={mutedParticipants}
1015+
onGrantControl={(participant) => {
1016+
void handleGrantControl(participant.id);
1017+
}}
1018+
onRevokeControl={(participant) => {
1019+
void handleRevokeControl(participant.id);
1020+
}}
1021+
onKickParticipant={(participant) => {
1022+
void handleKickParticipant(participant.id);
1023+
}}
1024+
onMuteParticipant={(participant, muted) => {
1025+
const targetId =
1026+
resolveViewerTargetId(participant.id) ?? participant.user_id ?? participant.id;
1027+
handleMuteParticipant(targetId, muted);
1028+
}}
9621029
isCollapsed={!showChat}
9631030
onToggleCollapse={() => {
9641031
setShowChat(!showChat);

apps/desktop/src/renderer/components/chat/ChatPanel.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,20 @@ import { ParticipantList } from './ParticipantList';
1616
import { useChat } from './useChat';
1717
import { useParticipants } from './useParticipants';
1818
import type { ChatMessage as ChatMessageType } from '@pairux/shared-types';
19+
import type { SessionParticipant } from '@pairux/shared-types';
1920

2021
interface ChatPanelProps {
2122
sessionId: string;
2223
currentUserId?: string | null;
2324
participantId?: string;
2425
isCollapsed?: boolean;
2526
onToggleCollapse?: () => void;
27+
isHost?: boolean;
28+
mutedParticipants?: Set<string>;
29+
onGrantControl?: (participant: SessionParticipant) => void;
30+
onRevokeControl?: (participant: SessionParticipant) => void;
31+
onKickParticipant?: (participant: SessionParticipant) => void;
32+
onMuteParticipant?: (participant: SessionParticipant, muted: boolean) => void;
2633
}
2734

2835
const MAX_MESSAGE_LENGTH = 500;
@@ -33,6 +40,12 @@ export function ChatPanel({
3340
participantId,
3441
isCollapsed: controlledCollapsed,
3542
onToggleCollapse,
43+
isHost = false,
44+
mutedParticipants,
45+
onGrantControl,
46+
onRevokeControl,
47+
onKickParticipant,
48+
onMuteParticipant,
3649
}: ChatPanelProps) {
3750
const [internalCollapsed, setInternalCollapsed] = useState(false);
3851
const isCollapsed = controlledCollapsed ?? internalCollapsed;
@@ -174,6 +187,12 @@ export function ChatPanel({
174187
currentUserId={currentUserId}
175188
currentParticipantId={participantId}
176189
isLoading={participantsLoading}
190+
isHost={isHost}
191+
mutedParticipants={mutedParticipants}
192+
onGrantControl={onGrantControl}
193+
onRevokeControl={onRevokeControl}
194+
onKickParticipant={onKickParticipant}
195+
onMuteParticipant={onMuteParticipant}
177196
/>
178197

179198
{/* Message list */}

0 commit comments

Comments
 (0)