Apply `strictNullChecks` to `src/components/views/rooms/*` (#10875)

* fix everything except notificationbadge

* unit test ThirdPartyMemberInfo

* fix RoomPreviewBar dm tests

* lint

* test PinnedEventTile
pull/28788/head^2
Kerry 2023-05-22 11:53:23 +12:00 committed by GitHub
parent 08c0f332b3
commit 74d30187a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 326 additions and 17 deletions

View File

@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import classNames from "classnames";
import { Resizable } from "re-resizable";
import { Resizable, Size } from "re-resizable";
import { Room } from "matrix-js-sdk/src/models/room";
import { IWidget } from "matrix-widget-api";
@ -124,7 +124,7 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
Container.Top,
this.topApps()
.slice(1)
.map((_, i) => this.resizer.forHandleAt(i).size),
.map((_, i) => this.resizer.forHandleAt(i)!.size),
);
this.setState({ resizingHorizontal: false });
},
@ -339,7 +339,9 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
return (
<Resizable
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
// types do not support undefined height/width
// but resizable code checks specifically for undefined on Size prop
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined } as unknown as Size}
minHeight={minHeight}
maxHeight={maxHeight}
onResizeStart={() => {

View File

@ -49,6 +49,7 @@ import { PosthogAnalytics } from "../../../PosthogAnalytics";
import { editorRoomKey, editorStateKey } from "../../../Editing";
import DocumentOffset from "../../../editor/offset";
import { attachMentions, attachRelation } from "./SendMessageComposer";
import { filterBoolean } from "../../../utils/arrays";
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body;
@ -149,8 +150,14 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
this.dispatcherRef = dis.register(this.onAction);
}
private getRoom(): Room | null {
return this.props.mxClient.getRoom(this.props.editState.getEvent().getRoomId());
private getRoom(): Room {
const roomId = this.props.editState.getEvent().getRoomId();
const room = this.props.mxClient.getRoom(roomId);
// Something is very wrong if we encounter this
if (!room) {
throw new Error(`Cannot find room for event ${roomId}`);
}
return room;
}
private onKeyDown = (event: KeyboardEvent): void => {
@ -411,7 +418,8 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
if (editState.hasEditorState()) {
// if restoring state from a previous editor,
// restore serialized parts from the state
parts = editState.getSerializedParts().map((p) => partCreator.deserializePart(p));
// (editState.hasEditorState() checks getSerializedParts is not null)
parts = filterBoolean<Part>(editState.getSerializedParts()!.map((p) => partCreator.deserializePart(p)));
} else {
// otherwise, either restore serialized parts from localStorage or parse the body of the event
const restoredParts = this.restoreStoredEditorState(partCreator);

View File

@ -71,6 +71,10 @@ export default class PinnedEventTile extends React.Component<IProps> {
public render(): React.ReactNode {
const sender = this.props.event.getSender();
if (!sender) {
throw new Error("Pinned event unexpectedly has no sender");
}
let unpinButton: JSX.Element | undefined;
if (this.props.onUnpinClicked) {
unpinButton = (

View File

@ -165,7 +165,7 @@ export default class ReadReceiptMarker extends React.PureComponent<IProps, IStat
return 0;
}
return info.top + info.parent.getBoundingClientRect().top;
return (info.top ?? 0) + info.parent.getBoundingClientRect().top;
}
private animateMarker(): void {

View File

@ -493,7 +493,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> {
const isDM = this.isDMInvite();
if (isDM) {
title = _t("Do you want to chat with %(user)s?", { user: inviteMember.name });
title = _t("Do you want to chat with %(user)s?", {
user: inviteMember?.name ?? this.props.inviterName,
});
subTitle = [avatar, _t("<userName/> wants to chat", {}, { userName: () => inviterElement })];
primaryActionLabel = _t("Start chatting");
} else {

View File

@ -53,11 +53,12 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
const me = this.room?.getMember(MatrixClientPeg.get().getUserId()!);
const powerLevels = this.room?.currentState.getStateEvents("m.room.power_levels", "");
const senderId = this.props.event.getSender()!;
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
if (typeof kickLevel !== "number") kickLevel = 50;
const sender = this.room?.getMember(this.props.event.getSender());
const sender = this.room?.getMember(senderId);
this.state = {
stateKey: this.props.event.getStateKey()!,
@ -65,7 +66,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
displayName: this.props.event.getContent().display_name,
invited: true,
canKick: me ? me.powerLevel > kickLevel : false,
senderName: sender?.name ?? this.props.event.getSender(),
senderName: sender?.name ?? senderId,
};
}

View File

@ -148,6 +148,12 @@ describe("<EditMessageComposer/>", () => {
expect(mockClient.sendMessage).toHaveBeenCalledWith(editedEvent.getRoomId()!, null, expectedBody);
});
it("should throw when room for message is not found", () => {
mockClient.getRoom.mockReturnValue(null);
const editState = new EditorStateTransfer(editedEvent);
expect(() => getComponent(editState)).toThrow("Cannot find room for event !abc:test");
});
describe("createEditContent", () => {
it("sends plaintext messages correctly", () => {
const model = new EditorModel([], createPartCreator());

View File

@ -0,0 +1,72 @@
/*
Copyright 2023 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 { render } from "@testing-library/react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import PinnedEventTile from "../../../../src/components/views/rooms/PinnedEventTile";
import { getMockClientWithEventEmitter } from "../../../test-utils";
describe("<PinnedEventTile />", () => {
const userId = "@alice:server.org";
const roomId = "!room:server.org";
const mockClient = getMockClientWithEventEmitter({
getRoom: jest.fn(),
});
const room = new Room(roomId, mockClient, userId);
const permalinkCreator = new RoomPermalinkCreator(room);
const getComponent = (event: MatrixEvent) =>
render(<PinnedEventTile permalinkCreator={permalinkCreator} event={event} />);
beforeEach(() => {
mockClient.getRoom.mockReturnValue(room);
});
it("should render pinned event", () => {
const pin1 = new MatrixEvent({
type: "m.room.message",
sender: userId,
content: {
body: "First pinned message",
msgtype: "m.text",
},
room_id: roomId,
origin_server_ts: 0,
});
const { container } = getComponent(pin1);
expect(container).toMatchSnapshot();
});
it("should throw when pinned event has no sender", () => {
const pin1 = new MatrixEvent({
type: "m.room.message",
sender: undefined,
content: {
body: "First pinned message",
msgtype: "m.text",
},
room_id: roomId,
origin_server_ts: 0,
});
expect(() => getComponent(pin1)).toThrow("Pinned event unexpectedly has no sender");
});
});

View File

@ -1,5 +1,5 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Copyright 2023 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.
@ -212,7 +212,7 @@ describe("<RoomPreviewBar />", () => {
const userMemberWithDmInvite = makeMockRoomMember({
userId,
membership: "invite",
memberContent: { is_direct: true },
memberContent: { is_direct: true, membership: "invite" },
});
const inviterMember = makeMockRoomMember({
userId: inviterUserId,
@ -299,7 +299,7 @@ describe("<RoomPreviewBar />", () => {
onRejectClick.mockClear();
});
it("renders invite message to a non-dm room", () => {
it("renders invite message", () => {
const component = getComponent({ inviterName, room });
expect(getMessage(component)).toMatchSnapshot();
});

View File

@ -0,0 +1,75 @@
/*
Copyright 2023 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 { render, screen } from "@testing-library/react";
import { EventType, IEvent, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import ThirdPartyMemberInfo from "../../../../src/components/views/rooms/ThirdPartyMemberInfo";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
describe("<ThirdPartyMemberInfo />", () => {
const userId = "@alice:server.org";
const roomId = "!room:server.org";
const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
getRoom: jest.fn(),
});
// make invite event with defaults
const makeInviteEvent = (props: Partial<IEvent> = {}): MatrixEvent =>
new MatrixEvent({
type: EventType.RoomThirdPartyInvite,
state_key: "123456",
sender: userId,
room_id: roomId,
content: {
display_name: "bob@bob.com",
key_validity_url: "https://isthiskeyvalid.org",
public_key: "abc123",
},
...props,
});
const defaultEvent = makeInviteEvent();
const getComponent = (event: MatrixEvent = defaultEvent) => render(<ThirdPartyMemberInfo event={event} />);
const room = new Room(roomId, mockClient, userId);
const aliceMember = new RoomMember(roomId, userId);
aliceMember.name = "Alice DisplayName";
beforeEach(() => {
jest.spyOn(room, "getMember").mockImplementation((id) => (id === userId ? aliceMember : null));
mockClient.getRoom.mockClear().mockReturnValue(room);
});
it("should render invite", () => {
const { container } = getComponent();
expect(container).toMatchSnapshot();
});
it("should render invite when room in not available", () => {
const event = makeInviteEvent({ room_id: "not_available" });
const { container } = getComponent(event);
expect(container).toMatchSnapshot();
});
it("should use inviter's id when room member is not available", () => {
const event = makeInviteEvent({ sender: "@charlie:server.org" });
getComponent(event);
expect(screen.getByText("Invited by @charlie:server.org")).toBeInTheDocument();
});
});

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PinnedEventTile /> should render pinned event 1`] = `
<div>
<div
class="mx_PinnedEventTile"
>
<span
class="mx_BaseAvatar mx_PinnedEventTile_senderAvatar"
role="presentation"
>
<span
aria-hidden="true"
class="mx_BaseAvatar_initial"
style="font-size: 15.600000000000001px; width: 24px; line-height: 24px;"
>
A
</span>
<img
alt=""
aria-hidden="true"
class="mx_BaseAvatar_image"
data-testid="avatar-img"
loading="lazy"
src="data:image/png;base64,00"
style="width: 24px; height: 24px;"
/>
</span>
<span
class="mx_PinnedEventTile_sender mx_Username_color6"
>
@alice:server.org
</span>
<div
class="mx_PinnedEventTile_message"
>
<div
class="mx_MTextBody mx_EventTile_content"
>
<span
class="mx_EventTile_body"
dir="auto"
>
First pinned message
</span>
</div>
</div>
<div
class="mx_PinnedEventTile_footer"
>
<span
class="mx_MessageTimestamp mx_PinnedEventTile_timestamp"
>
Thu, Jan 1 1970 00:00:00
</span>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_link"
role="button"
tabindex="0"
>
View message
</div>
</div>
</div>
</div>
`;

View File

@ -150,12 +150,12 @@ exports[`<RoomPreviewBar /> with an invite with an invited email when invitedEma
</div>
`;
exports[`<RoomPreviewBar /> with an invite without an invited email for a dm room renders invite message to a non-dm room 1`] = `
exports[`<RoomPreviewBar /> with an invite without an invited email for a dm room renders invite message 1`] = `
<div
class="mx_RoomPreviewBar_message"
>
<h3>
Do you want to join RoomPreviewBar-test-room?
Do you want to chat with @inviter:test.com?
</h3>
<p>
<span
@ -192,7 +192,7 @@ exports[`<RoomPreviewBar /> with an invite without an invited email for a dm roo
@inviter:test.com
)
</span>
invited you
wants to chat
</span>
</p>
</div>
@ -207,7 +207,7 @@ exports[`<RoomPreviewBar /> with an invite without an invited email for a dm roo
role="button"
tabindex="0"
>
Accept
Start chatting
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary"

View File

@ -0,0 +1,73 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ThirdPartyMemberInfo /> should render invite 1`] = `
<div>
<div
class="mx_MemberInfo"
role="tabpanel"
>
<div
class="mx_MemberInfo_name"
>
<div
class="mx_AccessibleButton mx_MemberInfo_cancel"
role="button"
tabindex="0"
title="Close"
/>
<h2>
bob@bob.com
</h2>
</div>
<div
class="mx_MemberInfo_container"
>
<div
class="mx_MemberInfo_profile"
>
<div
class="mx_MemberInfo_profileField"
>
Invited by Alice DisplayName
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ThirdPartyMemberInfo /> should render invite when room in not available 1`] = `
<div>
<div
class="mx_MemberInfo"
role="tabpanel"
>
<div
class="mx_MemberInfo_name"
>
<div
class="mx_AccessibleButton mx_MemberInfo_cancel"
role="button"
tabindex="0"
title="Close"
/>
<h2>
bob@bob.com
</h2>
</div>
<div
class="mx_MemberInfo_container"
>
<div
class="mx_MemberInfo_profile"
>
<div
class="mx_MemberInfo_profileField"
>
Invited by Alice DisplayName
</div>
</div>
</div>
</div>
</div>
`;