Skip to content

Commit 81d45bc

Browse files
feat(vc-poc): move LiveKit chat inside conference, update toggle, and fix room join button
- Chat panel now opens on the right inside the call surface and starts closed. - ActionToggleChat uses a chat icon and shows an unread-messages badge. - MediaCallRoomSection accepts children and renders the chat panel inside the call. - LiveKitConferenceEmbeddedPage passes ConferenceChat as children and unreadCount. - VideoConfManager/VideoConferenceBlock pass providerName/rid so the /conference window can open synchronously before the join request, avoiding popup blocking. - Update VideoConfContext and providers to support optional providerName/rid for joinCall. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent bfbb46f commit 81d45bc

8 files changed

Lines changed: 105 additions & 52 deletions

File tree

apps/meteor/client/lib/VideoConfManager.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,9 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
184184
case 'direct':
185185
return this.callUser({ uid: data.calleeId, rid: roomId, callId: data.callId });
186186
case 'videoconference':
187-
return this.joinCall(data.callId);
187+
return this.joinCall(data.callId, data.providerName, data.rid);
188188
case 'livechat':
189-
return this.joinCall(data.callId);
189+
return this.joinCall(data.callId, data.providerName, undefined);
190190
}
191191
}
192192

@@ -356,7 +356,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
356356
this._logLevel = Math.max(0, Math.min(level, 2));
357357
}
358358

