Skip to content
Open
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
11 changes: 10 additions & 1 deletion apps/theming/lib/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class ConfigLexicon implements ILexicon {
/** The cache buster index */
public const CACHE_BUSTER = 'cachebuster';
public const USER_THEMING_DISABLED = 'disable-user-theming';
public const TOAST_TIMEOUT = 'toast_timeout';
public const TOAST_TIMEOUT_DEFAULT = 7000;

/** Name of the software running on this instance (usually "Nextcloud") */
public const PRODUCT_NAME = 'productName';
Expand Down Expand Up @@ -114,6 +116,13 @@ public function getAppConfigs(): array {

#[\Override]
public function getUserConfigs(): array {
return [];
return [
new Entry(
self::TOAST_TIMEOUT,
ValueType::INT,
defaultRaw: self::TOAST_TIMEOUT_DEFAULT,
definition: 'How long toast notifications remain visible in milliseconds.',
),
];
}
}
15 changes: 14 additions & 1 deletion apps/theming/lib/Listener/BeforePreferenceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\Theming\Listener;

use OCA\Theming\AppInfo\Application;
use OCA\Theming\ConfigLexicon;
use OCP\App\IAppManager;
use OCP\Config\BeforePreferenceDeletedEvent;
use OCP\Config\BeforePreferenceSetEvent;
Expand All @@ -22,7 +23,15 @@ class BeforePreferenceListener implements IEventListener {
/**
* @var string[]
*/
private const ALLOWED_KEYS = ['force_enable_blur_filter', 'shortcuts_disabled', 'primary_color'];
private const ALLOWED_KEYS = ['force_enable_blur_filter', 'shortcuts_disabled', 'primary_color', ConfigLexicon::TOAST_TIMEOUT];

/**
* Allowed toast timeout values in milliseconds.
* Default (7000) is represented by deleting the preference.
*
* @var int[]
*/
public const array TOAST_TIMEOUT_VALUES = [15000, 30000, -1];

public function __construct(
private IAppManager $appManager,
Expand Down Expand Up @@ -62,6 +71,10 @@ private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDe
case 'primary_color':
$event->setValid(preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $event->getConfigValue()) === 1);
break;
case ConfigLexicon::TOAST_TIMEOUT:
$value = filter_var($event->getConfigValue(), FILTER_VALIDATE_INT);
$event->setValid($value !== false && in_array($value, self::TOAST_TIMEOUT_VALUES, true));
break;
default:
$event->setValid(false);
}
Expand Down
14 changes: 14 additions & 0 deletions apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
namespace OCA\Theming\Listener;

use OCA\Theming\AppInfo\Application;
use OCA\Theming\ConfigLexicon;
use OCA\Theming\Service\JSDataService;
use OCA\Theming\Service\ThemeInjectionService;
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Config\IUserConfig;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
Expand All @@ -32,6 +34,7 @@ public function __construct(
private ThemeInjectionService $themeInjectionService,
private IUserSession $userSession,
private IConfig $config,
private IUserConfig $userConfig,
) {
}

Expand All @@ -51,6 +54,17 @@ public function handle(Event $event): void {
}
return false;
});

$this->initialState->provideLazyInitialState('toastTimeout', function (): int {
if ($this->userSession->getUser()) {
$uid = $this->userSession->getUser()->getUID();
$value = $this->userConfig->getValueInt($uid, Application::APP_ID, ConfigLexicon::TOAST_TIMEOUT, ConfigLexicon::TOAST_TIMEOUT_DEFAULT);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$value = $this->userConfig->getValueInt($uid, Application::APP_ID, ConfigLexicon::TOAST_TIMEOUT, ConfigLexicon::TOAST_TIMEOUT_DEFAULT);
$value = $this->userConfig->getValueInt($uid, Application::APP_ID, ConfigLexicon::TOAST_TIMEOUT);

no need to specify the default as this is handled by the config lexicon

if (in_array($value, BeforePreferenceListener::TOAST_TIMEOUT_VALUES, true) || $value === ConfigLexicon::TOAST_TIMEOUT_DEFAULT) {
return $value;
}
}
return ConfigLexicon::TOAST_TIMEOUT_DEFAULT;
});
Comment on lines +58 to +67

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is better to exposed as a capability instead - because this might also be used by clients.

}

