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
13 changes: 12 additions & 1 deletion apps/theming/lib/Listener/BeforePreferenceListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,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', 'toast_timeout'];

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

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
public const TOAST_TIMEOUT_VALUES = ['15000', '30000', '-1'];
public const array TOAST_TIMEOUT_VALUES = ['15000', '30000', '-1'];

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.

Should be integers instead


public function __construct(
private IAppManager $appManager,
Expand Down Expand Up @@ -62,6 +70,9 @@ 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 'toast_timeout':
$event->setValid(in_array($event->getConfigValue(), self::TOAST_TIMEOUT_VALUES, true));
break;
default:
$event->setValid(false);
}
Expand Down
11 changes: 11 additions & 0 deletions apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@
}
return false;
});

$this->initialState->provideLazyInitialState('toastTimeout', function () {

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
$this->initialState->provideLazyInitialState('toastTimeout', function () {
$this->initialState->provideLazyInitialState('toastTimeout', function (): int {

if ($this->userSession->getUser()) {
$uid = $this->userSession->getUser()->getUID();
$value = $this->config->getUserValue($uid, Application::APP_ID, 'toast_timeout', '7000');

Check failure on line 58 in apps/theming/lib/Listener/BeforeTemplateRenderedListener.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

DeprecatedMethod

apps/theming/lib/Listener/BeforeTemplateRenderedListener.php:58:30: DeprecatedMethod: The method OCP\IConfig::getUserValue has been marked as deprecated (see https://psalm.dev/001)

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.

please use IUserConfig in new code

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.

Yes and ideally config lexicon

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.

Same here please use the correct type, there is no reason for string

if (in_array($value, BeforePreferenceListener::TOAST_TIMEOUT_VALUES, true) || $value === '7000') {
return (int)$value;
}
}
return 7000;
});
}

$this->themeInjectionService->injectHeaders();
Expand Down
112 changes: 112 additions & 0 deletions apps/theming/src/components/UserSectionToastTimeout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!--
- 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_DEFAULT = String(TOAST_DEFAULT_TIMEOUT)
const TOAST_TIMEOUT_15S = '15000'
const TOAST_TIMEOUT_30S = '30000'
const TOAST_TIMEOUT_NEVER = String(TOAST_PERMANENT_TIMEOUT)

const toastTimeout = ref(String(loadState('theming', 'toastTimeout', TOAST_DEFAULT_TIMEOUT)))
Comment on lines +22 to +27

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.

As mentioned above there is no reason for string type, this is an int so also pass it as integer


const options = computed(() => [
{
value: TOAST_TIMEOUT_DEFAULT,
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_TIMEOUT_NEVER,
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 = String(value)
const previous = toastTimeout.value
toastTimeout.value = nextValue
setToastTimeout(Number(nextValue))

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

try {
if (nextValue === TOAST_TIMEOUT_DEFAULT) {
await axios.delete(url)
} else {
await axios.post(url, {
configValue: nextValue,
})
}
} catch (error) {
toastTimeout.value = previous
setToastTimeout(Number(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