From 2c0965c240c8b148acbfdd0abfb043333d2ea750 Mon Sep 17 00:00:00 2001
From: Michael Telatynski <7t3chguy@gmail.com>
Date: Fri, 10 Jun 2022 22:38:46 +0100
Subject: [PATCH] Abstract electron settings properly to avoid boilerplate-hell
 (#22491)

* Remove unused method `BasePlatform::screenCaptureErrorString`

* Extract SeshatIndexManager into its own file

* Improve platform typescripting

* Consolidate IPC call promisification into IPCManager

* Abstract electron settings properly to avoid boilerplate-hell

* i18n

* Iterate PR
---
 src/i18n/strings/en_EN.json               |   1 -
 src/vector/platform/ElectronPlatform.tsx  | 340 +++++-----------------
 src/vector/platform/IPCManager.ts         |  70 +++++
 src/vector/platform/PWAPlatform.ts        |   2 +-
 src/vector/platform/SeshatIndexManager.ts | 105 +++++++
 src/vector/platform/VectorBasePlatform.ts |  14 +-
 src/vector/platform/WebPlatform.ts        |  62 ++--
 7 files changed, 283 insertions(+), 311 deletions(-)
 create mode 100644 src/vector/platform/IPCManager.ts
 create mode 100644 src/vector/platform/SeshatIndexManager.ts

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index aef27979d6..0cc4b0cb4d 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -14,7 +14,6 @@
     "Go to your browser to complete Sign In": "Go to your browser to complete Sign In",
     "Unknown device": "Unknown device",
     "%(appName)s (%(browserName)s, %(osName)s)": "%(appName)s (%(browserName)s, %(osName)s)",
-    "You need to be using HTTPS to place a screen-sharing call.": "You need to be using HTTPS to place a screen-sharing call.",
     "Powered by Matrix": "Powered by Matrix",
     "Use %(brand)s on mobile": "Use %(brand)s on mobile",
     "Unsupported browser": "Unsupported browser",
diff --git a/src/vector/platform/ElectronPlatform.tsx b/src/vector/platform/ElectronPlatform.tsx
index d41d239b5c..9c1bc3b9bc 100644
--- a/src/vector/platform/ElectronPlatform.tsx
+++ b/src/vector/platform/ElectronPlatform.tsx
@@ -18,13 +18,8 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
-import BaseEventIndexManager, {
-    ICrawlerCheckpoint,
-    IEventAndProfile,
-    IIndexStats,
-    ISearchArgs,
-} from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
+import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
+import BaseEventIndexManager from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
 import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
 import { _t } from 'matrix-react-sdk/src/languageHandler';
 import SdkConfig from 'matrix-react-sdk/src/SdkConfig';
@@ -43,11 +38,12 @@ import { showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/Update
 import { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
 import ToastStore from "matrix-react-sdk/src/stores/ToastStore";
 import GenericExpiringToast from "matrix-react-sdk/src/components/views/toasts/GenericExpiringToast";
-import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
 import { logger } from "matrix-js-sdk/src/logger";
 import { MatrixEvent } from "matrix-js-sdk/src/models/event";
 
 import VectorBasePlatform from './VectorBasePlatform';
+import { SeshatIndexManager } from "./SeshatIndexManager";
+import { IPCManager } from "./IPCManager";
 
 const electron = window.electron;
 const isMac = navigator.platform.toUpperCase().includes('MAC');
@@ -71,14 +67,14 @@ function platformFriendlyName(): string {
     }
 }
 
-function _onAction(payload: ActionPayload) {
+function onAction(payload: ActionPayload): void {
     // Whitelist payload actions, no point sending most across
     if (['call_state'].includes(payload.action)) {
         electron.send('app_onAction', payload);
     }
 }
 
-function getUpdateCheckStatus(status: boolean | string) {
+function getUpdateCheckStatus(status: boolean | string): UpdateStatus {
     if (status === true) {
         return { status: UpdateCheckStatus.Downloading };
     } else if (status === false) {
@@ -91,139 +87,16 @@ function getUpdateCheckStatus(status: boolean | string) {
     }
 }
 
-interface IPCPayload {
-    id?: number;
-    error?: string;
-    reply?: any;
-}
-
-class SeshatIndexManager extends BaseEventIndexManager {
-    private pendingIpcCalls: Record<number, { resolve, reject }> = {};
-    private nextIpcCallId = 0;
-
-    constructor() {
-        super();
-
-        electron.on('seshatReply', this.onIpcReply);
-    }
-
-    private async ipcCall(name: string, ...args: any[]): Promise<any> {
-        // TODO this should be moved into the preload.js file.
-        const ipcCallId = ++this.nextIpcCallId;
-        return new Promise((resolve, reject) => {
-            this.pendingIpcCalls[ipcCallId] = { resolve, reject };
-            window.electron.send('seshat', { id: ipcCallId, name, args });
-        });
-    }
-
-    private onIpcReply = (ev: {}, payload: IPCPayload) => {
-        if (payload.id === undefined) {
-            logger.warn("Ignoring IPC reply with no ID");
-            return;
-        }
-
-        if (this.pendingIpcCalls[payload.id] === undefined) {
-            logger.warn("Unknown IPC payload ID: " + payload.id);
-            return;
-        }
-
-        const callbacks = this.pendingIpcCalls[payload.id];
-        delete this.pendingIpcCalls[payload.id];
-        if (payload.error) {
-            callbacks.reject(payload.error);
-        } else {
-            callbacks.resolve(payload.reply);
-        }
-    };
-
-    async supportsEventIndexing(): Promise<boolean> {
-        return this.ipcCall('supportsEventIndexing');
-    }
-
-    async initEventIndex(userId: string, deviceId: string): Promise<void> {
-        return this.ipcCall('initEventIndex', userId, deviceId);
-    }
-
-    async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
-        return this.ipcCall('addEventToIndex', ev, profile);
-    }
-
-    async deleteEvent(eventId: string): Promise<boolean> {
-        return this.ipcCall('deleteEvent', eventId);
-    }
-
-    async isEventIndexEmpty(): Promise<boolean> {
-        return this.ipcCall('isEventIndexEmpty');
-    }
-
-    async isRoomIndexed(roomId: string): Promise<boolean> {
-        return this.ipcCall('isRoomIndexed', roomId);
-    }
-
-    async commitLiveEvents(): Promise<void> {
-        return this.ipcCall('commitLiveEvents');
-    }
-
-    async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> {
-        return this.ipcCall('searchEventIndex', searchConfig);
-    }
-
-    async addHistoricEvents(
-        events: IEventAndProfile[],
-        checkpoint: ICrawlerCheckpoint | null,
-        oldCheckpoint: ICrawlerCheckpoint | null,
-    ): Promise<boolean> {
-        return this.ipcCall('addHistoricEvents', events, checkpoint, oldCheckpoint);
-    }
-
-    async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
-        return this.ipcCall('addCrawlerCheckpoint', checkpoint);
-    }
-
-    async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
-        return this.ipcCall('removeCrawlerCheckpoint', checkpoint);
-    }
-
-    async loadFileEvents(args): Promise<IEventAndProfile[]> {
-        return this.ipcCall('loadFileEvents', args);
-    }
-
-    async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
-        return this.ipcCall('loadCheckpoints');
-    }
-
-    async closeEventIndex(): Promise<void> {
-        return this.ipcCall('closeEventIndex');
-    }
-
-    async getStats(): Promise<IIndexStats> {
-        return this.ipcCall('getStats');
-    }
-
-    async getUserVersion(): Promise<number> {
-        return this.ipcCall('getUserVersion');
-    }
-
-    async setUserVersion(version: number): Promise<void> {
-        return this.ipcCall('setUserVersion', version);
-    }
-
-    async deleteEventIndex(): Promise<void> {
-        return this.ipcCall('deleteEventIndex');
-    }
-}
-
 export default class ElectronPlatform extends VectorBasePlatform {
-    private eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
-    private pendingIpcCalls: Record<number, { resolve, reject }> = {};
-    private nextIpcCallId = 0;
+    private readonly ipc = new IPCManager("ipcCall", "ipcReply");
+    private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
     // this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile
-    private ssoID: string = randomString(32);
+    private readonly ssoID: string = randomString(32);
 
     constructor() {
         super();
 
-        dis.register(_onAction);
+        dis.register(onAction);
         /*
             IPC Call `check_updates` returns:
             true if there is an update available
@@ -243,7 +116,6 @@ export default class ElectronPlatform extends VectorBasePlatform {
             rageshake.flush();
         });
 
-        electron.on('ipcReply', this.onIpcReply);
         electron.on('update-downloaded', this.onUpdateDownloaded);
 
         electron.on('preferences', () => {
@@ -278,14 +150,14 @@ export default class ElectronPlatform extends VectorBasePlatform {
             });
         });
 
-        this.ipcCall("startSSOFlow", this.ssoID);
+        this.ipc.call("startSSOFlow", this.ssoID);
     }
 
-    async getConfig(): Promise<IConfigOptions> {
-        return this.ipcCall('getConfig');
+    public async getConfig(): Promise<IConfigOptions> {
+        return this.ipc.call('getConfig');
     }
 
-    onUpdateDownloaded = async (ev, { releaseNotes, releaseName }) => {
+    private onUpdateDownloaded = async (ev, { releaseNotes, releaseName }) => {
         dis.dispatch<CheckUpdatesPayload>({
             action: Action.CheckUpdates,
             status: UpdateCheckStatus.Ready,
@@ -295,7 +167,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
         }
     };
 
-    getHumanReadableName(): string {
+    public getHumanReadableName(): string {
         return 'Electron Platform'; // no translation required: only used for analytics
     }
 
@@ -303,7 +175,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
      * Return true if platform supports multi-language
      * spell-checking, otherwise false.
      */
-    supportsMultiLanguageSpellCheck(): boolean {
+    public supportsMultiLanguageSpellCheck(): boolean {
         // Electron uses OS spell checking on macOS, so no need for in-app options
         if (isMac) return false;
         return true;
@@ -320,15 +192,21 @@ export default class ElectronPlatform extends VectorBasePlatform {
         electron.send('setBadgeCount', count);
     }
 
-    supportsNotifications(): boolean {
+    public supportsNotifications(): boolean {
         return true;
     }
 
-    maySendNotifications(): boolean {
+    public maySendNotifications(): boolean {
         return true;
     }
 
-    displayNotification(title: string, msg: string, avatarUrl: string, room: Room, ev?: MatrixEvent): Notification {
+    public displayNotification(
+        title: string,
+        msg: string,
+        avatarUrl: string,
+        room: Room,
+        ev?: MatrixEvent,
+    ): Notification {
         // GNOME notification spec parses HTML tags for styling...
         // Electron Docs state all supported linux notification systems follow this markup spec
         // https://github.com/electron/electron/blob/master/docs/tutorial/desktop-environment-integration.md#linux
@@ -350,100 +228,56 @@ export default class ElectronPlatform extends VectorBasePlatform {
         const handler = notification.onclick as Function;
         notification.onclick = () => {
             handler?.();
-            this.ipcCall('focusWindow');
+            this.ipc.call('focusWindow');
         };
 
         return notification;
     }
 
-    loudNotification(ev: MatrixEvent, room: Room) {
+    public loudNotification(ev: MatrixEvent, room: Room) {
         electron.send('loudNotification');
     }
 
-    async getAppVersion(): Promise<string> {
-        return this.ipcCall('getAppVersion');
+    public async getAppVersion(): Promise<string> {
+        return this.ipc.call('getAppVersion');
     }
 
-    supportsAutoLaunch(): boolean {
-        return true;
+    public supportsSetting(settingName?: string): boolean {
+        switch (settingName) {
+            case "Electron.showTrayIcon": // Things other than Mac support tray icons
+            case "Electron.alwaysShowMenuBar": // This isn't relevant on Mac as Menu bars don't live in the app window
+                return !isMac;
+            default:
+                return true;
+        }
     }
 
-    async getAutoLaunchEnabled(): Promise<boolean> {
-        return this.ipcCall('getAutoLaunchEnabled');
+    public getSettingValue(settingName: string): Promise<any> {
+        return this.ipc.call("getSettingValue", settingName);
     }
 
-    async setAutoLaunchEnabled(enabled: boolean): Promise<void> {
-        return this.ipcCall('setAutoLaunchEnabled', enabled);
-    }
-
-    supportsWarnBeforeExit(): boolean {
-        return true;
-    }
-
-    async shouldWarnBeforeExit(): Promise<boolean> {
-        return this.ipcCall('shouldWarnBeforeExit');
-    }
-
-    async setWarnBeforeExit(enabled: boolean): Promise<void> {
-        return this.ipcCall('setWarnBeforeExit', enabled);
-    }
-
-    supportsAutoHideMenuBar(): boolean {
-        // This is irelevant on Mac as Menu bars don't live in the app window
-        return !isMac;
-    }
-
-    async getAutoHideMenuBarEnabled(): Promise<boolean> {
-        return this.ipcCall('getAutoHideMenuBarEnabled');
-    }
-
-    async setAutoHideMenuBarEnabled(enabled: boolean): Promise<void> {
-        return this.ipcCall('setAutoHideMenuBarEnabled', enabled);
-    }
-
-    supportsMinimizeToTray(): boolean {
-        // Things other than Mac support tray icons
-        return !isMac;
-    }
-
-    async getMinimizeToTrayEnabled(): Promise<boolean> {
-        return this.ipcCall('getMinimizeToTrayEnabled');
-    }
-
-    async setMinimizeToTrayEnabled(enabled: boolean): Promise<void> {
-        return this.ipcCall('setMinimizeToTrayEnabled', enabled);
-    }
-
-    public supportsTogglingHardwareAcceleration(): boolean {
-        return true;
-    }
-
-    public async getHardwareAccelerationEnabled(): Promise<boolean> {
-        return this.ipcCall('getHardwareAccelerationEnabled');
-    }
-
-    public async setHardwareAccelerationEnabled(enabled: boolean): Promise<void> {
-        return this.ipcCall('setHardwareAccelerationEnabled', enabled);
+    public setSettingValue(settingName: string, value: any): Promise<void> {
+        return this.ipc.call("setSettingValue", settingName, value);
     }
 
     async canSelfUpdate(): Promise<boolean> {
-        const feedUrl = await this.ipcCall('getUpdateFeedUrl');
+        const feedUrl = await this.ipc.call('getUpdateFeedUrl');
         return Boolean(feedUrl);
     }
 
-    startUpdateCheck() {
+    public startUpdateCheck() {
         super.startUpdateCheck();
         electron.send('check_updates');
     }
 
-    installUpdate() {
+    public installUpdate() {
         // IPC to the main process to install the update, since quitAndInstall
         // doesn't fire the before-quit event so the main process needs to know
         // it should exit.
         electron.send('install_update');
     }
 
-    getDefaultDeviceDisplayName(): string {
+    public getDefaultDeviceDisplayName(): string {
         const brand = SdkConfig.get().brand;
         return _t('%(brand)s Desktop (%(platformName)s)', {
             brand,
@@ -451,86 +285,58 @@ export default class ElectronPlatform extends VectorBasePlatform {
         });
     }
 
-    screenCaptureErrorString(): string | null {
-        return null;
-    }
-
-    requestNotificationPermission(): Promise<string> {
+    public requestNotificationPermission(): Promise<string> {
         return Promise.resolve('granted');
     }
 
-    reload() {
+    public reload() {
         window.location.reload();
     }
 
-    private async ipcCall(name: string, ...args: any[]): Promise<any> {
-        const ipcCallId = ++this.nextIpcCallId;
-        return new Promise((resolve, reject) => {
-            this.pendingIpcCalls[ipcCallId] = { resolve, reject };
-            window.electron.send('ipcCall', { id: ipcCallId, name, args });
-            // Maybe add a timeout to these? Probably not necessary.
-        });
-    }
-
-    private onIpcReply = (ev, payload) => {
-        if (payload.id === undefined) {
-            logger.warn("Ignoring IPC reply with no ID");
-            return;
-        }
-
-        if (this.pendingIpcCalls[payload.id] === undefined) {
-            logger.warn("Unknown IPC payload ID: " + payload.id);
-            return;
-        }
-
-        const callbacks = this.pendingIpcCalls[payload.id];
-        delete this.pendingIpcCalls[payload.id];
-        if (payload.error) {
-            callbacks.reject(payload.error);
-        } else {
-            callbacks.resolve(payload.reply);
-        }
-    };
-
-    getEventIndexingManager(): BaseEventIndexManager | null {
+    public getEventIndexingManager(): BaseEventIndexManager | null {
         return this.eventIndexManager;
     }
 
-    async setLanguage(preferredLangs: string[]) {
-        return this.ipcCall('setLanguage', preferredLangs);
+    public async setLanguage(preferredLangs: string[]) {
+        return this.ipc.call('setLanguage', preferredLangs);
     }
 
-    setSpellCheckLanguages(preferredLangs: string[]) {
-        this.ipcCall('setSpellCheckLanguages', preferredLangs).catch(error => {
+    public setSpellCheckLanguages(preferredLangs: string[]) {
+        this.ipc.call('setSpellCheckLanguages', preferredLangs).catch(error => {
             logger.log("Failed to send setSpellCheckLanguages IPC to Electron");
             logger.error(error);
         });
     }
 
-    async getSpellCheckLanguages(): Promise<string[]> {
-        return this.ipcCall('getSpellCheckLanguages');
+    public async getSpellCheckLanguages(): Promise<string[]> {
+        return this.ipc.call('getSpellCheckLanguages');
     }
 
-    async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> {
-        return this.ipcCall('getDesktopCapturerSources', options);
+    public async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> {
+        return this.ipc.call('getDesktopCapturerSources', options);
     }
 
-    supportsDesktopCapturer(): boolean {
+    public supportsDesktopCapturer(): boolean {
         return true;
     }
 
-    async getAvailableSpellCheckLanguages(): Promise<string[]> {
-        return this.ipcCall('getAvailableSpellCheckLanguages');
+    public async getAvailableSpellCheckLanguages(): Promise<string[]> {
+        return this.ipc.call('getAvailableSpellCheckLanguages');
     }
 
-    getSSOCallbackUrl(fragmentAfterLogin: string): URL {
+    public getSSOCallbackUrl(fragmentAfterLogin: string): URL {
         const url = super.getSSOCallbackUrl(fragmentAfterLogin);
         url.protocol = "element";
         url.searchParams.set("element-desktop-ssoid", this.ssoID);
         return url;
     }
 
-    startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) {
+    public startSingleSignOn(
+        mxClient: MatrixClient,
+        loginType: "sso" | "cas",
+        fragmentAfterLogin: string,
+        idpId?: string,
+    ) {
         // this will get intercepted by electron-main will-navigate
         super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId);
         Modal.createTrackedDialog('Electron', 'SSO', InfoDialog, {
@@ -540,16 +346,16 @@ export default class ElectronPlatform extends VectorBasePlatform {
     }
 
     public navigateForwardBack(back: boolean): void {
-        this.ipcCall(back ? "navigateBack" : "navigateForward");
+        this.ipc.call(back ? "navigateBack" : "navigateForward");
     }
 
     public overrideBrowserShortcuts(): boolean {
         return true;
     }
 
-    async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
+    public async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
         try {
-            return await this.ipcCall('getPickleKey', userId, deviceId);
+            return await this.ipc.call('getPickleKey', userId, deviceId);
         } catch (e) {
             // if we can't connect to the password storage, assume there's no
             // pickle key
@@ -557,9 +363,9 @@ export default class ElectronPlatform extends VectorBasePlatform {
         }
     }
 
-    async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
+    public async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
         try {
-            return await this.ipcCall('createPickleKey', userId, deviceId);
+            return await this.ipc.call('createPickleKey', userId, deviceId);
         } catch (e) {
             // if we can't connect to the password storage, assume there's no
             // pickle key
@@ -567,9 +373,9 @@ export default class ElectronPlatform extends VectorBasePlatform {
         }
     }
 
-    async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
+    public async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
         try {
-            await this.ipcCall('destroyPickleKey', userId, deviceId);
+            await this.ipc.call('destroyPickleKey', userId, deviceId);
         } catch (e) {}
     }
 }
diff --git a/src/vector/platform/IPCManager.ts b/src/vector/platform/IPCManager.ts
new file mode 100644
index 0000000000..c0ceda64ea
--- /dev/null
+++ b/src/vector/platform/IPCManager.ts
@@ -0,0 +1,70 @@
+/*
+Copyright 2022 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { defer, IDeferred } from 'matrix-js-sdk/src/utils';
+import { logger } from "matrix-js-sdk/src/logger";
+
+import { ElectronChannel } from "../../@types/global";
+
+const electron = window.electron;
+
+interface IPCPayload {
+    id?: number;
+    error?: string;
+    reply?: any;
+}
+
+export class IPCManager {
+    private pendingIpcCalls: { [ipcCallId: number]: IDeferred<any> } = {};
+    private nextIpcCallId = 0;
+
+    public constructor(
+        private readonly sendChannel: ElectronChannel = "ipcCall",
+        private readonly recvChannel: ElectronChannel = "ipcReply",
+    ) {
+        electron.on(this.recvChannel, this.onIpcReply);
+    }
+
+    public async call(name: string, ...args: any[]): Promise<any> {
+        // TODO this should be moved into the preload.js file.
+        const ipcCallId = ++this.nextIpcCallId;
+        const deferred = defer<any>();
+        this.pendingIpcCalls[ipcCallId] = deferred;
+        // Maybe add a timeout to these? Probably not necessary.
+        window.electron.send(this.sendChannel, { id: ipcCallId, name, args });
+        return deferred.promise;
+    }
+
+    private onIpcReply = (ev: {}, payload: IPCPayload): void => {
+        if (payload.id === undefined) {
+            logger.warn("Ignoring IPC reply with no ID");
+            return;
+        }
+
+        if (this.pendingIpcCalls[payload.id] === undefined) {
+            logger.warn("Unknown IPC payload ID: " + payload.id);
+            return;
+        }
+
+        const callbacks = this.pendingIpcCalls[payload.id];
+        delete this.pendingIpcCalls[payload.id];
+        if (payload.error) {
+            callbacks.reject(payload.error);
+        } else {
+            callbacks.resolve(payload.reply);
+        }
+    };
+}
diff --git a/src/vector/platform/PWAPlatform.ts b/src/vector/platform/PWAPlatform.ts
index de9d9884f6..ea0c9cf168 100644
--- a/src/vector/platform/PWAPlatform.ts
+++ b/src/vector/platform/PWAPlatform.ts
@@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
 import WebPlatform from "./WebPlatform";
 
 export default class PWAPlatform extends WebPlatform {
-    setNotificationCount(count: number) {
+    public setNotificationCount(count: number): void {
         if (!navigator.setAppBadge) return super.setNotificationCount(count);
         if (this.notificationCount === count) return;
         this.notificationCount = count;
diff --git a/src/vector/platform/SeshatIndexManager.ts b/src/vector/platform/SeshatIndexManager.ts
new file mode 100644
index 0000000000..2f08f49296
--- /dev/null
+++ b/src/vector/platform/SeshatIndexManager.ts
@@ -0,0 +1,105 @@
+/*
+Copyright 2022 New Vector Ltd
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import BaseEventIndexManager, {
+    ICrawlerCheckpoint,
+    IEventAndProfile,
+    IIndexStats,
+    ISearchArgs,
+} from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
+import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
+
+import { IPCManager } from "./IPCManager";
+
+export class SeshatIndexManager extends BaseEventIndexManager {
+    private readonly ipc = new IPCManager("seshat", "seshatReply");
+
+    public async supportsEventIndexing(): Promise<boolean> {
+        return this.ipc.call('supportsEventIndexing');
+    }
+
+    public async initEventIndex(userId: string, deviceId: string): Promise<void> {
+        return this.ipc.call('initEventIndex', userId, deviceId);
+    }
+
+    public async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
+        return this.ipc.call('addEventToIndex', ev, profile);
+    }
+
+    public async deleteEvent(eventId: string): Promise<boolean> {
+        return this.ipc.call('deleteEvent', eventId);
+    }
+
+    public async isEventIndexEmpty(): Promise<boolean> {
+        return this.ipc.call('isEventIndexEmpty');
+    }
+
+    public async isRoomIndexed(roomId: string): Promise<boolean> {
+        return this.ipc.call('isRoomIndexed', roomId);
+    }
+
+    public async commitLiveEvents(): Promise<void> {
+        return this.ipc.call('commitLiveEvents');
+    }
+
+    public async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> {
+        return this.ipc.call('searchEventIndex', searchConfig);
+    }
+
+    public async addHistoricEvents(
+        events: IEventAndProfile[],
+        checkpoint: ICrawlerCheckpoint | null,
+        oldCheckpoint: ICrawlerCheckpoint | null,
+    ): Promise<boolean> {
+        return this.ipc.call('addHistoricEvents', events, checkpoint, oldCheckpoint);
+    }
+
+    public async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
+        return this.ipc.call('addCrawlerCheckpoint', checkpoint);
+    }
+
+    public async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
+        return this.ipc.call('removeCrawlerCheckpoint', checkpoint);
+    }
+
+    public async loadFileEvents(args): Promise<IEventAndProfile[]> {
+        return this.ipc.call('loadFileEvents', args);
+    }
+
+    public async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
+        return this.ipc.call('loadCheckpoints');
+    }
+
+    public async closeEventIndex(): Promise<void> {
+        return this.ipc.call('closeEventIndex');
+    }
+
+    public async getStats(): Promise<IIndexStats> {
+        return this.ipc.call('getStats');
+    }
+
+    public async getUserVersion(): Promise<number> {
+        return this.ipc.call('getUserVersion');
+    }
+
+    public async setUserVersion(version: number): Promise<void> {
+        return this.ipc.call('setUserVersion', version);
+    }
+
+    public async deleteEventIndex(): Promise<void> {
+        return this.ipc.call('deleteEventIndex');
+    }
+}
diff --git a/src/vector/platform/VectorBasePlatform.ts b/src/vector/platform/VectorBasePlatform.ts
index 382fd62604..b6e78629eb 100644
--- a/src/vector/platform/VectorBasePlatform.ts
+++ b/src/vector/platform/VectorBasePlatform.ts
@@ -30,11 +30,11 @@ import Favicon from "../../favicon";
 export default abstract class VectorBasePlatform extends BasePlatform {
     protected _favicon: Favicon;
 
-    async getConfig(): Promise<IConfigOptions> {
+    public async getConfig(): Promise<IConfigOptions> {
         return getVectorConfig();
     }
 
-    getHumanReadableName(): string {
+    public getHumanReadableName(): string {
         return 'Vector Base Platform'; // no translation required: only used for analytics
     }
 
@@ -43,7 +43,7 @@ export default abstract class VectorBasePlatform extends BasePlatform {
      * it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode.
      * See https://github.com/vector-im/element-web/issues/9605.
      */
-    get favicon() {
+    public get favicon() {
         if (this._favicon) {
             return this._favicon;
         }
@@ -62,13 +62,13 @@ export default abstract class VectorBasePlatform extends BasePlatform {
         this.favicon.badge(notif, { bgColor });
     }
 
-    setNotificationCount(count: number) {
+    public setNotificationCount(count: number) {
         if (this.notificationCount === count) return;
         super.setNotificationCount(count);
         this.updateFavicon();
     }
 
-    setErrorStatus(errorDidOccur: boolean) {
+    public setErrorStatus(errorDidOccur: boolean) {
         if (this.errorDidOccur === errorDidOccur) return;
         super.setErrorStatus(errorDidOccur);
         this.updateFavicon();
@@ -77,14 +77,14 @@ export default abstract class VectorBasePlatform extends BasePlatform {
     /**
      * Begin update polling, if applicable
      */
-    startUpdater() {
+    public startUpdater() {
     }
 
     /**
      * Get a sensible default display name for the
      * device Vector is running on
      */
-    getDefaultDeviceDisplayName(): string {
+    public getDefaultDeviceDisplayName(): string {
         return _t("Unknown device");
     }
 }
diff --git a/src/vector/platform/WebPlatform.ts b/src/vector/platform/WebPlatform.ts
index 4f57908782..bef9c51d30 100644
--- a/src/vector/platform/WebPlatform.ts
+++ b/src/vector/platform/WebPlatform.ts
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
+import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
 import request from 'browser-request';
 import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
 import { _t } from 'matrix-react-sdk/src/languageHandler';
@@ -31,6 +31,15 @@ import { parseQs } from "../url_utils";
 
 const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
 
+function getNormalizedAppVersion(version: string): string {
+    // if version looks like semver with leading v, strip it (matches scripts/normalize-version.sh)
+    const semVerRegex = /^v\d+.\d+.\d+(-.+)?$/;
+    if (semVerRegex.test(version)) {
+        return version.substring(1);
+    }
+    return version;
+}
+
 export default class WebPlatform extends VectorBasePlatform {
     constructor() {
         super();
@@ -40,7 +49,7 @@ export default class WebPlatform extends VectorBasePlatform {
         }
     }
 
-    getHumanReadableName(): string {
+    public getHumanReadableName(): string {
         return 'Web Platform'; // no translation required: only used for analytics
     }
 
@@ -48,7 +57,7 @@ export default class WebPlatform extends VectorBasePlatform {
      * Returns true if the platform supports displaying
      * notifications, otherwise false.
      */
-    supportsNotifications(): boolean {
+    public supportsNotifications(): boolean {
         return Boolean(window.Notification);
     }
 
@@ -56,7 +65,7 @@ export default class WebPlatform extends VectorBasePlatform {
      * Returns true if the application currently has permission
      * to display notifications. Otherwise false.
      */
-    maySendNotifications(): boolean {
+    public maySendNotifications(): boolean {
         return window.Notification.permission === 'granted';
     }
 
@@ -67,7 +76,7 @@ export default class WebPlatform extends VectorBasePlatform {
      * that is 'granted' if the user allowed the request or
      * 'denied' otherwise.
      */
-    requestNotificationPermission(): Promise<string> {
+    public requestNotificationPermission(): Promise<string> {
         // annoyingly, the latest spec says this returns a
         // promise, but this is only supported in Chrome 46
         // and Firefox 47, so adapt the callback API.
@@ -99,26 +108,17 @@ export default class WebPlatform extends VectorBasePlatform {
                         return;
                     }
 
-                    resolve(this.getNormalizedAppVersion(body.trim()));
+                    resolve(getNormalizedAppVersion(body.trim()));
                 },
             );
         });
     }
 
-    getNormalizedAppVersion(version: string): string {
-        // if version looks like semver with leading v, strip it (matches scripts/normalize-version.sh)
-        const semVerRegex = /^v\d+.\d+.\d+(-.+)?$/;
-        if (semVerRegex.test(version)) {
-            return version.substring(1);
-        }
-        return version;
+    public getAppVersion(): Promise<string> {
+        return Promise.resolve(getNormalizedAppVersion(process.env.VERSION));
     }
 
-    getAppVersion(): Promise<string> {
-        return Promise.resolve(this.getNormalizedAppVersion(process.env.VERSION));
-    }
-
-    startUpdater() {
+    public startUpdater(): void {
         // Poll for an update immediately, and reload the page now if we're out of date
         // already as we've just initialised an old version of the app somehow.
         //
@@ -127,7 +127,7 @@ export default class WebPlatform extends VectorBasePlatform {
         //
         // Ideally, loading an old copy would be impossible with the
         // cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/
-        console.log("startUpdater, current version is " + this.getNormalizedAppVersion(process.env.VERSION));
+        console.log("startUpdater, current version is " + getNormalizedAppVersion(process.env.VERSION));
         this.pollForUpdate((version: string, newVersion: string) => {
             const query = parseQs(location);
             if (query.updated) {
@@ -147,16 +147,16 @@ export default class WebPlatform extends VectorBasePlatform {
         setInterval(() => this.pollForUpdate(showUpdateToast, hideUpdateToast), POKE_RATE_MS);
     }
 
-    async canSelfUpdate(): Promise<boolean> {
+    public async canSelfUpdate(): Promise<boolean> {
         return true;
     }
 
-    pollForUpdate = (
+    private pollForUpdate = (
         showUpdate: (currentVersion: string, mostRecentVersion: string) => void,
         showNoUpdate?: () => void,
-    ) => {
+    ): Promise<UpdateStatus> => {
         return this.getMostRecentVersion().then((mostRecentVersion) => {
-            const currentVersion = this.getNormalizedAppVersion(process.env.VERSION);
+            const currentVersion = getNormalizedAppVersion(process.env.VERSION);
 
             if (currentVersion !== mostRecentVersion) {
                 if (this.shouldShowUpdate(mostRecentVersion)) {
@@ -181,7 +181,7 @@ export default class WebPlatform extends VectorBasePlatform {
         });
     };
 
-    startUpdateCheck() {
+    public startUpdateCheck(): void {
         super.startUpdateCheck();
         this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
             dis.dispatch<CheckUpdatesPayload>({
@@ -191,11 +191,11 @@ export default class WebPlatform extends VectorBasePlatform {
         });
     }
 
-    installUpdate() {
+    public installUpdate(): void {
         window.location.reload();
     }
 
-    getDefaultDeviceDisplayName(): string {
+    public getDefaultDeviceDisplayName(): string {
         // strip query-string and fragment from uri
         const url = new URL(window.location.href);
 
@@ -217,15 +217,7 @@ export default class WebPlatform extends VectorBasePlatform {
         });
     }
 
-    screenCaptureErrorString(): string | null {
-        // it won't work at all if you're not on HTTPS so whine whine whine
-        if (window.location.protocol !== "https:") {
-            return _t("You need to be using HTTPS to place a screen-sharing call.");
-        }
-        return null;
-    }
-
-    reload() {
+    public reload(): void {
         window.location.reload();
     }
 }