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
20 changes: 19 additions & 1 deletion playwright/e2e/timeline/timeline.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,7 @@ test.describe("Timeline", () => {
const reply = "Reply";
const viewRoomSendMessageAndSetupReply = async (page: Page, app: ElementAppPage, roomId: string) => {
// View room
await page.goto(`/#/room/${roomId}`);
await app.viewRoomById(roomId);

// Send a message
const composer = app.getComposerField();
Expand Down Expand Up @@ -993,6 +993,24 @@ test.describe("Timeline", () => {
await expect(eventTileLine.getByText(reply)).toHaveCount(1);
});

test("can send a voice message", { tag: "@screenshot" }, async ({ page, app, room, context }) => {
await app.viewRoomById(room.roomId);

const composerOptions = await app.openMessageComposerOptions();
await composerOptions.getByRole("menuitem", { name: "Voice Message" }).click();

// Record an empty message
await page.waitForTimeout(3000);

const roomViewBody = page.locator(".mx_RoomView_body");
await roomViewBody
.locator(".mx_MessageComposer")
.getByRole("button", { name: "Send voice message" })
.click();

await expect(roomViewBody.locator(".mx_MVoiceMessageBody")).toMatchScreenshot("voice-message.png");
});

test("can reply with a voice message", async ({ page, app, room, context }) => {
await viewRoomSendMessageAndSetupReply(page, app, room.roomId);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 9 additions & 3 deletions src/audio/PlaybackQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,15 @@ export class PlaybackQueue {
private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState): void {
// Remember where the user got to in playback
const wasLastPlaying = this.currentPlaybackId === mxEvent.getId();
if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()!) && !wasLastPlaying) {
// noinspection JSIgnoredPromiseFromCall
playback.skipTo(this.clockStates.get(mxEvent.getId()!)!);
const currentClockState = this.clockStates.get(mxEvent.getId()!);
if (newState === PlaybackState.Stopped && currentClockState !== undefined && !wasLastPlaying) {
if (currentClockState > 0) {
// skipTo will pause playback, which causes the clock to render the current
// playback seconds. If the clock state is 0, then we can just ignore
// skipping entirely.
// noinspection JSIgnoredPromiseFromCall
playback.skipTo(currentClockState);
}
} else if (newState === PlaybackState.Stopped) {
// Remove the now-useless clock for some space savings
this.clockStates.delete(mxEvent.getId()!);
Expand Down
74 changes: 74 additions & 0 deletions test/unit-tests/audio/PlaybackQueue-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
Copyright 2025 New Vector Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type Mocked } from "jest-mock";
import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix";
import { SimpleObservable } from "matrix-widget-api";

import { PlaybackQueue } from "../../../src/audio/PlaybackQueue";
import { PlaybackState, type Playback } from "../../../src/audio/Playback";
import { MockEventEmitter } from "../../test-utils";
import { UPDATE_EVENT } from "../../../src/stores/AsyncStore";

describe("PlaybackQueue", () => {
let playbackQueue: PlaybackQueue;

beforeEach(() => {
const mockRoom = {
getMember: jest.fn(),
} as unknown as Mocked<Room>;
playbackQueue = new PlaybackQueue(mockRoom);
});

it("does not call skipTo on playback if clock advances to 0s", () => {
const mockEvent = {
getId: jest.fn().mockReturnValue("$foo:bar"),
} as unknown as Mocked<MatrixEvent>;
const mockPlayback = new MockEventEmitter({
clockInfo: {
liveData: new SimpleObservable<number[]>(),
},
skipTo: jest.fn(),
}) as unknown as Mocked<Playback>;

// Enqueue
playbackQueue.unsortedEnqueue(mockEvent, mockPlayback);

// Emit our clockInfo of 0, which will playbackQueue to save the state.
mockPlayback.clockInfo.liveData.update([0]);

// Fire an update event to say that we have stopped.
// Note that Playback really emits an UPDATE_EVENT whenever state changes, the types are lies.
mockPlayback.emit(UPDATE_EVENT as any, PlaybackState.Stopped);

expect(mockPlayback.skipTo).not.toHaveBeenCalled();
});

it("does call skipTo on playback if clock advances to 0s", () => {
const mockEvent = {
getId: jest.fn().mockReturnValue("$foo:bar"),
} as unknown as Mocked<MatrixEvent>;
const mockPlayback = new MockEventEmitter({
clockInfo: {
liveData: new SimpleObservable<number[]>(),
},
skipTo: jest.fn(),
}) as unknown as Mocked<Playback>;

// Enqueue
playbackQueue.unsortedEnqueue(mockEvent, mockPlayback);

// Emit our clockInfo of 0, which will playbackQueue to save the state.
mockPlayback.clockInfo.liveData.update([1]);

// Fire an update event to say that we have stopped.
// Note that Playback really emits an UPDATE_EVENT whenever state changes, the types are lies.
mockPlayback.emit(UPDATE_EVENT as any, PlaybackState.Stopped);

expect(mockPlayback.skipTo).toHaveBeenCalledWith(1);
});
});
Loading