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
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X
}[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";
function getIdServerDomain(): string {
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
const idBaseUrl = MatrixClientPeg.get().idBaseUrl;
if (!idBaseUrl) {
throw new Error("Identity server not set");
}
return idBaseUrl.split("://")[1];
}
export type Binding = {
@ -190,6 +194,9 @@ export default class AddThreepid {
if (this.bind) {
const authClient = new IdentityAuthClient();
const identityAccessToken = await authClient.getAccessToken();
if (!identityAccessToken) {
throw new Error("No identity access token found");
}
await MatrixClientPeg.get().bindThreePid({
sid: this.sessionId,
client_secret: this.clientSecret,
@ -279,7 +286,9 @@ export default class AddThreepid {
* with a "message" property which contains a human-readable message detailing why
* the request failed.
*/
public async haveMsisdnToken(msisdnToken: string): Promise<any[] | undefined> {
public async haveMsisdnToken(
msisdnToken: string,
): Promise<[success?: boolean, result?: IAuthData | Error | null] | undefined> {
const authClient = new IdentityAuthClient();
const supportsSeparateAddAndBind = await MatrixClientPeg.get().doesServerSupportSeparateAddAndBind();

View file

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

View file

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

View file

@ -26,7 +26,7 @@ import { _t } from "../languageHandler";
import { AsyncActionPayload } from "../dispatcher/payloads";
import RoomListStore from "../stores/room-list/RoomListStore";
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";
export default class RoomListActions {
@ -49,10 +49,10 @@ export default class RoomListActions {
public static tagRoom(
matrixClient: MatrixClient,
room: Room,
oldTag: string,
newTag: string,
oldIndex: number | null,
newIndex: number | null,
oldTag: TagID | null,
newTag: TagID | null,
oldIndex?: number,
newIndex?: number,
): AsyncActionPayload {
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
this.clockStates.delete(mxEvent.getId()!);
if (wasLastPlaying) {
if (wasLastPlaying && this.currentPlaybackId) {
this.recentFullPlays.add(this.currentPlaybackId);
const orderClone = arrayFastClone(this.playbackIdOrder);
const last = orderClone.pop();
@ -188,8 +188,8 @@ export class PlaybackQueue {
if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) {
const lastInstance = this.playbacks.get(this.currentPlaybackId);
if (
lastInstance.currentState === PlaybackState.Playing ||
lastInstance.currentState === PlaybackState.Paused
lastInstance &&
[PlaybackState.Playing, PlaybackState.Paused].includes(lastInstance.currentState)
) {
order.push(this.currentPlaybackId);
}

View file

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

View file

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

View file

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

View file

@ -177,7 +177,8 @@ export function GenericDropdownMenu<T>({
</>
);
}
const contextMenu = menuDisplayed ? (
const contextMenu =
menuDisplayed && button.current ? (
<ContextMenu
onFinished={closeMenu}
chevronFace={ChevronFace.Top}

View file

@ -44,7 +44,7 @@ export default class IndicatorScrollbar<T extends keyof JSX.IntrinsicElements> e
> {
private autoHideScrollbar = createRef<AutoHideScrollbar<any>>();
private scrollElement: HTMLDivElement;
private likelyTrackpadUser: boolean = null;
private likelyTrackpadUser: boolean | null = null;
private checkAgainForTrackpad = 0; // ts in milliseconds to recheck this._likelyTrackpadUser
public constructor(props: IProps<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
// the necessary top/bottom padding to put the sticky header in
const listWrapper = list.parentElement; // .mx_LeftPanel_roomListWrapper
if (!listWrapper) return;
if (lastTopHeader) {
listWrapper.classList.add("mx_LeftPanel_roomListWrapper_stickyTop");
} else {

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import { _t } from "../../languageHandler";
import AutoHideScrollbar from "./AutoHideScrollbar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { PosthogScreenTracker, ScreenName } from "../../PosthogTrackers";
import { NonEmptyArray } from "../../@types/common";
/**
* Represents a tab for the TabbedView.
@ -40,7 +41,7 @@ export class Tab {
public constructor(
public readonly id: string,
public readonly label: string,
public readonly icon: string,
public readonly icon: string | null,
public readonly body: React.ReactNode,
public readonly screenName?: ScreenName,
) {}
@ -52,7 +53,7 @@ export enum TabLocation {
}
interface IProps {
tabs: Tab[];
tabs: NonEmptyArray<Tab>;
initialTabId?: string;
tabLocation: TabLocation;
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);
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";
let tabIcon = null;
let tabIcon: JSX.Element | undefined;
if (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,
});
const screenName = tab?.screenName ?? this.props.screenName;
return (
<div className={tabbedViewClasses}>
<PosthogScreenTracker screenName={tab?.screenName ?? this.props.screenName} />
{screenName && <PosthogScreenTracker screenName={screenName} />}
<div className="mx_TabbedView_tabLabels">{labels}</div>
{panel}
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,7 +88,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
private onVerifyClick = (): void => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const userId = cli.getSafeUserId();
const requestPromise = cli.requestVerification(userId);
// We need to call onFinished now to close this dialog, and
@ -212,7 +212,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
{useRecoveryKeyButton}
</div>
<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) => (
<AccessibleButton
kind="link_inline"
@ -228,7 +228,7 @@ export default class SetupEncryptionBody extends React.Component<IProps, IState>
);
}
} else if (phase === Phase.Done) {
let message;
let message: JSX.Element;
if (this.state.backupInfo) {
message = (
<p>

View file

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

View file

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

View file

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

View file

@ -40,6 +40,7 @@ import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import LazyRenderList from "../elements/LazyRenderList";
import { useSettingValue } from "../../../hooks/useSettings";
import { filterBoolean } from "../../../utils/arrays";
// These values match CSS
const ROW_HEIGHT = 32 + 12;
@ -56,7 +57,7 @@ interface IProps {
export const Entry: React.FC<{
room: Room;
checked: boolean;
onChange(value: boolean): void;
onChange?(value: boolean): void;
}> = ({ room, checked, onChange }) => {
return (
<label className="mx_AddExistingToSpace_entry">
@ -67,7 +68,7 @@ export const Entry: React.FC<{
)}
<span className="mx_AddExistingToSpace_entry_name">{room.name}</span>
<StyledCheckbox
onChange={onChange ? (e) => onChange(e.currentTarget.checked) : null}
onChange={onChange ? (e) => onChange(e.currentTarget.checked) : undefined}
checked={checked}
disabled={!onChange}
/>
@ -150,8 +151,8 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
});
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
const [progress, setProgress] = useState<number>(null);
const [error, setError] = useState<Error>(null);
const [progress, setProgress] = useState<number | null>(null);
const [error, setError] = useState<Error | null>(null);
const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase().trim();
@ -164,7 +165,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
if (lcQuery) {
const matcher = new QueryMatcher<Room>(visibleRooms, {
keys: ["name"],
funcs: [(r) => [r.getCanonicalAlias(), ...r.getAltAliases()].filter(Boolean)],
funcs: [(r) => filterBoolean([r.getCanonicalAlias(), ...r.getAltAliases()])],
shouldMatchWordsOnly: false,
});
@ -172,7 +173,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
}
const joinRule = space.getJoinRule();
return sortRooms(rooms).reduce(
return sortRooms(rooms).reduce<[spaces: Room[], rooms: Room[], dms: Room[]]>(
(arr, room) => {
if (room.isSpaceRoom()) {
if (room !== space && !existingSubspacesSet.has(room)) {
@ -289,7 +290,7 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
}
setSelectedToAdd(new Set(selectedToAdd));
}
: null;
: undefined;
// 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;
@ -373,7 +374,7 @@ const defaultRendererFactory =
? (checked: boolean) => {
onChange(checked, room);
}
: null
: undefined
}
/>
)}
@ -397,7 +398,7 @@ export const SubspaceSelector: React.FC<ISubspaceSelectorProps> = ({ title, spac
return [
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]);

View file

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

View file

@ -74,6 +74,7 @@ import { InviteKind } from "./InviteDialogTypes";
import Modal from "../../../Modal";
import dis from "../../../dispatcher/dispatcher";
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.
/* eslint-disable camelcase */
@ -1421,10 +1422,9 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
let dialogContent;
if (this.props.kind === InviteKind.CallTransfer) {
const tabs: Tab[] = [];
tabs.push(
const tabs: NonEmptyArray<Tab> = [
new Tab(TabId.UserDirectory, _td("User Directory"), "mx_InviteDialog_userDirectoryIcon", usersSection),
);
];
const backspaceButton = <DialPadBackspaceButton onBackspacePress={this.onDeletePress} />;

View file

@ -34,6 +34,7 @@ import BaseDialog from "./BaseDialog";
import { Action } from "../../../dispatcher/actions";
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NonEmptyArray } from "../../../@types/common";
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_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 => {
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[] = [];
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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,7 +31,7 @@ const defaultOptions: QRCodeToDataURLOptions = {
};
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(() => {
let cancelled = false;
toDataURL(data, { ...defaultOptions, ...options }).then((uri) => {

View file

@ -63,7 +63,7 @@ interface IState {
// The loaded events to be rendered as linear-replies
events: MatrixEvent[];
// The latest loaded event which has not yet been shown
loadedEv: MatrixEvent;
loadedEv: MatrixEvent | null;
// Whether the component is still loading more events
loading: boolean;
// 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 {
const inReplyToEventId = getParentEventId(ev);
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;
const event = this.room.findEventById(eventId);
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;
}
return this.room.findEventById(eventId);
return this.room.findEventById(eventId) ?? null;
}
public canCollapse = (): boolean => {
@ -182,7 +182,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
private onQuoteClick = async (event: ButtonEvent): Promise<void> => {
const events = [this.state.loadedEv, ...this.state.events];
let loadedEv = null;
let loadedEv: MatrixEvent | null = null;
if (events.length > 0) {
loadedEv = await this.getNextEvent(events[0]);
}
@ -200,7 +200,7 @@ export default class ReplyChain extends React.Component<IProps, IState> {
}
public render(): React.ReactNode {
let header = null;
let header: JSX.Element | undefined;
if (this.state.err) {
header = (
<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) {
const offset = this.offset(this.props.values, this.props.value);

View file

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

View file

@ -37,6 +37,7 @@ import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContex
import { ReadPinsEventId } from "./types";
import Heading from "../typography/Heading";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { filterBoolean } from "../../../utils/arrays";
interface IProps {
room: Room;
@ -44,7 +45,7 @@ interface IProps {
onClose(): void;
}
export const usePinnedEvents = (room: Room): string[] => {
export const usePinnedEvents = (room?: Room): string[] => {
const [pinnedEvents, setPinnedEvents] = useState<string[]>([]);
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
content = pinnedEvents
.filter(Boolean)
content = filterBoolean(pinnedEvents)
.reverse()
.map((ev) => (
<PinnedEventTile

View file

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

View file

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

View file

@ -43,7 +43,7 @@ const PRESENCE_CLASS: Record<PresenceState, string> = {
unavailable: "mx_EntityTile_unavailable",
};
function presenceClassForMember(presenceState: PresenceState, lastActiveAgo: number, showPresence: boolean): string {
function presenceClassForMember(presenceState?: PresenceState, lastActiveAgo?: number, showPresence?: boolean): string {
if (showPresence === false) {
return "mx_EntityTile_online_beenactive";
}
@ -74,7 +74,7 @@ interface IProps {
presenceLastTs?: number;
presenceCurrentlyActive?: boolean;
showInviteButton?: boolean;
onClick?(): void;
onClick(): void;
suppressOnHover?: boolean;
showPresence?: boolean;
subtextLabel?: string;
@ -108,7 +108,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
public render(): React.ReactNode {
const mainClassNames: Record<string, boolean> = {
mx_EntityTile: true,
mx_EntityTile_noHover: this.props.suppressOnHover,
mx_EntityTile_noHover: !!this.props.suppressOnHover,
};
if (this.props.className) mainClassNames[this.props.className] = true;
@ -127,7 +127,7 @@ export default class EntityTile extends React.PureComponent<IProps, IState> {
? Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)
: -1;
let presenceLabel = null;
let presenceLabel: JSX.Element | undefined;
if (this.props.showPresence) {
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
// XXX: This would be better handled using a culture-aware library, but we don't use one yet.
private getDuration(time: number): string {
private getDuration(time: number): string | undefined {
if (!time) return;
const t = Math.round(time / 1000);
const s = t % 60;
@ -61,11 +61,11 @@ export default class PresenceLabel extends React.Component<IProps> {
return _t("%(duration)sd", { duration: d });
}
private getPrettyPresence(presence: string, activeAgo: number, currentlyActive: boolean): string {
private getPrettyPresence(presence?: string, activeAgo?: number, currentlyActive?: boolean): string {
// for busy presence, we ignore the 'currentlyActive' flag: they're busy whether
// they're active or not. It can be set while the user is active in which case
// the 'active ago' ends up being 0.
if (BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy");
if (presence && BUSY_PRESENCE_NAME.matches(presence)) return _t("Busy");
if (!currentlyActive && activeAgo !== undefined && activeAgo > 0) {
const duration = this.getDuration(activeAgo);

View file

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

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

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;
const roomInfo = this.roomMap.get(room.roomId) || <IRoomWidgets>{};
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";
export function isLocalRoom(roomOrID: Room | string): boolean {
export function isLocalRoom(roomOrID?: Room | string): boolean {
if (typeof roomOrID === "string") {
return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX);
}

View file

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

View file

@ -69,15 +69,3 @@ exports[`<TabbedView /> renders tabs 1`] = `
</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
aria-labelledby="mx_BaseDialog_title"
class="undefined mx_Dialog_fixedWidth"
class="mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>

View file

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