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);