Files
blap/apps/web/src/components/views/settings/EventIndexPanel.tsx
T
Michael Telatynski 30f442208a Fix React hydration issues (#32958)
* Add more playwright axe tests to settings dialogs

* Add utility to jest setupTests to detect React hydration errors

* Iterate jest utility

* Fix axe issue heading-order

* Fix div-in-p issues

* Fix setupTests.ts

* Fix heading order

* Make types happier

* Fix hydration issues of thead containing text nodes

* Update tests

* Fix form-in-form React hydration issues

* Fix li-in-li React hydration issues

* Fix checked in form without onChange React hydration issue

* Fix styling bleeding from _common.pcss

* Update snapshots

* Fix more remaining issues

* Remove _common.pcss h2 rule altogether

* Fix test

* Update snapshots

* Iterate

* Iterate

* Update snapshots

* Simplify diff

* Test

* Update screenshots

* Update screenshot
2026-04-16 13:35:40 +00:00

242 lines
9.2 KiB
TypeScript

/*
Copyright 2024 New Vector Ltd.
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import React, { type JSX, lazy } from "react";
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig";
import Modal from "../../../Modal";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton";
import { formatBytes, formatCountLong } from "../../../utils/FormattingUtils";
import EventIndexPeg from "../../../indexing/EventIndexPeg";
import { SettingLevel } from "../../../settings/SettingLevel";
import SeshatResetDialog from "../dialogs/SeshatResetDialog";
import InlineSpinner from "../elements/InlineSpinner";
import ExternalLink from "../elements/ExternalLink";
import { SettingsSubsectionText } from "./shared/SettingsSubsection";
interface IState {
enabling: boolean;
eventIndexSize: number;
roomCount: number;
eventIndexingEnabled: boolean;
}
export default class EventIndexPanel extends React.Component<EmptyObject, IState> {
public constructor(props: EmptyObject) {
super(props);
this.state = {
enabling: false,
eventIndexSize: 0,
roomCount: 0,
eventIndexingEnabled: SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing"),
};
}
public updateCurrentRoom = async (): Promise<void> => {
const eventIndex = EventIndexPeg.get();
const stats = await eventIndex?.getStats().catch(() => {});
// This call may fail if sporadically, not a huge issue as we will try later again and probably succeed.
if (!stats) return;
this.setState({
eventIndexSize: stats.size,
roomCount: stats.roomCount,
});
};
public componentWillUnmount(): void {
const eventIndex = EventIndexPeg.get();
if (eventIndex !== null) {
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom);
}
}
public componentDidMount(): void {
this.updateState();
}
public async updateState(): Promise<void> {
const eventIndex = EventIndexPeg.get();
const eventIndexingEnabled = SettingsStore.getValueAt(SettingLevel.DEVICE, "enableEventIndexing");
const enabling = false;
let eventIndexSize = 0;
let roomCount = 0;
if (eventIndex !== null) {
eventIndex.on("changedCheckpoint", this.updateCurrentRoom);
const stats = await eventIndex.getStats().catch(() => {});
// This call may fail if sporadically, not a huge issue as we
// will try later again in the updateCurrentRoom call and
// probably succeed.
if (stats) {
eventIndexSize = stats.size;
roomCount = stats.roomCount;
}
}
this.setState({
enabling,
eventIndexSize,
roomCount,
eventIndexingEnabled,
});
}
private onManage = async (): Promise<void> => {
Modal.createDialog(
lazy(() => import("../../../async-components/views/dialogs/eventindex/ManageEventIndexDialog")),
{
onFinished: () => {},
},
undefined,
/* priority = */ false,
/* static = */ true,
);
};
private onEnable = async (): Promise<void> => {
this.setState({
enabling: true,
});
await EventIndexPeg.initEventIndex();
await EventIndexPeg.get()?.addInitialCheckpoints();
EventIndexPeg.get()?.startCrawler();
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, true);
await this.updateState();
};
private confirmEventStoreReset = (): void => {
const { finished, close } = Modal.createDialog(SeshatResetDialog);
finished.then(async ([success]) => {
if (success) {
await SettingsStore.setValue("enableEventIndexing", null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
await this.onEnable();
close();
}
});
};
public render(): React.ReactNode {
let eventIndexingSettings: JSX.Element | undefined;
const { brand, desktop_builds: desktopBuilds } = SdkConfig.get();
if (EventIndexPeg.get() !== null) {
eventIndexingSettings = (
<>
<SettingsSubsectionText>
{_t("settings|security|message_search_enabled", {
size: formatBytes(this.state.eventIndexSize, 0),
// This drives the singular / plural string
// selection for "room" / "rooms" only.
count: this.state.roomCount,
rooms: formatCountLong(this.state.roomCount),
})}
</SettingsSubsectionText>
<AccessibleButton kind="primary" onClick={this.onManage}>
{_t("action|manage")}
</AccessibleButton>
</>
);
} else if (!this.state.eventIndexingEnabled && EventIndexPeg.supportIsInstalled()) {
eventIndexingSettings = (
<>
<SettingsSubsectionText>{_t("settings|security|message_search_disabled")}</SettingsSubsectionText>
<div>
<AccessibleButton kind="primary" disabled={this.state.enabling} onClick={this.onEnable}>
{_t("action|enable")}
</AccessibleButton>
{this.state.enabling ? <InlineSpinner /> : <div />}
</div>
</>
);
} else if (EventIndexPeg.platformHasSupport() && !EventIndexPeg.supportIsInstalled()) {
const nativeLink =
"https://github.com/element-hq/element-web/blob/develop/apps/desktop/" +
"docs/native-node-modules.md#" +
"adding-seshat-for-search-in-e2e-encrypted-rooms";
eventIndexingSettings = (
<SettingsSubsectionText>
{_t(
"settings|security|message_search_unsupported",
{
brand,
},
{
nativeLink: (sub) => (
<ExternalLink href={nativeLink} target="_blank" rel="noreferrer noopener">
{sub}
</ExternalLink>
),
},
)}
</SettingsSubsectionText>
);
} else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = (
<SettingsSubsectionText>
{_t(
"settings|security|message_search_unsupported_web",
{
brand,
},
{
desktopLink: (sub) => (
<ExternalLink href={desktopBuilds.url} target="_blank" rel="noreferrer noopener">
{sub}
</ExternalLink>
),
},
)}
</SettingsSubsectionText>
);
} else {
eventIndexingSettings = (
<>
<SettingsSubsectionText>
{this.state.enabling ? <InlineSpinner /> : _t("settings|security|message_search_failed")}
</SettingsSubsectionText>
{EventIndexPeg.error ? (
<SettingsSubsectionText>
<details>
<summary>{_t("common|advanced")}</summary>
<code>
{EventIndexPeg.error instanceof Error
? EventIndexPeg.error.message
: _t("error|unknown")}
</code>
<p>
<AccessibleButton
element="button"
key="delete"
kind="danger"
onClick={this.confirmEventStoreReset}
>
{_t("action|reset")}
</AccessibleButton>
</p>
</details>
</SettingsSubsectionText>
) : undefined}
</>
);
}
return eventIndexingSettings;
}
}