Merge branch 'develop' into unread-title-indicator

This commit is contained in:
Florian Duros 2023-02-07 11:37:34 +01:00 committed by GitHub
commit 4c7945552c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 312 additions and 191 deletions

View file

@ -1,6 +1,9 @@
module.exports = { module.exports = {
plugins: ["matrix-org"], plugins: ["matrix-org"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"], extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
},
env: { env: {
browser: true, browser: true,
node: true, node: true,
@ -168,6 +171,12 @@ module.exports = {
"@typescript-eslint/explicit-member-accessibility": "off", "@typescript-eslint/explicit-member-accessibility": "off",
}, },
}, },
{
files: ["cypress/**/*.ts"],
parserOptions: {
project: ["./cypress/tsconfig.json"],
},
},
], ],
settings: { settings: {
react: { react: {

View file

@ -52,6 +52,8 @@ const handleVerificationRequest = (request: VerificationRequest): Chainable<Emoj
verifier.on("show_sas", onShowSas); verifier.on("show_sas", onShowSas);
verifier.verify(); verifier.verify();
}), }),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
); );
}; };
@ -111,9 +113,8 @@ describe("Decryption Failure Bar", () => {
}) })
.then(() => { .then(() => {
cy.botSendMessage(bot, roomId, "test"); cy.botSendMessage(bot, roomId, "test");
cy.wait(5000); cy.contains(
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should( ".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline",
"have.text",
"Verify this device to access all messages", "Verify this device to access all messages",
); );
@ -124,6 +125,7 @@ describe("Decryption Failure Bar", () => {
const verificationRequestPromise = waitForVerificationRequest(otherDevice); const verificationRequestPromise = waitForVerificationRequest(otherDevice);
cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click(); cy.get(".mx_CompleteSecurity_actionRow .mx_AccessibleButton").click();
cy.contains("To proceed, please accept the verification request on your other device.");
cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => { cy.wrap(verificationRequestPromise).then((verificationRequest: VerificationRequest) => {
cy.wrap(verificationRequest.accept()); cy.wrap(verificationRequest.accept());
handleVerificationRequest(verificationRequest).then((emojis) => { handleVerificationRequest(verificationRequest).then((emojis) => {
@ -170,9 +172,8 @@ describe("Decryption Failure Bar", () => {
); );
cy.botSendMessage(bot, roomId, "test"); cy.botSendMessage(bot, roomId, "test");
cy.wait(5000); cy.contains(
cy.get(".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline").should( ".mx_DecryptionFailureBar .mx_DecryptionFailureBar_message_headline",
"have.text",
"Reset your keys to prevent future decryption errors", "Reset your keys to prevent future decryption errors",
); );

View file

@ -163,6 +163,8 @@ function setupBotClient(
} }
}) })
.then(() => cli), .then(() => cli),
// extra timeout, as this sometimes takes a while
{ timeout: 30_000 },
); );
}); });
} }

View file

@ -190,7 +190,7 @@
"eslint-plugin-deprecate": "^0.7.0", "eslint-plugin-deprecate": "^0.7.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-matrix-org": "0.9.0", "eslint-plugin-matrix-org": "0.10.0",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0", "eslint-plugin-unicorn": "^45.0.0",

View file

@ -38,6 +38,8 @@ limitations under the License.
} }
.mx_AddExistingToSpace_section { .mx_AddExistingToSpace_section {
margin-right: 12px; // provides space for scrollbar so that checkbox and scrollbar do not collide
&:not(:first-child) { &:not(:first-child) {
margin-top: 24px; margin-top: 24px;
} }

View file

@ -258,17 +258,16 @@ class PipContainerInner extends React.Component<IProps, IState> {
} }
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren { private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
if (this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId()) { const content =
return ({ onStartMoving }) => ( this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<div onMouseDown={onStartMoving}>
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} /> <VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
</div> ) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
); );
}
return ({ onStartMoving }) => ( return ({ onStartMoving }) => (
<div onMouseDown={onStartMoving}> <div key={voiceBroadcastPlayback.infoEvent.getId()} onMouseDown={onStartMoving}>
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} /> {content}
</div> </div>
); );
} }

View file

