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 spell checking config #855

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
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
3,002 changes: 3,002 additions & 0 deletions resources/locales.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/app/AppConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ export type AppConfig = {
* Default: 1.
*/
zoomFactor: number
/**
* List of languages to use for spell checking in addition to the system language.
* Default: [], use system preferences
*/
spellCheckLanguages: string[]

// ----------------
// Privacy settings
Expand All @@ -74,6 +79,7 @@ const defaultAppConfig: AppConfig = {
systemTitleBar: isLinux(),
monochromeTrayIcon: isMac(),
zoomFactor: 1,
spellCheckLanguages: [],
}

/** Local cache of the config file mixed with the default values */
Expand Down
154 changes: 154 additions & 0 deletions src/app/__AppConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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, isMac } 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
// ----------------

/** Whether the app should run on startup. Not implemented */
runOnStartup: boolean

// -------------------
// Appearance settings
// -------------------

/** Whether to use a monochrome tray icon. By default, true only on Mac. Not implemented */
monochromeTrayIcon: boolean
/** The scale factor for the app. By default, 1. Not implemented */
scale: number
/** List of languages to use for spell checking in addition to the system language. By default, none. Not implemented */
spellCheckLanguages: string[]

// ----------------
// Privacy settings
// ----------------

/** Whether to show message previews in notifications. By default, true. Not implemented */
showMessagePreviewInNotifications: boolean

// ----------------------
// Notifications settings
// ----------------------
/**
* Whether to play a sound when a notification is received
* - always: always play sound
* - respect-dnd: play sound only if user status isn't Do-Not-Disturb [default]
* - never: disable notification sound
* Not implemented
*/
playSound: 'always' | 'respect-dnd' | 'never'
}

/**
* Get the default config
*/
const defaultAppConfig: AppConfig = {
runOnStartup: false,
theme: 'default',
systemTitleBar: isLinux(),
monochromeTrayIcon: isMac(),
scale: 1,
spellCheckLanguages: [],
showMessagePreviewInNotifications: true,
playSound: 'respect-dnd',
}

