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

(cherry picked from commit 2c0965c240)
This commit is contained in:
Michael Telatynski 2022-06-10 22:38:46 +01:00
parent b7b5408fb1
commit 3a4ac1373c
No known key found for this signature in database
GPG key ID: 98BC6A2B829297FE
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",
"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",

View file

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

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";
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;

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 {
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");
}
}

View file

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