From 8cb8cd4eb1b06832ebc0930d50cebae8fde6b604 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 14 Mar 2023 11:09:35 +0000 Subject: [PATCH] Conform more code to `strictNullChecks` (#10368 * Conform more code to `strictNullChecks` * Iterate --- src/Modal.tsx | 13 +++- src/components/structures/RoomView.tsx | 38 ++++++------ src/components/structures/SpaceHierarchy.tsx | 62 +++++++++---------- .../views/dialogs/BugReportDialog.tsx | 12 ++-- .../views/dialogs/CreateRoomDialog.tsx | 25 ++++---- .../views/dialogs/CreateSubspaceDialog.tsx | 6 +- src/components/views/dialogs/ExportDialog.tsx | 4 +- src/components/views/dialogs/LogoutDialog.tsx | 2 +- .../views/dialogs/ModalWidgetDialog.tsx | 22 ++++--- .../views/dialogs/ReportEventDialog.tsx | 20 +++--- .../dialogs/RoomUpgradeWarningDialog.tsx | 30 ++++++--- .../dialogs/SlidingSyncOptionsDialog.tsx | 4 +- .../views/dialogs/TextInputDialog.tsx | 17 ++--- .../views/dialogs/UploadConfirmDialog.tsx | 11 ++-- .../security/RestoreKeyBackupDialog.tsx | 16 ++--- .../dialogs/spotlight/SpotlightDialog.tsx | 2 +- src/components/views/right_panel/UserInfo.tsx | 31 +++++----- .../views/spaces/SpaceBasicSettings.tsx | 2 +- src/customisations/ChatExport.ts | 2 +- src/i18n/strings/en_EN.json | 2 +- src/rageshake/submit-rageshake.ts | 2 +- src/sentry.ts | 2 +- src/utils/pillify.tsx | 4 +- .../views/right_panel/UserInfo-test.tsx | 4 +- 24 files changed, 176 insertions(+), 157 deletions(-) diff --git a/src/Modal.tsx b/src/Modal.tsx index 54ae185ad1..b3887795b2 100644 --- a/src/Modal.tsx +++ b/src/Modal.tsx @@ -32,8 +32,19 @@ export type ComponentType = React.ComponentType<{ onFinished?(...args: any): void; }>; +type Defaultize = P extends any + ? string extends keyof P + ? P + : Pick> & + Partial>> & + Partial>> + : never; + // Generic type which returns the props of the Modal component with the onFinished being optional. -export type ComponentProps = Omit, "onFinished"> & +export type ComponentProps = Defaultize< + Omit, "onFinished">, + C["defaultProps"] +> & Partial, "onFinished">>; export interface IModal { diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 451e893b11..bc533a137f 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1020,7 +1020,7 @@ export class RoomView extends React.Component { }); }; - private onPageUnload = (event: BeforeUnloadEvent): string => { + private onPageUnload = (event: BeforeUnloadEvent): string | undefined => { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return (event.returnValue = _t("You seem to be uploading files, are you sure you want to quit?")); } else if (this.getCallForRoom() && this.state.callState !== "ended") { @@ -1034,7 +1034,7 @@ export class RoomView extends React.Component { const action = getKeyBindingsManager().getRoomAction(ev); switch (action) { case KeyBindingAction.DismissReadMarker: - this.messagePanel.forgetReadMarker(); + this.messagePanel?.forgetReadMarker(); this.jumpToLiveTimeline(); handled = true; break; @@ -1067,7 +1067,7 @@ export class RoomView extends React.Component { if (!roomId) return; const call = this.getCallForRoom(); - this.setState({ callState: call ? call.state : null }); + this.setState({ callState: call?.state }); }; private onAction = async (payload: ActionPayload): Promise => { @@ -1087,7 +1087,7 @@ export class RoomView extends React.Component { ContentMessages.sharedInstance().sendContentListToRoom( [payload.file], this.state.room.roomId, - null, + undefined, this.context.client, ); break; @@ -1117,7 +1117,7 @@ export class RoomView extends React.Component { if (!this.state.matrixClientIsReady) { this.setState( { - matrixClientIsReady: this.context.client?.isInitialSyncComplete(), + matrixClientIsReady: !!this.context.client?.isInitialSyncComplete(), }, () => { // send another "initial" RVS update to trigger peeking if needed @@ -1137,7 +1137,7 @@ export class RoomView extends React.Component { case Action.EditEvent: { // Quit early if we're trying to edit events in wrong rendering context if (payload.timelineRenderingType !== this.state.timelineRenderingType) return; - const editState = payload.event ? new EditorStateTransfer(payload.event) : null; + const editState = payload.event ? new EditorStateTransfer(payload.event) : undefined; this.setState({ editState }, () => { if (payload.event) { this.messagePanel?.scrollToEventIfNeeded(payload.event.getId()); @@ -1194,7 +1194,7 @@ export class RoomView extends React.Component { private onRoomTimeline = ( ev: MatrixEvent, - room: Room | null, + room: Room | undefined, toStartOfTimeline: boolean, removed: boolean, data?: IRoomTimelineData, @@ -1228,7 +1228,7 @@ export class RoomView extends React.Component { this.handleEffects(ev); } - if (ev.getSender() !== this.context.client.credentials.userId) { + if (ev.getSender() !== this.context.client.getSafeUserId()) { // update unread count when scrolled up if (!this.state.search && this.state.atEndOfLiveTimeline) { // no change @@ -1325,7 +1325,7 @@ export class RoomView extends React.Component { }; private getRoomTombstone(room = this.state.room): MatrixEvent | undefined { - return room?.currentState.getStateEvents(EventType.RoomTombstone, ""); + return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; } private async calculateRecommendedVersion(room: Room): Promise { @@ -1336,7 +1336,7 @@ export class RoomView extends React.Component { private async loadMembersIfJoined(room: Room): Promise { // lazy load members if enabled - if (this.context.client.hasLazyLoadMembersEnabled()) { + if (this.context.client?.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === "join") { try { await room.loadMembersIfNeeded(); @@ -1415,7 +1415,7 @@ export class RoomView extends React.Component { }; private async updateE2EStatus(room: Room): Promise { - if (!this.context.client.isRoomEncrypted(room.roomId)) return; + if (!this.context.client?.isRoomEncrypted(room.roomId)) return; // If crypto is not currently enabled, we aren't tracking devices at all, // so we don't know what the answer is. Let's error on the safe side and show @@ -2093,7 +2093,7 @@ export class RoomView extends React.Component { // We have successfully loaded this room, and are not previewing. // Display the "normal" room view. - let activeCall = null; + let activeCall: MatrixCall | null = null; { // New block because this variable doesn't need to hang around for the rest of the function const call = this.getCallForRoom(); @@ -2102,7 +2102,7 @@ export class RoomView extends React.Component { } } - let statusBar; + let statusBar: JSX.Element | undefined; let isStatusAreaExpanded = true; if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { @@ -2301,7 +2301,7 @@ export class RoomView extends React.Component { /> ); - let topUnreadMessagesBar = null; + let topUnreadMessagesBar: JSX.Element | undefined; // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense if (this.state.showTopUnreadMessagesBar && !this.state.search) { topUnreadMessagesBar = ( @@ -2342,7 +2342,7 @@ export class RoomView extends React.Component { const showChatEffects = SettingsStore.getValue("showChatEffects"); - let mainSplitBody: React.ReactFragment; + let mainSplitBody: JSX.Element | undefined; let mainSplitContentClassName: string; // Decide what to show in the main split switch (this.state.mainSplitContentType) { @@ -2396,10 +2396,10 @@ export class RoomView extends React.Component { const mainSplitContentClasses = classNames("mx_RoomView_body", mainSplitContentClassName); let excludedRightPanelPhaseButtons = [RightPanelPhases.Timeline]; - let onAppsClick = this.onAppsClick; - let onForgetClick = this.onForgetClick; - let onSearchClick = this.onSearchClick; - let onInviteClick = null; + let onAppsClick: (() => void) | null = this.onAppsClick; + let onForgetClick: (() => void) | null = this.onForgetClick; + let onSearchClick: (() => void) | null = this.onSearchClick; + let onInviteClick: (() => void) | null = null; let viewingCall = false; // Simplify the header for other main split types diff --git a/src/components/structures/SpaceHierarchy.tsx b/src/components/structures/SpaceHierarchy.tsx index a0558413dd..e0d124af0c 100644 --- a/src/components/structures/SpaceHierarchy.tsx +++ b/src/components/structures/SpaceHierarchy.tsx @@ -100,9 +100,9 @@ const Tile: React.FC = ({ children, }) => { const cli = useContext(MatrixClientContext); - const [joinedRoom, setJoinedRoom] = useState(() => { + const [joinedRoom, setJoinedRoom] = useState(() => { const cliRoom = cli.getRoom(room.room_id); - return cliRoom?.getMyMembership() === "join" ? cliRoom : null; + return cliRoom?.getMyMembership() === "join" ? cliRoom : undefined; }); const joinedRoomName = useTypedEventEmitterState(joinedRoom, RoomEvent.Name, (room) => room?.name); const name = @@ -264,9 +264,9 @@ const Tile: React.FC = ({ ); - let childToggle: JSX.Element; - let childSection: JSX.Element; - let onKeyDown: KeyboardEventHandler; + let childToggle: JSX.Element | undefined; + let childSection: JSX.Element | undefined; + let onKeyDown: KeyboardEventHandler | undefined; if (children) { // the chevron is purposefully a div rather than a button as it should be ignored for a11y childToggle = ( @@ -386,7 +386,7 @@ export const showRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st }); }; -export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): Promise => { +export const joinRoom = async (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: string): Promise => { // Don't let the user view a room they won't be able to either peek or join: // fail earlier so they don't have to click back to the directory. if (cli.isGuest()) { @@ -394,24 +394,20 @@ export const joinRoom = (cli: MatrixClient, hierarchy: RoomHierarchy, roomId: st return; } - const prom = cli.joinRoom(roomId, { - viaServers: Array.from(hierarchy.viaMap.get(roomId) || []), + try { + await cli.joinRoom(roomId, { + viaServers: Array.from(hierarchy.viaMap.get(roomId) || []), + }); + } catch (err) { + SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId); + return; + } + + defaultDispatcher.dispatch({ + action: Action.JoinRoomReady, + roomId, + metricsTrigger: "SpaceHierarchy", }); - - prom.then( - () => { - defaultDispatcher.dispatch({ - action: Action.JoinRoomReady, - roomId, - metricsTrigger: "SpaceHierarchy", - }); - }, - (err) => { - SdkContextClass.instance.roomViewStore.showJoinRoomError(err, roomId); - }, - ); - - return prom; }; interface IHierarchyLevelProps { @@ -433,7 +429,7 @@ export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: ); // Pick latest room that is actually part of the hierarchy - let cliRoom = null; + let cliRoom: Room | null = null; for (let idx = history.length - 1; idx >= 0; --idx) { if (hierarchy.roomMap.get(history[idx].roomId)) { cliRoom = history[idx]; @@ -448,7 +444,7 @@ export const toLocalRoom = (cli: MatrixClient, room: IHierarchyRoom, hierarchy: room_type: cliRoom.getType(), name: cliRoom.name, topic: cliRoom.currentState.getStateEvents(EventType.RoomTopic, "")?.getContent().topic, - avatar_url: cliRoom.getMxcAvatarUrl(), + avatar_url: cliRoom.getMxcAvatarUrl() ?? undefined, canonical_alias: cliRoom.getCanonicalAlias() ?? undefined, aliases: cliRoom.getAltAliases(), world_readable: @@ -476,7 +472,7 @@ export const HierarchyLevel: React.FC = ({ }) => { const cli = useContext(MatrixClientContext); const space = cli.getRoom(root.room_id); - const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getSafeUserId()); const sortedChildren = sortBy(root.children_state, (ev) => { return getChildOrder(ev.content.order, ev.origin_server_ts, ev.state_key); @@ -579,7 +575,7 @@ export const useRoomHierarchy = ( const loadMore = useCallback( async (pageSize?: number): Promise => { - if (hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport || error) return; + if (!hierarchy || hierarchy.loading || !hierarchy.canLoadMore || hierarchy.noSupport || error) return; await hierarchy.load(pageSize).catch(setError); setRooms(hierarchy.rooms); }, @@ -673,7 +669,7 @@ const ManageButtons: React.FC = ({ hierarchy, selected, set onClick={async (): Promise => { setRemoving(true); try { - const userId = cli.getUserId(); + const userId = cli.getSafeUserId(); for (const [parentId, childId] of selectedRelations) { await cli.sendStateEvent(parentId, EventType.SpaceChild, {}, childId); @@ -759,7 +755,7 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a const visited = new Set(); const queue = [...directMatches.map((r) => r.room_id)]; while (queue.length) { - const roomId = queue.pop(); + const roomId = queue.pop()!; visited.add(roomId); hierarchy.backRefs.get(roomId)?.forEach((parentId) => { if (!visited.has(parentId)) { @@ -797,7 +793,7 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a return; } - const parentSet = selected.get(parentId); + const parentSet = selected.get(parentId)!; if (!parentSet.has(childId)) { setSelected(new Map(selected.set(parentId, new Set([...parentSet, childId])))); return; @@ -816,9 +812,9 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a } else { const hasPermissions = space?.getMyMembership() === "join" && - space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()); + space.currentState.maySendStateEvent(EventType.SpaceChild, cli.getSafeUserId()); - let results: JSX.Element; + let results: JSX.Element | undefined; if (filteredRoomSet.size) { results = ( <> @@ -843,7 +839,7 @@ const SpaceHierarchy: React.FC = ({ space, initialText = "", showRoom, a ); } - let loader: JSX.Element; + let loader: JSX.Element | undefined; if (hierarchy.canLoadMore) { loader = (
diff --git a/src/components/views/dialogs/BugReportDialog.tsx b/src/components/views/dialogs/BugReportDialog.tsx index 4074db6d3e..319f7db817 100644 --- a/src/components/views/dialogs/BugReportDialog.tsx +++ b/src/components/views/dialogs/BugReportDialog.tsx @@ -43,12 +43,12 @@ interface IProps { interface IState { sendLogs: boolean; busy: boolean; - err: string; + err: string | null; issueUrl: string; text: string; - progress: string; + progress: string | null; downloadBusy: boolean; - downloadProgress: string; + downloadProgress: string | null; } export default class BugReportDialog extends React.Component { @@ -181,12 +181,12 @@ export default class BugReportDialog extends React.Component { }; public render(): React.ReactNode { - let error = null; + let error: JSX.Element | undefined; if (this.state.err) { error =
{this.state.err}
; } - let progress = null; + let progress: JSX.Element | undefined; if (this.state.busy) { progress = (
@@ -196,7 +196,7 @@ export default class BugReportDialog extends React.Component { ); } - let warning; + let warning: JSX.Element | undefined; if (window.Modernizr && Object.values(window.Modernizr).some((support) => support === false)) { warning = (

diff --git a/src/components/views/dialogs/CreateRoomDialog.tsx b/src/components/views/dialogs/CreateRoomDialog.tsx index b15fb800d2..1d91330023 100644 --- a/src/components/views/dialogs/CreateRoomDialog.tsx +++ b/src/components/views/dialogs/CreateRoomDialog.tsx @@ -126,7 +126,7 @@ export default class CreateRoomDialog extends React.Component { public componentDidMount(): void { // move focus to first field when showing dialog - this.nameField.current.focus(); + this.nameField.current?.focus(); } private onKeyDown = (event: KeyboardEvent): void => { @@ -141,10 +141,9 @@ export default class CreateRoomDialog extends React.Component { }; private onOk = async (): Promise => { + if (!this.nameField.current) return; const activeElement = document.activeElement as HTMLElement; - if (activeElement) { - activeElement.blur(); - } + activeElement?.blur(); await this.nameField.current.validate({ allowEmpty: false }); if (this.aliasField.current) { await this.aliasField.current.validate({ allowEmpty: false }); @@ -155,7 +154,7 @@ export default class CreateRoomDialog extends React.Component { if (this.state.nameIsValid && (!this.aliasField.current || this.aliasField.current.isValid)) { this.props.onFinished(true, this.roomCreateOptions()); } else { - let field; + let field: RoomAliasField | Field | null = null; if (!this.state.nameIsValid) { field = this.nameField.current; } else if (this.aliasField.current && !this.aliasField.current.isValid) { @@ -163,7 +162,7 @@ export default class CreateRoomDialog extends React.Component { } if (field) { field.focus(); - field.validate({ allowEmpty: false, focused: true }); + await field.validate({ allowEmpty: false, focused: true }); } } }; @@ -202,7 +201,7 @@ export default class CreateRoomDialog extends React.Component { private onNameValidate = async (fieldState: IFieldState): Promise => { const result = await CreateRoomDialog.validateRoomName(fieldState); - this.setState({ nameIsValid: result.valid }); + this.setState({ nameIsValid: !!result.valid }); return result; }; @@ -219,9 +218,9 @@ export default class CreateRoomDialog extends React.Component { public render(): React.ReactNode { const isVideoRoom = this.props.type === RoomType.ElementVideo; - let aliasField: JSX.Element; + let aliasField: JSX.Element | undefined; if (this.state.joinRule === JoinRule.Public) { - const domain = MatrixClientPeg.get().getDomain(); + const domain = MatrixClientPeg.get().getDomain()!; aliasField = (

{ ); } - let publicPrivateLabel: JSX.Element; + let publicPrivateLabel: JSX.Element | undefined; if (this.state.joinRule === JoinRule.Restricted) { publicPrivateLabel = (

@@ -242,7 +241,7 @@ export default class CreateRoomDialog extends React.Component { "Everyone in will be able to find and join this room.", {}, { - SpaceName: () => {this.props.parentSpace.name}, + SpaceName: () => {this.props.parentSpace?.name ?? _t("Unnamed Space")}, }, )}   @@ -256,7 +255,7 @@ export default class CreateRoomDialog extends React.Component { "Anyone will be able to find and join this room, not just members of .", {}, { - SpaceName: () => {this.props.parentSpace.name}, + SpaceName: () => {this.props.parentSpace?.name ?? _t("Unnamed Space")}, }, )}   @@ -281,7 +280,7 @@ export default class CreateRoomDialog extends React.Component { ); } - let e2eeSection: JSX.Element; + let e2eeSection: JSX.Element | undefined; if (this.state.joinRule !== JoinRule.Public) { let microcopy: string; if (privateShouldBeEncrypted()) { diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 5e01396daf..4c12587ade 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -44,7 +44,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick const spaceNameField = useRef(); const [alias, setAlias] = useState(""); const spaceAliasField = useRef(); - const [avatar, setAvatar] = useState(null); + const [avatar, setAvatar] = useState(); const [topic, setTopic] = useState(""); const spaceJoinRule = space.getJoinRule(); @@ -56,7 +56,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick const onCreateSubspaceClick = async (e: ButtonEvent): Promise => { e.preventDefault(); - if (busy) return; + if (busy || !spaceNameField.current || !spaceAliasField.current) return; setBusy(true); // require & validate the space name field @@ -83,7 +83,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick } }; - let joinRuleMicrocopy: JSX.Element; + let joinRuleMicrocopy: JSX.Element | undefined; if (joinRule === JoinRule.Restricted) { joinRuleMicrocopy = (

diff --git a/src/components/views/dialogs/ExportDialog.tsx b/src/components/views/dialogs/ExportDialog.tsx index 84afb0a4cc..9ae9272a9b 100644 --- a/src/components/views/dialogs/ExportDialog.tsx +++ b/src/components/views/dialogs/ExportDialog.tsx @@ -73,7 +73,7 @@ const useExportFormState = (): ExportConfig => { const [exportType, setExportType] = useState(config.range ?? ExportType.Timeline); const [includeAttachments, setAttachments] = useState(config.includeAttachments ?? false); const [numberOfMessages, setNumberOfMessages] = useState(config.numberOfMessages ?? 100); - const [sizeLimit, setSizeLimit] = useState(config.sizeMb ?? 8); + const [sizeLimit, setSizeLimit] = useState(config.sizeMb ?? 8); return { exportFormat, @@ -260,7 +260,7 @@ const ExportDialog: React.FC = ({ room, onFinished }) => { ); }); - let messageCount = null; + let messageCount: JSX.Element | undefined; if (exportType === ExportType.LastNMessages && setNumberOfMessages) { messageCount = ( { dis.dispatch({ action: "logout" }); } // close dialog - this.props.onFinished(confirmed); + this.props.onFinished(!!confirmed); }; private onSetRecoveryMethodClick = (): void => { diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index e62e8b53f8..7a73d25d23 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -70,7 +70,7 @@ export default class ModalWidgetDialog extends React.PureComponent b.id); @@ -78,21 +78,23 @@ export default class ModalWidgetDialog extends React.PureComponent { - this.state.messaging.sendWidgetConfig(this.props.widgetDefinition); + this.state.messaging?.sendWidgetConfig(this.props.widgetDefinition); }; private onLoad = (): void => { + if (!this.state.messaging) return; this.state.messaging.once("ready", this.onReady); this.state.messaging.on(`action:${WidgetApiFromWidgetAction.CloseModalWidget}`, this.onWidgetClose); this.state.messaging.on(`action:${WidgetApiFromWidgetAction.SetModalButtonEnabled}`, this.onButtonEnableToggle); @@ -106,7 +108,7 @@ export default class ModalWidgetDialog extends React.PureComponent { - this.state.messaging.notifyModalWidgetButtonClicked(def.id); + this.state.messaging?.notifyModalWidgetButtonClicked(def.id); }; const isDisabled = this.state.disabledButtonIds.includes(def.id); @@ -201,7 +203,7 @@ export default class ModalWidgetDialog extends React.PureComponent