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
This commit is contained in:
Michael Telatynski 2022-06-10 22:38:46 +01:00 committed by GitHub
parent 867fc30ebf
commit 2c0965c240
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 283 additions and 311 deletions

View file

@ -14,7 +14,6 @@
"Go to your browser to complete Sign In": "Go to your browser to complete Sign In", "Go to your browser to complete Sign In": "Go to your browser to complete Sign In",
"Unknown device": "Unknown device", "Unknown device": "Unknown device",
"%(appName)s (%(browserName)s, %(osName)s)": "%(appName)s (%(browserName)s, %(osName)s)", "%(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", "Powered by Matrix": "Powered by Matrix",
"Use %(brand)s on mobile": "Use %(brand)s on mobile", "Use %(brand)s on mobile": "Use %(brand)s on mobile",
"Unsupported browser": "Unsupported browser", "Unsupported browser": "Unsupported browser",

View file

@ -18,13 +18,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform"; import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import BaseEventIndexManager, { import BaseEventIndexManager from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
ICrawlerCheckpoint,
IEventAndProfile,
IIndexStats,
ISearchArgs,
} from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { _t } from 'matrix-react-sdk/src/languageHandler'; import { _t } from 'matrix-react-sdk/src/languageHandler';
import SdkConfig from 'matrix-react-sdk/src/SdkConfig'; 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 { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
import ToastStore from "matrix-react-sdk/src/stores/ToastStore"; import ToastStore from "matrix-react-sdk/src/stores/ToastStore";
import GenericExpiringToast from "matrix-react-sdk/src/components/views/toasts/GenericExpiringToast"; 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 { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import VectorBasePlatform from './VectorBasePlatform'; import VectorBasePlatform from './VectorBasePlatform';
import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
const electron = window.electron; const electron = window.electron;
const isMac = navigator.platform.toUpperCase().includes('MAC'); 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 // Whitelist payload actions, no point sending most across
if (['call_state'].includes(payload.action)) { if (['call_state'].includes(payload.action)) {
electron.send('app_onAction', payload); electron.send('app_onAction', payload);
} }
} }
function getUpdateCheckStatus(status: boolean | string) { function getUpdateCheckStatus(status: boolean | string): UpdateStatus {
if (status === true) { if (status === true) {
return { status: UpdateCheckStatus.Downloading }; return { status: UpdateCheckStatus.Downloading };
} else if (status === false) { } 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 { export default class ElectronPlatform extends VectorBasePlatform {
private eventIndexManager: BaseEventIndexManager = new SeshatIndexManager(); private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private pendingIpcCalls: Record<number, { resolve, reject }> = {}; private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
private nextIpcCallId = 0;
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile // 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() { constructor() {
super(); super();
dis.register(_onAction); dis.register(onAction);
/* /*
IPC Call `check_updates` returns: IPC Call `check_updates` returns:
true if there is an update available true if there is an update available
@ -243,7 +116,6 @@ export default class ElectronPlatform extends VectorBasePlatform {
rageshake.flush(); rageshake.flush();
}); });
electron.on('ipcReply', this.onIpcReply);
electron.on('update-downloaded', this.onUpdateDownloaded); electron.on('update-downloaded', this.onUpdateDownloaded);
electron.on('preferences', () => { 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> { public async getConfig(): Promise<IConfigOptions> {
return this.ipcCall('getConfig'); return this.ipc.call('getConfig');
} }
onUpdateDownloaded = async (ev, { releaseNotes, releaseName }) => { private onUpdateDownloaded = async (ev, { releaseNotes, releaseName }) => {
dis.dispatch<CheckUpdatesPayload>({ dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates, action: Action.CheckUpdates,
status: UpdateCheckStatus.Ready, 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 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 * Return true if platform supports multi-language
* spell-checking, otherwise false. * spell-checking, otherwise false.
*/ */
supportsMultiLanguageSpellCheck(): boolean { public supportsMultiLanguageSpellCheck(): boolean {
// Electron uses OS spell checking on macOS, so no need for in-app options // Electron uses OS spell checking on macOS, so no need for in-app options
if (isMac) return false; if (isMac) return false;
return true; return true;
@ -320,15 +192,21 @@ export default class ElectronPlatform extends VectorBasePlatform {
electron.send('setBadgeCount', count); electron.send('setBadgeCount', count);
} }
supportsNotifications(): boolean { public supportsNotifications(): boolean {
return true; return true;
} }
maySendNotifications(): boolean { public maySendNotifications(): boolean {
return true; 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... // GNOME notification spec parses HTML tags for styling...
// Electron Docs state all supported linux notification systems follow this markup spec // 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 // 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; const handler = notification.onclick as Function;
notification.onclick = () => { notification.onclick = () => {
handler?.(); handler?.();
this.ipcCall('focusWindow'); this.ipc.call('focusWindow');
}; };
return notification; return notification;
} }
loudNotification(ev: MatrixEvent, room: Room) { public loudNotification(ev: MatrixEvent, room: Room) {
electron.send('loudNotification'); electron.send('loudNotification');
} }
async getAppVersion(): Promise<string> { public async getAppVersion(): Promise<string> {
return this.ipcCall('getAppVersion'); return this.ipc.call('getAppVersion');
} }
supportsAutoLaunch(): boolean { public supportsSetting(settingName?: string): boolean {
return true; 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> { public getSettingValue(settingName: string): Promise<any> {
return this.ipcCall('getAutoLaunchEnabled'); return this.ipc.call("getSettingValue", settingName);
} }
async setAutoLaunchEnabled(enabled: boolean): Promise<void> { public setSettingValue(settingName: string, value: any): Promise<void> {
return this.ipcCall('setAutoLaunchEnabled', enabled); return this.ipc.call("setSettingValue", settingName, value);
}
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);
} }
async canSelfUpdate(): Promise<boolean> { async canSelfUpdate(): Promise<boolean> {
const feedUrl = await this.ipcCall('getUpdateFeedUrl'); const feedUrl = await this.ipc.call('getUpdateFeedUrl');
return Boolean(feedUrl); return Boolean(feedUrl);
} }
startUpdateCheck() { public startUpdateCheck() {
super.startUpdateCheck(); super.startUpdateCheck();
electron.send('check_updates'); electron.send('check_updates');
} }
installUpdate() { public installUpdate() {
// IPC to the main process to install the update, since quitAndInstall // 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 // doesn't fire the before-quit event so the main process needs to know
// it should exit. // it should exit.
electron.send('install_update'); electron.send('install_update');
} }
getDefaultDeviceDisplayName(): string { public getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
return _t('%(brand)s Desktop (%(platformName)s)', { return _t('%(brand)s Desktop (%(platformName)s)', {
brand, brand,
@ -451,86 +285,58 @@ export default class ElectronPlatform extends VectorBasePlatform {
}); });
} }
screenCaptureErrorString(): string | null { public requestNotificationPermission(): Promise<string> {
return null;
}
requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted'); return Promise.resolve('granted');
} }
reload() { public reload() {
window.location.reload(); window.location.reload();
} }
private async ipcCall(name: string, ...args: any[]): Promise<any> { public getEventIndexingManager(): BaseEventIndexManager | null {
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 {
return this.eventIndexManager; return this.eventIndexManager;
} }
async setLanguage(preferredLangs: string[]) { public async setLanguage(preferredLangs: string[]) {
return this.ipcCall('setLanguage', preferredLangs); return this.ipc.call('setLanguage', preferredLangs);
} }
setSpellCheckLanguages(preferredLangs: string[]) { public setSpellCheckLanguages(preferredLangs: string[]) {
this.ipcCall('setSpellCheckLanguages', preferredLangs).catch(error => { this.ipc.call('setSpellCheckLanguages', preferredLangs).catch(error => {
logger.log("Failed to send setSpellCheckLanguages IPC to Electron"); logger.log("Failed to send setSpellCheckLanguages IPC to Electron");
logger.error(error); logger.error(error);
}); });
} }
async getSpellCheckLanguages(): Promise<string[]> { public async getSpellCheckLanguages(): Promise<string[]> {
return this.ipcCall('getSpellCheckLanguages'); return this.ipc.call('getSpellCheckLanguages');
} }
async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> { public async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> {
return this.ipcCall('getDesktopCapturerSources', options); return this.ipc.call('getDesktopCapturerSources', options);
} }
supportsDesktopCapturer(): boolean { public supportsDesktopCapturer(): boolean {
return true; return true;
} }
async getAvailableSpellCheckLanguages(): Promise<string[]> { public async getAvailableSpellCheckLanguages(): Promise<string[]> {
return this.ipcCall('getAvailableSpellCheckLanguages'); return this.ipc.call('getAvailableSpellCheckLanguages');
} }
getSSOCallbackUrl(fragmentAfterLogin: string): URL { public getSSOCallbackUrl(fragmentAfterLogin: string): URL {
const url = super.getSSOCallbackUrl(fragmentAfterLogin); const url = super.getSSOCallbackUrl(fragmentAfterLogin);
url.protocol = "element"; url.protocol = "element";
url.searchParams.set("element-desktop-ssoid", this.ssoID); url.searchParams.set("element-desktop-ssoid", this.ssoID);
return url; 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 // this will get intercepted by electron-main will-navigate
super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId); super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId);
Modal.createTrackedDialog('Electron', 'SSO', InfoDialog, { Modal.createTrackedDialog('Electron', 'SSO', InfoDialog, {
@ -540,16 +346,16 @@ export default class ElectronPlatform extends VectorBasePlatform {
} }
public navigateForwardBack(back: boolean): void { public navigateForwardBack(back: boolean): void {
this.ipcCall(back ? "navigateBack" : "navigateForward"); this.ipc.call(back ? "navigateBack" : "navigateForward");
} }
public overrideBrowserShortcuts(): boolean { public overrideBrowserShortcuts(): boolean {
return true; return true;
} }
async getPickleKey(userId: string, deviceId: string): Promise<string | null> { public async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
try { try {
return await this.ipcCall('getPickleKey', userId, deviceId); return await this.ipc.call('getPickleKey', userId, deviceId);
} catch (e) { } catch (e) {
// if we can't connect to the password storage, assume there's no // if we can't connect to the password storage, assume there's no
// pickle key // 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 { try {
return await this.ipcCall('createPickleKey', userId, deviceId); return await this.ipc.call('createPickleKey', userId, deviceId);
} catch (e) { } catch (e) {
// if we can't connect to the password storage, assume there's no // if we can't connect to the password storage, assume there's no
// pickle key // 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 { try {
await this.ipcCall('destroyPickleKey', userId, deviceId); await this.ipc.call('destroyPickleKey', userId, deviceId);
} catch (e) {} } catch (e) {}
} }
} }

View file

@ -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);
}
};
}

View file

@ -19,7 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import WebPlatform from "./WebPlatform"; import WebPlatform from "./WebPlatform";
export default class PWAPlatform extends WebPlatform { export default class PWAPlatform extends WebPlatform {
setNotificationCount(count: number) { public setNotificationCount(count: number): void {
if (!navigator.setAppBadge) return super.setNotificationCount(count); if (!navigator.setAppBadge) return super.setNotificationCount(count);
if (this.notificationCount === count) return; if (this.notificationCount === count) return;
this.notificationCount = count; this.notificationCount = count;

View file

@ -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');
}
}

View file

@ -30,11 +30,11 @@ import Favicon from "../../favicon";
export default abstract class VectorBasePlatform extends BasePlatform { export default abstract class VectorBasePlatform extends BasePlatform {
protected _favicon: Favicon; protected _favicon: Favicon;
async getConfig(): Promise<IConfigOptions> { public async getConfig(): Promise<IConfigOptions> {
return getVectorConfig(); return getVectorConfig();
} }
getHumanReadableName(): string { public getHumanReadableName(): string {
return 'Vector Base Platform'; // no translation required: only used for analytics 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. * 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. * See https://github.com/vector-im/element-web/issues/9605.
*/ */
get favicon() { public get favicon() {
if (this._favicon) { if (this._favicon) {
return this._favicon; return this._favicon;
} }
@ -62,13 +62,13 @@ export default abstract class VectorBasePlatform extends BasePlatform {
this.favicon.badge(notif, { bgColor }); this.favicon.badge(notif, { bgColor });
} }
setNotificationCount(count: number) { public setNotificationCount(count: number) {
if (this.notificationCount === count) return; if (this.notificationCount === count) return;
super.setNotificationCount(count); super.setNotificationCount(count);
this.updateFavicon(); this.updateFavicon();
} }
setErrorStatus(errorDidOccur: boolean) { public setErrorStatus(errorDidOccur: boolean) {
if (this.errorDidOccur === errorDidOccur) return; if (this.errorDidOccur === errorDidOccur) return;
super.setErrorStatus(errorDidOccur); super.setErrorStatus(errorDidOccur);
this.updateFavicon(); this.updateFavicon();
@ -77,14 +77,14 @@ export default abstract class VectorBasePlatform extends BasePlatform {
/** /**
* Begin update polling, if applicable * Begin update polling, if applicable
*/ */
startUpdater() { public startUpdater() {
} }
/** /**
* Get a sensible default display name for the * Get a sensible default display name for the
* device Vector is running on * device Vector is running on
*/ */
getDefaultDeviceDisplayName(): string { public getDefaultDeviceDisplayName(): string {
return _t("Unknown device"); return _t("Unknown device");
} }
} }

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 request from 'browser-request';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher'; import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { _t } from 'matrix-react-sdk/src/languageHandler'; 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 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 { export default class WebPlatform extends VectorBasePlatform {
constructor() { constructor() {
super(); 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 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 * Returns true if the platform supports displaying
* notifications, otherwise false. * notifications, otherwise false.
*/ */
supportsNotifications(): boolean { public supportsNotifications(): boolean {
return Boolean(window.Notification); return Boolean(window.Notification);
} }
@ -56,7 +65,7 @@ export default class WebPlatform extends VectorBasePlatform {
* Returns true if the application currently has permission * Returns true if the application currently has permission
* to display notifications. Otherwise false. * to display notifications. Otherwise false.
*/ */
maySendNotifications(): boolean { public maySendNotifications(): boolean {
return window.Notification.permission === 'granted'; 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 * that is 'granted' if the user allowed the request or
* 'denied' otherwise. * 'denied' otherwise.
*/ */
requestNotificationPermission(): Promise<string> { public requestNotificationPermission(): Promise<string> {
// annoyingly, the latest spec says this returns a // annoyingly, the latest spec says this returns a
// promise, but this is only supported in Chrome 46 // promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API. // and Firefox 47, so adapt the callback API.
@ -99,26 +108,17 @@ export default class WebPlatform extends VectorBasePlatform {
return; return;
} }
resolve(this.getNormalizedAppVersion(body.trim())); resolve(getNormalizedAppVersion(body.trim()));
}, },
); );
}); });
} }
getNormalizedAppVersion(version: string): string { public getAppVersion(): Promise<string> {
// if version looks like semver with leading v, strip it (matches scripts/normalize-version.sh) return Promise.resolve(getNormalizedAppVersion(process.env.VERSION));
const semVerRegex = /^v\d+.\d+.\d+(-.+)?$/;
if (semVerRegex.test(version)) {
return version.substring(1);
}
return version;
} }
getAppVersion(): Promise<string> { public startUpdater(): void {
return Promise.resolve(this.getNormalizedAppVersion(process.env.VERSION));
}
startUpdater() {
// Poll for an update immediately, and reload the page now if we're out of date // 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. // 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 // Ideally, loading an old copy would be impossible with the
// cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/ // 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) => { this.pollForUpdate((version: string, newVersion: string) => {
const query = parseQs(location); const query = parseQs(location);
if (query.updated) { if (query.updated) {
@ -147,16 +147,16 @@ export default class WebPlatform extends VectorBasePlatform {
setInterval(() => this.pollForUpdate(showUpdateToast, hideUpdateToast), POKE_RATE_MS); setInterval(() => this.pollForUpdate(showUpdateToast, hideUpdateToast), POKE_RATE_MS);
} }
async canSelfUpdate(): Promise<boolean> { public async canSelfUpdate(): Promise<boolean> {
return true; return true;
} }
pollForUpdate = ( private pollForUpdate = (
showUpdate: (currentVersion: string, mostRecentVersion: string) => void, showUpdate: (currentVersion: string, mostRecentVersion: string) => void,
showNoUpdate?: () => void, showNoUpdate?: () => void,
) => { ): Promise<UpdateStatus> => {
return this.getMostRecentVersion().then((mostRecentVersion) => { return this.getMostRecentVersion().then((mostRecentVersion) => {
const currentVersion = this.getNormalizedAppVersion(process.env.VERSION); const currentVersion = getNormalizedAppVersion(process.env.VERSION);
if (currentVersion !== mostRecentVersion) { if (currentVersion !== mostRecentVersion) {
if (this.shouldShowUpdate(mostRecentVersion)) { if (this.shouldShowUpdate(mostRecentVersion)) {
@ -181,7 +181,7 @@ export default class WebPlatform extends VectorBasePlatform {
}); });
}; };
startUpdateCheck() { public startUpdateCheck(): void {
super.startUpdateCheck(); super.startUpdateCheck();
this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => { this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
dis.dispatch<CheckUpdatesPayload>({ dis.dispatch<CheckUpdatesPayload>({
@ -191,11 +191,11 @@ export default class WebPlatform extends VectorBasePlatform {
}); });
} }
installUpdate() { public installUpdate(): void {
window.location.reload(); window.location.reload();
} }
getDefaultDeviceDisplayName(): string { public getDefaultDeviceDisplayName(): string {
// strip query-string and fragment from uri // strip query-string and fragment from uri
const url = new URL(window.location.href); const url = new URL(window.location.href);
@ -217,15 +217,7 @@ export default class WebPlatform extends VectorBasePlatform {
}); });
} }
screenCaptureErrorString(): string | null { public reload(): void {
// 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() {
window.location.reload(); window.location.reload();
} }
} }