Просмотр исходного кода

configurable notification timeout (#7942)

Ref #7931
Ref #5466
Ref #6409

added configuration in "Display"
<img width="636" height="167" alt="grafik" src="https://github.com/user-attachments/assets/7bbc9f26-d91b-4dd2-b715-1d3f9b7a9ad3" />

* i18n: fr

* Update app/i18n/pl/conf.php

Co-authored-by: Inverle <inverle@proton.me>

* make fix-all

* max()

* Minor whitespace
(I am not a fan of excessive vertical indenting)

---------

Co-authored-by: Alexandre Alapetite <alexandre@alapetite.fr>
Co-authored-by: Inverle <inverle@proton.me>
maTh 6 месяцев назад
Родитель
Сommit
2bcc090622
49 измененных файлов с 561 добавлено и 75 удалено
  1. 5 1
      app/Controllers/apiController.php
  2. 20 4
      app/Controllers/authController.php
  3. 25 5
      app/Controllers/categoryController.php
  4. 57 12
      app/Controllers/configureController.php
  5. 12 6
      app/Controllers/entryController.php
  6. 15 3
      app/Controllers/extensionController.php
  7. 45 15
      app/Controllers/feedController.php
  8. 4 1
      app/Controllers/importExportController.php
  9. 10 2
      app/Controllers/subscriptionController.php
  10. 14 3
      app/Controllers/tagController.php
  11. 4 1
      app/Controllers/updateController.php
  12. 31 8
      app/Controllers/userController.php
  13. 2 0
      app/Models/UserConfiguration.php
  14. 10 0
      app/i18n/cs/conf.php
  15. 10 0
      app/i18n/de/conf.php
  16. 10 0
      app/i18n/el/conf.php
  17. 10 0
      app/i18n/en-us/conf.php
  18. 10 0
      app/i18n/en/conf.php
  19. 10 0
      app/i18n/es/conf.php
  20. 10 0
      app/i18n/fa/conf.php
  21. 10 0
      app/i18n/fi/conf.php
  22. 10 0
      app/i18n/fr/conf.php
  23. 10 0
      app/i18n/he/conf.php
  24. 10 0
      app/i18n/hu/conf.php
  25. 10 0
      app/i18n/id/conf.php
  26. 10 0
      app/i18n/it/conf.php
  27. 10 0
      app/i18n/ja/conf.php
  28. 10 0
      app/i18n/ko/conf.php
  29. 10 0
      app/i18n/lv/conf.php
  30. 10 0
      app/i18n/nl/conf.php
  31. 10 0
      app/i18n/oc/conf.php
  32. 10 0
      app/i18n/pl/conf.php
  33. 10 0
      app/i18n/pt-br/conf.php
  34. 10 0
      app/i18n/pt-pt/conf.php
  35. 10 0
      app/i18n/ru/conf.php
  36. 10 0
      app/i18n/sk/conf.php
  37. 10 0
      app/i18n/tr/conf.php
  38. 10 0
      app/i18n/uk/conf.php
  39. 10 0
      app/i18n/zh-cn/conf.php
  40. 10 0
      app/i18n/zh-tw/conf.php
  41. 17 1
      app/views/configure/display.phtml
  42. 5 0
      app/views/helpers/javascript_vars.phtml
  43. 2 0
      config-user.default.php
  44. 1 1
      docs/en/developers/Minz/index.md
  45. 1 1
      docs/fr/developers/Minz/index.md
  46. 1 1
      docs/i18n/flags/gen/es.svg
  47. 1 1
      docs/i18n/flags/gen/fa.svg
  48. 4 2
      lib/Minz/Request.php
  49. 15 7
      p/scripts/main.js

+ 5 - 1
app/Controllers/apiController.php

@@ -58,7 +58,11 @@ class FreshRSS_api_Controller extends FreshRSS_ActionController {
 		if (is_string($error)) {
 			Minz_Request::bad($error, $return_url);
 		} else {
-			Minz_Request::good(_t('feedback.api.password.updated'), $return_url);
+			Minz_Request::good(
+				_t('feedback.api.password.updated'),
+				$return_url,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 	}
 }

+ 20 - 4
app/Controllers/authController.php

@@ -56,7 +56,11 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 			invalidateHttpCache();
 
 			if ($ok) {
-				Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'auth', 'a' => 'index' ]);
+				Minz_Request::good(
+					_t('feedback.conf.updated'),
+					[ 'c' => 'auth', 'a' => 'index' ],
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.conf.error'), [ 'c' => 'auth', 'a' => 'index' ]);
 			}
@@ -176,7 +180,11 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 				if (empty($url)) {
 					$url = [ 'c' => 'index', 'a' => 'index' ];
 				}
-				Minz_Request::good(_t('feedback.auth.login.success'), $url);
+				Minz_Request::good(
+					_t('feedback.auth.login.success'),
+					$url,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Log::warning("Password mismatch for user={$username}, nonce={$nonce}, c={$challenge}");
 				header('HTTP/1.1 403 Forbidden');
@@ -214,7 +222,11 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 
 				Minz_Translate::init(FreshRSS_Context::userConf()->language);
 
-				Minz_Request::good(_t('feedback.auth.login.success'), ['c' => 'index', 'a' => 'index']);
+				Minz_Request::good(
+					_t('feedback.auth.login.success'),
+					['c' => 'index', 'a' => 'index'],
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Log::warning('Unsafe password mismatch for user ' . $username);
 				Minz_Request::bad(
@@ -263,7 +275,11 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
 			invalidateHttpCache();
 			FreshRSS_Auth::removeAccess();
 			Minz_Session::regenerateID('FreshRSS');
-			Minz_Request::good(_t('feedback.auth.logout.success'), [ 'c' => 'index', 'a' => 'index' ]);
+			Minz_Request::good(
+				_t('feedback.auth.logout.success'),
+				[ 'c' => 'index', 'a' => 'index' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
 			Minz_Error::error(403);
 		}

+ 25 - 5
app/Controllers/categoryController.php

@@ -70,7 +70,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 
 			if ($catDAO->addCategoryObject($cat)) {
 				$url_redirect['a'] = 'index';
-				Minz_Request::good(_t('feedback.sub.category.created', $cat->name()), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.category.created', $cat->name()),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 			}
@@ -156,7 +160,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 
 			$url_redirect = ['c' => 'subscription', 'params' => ['id' => $id, 'type' => 'category']];
 			if (false !== $categoryDAO->updateCategory($id, $values)) {
-				Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.category.updated'),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 			}
@@ -201,7 +209,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->queries = $queries;
 			FreshRSS_Context::userConf()->save();
 
-			Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect);
+			Minz_Request::good(
+				_t('feedback.sub.category.deleted'),
+				$url_redirect,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		Minz_Request::forward($url_redirect, true);
@@ -243,7 +255,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 				}
 				FreshRSS_Context::userConf()->save();
 
-				Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.category.emptied'),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 			}
@@ -284,7 +300,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
 				$this->view->_layout(null);
 			} else {
 				if ($ok) {
-					Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
+					Minz_Request::good(
+						_t('feedback.sub.category.updated'),
+						$url_redirect,
+						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+					);
 				} else {
 					Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
 				}

+ 57 - 12
app/Controllers/configureController.php

@@ -74,14 +74,20 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->bottomline_date = Minz_Request::paramBoolean('bottomline_date');
 			FreshRSS_Context::userConf()->bottomline_link = Minz_Request::paramBoolean('bottomline_link');
 			FreshRSS_Context::userConf()->show_nav_buttons = Minz_Request::paramBoolean('show_nav_buttons');
-			FreshRSS_Context::userConf()->html5_notif_timeout = Minz_Request::paramInt('html5_notif_timeout');
+			FreshRSS_Context::userConf()->html5_notif_timeout = max(0, Minz_Request::paramInt('html5_notif_timeout'));
+			FreshRSS_Context::userConf()->good_notification_timeout = max(0, Minz_Request::paramInt('good_notification_timeout'));
+			FreshRSS_Context::userConf()->bad_notification_timeout = max(1, Minz_Request::paramInt('bad_notification_timeout'));
 			FreshRSS_Context::userConf()->save();
 
 			Minz_Session::_param('language', FreshRSS_Context::userConf()->language);
 			Minz_Translate::reset(FreshRSS_Context::userConf()->language);
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'display' ], 'displayAction');
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'display' ],
+				notificationName: 'displayAction',
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 		}
 
 		$this->view->themes = FreshRSS_Themes::get();
@@ -163,7 +169,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->save();
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'reading' ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'reading' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		$this->view->viewModes = FreshRSS_ViewMode::getAllModes();
@@ -197,7 +207,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 				invalidateHttpCache();
 			}
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'integration' ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'integration' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		FreshRSS_View::prependTitle(_t('conf.sharing.title') . ' · ');
@@ -229,7 +243,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->save();
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.shortcuts_updated'), ['c' => 'configure', 'a' => 'shortcut']);
+			Minz_Request::good(
+				_t('feedback.conf.shortcuts_updated'),
+				['c' => 'configure', 'a' => 'shortcut'],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		FreshRSS_View::prependTitle(_t('conf.shortcut.title') . ' · ');
@@ -277,7 +295,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->save();
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'archiving' ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'archiving' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		$volatile = [
@@ -341,7 +363,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->queries = $queries;
 			FreshRSS_Context::userConf()->save();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'queries' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
 			$this->view->queries = [];
 			foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
@@ -433,7 +459,10 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->queries = $queries;
 			FreshRSS_Context::userConf()->save();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries', 'params' => ['id' => (string)$id] ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'queries', 'params' => ['id' => (string)$id] ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 		}
 
 		FreshRSS_View::prependTitle($query->getName() . ' · ' . _t('conf.query.title') . ' · ');
@@ -458,7 +487,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 		FreshRSS_Context::userConf()->queries = $queries;
 		FreshRSS_Context::userConf()->save();
 
-		Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
+		Minz_Request::good(
+			_t('feedback.conf.updated'),
+			[ 'c' => 'configure', 'a' => 'queries' ],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**
@@ -488,7 +521,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 		FreshRSS_Context::userConf()->queries = $queries;
 		FreshRSS_Context::userConf()->save();
 
-		Minz_Request::good(_t('feedback.conf.query_created', $params['name']), [ 'c' => 'configure', 'a' => 'queries' ]);
+		Minz_Request::good(
+			_t('feedback.conf.query_created', $params['name']),
+			[ 'c' => 'configure', 'a' => 'queries' ],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**
@@ -525,7 +562,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'system' ]);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				[ 'c' => 'configure', 'a' => 'system' ],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 	}
 
@@ -535,7 +576,11 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
 			FreshRSS_Context::userConf()->save();
 			invalidateHttpCache();
 
-			Minz_Request::good(_t('feedback.conf.updated'), ['c' => 'configure', 'a' => 'privacy']);
+			Minz_Request::good(
+				_t('feedback.conf.updated'),
+				['c' => 'configure', 'a' => 'privacy'],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 
 		FreshRSS_View::prependTitle(_t('conf.privacy') . ' · ');

+ 12 - 6
app/Controllers/entryController.php

@@ -196,7 +196,8 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 					'a' => 'index',
 					'params' => $params,
 				],
-				'readAction'
+				notificationName: 'readAction ',
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
 			);
 		}
 	}
