Extract useVoiceBroadcastRecording hook (#9406)

* Extract useVoiceBroadcastRecording hook

* Trigger CI
This commit is contained in:
Michael Weimann 2022-10-13 14:02:53 +02:00 committed by GitHub
parent 1800cb8c71
commit 7a1c47a23e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 192 additions and 185 deletions

View file

@ -14,42 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useState } from "react"; import React from "react";
import { import {
VoiceBroadcastInfoState,
VoiceBroadcastRecordingBody, VoiceBroadcastRecordingBody,
VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingEvent,
} from ".."; } from "..";
import { IBodyProps } from "../../components/views/messages/IBodyProps"; import { IBodyProps } from "../../components/views/messages/IBodyProps";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => { export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(mxEvent.getRoomId());
const recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(mxEvent, client); const recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(mxEvent, client);
const [recordingState, setRecordingState] = useState(recording.getState());
useTypedEventEmitter(
recording,
VoiceBroadcastRecordingEvent.StateChanged,
(state: VoiceBroadcastInfoState, _recording: VoiceBroadcastRecording) => {
setRecordingState(state);
},
);
const stopVoiceBroadcast = () => {
if (recordingState !== VoiceBroadcastInfoState.Started) return;
recording.stop();
};
return <VoiceBroadcastRecordingBody return <VoiceBroadcastRecordingBody
onClick={stopVoiceBroadcast} recording={recording}
live={recordingState === VoiceBroadcastInfoState.Started}
sender={mxEvent.sender}
roomName={room.name}
/>; />;
}; };

View file

