mirror of https://github.com/vector-im/riot-web
Update voice broadcast time display (#9646)
parent
5f6b1dda8d
commit
70a7961681
|
@ -21,6 +21,10 @@ limitations under the License.
|
|||
display: inline-block;
|
||||
font-size: $font-12px;
|
||||
padding: $spacing-12;
|
||||
|
||||
.mx_Clock {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_VoiceBroadcastBody--pip {
|
||||
|
@ -44,9 +48,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_VoiceBroadcastBody_timerow {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $spacing-4;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
|
||||
|
|
|
@ -125,6 +125,9 @@ export function formatTime(date: Date, showTwelveHour = false): string {
|
|||
}
|
||||
|
||||
export function formatSeconds(inSeconds: number): string {
|
||||
const isNegative = inSeconds < 0;
|
||||
inSeconds = Math.abs(inSeconds);
|
||||
|
||||
const hours = Math.floor(inSeconds / (60 * 60)).toFixed(0).padStart(2, '0');
|
||||
const minutes = Math.floor((inSeconds % (60 * 60)) / 60).toFixed(0).padStart(2, '0');
|
||||
const seconds = Math.floor(((inSeconds % (60 * 60)) % 60)).toFixed(0).padStart(2, '0');
|
||||
|
@ -133,6 +136,10 @@ export function formatSeconds(inSeconds: number): string {
|
|||
if (hours !== "00") output += `${hours}:`;
|
||||
output += `${minutes}:${seconds}`;
|
||||
|
||||
if (isNegative) {
|
||||
output = "-" + output;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
|
|
@ -46,10 +46,9 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
|||
playback,
|
||||
}) => {
|
||||
const {
|
||||
duration,
|
||||
times,
|
||||
liveness,
|
||||
playbackState,
|
||||
position,
|
||||
room,
|
||||
sender,
|
||||
toggle,
|
||||
|
@ -94,7 +93,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
|||
|
||||
if (playbackState !== VoiceBroadcastPlaybackState.Stopped) {
|
||||
const onSeekBackwardButtonClick = () => {
|
||||
playback.skipTo(Math.max(0, position - SEEK_TIME));
|
||||
playback.skipTo(Math.max(0, times.position - SEEK_TIME));
|
||||
};
|
||||
|
||||
seekBackwardButton = <SeekButton
|
||||
|
@ -104,7 +103,7 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
|||
/>;
|
||||
|
||||
const onSeekForwardButtonClick = () => {
|
||||
playback.skipTo(Math.min(duration, position + SEEK_TIME));
|
||||
playback.skipTo(Math.min(times.duration, times.position + SEEK_TIME));
|
||||
};
|
||||
|
||||
seekForwardButton = <SeekButton
|
||||
|
@ -132,9 +131,10 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
|
|||
{ control }
|
||||
{ seekForwardButton }
|
||||
</div>
|
||||
<SeekBar playback={playback} />
|
||||
<div className="mx_VoiceBroadcastBody_timerow">
|
||||
<SeekBar playback={playback} />
|
||||
<Clock seconds={duration} />
|
||||
<Clock seconds={times.position} />
|
||||
<Clock seconds={-times.timeLeft} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -40,18 +40,15 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
|
|||
},
|
||||
);
|
||||
|
||||
const [duration, setDuration] = useState(playback.durationSeconds);
|
||||
const [times, setTimes] = useState({
|
||||
duration: playback.durationSeconds,
|
||||
position: playback.timeSeconds,
|
||||
timeLeft: playback.timeLeftSeconds,
|
||||
});
|
||||
useTypedEventEmitter(
|
||||
playback,
|
||||
VoiceBroadcastPlaybackEvent.LengthChanged,
|
||||
d => setDuration(d / 1000),
|
||||
);
|
||||
|
||||
const [position, setPosition] = useState(playback.timeSeconds);
|
||||
useTypedEventEmitter(
|
||||
playback,
|
||||
VoiceBroadcastPlaybackEvent.PositionChanged,
|
||||
p => setPosition(p / 1000),
|
||||
VoiceBroadcastPlaybackEvent.TimesChanged,
|
||||
t => setTimes(t),
|
||||
);
|
||||
|
||||
const [liveness, setLiveness] = useState(playback.getLiveness());
|
||||
|
@ -62,10 +59,9 @@ export const useVoiceBroadcastPlayback = (playback: VoiceBroadcastPlayback) => {
|
|||
);
|
||||
|
||||
return {
|
||||
duration,
|
||||
times,
|
||||
liveness: liveness,
|
||||
playbackState,
|
||||
position,
|
||||
room: room,
|
||||
sender: playback.infoEvent.sender,
|
||||
toggle: playbackToggle,
|
||||
|
|
|
@ -43,16 +43,20 @@ export enum VoiceBroadcastPlaybackState {
|
|||
}
|
||||
|
||||
export enum VoiceBroadcastPlaybackEvent {
|
||||
PositionChanged = "position_changed",
|
||||
LengthChanged = "length_changed",
|
||||
TimesChanged = "times_changed",
|
||||
LivenessChanged = "liveness_changed",
|
||||
StateChanged = "state_changed",
|
||||
InfoStateChanged = "info_state_changed",
|
||||
}
|
||||
|
||||
type VoiceBroadcastPlaybackTimes = {
|
||||
duration: number;
|
||||
position: number;
|
||||
timeLeft: number;
|
||||
};
|
||||
|
||||
interface EventMap {
|
||||
[VoiceBroadcastPlaybackEvent.PositionChanged]: (position: number) => void;
|
||||
[VoiceBroadcastPlaybackEvent.LengthChanged]: (length: number) => void;
|
||||
[VoiceBroadcastPlaybackEvent.TimesChanged]: (times: VoiceBroadcastPlaybackTimes) => void;
|
||||
[VoiceBroadcastPlaybackEvent.LivenessChanged]: (liveness: VoiceBroadcastLiveness) => void;
|
||||
[VoiceBroadcastPlaybackEvent.StateChanged]: (
|
||||
state: VoiceBroadcastPlaybackState,
|
||||
|
@ -229,7 +233,7 @@ export class VoiceBroadcastPlayback
|
|||
if (this.duration === duration) return;
|
||||
|
||||
this.duration = duration;
|
||||
this.emit(VoiceBroadcastPlaybackEvent.LengthChanged, this.duration);
|
||||
this.emitTimesChanged();
|
||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
||||
}
|
||||
|
||||
|
@ -237,10 +241,21 @@ export class VoiceBroadcastPlayback
|
|||
if (this.position === position) return;
|
||||
|
||||
this.position = position;
|
||||
this.emit(VoiceBroadcastPlaybackEvent.PositionChanged, this.position);
|
||||
this.emitTimesChanged();
|
||||
this.liveData.update([this.timeSeconds, this.durationSeconds]);
|
||||
}
|
||||
|
||||
private emitTimesChanged(): void {
|
||||
this.emit(
|
||||
VoiceBroadcastPlaybackEvent.TimesChanged,
|
||||
{
|
||||
duration: this.durationSeconds,
|
||||
position: this.timeSeconds,
|
||||
timeLeft: this.timeLeftSeconds,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private onPlaybackStateChange = async (event: MatrixEvent, newState: PlaybackState): Promise<void> => {
|
||||
if (event !== this.currentlyPlaying) return;
|
||||
if (newState !== PlaybackState.Stopped) return;
|
||||
|
@ -337,6 +352,10 @@ export class VoiceBroadcastPlayback
|
|||
return this.duration / 1000;
|
||||
}
|
||||
|
||||
public get timeLeftSeconds(): number {
|
||||
return Math.round(this.durationSeconds) - this.timeSeconds;
|
||||
}
|
||||
|
||||
public async skipTo(timeSeconds: number): Promise<void> {
|
||||
const time = timeSeconds * 1000;
|
||||
const event = this.chunkEvents.findByTime(time);
|
||||
|
|
|
@ -29,12 +29,14 @@ describe("formatSeconds", () => {
|
|||
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (55))).toBe("03:31:55");
|
||||
expect(formatSeconds((60 * 60 * 3) + (60 * 0) + (55))).toBe("03:00:55");
|
||||
expect(formatSeconds((60 * 60 * 3) + (60 * 31) + (0))).toBe("03:31:00");
|
||||
expect(formatSeconds(-((60 * 60 * 3) + (60 * 31) + (0)))).toBe("-03:31:00");
|
||||
});
|
||||
|
||||
it("correctly formats time without hours", () => {
|
||||
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (55))).toBe("31:55");
|
||||
expect(formatSeconds((60 * 60 * 0) + (60 * 0) + (55))).toBe("00:55");
|
||||
expect(formatSeconds((60 * 60 * 0) + (60 * 31) + (0))).toBe("31:00");
|
||||
expect(formatSeconds(-((60 * 60 * 0) + (60 * 31) + (0)))).toBe("-31:00");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
|
|||
describe("VoiceBroadcastPlaybackBody", () => {
|
||||
const userId = "@user:example.com";
|
||||
const roomId = "!room:example.com";
|
||||
const duration = 23 * 60 + 42; // 23:42
|
||||
let client: MatrixClient;
|
||||
let infoEvent: MatrixEvent;
|
||||
let playback: VoiceBroadcastPlayback;
|
||||
|
@ -66,7 +67,7 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
|||
jest.spyOn(playback, "getLiveness");
|
||||
jest.spyOn(playback, "getState");
|
||||
jest.spyOn(playback, "skipTo");
|
||||
jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(23 * 60 + 42); // 23:42
|
||||
jest.spyOn(playback, "durationSeconds", "get").mockReturnValue(duration);
|
||||
});
|
||||
|
||||
describe("when rendering a buffering voice broadcast", () => {
|
||||
|
@ -95,7 +96,11 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
|||
describe("and being in the middle of the playback", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.PositionChanged, 10 * 60 * 1000); // 10:00
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||
duration,
|
||||
position: 10 * 60,
|
||||
timeLeft: duration - 10 * 60,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -146,15 +151,20 @@ describe("VoiceBroadcastPlaybackBody", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("and the length updated", () => {
|
||||
describe("and the times update", () => {
|
||||
beforeEach(() => {
|
||||
act(() => {
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.LengthChanged, 42000); // 00:42
|
||||
playback.emit(VoiceBroadcastPlaybackEvent.TimesChanged, {
|
||||
duration,
|
||||
position: 5 * 60 + 13,
|
||||
timeLeft: 7 * 60 + 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the new length", async () => {
|
||||
expect(await screen.findByText("00:42")).toBeInTheDocument();
|
||||
it("should render the times", async () => {
|
||||
expect(await screen.findByText("05:13")).toBeInTheDocument();
|
||||
expect(await screen.findByText("-07:05")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,23 +76,28 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 0/not-live broadcast should
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
23:42
|
||||
00:00
|
||||
</span>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
-23:42
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -183,23 +188,28 @@ exports[`VoiceBroadcastPlaybackBody when rendering a 1/live broadcast should ren
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
23:42
|
||||
00:00
|
||||
</span>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
-23:42
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -291,23 +301,28 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
23:42
|
||||
00:00
|
||||
</span>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
-23:42
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -390,23 +405,28 @@ exports[`VoiceBroadcastPlaybackBody when rendering a playing broadcast should re
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
23:42
|
||||
00:00
|
||||
</span>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
-23:42
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -469,23 +489,28 @@ exports[`VoiceBroadcastPlaybackBody when rendering a stopped broadcast should re
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<div
|
||||
class="mx_VoiceBroadcastBody_timerow"
|
||||
>
|
||||
<input
|
||||
class="mx_SeekBar"
|
||||
max="1"
|
||||
min="0"
|
||||
step="0.001"
|
||||
style="--fillTo: 0;"
|
||||
tabindex="0"
|
||||
type="range"
|
||||
value="0"
|
||||
/>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
23:42
|
||||
00:00
|
||||
</span>
|
||||
<span
|
||||
class="mx_Clock"
|
||||
>
|
||||
-23:42
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue