Skip to content

Commit cad1c1b

Browse files
feat: Persistent Chat
1 parent 981c7e4 commit cad1c1b

81 files changed

Lines changed: 4335 additions & 578 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/meteor/client/definitions/global.d.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ declare global {
44
interface Window {
55
RocketChatDesktop?: IRocketChatDesktop;
66

7+
// Bridge injected into the desktop app's internal video-chat window (separate from
8+
// `RocketChatDesktop`, which is only present in the main app webview).
9+
videoCallWindow?: {
10+
// Navigate the main app window to an in-app route (e.g. "/channel/general") and focus it.
11+
openInMainWindow?: (path: string) => void;
12+
// Close the conference window (renderer `window.close()` can't close a main-process window).
13+
close?: () => void;
14+
};
15+
716
/** @deprecated use `window.RTCPeerConnection` */
817
mozRTCPeerConnection?: RTCPeerConnection;
918
/** @deprecated use `window.RTCPeerConnection` */

apps/meteor/client/hooks/notification/useNotification.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings';
22
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
33
import { Random } from '@rocket.chat/random';
44
import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts';
5+
import { useVideoConfJoinCall } from '@rocket.chat/ui-video-conf';
56

67
import { useNotificationAllowed } from './useNotificationAllowed';
78
import { getUserAvatarURL } from '../../../app/utils/client';
@@ -10,8 +11,9 @@ import { stripTags } from '../../../lib/utils/stringUtils';
1011
import { onClientMessageReceived } from '../../lib/onClientMessageReceived';
1112

1213
export const useNotification = () => {
13-
const requireInteraction = useUserPreference('desktopNotificationRequireInteraction');
14+
const requireInteractionPreference = useUserPreference('desktopNotificationRequireInteraction');
1415
const router = useRouter();
16+
const joinCall = useVideoConfJoinCall();
1517
const notificationAllowed = useNotificationAllowed();
1618

1719
const notify = useStableCallback(async (notification: INotificationDesktop) => {
@@ -22,6 +24,9 @@ export const useNotification = () => {
2224
return;
2325
}
2426

27+
// A notification can opt into staying until interacted with, on top of the user preference.
28+
const requireInteraction = Boolean(notification.requireInteraction || requireInteractionPreference);
29+
2530
const { rid, name: roomName, _id: msgId } = notification.payload;
2631
if (!rid) {
2732
return;
@@ -39,6 +44,7 @@ export const useNotification = () => {
3944
canReply: true,
4045
silent: true,
4146
requireInteraction,
47+
...(window.RocketChatDesktop && notification.actions?.length ? { actions: notification.actions } : {}),
4248
} as NotificationOptions & {
4349
canReply?: boolean;
4450
});
@@ -59,6 +65,16 @@ export const useNotification = () => {
5965
},
6066
}),
6167
);
68+
69+
// "Join" action (desktop app): join the call the same way the ongoing-call banner does.
70+
const { conferenceId } = notification.payload;
71+
if (conferenceId) {
72+
n.addEventListener('action', () => {
73+
n.close();
74+
window.focus();
75+
joinCall(conferenceId);
76+
});
77+
}
6278
}
6379

6480
n.onclick = () => {

apps/meteor/client/hooks/roomActions/useCallsRoomAction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,14 @@ export const useCallsRoomAction = () => {
2222
return {
2323
id: 'calls',
2424
groups: ['channel', 'group', 'team', 'direct', 'direct_multiple'],
25-
icon: 'phone',
26-
title: 'Calls',
25+
icon: 'history',
26+
title: 'Conference_call_history',
2727
...(federated && {
2828
tooltip: t('core.Video_Call_unavailable_for_this_type_of_room'),
2929
disabled: true,
3030
}),
3131
tabComponent: VideoConfList,
32-
order: 999,
32+
order: 8,
3333
};
3434
}, [licensed, federated, t]);
3535
};

apps/meteor/client/lib/appLayout.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ class AppLayoutSubscription extends Emitter<{ update: void }> {
2525
this.setCurrentValue(element);
2626
}
2727

