From d699f5607bc503400cb4901389d1cec1dff4b11f Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 21 Nov 2022 08:47:09 +0100 Subject: [PATCH] Add voice broadcast seek 30s forward/backward buttons (#9592) --- res/css/compound/_Icon.pcss | 6 + res/css/views/elements/_AccessibleButton.pcss | 4 + .../atoms/_VoiceBroadcastControl.pcss | 1 - .../molecules/_VoiceBroadcastBody.pcss | 5 +- res/img/element-icons/Back30s.svg | 1 + res/img/element-icons/Forward30s.svg | 1 + .../views/elements/AccessibleButton.tsx | 1 + src/i18n/strings/en_EN.json | 2 + .../components/atoms/SeekButton.tsx | 39 +++++ .../molecules/VoiceBroadcastPlaybackBody.tsx | 37 +++- .../hooks/useVoiceBroadcastPlayback.ts | 10 +- .../VoiceBroadcastPlaybackBody-test.tsx | 57 +++++- .../VoiceBroadcastPlaybackBody-test.tsx.snap | 163 +++++++++++++++++- 13 files changed, 316 insertions(+), 11 deletions(-) create mode 100644 res/img/element-icons/Back30s.svg create mode 100644 res/img/element-icons/Forward30s.svg create mode 100644 src/voice-broadcast/components/atoms/SeekButton.tsx diff --git a/res/css/compound/_Icon.pcss b/res/css/compound/_Icon.pcss index 1c8e9c98b1b..fe0924597f6 100644 --- a/res/css/compound/_Icon.pcss +++ b/res/css/compound/_Icon.pcss @@ -36,3 +36,9 @@ limitations under the License. flex: 0 0 16px; width: 16px; } + +.mx_Icon_24 { + height: 24px; + flex: 0 0 24px; + width: 24px; +} diff --git a/res/css/views/elements/_AccessibleButton.pcss b/res/css/views/elements/_AccessibleButton.pcss index dd3d0945e02..03a74653137 100644 --- a/res/css/views/elements/_AccessibleButton.pcss +++ b/res/css/views/elements/_AccessibleButton.pcss @@ -111,6 +111,10 @@ limitations under the License. color: $accent; } + &.mx_AccessibleButton_kind_secondary_content { + color: $secondary-content; + } + &.mx_AccessibleButton_kind_danger { color: $button-danger-fg-color; background-color: $alert; diff --git a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss b/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss index f7cba048709..ff892767e4a 100644 --- a/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss +++ b/res/css/voice-broadcast/atoms/_VoiceBroadcastControl.pcss @@ -22,7 +22,6 @@ limitations under the License. display: flex; height: 32px; justify-content: center; - margin-bottom: $spacing-8; width: 32px; } diff --git a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss index 0a16dc96f4e..bf4118b806b 100644 --- a/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss +++ b/res/css/voice-broadcast/molecules/_VoiceBroadcastBody.pcss @@ -36,8 +36,11 @@ limitations under the License. } .mx_VoiceBroadcastBody_controls { + align-items: center; display: flex; - justify-content: space-around; + gap: $spacing-32; + justify-content: center; + margin-bottom: $spacing-8; } .mx_VoiceBroadcastBody_timerow { diff --git a/res/img/element-icons/Back30s.svg b/res/img/element-icons/Back30s.svg new file mode 100644 index 00000000000..6caba0a015b --- /dev/null +++ b/res/img/element-icons/Back30s.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/res/img/element-icons/Forward30s.svg b/res/img/element-icons/Forward30s.svg new file mode 100644 index 00000000000..cc96f846e5f --- /dev/null +++ b/res/img/element-icons/Forward30s.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 18b5d4b60fb..cfc2de93cd6 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -26,6 +26,7 @@ type AccessibleButtonKind = | 'primary' | 'primary_outline' | 'primary_sm' | 'secondary' + | 'secondary_content' | 'content_inline' | 'danger' | 'danger_outline' diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 15a2aff2fd9..5146ef4922e 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -651,6 +651,8 @@ "play voice broadcast": "play voice broadcast", "resume voice broadcast": "resume voice broadcast", "pause voice broadcast": "pause voice broadcast", + "30s backward": "30s backward", + "30s forward": "30s forward", "Go live": "Go live", "Live": "Live", "Voice broadcast": "Voice broadcast", diff --git a/src/voice-broadcast/components/atoms/SeekButton.tsx b/src/voice-broadcast/components/atoms/SeekButton.tsx new file mode 100644 index 00000000000..11bf99123a6 --- /dev/null +++ b/src/voice-broadcast/components/atoms/SeekButton.tsx @@ -0,0 +1,39 @@ +/* +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 React from "react"; + +import AccessibleButton from "../../../components/views/elements/AccessibleButton"; + +interface Props { + icon: React.FC>; + label: string; + onClick: () => void; +} + +export const SeekButton: React.FC = ({ + onClick, + icon: Icon, + label, +}) => { + return + + ; +}; diff --git a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx index b3973bd749c..beb4864368b 100644 --- a/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx +++ b/src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactElement } from "react"; import { VoiceBroadcastControl, @@ -26,9 +26,14 @@ import Spinner from "../../../components/views/elements/Spinner"; import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback"; import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg"; import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg"; +import { Icon as Back30sIcon } from "../../../../res/img/element-icons/Back30s.svg"; +import { Icon as Forward30sIcon } from "../../../../res/img/element-icons/Forward30s.svg"; import { _t } from "../../../languageHandler"; import Clock from "../../../components/views/audio_messages/Clock"; import SeekBar from "../../../components/views/audio_messages/SeekBar"; +import { SeekButton } from "../atoms/SeekButton"; + +const SEEK_TIME = 30; interface VoiceBroadcastPlaybackBodyProps { playback: VoiceBroadcastPlayback; @@ -40,10 +45,11 @@ export const VoiceBroadcastPlaybackBody: React.FC; } + let seekBackwardButton: ReactElement | null = null; + let seekForwardButton: ReactElement | null = null; + + if (playbackState !== VoiceBroadcastPlaybackState.Stopped) { + const onSeekBackwardButtonClick = () => { + playback.skipTo(Math.max(0, position - SEEK_TIME)); + }; + + seekBackwardButton = ; + + const onSeekForwardButtonClick = () => { + playback.skipTo(Math.min(duration, position + SEEK_TIME)); + }; + + seekForwardButton = ; + } + return (
+ { seekBackwardButton } { control } + { seekForwardButton }
diff --git a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts index 67b0cb8875f..1828b31d01a 100644 --- a/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts +++ b/src/voice-broadcast/hooks/useVoiceBroadcastPlayback.ts @@ -47,6 +47,13 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { d => setDuration(d / 1000), ); + const [position, setPosition] = useState(playback.timeSeconds); + useTypedEventEmitter( + playback, + VoiceBroadcastPlaybackEvent.PositionChanged, + p => setPosition(p / 1000), + ); + const [liveness, setLiveness] = useState(playback.getLiveness()); useTypedEventEmitter( playback, @@ -57,9 +64,10 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => { return { duration, liveness: liveness, + playbackState, + position, room: room, sender: playback.infoEvent.sender, toggle: playbackToggle, - playbackState, }; }; diff --git a/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx b/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx index f7848e74c84..a2e95a856ed 100644 --- a/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx +++ b/test/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody-test.tsx @@ -16,7 +16,7 @@ limitations under the License. import React from "react"; import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix"; -import { act, render, RenderResult } from "@testing-library/react"; +import { act, render, RenderResult, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { mocked } from "jest-mock"; @@ -54,7 +54,7 @@ describe("VoiceBroadcastPlaybackBody", () => { infoEvent = mkVoiceBroadcastInfoStateEvent( roomId, - VoiceBroadcastInfoState.Started, + VoiceBroadcastInfoState.Stopped, userId, client.getDeviceId(), ); @@ -65,6 +65,7 @@ describe("VoiceBroadcastPlaybackBody", () => { jest.spyOn(playback, "toggle").mockImplementation(() => Promise.resolve()); jest.spyOn(playback, "getLiveness"); jest.spyOn(playback, "getState"); + jest.spyOn(playback, "skipTo"); jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(23 * 60 + 42); // 23:42 }); @@ -80,6 +81,50 @@ describe("VoiceBroadcastPlaybackBody", () => { }); }); + describe("when rendering a playing broadcast", () => { + beforeEach(() => { + mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Playing); + mocked(playback.getLiveness).mockReturnValue("not-live"); + renderResult = render(); + }); + + it("should render as expected", () => { + expect(renderResult.container).toMatchSnapshot(); + }); + + describe("and being in the middle of the playback", () => { + beforeEach(() => { + act(() => { + playback.emit(VoiceBroadcastPlaybackEvent.PositionChanged, 10 * 60 * 1000); // 10:00 + }); + }); + + describe("and clicking 30s backward", () => { + beforeEach(async () => { + await act(async () => { + await userEvent.click(screen.getByLabelText("30s backward")); + }); + }); + + it("should seek 30s backward", () => { + expect(playback.skipTo).toHaveBeenCalledWith(9 * 60 + 30); + }); + }); + + describe("and clicking 30s forward", () => { + beforeEach(async () => { + await act(async () => { + await userEvent.click(screen.getByLabelText("30s forward")); + }); + }); + + it("should seek 30s forward", () => { + expect(playback.skipTo).toHaveBeenCalledWith(10 * 60 + 30); + }); + }); + }); + }); + describe(`when rendering a stopped broadcast`, () => { beforeEach(() => { mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped); @@ -87,6 +132,10 @@ describe("VoiceBroadcastPlaybackBody", () => { renderResult = render(); }); + it("should render as expected", () => { + expect(renderResult.container).toMatchSnapshot(); + }); + describe("and clicking the play button", () => { beforeEach(async () => { await userEvent.click(renderResult.getByLabelText("play voice broadcast")); @@ -104,8 +153,8 @@ describe("VoiceBroadcastPlaybackBody", () => { }); }); - it("should render as expected", () => { - expect(renderResult.container).toMatchSnapshot(); + it("should render the new length", async () => { + expect(await screen.findByText("00:42")).toBeInTheDocument(); }); }); }); diff --git a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap index 5ec5ca56e32..71275fda517 100644 --- a/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap +++ b/test/voice-broadcast/components/molecules/__snapshots__/VoiceBroadcastPlaybackBody-test.tsx.snap @@ -45,6 +45,16 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0/not-live broadcast should
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -230,6 +280,16 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s style="width: 32px; height: 32px;" />
+
+
+
`; -exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast and the length updated should render as expected 1`] = ` +exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should render as expected 1`] = ` +
+
+
+
+ room avatar: + My room +
+
+
+ My room +
+
+
+ + @user:example.com + +
+
+
+ Voice broadcast +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + 23:42 + +
+
+
+`; + +exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should render as expected 1`] = `
- 00:42 + 23:42