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
34 changes: 34 additions & 0 deletions apps/meteor/client/views/conference/ConferenceChat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { RoomType } from '@rocket.chat/core-typings';
import { Box, Button } from '@rocket.chat/fuselage';
import { useState } from 'react';

import RoomOpenerEmbedded from '../room/RoomOpenerEmbedded';
import EmbeddedPreload from '../root/MainLayout/EmbeddedPreload';

type ConferenceChatProps = {
type: RoomType;
reference: string;
loading: boolean;
};
const ConferenceChat = ({ type, loading }: ConferenceChatProps) => {
const [reference, setReference] = useState('general');

if (loading) {
return <div>Loading...</div>;
}

return (
<Box position='relative' display='flex' flexDirection='column' flexGrow={1}>
<div>
{/* Temporary buttons to test room change */}
<Button onClick={() => setReference('general')}>general</Button>
<Button onClick={() => setReference('important')}>important</Button>
</div>
<EmbeddedPreload type={type} reference={reference}>
<RoomOpenerEmbedded type={type} reference={reference} />
</EmbeddedPreload>
</Box>
);
};

export default ConferenceChat;
27 changes: 27 additions & 0 deletions apps/meteor/client/views/conference/ConferenceIframe.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type ConferenceIframeProps = {
url: string | undefined;
loading: boolean;
};

const ConferenceIframe = ({ url, loading }: ConferenceIframeProps) => {
if (!url) {
return <div>No conference URL provided.</div>;
}

if (loading) {
return <div>Loading...</div>;
}

return (
<iframe
style={{ width: '100%', height: '100%' }}
title='external-frame'
src={url}
allow='camera; microphone; display-capture; fullscreen; autoplay; speaker-selection; clipboard-write; clipboard-read; compute-pressure'
allowFullScreen
referrerPolicy='strict-origin-when-cross-origin'
/>
);
};

export default ConferenceIframe;
27 changes: 27 additions & 0 deletions apps/meteor/client/views/conference/ConferenceInlinePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Box } from '@rocket.chat/fuselage';

import ConferenceChat from './ConferenceChat';
import ConferenceIframe from './ConferenceIframe';
import { useConferenceEmbedded } from './hooks/useConferenceEmbedded';

type ConferenceInlinePageProps = {
callId: string;
};

const ConferenceInlinePage = ({ callId }: ConferenceInlinePageProps) => {
const { room, conference } = useConferenceEmbedded(callId);

return (
<Box bg='surface-light' width='full' height='full' display='flex'>
<Box width='30%' display='flex' flexDirection='column' minWidth={300} p={4} bg='tint' borderInlineEndWidth={1} borderColor='divider'>
<ConferenceChat type={room.type} reference={room.reference} loading={room.loading} />
</Box>

<Box width='70%' margin={24} borderColor='divider' borderWidth={1}>
<ConferenceIframe url={conference.url} loading={conference.loading} />
</Box>
</Box>
);
};

export default ConferenceInlinePage;
34 changes: 10 additions & 24 deletions apps/meteor/client/views/conference/ConferencePage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useUserDisplayName } from '@rocket.chat/ui-client';
import { useRoute, useSetModal, useUser } from '@rocket.chat/ui-contexts';
import { useRouteParameter } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useEffect } from 'react';

import ConferenceInlinePage from './ConferenceInlinePage';
import ConferencePageError from './ConferencePageError';
import { useVideoConfOpenCall } from '../room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
import PageLoading from '../root/PageLoading';
import ConferenceRedirectPage from './ConferenceRedirectPage';

