Conform more of the codebase to strictNullChecks (#10573)

* Conform more of the codebase to `strictNullChecks`

* Iterate
This commit is contained in:
Michael Telatynski 2023-04-13 08:52:57 +01:00 committed by GitHub
parent b4d7f6b592
commit 605ef084ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 119 additions and 104 deletions

View file

@ -28,9 +28,16 @@ limitations under the License.
* consume in the timeline, when performing scroll offset calculations * consume in the timeline, when performing scroll offset calculations
* (e.g. scroll locking) * (e.g. scroll locking)
*/ */
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number): number;
export function thumbHeight( export function thumbHeight(
fullWidth: number, fullWidth: number | undefined,
fullHeight: number, fullHeight: number | undefined,
thumbWidth: number,
thumbHeight: number,
): null;
export function thumbHeight(
fullWidth: number | undefined,
fullHeight: number | undefined,
thumbWidth: number, thumbWidth: number,
thumbHeight: number, thumbHeight: number,
): number | null { ): number | null {

View file

@ -23,7 +23,7 @@ type DynamicHtmlElementProps<T extends keyof JSX.IntrinsicElements> =
type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], "ref">>; type DynamicElementProps<T extends keyof JSX.IntrinsicElements> = Partial<Omit<JSX.IntrinsicElements[T], "ref">>;
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElementProps<T>, "onScroll"> & { export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElementProps<T>, "onScroll"> & {
element?: T; element: T;
className?: string; className?: string;
onScroll?: (event: Event) => void; onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void; onWheel?: (event: WheelEvent) => void;

View file

@ -19,7 +19,8 @@ import React, { createRef } from "react";
import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar"; import AutoHideScrollbar, { IProps as AutoHideScrollbarProps } from "./AutoHideScrollbar";
import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import UIStore, { UI_EVENTS } from "../../stores/UIStore";
export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "onWheel"> & { export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<AutoHideScrollbarProps<T>, "onWheel" | "element"> & {
element?: T;
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning // and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
// by the parent element. // by the parent element.

View file

@ -90,11 +90,13 @@ export default class LeftPanel extends React.Component<IProps, IState> {
} }
public componentDidMount(): void { public componentDidMount(): void {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); if (this.listContainerRef.current) {
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
// Using the passive option to not block the main thread
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
this.listContainerRef.current.addEventListener("scroll", this.onScroll, { passive: true });
}
UIStore.instance.on("ListContainer", this.refreshStickyHeaders); UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
// Using the passive option to not block the main thread
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
this.listContainerRef.current?.addEventListener("scroll", this.onScroll, { passive: true });
} }
public componentWillUnmount(): void { public componentWillUnmount(): void {

View file

@ -177,9 +177,9 @@ export default class LegacyCallEventGrouper extends EventEmitter {
} }
private setState = (): void => { private setState = (): void => {
if (CONNECTING_STATES.includes(this.call?.state)) { if (this.call && CONNECTING_STATES.includes(this.call.state)) {
this.state = CallState.Connecting; this.state = CallState.Connecting;
} else if (SUPPORTED_STATES.includes(this.call?.state)) { } else if (this.call && SUPPORTED_STATES.includes(this.call.state)) {
this.state = this.call.state; this.state = this.call.state;
} else { } else {
if (this.callWasMissed) this.state = CustomCallState.Missed; if (this.callWasMissed) this.state = CustomCallState.Missed;

View file

@ -471,11 +471,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
); );
}, 1000); }, 1000);
private getFallbackHsUrl(): string | null { private getFallbackHsUrl(): string | undefined {
if (this.props.serverConfig?.isDefault) { if (this.props.serverConfig?.isDefault) {
return this.props.config.fallback_hs_url ?? null; return this.props.config.fallback_hs_url;
} else {
return null;
} }
} }
@ -577,7 +575,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (payload.event_type === "m.identity_server") { if (payload.event_type === "m.identity_server") {
const fullUrl = payload.event_content ? payload.event_content["base_url"] : null; const fullUrl = payload.event_content ? payload.event_content["base_url"] : null;
if (!fullUrl) { if (!fullUrl) {
MatrixClientPeg.get().setIdentityServerUrl(null); MatrixClientPeg.get().setIdentityServerUrl(undefined);
localStorage.removeItem("mx_is_access_token"); localStorage.removeItem("mx_is_access_token");
localStorage.removeItem("mx_is_url"); localStorage.removeItem("mx_is_url");
} else { } else {
@ -1229,6 +1227,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
* @returns {string} The room ID of the new room, or null if no room was created * @returns {string} The room ID of the new room, or null if no room was created
*/ */
private async startWelcomeUserChat(): Promise<string | null> { private async startWelcomeUserChat(): Promise<string | null> {
const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config);
const welcomeUserId = snakedConfig.get("welcome_user_id");
if (!welcomeUserId) return null;
// We can end up with multiple tabs post-registration where the user // We can end up with multiple tabs post-registration where the user
// might then end up with a session and we don't want them all making // might then end up with a session and we don't want them all making
// a chat with the welcome user: try to de-dupe. // a chat with the welcome user: try to de-dupe.
@ -1242,8 +1244,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
await waitFor; await waitFor;
const snakedConfig = new SnakedObject<IConfigOptions>(this.props.config); const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(welcomeUserId);
const welcomeUserRooms = DMRoomMap.shared().getDMRoomsForUserId(snakedConfig.get("welcome_user_id"));
if (welcomeUserRooms.length === 0) { if (welcomeUserRooms.length === 0) {
const roomId = await createRoom({ const roomId = await createRoom({
dmUserId: snakedConfig.get("welcome_user_id"), dmUserId: snakedConfig.get("welcome_user_id"),
@ -1260,7 +1261,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// user room (it doesn't wait for new data from the server, just // user room (it doesn't wait for new data from the server, just
// the saved sync to be loaded). // the saved sync to be loaded).
const saveWelcomeUser = (ev: MatrixEvent): void => { const saveWelcomeUser = (ev: MatrixEvent): void => {
if (ev.getType() === EventType.Direct && ev.getContent()[snakedConfig.get("welcome_user_id")]) { if (ev.getType() === EventType.Direct && ev.getContent()[welcomeUserId]) {
MatrixClientPeg.get().store.save(true); MatrixClientPeg.get().store.save(true);
MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser); MatrixClientPeg.get().removeListener(ClientEvent.AccountData, saveWelcomeUser);
} }

View file

@ -869,7 +869,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
const receiptsByEvent: Map<string, IReadReceiptProps[]> = new Map(); const receiptsByEvent: Map<string, IReadReceiptProps[]> = new Map();
const receiptsByUserId: Map<string, IReadReceiptForUser> = new Map(); const receiptsByUserId: Map<string, IReadReceiptForUser> = new Map();
let lastShownEventId: string; let lastShownEventId: string | undefined;
for (const event of this.props.events) { for (const event of this.props.events) {
if (this.shouldShowEvent(event)) { if (this.shouldShowEvent(event)) {
lastShownEventId = event.getId(); lastShownEventId = event.getId();
@ -1018,11 +1018,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
let ircResizer: JSX.Element | undefined; let ircResizer: JSX.Element | undefined;
if (this.props.layout == Layout.IRC) { if (this.props.layout == Layout.IRC) {
ircResizer = ( ircResizer = (
<IRCTimelineProfileResizer <IRCTimelineProfileResizer minWidth={20} maxWidth={600} roomId={this.props.room?.roomId ?? null} />
minWidth={20}
maxWidth={600}
roomId={this.props.room ? this.props.room.roomId : null}
/>
); );
} }
@ -1206,7 +1202,7 @@ class CreationGrouper extends BaseGrouper {
let summaryText: string; let summaryText: string;
const roomId = ev.getRoomId(); const roomId = ev.getRoomId();
const creator = ev.sender?.name ?? ev.getSender(); const creator = ev.sender?.name ?? ev.getSender();
if (DMRoomMap.shared().getUserIdForRoomId(roomId)) { if (roomId && DMRoomMap.shared().getUserIdForRoomId(roomId)) {
summaryText = _t("%(creator)s created this DM.", { creator }); summaryText = _t("%(creator)s created this DM.", { creator });
} else { } else {
summaryText = _t("%(creator)s created and configured the room.", { creator }); summaryText = _t("%(creator)s created and configured the room.", { creator });

View file

@ -141,7 +141,7 @@ export default class RightPanel extends React.Component<IProps, IState> {
// When the user clicks close on the encryption panel cancel the pending request first if any // When the user clicks close on the encryption panel cancel the pending request first if any
this.state.cardState.verificationRequest.cancel(); this.state.cardState.verificationRequest.cancel();
} else { } else {
RightPanelStore.instance.togglePanel(this.props.room?.roomId); RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null);
} }
}; };

View file

@ -1597,6 +1597,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}; };
private injectSticker(url: string, info: object, text: string, threadId: string | null): void { private injectSticker(url: string, info: object, text: string, threadId: string | null): void {
if (!this.context.client) return;
if (this.context.client.isGuest()) { if (this.context.client.isGuest()) {
dis.dispatch({ action: "require_registration" }); dis.dispatch({ action: "require_registration" });
return; return;

View file

@ -189,7 +189,7 @@ export default class ScrollPanel extends React.Component<IProps> {
// Is that next fill request scheduled because of a props update? // Is that next fill request scheduled because of a props update?
private pendingFillDueToPropsUpdate: boolean; private pendingFillDueToPropsUpdate: boolean;
private scrollState: IScrollState; private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState; private preventShrinkingState: IPreventShrinkingState | null;
private unfillDebouncer: number | null; private unfillDebouncer: number | null;
private bottomGrowth: number; private bottomGrowth: number;
private minListHeight: number; private minListHeight: number;
@ -676,7 +676,7 @@ export default class ScrollPanel extends React.Component<IProps> {
debuglog("unable to save scroll state: found no children in the viewport"); debuglog("unable to save scroll state: found no children in the viewport");
return; return;
} }
const scrollToken = node!.dataset.scrollTokens.split(",")[0]; const scrollToken = node!.dataset.scrollTokens?.split(",")[0];
debuglog("saving anchored scroll state to message", scrollToken); debuglog("saving anchored scroll state to message", scrollToken);
const bottomOffset = this.topFromBottom(node); const bottomOffset = this.topFromBottom(node);
this.scrollState = { this.scrollState = {

View file

@ -50,7 +50,7 @@ const debuglog = (...args: any[]): void => {
interface IProps { interface IProps {
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
defaultDeviceDisplayName: string; defaultDeviceDisplayName?: string;
email?: string; email?: string;
brand?: string; brand?: string;
clientSecret?: string; clientSecret?: string;

View file

@ -53,7 +53,7 @@ const ConfirmSpaceUserActionDialog: React.FC<IProps> = ({
const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]); const [roomsToLeave, setRoomsToLeave] = useState<Room[]>([]);
const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]); const selectedRooms = useMemo(() => new Set(roomsToLeave), [roomsToLeave]);
let warning: JSX.Element; let warning: JSX.Element | undefined;
if (warningMessage) { if (warningMessage) {
warning = <div className="mx_ConfirmSpaceUserActionDialog_warning">{warningMessage}</div>; warning = <div className="mx_ConfirmSpaceUserActionDialog_warning">{warningMessage}</div>;
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useRef, useState } from "react"; import React, { RefObject, useRef, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -41,9 +41,9 @@ const CreateSubspaceDialog: React.FC<IProps> = ({ space, onAddExistingSpaceClick
const [busy, setBusy] = useState<boolean>(false); const [busy, setBusy] = useState<boolean>(false);
const [name, setName] = useState(""); const [name, setName] = useState("");
const spaceNameField = useRef<Field>(); const spaceNameField = useRef() as RefObject<Field>;
const [alias, setAlias] = useState(""); const [alias, setAlias] = useState("");
const spaceAliasField = useRef<RoomAliasField>(); const spaceAliasField = useRef() as RefObject<RoomAliasField>;
const [avatar, setAvatar] = useState<File | undefined>(); const [avatar, setAvatar] = useState<File | undefined>();
const [topic, setTopic] = useState<string>(""); const [topic, setTopic] = useState<string>("");

View file

@ -26,6 +26,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { filterBoolean } from "../../../utils/arrays";
interface IProps { interface IProps {
room: Room; room: Room;
@ -65,7 +66,7 @@ const Entry: React.FC<{
)} )}
</div> </div>
<StyledCheckbox <StyledCheckbox
onChange={onChange ? (e) => onChange(e.target.checked) : null} onChange={onChange ? (e) => onChange(e.target.checked) : undefined}
checked={checked} checked={checked}
disabled={!onChange} disabled={!onChange}
/> />
@ -80,7 +81,7 @@ const addAllParents = (set: Set<Room>, room: Room): void => {
); );
parents.forEach((parent) => { parents.forEach((parent) => {
if (set.has(parent)) return; if (!parent || set.has(parent)) return;
set.add(parent); set.add(parent);
addAllParents(set, parent); addAllParents(set, parent);
}); });
@ -97,8 +98,8 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
addAllParents(parents, room); addAllParents(parents, room);
return [ return [
Array.from(parents), Array.from(parents),
selected filterBoolean(
.map((roomId) => { selected.map((roomId) => {
const room = cli.getRoom(roomId); const room = cli.getRoom(roomId);
if (!room) { if (!room) {
return { roomId, name: roomId } as Room; return { roomId, name: roomId } as Room;
@ -106,8 +107,8 @@ const ManageRestrictedJoinRuleDialog: React.FC<IProps> = ({ room, selected = [],
if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) { if (room.getMyMembership() !== "join" || !room.isSpaceRoom()) {
return room; return room;
} }
}) }),
.filter(Boolean), ),
]; ];
}, [cli, selected, room]); }, [cli, selected, room]);

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import { SyntheticEvent, useRef, useState } from "react"; import { RefObject, SyntheticEvent, useRef, useState } from "react";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
import Field from "../elements/Field"; import Field from "../elements/Field";
@ -30,7 +30,7 @@ interface IProps {
const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => { const RegistrationEmailPromptDialog: React.FC<IProps> = ({ onFinished }) => {
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const fieldRef = useRef<Field>(); const fieldRef = useRef() as RefObject<Field>;
const onSubmit = async (e: SyntheticEvent): Promise<void> => { const onSubmit = async (e: SyntheticEvent): Promise<void> => {
e.preventDefault(); e.preventDefault();

View file

@ -288,8 +288,8 @@ interface IDirectoryOpts {
} }
const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = null, onFinished }) => { const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = null, onFinished }) => {
const inputRef = useRef<HTMLInputElement>(); const inputRef = useRef() as RefObject<HTMLInputElement>;
const scrollContainerRef = useRef<HTMLDivElement>(); const scrollContainerRef = useRef() as RefObject<HTMLDivElement>;
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const rovingContext = useContext(RovingTabIndexContext); const rovingContext = useContext(RovingTabIndexContext);
const [query, _setQuery] = useState(initialText); const [query, _setQuery] = useState(initialText);

View file

@ -38,7 +38,7 @@ interface IProps {
// The text to show as the summary of this event list // The text to show as the summary of this event list
"summaryText"?: ReactNode; "summaryText"?: ReactNode;
// An array of EventTiles to render when expanded // An array of EventTiles to render when expanded
"children": ReactNode[]; "children": ReactNode[] | null;
// Called when the event list expansion is toggled // Called when the event list expansion is toggled
onToggle?(): void; onToggle?(): void;
// The layout currently used // The layout currently used

View file

@ -45,7 +45,11 @@ interface IProps {
} }
interface IState { interface IState {
apps: Partial<{ [id in Container]: IApp[] }>; apps: {
[Container.Top]: IApp[];
[Container.Center]: IApp[];
[Container.Right]?: IApp[];
};
resizingVertical: boolean; // true when changing the height of the apps drawer resizingVertical: boolean; // true when changing the height of the apps drawer
resizingHorizontal: boolean; // true when changing the distribution of the width between widgets resizingHorizontal: boolean; // true when changing the distribution of the width between widgets
resizing: boolean; resizing: boolean;
@ -203,12 +207,10 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
} }
}; };
private getApps = (): Partial<{ [id in Container]: IApp[] }> => { private getApps = (): IState["apps"] => ({
const appsDict: Partial<{ [id in Container]: IApp[] }> = {}; [Container.Top]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top),
appsDict[Container.Top] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Top); [Container.Center]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center),
appsDict[Container.Center] = WidgetLayoutStore.instance.getContainerWidgets(this.props.room, Container.Center); });
return appsDict;
};
private topApps = (): IApp[] => this.state.apps[Container.Top]; private topApps = (): IApp[] => this.state.apps[Container.Top];
private centerApps = (): IApp[] => this.state.apps[Container.Center]; private centerApps = (): IApp[] => this.state.apps[Container.Center];
@ -348,7 +350,7 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
resizeNotifier.notifyTimelineHeightChanged(); resizeNotifier.notifyTimelineHeightChanged();
}} }}
onResizeStop={(e, dir, ref, d) => { onResizeStop={(e, dir, ref, d) => {
let newHeight = defaultHeight + d.height; let newHeight = defaultHeight! + d.height;
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100; newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight); WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);

View file

@ -231,6 +231,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => { private updateEditorState = (selection: Caret, inputType?: string, diff?: IDiff): void => {
if (!this.editorRef.current) return;
renderModel(this.editorRef.current, this.props.model); renderModel(this.editorRef.current, this.props.model);
if (selection) { if (selection) {
// set the caret/selection // set the caret/selection
@ -358,6 +359,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => { private onPaste = (event: ClipboardEvent<HTMLDivElement>): boolean | undefined => {
event.preventDefault(); // we always handle the paste ourselves event.preventDefault(); // we always handle the paste ourselves
if (!this.editorRef.current) return;
if (this.props.onPaste?.(event, this.props.model)) { if (this.props.onPaste?.(event, this.props.model)) {
// to prevent double handling, allow props.onPaste to skip internal onPaste // to prevent double handling, allow props.onPaste to skip internal onPaste
return true; return true;
@ -377,7 +379,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
this.modifiedFlag = true; this.modifiedFlag = true;
const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()); const range = getRangeForSelection(this.editorRef.current, model, document.getSelection()!);
// If the user is pasting a link, and has a range selected which is not a link, wrap the range with the link // If the user is pasting a link, and has a range selected which is not a link, wrap the range with the link
if (plainText && range.length > 0 && linkify.test(plainText) && !linkify.test(range.text)) { if (plainText && range.length > 0 && linkify.test(plainText) && !linkify.test(range.text)) {
@ -388,18 +390,20 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}; };
private onInput = (event: Partial<InputEvent>): void => { private onInput = (event: Partial<InputEvent>): void => {
if (!this.editorRef.current) return;
// ignore any input while doing IME compositions // ignore any input while doing IME compositions
if (this.isIMEComposing) { if (this.isIMEComposing) {
return; return;
} }
this.modifiedFlag = true; this.modifiedFlag = true;
const sel = document.getSelection(); const sel = document.getSelection()!;
const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel); const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel);
this.props.model.update(text, event.inputType, caret); this.props.model.update(text, event.inputType, caret);
}; };
private insertText(textToInsert: string, inputType = "insertText"): void { private insertText(textToInsert: string, inputType = "insertText"): void {
const sel = document.getSelection(); if (!this.editorRef.current) return;
const sel = document.getSelection()!;
const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel); const { caret, text } = getCaretOffsetAndText(this.editorRef.current, sel);
const newText = text.slice(0, caret.offset) + textToInsert + text.slice(caret.offset); const newText = text.slice(0, caret.offset) + textToInsert + text.slice(caret.offset);
caret.offset += textToInsert.length; caret.offset += textToInsert.length;
@ -468,6 +472,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}; };
private onSelectionChange = (): void => { private onSelectionChange = (): void => {
if (!this.editorRef.current) return;
const { isEmpty } = this.props.model; const { isEmpty } = this.props.model;
this.refreshLastCaretIfNeeded(); this.refreshLastCaretIfNeeded();
@ -486,6 +491,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
}; };
private onKeyDown = (event: React.KeyboardEvent): void => { private onKeyDown = (event: React.KeyboardEvent): void => {
if (!this.editorRef.current) return;
const model = this.props.model; const model = this.props.model;
let handled = false; let handled = false;
@ -497,7 +503,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const selectionRange = getRangeForSelection( const selectionRange = getRangeForSelection(
this.editorRef.current, this.editorRef.current,
this.props.model, this.props.model,
document.getSelection(), document.getSelection()!,
); );
// trim the range as we want it to exclude leading/trailing spaces // trim the range as we want it to exclude leading/trailing spaces
selectionRange.trim(); selectionRange.trim();
@ -745,11 +751,11 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
} }
public onFormatAction = (action: Formatting): void => { public onFormatAction = (action: Formatting): void => {
if (!this.state.useMarkdown) { if (!this.state.useMarkdown || !this.editorRef.current) {
return; return;
} }
const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()); const range: Range = getRangeForSelection(this.editorRef.current, this.props.model, document.getSelection()!);
this.historyManager.ensureLastChangesPushed(this.props.model); this.historyManager.ensureLastChangesPushed(this.props.model);
this.modifiedFlag = true; this.modifiedFlag = true;

View file

@ -238,9 +238,11 @@ class EditMessageComposer extends React.Component<IEditMessageComposerProps, ISt
private get events(): MatrixEvent[] { private get events(): MatrixEvent[] {
const liveTimelineEvents = this.context.liveTimeline?.getEvents(); const liveTimelineEvents = this.context.liveTimeline?.getEvents();
const pendingEvents = this.getRoom()?.getPendingEvents(); const room = this.getRoom();
if (!liveTimelineEvents || !room) return [];
const pendingEvents = room.getPendingEvents();
const isInThread = Boolean(this.props.editState.getEvent().getThread()); const isInThread = Boolean(this.props.editState.getEvent().getThread());
return liveTimelineEvents?.concat(isInThread ? [] : pendingEvents) ?? []; return liveTimelineEvents.concat(isInThread ? [] : pendingEvents);
} }
private cancelEdit = (): void => { private cancelEdit = (): void => {

View file

@ -89,15 +89,9 @@ export default class LinkPreviewWidget extends React.Component<IProps> {
image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, "scale"); image = mediaFromMxc(image).getThumbnailOfSourceHttp(imageMaxWidth, imageMaxHeight, "scale");
} }
let thumbHeight = imageMaxHeight; const thumbHeight =
if (p["og:image:width"] && p["og:image:height"]) { ImageUtils.thumbHeight(p["og:image:width"], p["og:image:height"], imageMaxWidth, imageMaxHeight) ??
thumbHeight = ImageUtils.thumbHeight( imageMaxHeight;
p["og:image:width"],
p["og:image:height"],
imageMaxWidth,
imageMaxHeight,
);
}
let img: JSX.Element | undefined; let img: JSX.Element | undefined;
if (image) { if (image) {

View file

@ -276,7 +276,8 @@ export class MessageComposer extends React.Component<IProps, IState> {
if (createEvent?.getId()) createEventId = createEvent.getId(); if (createEvent?.getId()) createEventId = createEvent.getId();
} }
const viaServers = [this.context.tombstone.getSender().split(":").slice(1).join(":")]; const sender = this.context.tombstone?.getSender();
const viaServers = sender ? [sender.split(":").slice(1).join(":")] : undefined;
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,

View file

@ -17,7 +17,7 @@ limitations under the License.
import classNames from "classnames"; import classNames from "classnames";
import { IEventRelation } from "matrix-js-sdk/src/models/event"; import { IEventRelation } from "matrix-js-sdk/src/models/event";
import { M_POLL_START } from "matrix-js-sdk/src/@types/polls"; import { M_POLL_START } from "matrix-js-sdk/src/@types/polls";
import React, { createContext, MouseEventHandler, ReactElement, ReactNode, useContext, useRef } from "react"; import React, { createContext, MouseEventHandler, ReactElement, ReactNode, RefObject, useContext, useRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread"; import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
@ -180,7 +180,7 @@ interface IUploadButtonProps {
const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, relation, children }) => { const UploadButtonContextProvider: React.FC<IUploadButtonProps> = ({ roomId, relation, children }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext); const roomContext = useContext(RoomContext);
const uploadInput = useRef<HTMLInputElement>(); const uploadInput = useRef() as RefObject<HTMLInputElement>;
const onUploadClick = (): void => { const onUploadClick = (): void => {
if (cli?.isGuest()) { if (cli?.isGuest()) {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { useRef } from "react"; import React, { RefObject, useRef } from "react";
import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore"; import { BreadcrumbsStore } from "../../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
@ -30,7 +30,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
const RecentlyViewedButton: React.FC = () => { const RecentlyViewedButton: React.FC = () => {
const tooltipRef = useRef<InteractiveTooltip>(); const tooltipRef = useRef() as RefObject<InteractiveTooltip>;
const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms); const crumbs = useEventEmitterState(BreadcrumbsStore.instance, UPDATE_EVENT, () => BreadcrumbsStore.instance.rooms);
const content = ( const content = (

View file

@ -33,7 +33,7 @@ function cancelQuoting(context: TimelineRenderingType): void {
} }
interface IProps { interface IProps {
permalinkCreator: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
replyToEvent?: MatrixEvent; replyToEvent?: MatrixEvent;
} }

View file

@ -73,7 +73,7 @@ interface IProps {
interface IState { interface IState {
defaultIdServer?: string; defaultIdServer?: string;
currentClientIdServer?: string; currentClientIdServer?: string;
idServer?: string; idServer: string;
error?: string; error?: string;
busy: boolean; busy: boolean;
disconnectBusy: boolean; disconnectBusy: boolean;

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { JoinRule } from "matrix-js-sdk/src/@types/partials"; import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { EventType } from "matrix-js-sdk/src/@types/event"; import { EventType } from "matrix-js-sdk/src/@types/event";
import { RoomState } from "matrix-js-sdk/src/models/room-state";
import { _t } from "../../../../../languageHandler"; import { _t } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -33,15 +34,15 @@ interface ElementCallSwitchProps {
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => { const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]); const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]);
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]); const isPublic = useMemo(() => room?.getJoinRule() === JoinRule.Public, [room]);
const [content, events, maySend] = useRoomState( const [content, events, maySend] = useRoomState(
room, room ?? undefined,
useCallback((state) => { useCallback((state: RoomState) => {
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent(); const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
return [ return [
content ?? {}, content ?? {},
content?.["events"] ?? {}, content?.["events"] ?? {},
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()), state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getSafeUserId()),
]; ];
}, []), }, []),
); );

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, { ChangeEvent, useRef, useState } from "react"; import React, { ChangeEvent, RefObject, useRef, useState } from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
@ -38,7 +38,7 @@ export const SpaceAvatar: React.FC<Pick<IProps, "avatarUrl" | "avatarDisabled" |
avatarDisabled = false, avatarDisabled = false,
setAvatar, setAvatar,
}) => { }) => {
const avatarUploadRef = useRef<HTMLInputElement>(); const avatarUploadRef = useRef() as RefObject<HTMLInputElement>;
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
let avatarSection; let avatarSection;

View file

@ -28,7 +28,7 @@ export interface ICallback {
} }
export type UpdateCallback = (data: ICallback) => void; export type UpdateCallback = (data: ICallback) => void;
export type GetAutocompleterComponent = () => Autocomplete; export type GetAutocompleterComponent = () => Autocomplete | null;
export type UpdateQuery = (test: string) => Promise<void>; export type UpdateQuery = (test: string) => Promise<void>;
export default class AutocompleteWrapperModel { export default class AutocompleteWrapperModel {
@ -42,7 +42,7 @@ export default class AutocompleteWrapperModel {
) {} ) {}
public onEscape(e: KeyboardEvent): void { public onEscape(e: KeyboardEvent): void {
this.getAutocompleterComponent().onEscape(e); this.getAutocompleterComponent()?.onEscape(e);
} }
public close(): void { public close(): void {
@ -50,16 +50,16 @@ export default class AutocompleteWrapperModel {
} }
public hasSelection(): boolean { public hasSelection(): boolean {
return this.getAutocompleterComponent().hasSelection(); return !!this.getAutocompleterComponent()?.hasSelection();
} }
public hasCompletions(): boolean { public hasCompletions(): boolean {
const ac = this.getAutocompleterComponent(); const ac = this.getAutocompleterComponent();
return ac && ac.countCompletions() > 0; return !!ac && ac.countCompletions() > 0;
} }
public confirmCompletion(): void { public confirmCompletion(): void {
this.getAutocompleterComponent().onConfirmCompletion(); this.getAutocompleterComponent()?.onConfirmCompletion();
this.updateCallback({ close: true }); this.updateCallback({ close: true });
} }
@ -68,18 +68,18 @@ export default class AutocompleteWrapperModel {
*/ */
public async startSelection(): Promise<void> { public async startSelection(): Promise<void> {
const acComponent = this.getAutocompleterComponent(); const acComponent = this.getAutocompleterComponent();
if (acComponent.countCompletions() === 0) { if (acComponent && acComponent.countCompletions() === 0) {
// Force completions to show for the text currently entered // Force completions to show for the text currently entered
await acComponent.forceComplete(); await acComponent.forceComplete();
} }
} }
public selectPreviousSelection(): void { public selectPreviousSelection(): void {
this.getAutocompleterComponent().moveSelection(-1); this.getAutocompleterComponent()?.moveSelection(-1);
} }
public selectNextSelection(): void { public selectNextSelection(): void {
this.getAutocompleterComponent().moveSelection(+1); this.getAutocompleterComponent()?.moveSelection(+1);
} }
public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> { public onPartUpdate(part: Part, pos: DocumentPosition): Promise<void> {

View file

@ -44,7 +44,7 @@ import { Caret } from "./caret";
* @return the caret position * @return the caret position
*/ */
type TransformCallback = (caretPosition: DocumentPosition, inputType: string, diff: IDiff) => number | void; type TransformCallback = (caretPosition: DocumentPosition, inputType: string | undefined, diff: IDiff) => number | void;
type UpdateCallback = (caret?: Caret, inputType?: string, diff?: IDiff) => void; type UpdateCallback = (caret?: Caret, inputType?: string, diff?: IDiff) => void;
type ManualTransformCallback = () => Caret; type ManualTransformCallback = () => Caret;
@ -151,7 +151,7 @@ export default class EditorModel {
return this._parts.map((p) => p.serialize()); return this._parts.map((p) => p.serialize());
} }
private diff(newValue: string, inputType: string, caret: DocumentOffset): IDiff { private diff(newValue: string, inputType: string | undefined, caret: DocumentOffset): IDiff {
const previousValue = this.parts.reduce((text, p) => text + p.text, ""); const previousValue = this.parts.reduce((text, p) => text + p.text, "");
// can't use caret position with drag and drop // can't use caret position with drag and drop
if (inputType === "deleteByDrag") { if (inputType === "deleteByDrag") {
@ -196,7 +196,7 @@ export default class EditorModel {
return newTextLength; return newTextLength;
} }
public update(newValue: string, inputType: string, caret: DocumentOffset): Promise<void> { public update(newValue: string, inputType: string | undefined, caret: DocumentOffset): Promise<void> {
const diff = this.diff(newValue, inputType, caret); const diff = this.diff(newValue, inputType, caret);
const position = this.positionForOffset(diff.at || 0, caret.atNodeEnd); const position = this.positionForOffset(diff.at || 0, caret.atNodeEnd);
let removedOffsetDecrease = 0; let removedOffsetDecrease = 0;
@ -220,7 +220,7 @@ export default class EditorModel {
return acPromise; return acPromise;
} }
private getTransformAddedLen(newPosition: DocumentPosition, inputType: string, diff: IDiff): number { private getTransformAddedLen(newPosition: DocumentPosition, inputType: string | undefined, diff: IDiff): number {
const result = this.transformCallback?.(newPosition, inputType, diff); const result = this.transformCallback?.(newPosition, inputType, diff);
return Number.isFinite(result) ? (result as number) : 0; return Number.isFinite(result) ? (result as number) : 0;
} }
@ -360,7 +360,7 @@ export default class EditorModel {
* @return {Number} how far from position (in characters) the insertion ended. * @return {Number} how far from position (in characters) the insertion ended.
* This can be more than the length of `str` when crossing non-editable parts, which are skipped. * This can be more than the length of `str` when crossing non-editable parts, which are skipped.
*/ */
private addText(pos: IPosition, str: string, inputType: string): number { private addText(pos: IPosition, str: string, inputType: string | undefined): number {
let { index } = pos; let { index } = pos;
const { offset } = pos; const { offset } = pos;
let addLen = str.length; let addLen = str.length;

View file

@ -65,8 +65,8 @@ interface IBasePart {
serialize(): SerializedPart; serialize(): SerializedPart;
remove(offset: number, len: number): string | undefined; remove(offset: number, len: number): string | undefined;
split(offset: number): IBasePart; split(offset: number): IBasePart;
validateAndInsert(offset: number, str: string, inputType: string): boolean; validateAndInsert(offset: number, str: string, inputType: string | undefined): boolean;
appendUntilRejected(str: string, inputType: string): string | undefined; appendUntilRejected(str: string, inputType: string | undefined): string | undefined;
updateDOMNode(node: Node): void; updateDOMNode(node: Node): void;
canUpdateDOMNode(node: Node): boolean; canUpdateDOMNode(node: Node): boolean;
toDOMNode(): Node; toDOMNode(): Node;

View file

@ -28,8 +28,8 @@ const defaultMapper: Mapper<RoomState> = (roomState: RoomState) => roomState;
export const useRoomState = <T extends any = RoomState>( export const useRoomState = <T extends any = RoomState>(
room?: Room, room?: Room,
mapper: Mapper<T> = defaultMapper as Mapper<T>, mapper: Mapper<T> = defaultMapper as Mapper<T>,
): T | undefined => { ): T => {
const [value, setValue] = useState<T | undefined>(room ? mapper(room.currentState) : undefined); const [value, setValue] = useState<T>(room ? mapper(room.currentState) : (undefined as T));
const update = useCallback(() => { const update = useCallback(() => {
if (!room) return; if (!room) return;
@ -40,8 +40,8 @@ export const useRoomState = <T extends any = RoomState>(
useEffect(() => { useEffect(() => {
update(); update();
return () => { return () => {
setValue(undefined); setValue(room ? mapper(room.currentState) : (undefined as T));
}; };
}, [update]); }, [room, mapper, update]);
return value; return value;
}; };

View file

@ -15,8 +15,8 @@ limitations under the License.
*/ */
export interface ScrollState { export interface ScrollState {
focussedEvent: string; focussedEvent?: string;
pixelOffset: number; pixelOffset?: number;
} }
/** /**

View file

@ -22,7 +22,7 @@ import url from "url";
* @param {string} u The url to be abbreviated * @param {string} u The url to be abbreviated
* @returns {string} The abbreviated url * @returns {string} The abbreviated url
*/ */
export function abbreviateUrl(u: string): string { export function abbreviateUrl(u?: string): string {
if (!u) return ""; if (!u) return "";
const parsedUrl = url.parse(u); const parsedUrl = url.parse(u);