diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 5121fdb51a..fdf0fc65f1 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -293,7 +293,7 @@ export default class AddThreepid { const authClient = new IdentityAuthClient(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); - let result; + let result: { success: boolean } | MatrixError; if (this.submitUrl) { result = await MatrixClientPeg.get().submitMsisdnTokenOtherUrl( this.submitUrl, @@ -311,7 +311,7 @@ export default class AddThreepid { } else { throw new UserFriendlyError("The add / bind with MSISDN flow is misconfigured"); } - if (result.errcode) { + if (result instanceof Error) { throw result; } diff --git a/src/DateUtils.ts b/src/DateUtils.ts index 0e39205916..e743b3feea 100644 --- a/src/DateUtils.ts +++ b/src/DateUtils.ts @@ -16,6 +16,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { Optional } from "matrix-events-sdk"; + import { _t } from "./languageHandler"; function getDaysArray(): string[] { @@ -194,10 +196,7 @@ function withinCurrentYear(prevDate: Date, nextDate: Date): boolean { return prevDate.getFullYear() === nextDate.getFullYear(); } -export function wantsDateSeparator( - prevEventDate: Date | null | undefined, - nextEventDate: Date | null | undefined, -): boolean { +export function wantsDateSeparator(prevEventDate: Optional, nextEventDate: Optional): boolean { if (!nextEventDate || !prevEventDate) { return false; } diff --git a/src/LegacyCallHandler.tsx b/src/LegacyCallHandler.tsx index e6a4388e65..f84509238b 100644 --- a/src/LegacyCallHandler.tsx +++ b/src/LegacyCallHandler.tsx @@ -288,8 +288,8 @@ export default class LegacyCallHandler extends EventEmitter { this.play(AudioID.Ring); } - public isCallSilenced(callId: string): boolean { - return this.isForcedSilent() || this.silencedCalls.has(callId); + public isCallSilenced(callId?: string): boolean { + return this.isForcedSilent() || (!!callId && this.silencedCalls.has(callId)); } /** @@ -395,6 +395,7 @@ export default class LegacyCallHandler extends EventEmitter { } const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call); + if (!mappedRoomId) return; if (this.getCallForRoom(mappedRoomId)) { logger.log( "Got incoming call for room " + mappedRoomId + " but there's already a call for this room: ignoring", @@ -411,7 +412,8 @@ export default class LegacyCallHandler extends EventEmitter { // the call, we'll be ready to send. NB. This is the protocol-level room ID not // the mapped one: that's where we'll send the events. const cli = MatrixClientPeg.get(); - cli.prepareToEncrypt(cli.getRoom(call.roomId)); + const room = cli.getRoom(call.roomId); + if (room) cli.prepareToEncrypt(room); }; public getCallById(callId: string): MatrixCall | null { @@ -505,7 +507,7 @@ export default class LegacyCallHandler extends EventEmitter { if (this.audioPromises.has(audioId)) { this.audioPromises.set( audioId, - this.audioPromises.get(audioId).then(() => { + this.audioPromises.get(audioId)!.then(() => { audio.load(); return playAudio(); }), @@ -531,7 +533,7 @@ export default class LegacyCallHandler extends EventEmitter { }; if (audio) { if (this.audioPromises.has(audioId)) { - this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(pauseAudio)); + this.audioPromises.set(audioId, this.audioPromises.get(audioId)!.then(pauseAudio)); } else { pauseAudio(); } @@ -546,7 +548,7 @@ export default class LegacyCallHandler extends EventEmitter { // is the call we consider 'the' call for its room. const mappedRoomId = this.roomIdForCall(call); - const callForThisRoom = this.getCallForRoom(mappedRoomId); + const callForThisRoom = mappedRoomId ? this.getCallForRoom(mappedRoomId) : null; return !!callForThisRoom && call.callId === callForThisRoom.callId; } @@ -840,7 +842,7 @@ export default class LegacyCallHandler extends EventEmitter { cancelButton: _t("OK"), onFinished: (allow) => { SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow); - cli.setFallbackICEServerAllowed(allow); + cli.setFallbackICEServerAllowed(!!allow); }, }, undefined, @@ -898,7 +900,7 @@ export default class LegacyCallHandler extends EventEmitter { // previous calls that are probably stale by now, so just cancel them. if (mappedRoomId !== roomId) { const mappedRoom = MatrixClientPeg.get().getRoom(mappedRoomId); - if (mappedRoom.getPendingEvents().length > 0) { + if (mappedRoom?.getPendingEvents().length) { Resend.cancelUnsentEvents(mappedRoom); } } @@ -933,7 +935,7 @@ export default class LegacyCallHandler extends EventEmitter { } } - public async placeCall(roomId: string, type?: CallType, transferee?: MatrixCall): Promise { + public async placeCall(roomId: string, type: CallType, transferee?: MatrixCall): Promise { // Pause current broadcast, if any SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause(); diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx index a6cce06c35..ba5a68d333 100644 --- a/src/components/structures/FilePanel.tsx +++ b/src/components/structures/FilePanel.tsx @@ -273,7 +273,9 @@ class FilePanel extends React.Component { withoutScrollContainer ref={this.card} > - + {this.card.current && ( + + )} { } private doStickyHeaders(list: HTMLDivElement): void { + if (!list.parentElement) return; const topEdge = list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist:not(.mx_RoomSublist_hidden)"); diff --git a/src/components/structures/LegacyCallEventGrouper.ts b/src/components/structures/LegacyCallEventGrouper.ts index 2d51b4ee18..d28224729a 100644 --- a/src/components/structures/LegacyCallEventGrouper.ts +++ b/src/components/structures/LegacyCallEventGrouper.ts @@ -123,8 +123,8 @@ export default class LegacyCallEventGrouper extends EventEmitter { } public get duration(): number | null { - if (!this.hangup || !this.selectAnswer) return null; - return this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime(); + if (!this.hangup?.getDate() || !this.selectAnswer?.getDate()) return null; + return this.hangup.getDate()!.getTime() - this.selectAnswer.getDate()!.getTime(); } /** diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a503895058..07590446a3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -169,7 +169,7 @@ interface IProps { // the initial queryParams extracted from the hash-fragment of the URI startingFragmentQueryParams?: QueryDict; // called when we have completed a token login - onTokenLoginCompleted?: () => void; + onTokenLoginCompleted: () => void; // Represents the screen to display as a result of parsing the initial window.location initialScreenAfterLogin?: IScreen; // displayname, if any, to set on the device when logging in/registering. @@ -1200,7 +1200,7 @@ export default class MatrixChat extends React.PureComponent { // We have to manually update the room list because the forgotten room will not // be notified to us, therefore the room list will have no other way of knowing // the room is forgotten. - RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); + if (room) RoomListStore.instance.manualRoomUpdate(room, RoomUpdateCause.RoomRemoved); }) .catch((err) => { const errCode = err.errcode || _td("unknown error code"); @@ -2124,7 +2124,7 @@ export default class MatrixChat extends React.PureComponent { onForgotPasswordClick={showPasswordReset ? this.onForgotPasswordClick : undefined} onServerConfigChange={this.onServerConfigChange} fragmentAfterLogin={fragmentAfterLogin} - defaultUsername={this.props.startingFragmentQueryParams.defaultUsername as string} + defaultUsername={this.props.startingFragmentQueryParams?.defaultUsername as string | undefined} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index ab01fad939..ed6f778bd5 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -24,6 +24,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state"; import { M_BEACON_INFO } from "matrix-js-sdk/src/@types/beacon"; import { isSupportedReceiptType } from "matrix-js-sdk/src/utils"; +import { Optional } from "matrix-events-sdk"; import shouldHideEvent from "../../shouldHideEvent"; import { wantsDateSeparator } from "../../DateUtils"; @@ -436,10 +437,8 @@ export default class MessagePanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - public scrollToEvent(eventId: string, pixelOffset: number, offsetBase: number): void { - if (this.scrollPanel.current) { - this.scrollPanel.current.scrollToToken(eventId, pixelOffset, offsetBase); - } + public scrollToEvent(eventId: string, pixelOffset?: number, offsetBase?: number): void { + this.scrollPanel.current?.scrollToToken(eventId, pixelOffset, offsetBase); } public scrollToEventIfNeeded(eventId: string): void { @@ -590,16 +589,16 @@ export default class MessagePanel extends React.Component { return { nextEventAndShouldShow, nextTile }; } - private get pendingEditItem(): string | undefined { + private get pendingEditItem(): string | null { if (!this.props.room) { - return undefined; + return null; } try { return localStorage.getItem(editorRoomKey(this.props.room.roomId, this.context.timelineRenderingType)); } catch (err) { logger.error(err); - return undefined; + return null; } } @@ -815,7 +814,7 @@ export default class MessagePanel extends React.Component { return ret; } - public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Date): boolean { + public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Optional): boolean { if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { return false; } @@ -1174,7 +1173,7 @@ class CreationGrouper extends BaseGrouper { const ts = createEvent.event.getTs(); ret.push(
  • - +
  • , ); } @@ -1326,7 +1325,7 @@ class MainGrouper extends BaseGrouper { const ts = this.events[0].getTs(); ret.push(
  • - +
  • , ); } diff --git a/src/components/structures/PipContainer.tsx b/src/components/structures/PipContainer.tsx index 5c2d6ef2c6..8ef9b1b816 100644 --- a/src/components/structures/PipContainer.tsx +++ b/src/components/structures/PipContainer.tsx @@ -71,8 +71,8 @@ interface IState { secondaryCall: MatrixCall; // widget candidate to be displayed in the pip view. - persistentWidgetId: string; - persistentRoomId: string; + persistentWidgetId: string | null; + persistentRoomId: string | null; showWidgetInPip: boolean; } @@ -225,7 +225,7 @@ class PipContainerInner extends React.Component { if (callRoomId ?? this.state.persistentRoomId) { dis.dispatch({ action: Action.ViewRoom, - room_id: callRoomId ?? this.state.persistentRoomId, + room_id: callRoomId ?? this.state.persistentRoomId ?? undefined, metricsTrigger: "WebFloatingCallWindow", }); } @@ -318,7 +318,7 @@ class PipContainerInner extends React.Component { pipContent.push(({ onStartMoving }) => ( { } break; case RightPanelPhases.Timeline: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.FilePanel: card = ; break; case RightPanelPhases.ThreadView: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.ThreadPanel: @@ -262,18 +266,22 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.RoomSummary: - card = ( - - ); + if (this.props.room) { + card = ( + + ); + } break; case RightPanelPhases.Widget: - card = ; + if (this.props.room) { + card = ; + } break; } diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 1eaa2cf16e..4e14733d04 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -654,7 +654,7 @@ export class RoomView extends React.Component { // and the root event. // The rest will be lost for now, until the aggregation API on the server // becomes available to fetch a whole thread - if (!initialEvent && this.context.client) { + if (!initialEvent && this.context.client && roomId) { initialEvent = (await fetchInitialEvent(this.context.client, roomId, initialEventId)) ?? undefined; } @@ -741,7 +741,7 @@ export class RoomView extends React.Component { // If an event ID wasn't specified, default to the one saved for this room // in the scroll state store. Assume initialEventPixelOffset should be set. - if (!newState.initialEventId) { + if (!newState.initialEventId && newState.roomId) { const roomScrollState = RoomScrollStateStore.getScrollState(newState.roomId); if (roomScrollState) { newState.initialEventId = roomScrollState.focussedEvent; @@ -770,7 +770,7 @@ export class RoomView extends React.Component { // callback because this would prevent the setStates from being batched, // ie. cause it to render RoomView twice rather than the once that is necessary. if (initial) { - this.setupRoom(newState.room, newState.roomId, newState.joining, newState.shouldPeek); + this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); } }; @@ -794,13 +794,13 @@ export class RoomView extends React.Component { this.setState({ activeCall }); }; - private getRoomId = (): string => { + private getRoomId = (): string | undefined => { // According to `onRoomViewStoreUpdate`, `state.roomId` can be null // if we have a room alias we haven't resolved yet. To work around this, // first we'll try the room object if it's there, and then fallback to // the bare room ID. (We may want to update `state.roomId` after // resolving aliases, so we could always trust it.) - return this.state.room ? this.state.room.roomId : this.state.roomId; + return this.state.room?.roomId ?? this.state.roomId; }; private getPermalinkCreatorForRoom(room: Room): RoomPermalinkCreator { @@ -1008,7 +1008,7 @@ export class RoomView extends React.Component { SettingsStore.unwatchSetting(watcher); } - if (this.viewsLocalRoom) { + if (this.viewsLocalRoom && this.state.room) { // clean up if this was a local room this.context.client?.store.removeRoom(this.state.room.roomId); } @@ -1188,16 +1188,16 @@ export class RoomView extends React.Component { }; private onLocalRoomEvent(roomId: string): void { - if (roomId !== this.state.room.roomId) return; + if (!this.context.client || !this.state.room || roomId !== this.state.room.roomId) return; createRoomFromLocalRoom(this.context.client, this.state.room as LocalRoom); } private onRoomTimeline = ( ev: MatrixEvent, room: Room | undefined, - toStartOfTimeline: boolean, + toStartOfTimeline: boolean | undefined, removed: boolean, - data?: IRoomTimelineData, + data: IRoomTimelineData, ): void => { if (this.unmounted) return; @@ -1249,6 +1249,7 @@ export class RoomView extends React.Component { }; private handleEffects = (ev: MatrixEvent): void => { + if (!this.state.room) return; const notifState = this.context.roomNotificationStateStore.getRoomState(this.state.room); if (!notifState.isUnread) return; @@ -1362,7 +1363,7 @@ export class RoomView extends React.Component { private updatePreviewUrlVisibility({ roomId }: Room): void { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = this.context.client.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; + const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; this.setState({ showUrlPreview: SettingsStore.getValue(key, roomId), }); @@ -1661,7 +1662,7 @@ export class RoomView extends React.Component { this.setState({ rejecting: true, }); - this.context.client.leave(this.state.roomId).then( + this.context.client?.leave(this.state.roomId).then( () => { dis.dispatch({ action: Action.ViewHomePage }); this.setState({ @@ -1691,13 +1692,13 @@ export class RoomView extends React.Component { }); try { - const myMember = this.state.room.getMember(this.context.client.getUserId()); - const inviteEvent = myMember.events.member; - const ignoredUsers = this.context.client.getIgnoredUsers(); - ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk - await this.context.client.setIgnoredUsers(ignoredUsers); + const myMember = this.state.room!.getMember(this.context.client!.getSafeUserId()); + const inviteEvent = myMember!.events.member; + const ignoredUsers = this.context.client!.getIgnoredUsers(); + ignoredUsers.push(inviteEvent!.getSender()!); // de-duped internally in the js-sdk + await this.context.client!.setIgnoredUsers(ignoredUsers); - await this.context.client.leave(this.state.roomId); + await this.context.client!.leave(this.state.roomId!); dis.dispatch({ action: Action.ViewHomePage }); this.setState({ rejecting: false, diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx index c732f07d3a..3b380c1d19 100644 --- a/src/components/structures/ScrollPanel.tsx +++ b/src/components/structures/ScrollPanel.tsx @@ -382,13 +382,12 @@ export default class ScrollPanel extends React.Component { } const itemlist = this.itemlist.current; - const firstTile = itemlist && (itemlist.firstElementChild as HTMLElement); - const contentTop = firstTile && firstTile.offsetTop; + const firstTile = itemlist?.firstElementChild as HTMLElement | undefined; const fillPromises: Promise[] = []; // if scrollTop gets to 1 screen from the top of the first tile, // try backward filling - if (!firstTile || sn.scrollTop - contentTop < sn.clientHeight) { + if (!firstTile || sn.scrollTop - firstTile.offsetTop < sn.clientHeight) { // need to back-fill fillPromises.push(this.maybeFill(depth, true)); } @@ -424,7 +423,7 @@ export default class ScrollPanel extends React.Component { // check if unfilling is possible and send an unfill request if necessary private checkUnfillState(backwards: boolean): void { let excessHeight = this.getExcessHeight(backwards); - if (excessHeight <= 0) { + if (excessHeight <= 0 || !this.itemlist.current) { return; } @@ -617,10 +616,7 @@ export default class ScrollPanel extends React.Component { * node (specifically, the bottom of it) will be positioned. If omitted, it * defaults to 0. */ - public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => { - pixelOffset = pixelOffset || 0; - offsetBase = offsetBase || 0; - + public scrollToToken = (scrollToken: string, pixelOffset = 0, offsetBase = 0): void => { // set the trackedScrollToken, so we can get the node through getTrackedNode this.scrollState = { stuckAtBottom: false, @@ -652,6 +648,7 @@ export default class ScrollPanel extends React.Component { const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight); const itemlist = this.itemlist.current; + if (!itemlist) return; const messages = itemlist.children; let node: HTMLElement | null = null; @@ -705,7 +702,7 @@ export default class ScrollPanel extends React.Component { this.bottomGrowth += bottomDiff; scrollState.bottomOffset = newBottomOffset; const newHeight = `${this.getListHeight()}px`; - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } debuglog("balancing height because messages below viewport grew by", bottomDiff); @@ -755,7 +752,7 @@ export default class ScrollPanel extends React.Component { const scrollState = this.scrollState; if (scrollState.stuckAtBottom) { - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } if (sn.scrollTop !== sn.scrollHeight) { @@ -770,7 +767,7 @@ export default class ScrollPanel extends React.Component { // the currently filled piece of the timeline if (trackedNode) { const oldTop = trackedNode.offsetTop; - if (itemlist.style.height !== newHeight) { + if (itemlist && itemlist.style.height !== newHeight) { itemlist.style.height = newHeight; } const newTop = trackedNode.offsetTop; @@ -823,9 +820,9 @@ export default class ScrollPanel extends React.Component { private getMessagesHeight(): number { const itemlist = this.itemlist.current; - const lastNode = itemlist.lastElementChild as HTMLElement; + const lastNode = itemlist?.lastElementChild as HTMLElement; const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0; - const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0; + const firstNodeTop = (itemlist?.firstElementChild as HTMLElement)?.offsetTop ?? 0; // 18 is itemlist padding return lastNodeBottom - firstNodeTop + 18 * 2; } @@ -865,8 +862,8 @@ export default class ScrollPanel extends React.Component { */ public preventShrinking = (): void => { const messageList = this.itemlist.current; - const tiles = messageList && messageList.children; - if (!messageList) { + const tiles = messageList?.children; + if (!tiles) { return; } let lastTileNode; diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index 3a28a2a4cd..6172d6228a 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -777,7 +777,7 @@ class TimelinePanel extends React.Component { } }; - public canResetTimeline = (): boolean => this.messagePanel?.current?.isAtBottom(); + public canResetTimeline = (): boolean => this.messagePanel?.current?.isAtBottom() === true; private onRoomRedaction = (ev: MatrixEvent, room: Room): void => { if (this.unmounted) return; @@ -1044,7 +1044,7 @@ class TimelinePanel extends React.Component { const sendRRs = SettingsStore.getValue("sendReadReceipts", roomId); debuglog( - `Sending Read Markers for ${this.props.timelineSet.room.roomId}: `, + `Sending Read Markers for ${roomId}: `, `rm=${this.state.readMarkerEventId} `, `rr=${sendRRs ? lastReadEvent?.getId() : null} `, `prr=${lastReadEvent?.getId()}`, @@ -1092,7 +1092,7 @@ class TimelinePanel extends React.Component { // we only do this if we're right at the end, because we're just assuming // that sending an RR for the latest message will set our notif counter // to zero: it may not do this if we send an RR for somewhere before the end. - if (this.isAtEndOfLiveTimeline()) { + if (this.isAtEndOfLiveTimeline() && this.props.timelineSet.room) { this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Total, 0); this.props.timelineSet.room.setUnreadNotificationCount(NotificationCountType.Highlight, 0); dis.dispatch({ diff --git a/src/components/views/rooms/MessageComposerFormatBar.tsx b/src/components/views/rooms/MessageComposerFormatBar.tsx index e1a2f9d4bd..9730806e3e 100644 --- a/src/components/views/rooms/MessageComposerFormatBar.tsx +++ b/src/components/views/rooms/MessageComposerFormatBar.tsx @@ -99,7 +99,7 @@ export default class MessageComposerFormatBar extends React.PureComponent { return {}; } const kickerMember = this.props.room?.currentState.getMember(myMember.events.member.getSender()); - const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender(); + const memberName = kickerMember?.name ?? myMember.events.member?.getSender(); const reason = myMember.events.member?.getContent().reason; return { memberName, reason }; } diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx index 6e471dde42..bca5388d1e 100644 --- a/src/components/views/rooms/RoomSublist.tsx +++ b/src/components/views/rooms/RoomSublist.tsx @@ -433,9 +433,11 @@ export default class RoomSublist extends React.Component { }; private onHeaderClick = (): void => { - const possibleSticky = this.headerButton.current.parentElement; - const sublist = possibleSticky.parentElement.parentElement; - const list = sublist.parentElement.parentElement; + const possibleSticky = this.headerButton.current?.parentElement; + const sublist = possibleSticky?.parentElement?.parentElement; + const list = sublist?.parentElement?.parentElement; + if (!possibleSticky || !list) return; + // the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky const listScrollTop = Math.round(list.scrollTop); const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT); diff --git a/src/components/views/rooms/WhoIsTypingTile.tsx b/src/components/views/rooms/WhoIsTypingTile.tsx index 3c23e76482..2ff8d1d9b4 100644 --- a/src/components/views/rooms/WhoIsTypingTile.tsx +++ b/src/components/views/rooms/WhoIsTypingTile.tsx @@ -91,7 +91,7 @@ export default class WhoIsTypingTile extends React.Component { private onRoomTimeline = (event: MatrixEvent, room?: Room): void => { if (room?.roomId === this.props.room.roomId) { - const userId = event.getSender(); + const userId = event.getSender()!; // remove user from usersTyping const usersTyping = this.state.usersTyping.filter((m) => m.userId !== userId); if (usersTyping.length !== this.state.usersTyping.length) { @@ -200,14 +200,15 @@ export default class WhoIsTypingTile extends React.Component { } public render(): React.ReactNode { - let usersTyping = this.state.usersTyping; - const stoppedUsersOnTimer = Object.keys(this.state.delayedStopTypingTimers).map((userId) => - this.props.room.getMember(userId), - ); + const usersTyping = this.state.usersTyping; // append the users that have been reported not typing anymore // but have a timeout timer running so they can disappear // when a message comes in - usersTyping = usersTyping.concat(stoppedUsersOnTimer); + for (const userId in this.state.delayedStopTypingTimers) { + const member = this.props.room.getMember(userId); + if (member) usersTyping.push(member); + } + // sort them so the typing members don't change order when // moved to delayedStopTypingTimers usersTyping.sort((a, b) => compare(a.name, b.name));