From 127a3b667cd18d71b46697f37e53d61569e8cf8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 10 Mar 2023 14:55:06 +0000 Subject: [PATCH] Conform more of the codebase to `strictNullChecks` (#10350 * Conform more of the codebase to `strictNullChecks` * Iterate * Generics ftw * Iterate --- src/@types/common.ts | 2 + src/AddThreepid.ts | 13 +++- .../context_menu/ContextMenuButton.tsx | 2 +- .../context_menu/ContextMenuTooltipButton.tsx | 2 +- src/actions/RoomListActions.ts | 10 +-- src/audio/PlaybackQueue.ts | 6 +- src/boundThreepids.ts | 3 + .../structures/AutoHideScrollbar.tsx | 2 +- src/components/structures/FilePanel.tsx | 4 +- .../structures/GenericDropdownMenu.tsx | 27 +++---- .../structures/IndicatorScrollbar.tsx | 2 +- src/components/structures/LeftPanel.tsx | 1 + .../structures/LegacyCallEventGrouper.ts | 12 ++-- src/components/structures/LoggedInView.tsx | 24 +++---- src/components/structures/MatrixChat.tsx | 70 ++++++++++--------- src/components/structures/MessagePanel.tsx | 39 +++++------ src/components/structures/ScrollPanel.tsx | 6 +- src/components/structures/TabbedView.tsx | 13 ++-- src/components/structures/TimelinePanel.tsx | 52 +++++++------- src/components/structures/UserMenu.tsx | 23 +++--- src/components/structures/ViewSource.tsx | 6 +- .../structures/auth/ForgotPassword.tsx | 2 +- src/components/structures/auth/Login.tsx | 8 +-- .../structures/auth/SetupEncryptionBody.tsx | 6 +- src/components/structures/auth/SoftLogout.tsx | 10 +-- .../auth/header/AuthHeaderDisplay.tsx | 2 +- .../views/context_menus/RoomContextMenu.tsx | 24 +++---- .../dialogs/AddExistingToSpaceDialog.tsx | 19 ++--- src/components/views/dialogs/BaseDialog.tsx | 5 +- src/components/views/dialogs/InviteDialog.tsx | 6 +- .../views/dialogs/RoomSettingsDialog.tsx | 7 +- .../views/dialogs/SpacePreferencesDialog.tsx | 3 +- .../views/dialogs/SpaceSettingsDialog.tsx | 3 +- .../views/dialogs/UserSettingsDialog.tsx | 5 +- .../elements/DesktopCapturerSourcePicker.tsx | 7 +- .../views/elements/PowerSelector.tsx | 19 ++--- src/components/views/elements/QRCode.tsx | 2 +- src/components/views/elements/ReplyChain.tsx | 12 ++-- src/components/views/elements/Slider.tsx | 2 +- .../views/elements/TruncatedList.tsx | 8 +-- .../views/right_panel/PinnedMessagesCard.tsx | 6 +- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/rooms/E2EIcon.tsx | 4 +- src/components/views/rooms/EntityTile.tsx | 8 +-- src/components/views/rooms/PresenceLabel.tsx | 6 +- .../views/settings/account/PhoneNumbers.tsx | 6 +- .../tabs/room/RolesRoomSettingsTab.tsx | 11 +-- src/stores/WidgetStore.ts | 2 +- src/utils/localRoom/isLocalRoom.ts | 2 +- .../components/structures/TabbedView-test.tsx | 8 +-- .../__snapshots__/TabbedView-test.tsx.snap | 12 ---- .../PollHistoryDialog-test.tsx.snap | 2 +- test/stores/BreadcrumbsStore-test.ts | 4 +- 53 files changed, 279 insertions(+), 263 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index be77708a82..3281ad6872 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -52,3 +52,5 @@ export type KeysStartingWith = { // eslint-disable-next-line @typescript-eslint/no-unused-vars [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X }[keyof Input]; + +export type NonEmptyArray = [T, ...T[]]; diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 51779042f9..2c37d00237 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -26,7 +26,11 @@ import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryCompon import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; function getIdServerDomain(): string { - return MatrixClientPeg.get().idBaseUrl.split("://")[1]; + const idBaseUrl = MatrixClientPeg.get().idBaseUrl; + if (!idBaseUrl) { + throw new Error("Identity server not set"); + } + return idBaseUrl.split("://")[1]; } export type Binding = { @@ -190,6 +194,9 @@ export default class AddThreepid { if (this.bind) { const authClient = new IdentityAuthClient(); const identityAccessToken = await authClient.getAccessToken(); + if (!identityAccessToken) { + throw new Error("No identity access token found"); + } await MatrixClientPeg.get().bindThreePid({ sid: this.sessionId, client_secret: this.clientSecret, @@ -279,7 +286,9 @@ export default class AddThreepid { * with a "message" property which contains a human-readable message detailing why * the request failed. */ - public async haveMsisdnToken(msisdnToken: string): Promise { + public async haveMsisdnToken( + msisdnToken: string, + ): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> { const authClient = new IdentityAuthClient(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); diff --git a/src/accessibility/context_menu/ContextMenuButton.tsx b/src/accessibility/context_menu/ContextMenuButton.tsx index fa70fa6fff..090b42333f 100644 --- a/src/accessibility/context_menu/ContextMenuButton.tsx +++ b/src/accessibility/context_menu/ContextMenuButton.tsx @@ -39,7 +39,7 @@ export const ContextMenuButton: React.FC = ({ = ({ [2] | null = null; diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts index b7920316ef..6ae899704b 100644 --- a/src/audio/PlaybackQueue.ts +++ b/src/audio/PlaybackQueue.ts @@ -106,7 +106,7 @@ export class PlaybackQueue { // Remove the now-useless clock for some space savings this.clockStates.delete(mxEvent.getId()!); - if (wasLastPlaying) { + if (wasLastPlaying && this.currentPlaybackId) { this.recentFullPlays.add(this.currentPlaybackId); const orderClone = arrayFastClone(this.playbackIdOrder); const last = orderClone.pop(); @@ -188,8 +188,8 @@ export class PlaybackQueue { if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { const lastInstance = this.playbacks.get(this.currentPlaybackId); if ( - lastInstance.currentState === PlaybackState.Playing || - lastInstance.currentState === PlaybackState.Paused + lastInstance && + [PlaybackState.Playing, PlaybackState.Paused].includes(lastInstance.currentState) ) { order.push(this.currentPlaybackId); } diff --git a/src/boundThreepids.ts b/src/boundThreepids.ts index b8bc870295..c5c87a7953 100644 --- a/src/boundThreepids.ts +++ b/src/boundThreepids.ts @@ -35,6 +35,9 @@ export async function getThreepidsWithBindStatus( try { const authClient = new IdentityAuthClient(); const identityAccessToken = await authClient.getAccessToken({ check: false }); + if (!identityAccessToken) { + throw new Error("No identity access token found"); + } // Restructure for lookup query const query = threepids.map(({ medium, address }): [string, string] => [medium, address]); diff --git a/src/components/structures/AutoHideScrollbar.tsx b/src/components/structures/AutoHideScrollbar.tsx index 2a811194ef..d8ee5b71bb 100644 --- a/src/components/structures/AutoHideScrollbar.tsx +++ b/src/components/structures/AutoHideScrollbar.tsx @@ -29,7 +29,7 @@ export type IProps = Omit void; style?: React.CSSProperties; tabIndex?: number; - wrappedRef?: (ref: HTMLDivElement) => void; + wrappedRef?: (ref: HTMLDivElement | null) => void; children: ReactNode; }; diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index fa3ce1a754..9d92892d3b 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -99,7 +99,7 @@ class FilePanel extends React.Component { const timeline = this.state.timelineSet.getLiveTimeline(); if (ev.getType() !== "m.room.message") return; - if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) { + if (!["m.file", "m.image", "m.video", "m.audio"].includes(ev.getContent().msgtype!)) { return; } @@ -176,7 +176,7 @@ class FilePanel extends React.Component { // the event index to fulfill the pagination request. Asking the server // to paginate won't ever work since the server can't correctly filter // out events containing URLs - if (client.isRoomEncrypted(roomId) && eventIndex !== null) { + if (room && client.isRoomEncrypted(roomId) && eventIndex !== null) { return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit); } else { return timelineWindow.paginate(direction, limit); diff --git a/src/components/structures/GenericDropdownMenu.tsx b/src/components/structures/GenericDropdownMenu.tsx index 98dfbf0851..27dd60e107 100644 --- a/src/components/structures/GenericDropdownMenu.tsx +++ b/src/components/structures/GenericDropdownMenu.tsx @@ -177,19 +177,20 @@ export function GenericDropdownMenu({ ); } - const contextMenu = menuDisplayed ? ( - - {contextMenuOptions} - {AdditionalOptions && ( - - )} - - ) : null; + const contextMenu = + menuDisplayed && button.current ? ( + + {contextMenuOptions} + {AdditionalOptions && ( + + )} + + ) : null; return ( <> e > { private autoHideScrollbar = createRef>(); private scrollElement: HTMLDivElement; - private likelyTrackpadUser: boolean = null; + private likelyTrackpadUser: boolean | null = null; private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser public constructor(props: IProps) { diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 5e237b8524..bb83005d7c 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -271,6 +271,7 @@ export default class LeftPanel extends React.Component { // add appropriate sticky classes to wrapper so it has // the necessary top/bottom padding to put the sticky header in const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper + if (!listWrapper) return; if (lastTopHeader) { listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop"); } else { diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 4abd4d8d19..ba218e8a89 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -85,23 +85,23 @@ export default class LegacyCallEventGrouper extends EventEmitter { ); } - private get invite(): MatrixEvent { + private get invite(): MatrixEvent | undefined { return [...this.events].find((event) => event.getType() === EventType.CallInvite); } - private get hangup(): MatrixEvent { + private get hangup(): MatrixEvent | undefined { return [...this.events].find((event) => event.getType() === EventType.CallHangup); } - private get reject(): MatrixEvent { + private get reject(): MatrixEvent | undefined { return [...this.events].find((event) => event.getType() === EventType.CallReject); } - private get selectAnswer(): MatrixEvent { + private get selectAnswer(): MatrixEvent | undefined { return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer); } - public get isVoice(): boolean { + public get isVoice(): boolean | undefined { const invite = this.invite; if (!invite) return; @@ -114,7 +114,7 @@ export default class LegacyCallEventGrouper extends EventEmitter { return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null; } - public get rejectParty(): string { + public get rejectParty(): string | undefined { return this.reject?.getSender(); } diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx index cb944a00d4..d389345af8 100644 --- a/src/components/structures/LoggedInView.tsx +++ b/src/components/structures/LoggedInView.tsx @@ -170,7 +170,7 @@ class LoggedInView extends React.Component { monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient); this._matrixClient.on(ClientEvent.Sync, this.onSync); // Call `onSync` with the current state as well - this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData()); + this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData() ?? undefined); this._matrixClient.on(RoomStateEvent.Events, this.onRoomStateEvents); this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onCompactLayoutChanged); @@ -271,11 +271,11 @@ class LoggedInView extends React.Component { } private loadResizerPreferences(): void { - let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); + let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size")!, 10); if (isNaN(lhsSize)) { lhsSize = 350; } - this.resizer.forHandleWithId("lp-resizer").resize(lhsSize); + this.resizer.forHandleWithId("lp-resizer")?.resize(lhsSize); } private onAccountData = (event: MatrixEvent): void => { @@ -291,13 +291,13 @@ class LoggedInView extends React.Component { }); }; - private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => { + private onSync = (syncState: SyncState | null, oldSyncState: SyncState | null, data?: ISyncStateData): void => { const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode; const newErrCode = (data?.error as MatrixError)?.errcode; if (syncState === oldSyncState && oldErrCode === newErrCode) return; this.setState({ - syncErrorData: syncState === SyncState.Error ? data : null, + syncErrorData: syncState === SyncState.Error ? data : undefined, }); if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) { @@ -355,7 +355,7 @@ class LoggedInView extends React.Component { const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); for (const eventId of pinnedEventIds) { const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId); - const event = timeline.getEvents().find((ev) => ev.getId() === eventId); + const event = timeline?.getEvents().find((ev) => ev.getId() === eventId); if (event) events.push(event); } } @@ -390,7 +390,7 @@ class LoggedInView extends React.Component { if (inputableElement?.focus) { inputableElement.focus(); } else { - const inThread = !!document.activeElement.closest(".mx_ThreadView"); + const inThread = !!document.activeElement?.closest(".mx_ThreadView"); // refocusing during a paste event will make the paste end up in the newly focused element, // so dispatch synchronously before paste happens dis.dispatch( @@ -533,11 +533,11 @@ class LoggedInView extends React.Component { }); break; case KeyBindingAction.PreviousVisitedRoomOrSpace: - PlatformPeg.get().navigateForwardBack(true); + PlatformPeg.get()?.navigateForwardBack(true); handled = true; break; case KeyBindingAction.NextVisitedRoomOrSpace: - PlatformPeg.get().navigateForwardBack(false); + PlatformPeg.get()?.navigateForwardBack(false); handled = true; break; } @@ -555,7 +555,7 @@ class LoggedInView extends React.Component { ); SettingsStore.setValue( "showHiddenEventsInTimeline", - undefined, + null, SettingLevel.DEVICE, !hiddenEventVisibility, ); @@ -567,7 +567,7 @@ class LoggedInView extends React.Component { if ( !handled && - PlatformPeg.get().overrideBrowserShortcuts() && + PlatformPeg.get()?.overrideBrowserShortcuts() && ev.code.startsWith("Digit") && ev.code !== "Digit0" && // this is the shortcut for reset zoom, don't override it isOnlyCtrlOrCmdKeyEvent(ev) @@ -599,7 +599,7 @@ class LoggedInView extends React.Component { // If the user is entering a printable character outside of an input field // redirect it to the composer for them. if (!isClickShortcut && isPrintable && !getInputableElement(ev.target as HTMLElement)) { - const inThread = !!document.activeElement.closest(".mx_ThreadView"); + const inThread = !!document.activeElement?.closest(".mx_ThreadView"); // synchronous dispatch so we focus before key generates input dis.dispatch( { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 00a05b169a..1787f6fea8 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -187,9 +187,9 @@ interface IState { // The ID of the room we're viewing. This is either populated directly // in the case where we view a room by ID or by RoomView when it resolves // what ID an alias points at. - currentRoomId?: string; + currentRoomId: string | null; // If we're trying to just view a user ID (i.e. /user URL), this is it - currentUserId?: string; + currentUserId: string | null; // this is persisted as mx_lhs_size, loaded in LoggedInView collapseLhs: boolean; // Parameters used in the registration dance with the IS @@ -202,7 +202,7 @@ interface IState { // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: boolean; - syncError?: Error; + syncError: Error | null; resizeNotifier: ResizeNotifier; serverConfig?: ValidatedServerConfig; ready: boolean; @@ -248,6 +248,8 @@ export default class MatrixChat extends React.PureComponent { this.state = { view: Views.LOADING, collapseLhs: false, + currentRoomId: null, + currentUserId: null, hideToSRUsers: false, @@ -469,9 +471,9 @@ export default class MatrixChat extends React.PureComponent { ); }, 1000); - private getFallbackHsUrl(): string { + private getFallbackHsUrl(): string | null { if (this.props.serverConfig?.isDefault) { - return this.props.config.fallback_hs_url; + return this.props.config.fallback_hs_url ?? null; } else { return null; } @@ -480,7 +482,7 @@ export default class MatrixChat extends React.PureComponent { private getServerProperties(): { serverConfig: ValidatedServerConfig } { let props = this.state.serverConfig; if (!props) props = this.props.serverConfig; // for unit tests - if (!props) props = SdkConfig.get("validated_server_config"); + if (!props) props = SdkConfig.get("validated_server_config")!; return { serverConfig: props }; } @@ -709,7 +711,7 @@ export default class MatrixChat extends React.PureComponent { Modal.createDialog( UserSettingsDialog, { initialTabId: tabPayload.initialTabId as UserTab }, - /*className=*/ null, + /*className=*/ undefined, /*isPriority=*/ false, /*isStatic=*/ true, ); @@ -978,7 +980,7 @@ export default class MatrixChat extends React.PureComponent { this.setState( { view: Views.LOGGED_IN, - currentRoomId: roomInfo.room_id || null, + currentRoomId: roomInfo.room_id ?? null, page_type: PageType.RoomView, threepidInvite: roomInfo.threepid_invite, roomOobData: roomInfo.oob_data, @@ -1063,7 +1065,7 @@ export default class MatrixChat extends React.PureComponent { const [shouldCreate, opts] = await modal.finished; if (shouldCreate) { - createRoom(opts); + createRoom(opts!); } } @@ -1122,7 +1124,7 @@ export default class MatrixChat extends React.PureComponent { // Show a warning if there are additional complications. const warnings: JSX.Element[] = []; - const memberCount = roomToLeave.currentState.getJoinedMemberCount(); + const memberCount = roomToLeave?.currentState.getJoinedMemberCount(); if (memberCount === 1) { warnings.push( @@ -1137,7 +1139,7 @@ export default class MatrixChat extends React.PureComponent { return warnings; } - const joinRules = roomToLeave.currentState.getStateEvents("m.room.join_rules", ""); + const joinRules = roomToLeave?.currentState.getStateEvents("m.room.join_rules", ""); if (joinRules) { const rule = joinRules.getContent().join_rule; if (rule !== "public") { @@ -1165,9 +1167,11 @@ export default class MatrixChat extends React.PureComponent { {isSpace ? _t("Are you sure you want to leave the space '%(spaceName)s'?", { - spaceName: roomToLeave.name, + spaceName: roomToLeave?.name ?? _t("Unnamed Space"), }) - : _t("Are you sure you want to leave the room '%(roomName)s'?", { roomName: roomToLeave.name })} + : _t("Are you sure you want to leave the room '%(roomName)s'?", { + roomName: roomToLeave?.name ?? _t("Unnamed Room"), + })} {warnings} ), @@ -1311,9 +1315,9 @@ export default class MatrixChat extends React.PureComponent { this.setStateForNewView({ view: Views.LOGGED_IN }); // If a specific screen is set to be shown after login, show that above // all else, as it probably means the user clicked on something already. - if (this.screenAfterLogin && this.screenAfterLogin.screen) { + if (this.screenAfterLogin?.screen) { this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params); - this.screenAfterLogin = null; + this.screenAfterLogin = undefined; } else if (MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); @@ -1403,7 +1407,7 @@ export default class MatrixChat extends React.PureComponent { // result in view_home_page, _user_settings or _room_directory if (this.screenAfterLogin && this.screenAfterLogin.screen) { this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params); - this.screenAfterLogin = null; + this.screenAfterLogin = undefined; } else if (localStorage && localStorage.getItem("mx_last_room_id")) { // Before defaulting to directory, show the last viewed room this.viewLastRoom(); @@ -1419,7 +1423,7 @@ export default class MatrixChat extends React.PureComponent { private viewLastRoom(): void { dis.dispatch({ action: Action.ViewRoom, - room_id: localStorage.getItem("mx_last_room_id"), + room_id: localStorage.getItem("mx_last_room_id") ?? undefined, metricsTrigger: undefined, // other }); } @@ -1486,12 +1490,12 @@ export default class MatrixChat extends React.PureComponent { return this.loggedInView.current.canResetTimelineInRoom(roomId); }); - cli.on(ClientEvent.Sync, (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => { + cli.on(ClientEvent.Sync, (state: SyncState, prevState: SyncState | null, data?: ISyncStateData) => { if (state === SyncState.Error || state === SyncState.Reconnecting) { - if (data.error instanceof InvalidStoreError) { + if (data?.error instanceof InvalidStoreError) { Lifecycle.handleInvalidStoreError(data.error); } - this.setState({ syncError: data.error }); + this.setState({ syncError: data?.error ?? null }); } else if (this.state.syncError) { this.setState({ syncError: null }); } @@ -1559,12 +1563,12 @@ export default class MatrixChat extends React.PureComponent { cancelButton: _t("Dismiss"), onFinished: (confirmed) => { if (confirmed) { - const wnd = window.open(consentUri, "_blank"); + const wnd = window.open(consentUri, "_blank")!; wnd.opener = null; } }, }, - null, + undefined, true, ); }); @@ -1655,7 +1659,7 @@ export default class MatrixChat extends React.PureComponent { { verifier: request.verifier, }, - null, + undefined, /* priority = */ false, /* static = */ true, ); @@ -1774,7 +1778,7 @@ export default class MatrixChat extends React.PureComponent { } const type = screen === "start_sso" ? "sso" : "cas"; - PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); + PlatformPeg.get()?.startSingleSignOn(cli, type, this.getFragmentAfterLogin()); } else if (screen.indexOf("room/") === 0) { // Rooms can have the following formats: // #room_alias:domain or !opaque_id:domain @@ -1786,7 +1790,7 @@ export default class MatrixChat extends React.PureComponent { eventOffset = domainOffset + room.substring(domainOffset).indexOf("/"); } const roomString = room.substring(0, eventOffset); - let eventId = room.substring(eventOffset + 1); // empty string if no event id given + let eventId: string | undefined = room.substring(eventOffset + 1); // empty string if no event id given // Previously we pulled the eventID from the segments in such a way // where if there was no eventId then we'd get undefined. However, we @@ -1797,9 +1801,9 @@ export default class MatrixChat extends React.PureComponent { // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 - let threepidInvite: IThreepidInvite; + let threepidInvite: IThreepidInvite | undefined; // if we landed here from a 3PID invite, persist it - if (params.signurl && params.email) { + if (params?.signurl && params?.email) { threepidInvite = ThreepidInviteStore.instance.storeInvite( roomString, params as IThreepidInviteWireFormat, @@ -1816,8 +1820,8 @@ export default class MatrixChat extends React.PureComponent { // to other levels. If there's just one ?via= then params.via is a // single string. If someone does something like ?via=one.com&via=two.com // then params.via is an array of strings. - let via = []; - if (params.via) { + let via: string[] = []; + if (params?.via) { if (typeof params.via === "string") via = [params.via]; else via = params.via; } @@ -1855,7 +1859,7 @@ export default class MatrixChat extends React.PureComponent { dis.dispatch({ action: "view_user_info", userId: userId, - subAction: params.action, + subAction: params?.action, }); } else { logger.info("Ignoring showScreen for '%s'", screen); @@ -1949,8 +1953,8 @@ export default class MatrixChat extends React.PureComponent { const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here if (PlatformPeg.get()) { - PlatformPeg.get().setErrorStatus(state === SyncState.Error); - PlatformPeg.get().setNotificationCount(numUnreadRooms); + PlatformPeg.get()!.setErrorStatus(state === SyncState.Error); + PlatformPeg.get()!.setNotificationCount(numUnreadRooms); } this.subTitleStatus = ""; @@ -1971,7 +1975,7 @@ export default class MatrixChat extends React.PureComponent { }; private makeRegistrationUrl = (params: QueryDict): string => { - if (this.props.startingFragmentQueryParams.referrer) { + if (this.props.startingFragmentQueryParams?.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; } return this.props.makeRegistrationUrl(params); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 434b3ddd6e..14224a081c 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -163,7 +163,7 @@ interface IProps { stickyBottom?: boolean; // className for the panel - className: string; + className?: string; // show twelve hour timestamps isTwelveHour?: boolean; @@ -177,7 +177,7 @@ interface IProps { // which layout to use layout?: Layout; - resizeNotifier: ResizeNotifier; + resizeNotifier?: ResizeNotifier; permalinkCreator?: RoomPermalinkCreator; editState?: EditorStateTransfer; @@ -345,12 +345,12 @@ export default class MessagePanel extends React.Component { }; /* get the DOM node representing the given event */ - public getNodeForEventId(eventId: string): HTMLElement { + public getNodeForEventId(eventId: string): HTMLElement | undefined { if (!this.eventTiles) { return undefined; } - return this.eventTiles[eventId]?.ref?.current; + return this.eventTiles[eventId]?.ref?.current ?? undefined; } public getTileForEventId(eventId?: string): UnwrappedEventTile | undefined { @@ -362,7 +362,7 @@ export default class MessagePanel extends React.Component { /* return true if the content is fully scrolled down right now; else false. */ - public isAtBottom(): boolean { + public isAtBottom(): boolean | undefined { return this.scrollPanel.current?.isAtBottom(); } @@ -371,7 +371,7 @@ export default class MessagePanel extends React.Component { * * returns null if we are not mounted. */ - public getScrollState(): IScrollState { + public getScrollState(): IScrollState | null { return this.scrollPanel.current?.getScrollState() ?? null; } @@ -381,7 +381,7 @@ export default class MessagePanel extends React.Component { // -1: read marker is above the window // 0: read marker is within the window // +1: read marker is below the window - public getReadMarkerPosition(): number { + public getReadMarkerPosition(): number | null { const readMarker = this.readMarkerNode.current; const messageWrapper = this.scrollPanel.current; @@ -633,9 +633,8 @@ export default class MessagePanel extends React.Component { break; } - const ret = []; - - let prevEvent = null; // the last event we showed + const ret: ReactNode[] = []; + let prevEvent: MatrixEvent | null = null; // the last event we showed // Note: the EventTile might still render a "sent/sending receipt" independent of // this information. When not providing read receipt information, the tile is likely @@ -645,7 +644,7 @@ export default class MessagePanel extends React.Component { this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); } - let grouper: BaseGrouper = null; + let grouper: BaseGrouper | null = null; for (let i = 0; i < events.length; i++) { const { event: mxEv, shouldShow } = events[i]; @@ -695,14 +694,14 @@ export default class MessagePanel extends React.Component { } public getTilesForEvent( - prevEvent: MatrixEvent, + prevEvent: MatrixEvent | null, mxEv: MatrixEvent, last = false, isGrouped = false, nextEvent?: MatrixEvent, nextEventWithTile?: MatrixEvent, ): ReactNode[] { - const ret = []; + const ret: ReactNode[] = []; const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. @@ -806,7 +805,7 @@ export default class MessagePanel extends React.Component { return ret; } - public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { + public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Date): boolean { if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { return false; } @@ -820,7 +819,7 @@ export default class MessagePanel extends React.Component { // Get a list of read receipts that should be shown next to this event // Receipts are objects which have a 'userId', 'roomMember' and 'ts'. - private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { + private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] | null { const myUserId = MatrixClientPeg.get().credentials.userId; // get list of read receipts, sorted most recent first @@ -939,7 +938,7 @@ export default class MessagePanel extends React.Component { private onTypingShown = (): void => { const scrollPanel = this.scrollPanel.current; // this will make the timeline grow, so checkScroll - scrollPanel.checkScroll(); + scrollPanel?.checkScroll(); if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { scrollPanel.preventShrinking(); } @@ -1018,7 +1017,7 @@ export default class MessagePanel extends React.Component { ); } - let ircResizer = null; + let ircResizer: JSX.Element | undefined; if (this.props.layout == Layout.IRC) { ircResizer = ( { if (e.getId() === panel.props.highlightedEventId) { highlightInSummary = true; diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index 569b184930..05fd42879b 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -149,7 +149,7 @@ interface IProps { */ export interface IScrollState { - stuckAtBottom: boolean; + stuckAtBottom?: boolean; trackedNode?: HTMLElement; trackedScrollToken?: string; bottomOffset?: number; @@ -173,7 +173,7 @@ export default class ScrollPanel extends React.Component { onScroll: function () {}, }; - private readonly pendingFillRequests: Record<"b" | "f", boolean> = { + private readonly pendingFillRequests: Record<"b" | "f", boolean | null> = { b: null, f: null, }; @@ -190,7 +190,7 @@ export default class ScrollPanel extends React.Component { private pendingFillDueToPropsUpdate: boolean; private scrollState: IScrollState; private preventShrinkingState: IPreventShrinkingState; - private unfillDebouncer: number; + private unfillDebouncer: number | null; private bottomGrowth: number; private minListHeight: number; private heightUpdateInProgress: boolean; diff --git a/src/components/structures/TabbedView.tsx b/src/components/structures/TabbedView.tsx index 542a6b0fc4..0d3d01041b 100644 --- a/src/components/structures/TabbedView.tsx +++ b/src/components/structures/TabbedView.tsx @@ -24,6 +24,7 @@ import { _t } from "../../languageHandler"; import AutoHideScrollbar from "./AutoHideScrollbar"; import AccessibleButton from "../views/elements/AccessibleButton"; import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers"; +import { NonEmptyArray } from "../../@types/common"; /** * Represents a tab for the TabbedView. @@ -40,7 +41,7 @@ export class Tab { public constructor( public readonly id: string, public readonly label: string, - public readonly icon: string, + public readonly icon: string | null, public readonly body: React.ReactNode, public readonly screenName?: ScreenName, ) {} @@ -52,7 +53,7 @@ export enum TabLocation { } interface IProps { - tabs: Tab[]; + tabs: NonEmptyArray; initialTabId?: string; tabLocation: TabLocation; onChange?: (tabId: string) => void; @@ -69,7 +70,7 @@ export default class TabbedView extends React.Component { const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); this.state = { - activeTabId: initialTabIdIsValid ? props.initialTabId : props.tabs[0]?.id, + activeTabId: initialTabIdIsValid ? props.initialTabId! : props.tabs[0].id, }; } @@ -101,7 +102,7 @@ export default class TabbedView extends React.Component { if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active"; - let tabIcon = null; + let tabIcon: JSX.Element | undefined; if (tab.icon) { tabIcon = ; } @@ -141,9 +142,11 @@ export default class TabbedView extends React.Component { mx_TabbedView_tabsOnTop: this.props.tabLocation == TabLocation.TOP, }); + const screenName = tab?.screenName ?? this.props.screenName; + return (
- + {screenName && }
{labels}
{panel}
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 8e6833954f..5ebab7c0a0 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -200,7 +200,7 @@ interface IState { forwardPaginating: boolean; // cache of matrixClient.getSyncState() (but from the 'sync' event) - clientSyncState: SyncState; + clientSyncState: SyncState | null; // should the event tiles have twelve hour times isTwelveHour: boolean; @@ -268,7 +268,7 @@ class TimelinePanel extends React.Component { // but for now we just do it per room for simplicity. let initialReadMarker: string | null = null; if (this.props.manageReadMarkers) { - const readmarker = this.props.timelineSet.room.getAccountData("m.fully_read"); + const readmarker = this.props.timelineSet.room?.getAccountData("m.fully_read"); if (readmarker) { initialReadMarker = readmarker.getContent().event_id; } else { @@ -414,7 +414,7 @@ class TimelinePanel extends React.Component { // Get the list of actually rendered events seen in the DOM. // This is useful to know for sure what's being shown on screen. // And we can suss out any corrupted React `key` problems. - let renderedEventIds: string[]; + let renderedEventIds: string[] | undefined; try { const messagePanel = this.messagePanel.current; if (messagePanel) { @@ -422,7 +422,7 @@ class TimelinePanel extends React.Component { if (messagePanelNode) { const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]"); renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => { - return renderedEvent.getAttribute("data-event-id"); + return renderedEvent.getAttribute("data-event-id")!; }); } } @@ -432,8 +432,8 @@ class TimelinePanel extends React.Component { // Get the list of events and threads for the room as seen by the // matrix-js-sdk. - let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[]; - let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[]; + let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[] | undefined; + let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[] | undefined; const serializedThreadsMap: { [key: string]: any } = {}; if (room) { const timelineSets = room.getTimelineSets(); @@ -469,15 +469,15 @@ class TimelinePanel extends React.Component { } } - let timelineWindowEventIds: string[]; + let timelineWindowEventIds: string[] | undefined; try { - timelineWindowEventIds = this.timelineWindow.getEvents().map((ev) => ev.getId()); + timelineWindowEventIds = this.timelineWindow?.getEvents().map((ev) => ev.getId()!); } catch (err) { logger.error(`onDumpDebugLogs: Failed to get event IDs from the timelineWindow`, err); } - let pendingEventIds: string[]; + let pendingEventIds: string[] | undefined; try { - pendingEventIds = this.props.timelineSet.getPendingEvents().map((ev) => ev.getId()); + pendingEventIds = this.props.timelineSet.getPendingEvents().map((ev) => ev.getId()!); } catch (err) { logger.error(`onDumpDebugLogs: Failed to get pending event IDs`, err); } @@ -491,10 +491,10 @@ class TimelinePanel extends React.Component { `\tserializedEventIdsFromThreadsTimelineSets=` + `${JSON.stringify(serializedEventIdsFromThreadsTimelineSets)}\n` + `\tserializedThreadsMap=${JSON.stringify(serializedThreadsMap)}\n` + - `\ttimelineWindowEventIds(${timelineWindowEventIds.length})=${JSON.stringify( + `\ttimelineWindowEventIds(${timelineWindowEventIds?.length})=${JSON.stringify( timelineWindowEventIds, )}\n` + - `\tpendingEventIds(${pendingEventIds.length})=${JSON.stringify(pendingEventIds)}`, + `\tpendingEventIds(${pendingEventIds?.length})=${JSON.stringify(pendingEventIds)}`, ); }; @@ -560,7 +560,7 @@ class TimelinePanel extends React.Component { return Promise.resolve(false); } - if (!this.timelineWindow.canPaginate(dir)) { + if (!this.timelineWindow?.canPaginate(dir)) { debuglog("can't", dir, "paginate any further"); this.setState({ [canPaginateKey]: false }); return Promise.resolve(false); @@ -576,7 +576,7 @@ class TimelinePanel extends React.Component { return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => { if (this.unmounted) { - return; + return false; } debuglog("paginate complete backwards:" + backwards + "; success:" + r); @@ -595,7 +595,7 @@ class TimelinePanel extends React.Component { // paginate in the other where we previously could not. const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const canPaginateOtherWayKey = backwards ? "canForwardPaginate" : "canBackPaginate"; - if (!this.state[canPaginateOtherWayKey] && this.timelineWindow.canPaginate(otherDirection)) { + if (!this.state[canPaginateOtherWayKey] && this.timelineWindow?.canPaginate(otherDirection)) { debuglog("can now", otherDirection, "paginate again"); newState[canPaginateOtherWayKey] = true; } @@ -666,7 +666,7 @@ class TimelinePanel extends React.Component { private onRoomTimeline = ( ev: MatrixEvent, - room: Room | null, + room: Room | undefined, toStartOfTimeline: boolean, removed: boolean, data: IRoomTimelineData, @@ -1008,7 +1008,7 @@ class TimelinePanel extends React.Component { if ( currentRREventId && currentRREventIndex === null && - this.timelineWindow.canPaginate(EventTimeline.FORWARDS) + this.timelineWindow?.canPaginate(EventTimeline.FORWARDS) ) { shouldSendRR = false; } @@ -1149,7 +1149,7 @@ class TimelinePanel extends React.Component { const events = this.timelineWindow.getEvents(); // first find where the current RM is - let i; + let i: number; for (i = 0; i < events.length; i++) { if (events[i].getId() == this.state.readMarkerEventId) { break; @@ -1182,7 +1182,7 @@ class TimelinePanel extends React.Component { // // Otherwise, reload the timeline rather than trying to paginate // through all of space-time. - if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { + if (this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) { this.loadTimeline(); } else { this.messagePanel.current?.scrollToBottom(); @@ -1231,7 +1231,7 @@ class TimelinePanel extends React.Component { // Look up the timestamp if we can find it const tl = this.props.timelineSet.getTimelineForEvent(rmId ?? ""); - let rmTs: number; + let rmTs: number | undefined; if (tl) { const event = tl.getEvents().find((e) => { return e.getId() == rmId; @@ -1264,7 +1264,7 @@ class TimelinePanel extends React.Component { * * returns null if we are not mounted. */ - public getScrollState = (): IScrollState => { + public getScrollState = (): IScrollState | null => { if (!this.messagePanel.current) { return null; } @@ -1277,7 +1277,7 @@ class TimelinePanel extends React.Component { // -1: read marker is above the window // 0: read marker is visible // +1: read marker is below the window - public getReadMarkerPosition = (): number => { + public getReadMarkerPosition = (): number | null => { if (!this.props.manageReadMarkers) return null; if (!this.messagePanel.current) return null; @@ -1449,7 +1449,7 @@ class TimelinePanel extends React.Component { this.setState({ timelineLoading: false }); logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error); - let onFinished: () => void; + let onFinished: (() => void) | undefined; // if we were given an event ID, then when the user closes the // dialog, let's jump to the end of the timeline. If we weren't, @@ -1745,7 +1745,7 @@ class TimelinePanel extends React.Component { const wrapperRect = messagePanelNode.getBoundingClientRect(); const myUserId = MatrixClientPeg.get().credentials.userId; - const isNodeInView = (node: HTMLElement): boolean => { + const isNodeInView = (node?: HTMLElement): boolean => { if (node) { const boundingRect = node.getBoundingClientRect(); if ( @@ -1828,7 +1828,7 @@ class TimelinePanel extends React.Component { return null; } - const myUserId = client.credentials.userId; + const myUserId = client.getSafeUserId(); const receiptStore: ReadReceipt = this.props.timelineSet.thread ?? this.props.timelineSet.room; return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized); } @@ -1943,7 +1943,7 @@ class TimelinePanel extends React.Component { canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0} showUrlPreview={this.props.showUrlPreview} showReadReceipts={this.props.showReadReceipts} - ourUserId={MatrixClientPeg.get().credentials.userId} + ourUserId={MatrixClientPeg.get().getSafeUserId()} stickyBottom={stickyBottom} onScroll={this.onMessageListScroll} onFillRequest={this.onMessageListFillRequest} diff --git a/src/components/structures/UserMenu.tsx b/src/components/structures/UserMenu.tsx index 50520cff2a..0780fc408d 100644 --- a/src/components/structures/UserMenu.tsx +++ b/src/components/structures/UserMenu.tsx @@ -145,7 +145,7 @@ export default class UserMenu extends React.Component { } else { const theme = SettingsStore.getValue("theme"); if (theme.startsWith("custom-")) { - return getCustomTheme(theme.substring("custom-".length)).is_dark; + return !!getCustomTheme(theme.substring("custom-".length)).is_dark; } return theme === "dark"; } @@ -236,7 +236,7 @@ export default class UserMenu extends React.Component { SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab }; - private onSettingsOpen = (ev: ButtonEvent, tabId: string): void => { + private onSettingsOpen = (ev: ButtonEvent, tabId?: string): void => { ev.preventDefault(); ev.stopPropagation(); @@ -319,7 +319,7 @@ export default class UserMenu extends React.Component { ); } - let homeButton = null; + let homeButton: JSX.Element | undefined; if (this.hasHomePage) { homeButton = ( { ); } - let feedbackButton; + let feedbackButton: JSX.Element | undefined; if (SettingsStore.getValue(UIFeature.Feedback)) { feedbackButton = ( { this.onSettingsOpen(e, null)} + onClick={(e) => this.onSettingsOpen(e)} /> {feedbackButton} { this.onSettingsOpen(e, null)} + onClick={(e) => this.onSettingsOpen(e)} /> {feedbackButton} @@ -395,9 +395,12 @@ export default class UserMenu extends React.Component { {OwnProfileStore.instance.displayName}
- {UserIdentifierCustomisations.getDisplayUserIdentifier(MatrixClientPeg.get().getUserId(), { - withDisplayName: true, - })} + {UserIdentifierCustomisations.getDisplayUserIdentifier( + MatrixClientPeg.get().getSafeUserId(), + { + withDisplayName: true, + }, + )} @@ -426,7 +429,7 @@ export default class UserMenu extends React.Component { const displayName = OwnProfileStore.instance.displayName || userId; const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); - let name: JSX.Element; + let name: JSX.Element | undefined; if (!this.props.isPanelCollapsed) { name =
{displayName}
; } diff --git a/src/components/structures/ViewSource.tsx b/src/components/structures/ViewSource.tsx index f804eb2ce8..bf1115c00f 100644 --- a/src/components/structures/ViewSource.tsx +++ b/src/components/structures/ViewSource.tsx @@ -68,7 +68,7 @@ export default class ViewSource extends React.Component { }; if (isEncrypted) { const copyDecryptedFunc = (): string => { - return stringify(decryptedEventSource); + return stringify(decryptedEventSource || {}); }; return ( <> @@ -117,7 +117,7 @@ export default class ViewSource extends React.Component { return ( {(cli) => ( - + )} @@ -128,7 +128,7 @@ export default class ViewSource extends React.Component { return ( {(cli) => ( - + )} diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx index b2a2ff0ef2..c4c0834987 100644 --- a/src/components/structures/auth/ForgotPassword.tsx +++ b/src/components/structures/auth/ForgotPassword.tsx @@ -395,7 +395,7 @@ export default class ForgotPassword extends React.Component { button: _t("Continue"), }); const [confirmed] = await finished; - return confirmed; + return !!confirmed; } public renderCheckEmail(): JSX.Element { diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx index 97f3866f96..3be905878d 100644 --- a/src/components/structures/auth/Login.tsx +++ b/src/components/structures/auth/Login.tsx @@ -368,7 +368,7 @@ export default class LoginComponent extends React.PureComponent isDefaultServer = true; } - const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl : null; + const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -514,7 +514,7 @@ export default class LoginComponent extends React.PureComponent // this is the ideal order we want to show the flows in const order = ["m.login.password", "m.login.sso"]; - const flows = filterBoolean(order.map((type) => this.state.flows.find((flow) => flow.type === type))); + const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type))); return ( {flows.map((flow) => { @@ -546,7 +546,7 @@ export default class LoginComponent extends React.PureComponent }; private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => { - const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; + const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; return ( flow={flow} loginType={loginType} fragmentAfterLogin={this.props.fragmentAfterLogin} - primary={!this.state.flows.find((flow) => flow.type === "m.login.password")} + primary={!this.state.flows?.find((flow) => flow.type === "m.login.password")} action={SSOAction.LOGIN} /> ); diff --git a/src/components/structures/auth/SetupEncryptionBody.tsx b/src/components/structures/auth/SetupEncryptionBody.tsx index fe8a24a619..9a28d20f75 100644 --- a/src/components/structures/auth/SetupEncryptionBody.tsx +++ b/src/components/structures/auth/SetupEncryptionBody.tsx @@ -88,7 +88,7 @@ export default class SetupEncryptionBody extends React.Component private onVerifyClick = (): void => { const cli = MatrixClientPeg.get(); - const userId = cli.getUserId(); + const userId = cli.getSafeUserId(); const requestPromise = cli.requestVerification(userId); // We need to call onFinished now to close this dialog, and @@ -212,7 +212,7 @@ export default class SetupEncryptionBody extends React.Component {useRecoveryKeyButton}
- {_t("Forgotten or lost all recovery methods? Reset all", null, { + {_t("Forgotten or lost all recovery methods? Reset all", undefined, { a: (sub) => ( ); } } else if (phase === Phase.Done) { - let message; + let message: JSX.Element; if (this.state.backupInfo) { message = (

diff --git a/src/components/structures/auth/SoftLogout.tsx b/src/components/structures/auth/SoftLogout.tsx index 46e8568697..3eb997c843 100644 --- a/src/components/structures/auth/SoftLogout.tsx +++ b/src/components/structures/auth/SoftLogout.tsx @@ -23,7 +23,7 @@ import { _t } from "../../../languageHandler"; import dis from "../../../dispatcher/dispatcher"; import * as Lifecycle from "../../../Lifecycle"; import Modal from "../../../Modal"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg"; import { sendLoginRequest } from "../../../Login"; import AuthPage from "../../views/auth/AuthPage"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; @@ -159,7 +159,7 @@ export default class SoftLogout extends React.Component { device_id: MatrixClientPeg.get().getDeviceId(), }; - let credentials = null; + let credentials: IMatrixClientCreds; try { credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); } catch (e) { @@ -192,7 +192,7 @@ export default class SoftLogout extends React.Component { device_id: MatrixClientPeg.get().getDeviceId(), }; - let credentials = null; + let credentials: IMatrixClientCreds; try { credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); } catch (e) { @@ -212,7 +212,7 @@ export default class SoftLogout extends React.Component { } private renderPasswordForm(introText: Optional): JSX.Element { - let error: JSX.Element = null; + let error: JSX.Element | undefined; if (this.state.errorText) { error = {this.state.errorText}; } @@ -267,7 +267,7 @@ export default class SoftLogout extends React.Component { return ; } - let introText = null; // null is translated to something area specific in this function + let introText: string | null = null; // null is translated to something area specific in this function if (this.state.keyBackupNeeded) { introText = _t( "Regain access to your account and recover encryption keys stored in this session. " + diff --git a/src/components/structures/auth/header/AuthHeaderDisplay.tsx b/src/components/structures/auth/header/AuthHeaderDisplay.tsx index 75c0feb82c..121859498d 100644 --- a/src/components/structures/auth/header/AuthHeaderDisplay.tsx +++ b/src/components/structures/auth/header/AuthHeaderDisplay.tsx @@ -27,7 +27,7 @@ interface Props { export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren): JSX.Element { const context = useContext(AuthHeaderContext); if (!context) { - return null; + return <>; } const current = context.state.length ? context.state[0] : null; return ( diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index 03914b329e..3b335b4dcd 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -61,7 +61,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { RoomListStore.instance.getTagsForRoom(room), ); - let leaveOption: JSX.Element; + let leaveOption: JSX.Element | undefined; if (roomTags.includes(DefaultTagID.Archived)) { const onForgetRoomClick = (ev: ButtonEvent): void => { ev.preventDefault(); @@ -112,7 +112,7 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { const isVideoRoom = videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())); - let inviteOption: JSX.Element; + let inviteOption: JSX.Element | undefined; if (room.canInvite(cli.getUserId()!) && !isDm) { const onInviteClick = (ev: ButtonEvent): void => { ev.preventDefault(); @@ -136,9 +136,9 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { ); } - let favouriteOption: JSX.Element; - let lowPriorityOption: JSX.Element; - let notificationOption: JSX.Element; + let favouriteOption: JSX.Element | undefined; + let lowPriorityOption: JSX.Element | undefined; + let notificationOption: JSX.Element | undefined; if (room.getMyMembership() === "join") { const isFavorite = roomTags.includes(DefaultTagID.Favourite); favouriteOption = ( @@ -208,8 +208,8 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { ); } - let peopleOption: JSX.Element; - let copyLinkOption: JSX.Element; + let peopleOption: JSX.Element | undefined; + let copyLinkOption: JSX.Element | undefined; if (!isDm) { peopleOption = ( = ({ room, onFinished, ...props }) => { ); } - let filesOption: JSX.Element; + let filesOption: JSX.Element | undefined; if (!isVideoRoom) { filesOption = ( = ({ room, onFinished, ...props }) => { } const pinningEnabled = useFeatureEnabled("feature_pinning"); - const pinCount = usePinnedEvents(pinningEnabled && room)?.length; + const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length; - let pinsOption: JSX.Element; + let pinsOption: JSX.Element | undefined; if (pinningEnabled && !isVideoRoom) { pinsOption = ( = ({ room, onFinished, ...props }) => { ); } - let widgetsOption: JSX.Element; + let widgetsOption: JSX.Element | undefined; if (!isVideoRoom) { widgetsOption = ( = ({ room, onFinished, ...props }) => { ); } - let exportChatOption: JSX.Element; + let exportChatOption: JSX.Element | undefined; if (!isVideoRoom) { exportChatOption = ( = ({ room, checked, onChange }) => { return (

diff --git a/src/components/views/elements/Slider.tsx b/src/components/views/elements/Slider.tsx index f6535679be..c8abeab620 100644 --- a/src/components/views/elements/Slider.tsx +++ b/src/components/views/elements/Slider.tsx @@ -81,7 +81,7 @@ export default class Slider extends React.Component { /> )); - let selection = null; + let selection: JSX.Element | undefined; if (!this.props.disabled) { const offset = this.offset(this.props.values, this.props.value); diff --git a/src/components/views/elements/TruncatedList.tsx b/src/components/views/elements/TruncatedList.tsx index c85a8658a6..01ad735a42 100644 --- a/src/components/views/elements/TruncatedList.tsx +++ b/src/components/views/elements/TruncatedList.tsx @@ -21,7 +21,7 @@ import { _t } from "../../../languageHandler"; interface IProps { // The number of elements to show before truncating. If negative, no truncation is done. - truncateAt?: number; + truncateAt: number; // The className to apply to the wrapping div className?: string; // A function that returns the children to be rendered into the element. @@ -34,7 +34,7 @@ interface IProps { getChildCount?: () => number; // A function which will be invoked when an overflow element is required. // This will be inserted after the children. - createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode; + createOverflowElement: (overflowCount: number, totalCount: number) => React.ReactNode; children?: ReactNode; } @@ -71,8 +71,8 @@ export default class TruncatedList extends React.Component { } } - public render(): React.ReactNode { - let overflowNode = null; + public render(): ReactNode { + let overflowNode: ReactNode | undefined; const totalChildren = this.getChildCount(); let upperBound = totalChildren; diff --git a/src/components/views/right_panel/PinnedMessagesCard.tsx b/src/components/views/right_panel/PinnedMessagesCard.tsx index f393ddd9a7..736ff03f2b 100644 --- a/src/components/views/right_panel/PinnedMessagesCard.tsx +++ b/src/components/views/right_panel/PinnedMessagesCard.tsx @@ -37,6 +37,7 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex import { ReadPinsEventId } from "./types"; import Heading from "../typography/Heading"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; +import { filterBoolean } from "../../../utils/arrays"; interface IProps { room: Room; @@ -44,7 +45,7 @@ interface IProps { onClose(): void; } -export const usePinnedEvents = (room: Room): string[] => { +export const usePinnedEvents = (room?: Room): string[] => { const [pinnedEvents, setPinnedEvents] = useState([]); const update = useCallback( @@ -173,8 +174,7 @@ const PinnedMessagesCard: React.FC = ({ room, onClose, permalinkCreator }; // show them in reverse, with latest pinned at the top - content = pinnedEvents - .filter(Boolean) + content = filterBoolean(pinnedEvents) .reverse() .map((ev) => ( = ({ room, permalinkCreator, onClose }) const memberCount = useRoomMemberCount(room); const pinningEnabled = useFeatureEnabled("feature_pinning"); - const pinCount = usePinnedEvents(pinningEnabled && room)?.length; + const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length; const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history"); diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index dd4960339c..b93f87abc5 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -44,7 +44,7 @@ const crossSigningRoomTitles: { [key in E2EState]?: string } = { interface IProps { isUser?: boolean; - status?: E2EState | E2EStatus; + status: E2EState | E2EStatus; className?: string; size?: number; onClick?: () => void; @@ -76,7 +76,7 @@ const E2EIcon: React.FC = ({ className, ); - let e2eTitle; + let e2eTitle: string | undefined; if (isUser) { e2eTitle = crossSigningUserTitles[status]; } else { diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx index 065bc04008..14d4bbc45a 100644 --- a/src/components/views/rooms/EntityTile.tsx +++ b/src/components/views/rooms/EntityTile.tsx @@ -43,7 +43,7 @@ const PRESENCE_CLASS: Record = { unavailable: "mx_EntityTile_unavailable", }; -function presenceClassForMember(presenceState: PresenceState, lastActiveAgo: number, showPresence: boolean): string { +function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string { if (showPresence === false) { return "mx_EntityTile_online_beenactive"; } @@ -74,7 +74,7 @@ interface IProps { presenceLastTs?: number; presenceCurrentlyActive?: boolean; showInviteButton?: boolean; - onClick?(): void; + onClick(): void; suppressOnHover?: boolean; showPresence?: boolean; subtextLabel?: string; @@ -108,7 +108,7 @@ export default class EntityTile extends React.PureComponent { public render(): React.ReactNode { const mainClassNames: Record = { mx_EntityTile: true, - mx_EntityTile_noHover: this.props.suppressOnHover, + mx_EntityTile_noHover: !!this.props.suppressOnHover, }; if (this.props.className) mainClassNames[this.props.className] = true; @@ -127,7 +127,7 @@ export default class EntityTile extends React.PureComponent { ? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo) : -1; - let presenceLabel = null; + let presenceLabel: JSX.Element | undefined; if (this.props.showPresence) { presenceLabel = ( { // Return duration as a string using appropriate time units // XXX: This would be better handled using a culture-aware library, but we don't use one yet. - private getDuration(time: number): string { + private getDuration(time: number): string | undefined { if (!time) return; const t = Math.round(time / 1000); const s = t % 60; @@ -61,11 +61,11 @@ export default class PresenceLabel extends React.Component { return _t("%(duration)sd", { duration: d }); } - private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string { + private getPrettyPresence(presence?: string, activeAgo?: number, currentlyActive?: boolean): string { // for busy presence, we ignore the 'currentlyActive' flag: they're busy whether // they're active or not. It can be set while the user is active in which case // the 'active ago' ends up being 0. - if (BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy"); + if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy"); if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) { const duration = this.getDuration(activeAgo); diff --git a/src/components/views/settings/account/PhoneNumbers.tsx b/src/components/views/settings/account/PhoneNumbers.tsx index f2233c5e6b..305a198bcc 100644 --- a/src/components/views/settings/account/PhoneNumbers.tsx +++ b/src/components/views/settings/account/PhoneNumbers.tsx @@ -129,9 +129,9 @@ interface IProps { interface IState { verifying: boolean; - verifyError: string; + verifyError: string | null; verifyMsisdn: string; - addTask: AddThreepid; + addTask: AddThreepid | null; continueDisabled: boolean; phoneCountry: string; newPhoneNumber: string; @@ -205,7 +205,7 @@ export default class PhoneNumbers extends React.Component { const token = this.state.newPhoneNumberCode; const address = this.state.verifyMsisdn; this.state.addTask - .haveMsisdnToken(token) + ?.haveMsisdnToken(token) .then(([finished]) => { let newPhoneNumber = this.state.newPhoneNumber; if (finished) { diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index db39176257..6e455e5131 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -21,6 +21,7 @@ import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { logger } from "matrix-js-sdk/src/logger"; import { throttle, get } from "lodash"; import { compare } from "matrix-js-sdk/src/utils"; +import { IContent } from "matrix-js-sdk/src/models/event"; import { _t, _td } from "../../../../../languageHandler"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; @@ -171,8 +172,8 @@ export default class RolesRoomSettingsTab extends React.Component { private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); - const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - let plContent = plEvent ? plEvent.getContent() || {} : {}; + const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); + let plContent = plEvent?.getContent() ?? {}; // Clone the power levels just in case plContent = Object.assign({}, plContent); @@ -185,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component { plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value; } else { const keyPath = powerLevelKey.split("."); - let parentObj; + let parentObj: IContent | undefined; let currentObj = plContent; for (const key of keyPath) { if (!currentObj[key]) { @@ -213,8 +214,8 @@ export default class RolesRoomSettingsTab extends React.Component { private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => { const client = MatrixClientPeg.get(); const room = client.getRoom(this.props.roomId); - const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); - let plContent = plEvent ? plEvent.getContent() || {} : {}; + const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, ""); + let plContent = plEvent?.getContent() ?? {}; // Clone the power levels just in case plContent = Object.assign({}, plContent); diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 849917bb8b..35668b4812 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -113,7 +113,7 @@ export default class WidgetStore extends AsyncStoreWithClient { }); } - private loadRoomWidgets(room: Room): void { + private loadRoomWidgets(room: Room | null): void { if (!room) return; const roomInfo = this.roomMap.get(room.roomId) || {}; roomInfo.widgets = []; diff --git a/src/utils/localRoom/isLocalRoom.ts b/src/utils/localRoom/isLocalRoom.ts index f2d7e3acfd..dffebd9777 100644 --- a/src/utils/localRoom/isLocalRoom.ts +++ b/src/utils/localRoom/isLocalRoom.ts @@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/matrix"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom"; -export function isLocalRoom(roomOrID: Room | string): boolean { +export function isLocalRoom(roomOrID?: Room | string): boolean { if (typeof roomOrID === "string") { return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX); } diff --git a/test/components/structures/TabbedView-test.tsx b/test/components/structures/TabbedView-test.tsx index 520d406e48..922c7ccc3f 100644 --- a/test/components/structures/TabbedView-test.tsx +++ b/test/components/structures/TabbedView-test.tsx @@ -18,6 +18,7 @@ import React from "react"; import { act, fireEvent, render } from "@testing-library/react"; import TabbedView, { Tab, TabLocation } from "../../../src/components/structures/TabbedView"; +import { NonEmptyArray } from "../../../src/@types/common"; describe("", () => { const generalTab = new Tab("GENERAL", "General", "general",
general
); @@ -25,7 +26,7 @@ describe("", () => { const securityTab = new Tab("SECURITY", "Security", "security",
security
); const defaultProps = { tabLocation: TabLocation.LEFT, - tabs: [generalTab, labsTab, securityTab], + tabs: [generalTab, labsTab, securityTab] as NonEmptyArray, }; const getComponent = (props = {}): React.ReactElement => ; @@ -58,11 +59,6 @@ describe("", () => { expect(getActiveTabBody(container)?.textContent).toEqual("security"); }); - it("renders without error when there are no tabs", () => { - const { container } = render(getComponent({ tabs: [] })); - expect(container).toMatchSnapshot(); - }); - it("sets active tab on tab click", () => { const { container, getByTestId } = render(getComponent()); diff --git a/test/components/structures/__snapshots__/TabbedView-test.tsx.snap b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap index bee195c1a0..77ead236a3 100644 --- a/test/components/structures/__snapshots__/TabbedView-test.tsx.snap +++ b/test/components/structures/__snapshots__/TabbedView-test.tsx.snap @@ -69,15 +69,3 @@ exports[` renders tabs 1`] = `
`; - -exports[` renders without error when there are no tabs 1`] = ` -
-
-
-
-
-`; diff --git a/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap b/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap index a9c8d7f3e1..0877123925 100644 --- a/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap +++ b/test/components/views/dialogs/polls/__snapshots__/PollHistoryDialog-test.tsx.snap @@ -38,7 +38,7 @@ exports[` renders a list of active polls when there are pol />