Merge branch 'develop' into unread-title-indicator

pull/28788/head^2
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 = {
plugins: ["matrix-org"],
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
parserOptions: {
project: ["./tsconfig.json"],
},
env: {
browser: true,
node: true,
@ -168,6 +171,12 @@ module.exports = {
"@typescript-eslint/explicit-member-accessibility": "off",
},
},
{
files: ["cypress/**/*.ts"],
parserOptions: {
project: ["./cypress/tsconfig.json"],
},
},
],
settings: {
react: {

View File

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

View File

@ -163,6 +163,8 @@ function setupBotClient(
}
})
.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-import": "^2.25.4",
"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-hooks": "^4.3.0",
"eslint-plugin-unicorn": "^45.0.0",

View File

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

View File

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

View File

@ -48,7 +48,8 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
private renderTimeline(): React.ReactElement[] {
return EchoStore.instance.contexts.map((c, i) => {
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 = (
<div className="mx_ServerOfflineDialog_content_context_timeline_header">
<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) => {
const type = e.getType();
let userId = e.getSender();
if (type === EventType.RoomMember) {
userId = e.getStateKey();
let userKey = e.getSender()!;
if (type === EventType.RoomThirdPartyInvite) {
userKey = e.getContent().display_name;
} else if (type === EventType.RoomMember) {
userKey = e.getStateKey();
} else if (e.isRedacted()) {
userId = e.getUnsigned()?.redacted_because?.sender;
userKey = e.getUnsigned()?.redacted_because?.sender;
}
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
if (!userEvents[userKey]) {
userEvents[userKey] = [];
}
let displayName = userId;
if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
if (e.sender) {
latestUserAvatarMember.set(userId, e.sender);
}
} else if (e.isRedacted()) {
const sender = this.context?.room.getMember(userId);
let displayName = userKey;
if (e.isRedacted()) {
const sender = this.context?.room?.getMember(userKey);
if (sender) {
displayName = sender.name;
latestUserAvatarMember.set(userId, sender);
latestUserAvatarMember.set(userKey, sender);
}
} else if (e.target && TARGET_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.target.name;
latestUserAvatarMember.set(userId, e.target);
} else if (e.sender) {
latestUserAvatarMember.set(userKey, e.target);
} else if (e.sender && type !== EventType.RoomThirdPartyInvite) {
displayName = e.sender.name;
latestUserAvatarMember.set(userId, e.sender);
latestUserAvatarMember.set(userKey, e.sender);
}
userEvents[userId].push({
userEvents[userKey].push({
mxEvent: e,
displayName,
index: index,

View File

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

View File

@ -185,6 +185,10 @@ export default class ProfileSettings extends React.Component<{}, IState> {
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 (
<form onSubmit={this.saveProfile} autoComplete="off" noValidate={true} className="mx_ProfileSettings">
<input
@ -216,7 +220,7 @@ export default class ProfileSettings extends React.Component<{}, IState> {
</p>
</div>
<AvatarSetting
avatarUrl={this.state.avatarUrl?.toString()}
avatarUrl={avatarUrl}
avatarName={this.state.displayName || this.state.userId}
avatarAltText={_t("Profile picture")}
uploadAvatar={this.uploadAvatar}

View File

@ -662,7 +662,7 @@
"Unable to decrypt voice broadcast": "Unable to decrypt voice broadcast",
"Unable to play this voice broadcast": "Unable to play this voice broadcast",
"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",
"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.",

View File

@ -84,7 +84,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true): Promise<Form
body.append("user_id", client.credentials.userId);
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()}`];
if (client.getDeviceCurve25519Key) {
keys.push(`curve25519:${client.getDeviceCurve25519Key()}`);
@ -259,7 +260,7 @@ export async function downloadBugReport(opts: IOpts = {}): Promise<void> {
reader.readAsArrayBuffer(value as Blob);
});
} else {
metadata += `${key} = ${value}\n`;
metadata += `${key} = ${value as string}\n`;
}
}
tape.append("issue.txt", metadata);

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import { IDestroyable } from "./IDestroyable";
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
@ -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
* 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> }[] = [];
/**

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactNode } from "react";
import React from "react";
import ReactDOM from "react-dom";
import { Room } from "matrix-js-sdk/src/models/room";
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");
}
protected async getRoomAvatar(): Promise<ReactNode> {
protected async getRoomAvatar(): Promise<string> {
let blob: Blob | undefined = undefined;
const avatarUrl = Avatar.avatarUrlForRoom(this.room, 32, 32, "crop");
const avatarPath = "room.png";

View File

@ -36,7 +36,7 @@ const showStopBroadcastingDialog = async (): Promise<boolean> => {
description: (
<p>
{_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.",
)}
</p>

View File

@ -396,7 +396,11 @@ export class VoiceBroadcastPlayback
}
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);
this.setState(currentState);
}
const playback = this.playbacks.get(eventId);

View File

@ -21,6 +21,7 @@ import { MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
import {
getMockClientWithEventEmitter,
mkEvent,
mkMembership,
mockClientMethodsUser,
unmockClientPeg,
@ -100,7 +101,7 @@ describe("EventListSummary", function () {
// is created by replacing the first "$" in userIdTemplate with `i` for
// `i = 0 .. n`.
const generateEventsForUsers = (userIdTemplate, n, events) => {
let eventsForUsers = [];
let eventsForUsers: MatrixEvent[] = [];
let userId = "";
for (let i = 0; i < n; i++) {
userId = userIdTemplate.replace("$", i);
@ -656,4 +657,56 @@ describe("EventListSummary", function () {
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 userEvent from "@testing-library/user-event";
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 { PlaybackManager } from "../../../src/audio/PlaybackManager";
@ -31,9 +32,10 @@ import {
VoiceBroadcastPlaybackState,
VoiceBroadcastRecording,
} 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 { mkVoiceBroadcastChunkEvent, mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
import { LazyValue } from "../../../src/utils/LazyValue";
jest.mock("../../../src/utils/MediaEventHelper", () => ({
MediaEventHelper: jest.fn(),
@ -49,6 +51,7 @@ describe("VoiceBroadcastPlayback", () => {
let playback: VoiceBroadcastPlayback;
let onStateChanged: (state: VoiceBroadcastPlaybackState) => void;
let chunk1Event: MatrixEvent;
let deplayedChunk1Event: MatrixEvent;
let chunk2Event: MatrixEvent;
let chunk2BEvent: MatrixEvent;
let chunk3Event: MatrixEvent;
@ -58,6 +61,7 @@ describe("VoiceBroadcastPlayback", () => {
const chunk1Data = new ArrayBuffer(2);
const chunk2Data = new ArrayBuffer(3);
const chunk3Data = new ArrayBuffer(3);
let delayedChunk1Helper: MediaEventHelper;
let chunk1Helper: MediaEventHelper;
let chunk2Helper: MediaEventHelper;
let chunk3Helper: MediaEventHelper;
@ -97,8 +101,8 @@ describe("VoiceBroadcastPlayback", () => {
};
const startPlayback = () => {
beforeEach(async () => {
await playback.start();
beforeEach(() => {
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) => {
return mkVoiceBroadcastInfoStateEvent(roomId, state, userId, deviceId);
};
const mkPlayback = async () => {
const mkPlayback = async (fakeTimers = false): Promise<VoiceBroadcastPlayback> => {
const playback = new VoiceBroadcastPlayback(
infoEvent,
client,
@ -140,7 +169,7 @@ describe("VoiceBroadcastPlayback", () => {
jest.spyOn(playback, "removeAllListeners");
jest.spyOn(playback, "destroy");
playback.on(VoiceBroadcastPlaybackEvent.StateChanged, onStateChanged);
await flushPromises();
fakeTimers ? await flushPromisesWithFakeTimers() : await flushPromises();
return playback;
};
@ -152,6 +181,7 @@ describe("VoiceBroadcastPlayback", () => {
const createChunkEvents = () => {
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.setTxnId("tx-id-1");
chunk2BEvent = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk2Length, 2);
@ -159,6 +189,7 @@ describe("VoiceBroadcastPlayback", () => {
chunk3Event = mkVoiceBroadcastChunkEvent(infoEvent.getId()!, userId, roomId, chunk3Length, 3);
chunk1Helper = mkChunkHelper(chunk1Data);
delayedChunk1Helper = mkDeplayedChunkHelper(chunk1Data);
chunk2Helper = mkChunkHelper(chunk2Data);
chunk3Helper = mkChunkHelper(chunk3Data);
@ -181,6 +212,7 @@ describe("VoiceBroadcastPlayback", () => {
mocked(MediaEventHelper).mockImplementation((event: MatrixEvent): any => {
if (event === chunk1Event) return chunk1Helper;
if (event === deplayedChunk1Event) return delayedChunk1Helper;
if (event === chunk2Event) return chunk2Helper;
if (event === chunk3Event) return chunk3Helper;
});
@ -488,11 +520,17 @@ describe("VoiceBroadcastPlayback", () => {
describe("when there is a stopped voice broadcast", () => {
beforeEach(async () => {
jest.useFakeTimers();
infoEvent = mkInfoEvent(VoiceBroadcastInfoState.Stopped);
createChunkEvents();
setUpChunkEvents([chunk2Event, chunk1Event, chunk3Event]);
room.addLiveEvents([infoEvent, chunk1Event, chunk2Event, chunk3Event]);
playback = await mkPlayback();
// use delayed first chunk here to simulate loading time
setUpChunkEvents([chunk2Event, deplayedChunk1Event, chunk3Event]);
room.addLiveEvents([infoEvent, deplayedChunk1Event, chunk2Event, chunk3Event]);
playback = await mkPlayback(true);
});
afterEach(() => {
jest.useRealTimers();
});
it("should expose the info event", () => {
@ -504,6 +542,13 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling start", () => {
startPlayback();
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Buffering);
describe("and the first chunk data has been loaded", () => {
beforeEach(async () => {
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
it("should play the chunks beginning with the first one", () => {
@ -525,7 +570,6 @@ describe("VoiceBroadcastPlayback", () => {
it("should update the time", () => {
expect(playback.timeSeconds).toBe(11);
expect(playback.timeLeftSeconds).toBe(2);
});
});
@ -660,10 +704,12 @@ describe("VoiceBroadcastPlayback", () => {
});
});
});
});
describe("and calling toggle for the first time", () => {
beforeEach(async () => {
await playback.toggle();
playback.toggle();
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);
@ -693,7 +739,8 @@ describe("VoiceBroadcastPlayback", () => {
describe("and calling toggle", () => {
beforeEach(async () => {
mocked(onStateChanged).mockReset();
await playback.toggle();
playback.toggle();
await simulateFirstChunkArrived();
});
itShouldSetTheStateTo(VoiceBroadcastPlaybackState.Playing);

View File

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