28-
wrap(element: ReactNode): ReactNode {
28+
// `embedded` standalone views (e.g. the conference page) omit the global announcement/banner
29+
// chrome so app-level banners (E2E password prompt, admin announcements) don't bleed into them.
30+
wrap(element: ReactNode, { embedded = false }: { embedded?: boolean } = {}): ReactNode {
2931
return (
3032
<AppLayoutThemeWrapper>
3133
<ConnectionStatusBar />
3234
<ActionManagerBusyState />
33-
<CloudAnnouncementsRegion />
34-
<BannerRegion />
35+
{!embedded && <CloudAnnouncementsRegion />}
36+
{!embedded && <BannerRegion />}
3537
{element}
3638
<ModalRegion />
3739
</AppLayoutThemeWrapper>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { IRoom, Serialized } from '@rocket.chat/core-typings';
2+
3+
import { mapMessageFromApi } from './mapMessageFromApi';
4+
5+
export const mapRoomFromApi = ({
6+
_updatedAt,
7+
lm,
8+
ts,
9+
lastMessage,
10+
webRtcCallStartTime,
11+
usersWaitingForE2EKeys,
12+
...room
13+
}: Serialized<IRoom>): IRoom => ({
14+
...room,
15+
_updatedAt: new Date(_updatedAt),
16+
...(lm && { lm: new Date(lm) }),
17+
...(ts && { ts: new Date(ts) }),
18+
...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }),
19+
...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }),
20+
...(usersWaitingForE2EKeys && {
21+
usersWaitingForE2EKeys: usersWaitingForE2EKeys.map((user) => ({ ...user, ts: new Date(user.ts) })),
22+
}),
23+
});

