Conform more of the codebase to strictNullChecks (#10350

* Conform more of the codebase to `strictNullChecks`

* Iterate

* Generics ftw

* Iterate
This commit is contained in:
Michael Telatynski 2023-03-10 14:55:06 +00:00 committed by GitHub
parent d53e91802d
commit 127a3b667c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 279 additions and 263 deletions

View file

@ -52,3 +52,5 @@ export type KeysStartingWith<Input extends object, Str extends string> = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X
}[keyof Input]; }[keyof Input];
export type NonEmptyArray<T> = [T, ...T[]];

View file

@ -26,7 +26,11 @@ import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryCompon
import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog"; import InteractiveAuthDialog from "./components/views/dialogs/InteractiveAuthDialog";
function getIdServerDomain(): string { function getIdServerDomain(): string {
return MatrixClientPeg.get().idBaseUrl.split("://")[1]; const idBaseUrl = MatrixClientPeg.get().idBaseUrl;
if (!idBaseUrl) {
throw new Error("Identity server not set");
}
return idBaseUrl.split("://")[1];
} }
export type Binding = { export type Binding = {
@ -190,6 +194,9 @@ export default class AddThreepid {
if (this.bind) { if (this.bind) {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken(); const identityAccessToken = await authClient.getAccessToken();
if (!identityAccessToken) {
throw new Error("No identity access token found");
}
await MatrixClientPeg.get().bindThreePid({ await MatrixClientPeg.get().bindThreePid({
sid: this.sessionId, sid: this.sessionId,
client_secret: this.clientSecret, client_secret: this.clientSecret,
@ -279,7 +286,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why * with a "message" property which contains a human-readable message detailing why
* the request failed. * the request failed.
*/ */
public async haveMsisdnToken(msisdnToken: string): Promise<any[] | undefined> { public async haveMsisdnToken(
msisdnToken: string,
): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind(); const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();

View file

@ -39,7 +39,7 @@ export const ContextMenuButton: React.FC<IProps> = ({
<AccessibleButton <AccessibleButton
{...props} {...props}
onClick={onClick} onClick={onClick}
onContextMenu={onContextMenu || onClick} onContextMenu={onContextMenu ?? onClick ?? undefined}
title={label} title={label}
aria-label={label} aria-label={label}
aria-haspopup={true} aria-haspopup={true}

View file

@ -37,7 +37,7 @@ export const ContextMenuTooltipButton: React.FC<IProps> = ({
<AccessibleTooltipButton <AccessibleTooltipButton
{...props} {...props}
onClick={onClick} onClick={onClick}
onContextMenu={onContextMenu || onClick} onContextMenu={onContextMenu ?? onClick ?? undefined}
aria-haspopup={true} aria-haspopup={true}
aria-expanded={isExpanded} aria-expanded={isExpanded}
forceHide={isExpanded} forceHide={isExpanded}

View file

@ -26,7 +26,7 @@ import { _t } from "../languageHandler";
import { AsyncActionPayload } from "../dispatcher/payloads"; import { AsyncActionPayload } from "../dispatcher/payloads";
import RoomListStore from "../stores/room-list/RoomListStore"; import RoomListStore from "../stores/room-list/RoomListStore";
import { SortAlgorithm } from "../stores/room-list/algorithms/models"; import { SortAlgorithm } from "../stores/room-list/algorithms/models";
import { DefaultTagID } from "../stores/room-list/models"; import { DefaultTagID, TagID } from "../stores/room-list/models";
import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import ErrorDialog from "../components/views/dialogs/ErrorDialog";
export default class RoomListActions { export default class RoomListActions {
@ -49,10 +49,10 @@ export default class RoomListActions {
public static tagRoom( public static tagRoom(
matrixClient: MatrixClient, matrixClient: MatrixClient,
room: Room, room: Room,
oldTag: string, oldTag: TagID | null,
newTag: string, newTag: TagID | null,
oldIndex: number | null, oldIndex?: number,
newIndex: number | null, newIndex?: number,
): AsyncActionPayload { ): AsyncActionPayload {
let metaData: Parameters<MatrixClient["setRoomTag"]>[2] | null = null; let metaData: Parameters<MatrixClient["setRoomTag"]>[2] | null = null;

View file

@ -106,7 +106,7 @@ export class PlaybackQueue {
// Remove the now-useless clock for some space savings // Remove the now-useless clock for some space savings
this.clockStates.delete(mxEvent.getId()!); this.clockStates.delete(mxEvent.getId()!);
if (wasLastPlaying) { if (wasLastPlaying && this.currentPlaybackId) {
this.recentFullPlays.add(this.currentPlaybackId); this.recentFullPlays.add(this.currentPlaybackId);
const orderClone = arrayFastClone(this.playbackIdOrder); const orderClone = arrayFastClone(this.playbackIdOrder);
const last = orderClone.pop(); const last = orderClone.pop();
@ -188,8 +188,8 @@ export class PlaybackQueue {
if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) {
const lastInstance = this.playbacks.get(this.currentPlaybackId); const lastInstance = this.playbacks.get(this.currentPlaybackId);
if ( if (
lastInstance.currentState === PlaybackState.Playing || lastInstance &&
lastInstance.currentState === PlaybackState.Paused [PlaybackState.Playing, PlaybackState.Paused].includes(lastInstance.currentState)
) { ) {
order.push(this.currentPlaybackId); order.push(this.currentPlaybackId);
} }

View file

@ -35,6 +35,9 @@ export async function getThreepidsWithBindStatus(
try { try {
const authClient = new IdentityAuthClient(); const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken({ check: false }); const identityAccessToken = await authClient.getAccessToken({ check: false });
if (!identityAccessToken) {
throw new Error("No identity access token found");
}
// Restructure for lookup query // Restructure for lookup query
const query = threepids.map(({ medium, address }): [string, string] => [medium, address]); const query = threepids.map(({ medium, address }): [string, string] => [medium, address]);

View file

@ -29,7 +29,7 @@ export type IProps<T extends keyof JSX.IntrinsicElements> = Omit<DynamicHtmlElem
onWheel?: (event: WheelEvent) => void; onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
tabIndex?: number; tabIndex?: number;
wrappedRef?: (ref: HTMLDivElement) => void; wrappedRef?: (ref: HTMLDivElement | null) => void;
children: ReactNode; children: ReactNode;
}; };

View file

@ -99,7 +99,7 @@ class FilePanel extends React.Component<IProps, IState> {
const timeline = this.state.timelineSet.getLiveTimeline(); const timeline = this.state.timelineSet.getLiveTimeline();
if (ev.getType() !== "m.room.message") return; if (ev.getType() !== "m.room.message") return;
if (["m.file", "m.image", "m.video", "m.audio"].indexOf(ev.getContent().msgtype) == -1) { if (!["m.file", "m.image", "m.video", "m.audio"].includes(ev.getContent().msgtype!)) {
return; return;
} }
@ -176,7 +176,7 @@ class FilePanel extends React.Component<IProps, IState> {
// the event index to fulfill the pagination request. Asking the server // the event index to fulfill the pagination request. Asking the server
// to paginate won't ever work since the server can't correctly filter // to paginate won't ever work since the server can't correctly filter
// out events containing URLs // out events containing URLs
if (client.isRoomEncrypted(roomId) && eventIndex !== null) { if (room && client.isRoomEncrypted(roomId) && eventIndex !== null) {
return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit); return eventIndex.paginateTimelineWindow(room, timelineWindow, direction, limit);
} else { } else {
return timelineWindow.paginate(direction, limit); return timelineWindow.paginate(direction, limit);

View file

@ -177,19 +177,20 @@ export function GenericDropdownMenu<T>({
</> </>
); );
} }
const contextMenu = menuDisplayed ? ( const contextMenu =
<ContextMenu menuDisplayed && button.current ? (
onFinished={closeMenu} <ContextMenu
chevronFace={ChevronFace.Top} onFinished={closeMenu}
wrapperClassName={classNames("mx_GenericDropdownMenu_wrapper", className)} chevronFace={ChevronFace.Top}
{...aboveLeftOf(button.current.getBoundingClientRect())} wrapperClassName={classNames("mx_GenericDropdownMenu_wrapper", className)}
> {...aboveLeftOf(button.current.getBoundingClientRect())}
{contextMenuOptions} >
{AdditionalOptions && ( {contextMenuOptions}
<AdditionalOptions menuDisplayed={menuDisplayed} openMenu={openMenu} closeMenu={closeMenu} /> {AdditionalOptions && (
)} <AdditionalOptions menuDisplayed={menuDisplayed} openMenu={openMenu} closeMenu={closeMenu} />
</ContextMenu> )}
) : null; </ContextMenu>
) : null;
return ( return (
<> <>
<ContextMenuButton <ContextMenuButton

View file

@ -44,7 +44,7 @@ export default class IndicatorScrollbar<T extends keyof JSX.IntrinsicElements> e
> { > {
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>(); private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
private scrollElement: HTMLDivElement; private scrollElement: HTMLDivElement;
private likelyTrackpadUser: boolean = null; private likelyTrackpadUser: boolean | null = null;
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
public constructor(props: IProps<T>) { public constructor(props: IProps<T>) {

View file

@ -271,6 +271,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// add appropriate sticky classes to wrapper so it has // add appropriate sticky classes to wrapper so it has
// the necessary top/bottom padding to put the sticky header in // the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper
if (!listWrapper) return;
if (lastTopHeader) { if (lastTopHeader) {
listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop"); listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop");
} else { } else {

View file

@ -85,23 +85,23 @@ export default class LegacyCallEventGrouper extends EventEmitter {
); );
} }
private get invite(): MatrixEvent { private get invite(): MatrixEvent | undefined {
return [...this.events].find((event) => event.getType() === EventType.CallInvite); return [...this.events].find((event) => event.getType() === EventType.CallInvite);
} }
private get hangup(): MatrixEvent { private get hangup(): MatrixEvent | undefined {
return [...this.events].find((event) => event.getType() === EventType.CallHangup); return [...this.events].find((event) => event.getType() === EventType.CallHangup);
} }
private get reject(): MatrixEvent { private get reject(): MatrixEvent | undefined {
return [...this.events].find((event) => event.getType() === EventType.CallReject); return [...this.events].find((event) => event.getType() === EventType.CallReject);
} }
private get selectAnswer(): MatrixEvent { private get selectAnswer(): MatrixEvent | undefined {
return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer); return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
} }
public get isVoice(): boolean { public get isVoice(): boolean | undefined {
const invite = this.invite; const invite = this.invite;
if (!invite) return; if (!invite) return;
@ -114,7 +114,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null; return this.call?.hangupReason ?? this.hangup?.getContent()?.reason ?? null;
} }
public get rejectParty(): string { public get rejectParty(): string | undefined {
return this.reject?.getSender(); return this.reject?.getSender();
} }

View file

@ -170,7 +170,7 @@ class LoggedInView extends React.Component<IProps, IState> {
monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient); monitorSyncedPushRules(this._matrixClient.getAccountData("m.push_rules"), this._matrixClient);
this._matrixClient.on(ClientEvent.Sync, this.onSync); this._matrixClient.on(ClientEvent.Sync, this.onSync);
// Call `onSync` with the current state as well // Call `onSync` with the current state as well
this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData()); this.onSync(this._matrixClient.getSyncState(), null, this._matrixClient.getSyncStateData() ?? undefined);
this._matrixClient.on(RoomStateEvent.Events, this.onRoomStateEvents); this._matrixClient.on(RoomStateEvent.Events, this.onRoomStateEvents);
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onCompactLayoutChanged); this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, this.onCompactLayoutChanged);
@ -271,11 +271,11 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
private loadResizerPreferences(): void { private loadResizerPreferences(): void {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10); let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size")!, 10);
if (isNaN(lhsSize)) { if (isNaN(lhsSize)) {
lhsSize = 350; lhsSize = 350;
} }
this.resizer.forHandleWithId("lp-resizer").resize(lhsSize); this.resizer.forHandleWithId("lp-resizer")?.resize(lhsSize);
} }
private onAccountData = (event: MatrixEvent): void => { private onAccountData = (event: MatrixEvent): void => {
@ -291,13 +291,13 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
}; };
private onSync = (syncState: SyncState, oldSyncState?: SyncState, data?: ISyncStateData): void => { private onSync = (syncState: SyncState | null, oldSyncState: SyncState | null, data?: ISyncStateData): void => {
const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode; const oldErrCode = (this.state.syncErrorData?.error as MatrixError)?.errcode;
const newErrCode = (data?.error as MatrixError)?.errcode; const newErrCode = (data?.error as MatrixError)?.errcode;
if (syncState === oldSyncState && oldErrCode === newErrCode) return; if (syncState === oldSyncState && oldErrCode === newErrCode) return;
this.setState({ this.setState({
syncErrorData: syncState === SyncState.Error ? data : null, syncErrorData: syncState === SyncState.Error ? data : undefined,
}); });
if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) { if (oldSyncState === SyncState.Prepared && syncState === SyncState.Syncing) {
@ -355,7 +355,7 @@ class LoggedInView extends React.Component<IProps, IState> {
const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM); const pinnedEventIds = pinStateEvent.getContent().pinned.slice(0, MAX_PINNED_NOTICES_PER_ROOM);
for (const eventId of pinnedEventIds) { for (const eventId of pinnedEventIds) {
const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId); const timeline = await this._matrixClient.getEventTimeline(room.getUnfilteredTimelineSet(), eventId);
const event = timeline.getEvents().find((ev) => ev.getId() === eventId); const event = timeline?.getEvents().find((ev) => ev.getId() === eventId);
if (event) events.push(event); if (event) events.push(event);
} }
} }
@ -390,7 +390,7 @@ class LoggedInView extends React.Component<IProps, IState> {
if (inputableElement?.focus) { if (inputableElement?.focus) {
inputableElement.focus(); inputableElement.focus();
} else { } else {
const inThread = !!document.activeElement.closest(".mx_ThreadView"); const inThread = !!document.activeElement?.closest(".mx_ThreadView");
// refocusing during a paste event will make the paste end up in the newly focused element, // refocusing during a paste event will make the paste end up in the newly focused element,
// so dispatch synchronously before paste happens // so dispatch synchronously before paste happens
dis.dispatch( dis.dispatch(
@ -533,11 +533,11 @@ class LoggedInView extends React.Component<IProps, IState> {
}); });
break; break;
case KeyBindingAction.PreviousVisitedRoomOrSpace: case KeyBindingAction.PreviousVisitedRoomOrSpace:
PlatformPeg.get().navigateForwardBack(true); PlatformPeg.get()?.navigateForwardBack(true);
handled = true; handled = true;
break; break;
case KeyBindingAction.NextVisitedRoomOrSpace: case KeyBindingAction.NextVisitedRoomOrSpace:
PlatformPeg.get().navigateForwardBack(false); PlatformPeg.get()?.navigateForwardBack(false);
handled = true; handled = true;
break; break;
} }
@ -555,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
); );
SettingsStore.setValue( SettingsStore.setValue(
"showHiddenEventsInTimeline", "showHiddenEventsInTimeline",
undefined, null,
SettingLevel.DEVICE, SettingLevel.DEVICE,
!hiddenEventVisibility, !hiddenEventVisibility,
); );
@ -567,7 +567,7 @@ class LoggedInView extends React.Component<IProps, IState> {
if ( if (
!handled && !handled &&
PlatformPeg.get().overrideBrowserShortcuts() && PlatformPeg.get()?.overrideBrowserShortcuts() &&
ev.code.startsWith("Digit") && ev.code.startsWith("Digit") &&
ev.code !== "Digit0" && // this is the shortcut for reset zoom, don't override it ev.code !== "Digit0" && // this is the shortcut for reset zoom, don't override it
isOnlyCtrlOrCmdKeyEvent(ev) isOnlyCtrlOrCmdKeyEvent(ev)
@ -599,7 +599,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// If the user is entering a printable character outside of an input field // If the user is entering a printable character outside of an input field
// redirect it to the composer for them. // redirect it to the composer for them.
if (!isClickShortcut && isPrintable && !getInputableElement(ev.target as HTMLElement)) { if (!isClickShortcut && isPrintable && !getInputableElement(ev.target as HTMLElement)) {
const inThread = !!document.activeElement.closest(".mx_ThreadView"); const inThread = !!document.activeElement?.closest(".mx_ThreadView");
// synchronous dispatch so we focus before key generates input // synchronous dispatch so we focus before key generates input
dis.dispatch( dis.dispatch(
{ {

View file

@ -187,9 +187,9 @@ interface IState {
// The ID of the room we're viewing. This is either populated directly // The ID of the room we're viewing. This is either populated directly
// in the case where we view a room by ID or by RoomView when it resolves // in the case where we view a room by ID or by RoomView when it resolves
// what ID an alias points at. // what ID an alias points at.
currentRoomId?: string; currentRoomId: string | null;
// If we're trying to just view a user ID (i.e. /user URL), this is it // If we're trying to just view a user ID (i.e. /user URL), this is it
currentUserId?: string; currentUserId: string | null;
// this is persisted as mx_lhs_size, loaded in LoggedInView // this is persisted as mx_lhs_size, loaded in LoggedInView
collapseLhs: boolean; collapseLhs: boolean;
// Parameters used in the registration dance with the IS // Parameters used in the registration dance with the IS
@ -202,7 +202,7 @@ interface IState {
// When showing Modal dialogs we need to set aria-hidden on the root app element // When showing Modal dialogs we need to set aria-hidden on the root app element
// and disable it when there are no dialogs // and disable it when there are no dialogs
hideToSRUsers: boolean; hideToSRUsers: boolean;
syncError?: Error; syncError: Error | null;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
serverConfig?: ValidatedServerConfig; serverConfig?: ValidatedServerConfig;
ready: boolean; ready: boolean;
@ -248,6 +248,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state = { this.state = {
view: Views.LOADING, view: Views.LOADING,
collapseLhs: false, collapseLhs: false,
currentRoomId: null,
currentUserId: null,
hideToSRUsers: false, hideToSRUsers: false,
@ -469,9 +471,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
); );
}, 1000); }, 1000);
private getFallbackHsUrl(): string { private getFallbackHsUrl(): string | null {
if (this.props.serverConfig?.isDefault) { if (this.props.serverConfig?.isDefault) {
return this.props.config.fallback_hs_url; return this.props.config.fallback_hs_url ?? null;
} else { } else {
return null; return null;
} }
@ -480,7 +482,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private getServerProperties(): { serverConfig: ValidatedServerConfig } { private getServerProperties(): { serverConfig: ValidatedServerConfig } {
let props = this.state.serverConfig; let props = this.state.serverConfig;
if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = this.props.serverConfig; // for unit tests
if (!props) props = SdkConfig.get("validated_server_config"); if (!props) props = SdkConfig.get("validated_server_config")!;
return { serverConfig: props }; return { serverConfig: props };
} }
@ -709,7 +711,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Modal.createDialog( Modal.createDialog(
UserSettingsDialog, UserSettingsDialog,
{ initialTabId: tabPayload.initialTabId as UserTab }, { initialTabId: tabPayload.initialTabId as UserTab },
/*className=*/ null, /*className=*/ undefined,
/*isPriority=*/ false, /*isPriority=*/ false,
/*isStatic=*/ true, /*isStatic=*/ true,
); );
@ -978,7 +980,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setState( this.setState(
{ {
view: Views.LOGGED_IN, view: Views.LOGGED_IN,
currentRoomId: roomInfo.room_id || null, currentRoomId: roomInfo.room_id ?? null,
page_type: PageType.RoomView, page_type: PageType.RoomView,
threepidInvite: roomInfo.threepid_invite, threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data, roomOobData: roomInfo.oob_data,
@ -1063,7 +1065,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const [shouldCreate, opts] = await modal.finished; const [shouldCreate, opts] = await modal.finished;
if (shouldCreate) { if (shouldCreate) {
createRoom(opts); createRoom(opts!);
} }
} }
@ -1122,7 +1124,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Show a warning if there are additional complications. // Show a warning if there are additional complications.
const warnings: JSX.Element[] = []; const warnings: JSX.Element[] = [];
const memberCount = roomToLeave.currentState.getJoinedMemberCount(); const memberCount = roomToLeave?.currentState.getJoinedMemberCount();
if (memberCount === 1) { if (memberCount === 1) {
warnings.push( warnings.push(
<span className="warning" key="only_member_warning"> <span className="warning" key="only_member_warning">
@ -1137,7 +1139,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return warnings; return warnings;
} }
const joinRules = roomToLeave.currentState.getStateEvents("m.room.join_rules", ""); const joinRules = roomToLeave?.currentState.getStateEvents("m.room.join_rules", "");
if (joinRules) { if (joinRules) {
const rule = joinRules.getContent().join_rule; const rule = joinRules.getContent().join_rule;
if (rule !== "public") { if (rule !== "public") {
@ -1165,9 +1167,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
<span> <span>
{isSpace {isSpace
? _t("Are you sure you want to leave the space '%(spaceName)s'?", { ? _t("Are you sure you want to leave the space '%(spaceName)s'?", {
spaceName: roomToLeave.name, spaceName: roomToLeave?.name ?? _t("Unnamed Space"),
}) })
: _t("Are you sure you want to leave the room '%(roomName)s'?", { roomName: roomToLeave.name })} : _t("Are you sure you want to leave the room '%(roomName)s'?", {
roomName: roomToLeave?.name ?? _t("Unnamed Room"),
})}
{warnings} {warnings}
</span> </span>
), ),
@ -1311,9 +1315,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setStateForNewView({ view: Views.LOGGED_IN }); this.setStateForNewView({ view: Views.LOGGED_IN });
// If a specific screen is set to be shown after login, show that above // If a specific screen is set to be shown after login, show that above
// all else, as it probably means the user clicked on something already. // all else, as it probably means the user clicked on something already.
if (this.screenAfterLogin && this.screenAfterLogin.screen) { if (this.screenAfterLogin?.screen) {
this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params); this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params);
this.screenAfterLogin = null; this.screenAfterLogin = undefined;
} else if (MatrixClientPeg.currentUserIsJustRegistered()) { } else if (MatrixClientPeg.currentUserIsJustRegistered()) {
MatrixClientPeg.setJustRegisteredUserId(null); MatrixClientPeg.setJustRegisteredUserId(null);
@ -1403,7 +1407,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// result in view_home_page, _user_settings or _room_directory // result in view_home_page, _user_settings or _room_directory
if (this.screenAfterLogin && this.screenAfterLogin.screen) { if (this.screenAfterLogin && this.screenAfterLogin.screen) {
this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params); this.showScreen(this.screenAfterLogin.screen, this.screenAfterLogin.params);
this.screenAfterLogin = null; this.screenAfterLogin = undefined;
} else if (localStorage && localStorage.getItem("mx_last_room_id")) { } else if (localStorage && localStorage.getItem("mx_last_room_id")) {
// Before defaulting to directory, show the last viewed room // Before defaulting to directory, show the last viewed room
this.viewLastRoom(); this.viewLastRoom();
@ -1419,7 +1423,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private viewLastRoom(): void { private viewLastRoom(): void {
dis.dispatch<ViewRoomPayload>({ dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom, action: Action.ViewRoom,
room_id: localStorage.getItem("mx_last_room_id"), room_id: localStorage.getItem("mx_last_room_id") ?? undefined,
metricsTrigger: undefined, // other metricsTrigger: undefined, // other
}); });
} }
@ -1486,12 +1490,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return this.loggedInView.current.canResetTimelineInRoom(roomId); return this.loggedInView.current.canResetTimelineInRoom(roomId);
}); });
cli.on(ClientEvent.Sync, (state: SyncState, prevState?: SyncState, data?: ISyncStateData) => { cli.on(ClientEvent.Sync, (state: SyncState, prevState: SyncState | null, data?: ISyncStateData) => {
if (state === SyncState.Error || state === SyncState.Reconnecting) { if (state === SyncState.Error || state === SyncState.Reconnecting) {
if (data.error instanceof InvalidStoreError) { if (data?.error instanceof InvalidStoreError) {
Lifecycle.handleInvalidStoreError(data.error); Lifecycle.handleInvalidStoreError(data.error);
} }
this.setState({ syncError: data.error }); this.setState({ syncError: data?.error ?? null });
} else if (this.state.syncError) { } else if (this.state.syncError) {
this.setState({ syncError: null }); this.setState({ syncError: null });
} }
@ -1559,12 +1563,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cancelButton: _t("Dismiss"), cancelButton: _t("Dismiss"),
onFinished: (confirmed) => { onFinished: (confirmed) => {
if (confirmed) { if (confirmed) {
const wnd = window.open(consentUri, "_blank"); const wnd = window.open(consentUri, "_blank")!;
wnd.opener = null; wnd.opener = null;
} }
}, },
}, },
null, undefined,
true, true,
); );
}); });
@ -1655,7 +1659,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
{ {
verifier: request.verifier, verifier: request.verifier,
}, },
null, undefined,
/* priority = */ false, /* priority = */ false,
/* static = */ true, /* static = */ true,
); );
@ -1774,7 +1778,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
const type = screen === "start_sso" ? "sso" : "cas"; const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin()); PlatformPeg.get()?.startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen.indexOf("room/") === 0) { } else if (screen.indexOf("room/") === 0) {
// Rooms can have the following formats: // Rooms can have the following formats:
// #room_alias:domain or !opaque_id:domain // #room_alias:domain or !opaque_id:domain
@ -1786,7 +1790,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
eventOffset = domainOffset + room.substring(domainOffset).indexOf("/"); eventOffset = domainOffset + room.substring(domainOffset).indexOf("/");
} }
const roomString = room.substring(0, eventOffset); const roomString = room.substring(0, eventOffset);
let eventId = room.substring(eventOffset + 1); // empty string if no event id given let eventId: string | undefined = room.substring(eventOffset + 1); // empty string if no event id given
// Previously we pulled the eventID from the segments in such a way // Previously we pulled the eventID from the segments in such a way
// where if there was no eventId then we'd get undefined. However, we // where if there was no eventId then we'd get undefined. However, we
@ -1797,9 +1801,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
let threepidInvite: IThreepidInvite; let threepidInvite: IThreepidInvite | undefined;
// if we landed here from a 3PID invite, persist it // if we landed here from a 3PID invite, persist it
if (params.signurl && params.email) { if (params?.signurl && params?.email) {
threepidInvite = ThreepidInviteStore.instance.storeInvite( threepidInvite = ThreepidInviteStore.instance.storeInvite(
roomString, roomString,
params as IThreepidInviteWireFormat, params as IThreepidInviteWireFormat,
@ -1816,8 +1820,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// to other levels. If there's just one ?via= then params.via is a // to other levels. If there's just one ?via= then params.via is a
// single string. If someone does something like ?via=one.com&via=two.com // single string. If someone does something like ?via=one.com&via=two.com
// then params.via is an array of strings. // then params.via is an array of strings.
let via = []; let via: string[] = [];
if (params.via) { if (params?.via) {
if (typeof params.via === "string") via = [params.via]; if (typeof params.via === "string") via = [params.via];
else via = params.via; else via = params.via;
} }
@ -1855,7 +1859,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({ dis.dispatch({
action: "view_user_info", action: "view_user_info",
userId: userId, userId: userId,
subAction: params.action, subAction: params?.action,
}); });
} else { } else {
logger.info("Ignoring showScreen for '%s'", screen); logger.info("Ignoring showScreen for '%s'", screen);
@ -1949,8 +1953,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
if (PlatformPeg.get()) { if (PlatformPeg.get()) {
PlatformPeg.get().setErrorStatus(state === SyncState.Error); PlatformPeg.get()!.setErrorStatus(state === SyncState.Error);
PlatformPeg.get().setNotificationCount(numUnreadRooms); PlatformPeg.get()!.setNotificationCount(numUnreadRooms);
} }
this.subTitleStatus = ""; this.subTitleStatus = "";
@ -1971,7 +1975,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}; };
private makeRegistrationUrl = (params: QueryDict): string => { private makeRegistrationUrl = (params: QueryDict): string => {
if (this.props.startingFragmentQueryParams.referrer) { if (this.props.startingFragmentQueryParams?.referrer) {
params.referrer = this.props.startingFragmentQueryParams.referrer; params.referrer = this.props.startingFragmentQueryParams.referrer;
} }
return this.props.makeRegistrationUrl(params); return this.props.makeRegistrationUrl(params);

View file

@ -163,7 +163,7 @@ interface IProps {
stickyBottom?: boolean; stickyBottom?: boolean;
// className for the panel // className for the panel
className: string; className?: string;
// show twelve hour timestamps // show twelve hour timestamps
isTwelveHour?: boolean; isTwelveHour?: boolean;
@ -177,7 +177,7 @@ interface IProps {
// which layout to use // which layout to use
layout?: Layout; layout?: Layout;
resizeNotifier: ResizeNotifier; resizeNotifier?: ResizeNotifier;
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
editState?: EditorStateTransfer; editState?: EditorStateTransfer;
@ -345,12 +345,12 @@ export default class MessagePanel extends React.Component<IProps, IState> {
}; };
/* get the DOM node representing the given event */ /* get the DOM node representing the given event */
public getNodeForEventId(eventId: string): HTMLElement { public getNodeForEventId(eventId: string): HTMLElement | undefined {
if (!this.eventTiles) { if (!this.eventTiles) {
return undefined; return undefined;
} }
return this.eventTiles[eventId]?.ref?.current; return this.eventTiles[eventId]?.ref?.current ?? undefined;
} }
public getTileForEventId(eventId?: string): UnwrappedEventTile | undefined { public getTileForEventId(eventId?: string): UnwrappedEventTile | undefined {
@ -362,7 +362,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
/* return true if the content is fully scrolled down right now; else false. /* return true if the content is fully scrolled down right now; else false.
*/ */
public isAtBottom(): boolean { public isAtBottom(): boolean | undefined {
return this.scrollPanel.current?.isAtBottom(); return this.scrollPanel.current?.isAtBottom();
} }
@ -371,7 +371,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
* *
* returns null if we are not mounted. * returns null if we are not mounted.
*/ */
public getScrollState(): IScrollState { public getScrollState(): IScrollState | null {
return this.scrollPanel.current?.getScrollState() ?? null; return this.scrollPanel.current?.getScrollState() ?? null;
} }
@ -381,7 +381,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// -1: read marker is above the window // -1: read marker is above the window
// 0: read marker is within the window // 0: read marker is within the window
// +1: read marker is below the window // +1: read marker is below the window
public getReadMarkerPosition(): number { public getReadMarkerPosition(): number | null {
const readMarker = this.readMarkerNode.current; const readMarker = this.readMarkerNode.current;
const messageWrapper = this.scrollPanel.current; const messageWrapper = this.scrollPanel.current;
@ -633,9 +633,8 @@ export default class MessagePanel extends React.Component<IProps, IState> {
break; break;
} }
const ret = []; const ret: ReactNode[] = [];
let prevEvent: MatrixEvent | null = null; // the last event we showed
let prevEvent = null; // the last event we showed
// Note: the EventTile might still render a "sent/sending receipt" independent of // Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely // this information. When not providing read receipt information, the tile is likely
@ -645,7 +644,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
this.readReceiptsByEvent = this.getReadReceiptsByShownEvent(); this.readReceiptsByEvent = this.getReadReceiptsByShownEvent();
} }
let grouper: BaseGrouper = null; let grouper: BaseGrouper | null = null;
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
const { event: mxEv, shouldShow } = events[i]; const { event: mxEv, shouldShow } = events[i];
@ -695,14 +694,14 @@ export default class MessagePanel extends React.Component<IProps, IState> {
} }
public getTilesForEvent( public getTilesForEvent(
prevEvent: MatrixEvent, prevEvent: MatrixEvent | null,
mxEv: MatrixEvent, mxEv: MatrixEvent,
last = false, last = false,
isGrouped = false, isGrouped = false,
nextEvent?: MatrixEvent, nextEvent?: MatrixEvent,
nextEventWithTile?: MatrixEvent, nextEventWithTile?: MatrixEvent,
): ReactNode[] { ): ReactNode[] {
const ret = []; const ret: ReactNode[] = [];
const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId(); const isEditing = this.props.editState?.getEvent().getId() === mxEv.getId();
// local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators. // local echoes have a fake date, which could even be yesterday. Treat them as 'today' for the date separators.
@ -806,7 +805,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
return ret; return ret;
} }
public wantsDateSeparator(prevEvent: MatrixEvent, nextEventDate: Date): boolean { public wantsDateSeparator(prevEvent: MatrixEvent | null, nextEventDate: Date): boolean {
if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) {
return false; return false;
} }
@ -820,7 +819,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
// Get a list of read receipts that should be shown next to this event // Get a list of read receipts that should be shown next to this event
// Receipts are objects which have a 'userId', 'roomMember' and 'ts'. // Receipts are objects which have a 'userId', 'roomMember' and 'ts'.
private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] { private getReadReceiptsForEvent(event: MatrixEvent): IReadReceiptProps[] | null {
const myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
// get list of read receipts, sorted most recent first // get list of read receipts, sorted most recent first
@ -939,7 +938,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
private onTypingShown = (): void => { private onTypingShown = (): void => {
const scrollPanel = this.scrollPanel.current; const scrollPanel = this.scrollPanel.current;
// this will make the timeline grow, so checkScroll // this will make the timeline grow, so checkScroll
scrollPanel.checkScroll(); scrollPanel?.checkScroll();
if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) { if (scrollPanel && scrollPanel.getScrollState().stuckAtBottom) {
scrollPanel.preventShrinking(); scrollPanel.preventShrinking();
} }
@ -1018,7 +1017,7 @@ export default class MessagePanel extends React.Component<IProps, IState> {
); );
} }
let ircResizer = null; let ircResizer: JSX.Element | undefined;
if (this.props.layout == Layout.IRC) { if (this.props.layout == Layout.IRC) {
ircResizer = ( ircResizer = (
<IRCTimelineProfileResizer <IRCTimelineProfileResizer
@ -1076,7 +1075,7 @@ abstract class BaseGrouper {
public constructor( public constructor(
public readonly panel: MessagePanel, public readonly panel: MessagePanel,
public readonly event: MatrixEvent, public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent, public readonly prevEvent: MatrixEvent | null,
public readonly lastShownEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent,
public readonly nextEvent?: MatrixEvent, public readonly nextEvent?: MatrixEvent,
public readonly nextEventTile?: MatrixEvent, public readonly nextEventTile?: MatrixEvent,
@ -1261,7 +1260,7 @@ class MainGrouper extends BaseGrouper {
public constructor( public constructor(
public readonly panel: MessagePanel, public readonly panel: MessagePanel,
public readonly event: MatrixEvent, public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent, public readonly prevEvent: MatrixEvent | null,
public readonly lastShownEvent: MatrixEvent, public readonly lastShownEvent: MatrixEvent,
nextEvent: MatrixEvent, nextEvent: MatrixEvent,
nextEventTile: MatrixEvent, nextEventTile: MatrixEvent,
@ -1341,7 +1340,7 @@ class MainGrouper extends BaseGrouper {
} }
let highlightInSummary = false; let highlightInSummary = false;
let eventTiles = this.events let eventTiles: ReactNode[] | null = this.events
.map((e, i) => { .map((e, i) => {
if (e.getId() === panel.props.highlightedEventId) { if (e.getId() === panel.props.highlightedEventId) {
highlightInSummary = true; highlightInSummary = true;

View file

@ -149,7 +149,7 @@ interface IProps {
*/ */
export interface IScrollState { export interface IScrollState {
stuckAtBottom: boolean; stuckAtBottom?: boolean;
trackedNode?: HTMLElement; trackedNode?: HTMLElement;
trackedScrollToken?: string; trackedScrollToken?: string;
bottomOffset?: number; bottomOffset?: number;
@ -173,7 +173,7 @@ export default class ScrollPanel extends React.Component<IProps> {
onScroll: function () {}, onScroll: function () {},
}; };
private readonly pendingFillRequests: Record<"b" | "f", boolean> = { private readonly pendingFillRequests: Record<"b" | "f", boolean | null> = {
b: null, b: null,
f: null, f: null,
}; };
@ -190,7 +190,7 @@ export default class ScrollPanel extends React.Component<IProps> {
private pendingFillDueToPropsUpdate: boolean; private pendingFillDueToPropsUpdate: boolean;
private scrollState: IScrollState; private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState; private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: number; private unfillDebouncer: number | null;
private bottomGrowth: number; private bottomGrowth: number;
private minListHeight: number; private minListHeight: number;
private heightUpdateInProgress: boolean; private heightUpdateInProgress: boolean;

View file

@ -24,6 +24,7 @@ import { _t } from "../../languageHandler";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers"; import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
import { NonEmptyArray } from "../../@types/common";
/** /**
* Represents a tab for the TabbedView. * Represents a tab for the TabbedView.
@ -40,7 +41,7 @@ export class Tab {
public constructor( public constructor(
public readonly id: string, public readonly id: string,
public readonly label: string, public readonly label: string,
public readonly icon: string, public readonly icon: string | null,
public readonly body: React.ReactNode, public readonly body: React.ReactNode,
public readonly screenName?: ScreenName, public readonly screenName?: ScreenName,
) {} ) {}
@ -52,7 +53,7 @@ export enum TabLocation {
} }
interface IProps { interface IProps {
tabs: Tab[]; tabs: NonEmptyArray<Tab>;
initialTabId?: string; initialTabId?: string;
tabLocation: TabLocation; tabLocation: TabLocation;
onChange?: (tabId: string) => void; onChange?: (tabId: string) => void;
@ -69,7 +70,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId); const initialTabIdIsValid = props.tabs.find((tab) => tab.id === props.initialTabId);
this.state = { this.state = {
activeTabId: initialTabIdIsValid ? props.initialTabId : props.tabs[0]?.id, activeTabId: initialTabIdIsValid ? props.initialTabId! : props.tabs[0].id,
}; };
} }
@ -101,7 +102,7 @@ export default class TabbedView extends React.Component<IProps, IState> {
if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active"; if (this.state.activeTabId === tab.id) classes += "mx_TabbedView_tabLabel_active";
let tabIcon = null; let tabIcon: JSX.Element | undefined;
if (tab.icon) { if (tab.icon) {
tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />; tabIcon = <span className={`mx_TabbedView_maskedIcon ${tab.icon}`} />;
} }
@ -141,9 +142,11 @@ export default class TabbedView extends React.Component<IProps, IState> {
mx_TabbedView_tabsOnTop: this.props.tabLocation == TabLocation.TOP, mx_TabbedView_tabsOnTop: this.props.tabLocation == TabLocation.TOP,
}); });
const screenName = tab?.screenName ?? this.props.screenName;
return ( return (
<div className={tabbedViewClasses}> <div className={tabbedViewClasses}>
<PosthogScreenTracker screenName={tab?.screenName ?? this.props.screenName} /> {screenName && <PosthogScreenTracker screenName={screenName} />}
<div className="mx_TabbedView_tabLabels">{labels}</div> <div className="mx_TabbedView_tabLabels">{labels}</div>
{panel} {panel}
</div> </div>

View file

@ -200,7 +200,7 @@ interface IState {
forwardPaginating: boolean; forwardPaginating: boolean;
// cache of matrixClient.getSyncState() (but from the 'sync' event) // cache of matrixClient.getSyncState() (but from the 'sync' event)
clientSyncState: SyncState; clientSyncState: SyncState | null;
// should the event tiles have twelve hour times // should the event tiles have twelve hour times
isTwelveHour: boolean; isTwelveHour: boolean;
@ -268,7 +268,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// but for now we just do it per room for simplicity. // but for now we just do it per room for simplicity.
let initialReadMarker: string | null = null; let initialReadMarker: string | null = null;
if (this.props.manageReadMarkers) { if (this.props.manageReadMarkers) {
const readmarker = this.props.timelineSet.room.getAccountData("m.fully_read"); const readmarker = this.props.timelineSet.room?.getAccountData("m.fully_read");
if (readmarker) { if (readmarker) {
initialReadMarker = readmarker.getContent().event_id; initialReadMarker = readmarker.getContent().event_id;
} else { } else {
@ -414,7 +414,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// Get the list of actually rendered events seen in the DOM. // Get the list of actually rendered events seen in the DOM.
// This is useful to know for sure what's being shown on screen. // This is useful to know for sure what's being shown on screen.
// And we can suss out any corrupted React `key` problems. // And we can suss out any corrupted React `key` problems.
let renderedEventIds: string[]; let renderedEventIds: string[] | undefined;
try { try {
const messagePanel = this.messagePanel.current; const messagePanel = this.messagePanel.current;
if (messagePanel) { if (messagePanel) {
@ -422,7 +422,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
if (messagePanelNode) { if (messagePanelNode) {
const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]"); const actuallyRenderedEvents = messagePanelNode.querySelectorAll("[data-event-id]");
renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => { renderedEventIds = [...actuallyRenderedEvents].map((renderedEvent) => {
return renderedEvent.getAttribute("data-event-id"); return renderedEvent.getAttribute("data-event-id")!;
}); });
} }
} }
@ -432,8 +432,8 @@ class TimelinePanel extends React.Component<IProps, IState> {
// Get the list of events and threads for the room as seen by the // Get the list of events and threads for the room as seen by the
// matrix-js-sdk. // matrix-js-sdk.
let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[]; let serializedEventIdsFromTimelineSets: { [key: string]: string[] }[] | undefined;
let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[]; let serializedEventIdsFromThreadsTimelineSets: { [key: string]: string[] }[] | undefined;
const serializedThreadsMap: { [key: string]: any } = {}; const serializedThreadsMap: { [key: string]: any } = {};
if (room) { if (room) {
const timelineSets = room.getTimelineSets(); const timelineSets = room.getTimelineSets();
@ -469,15 +469,15 @@ class TimelinePanel extends React.Component<IProps, IState> {
} }
} }
let timelineWindowEventIds: string[]; let timelineWindowEventIds: string[] | undefined;
try { try {
timelineWindowEventIds = this.timelineWindow.getEvents().map((ev) => ev.getId()); timelineWindowEventIds = this.timelineWindow?.getEvents().map((ev) => ev.getId()!);
} catch (err) { } catch (err) {
logger.error(`onDumpDebugLogs: Failed to get event IDs from the timelineWindow`, err); logger.error(`onDumpDebugLogs: Failed to get event IDs from the timelineWindow`, err);
} }
let pendingEventIds: string[]; let pendingEventIds: string[] | undefined;
try { try {
pendingEventIds = this.props.timelineSet.getPendingEvents().map((ev) => ev.getId()); pendingEventIds = this.props.timelineSet.getPendingEvents().map((ev) => ev.getId()!);
} catch (err) { } catch (err) {
logger.error(`onDumpDebugLogs: Failed to get pending event IDs`, err); logger.error(`onDumpDebugLogs: Failed to get pending event IDs`, err);
} }
@ -491,10 +491,10 @@ class TimelinePanel extends React.Component<IProps, IState> {
`\tserializedEventIdsFromThreadsTimelineSets=` + `\tserializedEventIdsFromThreadsTimelineSets=` +
`${JSON.stringify(serializedEventIdsFromThreadsTimelineSets)}\n` + `${JSON.stringify(serializedEventIdsFromThreadsTimelineSets)}\n` +
`\tserializedThreadsMap=${JSON.stringify(serializedThreadsMap)}\n` + `\tserializedThreadsMap=${JSON.stringify(serializedThreadsMap)}\n` +
`\ttimelineWindowEventIds(${timelineWindowEventIds.length})=${JSON.stringify( `\ttimelineWindowEventIds(${timelineWindowEventIds?.length})=${JSON.stringify(
timelineWindowEventIds, timelineWindowEventIds,
)}\n` + )}\n` +
`\tpendingEventIds(${pendingEventIds.length})=${JSON.stringify(pendingEventIds)}`, `\tpendingEventIds(${pendingEventIds?.length})=${JSON.stringify(pendingEventIds)}`,
); );
}; };
@ -560,7 +560,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
return Promise.resolve(false); return Promise.resolve(false);
} }
if (!this.timelineWindow.canPaginate(dir)) { if (!this.timelineWindow?.canPaginate(dir)) {
debuglog("can't", dir, "paginate any further"); debuglog("can't", dir, "paginate any further");
this.setState<null>({ [canPaginateKey]: false }); this.setState<null>({ [canPaginateKey]: false });
return Promise.resolve(false); return Promise.resolve(false);
@ -576,7 +576,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => { return this.onPaginationRequest(this.timelineWindow, dir, PAGINATE_SIZE).then((r) => {
if (this.unmounted) { if (this.unmounted) {
return; return false;
} }
debuglog("paginate complete backwards:" + backwards + "; success:" + r); debuglog("paginate complete backwards:" + backwards + "; success:" + r);
@ -595,7 +595,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// paginate in the other where we previously could not. // paginate in the other where we previously could not.
const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS; const otherDirection = backwards ? EventTimeline.FORWARDS : EventTimeline.BACKWARDS;
const canPaginateOtherWayKey = backwards ? "canForwardPaginate" : "canBackPaginate"; const canPaginateOtherWayKey = backwards ? "canForwardPaginate" : "canBackPaginate";
if (!this.state[canPaginateOtherWayKey] && this.timelineWindow.canPaginate(otherDirection)) { if (!this.state[canPaginateOtherWayKey] && this.timelineWindow?.canPaginate(otherDirection)) {
debuglog("can now", otherDirection, "paginate again"); debuglog("can now", otherDirection, "paginate again");
newState[canPaginateOtherWayKey] = true; newState[canPaginateOtherWayKey] = true;
} }
@ -666,7 +666,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
private onRoomTimeline = ( private onRoomTimeline = (
ev: MatrixEvent, ev: MatrixEvent,
room: Room | null, room: Room | undefined,
toStartOfTimeline: boolean, toStartOfTimeline: boolean,
removed: boolean, removed: boolean,
data: IRoomTimelineData, data: IRoomTimelineData,
@ -1008,7 +1008,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
if ( if (
currentRREventId && currentRREventId &&
currentRREventIndex === null && currentRREventIndex === null &&
this.timelineWindow.canPaginate(EventTimeline.FORWARDS) this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)
) { ) {
shouldSendRR = false; shouldSendRR = false;
} }
@ -1149,7 +1149,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const events = this.timelineWindow.getEvents(); const events = this.timelineWindow.getEvents();
// first find where the current RM is // first find where the current RM is
let i; let i: number;
for (i = 0; i < events.length; i++) { for (i = 0; i < events.length; i++) {
if (events[i].getId() == this.state.readMarkerEventId) { if (events[i].getId() == this.state.readMarkerEventId) {
break; break;
@ -1182,7 +1182,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// //
// Otherwise, reload the timeline rather than trying to paginate // Otherwise, reload the timeline rather than trying to paginate
// through all of space-time. // through all of space-time.
if (this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) { if (this.timelineWindow?.canPaginate(EventTimeline.FORWARDS)) {
this.loadTimeline(); this.loadTimeline();
} else { } else {
this.messagePanel.current?.scrollToBottom(); this.messagePanel.current?.scrollToBottom();
@ -1231,7 +1231,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// Look up the timestamp if we can find it // Look up the timestamp if we can find it
const tl = this.props.timelineSet.getTimelineForEvent(rmId ?? ""); const tl = this.props.timelineSet.getTimelineForEvent(rmId ?? "");
let rmTs: number; let rmTs: number | undefined;
if (tl) { if (tl) {
const event = tl.getEvents().find((e) => { const event = tl.getEvents().find((e) => {
return e.getId() == rmId; return e.getId() == rmId;
@ -1264,7 +1264,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
* *
* returns null if we are not mounted. * returns null if we are not mounted.
*/ */
public getScrollState = (): IScrollState => { public getScrollState = (): IScrollState | null => {
if (!this.messagePanel.current) { if (!this.messagePanel.current) {
return null; return null;
} }
@ -1277,7 +1277,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
// -1: read marker is above the window // -1: read marker is above the window
// 0: read marker is visible // 0: read marker is visible
// +1: read marker is below the window // +1: read marker is below the window
public getReadMarkerPosition = (): number => { public getReadMarkerPosition = (): number | null => {
if (!this.props.manageReadMarkers) return null; if (!this.props.manageReadMarkers) return null;
if (!this.messagePanel.current) return null; if (!this.messagePanel.current) return null;
@ -1449,7 +1449,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
this.setState({ timelineLoading: false }); this.setState({ timelineLoading: false });
logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error); logger.error(`Error loading timeline panel at ${this.props.timelineSet.room?.roomId}/${eventId}`, error);
let onFinished: () => void; let onFinished: (() => void) | undefined;
// if we were given an event ID, then when the user closes the // if we were given an event ID, then when the user closes the
// dialog, let's jump to the end of the timeline. If we weren't, // dialog, let's jump to the end of the timeline. If we weren't,
@ -1745,7 +1745,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
const wrapperRect = messagePanelNode.getBoundingClientRect(); const wrapperRect = messagePanelNode.getBoundingClientRect();
const myUserId = MatrixClientPeg.get().credentials.userId; const myUserId = MatrixClientPeg.get().credentials.userId;
const isNodeInView = (node: HTMLElement): boolean => { const isNodeInView = (node?: HTMLElement): boolean => {
if (node) { if (node) {
const boundingRect = node.getBoundingClientRect(); const boundingRect = node.getBoundingClientRect();
if ( if (
@ -1828,7 +1828,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
return null; return null;
} }
const myUserId = client.credentials.userId; const myUserId = client.getSafeUserId();
const receiptStore: ReadReceipt<any, any> = this.props.timelineSet.thread ?? this.props.timelineSet.room; const receiptStore: ReadReceipt<any, any> = this.props.timelineSet.thread ?? this.props.timelineSet.room;
return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized); return receiptStore?.getEventReadUpTo(myUserId, ignoreSynthesized);
} }
@ -1943,7 +1943,7 @@ class TimelinePanel extends React.Component<IProps, IState> {
canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0} canBackPaginate={this.state.canBackPaginate && this.state.firstVisibleEventIndex === 0}
showUrlPreview={this.props.showUrlPreview} showUrlPreview={this.props.showUrlPreview}
showReadReceipts={this.props.showReadReceipts} showReadReceipts={this.props.showReadReceipts}
ourUserId={MatrixClientPeg.get().credentials.userId} ourUserId={MatrixClientPeg.get().getSafeUserId()}
stickyBottom={stickyBottom} stickyBottom={stickyBottom}
onScroll={this.onMessageListScroll} onScroll={this.onMessageListScroll}
onFillRequest={this.onMessageListFillRequest} onFillRequest={this.onMessageListFillRequest}

View file

@ -145,7 +145,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
} else { } else {
const theme = SettingsStore.getValue("theme"); const theme = SettingsStore.getValue("theme");
if (theme.startsWith("custom-")) { if (theme.startsWith("custom-")) {
return getCustomTheme(theme.substring("custom-".length)).is_dark; return !!getCustomTheme(theme.substring("custom-".length)).is_dark;
} }
return theme === "dark"; return theme === "dark";
} }
@ -236,7 +236,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab SettingsStore.setValue("theme", null, SettingLevel.DEVICE, newTheme); // set at same level as Appearance tab
}; };
private onSettingsOpen = (ev: ButtonEvent, tabId: string): void => { private onSettingsOpen = (ev: ButtonEvent, tabId?: string): void => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -319,7 +319,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
); );
} }
let homeButton = null; let homeButton: JSX.Element | undefined;
if (this.hasHomePage) { if (this.hasHomePage) {
homeButton = ( homeButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -330,7 +330,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
); );
} }
let feedbackButton; let feedbackButton: JSX.Element | undefined;
if (SettingsStore.getValue(UIFeature.Feedback)) { if (SettingsStore.getValue(UIFeature.Feedback)) {
feedbackButton = ( feedbackButton = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -357,7 +357,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings" iconClassName="mx_UserMenu_iconSettings"
label={_t("All settings")} label={_t("All settings")}
onClick={(e) => this.onSettingsOpen(e, null)} onClick={(e) => this.onSettingsOpen(e)}
/> />
{feedbackButton} {feedbackButton}
<IconizedContextMenuOption <IconizedContextMenuOption
@ -376,7 +376,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings" iconClassName="mx_UserMenu_iconSettings"
label={_t("Settings")} label={_t("Settings")}
onClick={(e) => this.onSettingsOpen(e, null)} onClick={(e) => this.onSettingsOpen(e)}
/> />
{feedbackButton} {feedbackButton}
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
@ -395,9 +395,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
{OwnProfileStore.instance.displayName} {OwnProfileStore.instance.displayName}
</span> </span>
<span className="mx_UserMenu_contextMenu_userId"> <span className="mx_UserMenu_contextMenu_userId">
{UserIdentifierCustomisations.getDisplayUserIdentifier(MatrixClientPeg.get().getUserId(), { {UserIdentifierCustomisations.getDisplayUserIdentifier(
withDisplayName: true, MatrixClientPeg.get().getSafeUserId(),
})} {
withDisplayName: true,
},
)}
</span> </span>
</div> </div>
@ -426,7 +429,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
const displayName = OwnProfileStore.instance.displayName || userId; const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
let name: JSX.Element; let name: JSX.Element | undefined;
if (!this.props.isPanelCollapsed) { if (!this.props.isPanelCollapsed) {
name = <div className="mx_UserMenu_name">{displayName}</div>; name = <div className="mx_UserMenu_name">{displayName}</div>;
} }

View file

@ -68,7 +68,7 @@ export default class ViewSource extends React.Component<IProps, IState> {
}; };
if (isEncrypted) { if (isEncrypted) {
const copyDecryptedFunc = (): string => { const copyDecryptedFunc = (): string => {
return stringify(decryptedEventSource); return stringify(decryptedEventSource || {});
}; };
return ( return (
<> <>
@ -117,7 +117,7 @@ export default class ViewSource extends React.Component<IProps, IState> {
return ( return (
<MatrixClientContext.Consumer> <MatrixClientContext.Consumer>
{(cli) => ( {(cli) => (
<DevtoolsContext.Provider value={{ room: cli.getRoom(roomId) }}> <DevtoolsContext.Provider value={{ room: cli.getRoom(roomId)! }}>
<StateEventEditor onBack={this.onBack} mxEvent={mxEvent} /> <StateEventEditor onBack={this.onBack} mxEvent={mxEvent} />
</DevtoolsContext.Provider> </DevtoolsContext.Provider>
)} )}
@ -128,7 +128,7 @@ export default class ViewSource extends React.Component<IProps, IState> {
return ( return (
<MatrixClientContext.Consumer> <MatrixClientContext.Consumer>
{(cli) => ( {(cli) => (
<DevtoolsContext.Provider value={{ room: cli.getRoom(roomId) }}> <DevtoolsContext.Provider value={{ room: cli.getRoom(roomId)! }}>
<TimelineEventEditor onBack={this.onBack} mxEvent={mxEvent} /> <TimelineEventEditor onBack={this.onBack} mxEvent={mxEvent} />
</DevtoolsContext.Provider> </DevtoolsContext.Provider>
)} )}

View file

@ -395,7 +395,7 @@ export default class ForgotPassword extends React.Component<Props, State> {
button: _t("Continue"), button: _t("Continue"),
}); });
const [confirmed] = await finished; const [confirmed] = await finished;
return confirmed; return !!confirmed;
} }
public renderCheckEmail(): JSX.Element { public renderCheckEmail(): JSX.Element {

View file

@ -368,7 +368,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
isDefaultServer = true; isDefaultServer = true;
} }
const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl : null; const fallbackHsUrl = isDefaultServer ? this.props.fallbackHsUrl! : null;
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
@ -514,7 +514,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
// this is the ideal order we want to show the flows in // this is the ideal order we want to show the flows in
const order = ["m.login.password", "m.login.sso"]; const order = ["m.login.password", "m.login.sso"];
const flows = filterBoolean(order.map((type) => this.state.flows.find((flow) => flow.type === type))); const flows = filterBoolean(order.map((type) => this.state.flows?.find((flow) => flow.type === type)));
return ( return (
<React.Fragment> <React.Fragment>
{flows.map((flow) => { {flows.map((flow) => {
@ -546,7 +546,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
}; };
private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => { private renderSsoStep = (loginType: "cas" | "sso"): JSX.Element => {
const flow = this.state.flows.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow; const flow = this.state.flows?.find((flow) => flow.type === "m.login." + loginType) as ISSOFlow;
return ( return (
<SSOButtons <SSOButtons
@ -554,7 +554,7 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
flow={flow} flow={flow}
loginType={loginType} loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin} fragmentAfterLogin={this.props.fragmentAfterLogin}
primary={!this.state.flows.find((flow) => flow.type === "m.login.password")} primary={!this.state.flows?.find((flow) => flow.type === "m.login.password")}
action={SSOAction.LOGIN} action={SSOAction.LOGIN}
/> />
); );

View file

@ -88,7 +88,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
private onVerifyClick = (): void => { private onVerifyClick = (): void => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId(); const userId = cli.getSafeUserId();
const requestPromise = cli.requestVerification(userId); const requestPromise = cli.requestVerification(userId);
// We need to call onFinished now to close this dialog, and // We need to call onFinished now to close this dialog, and
@ -212,7 +212,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
{useRecoveryKeyButton} {useRecoveryKeyButton}
</div> </div>
<div className="mx_SetupEncryptionBody_reset"> <div className="mx_SetupEncryptionBody_reset">
{_t("Forgotten or lost all recovery methods? <a>Reset all</a>", null, { {_t("Forgotten or lost all recovery methods? <a>Reset all</a>", undefined, {
a: (sub) => ( a: (sub) => (
<AccessibleButton <AccessibleButton
kind="link_inline" kind="link_inline"
@ -228,7 +228,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
); );
} }
} else if (phase === Phase.Done) { } else if (phase === Phase.Done) {
let message; let message: JSX.Element;
if (this.state.backupInfo) { if (this.state.backupInfo) {
message = ( message = (
<p> <p>

View file

@ -23,7 +23,7 @@ import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import * as Lifecycle from "../../../Lifecycle"; import * as Lifecycle from "../../../Lifecycle";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { IMatrixClientCreds, MatrixClientPeg } from "../../../MatrixClientPeg";
import { sendLoginRequest } from "../../../Login"; import { sendLoginRequest } from "../../../Login";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform"; import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY } from "../../../BasePlatform";
@ -159,7 +159,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
device_id: MatrixClientPeg.get().getDeviceId(), device_id: MatrixClientPeg.get().getDeviceId(),
}; };
let credentials = null; let credentials: IMatrixClientCreds;
try { try {
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
} catch (e) { } catch (e) {
@ -192,7 +192,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
device_id: MatrixClientPeg.get().getDeviceId(), device_id: MatrixClientPeg.get().getDeviceId(),
}; };
let credentials = null; let credentials: IMatrixClientCreds;
try { try {
credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams); credentials = await sendLoginRequest(hsUrl, isUrl, loginType, loginParams);
} catch (e) { } catch (e) {
@ -212,7 +212,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
} }
private renderPasswordForm(introText: Optional<string>): JSX.Element { private renderPasswordForm(introText: Optional<string>): JSX.Element {
let error: JSX.Element = null; let error: JSX.Element | undefined;
if (this.state.errorText) { if (this.state.errorText) {
error = <span className="mx_Login_error">{this.state.errorText}</span>; error = <span className="mx_Login_error">{this.state.errorText}</span>;
} }
@ -267,7 +267,7 @@ export default class SoftLogout extends React.Component<IProps, IState> {
return <Spinner />; return <Spinner />;
} }
let introText = null; // null is translated to something area specific in this function let introText: string | null = null; // null is translated to something area specific in this function
if (this.state.keyBackupNeeded) { if (this.state.keyBackupNeeded) {
introText = _t( introText = _t(
"Regain access to your account and recover encryption keys stored in this session. " + "Regain access to your account and recover encryption keys stored in this session. " +

View file

@ -27,7 +27,7 @@ interface Props {
export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren<Props>): JSX.Element { export function AuthHeaderDisplay({ title, icon, serverPicker, children }: PropsWithChildren<Props>): JSX.Element {
const context = useContext(AuthHeaderContext); const context = useContext(AuthHeaderContext);
if (!context) { if (!context) {
return null; return <></>;
} }
const current = context.state.length ? context.state[0] : null; const current = context.state.length ? context.state[0] : null;
return ( return (

View file

@ -61,7 +61,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
RoomListStore.instance.getTagsForRoom(room), RoomListStore.instance.getTagsForRoom(room),
); );
let leaveOption: JSX.Element; let leaveOption: JSX.Element | undefined;
if (roomTags.includes(DefaultTagID.Archived)) { if (roomTags.includes(DefaultTagID.Archived)) {
const onForgetRoomClick = (ev: ButtonEvent): void => { const onForgetRoomClick = (ev: ButtonEvent): void => {
ev.preventDefault(); ev.preventDefault();
@ -112,7 +112,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
const isVideoRoom = const isVideoRoom =
videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom())); videoRoomsEnabled && (room.isElementVideoRoom() || (elementCallVideoRoomsEnabled && room.isCallRoom()));
let inviteOption: JSX.Element; let inviteOption: JSX.Element | undefined;
if (room.canInvite(cli.getUserId()!) && !isDm) { if (room.canInvite(cli.getUserId()!) && !isDm) {
const onInviteClick = (ev: ButtonEvent): void => { const onInviteClick = (ev: ButtonEvent): void => {
ev.preventDefault(); ev.preventDefault();
@ -136,9 +136,9 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
); );
} }
let favouriteOption: JSX.Element; let favouriteOption: JSX.Element | undefined;
let lowPriorityOption: JSX.Element; let lowPriorityOption: JSX.Element | undefined;
let notificationOption: JSX.Element; let notificationOption: JSX.Element | undefined;
if (room.getMyMembership() === "join") { if (room.getMyMembership() === "join") {
const isFavorite = roomTags.includes(DefaultTagID.Favourite); const isFavorite = roomTags.includes(DefaultTagID.Favourite);
favouriteOption = ( favouriteOption = (
@ -208,8 +208,8 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
); );
} }
let peopleOption: JSX.Element; let peopleOption: JSX.Element | undefined;
let copyLinkOption: JSX.Element; let copyLinkOption: JSX.Element | undefined;
if (!isDm) { if (!isDm) {
peopleOption = ( peopleOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -247,7 +247,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
); );
} }
let filesOption: JSX.Element; let filesOption: JSX.Element | undefined;
if (!isVideoRoom) { if (!isVideoRoom) {
filesOption = ( filesOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -266,9 +266,9 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
} }
const pinningEnabled = useFeatureEnabled("feature_pinning"); const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled && room)?.length; const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
let pinsOption: JSX.Element; let pinsOption: JSX.Element | undefined;
if (pinningEnabled && !isVideoRoom) { if (pinningEnabled && !isVideoRoom) {
pinsOption = ( pinsOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -288,7 +288,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
); );
} }
let widgetsOption: JSX.Element; let widgetsOption: JSX.Element | undefined;
if (!isVideoRoom) { if (!isVideoRoom) {
widgetsOption = ( widgetsOption = (
<IconizedContextMenuOption <IconizedContextMenuOption
@ -306,7 +306,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
); );
} }
let exportChatOption: JSX.Element; let exportChatOption: JSX.Element | undefined;
if (!isVideoRoom) { if (!isVideoRoom) {
exportChatOption = ( exportChatOption = (
<IconizedContextMenuOption <IconizedContextMenuOption

View file

@ -40,6 +40,7 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import LazyRenderList from "../elements/LazyRenderList"; import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings"; import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays";
// These values match CSS // These values match CSS
const ROW_HEIGHT = 32 + 12; const ROW_HEIGHT = 32 + 12;
@ -56,7 +57,7 @@ interface IProps {
export const Entry: React.FC<{ export const Entry: React.FC<{
room: Room; room: Room;
checked: boolean; checked: boolean;
onChange(value: boolean): void; onChange?(value: boolean): void;
}> = ({ room, checked, onChange }) => { }> = ({ room, checked, onChange }) => {
return ( return (
<label className="mx_AddExistingToSpace_entry"> <label className="mx_AddExistingToSpace_entry">
@ -67,7 +68,7 @@ export const Entry: React.FC<{
)} )}
<span className="mx_AddExistingToSpace_entry_name">{room.name}</span> <span className="mx_AddExistingToSpace_entry_name">{room.name}</span>
<StyledCheckbox <StyledCheckbox
onChange={onChange ? (e) => onChange(e.currentTarget.checked) : null} onChange={onChange ? (e) => onChange(e.currentTarget.checked) : undefined}
checked={checked} checked={checked}
disabled={!onChange} disabled={!onChange}
/> />
@ -150,8 +151,8 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
}); });
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>()); const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null); const [progress, setProgress] = useState<number | null>(null);
const [error, setError] = useState<Error>(null); const [error, setError] = useState<Error | null>(null);
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim(); const lcQuery = query.toLowerCase().trim();
@ -164,7 +165,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
if (lcQuery) { if (lcQuery) {
const matcher = new QueryMatcher<Room>(visibleRooms, { const matcher = new QueryMatcher<Room>(visibleRooms, {
keys: ["name"], keys: ["name"],
funcs: [(r) => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)], funcs: [(r) => filterBoolean([r.getCanonicalAlias(), ...r.getAltAliases()])],
shouldMatchWordsOnly: false, shouldMatchWordsOnly: false,
}); });
@ -172,7 +173,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
} }
const joinRule = space.getJoinRule(); const joinRule = space.getJoinRule();
return sortRooms(rooms).reduce( return sortRooms(rooms).reduce<[spaces: Room[], rooms: Room[], dms: Room[]]>(
(arr, room) => { (arr, room) => {
if (room.isSpaceRoom()) { if (room.isSpaceRoom()) {
if (room !== space && !existingSubspacesSet.has(room)) { if (room !== space && !existingSubspacesSet.has(room)) {
@ -289,7 +290,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
} }
setSelectedToAdd(new Set(selectedToAdd)); setSelectedToAdd(new Set(selectedToAdd));
} }
: null; : undefined;
// only count spaces when alone as they're shown on a separate modal all on their own // only count spaces when alone as they're shown on a separate modal all on their own
const numSpaces = spacesRenderer && !dmsRenderer && !roomsRenderer ? spaces.length : 0; const numSpaces = spacesRenderer && !dmsRenderer && !roomsRenderer ? spaces.length : 0;
@ -373,7 +374,7 @@ const defaultRendererFactory =
? (checked: boolean) => { ? (checked: boolean) => {
onChange(checked, room); onChange(checked, room);
} }
: null : undefined
} }
/> />
)} )}
@ -397,7 +398,7 @@ export const SubspaceSelector: React.FC<ISubspaceSelectorProps> = ({ title, spac
return [ return [
space, space,
...SpaceStore.instance.getChildSpaces(space.roomId).filter((space) => { ...SpaceStore.instance.getChildSpaces(space.roomId).filter((space) => {
return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.credentials.userId); return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.getSafeUserId());
}), }),
]; ];
}, [space]); }, [space]);

View file

@ -153,12 +153,11 @@ export default class BaseDialog extends React.Component<IProps> {
return ( return (
<MatrixClientContext.Provider value={this.matrixClient}> <MatrixClientContext.Provider value={this.matrixClient}>
<PosthogScreenTracker screenName={this.props.screenName} /> {this.props.screenName && <PosthogScreenTracker screenName={this.props.screenName} />}
<FocusLock <FocusLock
returnFocus={true} returnFocus={true}
lockProps={lockProps} lockProps={lockProps}
className={classNames({ className={classNames(this.props.className, {
[this.props.className]: true,
mx_Dialog_fixedWidth: this.props.fixedWidth, mx_Dialog_fixedWidth: this.props.fixedWidth,
})} })}
> >

View file

@ -74,6 +74,7 @@ import { InviteKind } from "./InviteDialogTypes";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import { privateShouldBeEncrypted } from "../../../utils/rooms"; import { privateShouldBeEncrypted } from "../../../utils/rooms";
import { NonEmptyArray } from "../../../@types/common";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -1421,10 +1422,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
let dialogContent; let dialogContent;
if (this.props.kind === InviteKind.CallTransfer) { if (this.props.kind === InviteKind.CallTransfer) {
const tabs: Tab[] = []; const tabs: NonEmptyArray<Tab> = [
tabs.push(
new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection), new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection),
); ];
const backspaceButton = <DialPadBackspaceButton onBackspacePress={this.onDeletePress} />; const backspaceButton = <DialPadBackspaceButton onBackspacePress={this.onDeletePress} />;

View file

@ -34,6 +34,7 @@ import BaseDialog from "./BaseDialog";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab"; import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
import { ActionPayload } from "../../../dispatcher/payloads"; import { ActionPayload } from "../../../dispatcher/payloads";
import { NonEmptyArray } from "../../../@types/common";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB"; export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB"; export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB";
@ -85,11 +86,11 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
private onRoomName = (): void => { private onRoomName = (): void => {
this.setState({ this.setState({
roomName: MatrixClientPeg.get().getRoom(this.props.roomId).name, roomName: MatrixClientPeg.get().getRoom(this.props.roomId)?.name ?? "",
}); });
}; };
private getTabs(): Tab[] { private getTabs(): NonEmptyArray<Tab> {
const tabs: Tab[] = []; const tabs: Tab[] = [];
tabs.push( tabs.push(
@ -178,7 +179,7 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
); );
} }
return tabs; return tabs as NonEmptyArray<Tab>;
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePreferencesPayload"; import { SpacePreferenceTab } from "../../../dispatcher/payloads/OpenSpacePreferencesPayload";
import { NonEmptyArray } from "../../../@types/common";
interface IProps { interface IProps {
space: Room; space: Room;
@ -69,7 +70,7 @@ const SpacePreferencesAppearanceTab: React.FC<Pick<IProps, "space">> = ({ space
}; };
const SpacePreferencesDialog: React.FC<IProps> = ({ space, initialTabId, onFinished }) => { const SpacePreferencesDialog: React.FC<IProps> = ({ space, initialTabId, onFinished }) => {
const tabs = [ const tabs: NonEmptyArray<Tab> = [
new Tab( new Tab(
SpacePreferenceTab.Appearance, SpacePreferenceTab.Appearance,
_td("Appearance"), _td("Appearance"),

View file

@ -30,6 +30,7 @@ import { UIFeature } from "../../../settings/UIFeature";
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { NonEmptyArray } from "../../../@types/common";
export enum SpaceSettingsTab { export enum SpaceSettingsTab {
General = "SPACE_GENERAL_TAB", General = "SPACE_GENERAL_TAB",
@ -79,7 +80,7 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />, <AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
) )
: null, : null,
].filter(Boolean); ].filter(Boolean) as NonEmptyArray<Tab>;
}, [cli, space, onFinished]); }, [cli, space, onFinished]);
return ( return (

View file

@ -36,6 +36,7 @@ import SidebarUserSettingsTab from "../settings/tabs/user/SidebarUserSettingsTab
import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsTab"; import KeyboardUserSettingsTab from "../settings/tabs/user/KeyboardUserSettingsTab";
import SessionManagerTab from "../settings/tabs/user/SessionManagerTab"; import SessionManagerTab from "../settings/tabs/user/SessionManagerTab";
import { UserTab } from "./UserTab"; import { UserTab } from "./UserTab";
import { NonEmptyArray } from "../../../@types/common";
interface IProps { interface IProps {
initialTabId?: UserTab; initialTabId?: UserTab;
@ -80,7 +81,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
this.setState({ newSessionManagerEnabled: newValue }); this.setState({ newSessionManagerEnabled: newValue });
}; };
private getTabs(): Tab[] { private getTabs(): NonEmptyArray<Tab> {
const tabs: Tab[] = []; const tabs: Tab[] = [];
tabs.push( tabs.push(
@ -207,7 +208,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
), ),
); );
return tabs; return tabs as NonEmptyArray<Tab>;
} }
public render(): React.ReactNode { public render(): React.ReactNode {

View file

@ -23,6 +23,7 @@ import DialogButtons from "./DialogButtons";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView"; import TabbedView, { Tab, TabLocation } from "../../structures/TabbedView";
import PlatformPeg from "../../../PlatformPeg"; import PlatformPeg from "../../../PlatformPeg";
import { NonEmptyArray } from "../../../@types/common";
export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> { export function getDesktopCapturerSources(): Promise<Array<DesktopCapturerSource>> {
const options: GetSourcesOptions = { const options: GetSourcesOptions = {
@ -80,7 +81,7 @@ export interface PickerIState {
selectedSource: DesktopCapturerSource | null; selectedSource: DesktopCapturerSource | null;
} }
export interface PickerIProps { export interface PickerIProps {
onFinished(sourceId: string): void; onFinished(sourceId?: string): void;
} }
export default class DesktopCapturerSourcePicker extends React.Component<PickerIProps, PickerIState> { export default class DesktopCapturerSourcePicker extends React.Component<PickerIProps, PickerIState> {
@ -129,7 +130,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<PickerI
}; };
private onCloseClick = (): void => { private onCloseClick = (): void => {
this.props.onFinished(null); this.props.onFinished();
}; };
private getTab(type: "screen" | "window", label: string): Tab { private getTab(type: "screen" | "window", label: string): Tab {
@ -150,7 +151,7 @@ export default class DesktopCapturerSourcePicker extends React.Component<PickerI
} }
public render(): React.ReactNode { public render(): React.ReactNode {
const tabs = [ const tabs: NonEmptyArray<Tab> = [
this.getTab("screen", _t("Share entire screen")), this.getTab("screen", _t("Share entire screen")),
this.getTab("window", _t("Application window")), this.getTab("window", _t("Application window")),
]; ];

View file

@ -25,7 +25,7 @@ import { objectHasDiff } from "../../../utils/objects";
const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM"; const CUSTOM_VALUE = "SELECT_VALUE_CUSTOM";
interface IProps { interface Props<K extends undefined | string> {
value: number; value: number;
// The maximum value that can be set with the power selector // The maximum value that can be set with the power selector
maxValue: number; maxValue: number;
@ -35,13 +35,14 @@ interface IProps {
// should the user be able to change the value? false by default. // should the user be able to change the value? false by default.
disabled?: boolean; disabled?: boolean;
onChange?: (value: number, powerLevelKey: string) => void;
// Optional key to pass as the second argument to `onChange`
powerLevelKey?: string;
// The name to annotate the selector with // The name to annotate the selector with
label?: string; label?: string;
onChange(value: number, powerLevelKey: K extends undefined ? void : K): void;
// Optional key to pass as the second argument to `onChange`
powerLevelKey: K extends undefined ? void : K;
} }
interface IState { interface IState {
@ -54,13 +55,13 @@ interface IState {
custom?: boolean; custom?: boolean;
} }
export default class PowerSelector extends React.Component<IProps, IState> { export default class PowerSelector<K extends undefined | string> extends React.Component<Props<K>, IState> {
public static defaultProps: Partial<IProps> = { public static defaultProps: Partial<Props<any>> = {
maxValue: Infinity, maxValue: Infinity,
usersDefault: 0, usersDefault: 0,
}; };
public constructor(props: IProps) { public constructor(props: Props<K>) {
super(props); super(props);
this.state = { this.state = {
@ -77,7 +78,7 @@ export default class PowerSelector extends React.Component<IProps, IState> {
this.initStateFromProps(); this.initStateFromProps();
} }
public componentDidUpdate(prevProps: Readonly<IProps>): void { public componentDidUpdate(prevProps: Readonly<Props<K>>): void {
if (objectHasDiff(this.props, prevProps)) { if (objectHasDiff(this.props, prevProps)) {
this.initStateFromProps(); this.initStateFromProps();
} }

View file

@ -31,7 +31,7 @@ const defaultOptions: QRCodeToDataURLOptions = {
}; };
const QRCode: React.FC<IProps> = ({ data, className, ...options }) => { const QRCode: React.FC<IProps> = ({ data, className, ...options }) => {
const [dataUri, setUri] = React.useState<string>(null); const [dataUri, setUri] = React.useState<string | null>(null);
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
toDataURL(data, { ...defaultOptions, ...options }).then((uri) => { toDataURL(data, { ...defaultOptions, ...options }).then((uri) => {

View file

@ -63,7 +63,7 @@ interface IState {
// The loaded events to be rendered as linear-replies // The loaded events to be rendered as linear-replies
events: MatrixEvent[]; events: MatrixEvent[];
// The latest loaded event which has not yet been shown // The latest loaded event which has not yet been shown
loadedEv: MatrixEvent; loadedEv: MatrixEvent | null;
// Whether the component is still loading more events // Whether the component is still loading more events
loading: boolean; loading: boolean;
// Whether as error was encountered fetching a replied to event. // Whether as error was encountered fetching a replied to event.
@ -145,7 +145,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
} }
} }
private async getNextEvent(ev: MatrixEvent): Promise<MatrixEvent> { private async getNextEvent(ev: MatrixEvent): Promise<MatrixEvent | null> {
try { try {
const inReplyToEventId = getParentEventId(ev); const inReplyToEventId = getParentEventId(ev);
return await this.getEvent(inReplyToEventId); return await this.getEvent(inReplyToEventId);
@ -154,7 +154,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
} }
} }
private async getEvent(eventId: string): Promise<MatrixEvent> { private async getEvent(eventId: string): Promise<MatrixEvent | null> {
if (!eventId) return null; if (!eventId) return null;
const event = this.room.findEventById(eventId); const event = this.room.findEventById(eventId);
if (event) return event; if (event) return event;
@ -168,7 +168,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
// Return null as it is falsy and thus should be treated as an error (as the event cannot be resolved). // Return null as it is falsy and thus should be treated as an error (as the event cannot be resolved).
return null; return null;
} }
return this.room.findEventById(eventId); return this.room.findEventById(eventId) ?? null;
} }
public canCollapse = (): boolean => { public canCollapse = (): boolean => {
@ -182,7 +182,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
private onQuoteClick = async (event: ButtonEvent): Promise<void> => { private onQuoteClick = async (event: ButtonEvent): Promise<void> => {
const events = [this.state.loadedEv, ...this.state.events]; const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null; let loadedEv: MatrixEvent | null = null;
if (events.length > 0) { if (events.length > 0) {
loadedEv = await this.getNextEvent(events[0]); loadedEv = await this.getNextEvent(events[0]);
} }
@ -200,7 +200,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
} }
public render(): React.ReactNode { public render(): React.ReactNode {
let header = null; let header: JSX.Element | undefined;
if (this.state.err) { if (this.state.err) {
header = ( header = (
<blockquote className="mx_ReplyChain mx_ReplyChain_error"> <blockquote className="mx_ReplyChain mx_ReplyChain_error">

View file

@ -81,7 +81,7 @@ export default class Slider extends React.Component<IProps> {
/> />
)); ));
let selection = null; let selection: JSX.Element | undefined;
if (!this.props.disabled) { if (!this.props.disabled) {
const offset = this.offset(this.props.values, this.props.value); const offset = this.offset(this.props.values, this.props.value);

View file

@ -21,7 +21,7 @@ import { _t } from "../../../languageHandler";
interface IProps { interface IProps {
// The number of elements to show before truncating. If negative, no truncation is done. // The number of elements to show before truncating. If negative, no truncation is done.
truncateAt?: number; truncateAt: number;
// The className to apply to the wrapping div // The className to apply to the wrapping div
className?: string; className?: string;
// A function that returns the children to be rendered into the element. // A function that returns the children to be rendered into the element.
@ -34,7 +34,7 @@ interface IProps {
getChildCount?: () => number; getChildCount?: () => number;
// A function which will be invoked when an overflow element is required. // A function which will be invoked when an overflow element is required.
// This will be inserted after the children. // This will be inserted after the children.
createOverflowElement?: (overflowCount: number, totalCount: number) => React.ReactNode; createOverflowElement: (overflowCount: number, totalCount: number) => React.ReactNode;
children?: ReactNode; children?: ReactNode;
} }
@ -71,8 +71,8 @@ export default class TruncatedList extends React.Component<IProps> {
} }
} }
public render(): React.ReactNode { public render(): ReactNode {
let overflowNode = null; let overflowNode: ReactNode | undefined;
const totalChildren = this.getChildCount(); const totalChildren = this.getChildCount();
let upperBound = totalChildren; let upperBound = totalChildren;

View file

@ -37,6 +37,7 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex
import { ReadPinsEventId } from "./types"; import { ReadPinsEventId } from "./types";
import Heading from "../typography/Heading"; import Heading from "../typography/Heading";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { filterBoolean } from "../../../utils/arrays";
interface IProps { interface IProps {
room: Room; room: Room;
@ -44,7 +45,7 @@ interface IProps {
onClose(): void; onClose(): void;
} }
export const usePinnedEvents = (room: Room): string[] => { export const usePinnedEvents = (room?: Room): string[] => {
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]); const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
const update = useCallback( const update = useCallback(
@ -173,8 +174,7 @@ const PinnedMessagesCard: React.FC<IProps> = ({ room, onClose, permalinkCreator
}; };
// show them in reverse, with latest pinned at the top // show them in reverse, with latest pinned at the top
content = pinnedEvents content = filterBoolean(pinnedEvents)
.filter(Boolean)
.reverse() .reverse()
.map((ev) => ( .map((ev) => (
<PinnedEventTile <PinnedEventTile

View file

@ -325,7 +325,7 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose })
const memberCount = useRoomMemberCount(room); const memberCount = useRoomMemberCount(room);
const pinningEnabled = useFeatureEnabled("feature_pinning"); const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled && room)?.length; const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history"); const isPollHistoryEnabled = useFeatureEnabled("feature_poll_history");

View file

@ -44,7 +44,7 @@ const crossSigningRoomTitles: { [key in E2EState]?: string } = {
interface IProps { interface IProps {
isUser?: boolean; isUser?: boolean;
status?: E2EState | E2EStatus; status: E2EState | E2EStatus;
className?: string; className?: string;
size?: number; size?: number;
onClick?: () => void; onClick?: () => void;
@ -76,7 +76,7 @@ const E2EIcon: React.FC<IProps> = ({
className, className,
); );
let e2eTitle; let e2eTitle: string | undefined;
if (isUser) { if (isUser) {
e2eTitle = crossSigningUserTitles[status]; e2eTitle = crossSigningUserTitles[status];
} else { } else {

View file

@ -43,7 +43,7 @@ const PRESENCE_CLASS: Record<PresenceState, string> = {
unavailable: "mx_EntityTile_unavailable", unavailable: "mx_EntityTile_unavailable",
}; };
function presenceClassForMember(presenceState: PresenceState, lastActiveAgo: number, showPresence: boolean): string { function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
if (showPresence === false) { if (showPresence === false) {
return "mx_EntityTile_online_beenactive"; return "mx_EntityTile_online_beenactive";
} }
@ -74,7 +74,7 @@ interface IProps {
presenceLastTs?: number; presenceLastTs?: number;
presenceCurrentlyActive?: boolean; presenceCurrentlyActive?: boolean;
showInviteButton?: boolean; showInviteButton?: boolean;
onClick?(): void; onClick(): void;
suppressOnHover?: boolean; suppressOnHover?: boolean;
showPresence?: boolean; showPresence?: boolean;
subtextLabel?: string; subtextLabel?: string;
@ -108,7 +108,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
public render(): React.ReactNode { public render(): React.ReactNode {
const mainClassNames: Record<string, boolean> = { const mainClassNames: Record<string, boolean> = {
mx_EntityTile: true, mx_EntityTile: true,
mx_EntityTile_noHover: this.props.suppressOnHover, mx_EntityTile_noHover: !!this.props.suppressOnHover,
}; };
if (this.props.className) mainClassNames[this.props.className] = true; if (this.props.className) mainClassNames[this.props.className] = true;
@ -127,7 +127,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo) ? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
: -1; : -1;
let presenceLabel = null; let presenceLabel: JSX.Element | undefined;
if (this.props.showPresence) { if (this.props.showPresence) {
presenceLabel = ( presenceLabel = (
<PresenceLabel <PresenceLabel

View file

@ -39,7 +39,7 @@ export default class PresenceLabel extends React.Component<IProps> {
// Return duration as a string using appropriate time units // Return duration as a string using appropriate time units
// XXX: This would be better handled using a culture-aware library, but we don't use one yet. // XXX: This would be better handled using a culture-aware library, but we don't use one yet.
private getDuration(time: number): string { private getDuration(time: number): string | undefined {
if (!time) return; if (!time) return;
const t = Math.round(time / 1000); const t = Math.round(time / 1000);
const s = t % 60; const s = t % 60;
@ -61,11 +61,11 @@ export default class PresenceLabel extends React.Component<IProps> {
return _t("%(duration)sd", { duration: d }); return _t("%(duration)sd", { duration: d });
} }
private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string { private getPrettyPresence(presence?: string, activeAgo?: number, currentlyActive?: boolean): string {
// for busy presence, we ignore the 'currentlyActive' flag: they're busy whether // for busy presence, we ignore the 'currentlyActive' flag: they're busy whether
// they're active or not. It can be set while the user is active in which case // they're active or not. It can be set while the user is active in which case
// the 'active ago' ends up being 0. // the 'active ago' ends up being 0.
if (BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy"); if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy");
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) { if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo); const duration = this.getDuration(activeAgo);

View file

@ -129,9 +129,9 @@ interface IProps {
interface IState { interface IState {
verifying: boolean; verifying: boolean;
verifyError: string; verifyError: string | null;
verifyMsisdn: string; verifyMsisdn: string;
addTask: AddThreepid; addTask: AddThreepid | null;
continueDisabled: boolean; continueDisabled: boolean;
phoneCountry: string; phoneCountry: string;
newPhoneNumber: string; newPhoneNumber: string;
@ -205,7 +205,7 @@ export default class PhoneNumbers extends React.Component<IProps, IState> {
const token = this.state.newPhoneNumberCode; const token = this.state.newPhoneNumberCode;
const address = this.state.verifyMsisdn; const address = this.state.verifyMsisdn;
this.state.addTask this.state.addTask
.haveMsisdnToken(token) ?.haveMsisdnToken(token)
.then(([finished]) => { .then(([finished]) => {
let newPhoneNumber = this.state.newPhoneNumber; let newPhoneNumber = this.state.newPhoneNumber;
if (finished) { if (finished) {

View file

@ -21,6 +21,7 @@ import { RoomState, RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { throttle, get } from "lodash"; import { throttle, get } from "lodash";
import { compare } from "matrix-js-sdk/src/utils"; import { compare } from "matrix-js-sdk/src/utils";
import { IContent } from "matrix-js-sdk/src/models/event";
import { _t, _td } from "../../../../../languageHandler"; import { _t, _td } from "../../../../../languageHandler";
import { MatrixClientPeg } from "../../../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
@ -171,8 +172,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => { private onPowerLevelsChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent ? plEvent.getContent() || {} : {}; let plContent = plEvent?.getContent() ?? {};
// Clone the power levels just in case // Clone the power levels just in case
plContent = Object.assign({}, plContent); plContent = Object.assign({}, plContent);
@ -185,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value; plContent["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
} else { } else {
const keyPath = powerLevelKey.split("."); const keyPath = powerLevelKey.split(".");
let parentObj; let parentObj: IContent | undefined;
let currentObj = plContent; let currentObj = plContent;
for (const key of keyPath) { for (const key of keyPath) {
if (!currentObj[key]) { if (!currentObj[key]) {
@ -213,8 +214,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => { private onUserPowerLevelChanged = (value: number, powerLevelKey: string): void => {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId); const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents(EventType.RoomPowerLevels, ""); const plEvent = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "");
let plContent = plEvent ? plEvent.getContent() || {} : {}; let plContent = plEvent?.getContent() ?? {};
// Clone the power levels just in case // Clone the power levels just in case
plContent = Object.assign({}, plContent); plContent = Object.assign({}, plContent);

View file

@ -113,7 +113,7 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
}); });
} }
private loadRoomWidgets(room: Room): void { private loadRoomWidgets(room: Room | null): void {
if (!room) return; if (!room) return;
const roomInfo = this.roomMap.get(room.roomId) || <IRoomWidgets>{}; const roomInfo = this.roomMap.get(room.roomId) || <IRoomWidgets>{};
roomInfo.widgets = []; roomInfo.widgets = [];

View file

@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/matrix";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom"; import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
export function isLocalRoom(roomOrID: Room | string): boolean { export function isLocalRoom(roomOrID?: Room | string): boolean {
if (typeof roomOrID === "string") { if (typeof roomOrID === "string") {
return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX); return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX);
} }

View file

@ -18,6 +18,7 @@ import React from "react";
import { act, fireEvent, render } from "@testing-library/react"; import { act, fireEvent, render } from "@testing-library/react";
import TabbedView, { Tab, TabLocation } from "../../../src/components/structures/TabbedView"; import TabbedView, { Tab, TabLocation } from "../../../src/components/structures/TabbedView";
import { NonEmptyArray } from "../../../src/@types/common";
describe("<TabbedView />", () => { describe("<TabbedView />", () => {
const generalTab = new Tab("GENERAL", "General", "general", <div>general</div>); const generalTab = new Tab("GENERAL", "General", "general", <div>general</div>);
@ -25,7 +26,7 @@ describe("<TabbedView />", () => {
const securityTab = new Tab("SECURITY", "Security", "security", <div>security</div>); const securityTab = new Tab("SECURITY", "Security", "security", <div>security</div>);
const defaultProps = { const defaultProps = {
tabLocation: TabLocation.LEFT, tabLocation: TabLocation.LEFT,
tabs: [generalTab, labsTab, securityTab], tabs: [generalTab, labsTab, securityTab] as NonEmptyArray<Tab>,
}; };
const getComponent = (props = {}): React.ReactElement => <TabbedView {...defaultProps} {...props} />; const getComponent = (props = {}): React.ReactElement => <TabbedView {...defaultProps} {...props} />;
@ -58,11 +59,6 @@ describe("<TabbedView />", () => {
expect(getActiveTabBody(container)?.textContent).toEqual("security"); expect(getActiveTabBody(container)?.textContent).toEqual("security");
}); });
it("renders without error when there are no tabs", () => {
const { container } = render(getComponent({ tabs: [] }));
expect(container).toMatchSnapshot();
});
it("sets active tab on tab click", () => { it("sets active tab on tab click", () => {
const { container, getByTestId } = render(getComponent()); const { container, getByTestId } = render(getComponent());

View file

@ -69,15 +69,3 @@ exports[`<TabbedView /> renders tabs 1`] = `
</div> </div>
</div> </div>
`; `;
exports[`<TabbedView /> renders without error when there are no tabs 1`] = `
<div>
<div
class="mx_TabbedView mx_TabbedView_tabsOnLeft"
>
<div
class="mx_TabbedView_tabLabels"
/>
</div>
</div>
`;

View file

@ -38,7 +38,7 @@ exports[`<PollHistoryDialog /> renders a list of active polls when there are pol
/> />
<div <div
aria-labelledby="mx_BaseDialog_title" aria-labelledby="mx_BaseDialog_title"
class="undefined mx_Dialog_fixedWidth" class="mx_Dialog_fixedWidth"
data-focus-lock-disabled="false" data-focus-lock-disabled="false"
role="dialog" role="dialog"
> >

View file

@ -190,8 +190,8 @@ describe("BreadcrumbsStore", () => {
/** /**
* Create as many fake rooms in an array as you ask for. * Create as many fake rooms in an array as you ask for.
*/ */
function fakeRooms(howMany: number): Array<Room> { function fakeRooms(howMany: number): Room[] {
const ret = []; const ret: Room[] = [];
for (let i = 0; i < howMany; i++) { for (let i = 0; i < howMany; i++) {
ret.push(fakeRoom()); ret.push(fakeRoom());
} }