@@ -254,7 +255,11 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 		$feedDAO->updateCachedValues();
 
 		invalidateHttpCache();
-		Minz_Request::good(_t('feedback.admin.optimization_complete'), $url_redirect);
+		Minz_Request::good(
+			_t('feedback.admin.optimization_complete'),
+			$url_redirect,
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**
@@ -290,9 +295,10 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
 		$databaseDAO->minorDbMaintenance();
 
 		invalidateHttpCache();
-		Minz_Request::good(_t('feedback.sub.purge_completed', $nb_total), [
-			'c' => 'configure',
-			'a' => 'archiving',
-		]);
+		Minz_Request::good(
+			_t('feedback.sub.purge_completed', $nb_total),
+			['c' => 'configure', 'a' => 'archiving'],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 }

+ 15 - 3
app/Controllers/extensionController.php

@@ -191,7 +191,11 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 				$conf->extensions_enabled = $ext_list;
 				$conf->save();
 
-				Minz_Request::good(_t('feedback.extensions.enable.ok', $ext_name), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.extensions.enable.ok', $ext_name),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Log::warning('Cannot enable extension ' . $ext_name . ': ' . $res);
 				Minz_Request::bad(_t('feedback.extensions.enable.ko', $ext_name, _url('index', 'logs')), $url_redirect);
@@ -253,7 +257,11 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 				$conf->extensions_enabled = $ext_list;
 				$conf->save();
 
-				Minz_Request::good(_t('feedback.extensions.disable.ok', $ext_name), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.extensions.disable.ok', $ext_name),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Log::warning('Cannot disable extension ' . $ext_name . ': ' . $res);
 				Minz_Request::bad(_t('feedback.extensions.disable.ko', $ext_name, _url('index', 'logs')), $url_redirect);
@@ -290,7 +298,11 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
 
 			$res = recursive_unlink($ext->getPath());
 			if ($res) {
-				Minz_Request::good(_t('feedback.extensions.removed', $ext_name), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.extensions.removed', $ext_name),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.extensions.cannot_remove', $ext_name), $url_redirect);
 			}

+ 45 - 15
app/Controllers/feedController.php

@@ -344,7 +344,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			// Entries are in DB, we redirect to feed configuration page.
 			$url_redirect['a'] = 'feed';
 			$url_redirect['params']['id'] = '' . $feed->id();
-			Minz_Request::good(_t('feedback.sub.feed.added', $feed->name()), $url_redirect);
+			Minz_Request::good(
+				_t('feedback.sub.feed.added', $feed->name()),
+				$url_redirect,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
 			// GET request: we must ask confirmation to user before adding feed.
 			FreshRSS_View::prependTitle(_t('sub.feed.title_add') . ' · ');
@@ -365,7 +369,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 				// Already subscribe so we redirect to the feed configuration page.
 				$url_redirect['a'] = 'feed';
 				$url_redirect['params']['id'] = $feed->id();
-				Minz_Request::good(_t('feedback.sub.feed.already_subscribed', $feed->name()), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.feed.already_subscribed', $feed->name()),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			}
 		}
 	}
@@ -400,7 +408,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		if ($n === false) {
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
 		} else {
-			Minz_Request::good(_t('feedback.sub.feed.n_entries_deleted', $n), $url_redirect);
+			Minz_Request::good(
+				_t('feedback.sub.feed.n_entries_deleted', $n),
+				$url_redirect,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 	}
 
@@ -952,13 +964,23 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 			$this->view->_layout(null);
 		} elseif ($feed instanceof FreshRSS_Feed && $id > 0) {
 			// Redirect to the main page with correct notification.
-			Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), [
-				'params' => ['get' => 'f_' . $id]
-			], 'actualizeAction');
+			Minz_Request::good(
+				_t('feedback.sub.feed.actualized', $feed->name()),
+				['params' => ['get' => 'f_' . $id]],
+				notificationName: 'actualizeAction',
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 		} elseif ($nbUpdatedFeeds >= 1) {
-			Minz_Request::good(_t('feedback.sub.feed.n_actualized', $nbUpdatedFeeds), []);
+			Minz_Request::good(
+				_t('feedback.sub.feed.n_actualized', $nbUpdatedFeeds),
+				[],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
-			Minz_Request::good(_t('feedback.sub.feed.no_refresh'), []);
+			Minz_Request::good(
+				_t('feedback.sub.feed.no_refresh'),
+				[],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		}
 		return $nbUpdatedFeeds;
 	}
@@ -1088,7 +1110,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		}
 
 		if (self::deleteFeed($id)) {
-			Minz_Request::good(_t('feedback.sub.feed.deleted'), $redirect_url);
+			Minz_Request::good(
+				_t('feedback.sub.feed.deleted'),
+				$redirect_url,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
 			Minz_Request::bad(_t('feedback.sub.feed.error'), $redirect_url);
 		}
@@ -1117,9 +1143,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 
 		$feed->clearCache();
 
-		Minz_Request::good(_t('feedback.sub.feed.cache_cleared', $feed->name()), [
-			'params' => ['get' => 'f_' . $feed->id()],
-		]);
+		Minz_Request::good(
+			_t('feedback.sub.feed.cache_cleared', $feed->name()),
+			['params' => ['get' => 'f_' . $feed->id()]],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**
@@ -1177,9 +1205,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
 		Minz_ModelPdo::$usesSharedPdo = true;
 
 		//Give feedback to user.
-		Minz_Request::good(_t('feedback.sub.feed.reloaded', $feed->name()), [
-			'params' => ['get' => 'f_' . $feed->id()]
-		]);
+		Minz_Request::good(
+			_t('feedback.sub.feed.reloaded', $feed->name()),
+			['params' => ['get' => 'f_' . $feed->id()]],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**

+ 4 - 1
app/Controllers/importExportController.php

@@ -200,7 +200,10 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
 
 		// And finally, we get import status and redirect to the home page
 		$content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : _t('feedback.import_export.feeds_imported');
-		Minz_Request::good($content_notif);
+		Minz_Request::good(
+			$content_notif,
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**

+ 10 - 2
app/Controllers/subscriptionController.php

@@ -383,7 +383,11 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 					Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
 					return;
 				}
-				Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.feed.updated'),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} elseif ($values['url'] != '' && $feedDAO->updateFeed($id, $values) !== false) {
 				$feed->_categoryId($values['category']);
 				// update url and website values for faviconPrepare
@@ -391,7 +395,11 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
 				$feed->_website($values['website'], false);
 				$feed->faviconPrepare();
 
-				Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.sub.feed.updated'),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				if ($values['url'] == '') {
 					Minz_Log::warning('Invalid feed URL!');

+ 14 - 3
app/Controllers/tagController.php

@@ -126,7 +126,11 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 
 			$url_redirect = ['c' => 'tag', 'a' => 'update', 'params' => ['id' => $id]];
 			if ($ok) {
-				Minz_Request::good(_t('feedback.tag.updated'), $url_redirect);
+				Minz_Request::good(
+					_t('feedback.tag.updated'),
+					$url_redirect,
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Request::bad(_t('feedback.tag.error'), $url_redirect);
 			}
@@ -167,7 +171,11 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 		}
 
 		$tagDAO->addTag(['name' => $name]);
-		Minz_Request::good(_t('feedback.tag.created', $name), $url_redirect);
+		Minz_Request::good(
+			_t('feedback.tag.created', $name),
+			$url_redirect,
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+		);
 	}
 
 	/**
@@ -203,7 +211,10 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
 			$tagDAO->deleteTag($sourceId);
 		}
 
-		Minz_Request::good(_t('feedback.tag.renamed', $sourceName, $targetName), ['c' => 'tag', 'a' => 'index']);
+		Minz_Request::good(
+			_t('feedback.tag.renamed', $sourceName, $targetName),
+			['c' => 'tag', 'a' => 'index'],
+			showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 	}
 
 	public function indexAction(): void {

+ 4 - 1
app/Controllers/updateController.php

@@ -289,7 +289,10 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
 				@unlink(UPDATE_FILENAME);
 				@file_put_contents(join_path(DATA_PATH, self::LASTUPDATEFILE), '');
 				Minz_Log::notice(_t('feedback.update.finished'));
-				Minz_Request::good(_t('feedback.update.finished'));
+				Minz_Request::good(
+					_t('feedback.update.finished'),
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+				);
 			} else {
 				Minz_Log::error(_t('feedback.update.error', is_string($res) ? $res : 'unknown'));
 				Minz_Request::bad(_t('feedback.update.error', is_string($res) ? $res : 'unknown'), [ 'c' => 'update', 'a' => 'index' ]);

+ 31 - 8
app/Controllers/userController.php

@@ -86,9 +86,17 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			if ($ok) {
 				$isSelfUpdate = Minz_User::name() === $username;
 				if ($newPasswordPlain == '' || !$isSelfUpdate) {
-					Minz_Request::good(_t('feedback.user.updated', $username), ['c' => 'user', 'a' => 'manage']);
+					Minz_Request::good(
+						_t('feedback.user.updated', $username),
+						['c' => 'user', 'a' => 'manage'],
+						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+					);
 				} else {
-					Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'index', 'a' => 'index']);
+					Minz_Request::good(
+						_t('feedback.profile.updated'),
+						['c' => 'index', 'a' => 'index'],
+						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+					);
 				}
 			} else {
 				Minz_Request::bad(_t('feedback.user.updated.error', $username), ['c' => 'user', 'a' => 'manage']);
@@ -179,9 +187,17 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 
 			if ($ok) {
 				if (FreshRSS_Context::systemConf()->force_email_validation && $email !== $old_email) {
-					Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'validateEmail']);
+					Minz_Request::good(
+						_t('feedback.profile.updated'),
+						['c' => 'user', 'a' => 'validateEmail'],
+						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+					);
 				} else {
-					Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'profile']);
+					Minz_Request::good(
+						_t('feedback.profile.updated'),
+						['c' => 'user', 'a' => 'profile'],
+						showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+					);
 				}
 			} else {
 				Minz_Request::bad(_t('feedback.profile.error'), ['c' => 'user', 'a' => 'profile']);
@@ -532,7 +548,8 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		if ($user_config->email_validation_token === '') {
 			Minz_Request::good(
 				_t('user.email.validation.feedback.unnecessary'),
-				['c' => 'index', 'a' => 'index']
+				['c' => 'index', 'a' => 'index'],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
 			);
 		}
 
@@ -548,7 +565,8 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 			if ($user_config->save()) {
 				Minz_Request::good(
 					_t('user.email.validation.feedback.ok'),
-					['c' => 'index', 'a' => 'index']
+					['c' => 'index', 'a' => 'index'],
+					showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
 				);
 			} else {
 				Minz_Request::bad(
@@ -594,7 +612,8 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		if ($ok) {
 			Minz_Request::good(
 				_t('user.email.validation.feedback.email_sent'),
-				$redirect_url
+				$redirect_url,
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
 			);
 		} else {
 			Minz_Request::bad(
@@ -709,7 +728,11 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
 		FreshRSS_UserDAO::touch($username);
 
 		if ($ok) {
-			Minz_Request::good(_t('feedback.user.updated', $username), ['c' => 'user', 'a' => 'manage']);
+			Minz_Request::good(
+				_t('feedback.user.updated', $username),
+				['c' => 'user', 'a' => 'manage'],
+				showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0
+			);
 		} else {
 			Minz_Request::bad(
 				_t('feedback.user.updated.error', $username),

+ 2 - 0
app/Models/UserConfiguration.php

@@ -28,6 +28,8 @@ declare(strict_types=1);
  * @property string $feverKey
  * @property bool $hide_read_feeds
  * @property int $html5_notif_timeout
+ * @property int $good_notification_timeout
+ * @property int $bad_notification_timeout
  * @property-read bool $is_admin
  * @property int|null $keep_history_default
  * @property string $language

+ 10 - 0
app/i18n/cs/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/de/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Keine',
 		'small' => 'Klein',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privatsphäre',
 		'retrieve_extension_list' => 'Erweiterungsliste abrufen',

+ 10 - 0
app/i18n/el/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/en-us/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// IGNORE
 		'small' => 'Small',	// IGNORE
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// IGNORE
+			'seconds' => 'seconds (at least 1)',	// IGNORE
+		),
+		'good' => array(
+			'label' => 'Show acknowledgment banner',
+			'seconds' => 'seconds (0 means not shown)',	// IGNORE
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// IGNORE
 		'retrieve_extension_list' => 'Retrieve extension list',	// IGNORE

+ 10 - 0
app/i18n/en/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',
 		'small' => 'Small',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',
 		'retrieve_extension_list' => 'Retrieve extension list',

+ 10 - 0
app/i18n/es/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/fa/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'هیچ',
 		'small' => 'کوچک',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'حریم خصوصی',
 		'retrieve_extension_list' => 'بازیابی لیست افزونه‌ها',

+ 10 - 0
app/i18n/fi/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Tietosuoja',
 		'retrieve_extension_list' => 'Nouda laajennusluettelo',

+ 10 - 0
app/i18n/fr/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Aucun',
 		'small' => 'Petit',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Afficher la bannière d’avertissement',
+			'seconds' => 'secondes (au moins 1)',
+		),
+		'good' => array(
+			'label' => 'Afficher la bannière de confirmation',
+			'seconds' => 'secondes (0 pour désactiver)',
+		),
+	),
 	'privacy' => array(
 		'_' => 'Vie privée',
 		'retrieve_extension_list' => 'Récupération de la liste des extensions',

+ 10 - 0
app/i18n/he/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/hu/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Egyik sem',
 		'small' => 'Kicsi',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Adatvédelem',
 		'retrieve_extension_list' => 'Kiterjesztés lista beszerzése',

+ 10 - 0
app/i18n/id/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Tidak ditampilkan',
 		'small' => 'Kecil',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privasi',
 		'retrieve_extension_list' => 'Ambil daftar ekstensi',

+ 10 - 0
app/i18n/it/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Nessuno',
 		'small' => 'Piccolo',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// IGNORE
 		'retrieve_extension_list' => 'Recupero dell’elenco delle estensioni',

+ 10 - 0
app/i18n/ja/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'プライバシー',
 		'retrieve_extension_list' => '拡張機能リストを取得する',

+ 10 - 0
app/i18n/ko/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/lv/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/nl/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Geen',
 		'small' => 'Klein',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// IGNORE
 		'retrieve_extension_list' => 'Extensielijst ophalen',

+ 10 - 0
app/i18n/oc/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/pl/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Brak',
 		'small' => 'Mały',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Pokaż baner ostrzeżenia',
+			'seconds' => 'sekundy (przynajmniej 1)',
+		),
+		'good' => array(
+			'label' => 'Pokaż baner potwierdzający',
+			'seconds' => 'sekundy (0 oznacza nie pokazuj)',
+		),
+	),
 	'privacy' => array(
 		'_' => 'Prywatność',
 		'retrieve_extension_list' => 'Pobieraj listę rozszerzeń',

+ 10 - 0
app/i18n/pt-br/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/pt-pt/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/ru/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/sk/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 10 - 0
app/i18n/tr/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Yok',
 		'small' => 'Küçük',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Gizlilik',
 		'retrieve_extension_list' => 'Eklenti listesini al',

+ 10 - 0
app/i18n/uk/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'Не показувати',
 		'small' => 'Мала',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Приватність',
 		'retrieve_extension_list' => 'Завантажувати список розширень',

+ 10 - 0
app/i18n/zh-cn/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => '无',
 		'small' => '小',
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => '获取扩展列表',

+ 10 - 0
app/i18n/zh-tw/conf.php

@@ -105,6 +105,16 @@ return array(
 		'none' => 'None',	// TODO
 		'small' => 'Small',	// TODO
 	),
+	'notification_timeout' => array(
+		'bad' => array(
+			'label' => 'Show warning banner',	// TODO
+			'seconds' => 'seconds (at least 1)',	// TODO
+		),
+		'good' => array(
+			'label' => 'Show acknowledgement banner',	// TODO
+			'seconds' => 'seconds (0 means not shown)',	// TODO
+		),
+	),
 	'privacy' => array(
 		'_' => 'Privacy',	// TODO
 		'retrieve_extension_list' => 'Retrieve extension list',	// TODO

+ 17 - 1
app/views/configure/display.phtml

@@ -242,11 +242,27 @@
 		<div class="form-group">
 			<label class="group-name" for="html5_notif_timeout"><?= _t('conf.display.notif_html5.timeout') ?></label>
 			<div class="group-controls">
-				<input type="number" id="html5_notif_timeout" name="html5_notif_timeout" value="<?=
+				<input type="number" min="0" max="60" id="html5_notif_timeout" name="html5_notif_timeout" value="<?=
 					FreshRSS_Context::userConf()->html5_notif_timeout ?>" /> <?= _t('conf.display.notif_html5.seconds') ?>
 			</div>
 		</div>
 
+		<div class="form-group">
+			<label class="group-name" for="good_notification_timeout"><?= _t('conf.notification_timeout.good.label') ?></label>
+			<div class="group-controls">
+				<input type="number" min="0" max="60" id="good_notification_timeout" name="good_notification_timeout" value="<?=
+					FreshRSS_Context::userConf()->good_notification_timeout ?>" /> <?= _t('conf.notification_timeout.good.seconds') ?>
+			</div>
+		</div>
+
+		<div class="form-group">
+			<label class="group-name" for="bad_notification_timeout"><?= _t('conf.notification_timeout.bad.label') ?></label>
+			<div class="group-controls">
+				<input type="number" min="1" max="60" id="bad_notification_timeout" name="bad_notification_timeout" value="<?=
+					FreshRSS_Context::userConf()->bad_notification_timeout ?>" />  <?= _t('conf.notification_timeout.bad.seconds') ?>
+			</div>
+		</div>
+
 		<div class="form-group">
 			<div class="group-controls">
 				<label class="checkbox" for="show_nav_buttons">

+ 5 - 0
app/views/helpers/javascript_vars.phtml

@@ -25,6 +25,11 @@ echo json_encode([
 		'sides_close_article' => !!FreshRSS_Context::userConf()->sides_close_article,
 		'sticky_post' => !!FreshRSS_Context::isStickyPostEnabled(),
 		'html5_notif_timeout' => FreshRSS_Context::userConf()->html5_notif_timeout,
+		'closeNotification' => [
+			'good' => FreshRSS_Context::userConf()->good_notification_timeout * 1000,
+			'bad' => FreshRSS_Context::userConf()->bad_notification_timeout * 1000,
+			'mouseLeave' => 3000,
+		],
 		'auth_type' => FreshRSS_Context::systemConf()->auth_type,
 		'current_view' => Minz_Request::actionName(),
 		'csrf' => FreshRSS_Auth::csrfToken(),

+ 2 - 0
config-user.default.php

@@ -125,6 +125,8 @@ return array (
 	'queries' => array (
 	),
 	'html5_notif_timeout' => 0,
+	'good_notification_timeout' => 3,
+	'bad_notification_timeout' => 8,
 	'show_nav_buttons' => true,
 	# List of enabled FreshRSS extensions.
 	'extensions_enabled' => [],

+ 1 - 1
docs/en/developers/Minz/index.md

@@ -173,7 +173,7 @@ $url_array = [
 $feedback_good = 'All went well!';
 $feedback_bad = 'Oops, something went wrong.';
 
-Minz_Request::good($feedback_good, $url_array);
+Minz_Request::good($feedback_good, $url_array, showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 
 // or
 

+ 1 - 1
docs/fr/developers/Minz/index.md

@@ -229,7 +229,7 @@ $url_array = [
 $feedback_good = 'Tout s’est bien passé !';
 $feedback_bad = 'Oups, quelque chose n’a pas marché.';
 
-Minz_Request::good($feedback_good, $url_array);
+Minz_Request::good($feedback_good, $url_array, showNotification: FreshRSS_Context::userConf()->good_notification_timeout > 0);
 
 // ou
 

+ 1 - 1
docs/i18n/flags/gen/es.svg

@@ -2,6 +2,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="70" height="20">
 	<g fill="white" font-size="12" font-family="Verdana" text-anchor="middle">
 		<rect rx="3" width="70" height="20" fill="green" />
-		<text x="34" y="14">🇪🇸 92%</text>
+		<text x="34" y="14">🇪🇸 91%</text>
 	</g>
 </svg>

+ 1 - 1
docs/i18n/flags/gen/fa.svg

@@ -2,6 +2,6 @@
 <svg xmlns="http://www.w3.org/2000/svg" width="70" height="20">
 	<g fill="white" font-size="12" font-family="Verdana" text-anchor="middle">
 		<rect rx="3" width="70" height="20" fill="green" />
-		<text x="34" y="14">🇮🇷 98%</text>
+		<text x="34" y="14">🇮🇷 97%</text>
 	</g>
 </svg>

+ 4 - 2
lib/Minz/Request.php

@@ -461,8 +461,10 @@ class Minz_Request {
 	 * @param string $msg notification content
 	 * @param array{c?:string,a?:string,params?:array<string,mixed>} $url url array to where we should be forwarded
 	 */
-	public static function good(string $msg, array $url = [], string $notificationName = ''): void {
-		Minz_Request::setGoodNotification($msg, $notificationName);
+	public static function good(string $msg, array $url = [], string $notificationName = '', bool $showNotification = true): void {
+		if ($showNotification) {
+			Minz_Request::setGoodNotification($msg);
+		}
 		Minz_Request::forward($url, true);
 	}
 

+ 15 - 7
p/scripts/main.js

@@ -1814,14 +1814,22 @@ function openNotification(msg, status) {
 	}
 	notification_working = true;
 	notification.querySelector('.msg').innerHTML = msg;
-	notification.className = 'notification';
-	notification.classList.add(status);
+
 	if (status == 'good') {
-		notification_interval = setTimeout(closeNotification, 4000);
+		if (context.closeNotification.good > 0) {
+			notification_interval = setTimeout(closeNotification, context.closeNotification.good);
+		} else {
+			notification.classList.add('closed');
+			notification_working = false;
+		}
 	} else {
 		// no status or f.e. status = 'bad', give some more time to read
-		notification_interval = setTimeout(closeNotification, 8000);
+		if (context.closeNotification.good > 0) {
+			notification_interval = setTimeout(closeNotification, context.closeNotification.bad);
+		}
 	}
+	notification.className = 'notification';
+	notification.classList.add(status);
 }
 
 function closeNotification() {
@@ -1844,16 +1852,16 @@ function init_notifications() {
 	});
 
 	notification.addEventListener('mouseleave', function () {
-		notification_interval = setTimeout(closeNotification, 3000);
+		notification_interval = setTimeout(closeNotification, context.closeNotification.mouseLeave);
 	});
 
 	if (notification.querySelector('.msg').innerHTML.length > 0) {
 		notification_working = true;
 		if (notification.classList.contains('good')) {
-			notification_interval = setTimeout(closeNotification, 4000);
+			notification_interval = setTimeout(closeNotification, context.closeNotification.good);
 		} else {
 			// no status or f.e. status = 'bad', give some more time to read
-			notification_interval = setTimeout(closeNotification, 8000);
+			notification_interval = setTimeout(closeNotification, context.closeNotification.bad);
 		}
 	}
 }