@ -48,7 +48,8 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
private renderTimeline(): React.ReactElement[] { private renderTimeline(): React.ReactElement[] {
return EchoStore.instance.contexts.map((c, i) => { return EchoStore.instance.contexts.map((c, i) => {
if (!c.firstFailedTime) return null; // not useful if (!c.firstFailedTime) return null; // not useful
if (!(c instanceof RoomEchoContext)) throw new Error("Cannot render unknown context: " + c); if (!(c instanceof RoomEchoContext))
throw new Error("Cannot render unknown context: " + c.constructor.name);
const header = ( const header = (
<div className="mx_ServerOfflineDialog_content_context_timeline_header"> <div className="mx_ServerOfflineDialog_content_context_timeline_header">
<RoomAvatar width={24} height={24} room={c.room} /> <RoomAvatar width={24} height={24} room={c.room} />

View file

@ -507,39 +507,36 @@ export default class EventListSummary extends React.Component<IProps> {
eventsToRender.forEach((e, index) => { eventsToRender.forEach((e, index) => {
const type = e.getType(); const type = e.getType();
let userId = e.getSender(); let userKey = e.getSender()!;
if (type === EventType.RoomMember) { if (type === EventType.RoomThirdPartyInvite) {
userId = e.getStateKey(); userKey = e.getContent().display_name;
} else if (type === EventType.RoomMember) {
userKey = e.getStateKey();
} else if (e.isRedacted()) { } else if (e.isRedacted()) {
userId = e.getUnsigned()?.redacted_because?.sender; userKey = e.getUnsigned()?.redacted_because?.sender;
} }
// Initialise a user's events // Initialise a user's events
if (!userEvents[userId]) { if (!userEvents[userKey]) {
userEvents[userId] = []; userEvents[userKey] = [];
} }
let displayName = userId; let displayName = userKey;
if (type === EventType.RoomThirdPartyInvite) { if (e.isRedacted()) {
displayName = e.getContent().display_name; const sender = this.context?.room?.getMember(userKey);
if (e.sender) {
latestUserAvatarMember.set(userId, e.sender);
}
} else if (e.isRedacted()) {
const sender = this.context?.room.getMember(userId);
if (sender) { if (sender) {
displayName = sender.name; displayName = sender.name;
latestUserAvatarMember.set(userId, sender); latestUserAvatarMember.set(userKey, sender);
} }
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) { } else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name; displayName = e.target.name;
latestUserAvatarMember.set(userId, e.target); latestUserAvatarMember.set(userKey, e.target);
} else if (e.sender) { } else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
displayName = e.sender.name; displayName = e.sender.name;
latestUserAvatarMember.set(userId, e.sender); latestUserAvatarMember.set(userKey, e.sender);
} }
userEvents[userId].push({ userEvents[userKey].push({
mxEvent: e, mxEvent: e,
displayName, displayName,
index: index, index: index,

View file

@ -116,7 +116,7 @@ const RoomPreviewCard: FC<IProps> = ({ room, onJoinButtonClicked, onRejectButton
joinButtons = ( joinButtons = (
<> <>
<AccessibleButton <AccessibleButton
kind="secondary" kind="primary_outline"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onRejectButtonClicked(); onRejectButtonClicked();

View file

@ -185,6 +185,10 @@ export default class ProfileSettings extends React.Component<{}, IState> {
withDisplayName: true, withDisplayName: true,
}); });
// False negative result from no-base-to-string rule, doesn't seem to account for Symbol.toStringTag
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const avatarUrl = this.state.avatarUrl?.toString();
return ( return (
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings"> <form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
<input <input
@ -216,7 +220,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
</p> </p>
</div> </div>
<AvatarSetting <AvatarSetting
avatarUrl={this.state.avatarUrl?.toString()} avatarUrl={avatarUrl}
avatarName={this.state.displayName || this.state.userId} avatarName={this.state.displayName || this.state.userId}
avatarAltText={_t("Profile picture")} avatarAltText={_t("Profile picture")}
uploadAvatar={this.uploadAvatar} uploadAvatar={this.uploadAvatar}

View file

@ -662,7 +662,7 @@
"Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast", "Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast",
"Unable to play this voice broadcast": "Unable to play this voice broadcast", "Unable to play this voice broadcast": "Unable to play this voice broadcast",
"Stop live broadcasting?": "Stop live broadcasting?", "Stop live broadcasting?": "Stop live broadcasting?",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.", "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast? This will end the broadcast and the full recording will be available in the room.",
"Yes, stop broadcast": "Yes, stop broadcast", "Yes, stop broadcast": "Yes, stop broadcast",
"Listen to live broadcast?": "Listen to live broadcast?", "Listen to live broadcast?": "Listen to live broadcast?",
"If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.", "If you start listening to this live broadcast, your current live broadcast recording will be ended.": "If you start listening to this live broadcast, your current live broadcast recording will be ended.",

View file

@ -84,7 +84,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("user_id", client.credentials.userId); body.append("user_id", client.credentials.userId);
body.append("device_id", client.deviceId); body.append("device_id", client.deviceId);
if (client.isCryptoEnabled()) { // TODO: make this work with rust crypto
if (client.isCryptoEnabled() && client.crypto) {
const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; const keys = [`ed25519:${client.getDeviceEd25519Key()}`];
if (client.getDeviceCurve25519Key) { if (client.getDeviceCurve25519Key) {
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`); keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
@ -259,7 +260,7 @@ export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
reader.readAsArrayBuffer(value as Blob); reader.readAsArrayBuffer(value as Blob);
}); });
} else { } else {
metadata += `${key} = ${value}\n`; metadata += `${key} = ${value as string}\n`;
} }
} }
tape.append("issue.txt", metadata); tape.append("issue.txt", metadata);

View file

@ -116,7 +116,8 @@ function getEnabledLabs(): string {
} }
async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> { async function getCryptoContext(client: MatrixClient): Promise<CryptoContext> {
if (!client.isCryptoEnabled()) { // TODO: make this work with rust crypto
if (!client.isCryptoEnabled() || !client.crypto) {
return {}; return {};
} }
const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; const keys = [`ed25519:${client.getDeviceEd25519Key()}`];

View file

@ -389,7 +389,7 @@ export class StopGapWidget extends EventEmitter {
// Now open the integration manager // Now open the integration manager
// TODO: Spec this interaction. // TODO: Spec this interaction.
const data = ev.detail.data; const data = ev.detail.data;
const integType = data?.integType; const integType = data?.integType as string;
const integId = <string>data?.integId; const integId = <string>data?.integId;
// noinspection JSIgnoredPromiseFromCall // noinspection JSIgnoredPromiseFromCall

View file

@ -69,7 +69,7 @@ export function presentableTextForFile(
// it since it is "ugly", users generally aren't aware what it // it since it is "ugly", users generally aren't aware what it
// means and the type of the attachment can usually be inferred // means and the type of the attachment can usually be inferred
// from the file extension. // from the file extension.
text += " (" + filesize(content.info.size) + ")"; text += " (" + <string>filesize(content.info.size) + ")";
} }
return text; return text;
} }

View file

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IDestroyable } from "./IDestroyable"; import { IDestroyable } from "./IDestroyable";
import { arrayFastClone } from "./arrays"; import { arrayFastClone } from "./arrays";
export type WhenFn<T> = (w: Whenable<T>) => void; export type WhenFn<T extends string | number> = (w: Whenable<T>) => void;
/** /**
* Whenables are a cheap way to have Observable patterns mixed with typical * Whenables are a cheap way to have Observable patterns mixed with typical
@ -27,7 +27,7 @@ export type WhenFn<T> = (w: Whenable<T>) => void;
* are intended to be used when a condition will be met multiple times and * are intended to be used when a condition will be met multiple times and
* the consumer needs to know *when* that happens. * the consumer needs to know *when* that happens.
*/ */
export abstract class Whenable<T> implements IDestroyable { export abstract class Whenable<T extends string | number> implements IDestroyable {
private listeners: { condition: T | null; fn: WhenFn<T> }[] = []; private listeners: { condition: T | null; fn: WhenFn<T> }[] = [];
/** /**

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ReactNode } from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
@ -65,7 +65,7 @@ export default class HTMLExporter extends Exporter {
this.threadsEnabled = SettingsStore.getValue("feature_threadenabled"); this.threadsEnabled = SettingsStore.getValue("feature_threadenabled");
} }
protected async getRoomAvatar(): Promise<ReactNode> { protected async getRoomAvatar(): Promise<string> {
let blob: Blob | undefined = undefined; let blob: Blob | undefined = undefined;
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop"); const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
const avatarPath = "room.png"; const avatarPath = "room.png";

View file

@ -36,7 +36,7 @@ const showStopBroadcastingDialog = async (): Promise<boolean> => {
description: ( description: (
<p> <p>
{_t( {_t(
"Are you sure you want to stop your live broadcast?" + "Are you sure you want to stop your live broadcast? " +
"This will end the broadcast and the full recording will be available in the room.", "This will end the broadcast and the full recording will be available in the room.",
)} )}
</p> </p>

View file

@ -396,7 +396,11 @@ export class VoiceBroadcastPlayback
} }
if (!this.playbacks.has(eventId)) { if (!this.playbacks.has(eventId)) {
// set to buffering while loading the chunk data
const currentState = this.getState();
this.setState(VoiceBroadcastPlaybackState.Buffering);
await this.loadPlayback(event); await this.loadPlayback(event);
this.setState(currentState);
} }
const playback = this.playbacks.get(eventId); const playback = this.playbacks.get(eventId);

View file

@ -21,6 +21,7 @@ import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import { import {
getMockClientWithEventEmitter, getMockClientWithEventEmitter,
mkEvent,
mkMembership, mkMembership,
mockClientMethodsUser, mockClientMethodsUser,
unmockClientPeg, unmockClientPeg,
@ -100,7 +101,7 @@ describe("EventListSummary", function () {
// is created by replacing the first "$" in userIdTemplate with `i` for // is created by replacing the first "$" in userIdTemplate with `i` for
// `i = 0 .. n`. // `i = 0 .. n`.
const generateEventsForUsers = (userIdTemplate, n, events) => { const generateEventsForUsers = (userIdTemplate, n, events) => {
let eventsForUsers = []; let eventsForUsers: MatrixEvent[] = [];
let userId = ""; let userId = "";
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
userId = userIdTemplate.replace("$", i); userId = userIdTemplate.replace("$", i);
@ -656,4 +657,56 @@ describe("EventListSummary", function () {
expect(summaryText).toBe("user_0, user_1 and 18 others joined"); expect(summaryText).toBe("user_0, user_1 and 18 others joined");
}); });
it("should not blindly group 3pid invites and treat them as distinct users instead", () => {
const events = [
mkEvent({
event: true,
skey: "randomstring1",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring2",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "n...@d...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
mkEvent({
event: true,
skey: "randomstring3",
user: "@user1:server",
type: "m.room.third_party_invite",
content: {
display_name: "d...@w...",
key_validity_url: "https://blah",
public_key: "public_key",
},
}),
];
const props = {
events: events,
children: generateTiles(events),
summaryLength: 2,
avatarsMaxLength: 5,
threshold: 3,
};
const wrapper = renderComponent(props);
const summary = wrapper.find(".mx_GenericEventListSummary_summary");
const summaryText = summary.text();
expect(summaryText).toBe("n...@d... was invited 2 times, d...@w... was invited");
});
}); });

View file

@ -18,6 +18,7 @@ import { mocked } from "jest-mock";
import { screen } from "@testing-library/react"; import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event"; import userEvent from "@testing-library/user-event";
import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix"; import { MatrixClient, MatrixEvent, MatrixEventEvent, Room } from "matrix-js-sdk/src/matrix";
import { defer } from "matrix-js-sdk/src/utils";
import { Playback, PlaybackState } from "../../../src/audio/Playback"; import { Playback, PlaybackState } from "../../../src/audio/Playback";
import { PlaybackManager } from "../../../src/audio/PlaybackManager"; import { PlaybackManager } from "../../../src/audio/PlaybackManager";
@ -31,9 +32,10 @@ import {
VoiceBroadcastPlaybackState, VoiceBroadcastPlaybackState,
VoiceBroadcastRecording, VoiceBroadcastRecording,
} from "../../../src/voice-broadcast"; } from "../../../src/voice-broadcast";
import { filterConsole, flushPromises, stubClient } from "../../test-utils"; import { filterConsole, flushPromises, flushPromisesWithFakeTimers, stubClient } from "../../test-utils";
import { createTestPlayback } from "../../test-utils/audio"; import { createTestPlayback } from "../../test-utils/audio";
import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils"; import { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
import { LazyValue } from "../../../src/utils/LazyValue";
jest.mock("../../../src/utils/MediaEventHelper", () => ({ jest.mock("../../../src/utils/MediaEventHelper", () => ({
MediaEventHelper: jest.fn(), MediaEventHelper: jest.fn(),
@ -49,6 +51,7 @@ describe("VoiceBroadcastPlayback", () => {
let playback: VoiceBroadcastPlayback; let playback: VoiceBroadcastPlayback;
let onStateChanged: (state: VoiceBroadcastPlaybackState) => void; let onStateChanged: (state: VoiceBroadcastPlaybackState) => void;
let chunk1Event: MatrixEvent; let chunk1Event: MatrixEvent;
let deplayedChunk1Event: MatrixEvent;
let chunk2Event: MatrixEvent; let chunk2Event: MatrixEvent;
let chunk2BEvent: MatrixEvent; let chunk2BEvent: MatrixEvent;
let chunk3Event: MatrixEvent; let chunk3Event: MatrixEvent;
@ -58,6 +61,7 @@ describe("VoiceBroadcastPlayback", () => {
const chunk1Data = new ArrayBuffer(2); const chunk1Data = new ArrayBuffer(2);
const chunk2Data = new ArrayBuffer(3); const chunk2Data = new ArrayBuffer(3);
const chunk3Data = new ArrayBuffer(3); const chunk3Data = new ArrayBuffer(3);
let delayedChunk1Helper: MediaEventHelper;
let chunk1Helper: MediaEventHelper; let chunk1Helper: MediaEventHelper;
let chunk2Helper: MediaEventHelper; let chunk2Helper: MediaEventHelper;
let chunk3Helper: MediaEventHelper; let chunk3Helper: MediaEventHelper;
@ -97,8 +101,8 @@ describe("VoiceBroadcastPlayback", () => {
}; };
const startPlayback = () => { const startPlayback = () => {
beforeEach(async () => { beforeEach(() => {
await playback.start(); playback.start();
}); });
}; };
@ -127,11 +131,36 @@ describe("VoiceBroadcastPlayback", () => {
}; };
}; };
const mkDeplayedChunkHelper = (data: ArrayBuffer): MediaEventHelper => {
const deferred = defer<LazyValue<Blob>>();
setTimeout(() => {
deferred.resolve({
// @ts-ignore
arrayBuffer: jest.fn().mockResolvedValue(data),
});
}, 7500);
return {
sourceBlob: {
cachedValue: new Blob(),
done: false,
// @ts-ignore
value: deferred.promise,
},
};
};
const simulateFirstChunkArrived = async (): Promise<void> => {
jest.advanceTimersByTime(10000);
await flushPromisesWithFakeTimers();
};
const mkInfoEvent = (state: VoiceBroadcastInfoState) => { const mkInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId); return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId);
}; };
const mkPlayback = async () => { const mkPlayback = async (fakeTimers = false): Promise<VoiceBroadcastPlayback> => {
const playback = new VoiceBroadcastPlayback( const playback = new VoiceBroadcastPlayback(
infoEvent, infoEvent,
client, client,
@ -140,7 +169,7 @@ describe("VoiceBroadcastPlayback", () => {
jest.spyOn(playback, "removeAllListeners"); jest.spyOn(playback, "removeAllListeners");
jest.spyOn(playback, "destroy"); jest.spyOn(playback, "destroy");
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged); playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
await flushPromises(); fakeTimers ? await flushPromisesWithFakeTimers() : await flushPromises();
return playback; return playback;
}; };
@ -152,6 +181,7 @@ describe("VoiceBroadcastPlayback", () => {
const createChunkEvents = () => { const createChunkEvents = () => {
chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1); chunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
deplayedChunk1Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk1Length, 1);
chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); chunk2Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
chunk2Event.setTxnId("tx-id-1"); chunk2Event.setTxnId("tx-id-1");
chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2); chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
@ -159,6 +189,7 @@ describe("VoiceBroadcastPlayback", () => {
chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3); chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3);
chunk1Helper = mkChunkHelper(chunk1Data); chunk1Helper = mkChunkHelper(chunk1Data);
delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data);
chunk2Helper = mkChunkHelper(chunk2Data); chunk2Helper = mkChunkHelper(chunk2Data);
chunk3Helper = mkChunkHelper(chunk3Data); chunk3Helper = mkChunkHelper(chunk3Data);
@ -181,6 +212,7 @@ describe("VoiceBroadcastPlayback", () => {
mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => { mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => {
if (event === chunk1Event) return chunk1Helper; if (event === chunk1Event) return chunk1Helper;
if (event === deplayedChunk1Event) return delayedChunk1Helper;
if (event === chunk2Event) return chunk2Helper; if (event === chunk2Event) return chunk2Helper;
if (event === chunk3Event) return chunk3Helper; if (event === chunk3Event) return chunk3Helper;
}); });
@ -488,11 +520,17 @@ describe("VoiceBroadcastPlayback", () => {
describe("when there is a stopped voice broadcast", () => { describe("when there is a stopped voice broadcast", () => {
beforeEach(async () => { beforeEach(async () => {
jest.useFakeTimers();
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped); infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
createChunkEvents(); createChunkEvents();
setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]); // use delayed first chunk here to simulate loading time
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]); setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]);
playback = await mkPlayback(); room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]);
playback = await mkPlayback(true);
});
afterEach(() => {
jest.useRealTimers();
}); });
it("should expose the info event", () => { it("should expose the info event", () => {
@ -504,6 +542,13 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling start", () => { describe("and calling start", () => {
startPlayback(); startPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
describe("and the first chunk data has been loaded", () => {
beforeEach(async () => {
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => { it("should play the chunks beginning with the first one", () => {
@ -525,7 +570,6 @@ describe("VoiceBroadcastPlayback", () => {
it("should update the time", () => { it("should update the time", () => {
expect(playback.timeSeconds).toBe(11); expect(playback.timeSeconds).toBe(11);
expect(playback.timeLeftSeconds).toBe(2);
}); });
}); });
@ -660,10 +704,12 @@ describe("VoiceBroadcastPlayback", () => {
}); });
}); });
}); });
});
describe("and calling toggle for the first time", () => { describe("and calling toggle for the first time", () => {
beforeEach(async () => { beforeEach(async () => {
await playback.toggle(); playback.toggle();
await simulateFirstChunkArrived();
}); });
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
@ -693,7 +739,8 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling toggle", () => { describe("and calling toggle", () => {
beforeEach(async () => { beforeEach(async () => {
mocked(onStateChanged).mockReset(); mocked(onStateChanged).mockReset();
await playback.toggle(); playback.toggle();
await simulateFirstChunkArrived();
}); });
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing); itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);

View file

@ -4226,10 +4226,10 @@ eslint-plugin-jsx-a11y@^6.5.1:
minimatch "^3.1.2" minimatch "^3.1.2"
semver "^6.3.0" semver "^6.3.0"
eslint-plugin-matrix-org@0.9.0: eslint-plugin-matrix-org@0.10.0:
version "0.9.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.9.0.tgz#b2a5186052ddbfa7dc9878779bafa5d68681c7b4" resolved "https://registry.yarnpkg.com/eslint-plugin-matrix-org/-/eslint-plugin-matrix-org-0.10.0.tgz#8d0998641a4d276343cae2abf253a01bb4d4cc60"
integrity sha512-+j6JuMnFH421Z2vOxc+0YMt5Su5vD76RSatviy3zHBaZpgd+sOeAWoCLBHD5E7mMz5oKae3Y3wewCt9LRzq2Nw== integrity sha512-L7ail0x1yUlF006kn4mHc+OT8/aYZI++i852YXPHxCbM1EY7jeg/fYAQ8tCx5+x08LyqXeS7inAVSL784m0C6Q==
eslint-plugin-react-hooks@^4.3.0: eslint-plugin-react-hooks@^4.3.0:
version "4.6.0" version "4.6.0"