Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add application config aka settings #835

Merged
merged 9 commits into from
Oct 22, 2024
Merged
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
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
},
],
}
144 changes: 144 additions & 0 deletions src/app/AppConfig.ts
Original file line number Diff line number Diff line change
@@ -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\<username>\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<AppConfig> = {}
/** 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<Partial<AppConfig>> {
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<AppConfig>) {
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<T extends keyof AppConfig>(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<T extends keyof AppConfig>(key?: T): AppConfig | AppConfig[T] {
if (!initialized) {
throw new Error('The application config is not initialized yet')
}

const config = Object.assign({}, defaultAppConfig, appConfig)
Antreesy marked this conversation as resolved.
Show resolved Hide resolved

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<AppConfig> - The full settings after the change
*/
export async function setAppConfig<K extends keyof AppConfig>(key: K, value?: AppConfig[K]) {
if (value !== undefined) {
appConfig[key] = value
} else {
delete appConfig[key]
}
await writeAppConfigFile(appConfig)
}
4 changes: 2 additions & 2 deletions src/authentication/authentication.window.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const { BrowserWindow } = require('electron')
const { BASE_TITLE, TITLE_BAR_HEIGHT } = require('../constants.js')
const { applyContextMenu } = require('../app/applyContextMenu.js')
const { getBrowserWindowIcon } = require('../shared/icons.utils.js')
const { isLinux } = require('../shared/os.utils.js')
const { getAppConfig } = require('../app/AppConfig.ts')

/**
* @return {import('electron').BrowserWindow}
Expand All @@ -29,7 +29,7 @@ function createAuthenticationWindow() {
preload: AUTHENTICATION_WINDOW_PRELOAD_WEBPACK_ENTRY,
},
icon: getBrowserWindowIcon(),
titleBarStyle: isLinux() ? 'default' : 'hidden',
titleBarStyle: getAppConfig('systemTitleBar') ? 'default' : 'hidden',
titleBarOverlay: {
color: '#00679E00', // Transparent
symbolColor: '#FFFFFF', // White
Expand Down
23 changes: 21 additions & 2 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -97,7 +100,15 @@ 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()

try {
await installVueDevtools()
} catch (error) {
Expand Down Expand Up @@ -256,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()
}
Expand All @@ -269,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()
}
})
19 changes: 18 additions & 1 deletion src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,26 @@ 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
* @return {Promise<Record<string, unknown> | 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<void>}
*/
setAppConfig: (key, value) => ipcRenderer.invoke('app:config:set', key, value),
/**
* Send appData to main process on restore
*
Expand Down
44 changes: 44 additions & 0 deletions src/shared/appConfig.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { AppConfig } from '../app/AppConfig.ts'

let appConfig: AppConfig | null = null

/**
* Initialize the AppConfig
*/
export async function initAppConfig() {
if (appConfig) {
return
}

appConfig = await window.TALK_DESKTOP.getAppConfig()
}

/**
* Get AppConfig
* @return - AppConfig
*/
export function getAppConfig() {
if (!appConfig) {
throw new Error('AppConfig is not initialized')
}

return appConfig
}

/**
* Get an application config value
* @param key - The key of the config value
* @return - The config value
*/
export function getAppConfigValue<K extends keyof AppConfig>(key: K) {
if (!appConfig) {
throw new Error('AppConfig is not initialized')
}

return appConfig[key]
}
4 changes: 3 additions & 1 deletion src/shared/setupWebPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { applyBodyThemeAttrs } from './theme.utils.js'
import { appData } from '../app/AppData.js'
import { initGlobals } from './globals/globals.js'
import { setupInitialState } from './initialState.service.js'
import { getAppConfigValue, initAppConfig } from './appConfig.service.ts' // eslint-disable-line import/namespace
import { TITLE_BAR_HEIGHT } from '../constants.js'

/**
Expand Down Expand Up @@ -204,11 +205,12 @@ function applyHeaderHeight() {
export async function setupWebPage() {
document.title = await window.TALK_DESKTOP.getAppName()
appData.restore()
await initAppConfig()
applyInitialState()
initGlobals()
window.OS = await window.TALK_DESKTOP.getOs()
applyUserData()
applyBodyThemeAttrs()
applyBodyThemeAttrs(getAppConfigValue('theme'))
applyHeaderHeight()
applyAxiosInterceptors()
await applyL10n()
Expand Down
Loading