/** 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)

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)
}
3 changes: 2 additions & 1 deletion src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

const path = require('node:path')
const { spawn } = require('node:child_process')
const { app, dialog, BrowserWindow, ipcMain, desktopCapturer, systemPreferences, shell } = require('electron')
const { app, dialog, BrowserWindow, ipcMain, desktopCapturer, systemPreferences, shell, session } = require('electron')
const { setupMenu } = require('./app/app.menu.js')
const { setupReleaseNotificationScheduler } = require('./app/githubReleaseNotification.service.js')
const { enableWebRequestInterceptor, disableWebRequestInterceptor } = require('./app/webRequestInterceptor.js')
Expand Down Expand Up @@ -68,6 +68,7 @@ ipcMain.handle('app:getSystemL10n', () => ({
locale: app.getLocale().replace('-', '_'),
language: app.getPreferredSystemLanguages()[0].replace('-', '_'),
}))
ipcMain.handle('app:getAvailableSpellCheckerLanguages', () => session.defaultSession.availableSpellCheckerLanguages)
ipcMain.handle('app:enableWebRequestInterceptor', (event, ...args) => enableWebRequestInterceptor(...args))
ipcMain.handle('app:disableWebRequestInterceptor', (event, ...args) => disableWebRequestInterceptor(...args))
ipcMain.handle('app:setBadgeCount', async (event, count) => app.setBadgeCount(count))
Expand Down
6 changes: 6 additions & 0 deletions src/preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ const TALK_DESKTOP = {
* @return {Promise<{ locale: string, language: string }>}
*/
getSystemL10n: () => ipcRenderer.invoke('app:getSystemL10n'),
/**
* Get available spell checker languages
*
* @return {Promise<string[]>}
*/
getAvailableSpellCheckerLanguages: () => ipcRenderer.invoke('app:getAvailableSpellCheckerLanguages'),
/**
* Enable web request intercepting
*
Expand Down
30 changes: 30 additions & 0 deletions src/talk/renderer/Settings/DesktopSettingsSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import { useNcSelectModel } from '../composables/useNcSelectModel.ts'
import { useAppConfig } from './appConfig.store.ts'
import { ZOOM_MIN, ZOOM_MAX } from '../../../constants.js'
import locales from '../../../../resources/locales.json'

const { isRelaunchRequired } = storeToRefs(useAppConfig())

Expand All @@ -36,6 +37,8 @@

const systemTitleBar = useAppConfigValue('systemTitleBar')
const monochromeTrayIcon = useAppConfigValue('monochromeTrayIcon')
const spellCheckLanguages = useAppConfigValue('spellCheckLanguages')
const availableSpellCheckLanguages = ref<Set<string>>(new Set())

const zoomFactorConfig = useAppConfigValue('zoomFactor')
const zoomFactor = computed({
Expand All @@ -57,6 +60,29 @@
resetKey: `<kbd>${ctrl} + 0</kbd>`,
}, undefined, { escape: false })

window.TALK_DESKTOP.getAvailableSpellCheckerLanguages().then((languages: string[]) => {
availableSpellCheckLanguages.value = new Set(languages.map((code) => code.replaceAll('_', '-')))
console.log('availableSpellCheckLanguages', languages)
})

const langToSelectOption = (code: string) => ({ label: locales.find(locale => locale), value: code }))

Check failure on line 68 in src/talk/renderer/Settings/DesktopSettingsSection.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

',' expected.
}

Check failure on line 69 in src/talk/renderer/Settings/DesktopSettingsSection.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

Declaration or statement expected.

const spellCheckLanguagesOptions = computed(() => [
{ label: t('talk_desktop', 'System default'), value: 'default' },
...locales
.filter(({ code }) => availableSpellCheckLanguages.value.has(code))
.map(({ code, name }) => ({ label: name, value: code })),
])
const selectedSpellCheckLanguagesOption = computed(() => {
get() {

Check failure on line 78 in src/talk/renderer/Settings/DesktopSettingsSection.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

';' expected.
return
},

Check failure on line 80 in src/talk/renderer/Settings/DesktopSettingsSection.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

Declaration or statement expected.
set(value) {

Check failure on line 81 in src/talk/renderer/Settings/DesktopSettingsSection.vue

View workflow job for this annotation

GitHub Actions / NPM typecheck

';' expected.

}
})

/**
* Restart the app
*/
Expand Down Expand Up @@ -129,6 +155,10 @@
</NcButton>
</template>
</SettingsFormGroup>

<SettingsSelect v-model="spellCheckLanguagesOption" :options="spellCheckLanguagesOptions" searchable tags>
{{ t('talk_desktop', 'Spell check languages') }}
</SettingsSelect>
</SettingsSubsection>
</div>
</template>
Expand Down
2 changes: 2 additions & 0 deletions src/talk/renderer/Settings/components/SettingsSelect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const model = computed({

<script lang="ts">
export default {
inheritAttrs: false,

model: {
prop: 'modelValue',
event: 'update:modelValue',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import { t } from '@nextcloud/l10n'
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js'
import IconAccountMultiplePlus from 'vue-material-design-icons/AccountMultiplePlus.vue'
import IconDelete from 'vue-material-design-icons/Delete.vue'
import SettingsSubsection from './SettingsSubsection.vue'
</script>

<template>
<SettingsSubsection>
<ul>
<NcListItem name="Grigorii K. Shartsev" details="nextcloud.local">
<template #icon>
<NcAvatar user="shgkme" />
</template>
<template #subname>
[email protected]
</template>
<template #actions>
<NcActionButton>
<template #icon>
<IconDelete :size="20" />
</template>
{{ t('talk_desktop', 'Remove') }}
</NcActionButton>
</template>
</NcListItem>
<NcListItem name="admin" details="stable30.local">
<template #icon>
<NcAvatar user="admin" />
</template>
<template #subname>
[email protected]
</template>
<template #actions>
<NcActionButton>
<template #icon>
<IconDelete :size="20" />
</template>
{{ t('talk_desktop', 'Remove') }}
</NcActionButton>
</template>
</NcListItem>
</ul>
<NcButton wide>
<template #icon>
<IconAccountMultiplePlus :size="20" />
</template>
{{ t('talk_desktop', 'Log into a new account') }}
</NcButton>
</SettingsSubsection>
</template>
Loading
Loading