@ -1,12 +1,9 @@
/* /*
Copyright 2022 The Matrix.org Foundation C.I.C. Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -14,28 +11,26 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { MouseEventHandler } from "react"; import React from "react";
import { RoomMember } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastHeader } from "../.."; import { useVoiceBroadcastRecording, VoiceBroadcastHeader, VoiceBroadcastRecording } from "../..";
interface VoiceBroadcastRecordingBodyProps { interface VoiceBroadcastRecordingBodyProps {
live: boolean; recording: VoiceBroadcastRecording;
onClick: MouseEventHandler<HTMLDivElement>;
roomName: string;
sender: RoomMember;
} }
export const VoiceBroadcastRecordingBody: React.FC<VoiceBroadcastRecordingBodyProps> = ({ export const VoiceBroadcastRecordingBody: React.FC<VoiceBroadcastRecordingBodyProps> = ({ recording }) => {
live, const {
onClick, live,
roomName, roomName,
sender, sender,
}) => { stopRecording,
} = useVoiceBroadcastRecording(recording);
return ( return (
<div <div
className="mx_VoiceBroadcastRecordingBody" className="mx_VoiceBroadcastRecordingBody"
onClick={onClick} onClick={stopRecording}
> >
<VoiceBroadcastHeader <VoiceBroadcastHeader
live={live} live={live}

View file

@ -0,0 +1,51 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { useState } from "react";
import {
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingEvent,
VoiceBroadcastRecordingsStore,
} from "..";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
import { MatrixClientPeg } from "../../MatrixClientPeg";
export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(recording.infoEvent.getRoomId());
const stopRecording = () => {
recording.stop();
VoiceBroadcastRecordingsStore.instance().clearCurrent();
};
const [live, setLive] = useState(recording.getState() === VoiceBroadcastInfoState.Started);
useTypedEventEmitter(
recording,
VoiceBroadcastRecordingEvent.StateChanged,
(state: VoiceBroadcastInfoState, _recording: VoiceBroadcastRecording) => {
setLive(state === VoiceBroadcastInfoState.Started);
},
);
return {
live,
roomName: room.name,
sender: recording.infoEvent.sender,
stopRecording,
};
};

View file

@ -30,6 +30,7 @@ export * from "./models/VoiceBroadcastRecording";
export * from "./stores/VoiceBroadcastRecordingsStore"; export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/shouldDisplayAsVoiceBroadcastTile"; export * from "./utils/shouldDisplayAsVoiceBroadcastTile";
export * from "./utils/startNewVoiceBroadcastRecording"; export * from "./utils/startNewVoiceBroadcastRecording";
export * from "./hooks/useVoiceBroadcastRecording";
export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info"; export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info";
export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk"; export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk";

View file

@ -50,6 +50,13 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
return this.current; return this.current;
} }
public clearCurrent(): void {
if (this.current === null) return;
this.current = null;
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null);
}
public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording { public getByInfoEvent(infoEvent: MatrixEvent, client: MatrixClient): VoiceBroadcastRecording {
const infoEventId = infoEvent.getId(); const infoEventId = infoEvent.getId();

View file

@ -16,9 +16,8 @@ limitations under the License.
import React from "react"; import React from "react";
import { render, screen } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock"; import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { import {
VoiceBroadcastBody, VoiceBroadcastBody,
@ -27,10 +26,8 @@ import {
VoiceBroadcastRecordingBody, VoiceBroadcastRecordingBody,
VoiceBroadcastRecordingsStore, VoiceBroadcastRecordingsStore,
VoiceBroadcastRecording, VoiceBroadcastRecording,
VoiceBroadcastRecordingEvent,
} from "../../../src/voice-broadcast"; } from "../../../src/voice-broadcast";
import { mkEvent, mkStubRoom, stubClient } from "../../test-utils"; import { mkEvent, stubClient } from "../../test-utils";
import { IBodyProps } from "../../../src/components/views/messages/IBodyProps";
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
VoiceBroadcastRecordingBody: jest.fn(), VoiceBroadcastRecordingBody: jest.fn(),
@ -38,12 +35,9 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecor
describe("VoiceBroadcastBody", () => { describe("VoiceBroadcastBody", () => {
const roomId = "!room:example.com"; const roomId = "!room:example.com";
const recordingTestid = "voice-recording";
let client: MatrixClient; let client: MatrixClient;
let room: Room;
let infoEvent: MatrixEvent; let infoEvent: MatrixEvent;
let recording: VoiceBroadcastRecording; let testRecording: VoiceBroadcastRecording;
let onRecordingStateChanged: (state: VoiceBroadcastInfoState) => void;
const mkVoiceBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => { const mkVoiceBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkEvent({ return mkEvent({
@ -58,104 +52,39 @@ describe("VoiceBroadcastBody", () => {
}; };
const renderVoiceBroadcast = () => { const renderVoiceBroadcast = () => {
const props: IBodyProps = { render(<VoiceBroadcastBody
mxEvent: infoEvent, mxEvent={infoEvent}
} as unknown as IBodyProps; mediaEventHelper={null}
render(<VoiceBroadcastBody {...props} />); onHeightChanged={() => {}}
recording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(infoEvent, client); onMessageAllowed={() => {}}
recording.on(VoiceBroadcastRecordingEvent.StateChanged, onRecordingStateChanged); permalinkCreator={null}
}; />);
testRecording = VoiceBroadcastRecordingsStore.instance().getByInfoEvent(infoEvent, client);
const itShouldRenderALiveVoiceBroadcast = () => {
it("should render a live voice broadcast", () => {
expect(VoiceBroadcastRecordingBody).toHaveBeenCalledWith(
{
onClick: expect.any(Function),
live: true,
sender: infoEvent.sender,
roomName: room.name,
},
{},
);
screen.getByTestId(recordingTestid);
screen.getByText("Live");
});
};
const itShouldRenderANonLiveVoiceBroadcast = () => {
it("should render a non-live voice broadcast", () => {
expect(VoiceBroadcastRecordingBody).toHaveBeenCalledWith(
{
onClick: expect.any(Function),
live: false,
sender: infoEvent.sender,
roomName: room.name,
},
{},
);
expect(screen.getByTestId(recordingTestid)).not.toBeNull();
screen.getByTestId(recordingTestid);
expect(screen.queryByText("live")).toBeNull();
});
}; };
beforeEach(() => { beforeEach(() => {
mocked(VoiceBroadcastRecordingBody).mockImplementation(
({
live,
sender,
onClick,
roomName,
}) => {
return (
<div
data-testid={recordingTestid}
onClick={onClick}
>
<div>{ sender.name }</div>
<div>{ roomName }</div>
<div>{ live && "Live" }</div>
</div>
);
},
);
client = stubClient(); client = stubClient();
room = mkStubRoom(roomId, "test room", client); infoEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started);
mocked(client.getRoom).mockImplementation((getRoomId: string) => { testRecording = new VoiceBroadcastRecording(infoEvent, client);
if (getRoomId === roomId) { mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => {
return room; if (testRecording === recording) {
return <div data-testid="voice-broadcast-recording-body" />;
} }
}); });
infoEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started);
onRecordingStateChanged = jest.fn(); jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "getByInfoEvent").mockImplementation(
(getEvent: MatrixEvent, getClient: MatrixClient) => {
if (getEvent === infoEvent && getClient === client) {
return testRecording;
}
},
);
}); });
afterEach(() => { describe("when rendering a voice broadcast", () => {
if (recording && onRecordingStateChanged) { it("should render a voice broadcast recording body", () => {
recording.off(VoiceBroadcastRecordingEvent.StateChanged, onRecordingStateChanged);
}
});
describe("when there is a Started Voice Broadcast info event", () => {
beforeEach(() => {
renderVoiceBroadcast(); renderVoiceBroadcast();
}); screen.getByTestId("voice-broadcast-recording-body");
itShouldRenderALiveVoiceBroadcast();
describe("and it is clicked", () => {
beforeEach(async () => {
mocked(VoiceBroadcastRecordingBody).mockClear();
mocked(onRecordingStateChanged).mockClear();
await userEvent.click(screen.getByTestId(recordingTestid));
});
itShouldRenderANonLiveVoiceBroadcast();
it("should call stop on the recording", () => {
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped);
expect(onRecordingStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Stopped);
});
}); });
}); });
}); });

View file

@ -14,45 +14,43 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { MouseEventHandler } from "react"; import React from "react";
import { render, RenderResult } from "@testing-library/react"; import { render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { RoomMember } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastHeader, VoiceBroadcastRecordingBody } from "../../../../src/voice-broadcast"; import {
VoiceBroadcastInfoEventType,
jest.mock("../../../../src/voice-broadcast/components/atoms/VoiceBroadcastHeader", () => ({ VoiceBroadcastInfoState,
VoiceBroadcastHeader: ({ live, sender, roomName }: React.ComponentProps<typeof VoiceBroadcastHeader>) => { VoiceBroadcastRecording,
return <div data-testid="voice-broadcast-header"> VoiceBroadcastRecordingBody,
live: { live }, } from "../../../../src/voice-broadcast";
sender: { sender.userId }, import { mkEvent, stubClient } from "../../../test-utils";
room name: { roomName }
</div>;
},
}));
describe("VoiceBroadcastRecordingBody", () => { describe("VoiceBroadcastRecordingBody", () => {
const testRoomName = "test room name";
const userId = "@user:example.com"; const userId = "@user:example.com";
const roomMember = new RoomMember("!room:example.com", userId); const roomId = "!room:example.com";
let onClick: MouseEventHandler<HTMLDivElement>; let client: MatrixClient;
let infoEvent: MatrixEvent;
let recording: VoiceBroadcastRecording;
beforeEach(() => { beforeAll(() => {
onClick = jest.fn(); client = stubClient();
infoEvent = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
content: {},
room: roomId,
user: userId,
});
recording = new VoiceBroadcastRecording(infoEvent, client);
}); });
describe("when rendered", () => { describe("when rendering a live broadcast", () => {
let renderResult: RenderResult; let renderResult: RenderResult;
beforeEach(() => { beforeEach(() => {
renderResult = render( renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />);
<VoiceBroadcastRecordingBody
onClick={onClick}
roomName={testRoomName}
live={true}
sender={roomMember}
/>,
);
}); });
it("should render the expected HTML", () => { it("should render the expected HTML", () => {
@ -61,27 +59,21 @@ describe("VoiceBroadcastRecordingBody", () => {
describe("and clicked", () => { describe("and clicked", () => {
beforeEach(async () => { beforeEach(async () => {
await userEvent.click(renderResult.getByTestId("voice-broadcast-header")); await userEvent.click(renderResult.getByText("My room"));
}); });
it("should call the onClick prop", () => { it("should stop the recording", () => {
expect(onClick).toHaveBeenCalled(); expect(recording.getState()).toBe(VoiceBroadcastInfoState.Stopped);
}); });
}); });
}); });
describe("when non-live rendered", () => { describe("when rendering a non-live broadcast", () => {
let renderResult: RenderResult; let renderResult: RenderResult;
beforeEach(() => { beforeEach(() => {
renderResult = render( recording.stop();
<VoiceBroadcastRecordingBody renderResult = render(<VoiceBroadcastRecordingBody recording={recording} />);
onClick={onClick}
roomName={testRoomName}
live={false}
sender={roomMember}
/>,
);
}); });
it("should not render the live badge", () => { it("should not render the live badge", () => {

View file

@ -1,18 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastRecordingBody when rendered should render the expected HTML 1`] = ` exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should render the expected HTML 1`] = `
<div> <div>
<div <div
class="mx_VoiceBroadcastRecordingBody" class="mx_VoiceBroadcastRecordingBody"
> >
<div <div
data-testid="voice-broadcast-header" class="mx_VoiceBroadcastHeader"
> >
live: <span
, sender: class="mx_BaseAvatar"
@user:example.com role="presentation"
, room name: >
test room name <span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 26px; width: 40px; line-height: 40px;"
>
U
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
src="data:image/png;base64,00"
style="width: 40px; height: 40px;"
title="@user:example.com"
/>
</span>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_sender"
>
@user:example.com
</div>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
</div>
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
Live
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -97,6 +97,20 @@ describe("VoiceBroadcastRecordingsStore", () => {
expect(onCurrentChanged).not.toHaveBeenCalled(); expect(onCurrentChanged).not.toHaveBeenCalled();
}); });
}); });
describe("and calling clearCurrent()", () => {
beforeEach(() => {
recordings.clearCurrent();
});
it("should clear the current recording", () => {
expect(recordings.getCurrent()).toBeNull();
});
it("should emit a current changed event", () => {
expect(onCurrentChanged).toHaveBeenCalledWith(null);
});
});
}); });
describe("getByInfoEventId", () => { describe("getByInfoEventId", () => {