const getQueryParams = () => {
const queryString = window.location.search;
Expand All @@ -16,30 +14,18 @@ const getQueryParams = () => {
};

const ConferencePage = (): ReactElement => {
const user = useUser();
const defaultRoute = useRoute('home');
const setModal = useSetModal();
const handleOpenCall = useVideoConfOpenCall();
const userDisplayName = useUserDisplayName({ name: user?.name, username: user?.username });

const id = useRouteParameter('id');
const { callUrlParam } = getQueryParams();
const callUrl = callUrlParam && userDisplayName ? `${callUrlParam}&name=${userDisplayName}` : callUrlParam;

useEffect(() => {
if (!callUrl) {
return;
}

handleOpenCall(callUrl);

defaultRoute.push();
}, [setModal, defaultRoute, callUrl, handleOpenCall, userDisplayName]);
if (callUrlParam) {
return <ConferenceRedirectPage callUrl={callUrlParam} />;
}

if (!callUrl) {
return <ConferencePageError />;
if (id) {
return <ConferenceInlinePage callId={id} />;
}

return <PageLoading />;
return <ConferencePageError />;
};

export default ConferencePage;
32 changes: 32 additions & 0 deletions apps/meteor/client/views/conference/ConferenceRedirectPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useRoute, useSetModal } from '@rocket.chat/ui-contexts';
import { useEffect } from 'react';

import { useVideoConfOpenCall } from '../room/contextualBar/VideoConference/hooks/useVideoConfOpenCall';
import PageLoading from '../root/PageLoading';
import { useConferenceCallUrl } from './hooks/useConferenceCallUrl';

type ConferenceRedirectPageProps = {
callUrl: string;
};

const ConferenceRedirectPage = ({ callUrl: baseCallUrl }: ConferenceRedirectPageProps) => {
const defaultRoute = useRoute('home');
const setModal = useSetModal();
const handleOpenCall = useVideoConfOpenCall();
const getConferenceCallUrl = useConferenceCallUrl();

useEffect(() => {
if (!baseCallUrl) {
return;
}

const callUrl = getConferenceCallUrl(baseCallUrl);
handleOpenCall(callUrl);

defaultRoute.push();
}, [setModal, defaultRoute, baseCallUrl, handleOpenCall, getConferenceCallUrl]);

return <PageLoading />;
};

export default ConferenceRedirectPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useUserDisplayName } from '@rocket.chat/ui-client';
import { useUser } from '@rocket.chat/ui-contexts';

