diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json
index 61231b11a8..720e615400 100644
--- a/public/language/en-GB/error.json
+++ b/public/language/en-GB/error.json
@@ -37,6 +37,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
+ "invalid-unread-cutoff": "Invalid unread cutoff value, must be at least 1 and at most %1",
"username-taken": "Username taken",
"email-taken": "Email address is already taken.",
diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json
index 3e0fab1e63..9186c80b74 100644
--- a/public/language/en-GB/user.json
+++ b/public/language/en-GB/user.json
@@ -111,7 +111,7 @@
"show-email": "Show My Email",
"show-fullname": "Show My Full Name",
"restrict-chats": "Only allow chat messages from users I follow",
- "disable-incoming-chats": "Disable incoming chat messages ",
+ "disable-incoming-chats": "Disable incoming chat messages ",
"chat-allow-list": "Allow chat messages from the following users",
"chat-deny-list": "Deny chat messages from the following users",
"chat-list-add-user": "Add user",
@@ -141,8 +141,8 @@
"hidden": "hidden",
"paginate-description" : "Paginate topics and posts instead of using infinite scroll",
- "topics-per-page": "Topics per Page",
- "posts-per-page": "Posts per Page",
+ "topics-per-page": "Topics per page",
+ "posts-per-page": "Posts per page",
"category-topic-sort": "Category topic sort",
"topic-post-sort": "Topic post sort",
"max-items-per-page": "Maximum %1",
@@ -157,6 +157,8 @@
"upvote-notif-freq.disabled": "Disabled",
"browsing": "Browsing Settings",
+ "unread.cutoff": "Unread cutoff (Maximum %1 days)",
+ "unread.cutoff-help": "Topics will be marked read if they have not been updated within this number of days.",
"open-links-in-new-tab": "Open outgoing links in new tab",
"enable-topic-searching": "Enable In-Topic Searching",
diff --git a/public/openapi/components/schemas/SettingsObj.yaml b/public/openapi/components/schemas/SettingsObj.yaml
index 779d2e2fb4..7ef95a0b19 100644
--- a/public/openapi/components/schemas/SettingsObj.yaml
+++ b/public/openapi/components/schemas/SettingsObj.yaml
@@ -4,6 +4,9 @@ Settings:
showemail:
type: boolean
description: Show user email in profile page
+ unreadCutoff:
+ type: number
+ description: Number of days after which a topic is no longer considered unread
usePagination:
type: boolean
description: Toggles between pagination (when enabled), or infinite scrolling (when disabled)
diff --git a/public/openapi/read/user/userslug/settings.yaml b/public/openapi/read/user/userslug/settings.yaml
index bc6f09a36a..2722958aaa 100644
--- a/public/openapi/read/user/userslug/settings.yaml
+++ b/public/openapi/read/user/userslug/settings.yaml
@@ -133,6 +133,8 @@ get:
type: number
maxPostsPerPage:
type: number
+ maxUnreadCutoff:
+ type: number
title:
type: string
- $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs
diff --git a/public/src/client/account/settings.js b/public/src/client/account/settings.js
index 2e220fb080..9a519f71c8 100644
--- a/public/src/client/account/settings.js
+++ b/public/src/client/account/settings.js
@@ -20,6 +20,8 @@ define('forum/account/settings', [
savedSkin = $('#bootswatchSkin').length && $('#bootswatchSkin').val();
header.init();
+ $('[data-bs-toggle]').tooltip();
+
$('#submitBtn').on('click', function () {
const settings = loadSettings();
diff --git a/src/api/users.js b/src/api/users.js
index 7a311bbb12..369a692cc0 100644
--- a/src/api/users.js
+++ b/src/api/users.js
@@ -132,6 +132,7 @@ usersAPI.updateSettings = async function (caller, data) {
let defaults = await user.getSettings(0);
defaults = {
+ unreadCutoff: defaults.unreadCutoff,
postsPerPage: defaults.postsPerPage,
topicsPerPage: defaults.topicsPerPage,
userLang: defaults.userLang,
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index cc88056409..dbc453ec2c 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -96,6 +96,7 @@ settingsController.get = async function (req, res, next) {
userData.maxTopicsPerPage = meta.config.maxTopicsPerPage;
userData.maxPostsPerPage = meta.config.maxPostsPerPage;
+ userData.maxUnreadCutoff = Math.max(meta.config.unreadCutoff, 14);
userData.title = '[[pages:account/settings]]';
userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: `/user/${userData.userslug}` }, { text: '[[user:settings]]' }]);
diff --git a/src/topics/unread.js b/src/topics/unread.js
index afdb3fc155..fd487850c0 100644
--- a/src/topics/unread.js
+++ b/src/topics/unread.js
@@ -11,7 +11,6 @@ const posts = require('../posts');
const notifications = require('../notifications');
const categories = require('../categories');
const privileges = require('../privileges');
-const meta = require('../meta');
const utils = require('../utils');
const plugins = require('../plugins');
@@ -48,8 +47,9 @@ module.exports = function (Topics) {
};
Topics.unreadCutoff = async function (uid) {
- const cutoff = Date.now() - (meta.config.unreadCutoff * 86400000);
- const data = await plugins.hooks.fire('filter:topics.unreadCutoff', { uid: uid, cutoff: cutoff });
+ const { unreadCutoff } = await user.getSettings(uid);
+ const cutoff = Date.now() - (unreadCutoff * 86400000);
+ const data = await plugins.hooks.fire('filter:topics.unreadCutoff', { uid, cutoff });
return parseInt(data.cutoff, 10);
};
diff --git a/src/user/settings.js b/src/user/settings.js
index 3d37d3fa28..c3c2213985 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -48,7 +48,7 @@ module.exports = function (User) {
};
async function onSettingsLoaded(uid, settings) {
- const data = await plugins.hooks.fire('filter:user.getSettings', { uid: uid, settings: settings });
+ const data = await plugins.hooks.fire('filter:user.getSettings', { uid, settings });
settings = data.settings;
const defaultTopicsPerPage = meta.config.topicsPerPage;
@@ -59,6 +59,12 @@ module.exports = function (User) {
settings.openOutgoingLinksInNewTab = parseInt(getSetting(settings, 'openOutgoingLinksInNewTab', 0), 10) === 1;
settings.dailyDigestFreq = getSetting(settings, 'dailyDigestFreq', 'off');
settings.usePagination = parseInt(getSetting(settings, 'usePagination', 0), 10) === 1;
+ settings.unreadCutoff = Math.max(1, Math.min(
+ Math.max(meta.config.unreadCutoff, 14),
+ settings.unreadCutoff ?
+ parseInt(settings.unreadCutoff, 10) :
+ meta.config.unreadCutoff,
+ ));
settings.topicsPerPage = Math.max(1, Math.min(
meta.config.maxTopicsPerPage,
settings.topicsPerPage ?
@@ -116,6 +122,60 @@ module.exports = function (User) {
}
User.saveSettings = async function (uid, data) {
+ await validateSettings(data);
+
+ data.userLang = data.userLang || meta.config.defaultLang;
+
+ plugins.hooks.fire('action:user.saveSettings', { uid: uid, settings: data });
+
+ const settings = {
+ showemail: data.showemail,
+ showfullname: data.showfullname,
+ unreadCutoff: data.unreadCutoff,
+ openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab,
+ dailyDigestFreq: data.dailyDigestFreq || 'off',
+ usePagination: data.usePagination,
+ topicsPerPage: data.topicsPerPage,
+ postsPerPage: data.postsPerPage,
+ userLang: data.userLang,
+ acpLang: data.acpLang || meta.config.defaultLang,
+ followTopicsOnCreate: data.followTopicsOnCreate,
+ followTopicsOnReply: data.followTopicsOnReply,
+ disableIncomingChats: data.disableIncomingChats,
+ topicSearchEnabled: data.topicSearchEnabled,
+ updateUrlWithPostIndex: data.updateUrlWithPostIndex,
+ homePageRoute: ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''),
+ scrollToMyPost: data.scrollToMyPost,
+ upvoteNotifFreq: data.upvoteNotifFreq,
+ bootswatchSkin: data.bootswatchSkin,
+ categoryWatchState: data.categoryWatchState,
+ categoryTopicSort: data.categoryTopicSort,
+ topicPostSort: data.topicPostSort,
+ chatAllowList: data.chatAllowList,
+ chatDenyList: data.chatDenyList,
+ };
+ const notificationTypes = await notifications.getAllNotificationTypes();
+ notificationTypes.forEach((notificationType) => {
+ if (data[notificationType]) {
+ settings[notificationType] = data[notificationType];
+ }
+ });
+ const result = await plugins.hooks.fire('filter:user.saveSettings', { uid, settings, data });
+ await db.setObject(`user:${uid}:settings`, result.settings);
+ await User.updateDigestSetting(uid, data.dailyDigestFreq);
+ return await User.getSettings(uid);
+ };
+
+ async function validateSettings(data) {
+ const maxUnreadCutoff = Math.max(meta.config.unreadCutoff, 14);
+ if (
+ !data.unreadCutoff ||
+ parseInt(data.unreadCutoff, 10) <= 0 ||
+ parseInt(data.unreadCutoff, 10) > maxUnreadCutoff
+ ) {
+ throw new Error(`[[error:invalid-unread-cutoff, ${maxUnreadCutoff}]]`);
+ }
+
const maxPostsPerPage = meta.config.maxPostsPerPage || 20;
if (
!data.postsPerPage ||
@@ -141,46 +201,7 @@ module.exports = function (User) {
if (data.acpLang && !languageCodes.includes(data.acpLang)) {
throw new Error('[[error:invalid-language]]');
}
- data.userLang = data.userLang || meta.config.defaultLang;
-
- plugins.hooks.fire('action:user.saveSettings', { uid: uid, settings: data });
-
- const settings = {
- showemail: data.showemail,
- showfullname: data.showfullname,
- openOutgoingLinksInNewTab: data.openOutgoingLinksInNewTab,
- dailyDigestFreq: data.dailyDigestFreq || 'off',
- usePagination: data.usePagination,
- topicsPerPage: Math.min(data.topicsPerPage, parseInt(maxTopicsPerPage, 10) || 20),
- postsPerPage: Math.min(data.postsPerPage, parseInt(maxPostsPerPage, 10) || 20),
- userLang: data.userLang || meta.config.defaultLang,
- acpLang: data.acpLang || meta.config.defaultLang,
- followTopicsOnCreate: data.followTopicsOnCreate,
- followTopicsOnReply: data.followTopicsOnReply,
- disableIncomingChats: data.disableIncomingChats,
- topicSearchEnabled: data.topicSearchEnabled,
- updateUrlWithPostIndex: data.updateUrlWithPostIndex,
- homePageRoute: ((data.homePageRoute === 'custom' ? data.homePageCustom : data.homePageRoute) || '').replace(/^\//, ''),
- scrollToMyPost: data.scrollToMyPost,
- upvoteNotifFreq: data.upvoteNotifFreq,
- bootswatchSkin: data.bootswatchSkin,
- categoryWatchState: data.categoryWatchState,
- categoryTopicSort: data.categoryTopicSort,
- topicPostSort: data.topicPostSort,
- chatAllowList: data.chatAllowList,
- chatDenyList: data.chatDenyList,
- };
- const notificationTypes = await notifications.getAllNotificationTypes();
- notificationTypes.forEach((notificationType) => {
- if (data[notificationType]) {
- settings[notificationType] = data[notificationType];
- }
- });
- const result = await plugins.hooks.fire('filter:user.saveSettings', { uid: uid, settings: settings, data: data });
- await db.setObject(`user:${uid}:settings`, result.settings);
- await User.updateDigestSetting(uid, data.dailyDigestFreq);
- return await User.getSettings(uid);
- };
+ }
User.updateDigestSetting = async function (uid, dailyDigestFreq) {
await db.sortedSetsRemove(['digest:day:uids', 'digest:week:uids', 'digest:month:uids'], uid);