359-
public async joinCall(callId: string): Promise<void> {
359+
public async joinCall(callId: string, providerName?: string, rid?: string): Promise<void> {
360360
this.debugLog(`[VideoConf] Joining call ${callId}.`);
361361

362362
if (this.incomingDirectCalls.has(callId)) {
@@ -376,7 +376,28 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
376376
},
377377
};
378378

379-
const { url, providerName, rid } = await sdk.rest.post('/v1/video-conference.join', params).catch((e) => {
379+
// LiveKit calls are rendered in a dedicated /conference/:callId window.
380+
// Open that window synchronously while the click user-gesture is still
381+
// active; the conference page itself validates the join on the server.
382+
if (providerName === 'livekit') {
383+
this.debugLog(`[VideoConf] Opening embedded ${providerName} call ${callId} in room ${rid}.`);
384+
this.emit('call/joinEmbedded', {
385+
callId,
386+
rid: rid || '',
387+
providerName,
388+
// Forward the same prefs the server received so the
389+
// embedded provider can publish/skip mic + camera tracks
390+
// according to the preflight popup's toggle state.
391+
preferences: { ...this._preferences },
392+
});
393+
return;
394+
}
395+
396+
const {
397+
url,
398+
providerName: responseProviderName,
399+
rid: responseRid,
400+
} = await sdk.rest.post('/v1/video-conference.join', params).catch((e) => {
380401
console.error(`[VideoConf] Failed to join call ${callId}`, e);
381402
this.emitError(e?.xhr?.responseJSON?.error || 'error-videoconf-join-failed');
382403

@@ -387,12 +408,12 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
387408
// url + a rid — the call is mounted inline by their React provider
388409
// instead of opened in a popup. Dispatch to that provider via a
389410
// distinct event so the URL-handling code path doesn't run.
390-
if (!url && providerName && rid) {
391-
this.debugLog(`[VideoConf] Joining embedded ${providerName} call ${callId} in room ${rid}.`);
411+
if (!url && responseProviderName && responseRid) {
412+
this.debugLog(`[VideoConf] Joining embedded ${responseProviderName} call ${callId} in room ${responseRid}.`);
392413
this.emit('call/joinEmbedded', {
393414
callId,
394-
rid,
395-
providerName,
415+
rid: responseRid,
416+
providerName: responseProviderName,
396417
// Forward the same prefs the server received so the
397418
// embedded provider can publish/skip mic + camera tracks
398419
// according to the preflight popup's toggle state.
@@ -407,7 +428,7 @@ export const VideoConfManager = new (class VideoConfManager extends Emitter<Vide
407428
}
408429

409430
this.debugLog(`[VideoConf] Opening ${url}.`);
410-
this.emit('call/join', { url, callId, providerName });
431+
this.emit('call/join', { url, callId, providerName: responseProviderName });
411432
}
412433

413434
public abortCall(): void {

apps/meteor/client/providers/VideoConfProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const VideoConfContextProvider = ({ children }: VideoConfContextProviderProps) =
7979
dismissOutgoing: () => setOutgoing(undefined),
8080
startCall: (rid, confTitle) => void VideoConfManager.startCall(rid, confTitle),
8181
acceptCall: (callId) => VideoConfManager.acceptIncomingCall(callId),
82-
joinCall: (callId) => void VideoConfManager.joinCall(callId),
82+
joinCall: (callId, providerName, rid) => void VideoConfManager.joinCall(callId, providerName, rid),
8383
dismissCall: (callId) => VideoConfManager.dismissIncomingCall(callId),
8484
rejectIncomingCall: (callId) => VideoConfManager.rejectIncomingCall(callId),
8585
abortCall: () => VideoConfManager.abortCall(),

apps/meteor/client/views/conference/LiveKitConferenceEmbeddedPage.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import type { CallPreferences } from '@rocket.chat/core-typings';
22
import { Box } from '@rocket.chat/fuselage';
33
import { useUserDisplayName } from '@rocket.chat/ui-client';
4-
import { useRouteParameter, useSearchParameter, useSetModal, useUser, useUserAvatarPath } from '@rocket.chat/ui-contexts';
4+
import {
5+
useRouteParameter,
6+
useSearchParameter,
7+
useSetModal,
8+
useUser,
9+
useUserAvatarPath,
10+
useUserSubscription,
11+
} from '@rocket.chat/ui-contexts';
512
import { MediaCallRoomSection, useMediaCallView } from '@rocket.chat/ui-voip';
613
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
714

@@ -40,7 +47,7 @@ const LiveKitConferenceEmbeddedPage = () => {
4047
const wasConnected = useRef(false);
4148
const hasJoined = useRef(false);
4249

43-
const [showChat, setShowChat] = useState(true);
50+
const [showChat, setShowChat] = useState(false);
4451
const toggleChat = useCallback(() => setShowChat((prev) => !prev), []);
4552
const closeChat = useCallback(() => setShowChat(false), []);
4653

@@ -97,6 +104,9 @@ const LiveKitConferenceEmbeddedPage = () => {
97104
[displayName, getUserAvatarPath, user?._id],
98105
);
99106

107+
const subscription = useUserSubscription(room.rid ?? '');
108+
const unreadCount = subscription?.unread ?? 0;
109+
100110
if (conference.error || (conference.providerName && conference.providerName !== 'livekit')) {
101111
return <ConferencePageError />;
102112
}
@@ -111,14 +121,9 @@ const LiveKitConferenceEmbeddedPage = () => {
111121

112122
return (
113123
<Box bg='surface-light' w='full' h='full' display='flex' overflow='hidden'>
114-
{showChat && (
115-
<Box display='flex' flexDirection='column' flexShrink={0} width={400} h='full' borderInlineEndWidth={1} borderColor='stroke-light'>
116-
<ConferenceChat rid={room.rid} loading={room.loading} onClose={closeChat} />
117-
</Box>
118-
)}
119-
<Box flexGrow={1} display='flex' flexDirection='column' position='relative' minWidth={0} minHeight={0}>
120-
<MediaCallRoomSection showChat={showChat} onToggleChat={toggleChat} user={ownUser} />
121-
</Box>
124+
<MediaCallRoomSection showChat={showChat} onToggleChat={toggleChat} user={ownUser} unreadCount={unreadCount}>
125+
<ConferenceChat rid={room.rid} loading={room.loading} onClose={closeChat} />
126+
</MediaCallRoomSection>
122127
</Box>
123128
);
124129
};

apps/meteor/client/views/room/contextualBar/VideoConference/VideoConfList/VideoConfListItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ const VideoConfListItem = ({
4747
createdAt,
4848
endedAt,
4949
discussionRid,
50+
providerName,
51+
rid,
5052
} = videoConfData;
5153

5254
const displayName = useUserDisplayName({ name, username });
@@ -63,7 +65,7 @@ const VideoConfListItem = ({
6365
`;
6466

6567
const handleJoinConference = useStableCallback((): void => {
66-
joinCall(callId);
68+
joinCall(callId, providerName, rid);
6769
return reload();
6870
});
6971

packages/fuselage-ui-kit/src/blocks/VideoConferenceBlock/VideoConferenceBlock.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
VideoConfMessageContent,
1515
VideoConfMessageActions,
1616
VideoConfMessageAction,
17+
useVideoConfJoinCall,
18+
useVideoConfSetPreferences,
1719
} from '@rocket.chat/ui-video-conf';
1820
import type { MouseEventHandler } from 'react';
1921
import { useContext, memo, useMemo } from 'react';
@@ -37,6 +39,8 @@ const VideoConferenceBlock = ({ block }: VideoConferenceBlockProps) => {
3739
const showRealName = useSetting('UI_Use_Real_Name', false);
3840

3941
const { action, viewId = undefined, rid } = useContext(UiKitContext);
42+
const joinCall = useVideoConfJoinCall();
43+
const setPreferences = useVideoConfSetPreferences();
4044

4145
if (surfaceType !== 'message') {
4246
throw new Error('VideoConferenceBlock cannot be rendered outside message');
@@ -48,19 +52,6 @@ const VideoConferenceBlock = ({ block }: VideoConferenceBlockProps) => {
4852

4953
const result = useVideoConfDataStream({ rid, callId });
5054

51-
const joinHandler: MouseEventHandler<HTMLButtonElement> = (e): void => {
52-
void action(
53-
{
54-
blockId: block.blockId || '',
55-
appId,
56-
actionId: 'join',
57-
value: block.blockId || '',
58-
viewId,
59-
},
60-
e,
61-
);
62-
};
63-
6455
const callAgainHandler: MouseEventHandler<HTMLButtonElement> = (e): void => {
6556
void action(
6657
{
@@ -130,6 +121,12 @@ const VideoConferenceBlock = ({ block }: VideoConferenceBlockProps) => {
130121
})
131122
: t('__usernames__joined', { usernames: joinedNamesOrUsernames });
132123

124+
const joinHandler: MouseEventHandler<HTMLButtonElement> = (e): void => {
125+
e.preventDefault();
126+
setPreferences({ mic: true, cam: false });
127+
void joinCall(callId, data.providerName, data.rid);
128+
};
129+
133130
const actions = (
134131
<VideoConfMessageActions>
135132
{data.discussionRid && <VideoConfMessageAction icon='discussion' title={t('Join_discussion')} onClick={openDiscussion} />}

packages/ui-video-conf/src/VideoConfContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type VideoConfContextValue = {
1212
dismissOutgoing: () => void;
1313
startCall: (rid: IRoom['_id'], title?: string) => void;
1414
acceptCall: (callId: string) => void;
15-
joinCall: (callId: string) => void;
15+
joinCall: (callId: string, providerName?: string, rid?: string) => void;
1616
dismissCall: (callId: string) => void;
1717
rejectIncomingCall: (callId: string) => void;
1818
abortCall: () => void;

packages/ui-voip/src/components/Actions/ActionToggleChat.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
import { Button } from '@rocket.chat/fuselage';
1+
import { Badge, Button } from '@rocket.chat/fuselage';
22
import { useTranslation } from 'react-i18next';
33

44
export type ActionToggleChatProps = {
55
pressed: boolean;
66
onClick: () => void;
7+
unreadCount?: number;
78
};
8-
const ActionToggleChat = ({ pressed, onClick }: ActionToggleChatProps) => {
9+
const ActionToggleChat = ({ pressed, onClick, unreadCount = 0 }: ActionToggleChatProps) => {
910
const { t } = useTranslation();
10-
const icon = pressed ? 'chevron-down' : 'chevron-up';
1111

12-
const label = pressed ? t('Hide_chat') : t('Show_chat');
12+
const displayCount = unreadCount > 99 ? '99+' : unreadCount;
13+
1314
return (
14-
<Button medium onClick={onClick} icon={icon}>
15-
{label}
15+
<Button medium primary={pressed} onClick={onClick} icon='chat'>
16+
{t('Chat')}
17+
{unreadCount > 0 && (
18+
<Badge small variant='danger' style={{ marginInlineStart: 4 }}>
19+
{displayCount}
20+
</Badge>
21+
)}
1622
</Button>
1723
);
1824
};

packages/ui-voip/src/views/MediaCallRoomSection/MediaCallRoomSection.tsx

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { css } from '@rocket.chat/css-in-js';
22
import { Box, ButtonGroup, Icon } from '@rocket.chat/fuselage';
3-
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3+
import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
44
import { useTranslation } from 'react-i18next';
55

66
import CallStage from './CallStage';
@@ -271,9 +271,13 @@ type MediaCallRoomSectionProps = {
271271
};
272272
/** When true, suppresses the chat-toggle button (used by the floating widget which has no chat slot). */
273273
hideChatToggle?: boolean;
274+
/** Optional chat panel rendered inside the call surface when showChat is true. */
275+
children?: ReactNode;
276+
/** Number of unread messages in the chat panel, shown as a badge on the toggle button. */
277+
unreadCount?: number;
274278
};
275279

276-
const MediaCallRoomSection = ({ showChat, onToggleChat, user, hideChatToggle }: MediaCallRoomSectionProps) => {
280+
const MediaCallRoomSection = ({ showChat, onToggleChat, user, hideChatToggle, children, unreadCount }: MediaCallRoomSectionProps) => {
277281
const { t } = useTranslation();
278282

279283
const {
@@ -777,19 +781,37 @@ const MediaCallRoomSection = ({ showChat, onToggleChat, user, hideChatToggle }:
777781
</Box>
778782
</Box>
779783
</Box>
780-
<CallStage
781-
localParticipant={localParticipant}
782-
remoteParticipants={remoteParticipants}
783-
onStopLocalScreenShare={onToggleScreenSharing}
784-
handPositions={handPositions}
785-
reactionsByParticipant={reactionsByParticipant}
786-
captionsByParticipant={activeCaptions}
787-
/>
784+
<Box display='flex' flexDirection='row' flexGrow={1} minWidth={0} minHeight={0} overflow='hidden'>
785+
<Box flexGrow={1} minWidth={0} minHeight={0} display='flex' flexDirection='column'>
786+
<CallStage
787+
localParticipant={localParticipant}
788+
remoteParticipants={remoteParticipants}
789+
onStopLocalScreenShare={onToggleScreenSharing}
790+
handPositions={handPositions}
791+
reactionsByParticipant={reactionsByParticipant}
792+
captionsByParticipant={activeCaptions}
793+
/>
794+
</Box>
795+
{showChat && children && (
796+
<Box
797+
display='flex'
798+
flexDirection='column'
799+
flexShrink={0}
800+
width={400}
801+
h='full'
802+
bg='surface-light'
803+
borderInlineStartWidth={1}
804+
borderColor='stroke-light'
805+
>
806+
{children}
807+
</Box>
808+
)}
809+
</Box>
788810
<ActionStrip
789811
rightSlot={
790812
!hideChatToggle ? (
791813
<ButtonGroup>
792-
<ActionToggleChat pressed={showChat} onClick={onToggleChat} />
814+
<ActionToggleChat pressed={showChat} onClick={onToggleChat} unreadCount={unreadCount} />
793815
<ToggleButton
794816
label={t('Open_in_new_window')}
795817
titles={[t('Open_in_new_window'), t('Return_to_main_window')]}

0 commit comments

Comments
 (0)