Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions apps/meteor/client/definitions/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ declare global {
interface Window {
RocketChatDesktop?: IRocketChatDesktop;

// Bridge injected into the desktop app's internal video-chat window (separate from
// `RocketChatDesktop`, which is only present in the main app webview).
videoCallWindow?: {
// Navigate the main app window to an in-app route (e.g. "/channel/general") and focus it.
openInMainWindow?: (path: string) => void;
// Close the conference window (renderer `window.close()` can't close a main-process window).
close?: () => void;
};

/** @deprecated use `window.RTCPeerConnection` */
mozRTCPeerConnection?: RTCPeerConnection;
/** @deprecated use `window.RTCPeerConnection` */
Expand Down
18 changes: 17 additions & 1 deletion apps/meteor/client/hooks/notification/useNotification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import { Random } from '@rocket.chat/random';
import { useRouter, useUserPreference } from '@rocket.chat/ui-contexts';
import { useVideoConfJoinCall } from '@rocket.chat/ui-video-conf';

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

export const useNotification = () => {
const requireInteraction = useUserPreference('desktopNotificationRequireInteraction');
const requireInteractionPreference = useUserPreference('desktopNotificationRequireInteraction');
const router = useRouter();
const joinCall = useVideoConfJoinCall();
const notificationAllowed = useNotificationAllowed();

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

// A notification can opt into staying until interacted with, on top of the user preference.
const requireInteraction = Boolean(notification.requireInteraction || requireInteractionPreference);

const { rid, name: roomName, _id: msgId } = notification.payload;
if (!rid) {
return;
Expand All @@ -39,6 +44,7 @@ export const useNotification = () => {
canReply: true,
silent: true,
requireInteraction,
...(window.RocketChatDesktop && notification.actions?.length ? { actions: notification.actions } : {}),
} as NotificationOptions & {
canReply?: boolean;
});
Expand All @@ -59,6 +65,16 @@ export const useNotification = () => {
},
}),
);

// "Join" action (desktop app): join the call the same way the ongoing-call banner does.
const { conferenceId } = notification.payload;
if (conferenceId) {
n.addEventListener('action', () => {
n.close();
window.focus();
joinCall(conferenceId);
});
}
}

n.onclick = () => {
Expand Down
6 changes: 3 additions & 3 deletions apps/meteor/client/hooks/roomActions/useCallsRoomAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ export const useCallsRoomAction = () => {
return {
id: 'calls',
groups: ['channel', 'group', 'team', 'direct', 'direct_multiple'],
icon: 'phone',
title: 'Calls',
icon: 'history',
title: 'Conference_call_history',
...(federated && {
tooltip: t('core.Video_Call_unavailable_for_this_type_of_room'),
disabled: true,
}),
tabComponent: VideoConfList,
order: 999,
order: 8,
};
}, [licensed, federated, t]);
};
8 changes: 5 additions & 3 deletions apps/meteor/client/lib/appLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ class AppLayoutSubscription extends Emitter<{ update: void }> {
this.setCurrentValue(element);
}

