From 503df6219103ae99eb34465a1827e5e4a469a0b5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Mar 2023 15:07:20 +0000 Subject: [PATCH] Conform more of the codebase to `strictNullChecks` (#10358 * Conform more of the codebase to `strictNullChecks` * Fix types * Iterate * Iterate --- src/components/views/auth/EmailField.tsx | 2 +- .../views/auth/PassphraseConfirmField.tsx | 2 +- src/components/views/auth/PassphraseField.tsx | 2 +- .../context_menus/MessageContextMenu.tsx | 50 +++++++++---------- .../dialogs/devtools/SettingExplorer.tsx | 2 +- .../views/directory/NetworkDropdown.tsx | 2 +- .../views/elements/AccessibleButton.tsx | 5 +- .../views/elements/AppPermission.tsx | 2 - .../elements/DesktopCapturerSourcePicker.tsx | 3 +- .../views/elements/DialogButtons.tsx | 8 +-- src/components/views/elements/Dropdown.tsx | 16 +++--- .../views/elements/EffectsOverlay.tsx | 6 +-- .../views/elements/ErrorBoundary.tsx | 10 ++-- .../views/elements/EventListSummary.tsx | 4 +- src/components/views/elements/Field.tsx | 4 +- .../elements/GenericEventListSummary.tsx | 2 +- .../elements/IRCTimelineProfileResizer.tsx | 4 +- src/components/views/elements/ImageView.tsx | 24 ++++----- .../views/elements/InteractiveTooltip.tsx | 16 +++--- .../views/elements/LanguageDropdown.tsx | 6 +-- .../views/elements/LazyRenderList.tsx | 4 +- .../views/elements/MiniAvatarUploader.tsx | 3 +- .../views/elements/PersistentApp.tsx | 2 +- .../views/elements/PollCreateDialog.tsx | 7 ++- src/components/views/elements/ReplyChain.tsx | 2 +- .../views/elements/RoomAliasField.tsx | 9 ++-- src/components/views/elements/RoomTopic.tsx | 4 +- .../views/elements/SearchWarning.tsx | 2 +- .../views/elements/ServerPicker.tsx | 2 +- .../views/elements/SettingsFlag.tsx | 4 +- .../elements/SpellCheckLanguagesDropdown.tsx | 13 +++-- .../views/elements/TextWithTooltip.tsx | 2 +- .../views/elements/UseCaseSelection.tsx | 2 +- .../views/elements/UseCaseSelectionButton.tsx | 2 +- src/components/views/elements/Validation.tsx | 2 +- .../views/messages/DownloadActionButton.tsx | 2 +- .../views/messages/EditHistoryMessage.tsx | 21 +++----- .../views/messages/JumpToDatePicker.tsx | 2 +- .../views/messages/LegacyCallEvent.tsx | 2 +- src/components/views/messages/MBeaconBody.tsx | 6 +-- src/components/views/messages/MImageBody.tsx | 38 +++++++------- .../views/messages/MJitsiWidgetEvent.tsx | 2 +- .../messages/MKeyVerificationRequest.tsx | 2 +- .../views/messages/MLocationBody.tsx | 13 +++-- src/components/views/messages/MPollBody.tsx | 2 +- .../views/messages/MStickerBody.tsx | 12 ++--- src/components/views/messages/MVideoBody.tsx | 24 ++++----- .../views/messages/MessageActionBar.tsx | 24 ++++----- .../views/messages/MessageEvent.tsx | 8 +-- .../views/messages/ReactionsRow.tsx | 4 +- .../views/messages/ReactionsRowButton.tsx | 4 +- .../views/messages/SenderProfile.tsx | 4 +- src/components/views/messages/TextualBody.tsx | 41 +++++++-------- .../views/messages/TileErrorBoundary.tsx | 6 +-- src/components/views/rooms/Autocomplete.tsx | 4 +- .../views/rooms/BasicMessageComposer.tsx | 18 +++---- src/components/views/rooms/E2EIcon.tsx | 18 ++++--- .../views/rooms/EditMessageComposer.tsx | 11 ++-- src/components/views/rooms/EntityTile.tsx | 12 ++--- src/components/views/rooms/EventTile.tsx | 40 +++++++-------- src/components/views/rooms/HistoryTile.tsx | 8 +-- .../views/rooms/LinkPreviewGroup.tsx | 4 +- .../views/rooms/LinkPreviewWidget.tsx | 8 +-- src/components/views/rooms/MemberList.tsx | 6 ++- src/components/views/rooms/MemberTile.tsx | 7 ++- .../views/rooms/MessageComposerButtons.tsx | 8 +-- src/components/views/rooms/NewRoomIntro.tsx | 14 +++--- .../views/rooms/NotificationBadge.tsx | 10 ++-- .../views/rooms/ReadReceiptGroup.tsx | 10 ++-- .../views/rooms/ReadReceiptMarker.tsx | 2 +- src/components/views/rooms/RoomPreviewBar.tsx | 9 ++-- src/components/views/rooms/RoomTile.tsx | 2 +- src/settings/SettingsStore.ts | 2 +- src/utils/EditorStateTransfer.ts | 2 +- src/utils/EventUtils.ts | 2 +- .../user/PreferencesUserSettingsTab-test.tsx | 6 +-- 76 files changed, 323 insertions(+), 327 deletions(-) diff --git a/src/components/views/auth/EmailField.tsx b/src/components/views/auth/EmailField.tsx index 0426c08f86..849f38fea7 100644 --- a/src/components/views/auth/EmailField.tsx +++ b/src/components/views/auth/EmailField.tsx @@ -21,7 +21,7 @@ import { _t, _td } from "../../../languageHandler"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import * as Email from "../../../email"; -interface IProps extends Omit<IInputProps, "onValidate"> { +interface IProps extends Omit<IInputProps, "onValidate" | "element"> { id?: string; fieldRef?: RefCallback<Field> | RefObject<Field>; value: string; diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index 2411547912..b314c3f838 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -20,7 +20,7 @@ import Field, { IInputProps } from "../elements/Field"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import { _t, _td } from "../../../languageHandler"; -interface IProps extends Omit<IInputProps, "onValidate" | "label"> { +interface IProps extends Omit<IInputProps, "onValidate" | "label" | "element"> { id?: string; fieldRef?: RefCallback<Field> | RefObject<Field>; autoComplete?: string; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index 5175de6aa7..1050e8f6e2 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -23,7 +23,7 @@ import withValidation, { IFieldState, IValidationResult } from "../elements/Vali import { _t, _td } from "../../../languageHandler"; import Field, { IInputProps } from "../elements/Field"; -interface IProps extends Omit<IInputProps, "onValidate"> { +interface IProps extends Omit<IInputProps, "onValidate" | "element"> { autoFocus?: boolean; id?: string; className?: string; diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index ad56409592..8392e6a14f 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -179,7 +179,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> private isPinned(): boolean { const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); - const pinnedEvent = room.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); + const pinnedEvent = room?.currentState.getStateEvents(EventType.RoomPinnedEvents, ""); if (!pinnedEvent) return false; const content = pinnedEvent.getContent(); return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId()); @@ -389,7 +389,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> timelineRenderingType === TimelineRenderingType.ThreadsList; const isThreadRootEvent = isThread && mxEvent?.getThread()?.rootEvent === mxEvent; - let resendReactionsButton: JSX.Element; + let resendReactionsButton: JSX.Element | undefined; if (!mxEvent.isRedacted() && unsentReactionsCount !== 0) { resendReactionsButton = ( <IconizedContextMenuOption @@ -400,7 +400,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let redactButton: JSX.Element; + let redactButton: JSX.Element | undefined; if (isSent && this.state.canRedact) { redactButton = ( <IconizedContextMenuOption @@ -411,7 +411,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let openInMapSiteButton: JSX.Element; + let openInMapSiteButton: JSX.Element | undefined; const shareableLocationEvent = getShareableLocationEvent(mxEvent, cli); if (shareableLocationEvent) { const mapSiteLink = createMapSiteLinkFromEvent(shareableLocationEvent); @@ -430,7 +430,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let forwardButton: JSX.Element; + let forwardButton: JSX.Element | undefined; const forwardableEvent = getForwardableEvent(mxEvent, cli); if (contentActionable && forwardableEvent) { forwardButton = ( @@ -442,7 +442,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let pinButton: JSX.Element; + let pinButton: JSX.Element | undefined; if (contentActionable && this.state.canPin) { pinButton = ( <IconizedContextMenuOption @@ -462,7 +462,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> /> ); - let unhidePreviewButton: JSX.Element; + let unhidePreviewButton: JSX.Element | undefined; if (eventTileOps?.isWidgetHidden()) { unhidePreviewButton = ( <IconizedContextMenuOption @@ -473,7 +473,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let permalinkButton: JSX.Element; + let permalinkButton: JSX.Element | undefined; if (permalink) { permalinkButton = ( <IconizedContextMenuOption @@ -493,7 +493,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let endPollButton: JSX.Element; + let endPollButton: JSX.Element | undefined; if (this.canEndPoll(mxEvent)) { endPollButton = ( <IconizedContextMenuOption @@ -504,7 +504,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let quoteButton: JSX.Element; + let quoteButton: JSX.Element | undefined; if (eventTileOps && canSendMessages) { // this event is rendered using TextualBody quoteButton = ( @@ -517,7 +517,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> } // Bridges can provide a 'external_url' to link back to the source. - let externalURLButton: JSX.Element; + let externalURLButton: JSX.Element | undefined; if ( typeof mxEvent.getContent().external_url === "string" && isUrlPermitted(mxEvent.getContent().external_url) @@ -540,7 +540,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let collapseReplyChainButton: JSX.Element; + let collapseReplyChainButton: JSX.Element | undefined; if (collapseReplyChain) { collapseReplyChainButton = ( <IconizedContextMenuOption @@ -551,7 +551,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let jumpToRelatedEventButton: JSX.Element; + let jumpToRelatedEventButton: JSX.Element | undefined; const relatedEventId = mxEvent.getWireContent()?.["m.relates_to"]?.event_id; if (relatedEventId && SettingsStore.getValue("developerMode")) { jumpToRelatedEventButton = ( @@ -563,7 +563,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let reportEventButton: JSX.Element; + let reportEventButton: JSX.Element | undefined; if (mxEvent.getSender() !== me) { reportEventButton = ( <IconizedContextMenuOption @@ -574,7 +574,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let copyLinkButton: JSX.Element; + let copyLinkButton: JSX.Element | undefined; if (link) { copyLinkButton = ( <IconizedContextMenuOption @@ -594,7 +594,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let copyButton: JSX.Element; + let copyButton: JSX.Element | undefined; if (rightClick && getSelectedText()) { copyButton = ( <IconizedContextMenuOption @@ -606,7 +606,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let editButton: JSX.Element; + let editButton: JSX.Element | undefined; if (rightClick && canEditContent(mxEvent)) { editButton = ( <IconizedContextMenuOption @@ -617,7 +617,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let replyButton: JSX.Element; + let replyButton: JSX.Element | undefined; if (rightClick && contentActionable && canSendMessages) { replyButton = ( <IconizedContextMenuOption @@ -628,7 +628,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let replyInThreadButton: JSX.Element; + let replyInThreadButton: JSX.Element | undefined; if ( rightClick && contentActionable && @@ -639,7 +639,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> replyInThreadButton = <ReplyInThreadButton mxEvent={mxEvent} closeMenu={this.closeMenu} />; } - let reactButton; + let reactButton: JSX.Element | undefined; if (rightClick && contentActionable && canReact) { reactButton = ( <IconizedContextMenuOption @@ -651,7 +651,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let viewInRoomButton: JSX.Element; + let viewInRoomButton: JSX.Element | undefined; if (isThreadRootEvent) { viewInRoomButton = ( <IconizedContextMenuOption @@ -662,7 +662,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let nativeItemsList: JSX.Element; + let nativeItemsList: JSX.Element | undefined; if (copyButton || copyLinkButton) { nativeItemsList = ( <IconizedContextMenuOptionList> @@ -672,7 +672,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState> ); } - let quickItemsList: JSX.Element; + let quickItemsList: JSX.Element | undefined; if (editButton || replyButton || reactButton) { quickItemsList = ( <IconizedContextMenuOptionList> @@ -703,12 +703,12 @@ export default class MessageContextMenu extends React.Component<IProps, IState> </IconizedContextMenuOptionList> ); - let redactItemList: JSX.Element; + let redactItemList: JSX.Element | undefined; if (redactButton) { redactItemList = <IconizedContextMenuOptionList red>{redactButton}</IconizedContextMenuOptionList>; } - let reactionPicker: JSX.Element; + let reactionPicker: JSX.Element | undefined; if (this.state.reactionPickerDisplayed) { const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect(); reactionPicker = ( diff --git a/src/components/views/dialogs/devtools/SettingExplorer.tsx b/src/components/views/dialogs/devtools/SettingExplorer.tsx index c0801cd062..c3470ad030 100644 --- a/src/components/views/dialogs/devtools/SettingExplorer.tsx +++ b/src/components/views/dialogs/devtools/SettingExplorer.tsx @@ -64,7 +64,7 @@ interface ICanEditLevelFieldProps { } const CanEditLevelField: React.FC<ICanEditLevelFieldProps> = ({ setting, roomId, level }) => { - const canEdit = SettingsStore.canSetValue(setting, roomId, level); + const canEdit = SettingsStore.canSetValue(setting, roomId ?? null, level); const className = canEdit ? "mx_DevTools_SettingsExplorer_mutable" : "mx_DevTools_SettingsExplorer_immutable"; return ( <td className={className}> diff --git a/src/components/views/directory/NetworkDropdown.tsx b/src/components/views/directory/NetworkDropdown.tsx index b4f5b5bc04..c84bb4a2f1 100644 --- a/src/components/views/directory/NetworkDropdown.tsx +++ b/src/components/views/directory/NetworkDropdown.tsx @@ -44,7 +44,7 @@ const validServer = withValidation<undefined, { error?: MatrixError }>({ // check if we can successfully load this server's room directory await MatrixClientPeg.get().publicRooms({ limit: 1, - server: value, + server: value ?? undefined, }); return {}; } catch (error) { diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 4bceab5ac9..10f18f1f31 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -14,7 +14,7 @@ limitations under the License. */ -import React, { HTMLAttributes, InputHTMLAttributes, ReactHTML, ReactNode } from "react"; +import React, { HTMLAttributes, InputHTMLAttributes, ReactNode } from "react"; import classnames from "classnames"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; @@ -91,7 +91,7 @@ export interface IAccessibleButtonProps extends React.InputHTMLAttributes<Elemen * @returns {Object} rendered react */ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>({ - element, + element = "div" as T, onClick, children, kind, @@ -169,7 +169,6 @@ export default function AccessibleButton<T extends keyof JSX.IntrinsicElements>( } AccessibleButton.defaultProps = { - element: "div" as keyof ReactHTML, role: "button", tabIndex: 0, }; diff --git a/src/components/views/elements/AppPermission.tsx b/src/components/views/elements/AppPermission.tsx index 3b1c18a692..7adae9113b 100644 --- a/src/components/views/elements/AppPermission.tsx +++ b/src/components/views/elements/AppPermission.tsx @@ -61,8 +61,6 @@ export default class AppPermission extends React.Component<IProps, IState> { // Set all this into the initial state this.state = { - widgetDomain: null, - isWrapped: null, roomMember, ...urlInfo, }; diff --git a/src/components/views/elements/DesktopCapturerSourcePicker.tsx b/src/components/views/elements/DesktopCapturerSourcePicker.tsx index cf8f6b018d..7be62a8999 100644 --- a/src/components/views/elements/DesktopCapturerSourcePicker.tsx +++ b/src/components/views/elements/DesktopCapturerSourcePicker.tsx @@ -33,7 +33,8 @@ export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource }, types: ["screen", "window"], }; - return PlatformPeg.get().getDesktopCapturerSources(options); + const plaf = PlatformPeg.get(); + return plaf ? plaf?.getDesktopCapturerSources(options) : Promise.resolve<DesktopCapturerSource[]>([]); } export enum Tabs { diff --git a/src/components/views/elements/DialogButtons.tsx b/src/components/views/elements/DialogButtons.tsx index 39c943b405..ddd5e274d0 100644 --- a/src/components/views/elements/DialogButtons.tsx +++ b/src/components/views/elements/DialogButtons.tsx @@ -69,7 +69,7 @@ export default class DialogButtons extends React.Component<IProps> { }; private onCancelClick = (event: React.MouseEvent): void => { - this.props.onCancel(event); + this.props.onCancel?.(event); }; public render(): React.ReactNode { @@ -77,9 +77,9 @@ export default class DialogButtons extends React.Component<IProps> { if (this.props.primaryButtonClass) { primaryButtonClassName += " " + this.props.primaryButtonClass; } - let cancelButton; - if (this.props.cancelButton || this.props.hasCancel) { + let cancelButton: JSX.Element | undefined; + if (this.props.hasCancel) { cancelButton = ( <button // important: the default type is 'submit' and this button comes before the @@ -95,7 +95,7 @@ export default class DialogButtons extends React.Component<IProps> { ); } - let additive = null; + let additive: JSX.Element | undefined; if (this.props.additive) { additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>; } diff --git a/src/components/views/elements/Dropdown.tsx b/src/components/views/elements/Dropdown.tsx index aecc8e8141..e16eaf978e 100644 --- a/src/components/views/elements/Dropdown.tsx +++ b/src/components/views/elements/Dropdown.tsx @@ -113,8 +113,8 @@ interface IState { */ export default class Dropdown extends React.Component<DropdownProps, IState> { private readonly buttonRef = createRef<HTMLDivElement>(); - private dropdownRootElement: HTMLDivElement = null; - private ignoreEvent: MouseEvent = null; + private dropdownRootElement: HTMLDivElement | null = null; + private ignoreEvent: MouseEvent | null = null; private childrenByKey: Record<string, ReactNode> = {}; public constructor(props: DropdownProps) { @@ -373,18 +373,14 @@ export default class Dropdown extends React.Component<DropdownProps, IState> { ); } - const dropdownClasses: Record<string, boolean> = { - mx_Dropdown: true, - mx_Dropdown_disabled: this.props.disabled, - }; - if (this.props.className) { - dropdownClasses[this.props.className] = true; - } + const dropdownClasses = classnames("mx_Dropdown", this.props.className, { + mx_Dropdown_disabled: !!this.props.disabled, + }); // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. return ( - <div className={classnames(dropdownClasses)} ref={this.collectRoot}> + <div className={dropdownClasses} ref={this.collectRoot}> <AccessibleButton className="mx_Dropdown_input mx_no_textinput" onClick={this.onAccessibleButtonClick} diff --git a/src/components/views/elements/EffectsOverlay.tsx b/src/components/views/elements/EffectsOverlay.tsx index cf11350c2b..604b513d30 100644 --- a/src/components/views/elements/EffectsOverlay.tsx +++ b/src/components/views/elements/EffectsOverlay.tsx @@ -30,7 +30,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => { const canvasRef = useRef<HTMLCanvasElement>(null); const effectsRef = useRef<Map<string, ICanvasEffect>>(new Map<string, ICanvasEffect>()); - const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect> => { + const lazyLoadEffectModule = async (name: string): Promise<ICanvasEffect | null> => { if (!name) return null; let effect: ICanvasEffect | null = effectsRef.current.get(name) || null; if (effect === null) { @@ -38,7 +38,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => { try { const { default: Effect } = await import(`../../../effects/${name}`); effect = new Effect(options); - effectsRef.current.set(name, effect); + effectsRef.current.set(name, effect!); } catch (err) { logger.warn(`Unable to load effect module at '../../../effects/${name}.`, err); } @@ -70,7 +70,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => { // eslint-disable-next-line react-hooks/exhaustive-deps const currentEffects = effectsRef.current; // this is not a react node ref, warning can be safely ignored for (const effect in currentEffects) { - const effectModule: ICanvasEffect = currentEffects.get(effect); + const effectModule: ICanvasEffect = currentEffects.get(effect)!; if (effectModule && effectModule.isRunning) { effectModule.stop(); } diff --git a/src/components/views/elements/ErrorBoundary.tsx b/src/components/views/elements/ErrorBoundary.tsx index 3653415db6..a230ab84c5 100644 --- a/src/components/views/elements/ErrorBoundary.tsx +++ b/src/components/views/elements/ErrorBoundary.tsx @@ -30,7 +30,7 @@ interface Props { } interface IState { - error: Error; + error?: Error; } /** @@ -41,9 +41,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> { public constructor(props: Props) { super(props); - this.state = { - error: null, - }; + this.state = {}; } public static getDerivedStateFromError(error: Error): Partial<IState> { @@ -66,7 +64,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> { MatrixClientPeg.get() .store.deleteAllData() .then(() => { - PlatformPeg.get().reload(); + PlatformPeg.get()?.reload(); }); }; @@ -121,7 +119,7 @@ export default class ErrorBoundary extends React.PureComponent<Props, IState> { ); } - let clearCacheButton: JSX.Element; + let clearCacheButton: JSX.Element | undefined; // we only show this button if there is an initialised MatrixClient otherwise we can't clear the cache if (MatrixClientPeg.get()) { clearCacheButton = ( diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx index 1ba14bb051..5498773777 100644 --- a/src/components/views/elements/EventListSummary.tsx +++ b/src/components/views/elements/EventListSummary.tsx @@ -113,7 +113,7 @@ export default class EventListSummary extends React.Component<IProps> { private generateSummary( eventAggregates: Record<string, string[]>, orderedTransitionSequences: string[], - ): string | JSX.Element { + ): ReactNode { const summaries = orderedTransitionSequences.map((transitions) => { const userNames = eventAggregates[transitions]; const nameList = this.renderNameList(userNames); @@ -392,7 +392,7 @@ export default class EventListSummary extends React.Component<IProps> { * @returns {string?} the transition type given to this event. This defaults to `null` * if a transition is not recognised. */ - private static getTransition(e: IUserEvents): TransitionType { + private static getTransition(e: IUserEvents): TransitionType | null { if (e.mxEvent.isRedacted()) { return TransitionType.MessageRemoved; } diff --git a/src/components/views/elements/Field.tsx b/src/components/views/elements/Field.tsx index 2e533e1925..c5e531360f 100644 --- a/src/components/views/elements/Field.tsx +++ b/src/components/views/elements/Field.tsx @@ -79,7 +79,7 @@ export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElemen // The ref pass through to the input inputRef?: RefObject<HTMLInputElement>; // The element to create. Defaults to "input". - element?: "input"; + element: "input"; // The input's value. This is a controlled component, so the value is required. value: string; } @@ -204,7 +204,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> { const value = this.inputRef.current?.value ?? null; const { valid, feedback } = await this.props.onValidate({ value, - focused, + focused: !!focused, allowEmpty, }); diff --git a/src/components/views/elements/GenericEventListSummary.tsx b/src/components/views/elements/GenericEventListSummary.tsx index c90c64d740..83d12a360b 100644 --- a/src/components/views/elements/GenericEventListSummary.tsx +++ b/src/components/views/elements/GenericEventListSummary.tsx @@ -36,7 +36,7 @@ interface IProps { // The list of room members for which to show avatars next to the summary "summaryMembers"?: RoomMember[]; // The text to show as the summary of this event list - "summaryText"?: string | JSX.Element; + "summaryText"?: ReactNode; // An array of EventTiles to render when expanded "children": ReactNode[]; // Called when the event list expansion is toggled diff --git a/src/components/views/elements/IRCTimelineProfileResizer.tsx b/src/components/views/elements/IRCTimelineProfileResizer.tsx index af1585e997..48bf1a188d 100644 --- a/src/components/views/elements/IRCTimelineProfileResizer.tsx +++ b/src/components/views/elements/IRCTimelineProfileResizer.tsx @@ -29,7 +29,7 @@ interface IProps { interface IState { width: number; - IRCLayoutRoot: HTMLElement; + IRCLayoutRoot: HTMLElement | null; } export default class IRCTimelineProfileResizer extends React.Component<IProps, IState> { @@ -77,7 +77,7 @@ export default class IRCTimelineProfileResizer extends React.Component<IProps, I }; private updateCSSWidth(newWidth: number): void { - this.state.IRCLayoutRoot.style.setProperty("--name-width", newWidth + "px"); + this.state.IRCLayoutRoot?.style.setProperty("--name-width", newWidth + "px"); } private onMoueUp = (): void => { diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 325c54304a..d8cf6c5ef7 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -16,7 +16,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from "react"; +import React, { createRef, CSSProperties } from "react"; import FocusLock from "react-focus-lock"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; @@ -395,7 +395,7 @@ export default class ImageView extends React.Component<IProps, IState> { }; private renderContextMenu(): JSX.Element { - let contextMenu = null; + let contextMenu: JSX.Element | undefined; if (this.state.contextMenuDisplayed) { contextMenu = ( <MessageContextMenu @@ -419,11 +419,6 @@ export default class ImageView extends React.Component<IProps, IState> { else if (this.state.moving || !this.imageIsLoaded) transitionClassName = ""; else transitionClassName = "mx_ImageView_image_animating"; - let cursor; - if (this.state.moving) cursor = "grabbing"; - else if (this.state.zoom === this.state.minZoom) cursor = "zoom-in"; - else cursor = "zoom-out"; - const rotationDegrees = this.state.rotation + "deg"; const zoom = this.state.zoom; const translatePixelsX = this.state.translationX + "px"; @@ -432,15 +427,18 @@ export default class ImageView extends React.Component<IProps, IState> { // First, we translate and only then we rotate, otherwise // we would apply the translation to an already rotated // image causing it translate in the wrong direction. - const style = { - cursor: cursor, + const style: CSSProperties = { transform: `translateX(${translatePixelsX}) translateY(${translatePixelsY}) scale(${zoom}) rotate(${rotationDegrees})`, }; - let info; + if (this.state.moving) style.cursor = "grabbing"; + else if (this.state.zoom === this.state.minZoom) style.cursor = "zoom-in"; + else style.cursor = "zoom-out"; + + let info: JSX.Element | undefined; if (showEventMeta) { const mxEvent = this.props.mxEvent; const showTwelveHour = SettingsStore.getValue("showTwelveHourTimestamps"); @@ -449,7 +447,7 @@ export default class ImageView extends React.Component<IProps, IState> { permalink = this.props.permalinkCreator.forEvent(this.props.mxEvent.getId()); } - const senderName = mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender(); + const senderName = mxEvent.sender?.name ?? mxEvent.getSender(); const sender = <div className="mx_ImageView_info_sender">{senderName}</div>; const messageTimestamp = ( <a @@ -491,7 +489,7 @@ export default class ImageView extends React.Component<IProps, IState> { info = <div />; } - let contextMenuButton; + let contextMenuButton: JSX.Element | undefined; if (this.props.mxEvent) { contextMenuButton = ( <ContextMenuTooltipButton @@ -519,7 +517,7 @@ export default class ImageView extends React.Component<IProps, IState> { /> ); - let title: JSX.Element; + let title: JSX.Element | undefined; if (this.props.mxEvent?.getContent()) { title = ( <div className="mx_ImageView_title"> diff --git a/src/components/views/elements/InteractiveTooltip.tsx b/src/components/views/elements/InteractiveTooltip.tsx index 1986de0243..3aecd8a743 100644 --- a/src/components/views/elements/InteractiveTooltip.tsx +++ b/src/components/views/elements/InteractiveTooltip.tsx @@ -293,7 +293,7 @@ interface IProps { } interface IState { - contentRect: DOMRect; + contentRect?: DOMRect; visible: boolean; } @@ -312,7 +312,6 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> super(props); this.state = { - contentRect: null, visible: false, }; } @@ -331,7 +330,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> document.removeEventListener("mousemove", this.onMouseMove); } - private collectContentRect = (element: HTMLElement): void => { + private collectContentRect = (element: HTMLElement | null): void => { // We don't need to clean up when unmounting, so ignore if (!element) return; @@ -354,7 +353,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> } else { const targetRight = targetRect.right + window.scrollX; const spaceOnRight = UIStore.instance.windowWidth - targetRight; - return contentRect && spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE; + return !!contentRect && spaceOnRight - contentRect.width < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE; } } @@ -368,7 +367,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> } else { const targetBottom = targetRect.bottom + window.scrollY; const spaceBelow = UIStore.instance.windowHeight - targetBottom; - return contentRect && spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE; + return !!contentRect && spaceBelow - contentRect.height < MIN_SAFE_DISTANCE_TO_WINDOW_EDGE; } } @@ -416,7 +415,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> document.removeEventListener("mousemove", this.onMouseMove); } - private renderTooltip(): JSX.Element { + private renderTooltip(): ReactNode { const { contentRect, visible } = this.state; if (!visible) { ReactDOM.unmountComponentAtNode(getOrCreateContainer()); @@ -435,7 +434,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> // tooltip content would extend past the safe area towards the window // edge, flip around to below the target. const position: Partial<IRect> = {}; - let chevronFace: ChevronFace = null; + let chevronFace: ChevronFace | null = null; if (this.isOnTheSide) { if (this.onLeftOfTarget()) { position.left = targetLeft; @@ -461,8 +460,7 @@ export default class InteractiveTooltip extends React.Component<IProps, IState> const chevron = <div className={"mx_InteractiveTooltip_chevron_" + chevronFace} />; - const menuClasses = classNames({ - mx_InteractiveTooltip: true, + const menuClasses = classNames("mx_InteractiveTooltip", { mx_InteractiveTooltip_withChevron_top: chevronFace === ChevronFace.Top, mx_InteractiveTooltip_withChevron_left: chevronFace === ChevronFace.Left, mx_InteractiveTooltip_withChevron_right: chevronFace === ChevronFace.Right, diff --git a/src/components/views/elements/LanguageDropdown.tsx b/src/components/views/elements/LanguageDropdown.tsx index ee03774cbc..178d85c4eb 100644 --- a/src/components/views/elements/LanguageDropdown.tsx +++ b/src/components/views/elements/LanguageDropdown.tsx @@ -40,7 +40,7 @@ interface IProps { interface IState { searchQuery: string; - langs: Languages; + langs: Languages | null; } export default class LanguageDropdown extends React.Component<IProps, IState> { @@ -103,8 +103,8 @@ export default class LanguageDropdown extends React.Component<IProps, IState> { // default value here too, otherwise we need to handle null / undefined // values between mounting and the initial value propagating - let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); - let value = null; + let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true); + let value: string | undefined; if (language) { value = this.props.value || language; } else { diff --git a/src/components/views/elements/LazyRenderList.tsx b/src/components/views/elements/LazyRenderList.tsx index e14941bd73..7fcbe1be2c 100644 --- a/src/components/views/elements/LazyRenderList.tsx +++ b/src/components/views/elements/LazyRenderList.tsx @@ -76,7 +76,7 @@ interface IProps<T> { } interface IState { - renderRange: ItemRange; + renderRange: ItemRange | null; } export default class LazyRenderList<T = any> extends React.Component<IProps<T>, IState> { @@ -93,7 +93,7 @@ export default class LazyRenderList<T = any> extends React.Component<IProps<T>, }; } - public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> { + public static getDerivedStateFromProps(props: IProps<unknown>, state: IState): Partial<IState> | null { const range = LazyRenderList.getVisibleRangeFromProps(props); const intersectRange = range.expand(props.overflowMargin); const renderRange = range.expand(props.overflowItems); diff --git a/src/components/views/elements/MiniAvatarUploader.tsx b/src/components/views/elements/MiniAvatarUploader.tsx index ec98fea43c..18652a4c62 100644 --- a/src/components/views/elements/MiniAvatarUploader.tsx +++ b/src/components/views/elements/MiniAvatarUploader.tsx @@ -64,7 +64,8 @@ const MiniAvatarUploader: React.FC<IProps> = ({ const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; const { room } = useContext(RoomContext); - const canSetAvatar = isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getUserId()); + const canSetAvatar = + isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>; const visible = !!label && (hover || show); diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 67ad09018d..a13588b96b 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -50,7 +50,7 @@ export default class PersistentApp extends React.Component<IProps> { app={app} fullWidth={true} room={this.room} - userId={this.context.credentials.userId} + userId={this.context.getSafeUserId()} creatorUserId={app.creatorUserId} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} waitForIframeLoad={app.waitForIframeLoad} diff --git a/src/components/views/elements/PollCreateDialog.tsx b/src/components/views/elements/PollCreateDialog.tsx index a30453ed87..dbade2d078 100644 --- a/src/components/views/elements/PollCreateDialog.tsx +++ b/src/components/views/elements/PollCreateDialog.tsx @@ -163,7 +163,12 @@ export default class PollCreateDialog extends ScrollableBaseModal<IProps, IState doMaybeLocalRoomAction( this.props.room.roomId, (actualRoomId: string) => - this.matrixClient.sendEvent(actualRoomId, this.props.threadId, pollEvent.type, pollEvent.content), + this.matrixClient.sendEvent( + actualRoomId, + this.props.threadId ?? null, + pollEvent.type, + pollEvent.content, + ), this.matrixClient, ) .then(() => this.props.onFinished(true)) diff --git a/src/components/views/elements/ReplyChain.tsx b/src/components/views/elements/ReplyChain.tsx index 9f80f4cf76..be415027c4 100644 --- a/src/components/views/elements/ReplyChain.tsx +++ b/src/components/views/elements/ReplyChain.tsx @@ -48,7 +48,7 @@ interface IProps { parentEv?: MatrixEvent; // called when the ReplyChain contents has changed, including EventTiles thereof onHeightChanged: () => void; - permalinkCreator: RoomPermalinkCreator; + permalinkCreator?: RoomPermalinkCreator; // Specifies which layout to use. layout?: Layout; // Whether to always show a timestamp diff --git a/src/components/views/elements/RoomAliasField.tsx b/src/components/views/elements/RoomAliasField.tsx index b2e6965c63..10549d79db 100644 --- a/src/components/views/elements/RoomAliasField.tsx +++ b/src/components/views/elements/RoomAliasField.tsx @@ -71,7 +71,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState> const postfix = domain ? <span title={`:${domain}`}>{`:${domain}`}</span> : <span />; const maxlength = domain ? 255 - domain.length - 2 : 255 - 1; // 2 for # and : const value = domain - ? this.props.value.substring(1, this.props.value.length - this.props.domain.length - 1) + ? this.props.value.substring(1, this.props.value.length - domain.length - 1) : this.props.value.substring(1); return { prefix, postfix, value, maxlength }; @@ -104,7 +104,7 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState> private onValidate = async (fieldState: IFieldState): Promise<IValidationResult> => { const result = await this.validationRules(fieldState); - this.setState({ isValid: result.valid }); + this.setState({ isValid: !!result.valid }); return result; }; @@ -225,8 +225,9 @@ export default class RoomAliasField extends React.PureComponent<IProps, IState> return this.state.isValid; } - public validate(options: IValidateOpts): Promise<boolean> { - return this.fieldRef.current?.validate(options); + public async validate(options: IValidateOpts): Promise<boolean> { + const val = await this.fieldRef.current?.validate(options); + return val ?? false; } public focus(): void { diff --git a/src/components/views/elements/RoomTopic.tsx b/src/components/views/elements/RoomTopic.tsx index e760d38218..7aa6bce949 100644 --- a/src/components/views/elements/RoomTopic.tsx +++ b/src/components/views/elements/RoomTopic.tsx @@ -33,7 +33,7 @@ import TooltipTarget from "./TooltipTarget"; import { Linkify, topicToHtml } from "../../../HtmlUtils"; interface IProps extends React.HTMLProps<HTMLDivElement> { - room?: Room; + room: Room; } export default function RoomTopic({ room, ...props }: IProps): JSX.Element { @@ -62,7 +62,7 @@ export default function RoomTopic({ room, ...props }: IProps): JSX.Element { useDispatcher(dis, (payload) => { if (payload.action === Action.ShowRoomTopic) { - const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getUserId()); + const canSetTopic = room.currentState.maySendStateEvent(EventType.RoomTopic, client.getSafeUserId()); const body = topicToHtml(topic?.text, topic?.html, ref, true); const modal = Modal.createDialog(InfoDialog, { diff --git a/src/components/views/elements/SearchWarning.tsx b/src/components/views/elements/SearchWarning.tsx index 14ffcbd510..421f550ff6 100644 --- a/src/components/views/elements/SearchWarning.tsx +++ b/src/components/views/elements/SearchWarning.tsx @@ -71,7 +71,7 @@ export default function SearchWarning({ isRoomEncrypted, kind }: IProps): JSX.El let text: ReactNode | undefined; let logo: JSX.Element | undefined; - if (desktopBuilds.get("available")) { + if (desktopBuilds?.get("available")) { logo = <img src={desktopBuilds.get("logo")} />; const buildUrl = desktopBuilds.get("url"); switch (kind) { diff --git a/src/components/views/elements/ServerPicker.tsx b/src/components/views/elements/ServerPicker.tsx index adfb263fa5..f940150a9e 100644 --- a/src/components/views/elements/ServerPicker.tsx +++ b/src/components/views/elements/ServerPicker.tsx @@ -33,7 +33,7 @@ interface IProps { } const showPickerDialog = ( - title: string, + title: string | undefined, serverConfig: ValidatedServerConfig, onFinished: (config: ValidatedServerConfig) => void, ): void => { diff --git a/src/components/views/elements/SettingsFlag.tsx b/src/components/views/elements/SettingsFlag.tsx index 87a6b38df4..b1e6cf41c0 100644 --- a/src/components/views/elements/SettingsFlag.tsx +++ b/src/components/views/elements/SettingsFlag.tsx @@ -69,14 +69,14 @@ export default class SettingsFlag extends React.Component<IProps, IState> { private save = async (val?: boolean): Promise<void> => { await SettingsStore.setValue( this.props.name, - this.props.roomId, + this.props.roomId ?? null, this.props.level, val !== undefined ? val : this.state.value, ); }; public render(): React.ReactNode { - const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level); + const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId ?? null, this.props.level); if (!canChange && this.props.hideIfCannotSet) return null; diff --git a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx index f4fd7fe9dc..7a725cf2d4 100644 --- a/src/components/views/elements/SpellCheckLanguagesDropdown.tsx +++ b/src/components/views/elements/SpellCheckLanguagesDropdown.tsx @@ -38,7 +38,7 @@ interface SpellCheckLanguagesDropdownIProps { interface SpellCheckLanguagesDropdownIState { searchQuery: string; - languages: Languages; + languages?: Languages; } export default class SpellCheckLanguagesDropdown extends React.Component< @@ -51,7 +51,6 @@ export default class SpellCheckLanguagesDropdown extends React.Component< this.state = { searchQuery: "", - languages: null, }; } @@ -59,7 +58,7 @@ export default class SpellCheckLanguagesDropdown extends React.Component< const plaf = PlatformPeg.get(); if (plaf) { plaf.getAvailableSpellCheckLanguages() - .then((languages) => { + ?.then((languages) => { languages.sort(function (a, b) { if (a < b) return -1; if (a > b) return 1; @@ -92,11 +91,11 @@ export default class SpellCheckLanguagesDropdown extends React.Component< } public render(): React.ReactNode { - if (this.state.languages === null) { + if (!this.state.languages) { return <Spinner />; } - let displayedLanguages; + let displayedLanguages: Languages; if (this.state.searchQuery) { displayedLanguages = this.state.languages.filter((lang) => { return languageMatchesSearchQuery(this.state.searchQuery, lang); @@ -111,8 +110,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component< // default value here too, otherwise we need to handle null / undefined; // values between mounting and the initial value propagating - let language = SettingsStore.getValue("language", null, /*excludeDefault:*/ true); - let value = null; + let language = SettingsStore.getValue<string | undefined>("language", null, /*excludeDefault:*/ true); + let value: string | undefined; if (language) { value = this.props.value || language; } else { diff --git a/src/components/views/elements/TextWithTooltip.tsx b/src/components/views/elements/TextWithTooltip.tsx index e54ed077c5..3ebdac5511 100644 --- a/src/components/views/elements/TextWithTooltip.tsx +++ b/src/components/views/elements/TextWithTooltip.tsx @@ -24,7 +24,7 @@ interface IProps extends HTMLAttributes<HTMLSpanElement> { tooltipClass?: string; tooltip: React.ReactNode; tooltipProps?: Omit<React.ComponentProps<typeof TooltipTarget>, "label" | "tooltipClassName" | "className">; - onClick?: (ev?: React.MouseEvent) => void; + onClick?: (ev: React.MouseEvent) => void; } export default class TextWithTooltip extends React.Component<IProps> { diff --git a/src/components/views/elements/UseCaseSelection.tsx b/src/components/views/elements/UseCaseSelection.tsx index 1e6cf09e75..2f7f171fad 100644 --- a/src/components/views/elements/UseCaseSelection.tsx +++ b/src/components/views/elements/UseCaseSelection.tsx @@ -40,7 +40,7 @@ export function UseCaseSelection({ onFinished }: Props): JSX.Element { onFinished(selection); }, TIMEOUT); return () => { - clearTimeout(handler); + if (handler !== null) clearTimeout(handler); handler = null; }; } diff --git a/src/components/views/elements/UseCaseSelectionButton.tsx b/src/components/views/elements/UseCaseSelectionButton.tsx index 13203b5864..5cfe26c734 100644 --- a/src/components/views/elements/UseCaseSelectionButton.tsx +++ b/src/components/views/elements/UseCaseSelectionButton.tsx @@ -28,7 +28,7 @@ interface Props { } export function UseCaseSelectionButton({ useCase, onClick, selected }: Props): JSX.Element { - let label: string; + let label: string | undefined; switch (useCase) { case UseCase.PersonalMessaging: label = _t("Friends and family"); diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index db51c46bae..b1101f41dc 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -43,7 +43,7 @@ interface IArgs<T, D = void> { } export interface IFieldState { - value: string; + value: string | null; focused: boolean; allowEmpty?: boolean; } diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index b6b8adfaa4..53cdf74b32 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -80,7 +80,7 @@ export default class DownloadActionButton extends React.PureComponent<IProps, IS } public render(): React.ReactNode { - let spinner: JSX.Element; + let spinner: JSX.Element | undefined; if (this.state.loading) { spinner = <Spinner w={18} h={18} />; } diff --git a/src/components/views/messages/EditHistoryMessage.tsx b/src/components/views/messages/EditHistoryMessage.tsx index ec10869a71..906867d6a8 100644 --- a/src/components/views/messages/EditHistoryMessage.tsx +++ b/src/components/views/messages/EditHistoryMessage.tsx @@ -47,7 +47,7 @@ interface IProps { interface IState { canRedact: boolean; - sendStatus: EventStatus; + sendStatus: EventStatus | null; } export default class EditHistoryMessage extends React.PureComponent<IProps, IState> { @@ -59,12 +59,10 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta super(props); const cli = MatrixClientPeg.get(); - const { userId } = cli.credentials; + const userId = cli.getSafeUserId(); const event = this.props.mxEvent; const room = cli.getRoom(event.getRoomId()); - if (event.localRedactionEvent()) { - event.localRedactionEvent().on(MatrixEventEvent.Status, this.onAssociatedStatusChanged); - } + event.localRedactionEvent()?.on(MatrixEventEvent.Status, this.onAssociatedStatusChanged); const canRedact = room.currentState.maySendRedactionForEvent(event, userId); this.state = { canRedact, sendStatus: event.getAssociatedStatus() }; } @@ -121,9 +119,7 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta unmountPills(this.pills); unmountTooltips(this.tooltips); const event = this.props.mxEvent; - if (event.localRedactionEvent()) { - event.localRedactionEvent().off(MatrixEventEvent.Status, this.onAssociatedStatusChanged); - } + event.localRedactionEvent()?.off(MatrixEventEvent.Status, this.onAssociatedStatusChanged); } public componentDidUpdate(): void { @@ -133,12 +129,12 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta private renderActionBar(): JSX.Element { // hide the button when already redacted - let redactButton: JSX.Element; + let redactButton: JSX.Element | undefined; if (!this.props.mxEvent.isRedacted() && !this.props.isBaseEvent && this.state.canRedact) { redactButton = <AccessibleButton onClick={this.onRedactClick}>{_t("Remove")}</AccessibleButton>; } - let viewSourceButton: JSX.Element; + let viewSourceButton: JSX.Element | undefined; if (SettingsStore.getValue("developerMode")) { viewSourceButton = ( <AccessibleButton onClick={this.onViewSourceClick}>{_t("View Source")}</AccessibleButton> @@ -189,9 +185,8 @@ export default class EditHistoryMessage extends React.PureComponent<IProps, ISta } const timestamp = formatTime(new Date(mxEvent.getTs()), this.props.isTwelveHour); - const isSending = ["sending", "queued", "encrypting"].indexOf(this.state.sendStatus) !== -1; - const classes = classNames({ - mx_EventTile: true, + const isSending = ["sending", "queued", "encrypting"].includes(this.state.sendStatus!); + const classes = classNames("mx_EventTile", { // Note: we keep the `sending` state class for tests, not for our styles mx_EventTile_sending: isSending, }); diff --git a/src/components/views/messages/JumpToDatePicker.tsx b/src/components/views/messages/JumpToDatePicker.tsx index ef6c39bd6f..fa13235809 100644 --- a/src/components/views/messages/JumpToDatePicker.tsx +++ b/src/components/views/messages/JumpToDatePicker.tsx @@ -22,7 +22,7 @@ import { RovingAccessibleButton, useRovingTabIndex } from "../../../accessibilit interface IProps { ts: number; - onDatePicked?: (dateString: string) => void; + onDatePicked: (dateString: string) => void; } const JumpToDatePicker: React.FC<IProps> = ({ ts, onDatePicked }: IProps) => { diff --git a/src/components/views/messages/LegacyCallEvent.tsx b/src/components/views/messages/LegacyCallEvent.tsx index 4678b1a2e0..a8ba902e61 100644 --- a/src/components/views/messages/LegacyCallEvent.tsx +++ b/src/components/views/messages/LegacyCallEvent.tsx @@ -165,7 +165,7 @@ export default class LegacyCallEvent extends React.PureComponent<IProps, IState> {this.props.timestamp} </div> ); - } else if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) { + } else if (!hangupReason || [CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) { // workaround for https://github.com/vector-im/element-web/issues/5178 // it seems Android randomly sets a reason of "user hangup" which is // interpreted as an error code :( diff --git a/src/components/views/messages/MBeaconBody.tsx b/src/components/views/messages/MBeaconBody.tsx index 363b69f7d4..3fa0b60696 100644 --- a/src/components/views/messages/MBeaconBody.tsx +++ b/src/components/views/messages/MBeaconBody.tsx @@ -140,7 +140,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati error?.message === LocationShareError.MapStyleUrlNotConfigured || error?.message === LocationShareError.MapStyleUrlNotReachable; const displayStatus = getBeaconDisplayStatus( - isLive, + !!isLive, latestLocationState, // if we are unable to display maps because it is not configured for the server // don't display an error @@ -174,7 +174,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati map = ( <Map id={mapId} - centerGeoUri={latestLocationState.uri} + centerGeoUri={latestLocationState?.uri} onError={setError} onClick={onClick} className="mx_MBeaconBody_map" @@ -184,7 +184,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati map={map} id={`${mapId}-marker`} geoUri={latestLocationState.uri} - roomMember={markerRoomMember} + roomMember={markerRoomMember ?? undefined} useMemberColor /> )} diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 67747fe1e2..f556b59cf7 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { ComponentProps, createRef } from "react"; +import React, { ComponentProps, createRef, ReactNode } from "react"; import { Blurhash } from "react-blurhash"; import classNames from "classnames"; import { CSSTransition, SwitchTransition } from "react-transition-group"; @@ -47,8 +47,8 @@ enum Placeholder { } interface IState { - contentUrl?: string; - thumbUrl?: string; + contentUrl: string | null; + thumbUrl: string | null; isAnimated?: boolean; error?: Error; imgError: boolean; @@ -78,6 +78,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { this.reconnectedListener = createReconnectedListener(this.clearError); this.state = { + contentUrl: null, + thumbUrl: null, imgError: false, imgLoaded: false, hover: false, @@ -126,7 +128,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { }; } - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true); } }; @@ -177,7 +179,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { this.setState({ imgLoaded: true, loadedImageDimensions }); }; - private getContentUrl(): string { + private getContentUrl(): string | null { // During export, the content url will point to the MSC, which will later point to a local url if (this.props.forExport) return this.media.srcMxc; return this.media.srcHttp; @@ -187,7 +189,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { return mediaFromContent(this.props.mxEvent.getContent()); } - private getThumbUrl(): string { + private getThumbUrl(): string | null { // FIXME: we let images grow as wide as you like, rather than capped to 800x600. // So either we need to support custom timeline widths here, or reimpose the cap, otherwise the // thumbnail resolution will be unnecessarily reduced. @@ -242,8 +244,8 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { private async downloadImage(): Promise<void> { if (this.state.contentUrl) return; // already downloaded - let thumbUrl: string; - let contentUrl: string; + let thumbUrl: string | null; + let contentUrl: string | null; if (this.props.mediaEventHelper.media.isEncrypted) { try { [contentUrl, thumbUrl] = await Promise.all([ @@ -276,7 +278,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { // If there is no included non-animated thumbnail then we will generate our own, we can't depend on the server // because 1. encryption and 2. we can't ask the server specifically for a non-animated thumbnail. if (isAnimated && !SettingsStore.getValue("autoplayGifs")) { - if (!thumbUrl || !content?.info.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) { + if (!thumbUrl || !content?.info?.thumbnail_info || mayBeAnimated(content.info.thumbnail_info.mimetype)) { const img = document.createElement("img"); const loadPromise = new Promise((resolve, reject) => { img.onload = resolve; @@ -364,7 +366,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } } - protected getBanner(content: IMediaEventContent): JSX.Element { + protected getBanner(content: IMediaEventContent): ReactNode { // Hide it for the threads list & the file panel where we show it as text anyway. if ( [TimelineRenderingType.ThreadsList, TimelineRenderingType.File].includes(this.context.timelineRenderingType) @@ -429,9 +431,9 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { forcedHeight ?? this.props.maxImageHeight, ); - let img: JSX.Element; - let placeholder: JSX.Element; - let gifLabel: JSX.Element; + let img: JSX.Element | undefined; + let placeholder: JSX.Element | undefined; + let gifLabel: JSX.Element | undefined; if (!this.props.forExport && !this.state.imgLoaded) { const classes = classNames("mx_MImageBody_placeholder", { @@ -471,7 +473,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; } - let banner: JSX.Element; + let banner: ReactNode | undefined; if (this.state.showImage && this.state.hover) { banner = this.getBanner(content); } @@ -526,7 +528,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } // Overridden by MStickerBody - protected getPlaceholder(width: number, height: number): JSX.Element { + protected getPlaceholder(width: number, height: number): ReactNode { const blurhash = this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]; if (blurhash) { @@ -540,12 +542,12 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } // Overridden by MStickerBody - protected getTooltip(): JSX.Element { + protected getTooltip(): ReactNode { return null; } // Overridden by MStickerBody - protected getFileBody(): string | JSX.Element { + protected getFileBody(): ReactNode { if (this.props.forExport) return null; /* * In the room timeline or the thread context we don't need the download @@ -577,7 +579,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> { } let contentUrl = this.state.contentUrl; - let thumbUrl: string; + let thumbUrl: string | undefined; if (this.props.forExport) { contentUrl = this.props.mxEvent.getContent().url ?? this.props.mxEvent.getContent().file?.url; thumbUrl = contentUrl; diff --git a/src/components/views/messages/MJitsiWidgetEvent.tsx b/src/components/views/messages/MJitsiWidgetEvent.tsx index 9eba08054e..50b180beed 100644 --- a/src/components/views/messages/MJitsiWidgetEvent.tsx +++ b/src/components/views/messages/MJitsiWidgetEvent.tsx @@ -41,7 +41,7 @@ export default class MJitsiWidgetEvent extends React.PureComponent<IProps> { const widgetId = this.props.mxEvent.getStateKey(); const widget = WidgetStore.instance.getRoom(room.roomId, true).widgets.find((w) => w.id === widgetId); - let joinCopy = _t("Join the conference at the top of this room"); + let joinCopy: string | null = _t("Join the conference at the top of this room"); if (widget && WidgetLayoutStore.instance.isInContainer(room, widget, Container.Right)) { joinCopy = _t("Join the conference from the room information card on the right"); } else if (!widget) { diff --git a/src/components/views/messages/MKeyVerificationRequest.tsx b/src/components/views/messages/MKeyVerificationRequest.tsx index 085ec60d72..8647a824ec 100644 --- a/src/components/views/messages/MKeyVerificationRequest.tsx +++ b/src/components/views/messages/MKeyVerificationRequest.tsx @@ -124,7 +124,7 @@ export default class MKeyVerificationRequest extends React.Component<IProps> { let title: string; let subtitle: string; - let stateNode: JSX.Element; + let stateNode: JSX.Element | undefined; if (!request.canAccept) { let stateLabel; diff --git a/src/components/views/messages/MLocationBody.tsx b/src/components/views/messages/MLocationBody.tsx index 3098ea2d5b..2932d361b8 100644 --- a/src/components/views/messages/MLocationBody.tsx +++ b/src/components/views/messages/MLocationBody.tsx @@ -37,7 +37,7 @@ import { IBodyProps } from "./IBodyProps"; import { createReconnectedListener } from "../../../utils/connection"; interface IState { - error: Error; + error?: Error; } export default class MLocationBody extends React.Component<IBodyProps, IState> { @@ -58,9 +58,7 @@ export default class MLocationBody extends React.Component<IBodyProps, IState> { this.reconnectedListener = createReconnectedListener(this.clearError); - this.state = { - error: undefined, - }; + this.state = {}; } private onClick = (): void => { @@ -149,7 +147,12 @@ export const LocationBodyContent: React.FC<LocationBodyContentProps> = ({ const mapElement = ( <Map id={mapId} centerGeoUri={geoUri} onClick={onClick} onError={onError} className="mx_MLocationBody_map"> {({ map }) => ( - <SmartMarker map={map} id={`${mapId}-marker`} geoUri={geoUri} roomMember={markerRoomMember} /> + <SmartMarker + map={map} + id={`${mapId}-marker`} + geoUri={geoUri} + roomMember={markerRoomMember ?? undefined} + /> )} </Map> ); diff --git a/src/components/views/messages/MPollBody.tsx b/src/components/views/messages/MPollBody.tsx index 62c53af518..e65a8f67e6 100644 --- a/src/components/views/messages/MPollBody.tsx +++ b/src/components/views/messages/MPollBody.tsx @@ -128,7 +128,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge PollCreateDialog, { room: MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), - threadId: mxEvent.getThread()?.id ?? null, + threadId: mxEvent.getThread()?.id, editingMxEvent: mxEvent, }, "mx_CompoundDialog", diff --git a/src/components/views/messages/MStickerBody.tsx b/src/components/views/messages/MStickerBody.tsx index 2aa5e16f80..4030b8e932 100644 --- a/src/components/views/messages/MStickerBody.tsx +++ b/src/components/views/messages/MStickerBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import MImageBody from "./MImageBody"; import { BLURHASH_FIELD } from "../../../utils/image-media"; @@ -33,7 +33,7 @@ export default class MStickerBody extends MImageBody { // MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding // which is added by mx_MStickerBody_wrapper protected wrapImage(contentUrl: string, children: React.ReactNode): JSX.Element { - let onClick = null; + let onClick: React.MouseEventHandler | undefined; if (!this.state.showImage) { onClick = this.onClick; } @@ -46,7 +46,7 @@ export default class MStickerBody extends MImageBody { } // Placeholder to show in place of the sticker image if img onLoad hasn't fired yet. - protected getPlaceholder(width: number, height: number): JSX.Element { + protected getPlaceholder(width: number, height: number): ReactNode { if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) return super.getPlaceholder(width, height); return ( <img @@ -61,7 +61,7 @@ export default class MStickerBody extends MImageBody { } // Tooltip to show on mouse over - protected getTooltip(): JSX.Element { + protected getTooltip(): ReactNode { const content = this.props.mxEvent && this.props.mxEvent.getContent(); if (!content || !content.body || !content.info || !content.info.w) return null; @@ -74,11 +74,11 @@ export default class MStickerBody extends MImageBody { } // Don't show "Download this_file.png ..." - protected getFileBody(): JSX.Element { + protected getFileBody(): ReactNode { return null; } - protected getBanner(content: IMediaEventContent): JSX.Element { + protected getBanner(content: IMediaEventContent): ReactNode { return null; // we don't need a banner, we have a tooltip } } diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 50623e2604..a7e60dfd5a 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import { decode } from "blurhash"; import { logger } from "matrix-js-sdk/src/logger"; @@ -31,13 +31,13 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex import MediaProcessingError from "./shared/MediaProcessingError"; interface IState { - decryptedUrl?: string; - decryptedThumbnailUrl?: string; - decryptedBlob?: Blob; + decryptedUrl: string | null; + decryptedThumbnailUrl: string | null; + decryptedBlob: Blob | null; error?: any; fetchingData: boolean; posterLoading: boolean; - blurhashUrl: string; + blurhashUrl: string | null; } export default class MVideoBody extends React.PureComponent<IBodyProps, IState> { @@ -61,21 +61,21 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState> }; } - private getContentUrl(): string | null { + private getContentUrl(): string | undefined { const content = this.props.mxEvent.getContent<IMediaEventContent>(); // During export, the content url will point to the MSC, which will later point to a local url - if (this.props.forExport) return content.file?.url || content.url; + if (this.props.forExport) return content.file?.url ?? content.url; const media = mediaFromContent(content); if (media.isEncrypted) { - return this.state.decryptedUrl; + return this.state.decryptedUrl ?? undefined; } else { - return media.srcHttp; + return media.srcHttp ?? undefined; } } private hasContentUrl(): boolean { const url = this.getContentUrl(); - return url && !url.startsWith("data:"); + return !!url && !url.startsWith("data:"); } private getThumbUrl(): string | null { @@ -227,7 +227,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState> ); } - private getFileBody = (): JSX.Element => { + private getFileBody = (): ReactNode => { if (this.props.forExport) return null; return this.showFileBody && <MFileBody {...this.props} showGenericPlaceholder={false} />; }; @@ -271,7 +271,7 @@ export default class MVideoBody extends React.PureComponent<IBodyProps, IState> const contentUrl = this.getContentUrl(); const thumbUrl = this.getThumbUrl(); - let poster = null; + let poster: string | undefined; let preload = "metadata"; if (content.info && thumbUrl) { poster = thumbUrl; diff --git a/src/components/views/messages/MessageActionBar.tsx b/src/components/views/messages/MessageActionBar.tsx index 1a7d7732a3..6ac1e74586 100644 --- a/src/components/views/messages/MessageActionBar.tsx +++ b/src/components/views/messages/MessageActionBar.tsx @@ -63,7 +63,7 @@ interface IOptionsButtonProps { mxEvent: MatrixEvent; // TODO: Types getTile: () => any | null; - getReplyChain: () => ReplyChain; + getReplyChain: () => ReplyChain | null; permalinkCreator: RoomPermalinkCreator; onFocusChange: (menuDisplayed: boolean) => void; getRelationsForEvent?: GetRelationsForEvent; @@ -97,10 +97,10 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({ [openMenu, onFocus], ); - let contextMenu: ReactElement | null; - if (menuDisplayed) { + let contextMenu: ReactElement | undefined; + if (menuDisplayed && button.current) { const tile = getTile && getTile(); - const replyChain = getReplyChain && getReplyChain(); + const replyChain = getReplyChain(); const buttonRect = button.current.getBoundingClientRect(); contextMenu = ( @@ -109,7 +109,7 @@ const OptionsButton: React.FC<IOptionsButtonProps> = ({ mxEvent={mxEvent} permalinkCreator={permalinkCreator} eventTileOps={tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined} - collapseReplyChain={replyChain && replyChain.canCollapse() ? replyChain.collapse : undefined} + collapseReplyChain={replyChain?.canCollapse() ? replyChain.collapse : undefined} onFinished={closeMenu} getRelationsForEvent={getRelationsForEvent} /> @@ -148,8 +148,8 @@ const ReactButton: React.FC<IReactButtonProps> = ({ mxEvent, reactions, onFocusC onFocusChange(menuDisplayed); }, [onFocusChange, menuDisplayed]); - let contextMenu; - if (menuDisplayed) { + let contextMenu: JSX.Element | undefined; + if (menuDisplayed && button.current) { const buttonRect = button.current.getBoundingClientRect(); contextMenu = ( <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} managed={false}> @@ -211,7 +211,7 @@ const ReplyInThreadButton: React.FC<IReplyInThreadButton> = ({ mxEvent }) => { if (mxEvent.getThread() && !mxEvent.isThreadRoot) { defaultDispatcher.dispatch<ShowThreadPayload>({ action: Action.ShowThread, - rootEvent: mxEvent.getThread().rootEvent, + rootEvent: mxEvent.getThread()!.rootEvent, initialEvent: mxEvent, scroll_into_view: true, highlighted: true, @@ -293,7 +293,7 @@ interface IMessageActionBarProps { reactions?: Relations | null | undefined; // TODO: Types getTile: () => any | null; - getReplyChain: () => ReplyChain | undefined; + getReplyChain: () => ReplyChain | null; permalinkCreator?: RoomPermalinkCreator; onFocusChange?: (menuDisplayed: boolean) => void; toggleThreadExpanded: () => void; @@ -421,7 +421,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction }; public render(): React.ReactNode { - const toolbarOpts = []; + const toolbarOpts: JSX.Element[] = []; if (canEditContent(this.props.mxEvent)) { toolbarOpts.push( <RovingAccessibleTooltipButton @@ -452,8 +452,8 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction // We show a different toolbar for failed events, so detect that first. const mxEvent = this.props.mxEvent; - const editStatus = mxEvent.replacingEvent() && mxEvent.replacingEvent().status; - const redactStatus = mxEvent.localRedactionEvent() && mxEvent.localRedactionEvent().status; + const editStatus = mxEvent.replacingEvent()?.status; + const redactStatus = mxEvent.localRedactionEvent()?.status; const allowCancel = canCancel(mxEvent.status) || canCancel(editStatus) || canCancel(redactStatus); const isFailed = [mxEvent.status, editStatus, redactStatus].includes(EventStatus.NOT_SENT); if (allowCancel && isFailed) { diff --git a/src/components/views/messages/MessageEvent.tsx b/src/components/views/messages/MessageEvent.tsx index a32747cd2e..7e2a8b8db9 100644 --- a/src/components/views/messages/MessageEvent.tsx +++ b/src/components/views/messages/MessageEvent.tsx @@ -58,7 +58,7 @@ interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper } export interface IOperableEventTile { - getEventTileOps(): IEventTileOps; + getEventTileOps(): IEventTileOps | null; } const baseBodyTypes = new Map<string, typeof React.Component>([ @@ -159,12 +159,12 @@ export default class MessageEvent extends React.Component<IProps> implements IMe if (this.props.mxEvent.isDecryptionFailure()) { BodyType = DecryptionFailureBody; } else if (type && this.evTypes.has(type)) { - BodyType = this.evTypes.get(type); + BodyType = this.evTypes.get(type)!; } else if (msgtype && this.bodyTypes.has(msgtype)) { - BodyType = this.bodyTypes.get(msgtype); + BodyType = this.bodyTypes.get(msgtype)!; } else if (content.url) { // Fallback to MFileBody if there's a content URL - BodyType = this.bodyTypes.get(MsgType.File); + BodyType = this.bodyTypes.get(MsgType.File)!; } else { // Fallback to UnknownBody otherwise if not redacted BodyType = UnknownBody; diff --git a/src/components/views/messages/ReactionsRow.tsx b/src/components/views/messages/ReactionsRow.tsx index 39cee90977..6d7b0b2d17 100644 --- a/src/components/views/messages/ReactionsRow.tsx +++ b/src/components/views/messages/ReactionsRow.tsx @@ -142,7 +142,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> { this.forceUpdate(); }; - private getMyReactions(): MatrixEvent[] { + private getMyReactions(): MatrixEvent[] | null { const reactions = this.props.reactions; if (!reactions) { return null; @@ -206,7 +206,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> { // Show the first MAX_ITEMS if there are MAX_ITEMS + 1 or more items. // The "+ 1" ensure that the "show all" reveals something that takes up // more space than the button itself. - let showAllButton: JSX.Element; + let showAllButton: JSX.Element | undefined; if (items.length > MAX_ITEMS_WHEN_LIMITED + 1 && !showAll) { items = items.slice(0, MAX_ITEMS_WHEN_LIMITED); showAllButton = ( diff --git a/src/components/views/messages/ReactionsRowButton.tsx b/src/components/views/messages/ReactionsRowButton.tsx index 38c1ca55e5..60bad283b7 100644 --- a/src/components/views/messages/ReactionsRowButton.tsx +++ b/src/components/views/messages/ReactionsRowButton.tsx @@ -106,9 +106,9 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta } const room = this.context.getRoom(mxEvent.getRoomId()); - let label: string; + let label: string | undefined; if (room) { - const senders = []; + const senders: string[] = []; for (const reactionEvent of reactionEvents) { const member = room.getMember(reactionEvent.getSender()); senders.push(member?.name || reactionEvent.getSender()); diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 4027c1ded4..ae82f40404 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -43,5 +43,7 @@ export default function SenderProfile({ mxEvent, onClick, withTooltip }: IProps) emphasizeDisplayName={true} withTooltip={withTooltip} /> - ) : null; + ) : ( + <></> + ); } diff --git a/src/components/views/messages/TextualBody.tsx b/src/components/views/messages/TextualBody.tsx index dc7cc4dc23..05f6da046d 100644 --- a/src/components/views/messages/TextualBody.tsx +++ b/src/components/views/messages/TextualBody.tsx @@ -342,7 +342,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { if (node.tagName === "SPAN" && typeof node.getAttribute("data-mx-spoiler") === "string") { const spoilerContainer = document.createElement("span"); - const reason = node.getAttribute("data-mx-spoiler"); + const reason = node.getAttribute("data-mx-spoiler") ?? undefined; node.removeAttribute("data-mx-spoiler"); // we don't want to recurse const spoiler = <Spoiler reason={reason} contentHtml={node.outerHTML} />; @@ -367,7 +367,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { const node = nodes[i]; if (node.tagName === "A" && node.getAttribute("href")) { if (this.isLinkPreviewable(node)) { - links.push(node.getAttribute("href")); + links.push(node.getAttribute("href")!); } } else if (node.tagName === "PRE" || node.tagName === "CODE" || node.tagName === "BLOCKQUOTE") { continue; @@ -380,7 +380,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { private isLinkPreviewable(node: Element): boolean { // don't try to preview relative links - if (!node.getAttribute("href").startsWith("http://") && !node.getAttribute("href").startsWith("https://")) { + const href = node.getAttribute("href") ?? ""; + if (!href.startsWith("http://") && !href.startsWith("https://")) { return false; } @@ -389,24 +390,24 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { // or from a full foo.bar/baz style schemeless URL) - or be a markdown-style // link, in which case we check the target text differs from the link value. // TODO: make this configurable? - if (node.textContent.indexOf("/") > -1) { + if (node.textContent?.includes("/")) { return true; + } + + const url = node.getAttribute("href"); + const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1]; + + // never preview permalinks (if anything we should give a smart + // preview of the room/user they point to: nobody needs to be reminded + // what the matrix.to site looks like). + if (isPermalinkHost(host)) return false; + + if (node.textContent?.toLowerCase().trim().startsWith(host.toLowerCase())) { + // it's a "foo.pl" style link + return false; } else { - const url = node.getAttribute("href"); - const host = url.match(/^https?:\/\/(.*?)(\/|$)/)[1]; - - // never preview permalinks (if anything we should give a smart - // preview of the room/user they point to: nobody needs to be reminded - // what the matrix.to site looks like). - if (isPermalinkHost(host)) return false; - - if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) { - // it's a "foo.pl" style link - return false; - } else { - // it's a [foo bar](http://foo.com) style link - return true; - } + // it's a [foo bar](http://foo.com) style link + return true; } } @@ -434,7 +435,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> { * to start with (e.g. pills, links in the content). */ private onBodyLinkClick = (e: MouseEvent): void => { - let target = e.target as HTMLLinkElement; + let target: HTMLLinkElement | null = e.target as HTMLLinkElement; // links processed by linkifyjs have their own handler so don't handle those here if (target.classList.contains(linkifyOpts.className as string)) return; if (target.nodeName !== "A") { diff --git a/src/components/views/messages/TileErrorBoundary.tsx b/src/components/views/messages/TileErrorBoundary.tsx index 220a1f0697..d87798a8e6 100644 --- a/src/components/views/messages/TileErrorBoundary.tsx +++ b/src/components/views/messages/TileErrorBoundary.tsx @@ -34,16 +34,14 @@ interface IProps { } interface IState { - error: Error; + error?: Error; } export default class TileErrorBoundary extends React.Component<IProps, IState> { public constructor(props: IProps) { super(props); - this.state = { - error: null, - }; + this.state = {}; } public static getDerivedStateFromError(error: Error): Partial<IState> { diff --git a/src/components/views/rooms/Autocomplete.tsx b/src/components/views/rooms/Autocomplete.tsx index ba26bf3a87..daf96fd508 100644 --- a/src/components/views/rooms/Autocomplete.tsx +++ b/src/components/views/rooms/Autocomplete.tsx @@ -117,7 +117,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> { // Hide the autocomplete box hide: true, }); - return Promise.resolve(null); + return Promise.resolve(); } let autocompleteDelay = SettingsStore.getValue("autocompleteDelay"); @@ -204,7 +204,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> { this.setSelection(1 + index); } - public onEscape(e: KeyboardEvent): boolean { + public onEscape(e: KeyboardEvent): boolean | undefined { const completionCount = this.countCompletions(); if (completionCount === 0) { // autocomplete is already empty, so don't preventDefault diff --git a/src/components/views/rooms/BasicMessageComposer.tsx b/src/components/views/rooms/BasicMessageComposer.tsx index b94923db3a..d251285816 100644 --- a/src/components/views/rooms/BasicMessageComposer.tsx +++ b/src/components/views/rooms/BasicMessageComposer.tsx @@ -132,7 +132,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> private _isCaretAtEnd: boolean; private lastCaret: DocumentOffset; - private lastSelection: ReturnType<typeof cloneSelection>; + private lastSelection: ReturnType<typeof cloneSelection> | null; private readonly useMarkdownHandle: string; private readonly emoticonSettingHandle: string; @@ -188,7 +188,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> } } - public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number { + public replaceEmoticon(caretPosition: DocumentPosition, regex: RegExp): number | undefined { const { model } = this.props; const range = model.startRange(caretPosition); // expand range max 9 characters backwards from caretPosition, @@ -352,7 +352,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> this.onCutCopy(event, "cut"); }; - private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean => { + private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => { event.preventDefault(); // we always handle the paste ourselves if (this.props.onPaste?.(event, this.props.model)) { // to prevent double handling, allow props.onPaste to skip internal onPaste @@ -415,7 +415,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> this.lastSelection = cloneSelection(document.getSelection()); } - private refreshLastCaretIfNeeded(): DocumentOffset { + private refreshLastCaretIfNeeded(): DocumentOffset | undefined { // XXX: needed when going up and down in editing messages ... not sure why yet // because the editors should stop doing this when when blurred ... // maybe it's on focus and the _editorRef isn't available yet or something. @@ -441,7 +441,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> } public isSelectionCollapsed(): boolean { - return !this.lastSelection || this.lastSelection.isCollapsed; + return !this.lastSelection || !!this.lastSelection.isCollapsed; } public isCaretAtStart(): boolean { @@ -537,7 +537,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> // there is no current autocomplete window, try to open it this.tabCompleteName(); handled = true; - } else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction)) { + } else if ([KeyBindingAction.Delete, KeyBindingAction.Backspace].includes(accessibilityAction!)) { this.formatBarRef.current.hide(); } @@ -750,7 +750,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> }; public render(): React.ReactNode { - let autoComplete; + let autoComplete: JSX.Element | undefined; if (this.state.autoComplete) { const query = this.state.query; const queryLen = query.length; @@ -785,7 +785,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> const { completionIndex } = this.state; const hasAutocomplete = Boolean(this.state.autoComplete); - let activeDescendant: string; + let activeDescendant: string | undefined; if (hasAutocomplete && completionIndex >= 0) { activeDescendant = generateCompletionDomId(completionIndex); } @@ -800,7 +800,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState> /> <div className={classes} - contentEditable={this.props.disabled ? null : true} + contentEditable={this.props.disabled ? undefined : true} tabIndex={0} onBlur={this.onBlur} onFocus={this.onFocus} diff --git a/src/components/views/rooms/E2EIcon.tsx b/src/components/views/rooms/E2EIcon.tsx index b93f87abc5..0103c308dd 100644 --- a/src/components/views/rooms/E2EIcon.tsx +++ b/src/components/views/rooms/E2EIcon.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { useState } from "react"; +import React, { CSSProperties, useState } from "react"; import classNames from "classnames"; import { _t, _td } from "../../../languageHandler"; @@ -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; @@ -77,13 +77,15 @@ const E2EIcon: React.FC<IProps> = ({ ); let e2eTitle: string | undefined; - if (isUser) { - e2eTitle = crossSigningUserTitles[status]; - } else { - e2eTitle = crossSigningRoomTitles[status]; + if (status) { + if (isUser) { + e2eTitle = crossSigningUserTitles[status]; + } else { + e2eTitle = crossSigningRoomTitles[status]; + } } - let style; + let style: CSSProperties | undefined; if (size) { style = { width: `${size}px`, height: `${size}px` }; } @@ -91,7 +93,7 @@ const E2EIcon: React.FC<IProps> = ({ const onMouseOver = (): void => setHover(true); const onMouseLeave = (): void => setHover(false); - let tip; + let tip: JSX.Element | undefined; if (hover && !hideTooltip) { tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />; } diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index 066d146f13..9d21284c27 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -47,6 +47,7 @@ import { getSlashCommand, isSlashCommand, runSlashCommand, shouldSendAnyway } fr import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; import { PosthogAnalytics } from "../../../PosthogAnalytics"; import { editorRoomKey, editorStateKey } from "../../../Editing"; +import DocumentOffset from "../../../editor/offset"; function getHtmlReplyFallback(mxEvent: MatrixEvent): string { const html = mxEvent.getContent().formatted_body; @@ -130,7 +131,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt private readonly editorRef = createRef<BasicMessageComposer>(); private readonly dispatcherRef: string; - private model: EditorModel = null; + private model: EditorModel; public constructor(props: IEditMessageComposerProps, context: React.ContextType<typeof RoomContext>) { super(props); @@ -250,7 +251,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt return localStorage.getItem(this.editorRoomKey) !== null; } - private restoreStoredEditorState(partCreator: PartCreator): Part[] { + private restoreStoredEditorState(partCreator: PartCreator): Part[] | undefined { const json = localStorage.getItem(this.editorStateKey); if (json) { try { @@ -382,7 +383,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt // editorstate so it can be restored when the remote echo event tile gets rendered // in case we're currently editing a pending event const sel = document.getSelection(); - let caret; + let caret: DocumentOffset | undefined; if (sel.focusNode) { caret = getCaretOffsetAndText(this.editorRef.current?.editorRef.current, sel).caret; } @@ -390,7 +391,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt // if caret is undefined because for some reason there isn't a valid selection, // then when mounting the editor again with the same editor state, // it will set the cursor at the end. - this.props.editState.setEditorState(caret, parts); + this.props.editState.setEditorState(caret ?? null, parts); window.removeEventListener("beforeunload", this.saveStoredEditorState); if (this.shouldSaveStoredEditorState) { this.saveStoredEditorState(); @@ -462,7 +463,7 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt model={this.model} room={this.getRoom()} threadId={this.props.editState?.getEvent()?.getThread()?.id} - initialCaret={this.props.editState.getCaret()} + initialCaret={this.props.editState.getCaret() ?? undefined} label={_t("Edit message")} onChange={this.onChange} /> diff --git a/src/components/views/rooms/EntityTile.tsx b/src/components/views/rooms/EntityTile.tsx index 14d4bbc45a..16523918aa 100644 --- a/src/components/views/rooms/EntityTile.tsx +++ b/src/components/views/rooms/EntityTile.tsx @@ -69,14 +69,14 @@ interface IProps { title?: string; avatarJsx?: JSX.Element; // <BaseAvatar /> className?: string; - presenceState?: PresenceState; - presenceLastActiveAgo?: number; - presenceLastTs?: number; + presenceState: PresenceState; + presenceLastActiveAgo: number; + presenceLastTs: number; presenceCurrentlyActive?: boolean; - showInviteButton?: boolean; + showInviteButton: boolean; onClick(): void; - suppressOnHover?: boolean; - showPresence?: boolean; + suppressOnHover: boolean; + showPresence: boolean; subtextLabel?: string; e2eStatus?: E2EState; powerStatus?: PowerStatus; diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c416c9005d..d8e02f2d9a 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -274,8 +274,6 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> verified: null, // The Relations model from the JS SDK for reactions to `mxEvent` reactions: this.getReactions(), - // Context menu position - contextMenu: null, hover: false, @@ -697,13 +695,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> }); }; - private renderE2EPadlock(): JSX.Element { + private renderE2EPadlock(): ReactNode { // if the event was edited, show the verification info for the edit, not // the original const ev = this.props.mxEvent.replacingEvent() ?? this.props.mxEvent; // no icon for local rooms - if (isLocalRoom(ev.getRoomId()!)) return; + if (isLocalRoom(ev.getRoomId()!)) return null; // event could not be decrypted if (ev.isDecryptionFailure()) { @@ -713,9 +711,9 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> // event is encrypted and not redacted, display padlock corresponding to whether or not it is verified if (ev.isEncrypted() && !ev.isRedacted()) { if (this.state.verified === E2EState.Normal) { - return; // no icon if we've not even cross-signed the user + return null; // no icon if we've not even cross-signed the user } else if (this.state.verified === E2EState.Verified) { - return; // no icon for verified + return null; // no icon for verified } else if (this.state.verified === E2EState.Unauthenticated) { return <E2ePadlockUnauthenticated />; } else if (this.state.verified === E2EState.Unknown) { @@ -729,16 +727,16 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> // else if room is encrypted // and event is being encrypted or is not_sent (Unknown Devices/Network Error) if (ev.status === EventStatus.ENCRYPTING) { - return; + return null; } if (ev.status === EventStatus.NOT_SENT) { - return; + return null; } if (ev.isState()) { - return; // we expect this to be unencrypted + return null; // we expect this to be unencrypted } if (ev.isRedacted()) { - return; // we expect this to be unencrypted + return null; // we expect this to be unencrypted } // if the event is not encrypted, but it's an e2e room, show the open padlock return <E2ePadlockUnencrypted />; @@ -752,16 +750,16 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> this.setState({ actionBarFocused }); }; - private getTile: () => IEventTileType = () => this.tile.current; + private getTile: () => IEventTileType | null = () => this.tile.current; - private getReplyChain = (): ReplyChain => this.replyChain.current; + private getReplyChain = (): ReplyChain | null => this.replyChain.current; - private getReactions = (): Relations => { + private getReactions = (): Relations | null => { if (!this.props.showReactions || !this.props.getRelationsForEvent) { return null; } const eventId = this.props.mxEvent.getId(); - return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction"); + return this.props.getRelationsForEvent(eventId, "m.annotation", "m.reaction") ?? null; }; private onReactionsCreated = (relationType: string, eventType: string): void => { @@ -795,7 +793,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> // Return if we're in a browser and click either an a tag or we have // selected text, as in those cases we want to use the native browser // menu - if (!PlatformPeg.get().allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return; + if (!PlatformPeg.get()?.allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return; // We don't want to show the menu when editing a message if (this.props.editState) return; @@ -817,7 +815,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> private onCloseMenu = (): void => { this.setState({ - contextMenu: null, + contextMenu: undefined, actionBarFocused: false, }); }; @@ -901,7 +899,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> this.props.mxEvent.getContent().msgtype === MsgType.Emote, }); - const isSending = ["sending", "queued", "encrypting"].indexOf(this.props.eventSendStatus) !== -1; + const isSending = ["sending", "queued", "encrypting"].includes(this.props.eventSendStatus!); const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); @@ -1110,7 +1108,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> const groupPadlock = !useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); const ircPadlock = useIRCLayout && !isBubbleMessage && this.renderE2EPadlock(); - let msgOption; + let msgOption: JSX.Element | undefined; if (this.props.showReadReceipts) { if (this.shouldShowSentReceipt || this.shouldShowSendingReceipt) { msgOption = <SentReceipt messageState={this.props.mxEvent.getAssociatedStatus()} />; @@ -1127,7 +1125,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState> } } - let replyChain; + let replyChain: JSX.Element | undefined; if ( haveRendererForEvent(this.props.mxEvent, this.context.showHiddenEvents) && shouldDisplayReply(this.props.mxEvent) @@ -1480,7 +1478,7 @@ class E2ePadlock extends React.Component<IE2ePadlockProps, IE2ePadlockState> { }; public render(): React.ReactNode { - let tooltip = null; + let tooltip: JSX.Element | undefined; if (this.state.hover) { tooltip = <Tooltip className="mx_EventTile_e2eIcon_tooltip" label={this.props.title} />; } @@ -1506,7 +1504,7 @@ function SentReceipt({ messageState }: ISentReceiptProps): JSX.Element { mx_EventTile_receiptSending: !isSent && !isFailed, }); - let nonCssBadge = null; + let nonCssBadge: JSX.Element | undefined; if (isFailed) { nonCssBadge = <NotificationBadge notification={StaticNotificationState.RED_EXCLAMATION} />; } diff --git a/src/components/views/rooms/HistoryTile.tsx b/src/components/views/rooms/HistoryTile.tsx index 6309f20366..c720a1b090 100644 --- a/src/components/views/rooms/HistoryTile.tsx +++ b/src/components/views/rooms/HistoryTile.tsx @@ -24,11 +24,11 @@ import { _t } from "../../../languageHandler"; const HistoryTile: React.FC = () => { const { room } = useContext(RoomContext); - const oldState = room.getLiveTimeline().getState(EventTimeline.BACKWARDS); - const encryptionState = oldState.getStateEvents("m.room.encryption")[0]; - const historyState = oldState.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; + const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); + const encryptionState = oldState?.getStateEvents("m.room.encryption")[0]; + const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; - let subtitle; + let subtitle: string | undefined; if (historyState == "invited") { subtitle = _t("You don't have permission to view messages from before you were invited."); } else if (historyState == "joined") { diff --git a/src/components/views/rooms/LinkPreviewGroup.tsx b/src/components/views/rooms/LinkPreviewGroup.tsx index ae32c6c166..e4a3df8c1d 100644 --- a/src/components/views/rooms/LinkPreviewGroup.tsx +++ b/src/components/views/rooms/LinkPreviewGroup.tsx @@ -54,7 +54,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH const showPreviews = expanded ? previews : previews.slice(0, INITIAL_NUM_PREVIEWS); - let toggleButton: JSX.Element; + let toggleButton: JSX.Element | undefined; if (previews.length > INITIAL_NUM_PREVIEWS) { toggleButton = ( <AccessibleButton onClick={toggleExpanded}> @@ -94,7 +94,7 @@ const LinkPreviewGroup: React.FC<IProps> = ({ links, mxEvent, onCancelClick, onH const fetchPreviews = (cli: MatrixClient, links: string[], ts: number): Promise<[string, IPreviewUrlResponse][]> => { return Promise.all<[string, IPreviewUrlResponse] | void>( - links.map(async (link): Promise<[string, IPreviewUrlResponse]> => { + links.map(async (link): Promise<[string, IPreviewUrlResponse] | undefined> => { try { const preview = await cli.getUrlPreview(link, ts); if (preview && Object.keys(preview).length > 0) { diff --git a/src/components/views/rooms/LinkPreviewWidget.tsx b/src/components/views/rooms/LinkPreviewWidget.tsx index 9618b5cd00..0172a61549 100644 --- a/src/components/views/rooms/LinkPreviewWidget.tsx +++ b/src/components/views/rooms/LinkPreviewWidget.tsx @@ -44,7 +44,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> { ev.preventDefault(); let src = p["og:image"]; - if (src && src.startsWith("mxc://")) { + if (src?.startsWith("mxc://")) { src = mediaFromMxc(src).srcHttp; } @@ -68,7 +68,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> { }; } - Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true); + Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true); }; public render(): React.ReactNode { @@ -78,7 +78,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> { } // FIXME: do we want to factor out all image displaying between this and MImageBody - especially for lightboxing? - let image = p["og:image"]; + let image: string | null = p["og:image"] ?? null; if (!SettingsStore.getValue("showImages")) { image = null; // Don't render a button to show the image, just hide it outright } @@ -99,7 +99,7 @@ export default class LinkPreviewWidget extends React.Component<IProps> { ); } - let img; + let img: JSX.Element | undefined; if (image) { img = ( <div className="mx_LinkPreviewWidget_image" style={{ height: thumbHeight }}> diff --git a/src/components/views/rooms/MemberList.tsx b/src/components/views/rooms/MemberList.tsx index 42811142d7..6c5a871a9a 100644 --- a/src/components/views/rooms/MemberList.tsx +++ b/src/components/views/rooms/MemberList.tsx @@ -123,7 +123,9 @@ export default class MemberList extends React.Component<IProps, IState> { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); - return room?.canInvite(cli.getUserId()) || (room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public); + return ( + !!room?.canInvite(cli.getSafeUserId()) || !!(room?.isSpaceRoom() && room.getJoinRule() === JoinRule.Public) + ); } private getMembersState(invitedMembers: Array<RoomMember>, joinedMembers: Array<RoomMember>): IState { @@ -276,7 +278,7 @@ export default class MemberList extends React.Component<IProps, IState> { }); }; - private getPending3PidInvites(): Array<MatrixEvent> { + private getPending3PidInvites(): MatrixEvent[] | undefined { // include 3pid invites (m.room.third_party_invite) state events. // The HS may have already converted these into m.room.member invites so // we shouldn't add them if the 3pid invite state key (token) is in the diff --git a/src/components/views/rooms/MemberTile.tsx b/src/components/views/rooms/MemberTile.tsx index d9fba0b029..cac793066b 100644 --- a/src/components/views/rooms/MemberTile.tsx +++ b/src/components/views/rooms/MemberTile.tsx @@ -41,7 +41,7 @@ interface IProps { interface IState { isRoomEncrypted: boolean; - e2eStatus: E2EState; + e2eStatus?: E2EState; } export default class MemberTile extends React.Component<IProps, IState> { @@ -57,7 +57,6 @@ export default class MemberTile extends React.Component<IProps, IState> { this.state = { isRoomEncrypted: false, - e2eStatus: null, }; } @@ -187,7 +186,7 @@ export default class MemberTile extends React.Component<IProps, IState> { public render(): React.ReactNode { const member = this.props.member; const name = this.getDisplayName(); - const presenceState = member.user?.presence ?? null; + const presenceState = member.user?.presence as PresenceState | undefined; const av = <MemberAvatar member={member} width={36} height={36} aria-hidden="true" />; @@ -222,7 +221,7 @@ export default class MemberTile extends React.Component<IProps, IState> { return ( <EntityTile {...this.props} - presenceState={presenceState as PresenceState | null} + presenceState={presenceState} presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0} presenceLastTs={member.user ? member.user.lastPresenceTs : 0} presenceCurrentlyActive={member.user ? member.user.currentlyActive : false} diff --git a/src/components/views/rooms/MessageComposerButtons.tsx b/src/components/views/rooms/MessageComposerButtons.tsx index b5faf878a6..30856cabc7 100644 --- a/src/components/views/rooms/MessageComposerButtons.tsx +++ b/src/components/views/rooms/MessageComposerButtons.tsx @@ -172,7 +172,7 @@ export const UploadButtonContext = createContext<UploadButtonFn | null>(null); interface IUploadButtonProps { roomId: string; - relation?: IEventRelation | null; + relation?: IEventRelation; children: ReactNode; } @@ -197,11 +197,11 @@ const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, rel }); const onUploadFileInputChange = (ev: React.ChangeEvent<HTMLInputElement>): void => { - if (ev.target.files.length === 0) return; + if (ev.target.files?.length === 0) return; // Take a copy, so we can safely reset the value of the form control ContentMessages.sharedInstance().sendContentListToRoom( - Array.from(ev.target.files), + Array.from(ev.target.files!), roomId, relation, cli, @@ -316,7 +316,7 @@ class PollButton extends React.PureComponent<IPollButtonProps> { }); } else { const threadId = - this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : null; + this.props.relation?.rel_type === THREAD_RELATION_TYPE.name ? this.props.relation.event_id : undefined; Modal.createDialog( PollCreateDialog, diff --git a/src/components/views/rooms/NewRoomIntro.tsx b/src/components/views/rooms/NewRoomIntro.tsx index 070999b4fd..7f2aa3ce3f 100644 --- a/src/components/views/rooms/NewRoomIntro.tsx +++ b/src/components/views/rooms/NewRoomIntro.tsx @@ -166,7 +166,7 @@ const NewRoomIntro: React.FC = () => { const creator = room.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); const creatorName = room?.getMember(creator)?.rawDisplayName || creator; - let createdText; + let createdText: string; if (creator === cli.getUserId()) { createdText = _t("You created this room."); } else { @@ -175,15 +175,15 @@ const NewRoomIntro: React.FC = () => { }); } - let parentSpace: Room; + let parentSpace: Room | undefined; if ( - SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getUserId()) && + SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getSafeUserId()) && SpaceStore.instance.isRoomInSpace(SpaceStore.instance.activeSpace, room.roomId) ) { parentSpace = SpaceStore.instance.activeSpaceRoom; } - let buttons; + let buttons: JSX.Element | undefined; if (parentSpace && shouldShowComponent(UIComponent.InviteUsers)) { buttons = ( <div className="mx_NewRoomIntro_buttons"> @@ -191,12 +191,12 @@ const NewRoomIntro: React.FC = () => { className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={() => { - showSpaceInvite(parentSpace); + showSpaceInvite(parentSpace!); }} > {_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })} </AccessibleButton> - {room.canInvite(cli.getUserId()) && ( + {room.canInvite(cli.getSafeUserId()) && ( <AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary_outline" @@ -209,7 +209,7 @@ const NewRoomIntro: React.FC = () => { )} </div> ); - } else if (room.canInvite(cli.getUserId()) && shouldShowComponent(UIComponent.InviteUsers)) { + } else if (room.canInvite(cli.getSafeUserId()) && shouldShowComponent(UIComponent.InviteUsers)) { buttons = ( <div className="mx_NewRoomIntro_buttons"> <AccessibleButton diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx index 968a49b8ca..51480df1fc 100644 --- a/src/components/views/rooms/NotificationBadge.tsx +++ b/src/components/views/rooms/NotificationBadge.tsx @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { MouseEvent } from "react"; +import React, { MouseEvent, ReactNode } from "react"; import SettingsStore from "../../../settings/SettingsStore"; import { XOR } from "../../../@types/common"; @@ -71,7 +71,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I ); } - private get roomId(): string { + private get roomId(): string | null { // We should convert this to null for safety with the SettingsStore return this.props.roomId || null; } @@ -110,7 +110,7 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I }); }; - public render(): React.ReactElement { + public render(): ReactNode { /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */ const { notification, showUnsentTooltip, forceCount, onClick } = this.props; @@ -119,8 +119,8 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I if (!notification.hasUnreadCount) return null; // Can't render a badge } - let label: string; - let tooltip: JSX.Element; + let label: string | undefined; + let tooltip: JSX.Element | undefined; if (showUnsentTooltip && this.state.showTooltip && notification.color === NotificationColor.Unsent) { label = _t("Message didn't send. Click for info."); tooltip = <Tooltip className="mx_RoleButton_tooltip" label={label} />; diff --git a/src/components/views/rooms/ReadReceiptGroup.tsx b/src/components/views/rooms/ReadReceiptGroup.tsx index 716d24fa33..3472ee8db3 100644 --- a/src/components/views/rooms/ReadReceiptGroup.tsx +++ b/src/components/views/rooms/ReadReceiptGroup.tsx @@ -42,9 +42,9 @@ export const READ_AVATAR_SIZE = 16; interface Props { readReceipts: IReadReceiptProps[]; readReceiptMap: { [userId: string]: IReadReceiptInfo }; - checkUnmounting: () => boolean; + checkUnmounting?: () => boolean; suppressAnimation: boolean; - isTwelveHour: boolean; + isTwelveHour?: boolean; } interface IAvatarPosition { @@ -169,8 +169,8 @@ export function ReadReceiptGroup({ ); } - let contextMenu; - if (menuDisplayed) { + let contextMenu: JSX.Element | undefined; + if (menuDisplayed && button.current) { const buttonRect = button.current.getBoundingClientRect(); contextMenu = ( <ContextMenu menuClassName="mx_ReadReceiptGroup_popup" onFinished={closeMenu} {...aboveLeftOf(buttonRect)}> @@ -226,7 +226,7 @@ export function ReadReceiptGroup({ } interface ReadReceiptPersonProps extends IReadReceiptProps { - isTwelveHour: boolean; + isTwelveHour?: boolean; onAfterClick?: () => void; } diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index 1a47719f58..e5e2fafdd7 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -48,7 +48,7 @@ interface IProps { suppressAnimation?: boolean; // an opaque object for storing information about this user's RR in this room - readReceiptInfo: IReadReceiptInfo; + readReceiptInfo?: IReadReceiptInfo; // A function which is used to check if the parent panel is being // unmounted, to avoid unnecessary work. Should return true if we diff --git a/src/components/views/rooms/RoomPreviewBar.tsx b/src/components/views/rooms/RoomPreviewBar.tsx index a968bd3794..c3f75c1f23 100644 --- a/src/components/views/rooms/RoomPreviewBar.tsx +++ b/src/components/views/rooms/RoomPreviewBar.tsx @@ -215,9 +215,9 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> { if (!myMember) { return {}; } - const kickerMember = this.props.room.currentState.getMember(myMember.events.member.getSender()); + const kickerMember = this.props.room?.currentState.getMember(myMember.events.member.getSender()); const memberName = kickerMember ? kickerMember.name : myMember.events.member.getSender(); - const reason = myMember.events.member.getContent().reason; + const reason = myMember.events.member?.getContent().reason; return { memberName, reason }; } @@ -252,8 +252,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> { if (!myMember) { return false; } - const memberEvent = myMember.events.member; - const memberContent = memberEvent.getContent(); + const memberContent = myMember.events.member.getContent(); return memberContent.membership === "invite" && memberContent.is_direct; } @@ -397,7 +396,7 @@ export default class RoomPreviewBar extends React.Component<IProps, IState> { const errCodeMessage = _t( "An error (%(errcode)s) was returned while trying to validate your " + "invite. You could try to pass this information on to the person who invited you.", - { errcode: this.state.threePidFetchError.errcode || _t("unknown error code") }, + { errcode: this.state.threePidFetchError?.errcode || _t("unknown error code") }, ); switch (joinRule) { case "invite": diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx index 86e1f28ff1..87166c94a1 100644 --- a/src/components/views/rooms/RoomTile.tsx +++ b/src/components/views/rooms/RoomTile.tsx @@ -204,7 +204,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> { private async generatePreview(): Promise<void> { if (!this.showMessagePreview) { - return null; + return; } const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(this.props.room, this.props.tag); diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts index d03f45bcc5..30c8a3c491 100644 --- a/src/settings/SettingsStore.ts +++ b/src/settings/SettingsStore.ts @@ -511,7 +511,7 @@ export default class SettingsStore { * check at. * @return {boolean} True if the user may set the setting, false otherwise. */ - public static canSetValue(settingName: string, roomId: string, level: SettingLevel): boolean { + public static canSetValue(settingName: string, roomId: string | null, level: SettingLevel): boolean { // Verify that the setting is actually a setting if (!SETTINGS[settingName]) { throw new Error("Setting '" + settingName + "' does not appear to be a setting."); diff --git a/src/utils/EditorStateTransfer.ts b/src/utils/EditorStateTransfer.ts index a61e328d8c..1604d22530 100644 --- a/src/utils/EditorStateTransfer.ts +++ b/src/utils/EditorStateTransfer.ts @@ -30,7 +30,7 @@ export default class EditorStateTransfer { public constructor(private readonly event: MatrixEvent) {} - public setEditorState(caret: DocumentOffset, serializedParts: SerializedPart[]): void { + public setEditorState(caret: DocumentOffset | null, serializedParts: SerializedPart[]): void { this.caret = caret; this.serializedParts = serializedParts; } diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index c33a90cc86..6f1b1a64fa 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -263,7 +263,7 @@ export function editEvent( } } -export function canCancel(status: EventStatus): boolean { +export function canCancel(status?: EventStatus | null): boolean { return status === EventStatus.QUEUED || status === EventStatus.NOT_SENT || status === EventStatus.ENCRYPTING; } diff --git a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx index 4f7441f59a..6b57422974 100644 --- a/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx +++ b/test/components/views/settings/tabs/user/PreferencesUserSettingsTab-test.tsx @@ -80,7 +80,7 @@ describe("PreferencesUserSettingsTab", () => { await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false")); fireEvent.click(toggle); - expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true); + expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true); }); it("can be disabled", async () => { @@ -89,7 +89,7 @@ describe("PreferencesUserSettingsTab", () => { await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false")); fireEvent.click(toggle); - expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, false); + expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, false); }); }); @@ -104,7 +104,7 @@ describe("PreferencesUserSettingsTab", () => { await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false")); fireEvent.click(toggle); - expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true); + expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true); }); it("cannot be disabled", async () => {