Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const WidgetDraggableProvider = ({ children }: WidgetDraggableProviderProps) =>
const { widgetPositionTracker } = useMediaCallView();
const [draggableRef, boundingRef, handleRef] = useDraggable({
onChangePosition: widgetPositionTracker?.onChangePosition,
restorePosition: widgetPositionTracker?.getRestorePosition(),
restorePosition: widgetPositionTracker?.lastKnownPosition,
});

useLayoutEffect(() => {
Expand Down
38 changes: 24 additions & 14 deletions packages/ui-voip/src/context/MediaCallInstanceContext.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,52 @@
import { Emitter } from '@rocket.chat/emitter';
import type { MediaSignalingSession } from '@rocket.chat/media-signaling';
import type { RefObject } from 'react';
import { createContext, useContext } from 'react';

import type { PeerAutocompleteOptions } from '../components';
import type { PeerInfo } from './definitions';

export type Signals = {
toggleWidget: { peerInfo?: PeerInfo };
};

export type AvailableViews = 'room' | 'popout' | 'widget';

type RegisterView = (view: AvailableViews) => void;
type UnregisterView = (view: AvailableViews) => void;

type MediaCallInstanceContextValue = {
type WidgetVisibility = 'open' | 'closed';

export type MediaCallInstanceContextValue = {
instance: MediaSignalingSession | undefined;
signalEmitter: Emitter<Signals>;
audioElement: RefObject<HTMLAudioElement | null> | undefined;
openRoomId: string | undefined;
currentViews: AvailableViews[];
setOpenRoomId: (openRoomId: string | undefined) => void;
getAutocompleteOptions: (filter: string) => Promise<PeerAutocompleteOptions[]>;

currentViews: Set<AvailableViews>;
registerView: RegisterView;
unregisterView: UnregisterView;

targetWidgetVisibility: WidgetVisibility;
targetPeer: PeerInfo | undefined;

setOpenRoomId: (openRoomId: string | undefined) => void;
getAutocompleteOptions: (filter: string) => Promise<PeerAutocompleteOptions[]>;
openWidget: (peerInfo?: PeerInfo) => void;
closeWidget: () => void;
setTargetPeer: (peerInfo?: PeerInfo) => void;
};

export const MediaCallInstanceContext = createContext<MediaCallInstanceContextValue>({
export const defaultContextValue = {
instance: undefined,
signalEmitter: new Emitter<Signals>(),
audioElement: undefined,
openRoomId: undefined,
setOpenRoomId: () => undefined,
getAutocompleteOptions: () => Promise.resolve([]),
currentViews: [],
currentViews: new Set<AvailableViews>(),
registerView: () => undefined,
unregisterView: () => undefined,
});
openWidget: () => undefined,
closeWidget: () => undefined,
setTargetPeer: () => undefined,
targetWidgetVisibility: 'closed' as const,
targetPeer: undefined,
};

export const MediaCallInstanceContext = createContext<MediaCallInstanceContextValue>(defaultContextValue);

export const useMediaCallInstance = (): MediaCallInstanceContextValue => useContext(MediaCallInstanceContext);
11 changes: 7 additions & 4 deletions packages/ui-voip/src/context/MediaCallViewContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type MediaCallStreams = {

type MediaCallViewContextValue = {
sessionState: SessionState;
targetPeer?: PeerInfo;
onClickDirectMessage?: () => void;
onMute: () => void;
onHold: () => void;
Expand All @@ -28,13 +29,13 @@ type MediaCallViewContextValue = {
streams: MediaCallStreams;
widgetPositionTracker?: {
onChangePosition: (position: LastKnownPosition | null) => void;
getRestorePosition: () => LastKnownPosition | null;
lastKnownPosition: LastKnownPosition | null;
};
};

const defaultSessionState: SessionState = {
state: 'closed',
connectionState: 'CONNECTED',
export const defaultSessionState: SessionState = {
state: 'none',
connectionState: 'CONNECTING',
peerInfo: undefined,
transferredBy: undefined,
hidden: false,
Expand All @@ -43,11 +44,13 @@ const defaultSessionState: SessionState = {
remoteMuted: false,
remoteHeld: false,
callId: undefined,
startedAt: undefined,
supportedFeatures: ['audio', 'transfer', 'hold'],
};

export const defaultMediaCallContextValue: MediaCallViewContextValue = {
sessionState: defaultSessionState,
targetPeer: undefined,
onMute: () => undefined,
onHold: () => undefined,
onDeviceChange: () => undefined,
Expand Down
4 changes: 2 additions & 2 deletions packages/ui-voip/src/context/definitions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type ConnectionState = 'CONNECTED' | 'CONNECTING' | 'RECONNECTING';

export type PeerInfo = InternalPeerInfo | ExternalPeerInfo;

export type State = 'closed' | 'new' | 'calling' | 'ringing' | 'ongoing';
export type State = 'none' | 'calling' | 'ringing' | 'ongoing';

interface IBaseSession {
state: State;
Expand All @@ -35,7 +35,7 @@ interface IBaseSession {
}

interface IEmptySession extends IBaseSession {
state: Extract<State, 'closed' | 'new'>;
state: Extract<State, 'none'>;
callId: undefined;
}

Expand Down
12 changes: 2 additions & 10 deletions packages/ui-voip/src/context/usePeekMediaSessionPeerInfo.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import type { CallContact } from '@rocket.chat/media-signaling';
import { renderHook, act } from '@testing-library/react';
import type { ReactNode } from 'react';

import type { Signals } from './MediaCallInstanceContext';
import { MediaCallInstanceContext } from './MediaCallInstanceContext';
import { MediaCallInstanceContext, defaultContextValue } from './MediaCallInstanceContext';
import { usePeekMediaSessionPeerInfo } from './usePeekMediaSessionPeerInfo';

type MockInstance = {
Expand All @@ -16,15 +15,8 @@ const createWrapper = (instance: MockInstance | undefined) => {
const wrapper = ({ children }: { children?: ReactNode }) => (
<MediaCallInstanceContext.Provider
value={{
currentViews: [],
registerView: () => undefined,
unregisterView: () => undefined,
...defaultContextValue,
instance: instance as any,
signalEmitter: new Emitter<Signals>(),
audioElement: undefined,
openRoomId: undefined,
setOpenRoomId: () => undefined,
getAutocompleteOptions: () => Promise.resolve([]),
}}
>
{children}
Expand Down
19 changes: 2 additions & 17 deletions packages/ui-voip/src/context/usePeekMediaSessionState.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import type { CallRole, CallState } from '@rocket.chat/media-signaling';
import { renderHook, act } from '@testing-library/react';
import type { ReactNode } from 'react';

import type { Signals } from './MediaCallInstanceContext';
import { MediaCallInstanceContext } from './MediaCallInstanceContext';
import { usePeekMediaSessionState } from './usePeekMediaSessionState';
import MockedInstanceProvider from '../providers/MockedInstanceProvider';

type MockInstance = {
getState: () => { state: CallState; localParticipant: { role: CallRole } } | null;
Expand All @@ -14,21 +13,7 @@ type MockInstance = {

const createWrapper = (instance: MockInstance | undefined) => {
const wrapper = ({ children }: { children?: ReactNode }) => (
<MediaCallInstanceContext.Provider
value={{
currentViews: [],
registerView: () => undefined,
unregisterView: () => undefined,
instance: instance as any,
signalEmitter: new Emitter<Signals>(),
audioElement: undefined,
openRoomId: undefined,
setOpenRoomId: () => undefined,
getAutocompleteOptions: () => Promise.resolve([]),
}}
>
{children}
</MediaCallInstanceContext.Provider>
<MockedInstanceProvider instance={instance as any}>{children}</MockedInstanceProvider>
);
return wrapper;
};
Expand Down
22 changes: 2 additions & 20 deletions packages/ui-voip/src/context/usePeerAutocomplete.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { UserStatus } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { mockAppRoot } from '@rocket.chat/mock-providers';
import { renderHook, waitFor, act } from '@testing-library/react';

import type { PeerInfo } from './definitions';
import type { PeerAutocompleteOptions } from '../components';
import type { Signals } from './MediaCallInstanceContext';
import { MediaCallInstanceContext } from './MediaCallInstanceContext';
import { usePeerAutocomplete, isFirstPeerAutocompleteOption } from './usePeerAutocomplete';
import MockedInstanceProvider from '../providers/MockedInstanceProvider';

jest.mock('@rocket.chat/ui-contexts', () => ({
...jest.requireActual('@rocket.chat/ui-contexts'),
Expand All @@ -21,23 +19,7 @@ const mockOnSelectPeer = jest.fn();

const appRoot = () =>
mockAppRoot()
.wrap((children) => (
<MediaCallInstanceContext.Provider
value={{
currentViews: [],
registerView: () => undefined,
unregisterView: () => undefined,
instance: undefined,
signalEmitter: new Emitter<Signals>(),
audioElement: undefined,
openRoomId: undefined,
setOpenRoomId: () => undefined,
getAutocompleteOptions: mockGetAutocompleteOptions,
}}
>
{children}
</MediaCallInstanceContext.Provider>
))
.wrap((children) => <MockedInstanceProvider getAutocompleteOptions={mockGetAutocompleteOptions}>{children}</MockedInstanceProvider>)
.build();

beforeEach(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui-voip/src/context/useRegisterView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useLayoutEffect } from 'react';
import { useMediaCallInstance } from '.';
import type { AvailableViews } from './MediaCallInstanceContext';

const useRegisterView = (view: AvailableViews): AvailableViews[] => {
const useRegisterView = (view: AvailableViews) => {
const { currentViews, registerView, unregisterView } = useMediaCallInstance();

useLayoutEffect(() => {
Expand Down
10 changes: 7 additions & 3 deletions packages/ui-voip/src/context/useWidgetExternalControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import type { PeerInfo } from './definitions';
import { getEndCall } from '../utils/instanceControlsGetters';

export const useWidgetExternalControls = () => {
const { signalEmitter, instance } = useMediaCallInstance();
const { instance, openWidget, closeWidget, targetWidgetVisibility } = useMediaCallInstance();

const toggleWidget = useCallback(
(peerInfo?: PeerInfo) => {
signalEmitter.emit('toggleWidget', { peerInfo });
if (targetWidgetVisibility === 'closed') {
openWidget(peerInfo);
return;
}
closeWidget();
},
[signalEmitter],
[closeWidget, openWidget, targetWidgetVisibility],
);

const endCall = useCallback(() => getEndCall(instance)(), [instance]);
Expand Down
20 changes: 2 additions & 18 deletions packages/ui-voip/src/hooks/useMediaCallOpenRoomTracker.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,14 @@
import { Emitter } from '@rocket.chat/emitter';
import { renderHook } from '@testing-library/react';
import type { ReactNode } from 'react';

import { useMediaCallOpenRoomTracker } from './useMediaCallOpenRoomTracker';
import type { Signals } from '../context/MediaCallInstanceContext';
import { MediaCallInstanceContext } from '../context/MediaCallInstanceContext';
import MockedInstanceProvider from '../providers/MockedInstanceProvider';

const setOpenRoomIdMock = jest.fn();

const createWrapper = () => {
const wrapper = ({ children }: { children?: ReactNode }) => (
<MediaCallInstanceContext.Provider
value={{
currentViews: [],
registerView: () => undefined,
unregisterView: () => undefined,
instance: undefined,
signalEmitter: new Emitter<Signals>(),
audioElement: undefined,
openRoomId: undefined,
setOpenRoomId: setOpenRoomIdMock,
getAutocompleteOptions: () => Promise.resolve([]),
}}
>
{children}
</MediaCallInstanceContext.Provider>
<MockedInstanceProvider setOpenRoomId={setOpenRoomIdMock}>{children}</MockedInstanceProvider>
);
return wrapper;
};
Expand Down
32 changes: 25 additions & 7 deletions packages/ui-voip/src/providers/MediaCallInstanceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { Emitter } from '@rocket.chat/emitter';
import { useUser } from '@rocket.chat/ui-contexts';
import { useMemo, useState, type ReactNode } from 'react';
import { useMemo, type ReactNode } from 'react';
import { createPortal } from 'react-dom';

import { useAudioStream } from './useAudioStream';
import useAvailableViewTracker from './useAvailableViewTracker';
import { useGetAutocompleteOptions } from './useGetAutocompleteOptions';
import { useInstanceState } from './useInstanceState';
import { useMediaSessionInstance } from './useMediaSessionInstance';
import { MediaCallInstanceContext } from '../context/MediaCallInstanceContext';
import type { Signals } from '../context/MediaCallInstanceContext';

export type MediaCallInstanceProviderProps = {
children: ReactNode;
enabled?: boolean;
};

const MediaCallInstanceProvider = ({ children, enabled = true }: MediaCallInstanceProviderProps) => {
const [openRoomId, setOpenRoomId] = useState<string | undefined>(undefined);
const { currentViews, registerView, unregisterView } = useAvailableViewTracker();
const user = useUser();
const instance = useMediaSessionInstance(user?._id, enabled);
const [signalEmitter] = useState(() => new Emitter<Signals>());

const { openWidget, closeWidget, targetPeer, setTargetPeer, targetWidgetVisibility, openRoomId, setOpenRoomId } =
useInstanceState(instance);

const [remoteStreamRefCallback, audioElement] = useAudioStream(instance);

Expand All @@ -29,16 +29,34 @@ const MediaCallInstanceProvider = ({ children, enabled = true }: MediaCallInstan
const value = useMemo(
() => ({
instance,
signalEmitter,
audioElement,
openRoomId,
setOpenRoomId,
getAutocompleteOptions,
currentViews,
registerView,
unregisterView,
openWidget,
closeWidget,
setTargetPeer,
targetPeer,
targetWidgetVisibility,
}),
[instance, signalEmitter, audioElement, openRoomId, setOpenRoomId, getAutocompleteOptions, currentViews, registerView, unregisterView],
[
instance,
audioElement,
openRoomId,
setOpenRoomId,
getAutocompleteOptions,
currentViews,
registerView,
unregisterView,
openWidget,
closeWidget,
setTargetPeer,
targetPeer,
targetWidgetVisibility,
],
);

return (
Expand Down
Loading
Loading