$this->themeInjectionService->injectHeaders();
Expand Down
110 changes: 110 additions & 0 deletions apps/theming/src/components/UserSectionToastTimeout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import axios from '@nextcloud/axios'
import {
setToastTimeout,
showError,
TOAST_DEFAULT_TIMEOUT,
TOAST_PERMANENT_TIMEOUT,
} from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
import { computed, ref } from 'vue'
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { logger } from '../utils/logger.ts'

const TOAST_TIMEOUT_15S = 15_000
const TOAST_TIMEOUT_30S = 30_000

const toastTimeout = ref(loadState<number>('theming', 'toastTimeout', TOAST_DEFAULT_TIMEOUT))

const options = computed(() => [
{
value: TOAST_DEFAULT_TIMEOUT,
label: t('theming', 'Default ({time} seconds)', { time: TOAST_DEFAULT_TIMEOUT / 1000 }),
},
{
value: TOAST_TIMEOUT_15S,
label: t('theming', '15 seconds'),
},
{
value: TOAST_TIMEOUT_30S,
label: t('theming', '30 seconds'),
},
{
value: TOAST_PERMANENT_TIMEOUT,
label: t('theming', 'Never dismiss'),
},
])

/**
* Persist and apply the selected toast timeout
*
* @param value - Selected timeout preference value
*/
async function updateToastTimeout(value: string | number | boolean) {
const nextValue = Number(value)
const previous = toastTimeout.value
toastTimeout.value = nextValue
setToastTimeout(nextValue)

const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
appId: 'theming',
configKey: 'toast_timeout',
})

try {
if (nextValue === TOAST_DEFAULT_TIMEOUT) {
await axios.delete(url)
} else {
await axios.post(url, {
configValue: String(nextValue),
})
}
} catch (error) {
toastTimeout.value = previous
setToastTimeout(previous)
logger.error('Could not update toast timeout', { error })
showError(t('theming', 'Could not update toast timeout'))
}
}
</script>

<template>
<NcSettingsSection
:name="t('theming', 'Toast notifications')"
:description="t('theming', 'Set how long toast messages stay visible. Choose a longer duration if you need more time to read them.')">
<fieldset class="toast-timeout">
<legend class="hidden-visually">
{{ t('theming', 'Toast timeout') }}
</legend>
<NcCheckboxRadioSwitch
v-for="option in options"
:key="option.value"
:modelValue="toastTimeout"
type="radio"
name="toast_timeout"
:value="option.value"
@update:modelValue="updateToastTimeout">
{{ option.label }}
</NcCheckboxRadioSwitch>
</fieldset>
</NcSettingsSection>
</template>

<style scoped lang="scss">
.toast-timeout {
display: flex;
flex-direction: column;
gap: 4px;
border: 0;
margin: 0;
padding: 0;
}
</style>
6 changes: 6 additions & 0 deletions apps/theming/src/theming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { setToastTimeout, TOAST_DEFAULT_TIMEOUT } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'

window.OCA.Theming = loadState('theming', 'data')

const toastTimeout = loadState('theming', 'toastTimeout', TOAST_DEFAULT_TIMEOUT)
if (typeof toastTimeout === 'number') {
setToastTimeout(toastTimeout)
}
2 changes: 2 additions & 0 deletions apps/theming/src/views/UserTheming.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
</template>

<UserSectionHotkeys />
<UserSectionToastTimeout />
<UserSectionAppMenu />
</template>

Expand All @@ -68,6 +69,7 @@ import UserSectionAppMenu from '../components/UserSectionAppMenu.vue'
import UserSectionBackground from '../components/UserSectionBackground.vue'
import UserSectionHotkeys from '../components/UserSectionHotkeys.vue'
import UserSectionPrimaryColor from '../components/UserSectionPrimaryColor.vue'
import UserSectionToastTimeout from '../components/UserSectionToastTimeout.vue'
import { refreshStyles } from '../utils/refreshStyles.js'

const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
Expand Down
Loading