apps/meteor/client/providers/MediaCallProvider.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { usePermission } from '@rocket.chat/ui-contexts';
1+
import { usePermission, useCurrentRoutePath } from '@rocket.chat/ui-contexts';
22
import { MediaCallProvider as MediaCallProviderBase } from '@rocket.chat/ui-voip';
33
import { MediaCallAppActionsProvider } from '@rocket.chat/ui-voip/dist/experimental/AppActionButtons';
44
import type { ReactNode } from 'react';
@@ -13,9 +13,13 @@ const MediaCallProvider = ({ children }: MediaCallProviderProps) => {
1313
const canMakeExternalCall = usePermission('allow-external-voice-calls');
1414
const { actions, handleInteraction } = useMediaCallWidgetAppsActionButtons();
1515

16+
const currentRoute = useCurrentRoutePath();
17+
18+
const isConferenceRoute = currentRoute?.includes('/conference');
19+
1620
const { data: hasModule = false } = useHasLicenseModule('teams-voip');
1721

18-
const enabled = hasModule && (canMakeInternalCall || canMakeExternalCall);
22+
const enabled = hasModule && (canMakeInternalCall || canMakeExternalCall) && !isConferenceRoute;
1923

2024
return (
2125
<MediaCallAppActionsProvider actions={actions} handleInteraction={handleInteraction}>

apps/meteor/client/providers/VideoConfProvider.tsx

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,44 @@
1-
import { useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts';
1+
import { useRouter, useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts';
22
import type { VideoConfPopupPayload, VideoConfContextValue } from '@rocket.chat/ui-video-conf';
33
import { VideoConfContext } from '@rocket.chat/ui-video-conf';
44
import type { ReactNode } from 'react';
55
import { useState, useMemo, useEffect } from 'react';
66
import { useTranslation } from 'react-i18next';
77

88
import { VideoConfManager } from '../lib/VideoConfManager';
9+
import { absoluteUrl } from '../lib/absoluteUrl';
910
import VideoConfPopups from '../views/room/contextualBar/VideoConference/VideoConfPopups';
1011
import { useVideoConfOpenCall } from '../views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
1112

1213
export type VideoConfContextProviderProps = { children: ReactNode };
1314

15+
// The internal Pexip integration (core provider) replaces the external Pexip app.
16+
const PEXIP_PROVIDER_NAME = 'core.pexip';
17+
1418
const VideoConfContextProvider = ({ children }: VideoConfContextProviderProps) => {
1519
const [outgoing, setOutgoing] = useState<VideoConfPopupPayload | undefined>();
1620
const handleOpenCall = useVideoConfOpenCall();
1721
const dispatchToastMessage = useToastMessageDispatch();
22+
const router = useRouter();
1823
const { t } = useTranslation();
1924
const logLevel = useSetting<number>('Log_Level', 0);
2025

2126
useEffect(() => VideoConfManager.setLogLevel(logLevel), [logLevel]);
2227

2328
useEffect(
2429
() =>
25-
VideoConfManager.on('call/join', (props) => {
26-
handleOpenCall(props.url, props.providerName);
30+
VideoConfManager.on('call/join', ({ url, callId, providerName }) => {
31+
// When the internal Pexip integration is the provider, open the in-product conference
32+
// page (persistent chat + call) in a new tab — mirroring voice-call escalation — instead
33+
// of the external provider URL.
34+
if (providerName === PEXIP_PROVIDER_NAME) {
35+
handleOpenCall(absoluteUrl(router.buildRoutePath({ name: 'conference', params: { id: callId } })), providerName);
36+
return;
37+
}
38+
39+
handleOpenCall(url, providerName);
2740
}),
28-
[handleOpenCall],
41+
[handleOpenCall, router],
2942
);
3043

3144
useEffect(

apps/meteor/client/startup/routes.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ router.defineRoutes([
211211
{
212212
path: '/conference/:id',
213213
id: 'conference',
214-
element: appLayout.wrap(<ConferenceRoute />),
214+
element: appLayout.wrap(<ConferenceRoute />, { embedded: true }),
215215
},
216216
{
217217
path: '/setup-wizard/:step?',

apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { IRoom, IMessage } from '@rocket.chat/core-typings';
22
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
33
import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit';
4-
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
4+
import { useRoomToolbox, useRouter } from '@rocket.chat/ui-contexts';
55
import {
66
useVideoConfDispatchOutgoing,
77
useVideoConfIsCalling,
@@ -10,7 +10,7 @@ import {
1010
useVideoConfLoadCapabilities,
1111
useVideoConfSetPreferences,
1212
} from '@rocket.chat/ui-video-conf';
13-
import type { ContextType } from 'react';
13+
import { useCallback, useSyncExternalStore, type ContextType } from 'react';
1414

1515
import { useUiKitActionManager } from './useUiKitActionManager';
1616
import { useVideoConfWarning } from '../../views/room/contextualBar/VideoConference/hooks/useVideoConfWarning';
@@ -24,6 +24,14 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
2424
const dispatchPopup = useVideoConfDispatchOutgoing();
2525
const loadVideoConfCapabilities = useVideoConfLoadCapabilities();
2626

27+
// Inside a conference window, block message-block actions that would open/join another conference.
28+
const router = useRouter();
29+
const routeName = useSyncExternalStore(
30+
router.subscribeToRouteChange,
31+
useCallback(() => router.getRouteName(), [router]),
32+
);
33+
const videoConfJoinDisabled = routeName === 'conference';
34+
2735
const handleOpenVideoConf = useStableCallback(async (rid: IRoom['_id']) => {
2836
if (isCalling || isRinging) {
2937
return;
@@ -45,6 +53,10 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
4553
action: ({ appId, actionId, blockId, value }, event) => {
4654
if (appId === 'videoconf-core') {
4755
event.preventDefault();
56+
// Don't let a user in a conference open/join another conference from a message block.
57+
if (videoConfJoinDisabled && (actionId === 'join' || actionId === 'callBack')) {
58+
return undefined;
59+
}
4860
setPreferences({ mic: true, cam: false });
4961
if (actionId === 'join') {
5062
return joinCall(blockId);
@@ -77,6 +89,7 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
7789
});
7890
},
7991
rid,
92+
videoConfJoinDisabled,
8093
values: {}, // TODO: this is a hack to make the context work, but it should be removed
8194
};
8295
};

0 commit comments

Comments
 (0)