Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

New group call experience: Room header and PiP designs #9351

Merged
merged 14 commits into from
Oct 7, 2022
Merged
Prev Previous commit
Next Next commit
Bring the room header in line with the group call designs
robintown committed Oct 5, 2022
commit 50cd85b956cd2259834f959976a6507e5650f401
2 changes: 1 addition & 1 deletion cypress/e2e/lazy-loading/lazy-loading.spec.ts
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ describe("Lazy Loading", () => {
}

function openMemberlist(): void {
cy.get('.mx_HeaderButtons [aria-label="Room Info"]').click();
cy.get('.mx_HeaderButtons [aria-label="Room info"]').click();
cy.get(".mx_RoomSummaryCard").within(() => {
cy.get(".mx_RoomSummaryCard_icon_people").click();
});
17 changes: 0 additions & 17 deletions res/css/structures/_HeaderButtons.pcss
Original file line number Diff line number Diff line change
@@ -17,20 +17,3 @@ limitations under the License.
.mx_HeaderButtons {
display: flex;
}

.mx_RoomHeader_buttons + .mx_HeaderButtons {
/* remove the | separator line for when next to RoomHeaderButtons */
/* TODO: remove this once when we redo communities and make the right panel similar to the new rooms one */
&::before {
content: unset;
}
}

.mx_HeaderButtons::before {
content: "";
background-color: $header-panel-text-primary-color;
opacity: 0.5;
margin: 6px 8px;
border-radius: 1px;
width: 1px;
}
78 changes: 57 additions & 21 deletions res/css/views/rooms/_RoomHeader.pcss
Original file line number Diff line number Diff line change
@@ -19,17 +19,28 @@ limitations under the License.
border-bottom: 1px solid $primary-hairline-color;
background-color: $background;

.mx_RoomHeader_e2eIcon {
.mx_RoomHeader_icon {
height: 12px;
width: 12px;

.mx_E2EIcon {
&.mx_RoomHeader_icon_video {
height: 14px;
width: 14px;
background-color: $secondary-content;
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
mask-size: 100%;
}

&.mx_E2EIcon {
margin: 0;
position: absolute;
height: 12px;
width: 12px;
height: 100%; /* To give the tooltip room to breathe */
}
}

.mx_CallDuration {
margin-top: calc(($font-15px - $font-13px) / 2); /* To align with the name */
font-size: $font-13px;
}
}

.mx_RoomHeader_wrapper {
@@ -38,7 +49,7 @@ limitations under the License.
align-items: center;
min-width: 0;
margin: 0 20px 0 16px;
padding-top: 8px;
padding-top: 6px;
border-bottom: 1px solid $system;

.mx_InviteOnlyIcon_large {
@@ -77,11 +88,6 @@ limitations under the License.
padding-right: 12px;
}

.mx_RoomHeader_buttons {
display: flex;
background-color: $background;
}

.mx_RoomHeader_info {
display: flex;
flex: 1;
@@ -93,9 +99,11 @@ limitations under the License.
overflow: hidden;
color: $primary-content;
font-weight: $font-semi-bold;
font-size: $font-18px;
font-size: $font-15px;
min-height: 24px;
align-items: center;
border-radius: 6px;
margin: 0 7px;
margin: 0 3px;
padding: 1px 4px;
display: flex;
user-select: none;
@@ -112,10 +120,10 @@ limitations under the License.

.mx_RoomHeader_chevron {
align-self: center;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
mask-position: center;
mask-size: contain;
mask-size: 20px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
background-color: $tertiary-content;
@@ -160,9 +168,6 @@ limitations under the License.
line-height: $lineHeight;
max-height: calc($lineHeight * $lines);

/* to align baseline of topic with room name */
margin: 4px 7px 0;

overflow: hidden;
-webkit-line-clamp: $lines; /* See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp */
-webkit-box-orient: vertical;
@@ -177,7 +182,7 @@ limitations under the License.

.mx_RoomHeader_avatar {
flex: 0;
margin: 0 6px 0 7px;
margin: 0 7px;
position: relative;
}

@@ -206,7 +211,7 @@ limitations under the License.
mask-size: contain;
}

&:hover {
&:not(.mx_RoomHeader_closeButton):hover {
background: rgba($accent, 0.1);

&::before {
@@ -249,6 +254,37 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}

.mx_RoomHeader_layoutButton--freedom::before,
.mx_RoomHeader_freedomIcon::before {
mask-image: url('$(res)/img/element-icons/call/freedom.svg');
}

.mx_RoomHeader_layoutButton--spotlight::before,
.mx_RoomHeader_spotlightIcon::before {
mask-image: url('$(res)/img/element-icons/call/spotlight.svg');
}

.mx_RoomHeader_closeButton::before {
mask-image: url('$(res)/img/cancel.svg');
mask-size: 20px;
mask-position: center;
}

.mx_RoomHeader_minimiseButton::before {
mask-image: url('$(res)/img/element-icons/reduce.svg');
}

.mx_RoomHeader_layoutMenu .mx_IconizedContextMenu_icon::before {
content: '';
width: 16px;
height: 16px;
display: block;
mask-position: center;
mask-size: 20px;
mask-repeat: no-repeat;
background: $primary-content;
}

@media only screen and (max-width: 480px) {
.mx_RoomHeader_wrapper {
padding: 0;
2 changes: 1 addition & 1 deletion res/css/views/voip/_LegacyCallViewHeader.pcss
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ limitations under the License.
width: 100%;

&.mx_LegacyCallViewHeader_pip {
cursor: pointer;
cursor: grab;
}
}

3 changes: 3 additions & 0 deletions res/img/element-icons/call/freedom.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions res/img/element-icons/call/spotlight.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions res/img/element-icons/reduce.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 24 additions & 1 deletion src/components/structures/RoomView.tsx
Original file line number Diff line number Diff line change
@@ -121,6 +121,7 @@ import { LargeLoader } from './LargeLoader';
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
import { isVideoRoom } from '../../utils/video-rooms';
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
import { Call } from "../../models/Call";

const DEBUG = false;
let debuglog = function(msg: string) {};
@@ -178,6 +179,7 @@ export interface IRoomState {
searchHighlights?: string[];
searchInProgress?: boolean;
callState?: CallState;
activeCall: Call | null;
canPeek: boolean;
canSelfRedact: boolean;
showApps: boolean;
@@ -303,6 +305,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
<main className="mx_RoomView_body" ref={props.roomView}>
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
@@ -353,6 +357,8 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
excludedRightPanelPhaseButtons={[]}
showButtons={false}
enableRoomOptionsMenu={false}
viewingCall={false}
activeCall={null}
/>
<div className="mx_RoomView_body">
<LargeLoader text={text} />
@@ -391,6 +397,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
numUnreadMessages: 0,
searchResults: null,
callState: null,
activeCall: null,
canPeek: false,
canSelfRedact: false,
showApps: false,
@@ -564,6 +571,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
initialEventId: null, // default to clearing this, will get set later in the method if needed
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
activeCall: CallStore.instance.getActiveCall(roomId),
};

if (
@@ -707,7 +715,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
};

private onActiveCalls = () => {
if (this.state.roomId !== undefined && !CallStore.instance.hasActiveCall(this.state.roomId)) {
if (this.state.roomId === undefined) return;
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);

if (activeCall === null) {
// We disconnected from the call, so stop viewing it
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -716,6 +727,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
metricsTrigger: undefined,
}, true); // Synchronous so that CallView disappears immediately
}

this.setState({ activeCall });
};

private getRoomId = () => {
@@ -2410,6 +2423,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let onForgetClick = this.onForgetClick;
let onSearchClick = this.onSearchClick;
let onInviteClick = null;
let viewingCall = false;

// Simplify the header for other main split types
switch (this.state.mainSplitContentType) {
@@ -2428,12 +2442,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
RightPanelPhases.PinnedMessages,
RightPanelPhases.NotificationPanel,
];
if (!isVideoRoom(this.state.room)) {
excludedRightPanelPhaseButtons.push(RightPanelPhases.RoomSummary);
if (this.state.activeCall === null) {
excludedRightPanelPhaseButtons.push(RightPanelPhases.Timeline);
}
}
onAppsClick = null;
onForgetClick = null;
onSearchClick = null;
if (this.state.room.canInvite(this.context.credentials.userId)) {
onInviteClick = this.onInviteClick;
}
viewingCall = true;
}

return (
@@ -2457,6 +2478,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
showButtons={!this.viewsLocalRoom}
enableRoomOptionsMenu={!this.viewsLocalRoom}
viewingCall={viewingCall}
activeCall={this.state.activeCall}
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>
2 changes: 2 additions & 0 deletions src/components/views/right_panel/HeaderButton.tsx
Original file line number Diff line number Diff line change
@@ -23,6 +23,7 @@ import classNames from 'classnames';

import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import { ButtonEvent } from "../elements/AccessibleButton";
import { Alignment } from "../elements/Tooltip";

interface IProps {
// Whether this button is highlighted
@@ -54,6 +55,7 @@ export default class HeaderButton extends React.Component<IProps> {
aria-selected={isHighlighted}
role="tab"
title={title}
alignment={Alignment.Bottom}
className={classes}
onClick={onClick}
/>;
2 changes: 1 addition & 1 deletion src/components/views/right_panel/RoomHeaderButtons.tsx
Original file line number Diff line number Diff line change
@@ -282,7 +282,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t('Room Info')}
title={_t('Room info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
/>,
16 changes: 13 additions & 3 deletions src/components/views/rooms/E2EIcon.tsx
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import classNames from 'classnames';

import { _t, _td } from '../../../languageHandler';
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip";
import Tooltip, { Alignment } from "../elements/Tooltip";
import { E2EStatus } from "../../../utils/ShieldUtils";

export enum E2EState {
@@ -49,10 +49,20 @@ interface IProps {
size?: number;
onClick?: () => void;
hideTooltip?: boolean;
tooltipAlignment?: Alignment;
bordered?: boolean;
}

const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
const E2EIcon: React.FC<IProps> = ({
isUser,
status,
className,
size,
onClick,
hideTooltip,
tooltipAlignment,
bordered,
}) => {
const [hover, setHover] = useState(false);

const classes = classNames({
@@ -80,7 +90,7 @@ const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, h

let tip;
if (hover && !hideTooltip) {
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
}

if (onClick) {
252 changes: 197 additions & 55 deletions src/components/views/rooms/RoomHeader.tsx

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/components/views/rooms/RoomTile.tsx
Original file line number Diff line number Diff line change
@@ -89,7 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
notificationsMenuPosition: null,
generalMenuPosition: null,
call: CallStore.instance.get(this.props.room.roomId),
call: CallStore.instance.getCall(this.props.room.roomId),
// generatePreview() will return nothing if the user has previews disabled
messagePreview: "",
};
@@ -159,7 +159,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {

// Recalculate the call for this room, since it could've changed between
// construction and mounting
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
this.setState({ call: CallStore.instance.getCall(this.props.room.roomId) });
}

public componentWillUnmount() {
1 change: 1 addition & 0 deletions src/contexts/RoomContext.ts
Original file line number Diff line number Diff line change
@@ -65,6 +65,7 @@ const RoomContext = createContext<IRoomState>({
threadId: undefined,
liveTimeline: undefined,
narrow: false,
activeCall: null,
});
RoomContext.displayName = "RoomContext";
export default RoomContext;
7 changes: 6 additions & 1 deletion src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
@@ -1888,12 +1888,17 @@
"You do not have permission to start video calls": "You do not have permission to start video calls",
"There's no one here to call": "There's no one here to call",
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
"Freedom": "Freedom",
"Spotlight": "Spotlight",
"Layout type": "Layout type",
"Forget room": "Forget room",
"Hide Widgets": "Hide Widgets",
"Show Widgets": "Show Widgets",
"Search": "Search",
"Invite": "Invite",
"Room options": "Room options",
"Close call": "Close call",
"View chat timeline": "View chat timeline",
"(~%(count)s results)|other": "(~%(count)s results)",
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
@@ -2089,7 +2094,7 @@
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
"Pinned messages": "Pinned messages",
"Chat": "Chat",
"Room Info": "Room Info",
"Room info": "Room info",
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
"Maximise": "Maximise",
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
14 changes: 7 additions & 7 deletions src/stores/CallStore.ts
Original file line number Diff line number Diff line change
@@ -73,7 +73,7 @@ export class CallStore extends AsyncStoreWithClient<{}> {
await Promise.all([
...uncleanlyDisconnectedRoomIds.map(async uncleanlyDisconnectedRoomId => {
logger.log(`Cleaning up call state for room ${uncleanlyDisconnectedRoomId}`);
await this.get(uncleanlyDisconnectedRoomId)?.clean();
await this.getCall(uncleanlyDisconnectedRoomId)?.clean();
}),
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []),
]);
@@ -152,18 +152,18 @@ export class CallStore extends AsyncStoreWithClient<{}> {
* @param {string} roomId The room's ID.
* @returns {Call | null} The call.
*/
public get(roomId: string): Call | null {
public getCall(roomId: string): Call | null {
return this.calls.get(roomId) ?? null;
}

/**
* Determines whether the given room has an active call.
* Gets the active call associated with the given room, if any.
* @param roomId The room's ID.
* @returns Whether the given room has an active call.
* @returns The active call.
*/
public hasActiveCall(roomId: string): boolean {
const call = this.get(roomId);
return call !== null && this.activeCalls.has(call);
public getActiveCall(roomId: string): Call | null {
const call = this.getCall(roomId);
return call !== null && this.activeCalls.has(call) ? call : null;
}

private onRoom = (room: Room) => this.updateRoom(room);
2 changes: 1 addition & 1 deletion src/stores/RoomViewStore.tsx
Original file line number Diff line number Diff line change
@@ -365,7 +365,7 @@ export class RoomViewStore extends EventEmitter {
viewingCall: payload.view_call ?? (
payload.room_id === this.state.roomId
? this.state.viewingCall
: CallStore.instance.hasActiveCall(payload.room_id)
: CallStore.instance.getActiveCall(payload.room_id) !== null
),
};

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions test/components/views/messages/CallEvent-test.tsx
Original file line number Diff line number Diff line change
@@ -82,7 +82,7 @@ describe("CallEvent", () => {
));

MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;

@@ -113,7 +113,7 @@ describe("CallEvent", () => {
});

it("shows placeholder info if the call isn't loaded yet", () => {
jest.spyOn(CallStore.instance, "get").mockReturnValue(null);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
jest.advanceTimersByTime(90000);
renderEvent();

Original file line number Diff line number Diff line change
@@ -268,6 +268,7 @@ function createRoomState(room: Room, narrow: boolean): IRoomState {
liveTimeline: undefined,
resizing: false,
narrow,
activeCall: null,
};
}

109 changes: 107 additions & 2 deletions test/components/views/rooms/RoomHeader-test.tsx
Original file line number Diff line number Diff line change
@@ -25,6 +25,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { ClientWidgetApi, Widget } from "matrix-widget-api";
import EventEmitter from "events";

import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -53,6 +55,10 @@ import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import WidgetStore from "../../../../src/stores/WidgetStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import WidgetUtils from "../../../../src/utils/WidgetUtils";
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";

describe('RoomHeader (Enzyme)', () => {
it('shows the room avatar in a room with only ourselves', () => {
@@ -173,13 +179,13 @@ describe('RoomHeader (Enzyme)', () => {
it("should render buttons if not passing showButtons (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(1);
expect(wrapper.find(".mx_RoomHeader_button")).not.toHaveLength(0);
});

it("should not render buttons if passing showButtons = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { showButtons: false });
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(0);
expect(wrapper.find(".mx_RoomHeader_button")).toHaveLength(0);
});

it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
@@ -252,6 +258,8 @@ function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoom
searchScope: SearchScope.Room,
searchCount: 0,
},
viewingCall: false,
activeCall: null,
...propsOverride,
};

@@ -381,6 +389,12 @@ describe("RoomHeader (React Testing Library)", () => {
await Promise.all([CallStore.instance, WidgetStore.instance].map(
store => setupAsyncStoreWithClient(store, client),
));

jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
[MediaDeviceKindEnum.AudioInput]: [],
[MediaDeviceKindEnum.VideoInput]: [],
[MediaDeviceKindEnum.AudioOutput]: [],
});
});

afterEach(async () => {
@@ -419,6 +433,32 @@ describe("RoomHeader (React Testing Library)", () => {
const mockLegacyCall = () => {
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue({} as unknown as MatrixCall);
};
const withCall = async (fn: (call: ElementCall) => (void | Promise<void>)): Promise<void> => {
await ElementCall.create(room);
const call = CallStore.instance.getCall(room.roomId);
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");

const widget = new Widget(call.widget);

const eventEmitter = new EventEmitter();
const messaging = {
on: eventEmitter.on.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
once: eventEmitter.once.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
stop: jest.fn(),
transport: {
send: jest.fn(),
reply: jest.fn(),
},
} as unknown as Mocked<ClientWidgetApi>;
WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging);

await fn(call);

call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, call.roomId);
};

const renderHeader = (props: Partial<RoomHeaderProps> = {}, roomContext: Partial<IRoomState> = {}) => {
render(
@@ -437,6 +477,8 @@ describe("RoomHeader (React Testing Library)", () => {
searchScope: SearchScope.Room,
searchCount: 0,
}}
viewingCall={false}
activeCall={null}
{...props}
/>
</RoomContext.Provider>,
@@ -724,4 +766,67 @@ describe("RoomHeader (React Testing Library)", () => {
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
});

it("shows a close button when viewing a call lobby that returns to the timeline when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);

renderHeader({ viewingCall: true });

const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: /close/i }));
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: false,
}));
defaultDispatcher.unregister(dispatcherRef);
});

it("shows a reduce button when viewing a call that returns to the timeline when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);

await withCall(async call => {
renderHeader({ viewingCall: true, activeCall: call });

const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: /timeline/i }));
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: false,
}));
defaultDispatcher.unregister(dispatcherRef);
});
});

it("shows a layout button when viewing a call that shows a menu when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);

await withCall(async call => {
await call.connect();
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget));
renderHeader({ viewingCall: true, activeCall: call });

// Should start with Freedom selected
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });

// Clicking Spotlight should tell the widget to switch and close the menu
fireEvent.click(screen.getByRole("menuitemradio", { name: "Spotlight" }));
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
expect(screen.queryByRole("menu")).toBeNull();

// When the widget responds and the user reopens the menu, they should see Spotlight selected
act(() => {
messaging.emit(
`action:${ElementWidgetActions.SpotlightLayout}`,
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
);
});
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
screen.getByRole("menuitemradio", { name: "Spotlight", checked: true });
});
});
});
2 changes: 1 addition & 1 deletion test/components/views/rooms/RoomTile-test.tsx
Original file line number Diff line number Diff line change
@@ -77,7 +77,7 @@ describe("RoomTile", () => {
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);

MockedCall.create(room, "1");
call = CallStore.instance.get(room.roomId) as MockedCall;
call = CallStore.instance.getCall(room.roomId) as MockedCall;

widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
1 change: 1 addition & 0 deletions test/components/views/rooms/SendMessageComposer-test.tsx
Original file line number Diff line number Diff line change
@@ -91,6 +91,7 @@ describe('<SendMessageComposer/>', () => {
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
describe("createMessageContent", () => {
const permalinkCreator = jest.fn() as any;
6 changes: 3 additions & 3 deletions test/components/views/voip/CallView-test.tsx
Original file line number Diff line number Diff line change
@@ -90,7 +90,7 @@ describe("CallLobby", () => {

beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;

@@ -171,8 +171,8 @@ describe("CallLobby", () => {
expect(Call.get(room)).toBeNull();

fireEvent.click(screen.getByRole("button", { name: "Join" }));
await waitFor(() => expect(CallStore.instance.get(room.roomId)).not.toBeNull());
const call = CallStore.instance.get(room.roomId)!;
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
const call = CallStore.instance.getCall(room.roomId)!;

const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
2 changes: 1 addition & 1 deletion test/stores/room-list/algorithms/Algorithm-test.ts
Original file line number Diff line number Diff line change
@@ -81,7 +81,7 @@ describe("Algorithm", () => {
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);

MockedCall.create(roomWithCall, "1");
const call = CallStore.instance.get(roomWithCall.roomId);
const call = CallStore.instance.getCall(roomWithCall.roomId);
if (call === null) throw new Error("Failed to create call");

const widget = new Widget(call.widget);