From 908cc312915029f0da6e3bf764f75c136eb13eae Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Mon, 21 Oct 2024 10:04:02 +0200 Subject: [PATCH 1/9] chore(ts): add TS support on main process Signed-off-by: Grigorii K. Shartsev --- webpack.main.config.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webpack.main.config.js b/webpack.main.config.js index 3fd3b8db..923fde57 100644 --- a/webpack.main.config.js +++ b/webpack.main.config.js @@ -16,6 +16,13 @@ module.exports = merge(baseConfig, { module: { rules: [ + { + test: /\.ts$/, + loader: 'esbuild-loader', + options: { + target: 'es2022', + }, + }, { test: /\.(png|ico|icns)$/, include: path.resolve(__dirname, './img/icons'), From ead16f7aa665c39e3d0d79ba92d564cfd56ad26e Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Mon, 21 Oct 2024 10:04:30 +0200 Subject: [PATCH 2/9] chore(lint): do not require jsdoc returns type in TS Signed-off-by: Grigorii K. Shartsev --- .eslintrc.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index af4a38dc..3e9b750c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -124,4 +124,13 @@ module.exports = { '@nextcloud/no-deprecations': 'off', '@nextcloud/no-removed-apis': 'off', }, + + overrides: [ + { + files: '*.ts', + rules: { + 'jsdoc/require-returns-type': 'off', // TODO upstream + }, + }, + ], } From dea0632df556693e7d8ba8e18bf82a9d303e6f92 Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Mon, 21 Oct 2024 11:25:47 +0200 Subject: [PATCH 3/9] feat(config): add AppConfig backend Signed-off-by: Grigorii K. Shartsev --- src/app/AppConfig.ts | 144 +++++++++++++++++++++++++++++++++++++++++++ src/main.js | 5 ++ src/preload.js | 13 ++++ 3 files changed, 162 insertions(+) create mode 100644 src/app/AppConfig.ts diff --git a/src/app/AppConfig.ts b/src/app/AppConfig.ts new file mode 100644 index 00000000..576a65ee --- /dev/null +++ b/src/app/AppConfig.ts @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { join } from 'node:path' +import { readFile, writeFile } from 'node:fs/promises' +import { app } from 'electron' +import { isLinux } from '../shared/os.utils.js' + +const APP_CONFIG_FILE_NAME = 'config.json' + +// Windows: C:\Users\\AppData\Roaming\Nextcloud Talk\config.json +// Linux: ~/.config/Nextcloud Talk/config.json (or $XDG_CONFIG_HOME) +// macOS: ~/Library/Application Support/Nextcloud Talk/config.json +const APP_CONFIG_FILE_PATH = join(app.getPath('userData'), APP_CONFIG_FILE_NAME) + +/** + * Application level config. Applied to all accounts and persist on re-login. + * Stored in the application data directory. + */ +export type AppConfig = { + // ---------------- + // General settings + // ---------------- + + // Nothing yet... + + // ------------------- + // Appearance settings + // ------------------- + + /** + * Application theme. + * Default: 'default' to follow the system theme. + */ + theme: 'default' | 'dark' | 'light' + /** + * Whether to use a custom title bar or the system default. + * Default: true on Linux, false otherwise. + */ + systemTitleBar: boolean + + // ---------------- + // Privacy settings + // ---------------- + + // Nothing yet... + + // ---------------------- + // Notifications settings + // ---------------------- + + // Nothing yet... +} + +/** + * Get the default config + */ +const defaultAppConfig: AppConfig = { + theme: 'default', + systemTitleBar: isLinux(), +} + +/** Local cache of the config file mixed with the default values */ +const appConfig: Partial = {} +/** Whether the application config has been read from the config file and ready to use */ +let initialized = false + +/** + * Read the application config from the file + */ +async function readAppConfigFile(): Promise> { + try { + const content = await readFile(APP_CONFIG_FILE_PATH, 'utf-8') + return JSON.parse(content) + } catch (error) { + if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { + console.error('Failed to read the application config file', error) + } + // No file or invalid file - no custom config + return {} + } +} + +/** + * Write the application config to the config file + * @param config - The config to write + */ +async function writeAppConfigFile(config: Partial) { + try { + // Format for readability + const content = JSON.stringify(config, null, 2) + await writeFile(APP_CONFIG_FILE_PATH, content) + } catch (error) { + console.error('Failed to write the application config file', error) + throw error + } +} + +/** + * Load the application config into the application memory + */ +export async function loadAppConfig() { + const config = await readAppConfigFile() + Object.assign(appConfig, config) + initialized = true +} + +export function getAppConfig(): AppConfig +export function getAppConfig(key?: T): AppConfig[T] +/** + * Get an application config value + * @param key - The config key to get + * @return - If key is provided, the value of the key. Otherwise, the full config + */ +export function getAppConfig(key?: T): AppConfig | AppConfig[T] { + if (!initialized) { + throw new Error('The application config is not initialized yet') + } + + const config = Object.assign({}, defaultAppConfig, appConfig) + + if (key) { + return config[key] + } + + return config +} + +/** + * Set an application config value + * @param key - Settings key to set + * @param value - Value to set or undefined to reset to the default value + * @return Promise - The full settings after the change + */ +export async function setAppConfig(key: K, value?: AppConfig[K]) { + if (value !== undefined) { + appConfig[key] = value + } else { + delete appConfig[key] + } + await writeAppConfigFile(appConfig) +} diff --git a/src/main.js b/src/main.js index 3be59e98..bea20d9e 100644 --- a/src/main.js +++ b/src/main.js @@ -16,6 +16,7 @@ const { getOs, isLinux, isMac, isWayland } = require('./shared/os.utils.js') const { createTalkWindow } = require('./talk/talk.window.js') const { createWelcomeWindow } = require('./welcome/welcome.window.js') const { installVueDevtools } = require('./install-vue-devtools.js') +const { loadAppConfig, getAppConfig, setAppConfig } = require('./app/AppConfig.ts') /** * Parse command line arguments @@ -67,6 +68,8 @@ ipcMain.on('app:relaunch', () => { app.relaunch() app.exit(0) }) +ipcMain.handle('app:config:get', (event, key) => getAppConfig(key)) +ipcMain.handle('app:config:set', (event, key, value) => setAppConfig(key, value)) ipcMain.handle('app:getDesktopCapturerSources', async () => { // macOS 10.15 Catalina or higher requires consent for screen access if (isMac() && systemPreferences.getMediaAccessStatus('screen') !== 'granted') { @@ -98,6 +101,8 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => { }) app.whenReady().then(async () => { + await loadAppConfig() + try { await installVueDevtools() } catch (error) { diff --git a/src/preload.js b/src/preload.js index 3f18f163..d31b42e0 100644 --- a/src/preload.js +++ b/src/preload.js @@ -89,6 +89,19 @@ const TALK_DESKTOP = { * Relaunch the application */ relaunch: () => ipcRenderer.send('app:relaunch'), + /** + * Get an application config value by key + * @param {string} [key] - Config key + * @return {Promise | unknown>} + */ + getAppConfig: (key) => ipcRenderer.invoke('app:config:get', key), + /** + * Set an application config value by key + * @param {string} key - Config key + * @param {any} [value] - Config value + * @return {Promise} + */ + setAppConfig: (key, value) => ipcRenderer.invoke('app:config:set', key, value), /** * Send appData to main process on restore * From a285460ef622062f3d7b794979c1e96cb70a5c2c Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Mon, 21 Oct 2024 11:37:25 +0200 Subject: [PATCH 4/9] feat(app): add relaunch window Signed-off-by: Grigorii K. Shartsev --- src/main.js | 18 ++++++++++++++++-- src/preload.js | 6 +++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main.js b/src/main.js index bea20d9e..50d4eb4f 100644 --- a/src/main.js +++ b/src/main.js @@ -100,6 +100,12 @@ ipcMain.handle('app:getDesktopCapturerSources', async () => { })) }) +/** + * Whether the window is being relaunched. + * At this moment there are no active windows, but the application should not quit yet. + */ +let isInWindowRelaunch = false + app.whenReady().then(async () => { await loadAppConfig() @@ -261,9 +267,17 @@ app.whenReady().then(async () => { mainWindow = upgradeWindow }) + ipcMain.on('app:relaunchWindow', () => { + isInWindowRelaunch = true + mainWindow.destroy() + mainWindow = createMainWindow() + mainWindow.once('ready-to-show', () => mainWindow.show()) + isInWindowRelaunch = false + }) + // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - app.on('activate', function() { + app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createMainWindow() } @@ -274,7 +288,7 @@ app.whenReady().then(async () => { // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { + if (process.platform !== 'darwin' && !isInWindowRelaunch) { app.quit() } }) diff --git a/src/preload.js b/src/preload.js index d31b42e0..e6fdae37 100644 --- a/src/preload.js +++ b/src/preload.js @@ -86,9 +86,13 @@ const TALK_DESKTOP = { */ getDesktopCapturerSources: () => ipcRenderer.invoke('app:getDesktopCapturerSources'), /** - * Relaunch the application + * Relaunch an entire application */ relaunch: () => ipcRenderer.send('app:relaunch'), + /** + * Relaunch the main window without relaunching an entire application + */ + relaunchWindow: () => ipcRenderer.send('app:relaunchWindow'), /** * Get an application config value by key * @param {string} [key] - Config key From c7dc948265855771d6e5a80d26602769f169185c Mon Sep 17 00:00:00 2001 From: "Grigorii K. Shartsev" Date: Mon, 21 Oct 2024 11:41:32 +0200 Subject: [PATCH 5/9] feat(config): add settings open button to the menu Signed-off-by: Grigorii K. Shartsev --- src/talk/renderer/components/MainMenu.vue | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/talk/renderer/components/MainMenu.vue b/src/talk/renderer/components/MainMenu.vue index fa9910a1..0ae40c82 100644 --- a/src/talk/renderer/components/MainMenu.vue +++ b/src/talk/renderer/components/MainMenu.vue @@ -6,6 +6,7 @@