Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions apps/desktop/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ module.exports = {

"n/file-extension-in-import": ["error", "always"],
"unicorn/prefer-node-protocol": ["error"],

"no-restricted-imports": [
"error",
{
paths: [
{
name: "electron",
importNames: ["ipcMain"],
message: "Use typedIpcMain instead.",
},
],
},
],
},
overrides: [
{
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@
"mkdirp": "^3.0.0",
"pacote": "^22.0.0",
"rimraf": "^6.0.0",
"shared-types": "workspace:*",
"tar": "^7.5.8",
"typescript": "catalog:",
"vitest": "catalog:"
Expand Down
15 changes: 8 additions & 7 deletions apps/desktop/src/@types/matrix-seshat.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ declare module "matrix-seshat" {
room_id: string;
origin_server_ts: number;
content: Record<string, any>;
type: string;
}

interface IMatrixProfile {
Expand All @@ -26,7 +27,7 @@ declare module "matrix-seshat" {
}

interface ISearchArgs {
searchTerm: number;
search_term: string;
limit: number;
before_limit: number;
after_limit: number;
Expand Down Expand Up @@ -54,7 +55,7 @@ declare module "matrix-seshat" {
interface ICheckpoint {
roomId: string;
token: string;
fullCrawl: boolean;
fullCrawl?: boolean;
direction: "b" | "f";
}

Expand All @@ -67,13 +68,13 @@ declare module "matrix-seshat" {
interface ILoadArgs {
roomId: string;
limit: number;
fromEvent: string;
direction: "b" | "f";
fromEvent?: string;
direction?: "b" | "f";
}

interface ILoadResult {
event: IMatrixEvent;
matrixProfile: IMatrixProfile;
profile: IMatrixProfile;
}

export class Seshat {
Expand All @@ -92,12 +93,12 @@ declare module "matrix-seshat" {
orderByRecency?: boolean,
): ISearchResult;
public addHistoricEventsSync(
events: IMatrixEvent[],
events: ILoadResult[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): boolean;
public addHistoricEvents(
events: IMatrixEvent[],
events: ILoadResult[],
newCheckpoint?: ICheckpoint,
oldCheckpoint?: ICheckpoint,
): Promise<boolean>;
Expand Down
40 changes: 19 additions & 21 deletions apps/desktop/src/badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
Please see LICENSE files in the repository root for full details.
*/

import { app, ipcMain, type IpcMainEvent, nativeImage } from "electron";
import { app, nativeImage } from "electron";

import { _t } from "./language-helper.js";
import { typedIpcMain } from "./ipc.js";

// Handles calculating the correct "badge" for the window, for notifications and error states.
// Tray icon updates are handled in tray.ts
Expand All @@ -16,30 +17,27 @@
// We only use setOverlayIcon on Windows as it's only supported on that platform, but has good support
// from all the Windows variants we support.
// https://www.electronjs.org/docs/latest/api/browser-window#winsetoverlayiconoverlay-description-windows
ipcMain.on(
"setBadgeCount",
function (_ev: IpcMainEvent, count: number, imageBuffer?: Buffer, isError?: boolean): void {
if (count === 0) {
// Flash frame is set to true in ipc.ts "loudNotification"
global.mainWindow?.flashFrame(false);
}
if (imageBuffer) {
global.mainWindow?.setOverlayIcon(
nativeImage.createFromBuffer(Buffer.from(imageBuffer)),
isError
? _t("icon_overlay|description_error")
: _t("icon_overlay|description_notifications", { count }),
);
} else {
global.mainWindow?.setOverlayIcon(null, "");
}
},
);
typedIpcMain.on("setBadgeCount", (_, count: number, imageBuffer?: ArrayBuffer, isError?: boolean): void => {

Check warning on line 20 in apps/desktop/src/badge.ts

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 20 is not covered by tests
if (count === 0) {
// Flash frame is set to true in ipc.ts "loudNotification"
global.mainWindow?.flashFrame(false);
}
if (imageBuffer) {
global.mainWindow?.setOverlayIcon(
nativeImage.createFromBuffer(Buffer.from(imageBuffer)),
isError
? _t("icon_overlay|description_error")
: _t("icon_overlay|description_notifications", { count }),
);
} else {
global.mainWindow?.setOverlayIcon(null, "");
}
});
} else {
// only set badgeCount on Mac/Linux, the docs say that only those platforms support it but turns out Electron
// has some Windows support too, and in some Windows environments this leads to two badges rendering atop
// each other. See https://github.com/vector-im/element-web/issues/16942
ipcMain.on("setBadgeCount", function (_ev: IpcMainEvent, count: number): void {
typedIpcMain.on("setBadgeCount", function (_, count: number): void {

Check warning on line 40 in apps/desktop/src/badge.ts

View workflow job for this annotation

GitHub Actions / Tests

Uncovered Line

Line 40 is not covered by tests
if (count === 0) {
// Flash frame is set to true in ipc.ts "loudNotification"
global.mainWindow?.flashFrame(false);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ Please see LICENSE files in the repository root for full details.

import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { type JsonObject } from "shared-types";

import { type JsonObject, loadJsonFile } from "./utils.js";
import { loadJsonFile } from "./utils.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

Expand Down
35 changes: 10 additions & 25 deletions apps/desktop/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,20 @@ Please see LICENSE files in the repository root for full details.

import { app, dialog } from "electron";
import path from "node:path";
import { type ResolveDefaults, type DesktopConfigJson, type JsonDocument } from "shared-types";

import { getAsarPath } from "./asar.js";
import { type Json, loadJsonFile } from "./utils.js";

export interface ConfigOptions {
brand: string;
help_url: string;
web_base_url: string;
modules?: string[];
sentry?: {
dsn?: string;
environment?: string;
};
update_base_url?: string;

// homeserver props
default_is_url?: string;
default_hs_url?: string;
default_server_name?: string;
default_server_config?: object;
}
import { loadJsonFile } from "./utils.js";

export type ConfigOptions = ResolveDefaults<DesktopConfigJson, typeof DEFAULTS>;

const ConfigFilename = "config.json";

let config: ConfigOptions;

const homeserverProps = ["default_is_url", "default_hs_url", "default_server_name", "default_server_config"] as const;

function loadLocalConfigFile(location: string | undefined): Json {
function loadLocalConfigFile(location: string | undefined): JsonDocument {
if (location) {
console.log("Loading local config: " + location);
return loadJsonFile(location);
Expand All @@ -50,9 +35,9 @@ const DEFAULTS = {
brand: "Element",
help_url: "https://element.io/help",
web_base_url: "https://app.element.io/",
} satisfies ConfigOptions;
} satisfies DesktopConfigJson;

function applyDefaults(conf: ConfigOptions): void {
function applyDefaults(conf: DesktopConfigJson): asserts conf is ConfigOptions {
for (const k in DEFAULTS) {
const key = k as keyof typeof DEFAULTS;
conf[key] ||= DEFAULTS[key];
Expand All @@ -71,7 +56,9 @@ export function loadConfig(localConfigPath: string | undefined): Promise<ConfigO
try {
console.log(`Loading app config: ${path.join(asarPath, ConfigFilename)}`);
// XXX: we trust that we built the package with a sane config, but should use something like zod here in future
config = loadJsonFile(asarPath, ConfigFilename) as unknown as ConfigOptions;
const loadedConfig = loadJsonFile(asarPath, ConfigFilename) as unknown as DesktopConfigJson;
applyDefaults(loadedConfig);
config = loadedConfig;
} catch {
// it would be nice to check the error code here and bail if the config
// is unparsable, but we get MODULE_NOT_FOUND in the case of a missing
Expand All @@ -80,8 +67,6 @@ export function loadConfig(localConfigPath: string | undefined): Promise<ConfigO
config = { ...DEFAULTS };
}

applyDefaults(config);

try {
// Load local config and use it to override values from the one baked with the build
const localConfig = loadLocalConfigFile(localConfigPath);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/electron-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import windowStateKeeper from "electron-window-state";
import { URL, fileURLToPath } from "node:url";

import "./ipc.js";
import "./seshat.js";
import { setupListeners as setupSeshatListeners } from "./seshat.js";
import "./settings.js";
import "./badge.js";
import * as tray from "./tray.js";
Expand Down Expand Up @@ -134,6 +134,7 @@ app.enableSandbox();
app.commandLine.appendSwitch("disable-features", "HardwareMediaKeyHandling,MediaSessionService");

const store = Store.initialize(args.storageMode); // must be called before any async actions
setupSeshatListeners(store);

// Disable hardware acceleration if the setting has been set.
if (store.get("disableHardwareAcceleration")) {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
*/

import { expect, describe, it, vi } from "vitest";
// eslint-disable-next-line no-restricted-imports
import { ipcMain, type IpcMainInvokeEvent } from "electron";

import { getConfig } from "./config.js";
Expand Down
Loading
Loading