wrap(element: ReactNode): ReactNode {
// `embedded` standalone views (e.g. the conference page) omit the global announcement/banner
// chrome so app-level banners (E2E password prompt, admin announcements) don't bleed into them.
wrap(element: ReactNode, { embedded = false }: { embedded?: boolean } = {}): ReactNode {
return (
<AppLayoutThemeWrapper>
<ConnectionStatusBar />
<ActionManagerBusyState />
<CloudAnnouncementsRegion />
<BannerRegion />
{!embedded && <CloudAnnouncementsRegion />}
{!embedded && <BannerRegion />}
{element}
<ModalRegion />
</AppLayoutThemeWrapper>
Expand Down
23 changes: 23 additions & 0 deletions apps/meteor/client/lib/utils/mapRoomFromApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { IRoom, Serialized } from '@rocket.chat/core-typings';

import { mapMessageFromApi } from './mapMessageFromApi';

export const mapRoomFromApi = ({
_updatedAt,
lm,
ts,
lastMessage,
webRtcCallStartTime,
usersWaitingForE2EKeys,
...room
}: Serialized<IRoom>): IRoom => ({
...room,
_updatedAt: new Date(_updatedAt),
...(lm && { lm: new Date(lm) }),
...(ts && { ts: new Date(ts) }),
...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }),
...(webRtcCallStartTime && { webRtcCallStartTime: new Date(webRtcCallStartTime) }),
...(usersWaitingForE2EKeys && {
usersWaitingForE2EKeys: usersWaitingForE2EKeys.map((user) => ({ ...user, ts: new Date(user.ts) })),
}),
});
8 changes: 6 additions & 2 deletions apps/meteor/client/providers/MediaCallProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { usePermission } from '@rocket.chat/ui-contexts';
import { usePermission, useCurrentRoutePath } from '@rocket.chat/ui-contexts';
import { MediaCallProvider as MediaCallProviderBase } from '@rocket.chat/ui-voip';
import { MediaCallAppActionsProvider } from '@rocket.chat/ui-voip/dist/experimental/AppActionButtons';
import type { ReactNode } from 'react';
Expand All @@ -13,9 +13,13 @@ const MediaCallProvider = ({ children }: MediaCallProviderProps) => {
const canMakeExternalCall = usePermission('allow-external-voice-calls');
const { actions, handleInteraction } = useMediaCallWidgetAppsActionButtons();

const currentRoute = useCurrentRoutePath();

const isConferenceRoute = currentRoute?.includes('/conference');

const { data: hasModule = false } = useHasLicenseModule('teams-voip');

const enabled = hasModule && (canMakeInternalCall || canMakeExternalCall);
const enabled = hasModule && (canMakeInternalCall || canMakeExternalCall) && !isConferenceRoute;

return (
<MediaCallAppActionsProvider actions={actions} handleInteraction={handleInteraction}>
Expand Down
21 changes: 17 additions & 4 deletions apps/meteor/client/providers/VideoConfProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,44 @@
import { useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts';
import { useRouter, useToastMessageDispatch, useSetting } from '@rocket.chat/ui-contexts';
import type { VideoConfPopupPayload, VideoConfContextValue } from '@rocket.chat/ui-video-conf';
import { VideoConfContext } from '@rocket.chat/ui-video-conf';
import type { ReactNode } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';

import { VideoConfManager } from '../lib/VideoConfManager';
import { absoluteUrl } from '../lib/absoluteUrl';
import VideoConfPopups from '../views/room/contextualBar/VideoConference/VideoConfPopups';
import { useVideoConfOpenCall } from '../views/room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';

export type VideoConfContextProviderProps = { children: ReactNode };

// The internal Pexip integration (core provider) replaces the external Pexip app.
const PEXIP_PROVIDER_NAME = 'core.pexip';

const VideoConfContextProvider = ({ children }: VideoConfContextProviderProps) => {
const [outgoing, setOutgoing] = useState<VideoConfPopupPayload | undefined>();
const handleOpenCall = useVideoConfOpenCall();
const dispatchToastMessage = useToastMessageDispatch();
const router = useRouter();
const { t } = useTranslation();
const logLevel = useSetting<number>('Log_Level', 0);

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

useEffect(
() =>
VideoConfManager.on('call/join', (props) => {
handleOpenCall(props.url, props.providerName);
VideoConfManager.on('call/join', ({ url, callId, providerName }) => {
// When the internal Pexip integration is the provider, open the in-product conference
// page (persistent chat + call) in a new tab — mirroring voice-call escalation — instead
// of the external provider URL.
if (providerName === PEXIP_PROVIDER_NAME) {
handleOpenCall(absoluteUrl(router.buildRoutePath({ name: 'conference', params: { id: callId } })), providerName);
return;
}

handleOpenCall(url, providerName);
}),
[handleOpenCall],
[handleOpenCall, router],
);

useEffect(
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/client/startup/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ router.defineRoutes([
{
path: '/conference/:id',
id: 'conference',
element: appLayout.wrap(<ConferenceRoute />),
element: appLayout.wrap(<ConferenceRoute />, { embedded: true }),
},
{
path: '/setup-wizard/:step?',
Expand Down
17 changes: 15 additions & 2 deletions apps/meteor/client/uikit/hooks/useMessageBlockContextValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { IRoom, IMessage } from '@rocket.chat/core-typings';
import { useStableCallback } from '@rocket.chat/fuselage-hooks';
import type { UiKitContext } from '@rocket.chat/fuselage-ui-kit';
import { useRoomToolbox } from '@rocket.chat/ui-contexts';
import { useRoomToolbox, useRouter } from '@rocket.chat/ui-contexts';
import {
useVideoConfDispatchOutgoing,
useVideoConfIsCalling,
Expand All @@ -10,7 +10,7 @@ import {
useVideoConfLoadCapabilities,
useVideoConfSetPreferences,
} from '@rocket.chat/ui-video-conf';
import type { ContextType } from 'react';
import { useCallback, useSyncExternalStore, type ContextType } from 'react';

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

// Inside a conference window, block message-block actions that would open/join another conference.
const router = useRouter();
const routeName = useSyncExternalStore(
router.subscribeToRouteChange,
useCallback(() => router.getRouteName(), [router]),
);
const videoConfJoinDisabled = routeName === 'conference';

const handleOpenVideoConf = useStableCallback(async (rid: IRoom['_id']) => {
if (isCalling || isRinging) {
return;
Expand All @@ -45,6 +53,10 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
action: ({ appId, actionId, blockId, value }, event) => {
if (appId === 'videoconf-core') {
event.preventDefault();
// Don't let a user in a conference open/join another conference from a message block.
if (videoConfJoinDisabled && (actionId === 'join' || actionId === 'callBack')) {
return undefined;
}
setPreferences({ mic: true, cam: false });
if (actionId === 'join') {
return joinCall(blockId);
Expand Down Expand Up @@ -77,6 +89,7 @@ export const useMessageBlockContextValue = (rid: IRoom['_id'], mid: IMessage['_i
});
},
rid,
videoConfJoinDisabled,
values: {}, // TODO: this is a hack to make the context work, but it should be removed
};
};
Loading
Loading