export const useConferenceCallUrl = () => {
const user = useUser();
const userDisplayName = useUserDisplayName({ name: user?.name, username: user?.username });

return (callUrl: string) => {
if (!userDisplayName) {
return callUrl;
}
const url = new URL(callUrl);
url.searchParams.set('name', userDisplayName);
return url.toString();
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEndpoint } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';

import { useConferenceCallUrl } from './useConferenceCallUrl';

export const useConferenceEmbedded = (callId: string) => {
const joinConference = useEndpoint('POST', '/v1/video-conference.join');
const getConferenceCallUrl = useConferenceCallUrl();

const { data, isPending } = useQuery({
queryKey: ['conference-embedded', callId],
queryFn: async () => joinConference({ callId, state: { mic: true, cam: false } }),
});

return {
room: { type: 'c', reference: 'general', loading: isPending } as const,
conference: {
url: data?.url ? getConferenceCallUrl(data.url) : undefined,
providerName: data?.providerName,
loading: isPending,
} as const,
};
};
82 changes: 43 additions & 39 deletions apps/meteor/client/views/room/RoomOpenerEmbedded.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ISubscription, RoomType } from '@rocket.chat/core-typings';
import { Box, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage';
import { Header } from '@rocket.chat/ui-client';
import { useStream, useUserId } from '@rocket.chat/ui-contexts';
import { LayoutContext, useLayout, useStream, useUserId } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { lazy, Suspense, useEffect } from 'react';
import { lazy, Suspense, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';

import NotSubscribedRoom from './NotSubscribedRoom';
Expand Down Expand Up @@ -31,6 +31,8 @@ const RoomOpenerEmbedded = ({ type, reference }: RoomOpenerProps): ReactElement
const { data, error, isSuccess, isError, isLoading } = useOpenRoom({ type, reference });
const uid = useUserId();
const subscribeToNotifyUser = useStream('notify-user');
const layoutContext = useLayout();
const layoutContextEmbedded = useMemo(() => ({ ...layoutContext, isEmbedded: true }), [layoutContext]);

const rid = data?.rid;

Expand All @@ -51,47 +53,49 @@ const RoomOpenerEmbedded = ({ type, reference }: RoomOpenerProps): ReactElement
const { t } = useTranslation();

return (
<Box display='flex' w='full' h='full'>
<Suspense fallback={<RoomSkeleton />}>
{isLoading && <RoomSkeleton />}
{isSuccess && (
<RoomProvider rid={data.rid}>
<Room />
</RoomProvider>
)}
{isError &&
(() => {
if (error instanceof OldUrlRoomError) {
return <RoomSkeleton />;
}
<LayoutContext.Provider value={layoutContextEmbedded}>
<Box display='flex' w='full' h='full'>
<Suspense fallback={<RoomSkeleton />}>
{isLoading && <RoomSkeleton />}
{isSuccess && (
<RoomProvider rid={data.rid}>
<Room />
</RoomProvider>
)}
{isError &&
(() => {
if (error instanceof OldUrlRoomError) {
return <RoomSkeleton />;
}

if (error instanceof RoomNotFoundError) {
return <RoomNotFound />;
}
if (error instanceof RoomNotFoundError) {
return <RoomNotFound />;
}

if (error instanceof NotSubscribedToRoomError) {
return <NotSubscribedRoom rid={error.details.rid} reference={reference} type={type} />;
}
if (error instanceof NotSubscribedToRoomError) {
return <NotSubscribedRoom rid={error.details.rid} reference={reference} type={type} />;
}

if (error instanceof NotAuthorizedError) {
return <NotAuthorizedPage />;
}
if (error instanceof NotAuthorizedError) {
return <NotAuthorizedPage />;
}

return (
<RoomLayout
header={<Header />}
body={
<States>
<StatesIcon name='circle-exclamation' variation='danger' />
<StatesTitle>{t('core.Error')}</StatesTitle>
<StatesSubtitle>{getErrorMessage(error)}</StatesSubtitle>
</States>
}
/>
);
})()}
</Suspense>
</Box>
return (
<RoomLayout
header={<Header />}
body={
<States>
<StatesIcon name='circle-exclamation' variation='danger' />
<StatesTitle>{t('core.Error')}</StatesTitle>
<StatesSubtitle>{getErrorMessage(error)}</StatesSubtitle>
</States>
}
/>
);
})()}
</Suspense>
</Box>
</LayoutContext.Provider>
);
};

Expand Down
20 changes: 16 additions & 4 deletions apps/meteor/client/views/root/MainLayout/EmbeddedPreload.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { RoomType } from '@rocket.chat/core-typings';
import { useEndpoint, useMethod, useRouter, useUserId } from '@rocket.chat/ui-contexts';
import { useQuery } from '@tanstack/react-query';
import type { ReactElement, ReactNode } from 'react';
Expand All @@ -7,15 +8,20 @@ import { RoomsCachedStore, SubscriptionsCachedStore } from '../../../cachedStore
import { roomsQueryKeys } from '../../../lib/queryKeys';
import { roomCoordinator } from '../../../lib/rooms/roomCoordinator';
import { mapSubscriptionFromApi } from '../../../lib/utils/mapSubscriptionFromApi';
import { Rooms } from '../../../stores';
import PageLoading from '../PageLoading';
import { useMainReady } from '../hooks/useMainReady';

const EmbeddedPreload = ({ children }: { children: ReactNode }): ReactElement => {
const EmbeddedPreload = ({ children, reference, type }: { children: ReactNode; reference?: string; type?: RoomType }): ReactElement => {
const ready = useMainReady();
const router = useRouter();
const uid = useUserId();

const roomParams = useMemo(() => {
if (reference && type) {
return { reference, type };
}

const routeName = router.getRouteName();
if (!routeName) {
return null;
Expand All @@ -32,22 +38,28 @@ const EmbeddedPreload = ({ children }: { children: ReactNode }): ReactElement =>
}

return directives.extractOpenRoomParams(router.getRouteParameters());
}, [router]);
}, [reference, router, type]);

const getRoomByTypeAndName = useMethod('getRoomByTypeAndName');
const getSubscription = useEndpoint('GET', '/v1/subscriptions.getOne');

const shouldFetch = !!roomParams && !!uid;

const { isLoading, isSuccess, isError } = useQuery({
const {
isPending: isLoading,
isSuccess,
isError,
} = useQuery({
queryKey: roomParams ? roomsQueryKeys.roomReference(roomParams.reference, roomParams.type, uid ?? undefined) : [],
queryFn: async () => {
if (!roomParams) {
return null;
}

const roomData = await getRoomByTypeAndName(roomParams.type, roomParams.reference);
if (!roomData?._id) {
if (roomData?._id) {
Rooms.state.store(roomData);
} else {
return null;
}

Expand Down
Loading
Loading