diff --git a/dump.rdb b/dump.rdb index 80eb17f6e2..16da9a5145 100644 Binary files a/dump.rdb and b/dump.rdb differ diff --git a/install/data/defaults.json b/install/data/defaults.json index a5921ab940..329f612938 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -119,6 +119,7 @@ "notificationType_post-queue": "notification", "notificationType_new-post-flag": "notification", "notificationType_new-user-flag": "notification", + "notificationType_faculty-reply": "notification", "topicStaleDays": 60, "maxTopicsPerPage": 20, "maxPostsPerPage": 20, diff --git a/public/language/ar/notifications.json b/public/language/ar/notifications.json index 06ddfa34c7..d2f6e335df 100644 --- a/public/language/ar/notifications.json +++ b/public/language/ar/notifications.json @@ -46,7 +46,6 @@ "user-flagged-user-dual": "%1 and %2 flagged a user profile (%3)", "user-flagged-user-triple": "%1, %2 and %3 flagged a user profile (%4)", "user-flagged-user-multiple": "%1, %2 and %3 others flagged a user profile (%4)", - "admin-posted-to": "[COURSE FACULTY] %1 أضاف ردًا إلى: %2", "user-posted-to": "%1 أضاف ردا إلى: %2", "user-posted-to-dual": "%1 and %2 have posted replies to: %3", "user-posted-to-triple": "%1, %2 and %3 have posted replies to: %4", @@ -62,6 +61,11 @@ "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", "user-started-following-you-multiple": "%1, %2 and %3 others started following you.", + + "faculty-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", + "faculty-posted-to-dual" : "[COURSE FACULTY] %1 and [COURSE FACULTY] %2 have posted replies to: %3", + "faculty-posted-to-multiple" : "[COURSE FACULTY] %1, [COURSE FACULTY] %2 and %3 other faculty members have posted replies to: %4", + "new-register": "%1 sent a registration request.", "new-register-multiple": "There are %1 registration requests awaiting review.", "flag-assigned-to-you": "تم تخصيص العلامة 1% لك", @@ -98,5 +102,7 @@ "notificationType-post-queue": "When a new post is queued", "notificationType-new-post-flag": "When a post is flagged", "notificationType-new-user-flag": "When a user is flagged", - "notificationType-new-reward": "When you earn a new reward" + "notificationType-new-reward": "When you earn a new reward", + "notificationType-faculty-reply": "When a course faculty replies to your post" + } \ No newline at end of file diff --git a/public/language/en-GB/notifications.json b/public/language/en-GB/notifications.json index eb5b4a3382..8683fe74c5 100644 --- a/public/language/en-GB/notifications.json +++ b/public/language/en-GB/notifications.json @@ -50,7 +50,6 @@ "user-flagged-user-dual": "%1 and %2 flagged a user profile (%3)", "user-flagged-user-triple": "%1, %2 and %3 flagged a user profile (%4)", "user-flagged-user-multiple": "%1, %2 and %3 others flagged a user profile (%4)", - "admin-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", "user-posted-to" : "%1 has posted a reply to: %2", "user-posted-to-dual" : "%1 and %2 have posted replies to: %3", "user-posted-to-triple" : "%1, %2 and %3 have posted replies to: %4", @@ -58,6 +57,11 @@ "user-posted-topic": "%1 has posted a new topic: %2", "user-edited-post" : "%1 has edited a post in %2", + "faculty-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", + "faculty-posted-to-dual" : "[COURSE FACULTY] %1 and [COURSE FACULTY] %2 have posted replies to: %3", + "faculty-posted-to-multiple" : "[COURSE FACULTY] %1, [COURSE FACULTY] %2 and %3 other faculty members have posted replies to: %4", + + "user-posted-topic-with-tag": "%1 has posted a new topic with tag %2", "user-posted-topic-with-tag-dual": "%1 has posted a new topic with tags %2 and %3", "user-posted-topic-with-tag-triple": "%1 has posted a new topic with tags %2, %3 and %4", @@ -90,6 +94,7 @@ "notification-only": "Notification Only", "email-only": "Email Only", "notification-and-email": "Notification & Email", + "notificationType-upvote": "When someone upvotes your post", "notificationType-new-topic": "When someone you follow posts a topic", "notificationType-new-topic-with-tag": "When a topic is posted with a tag you follow", @@ -107,5 +112,7 @@ "notificationType-post-queue": "When a new post is queued", "notificationType-new-post-flag": "When a post is flagged", "notificationType-new-user-flag": "When a user is flagged", - "notificationType-new-reward": "When you earn a new reward" + "notificationType-new-reward": "When you earn a new reward", + "notificationType-faculty-reply": "When a course faculty replies to your post" + } diff --git a/public/language/en-US/notifications.json b/public/language/en-US/notifications.json index c6d470da84..155d37153c 100644 --- a/public/language/en-US/notifications.json +++ b/public/language/en-US/notifications.json @@ -46,7 +46,6 @@ "user-flagged-user-dual": "%1 and %2 flagged a user profile (%3)", "user-flagged-user-triple": "%1, %2 and %3 flagged a user profile (%4)", "user-flagged-user-multiple": "%1, %2 and %3 others flagged a user profile (%4)", - "admin-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", "user-posted-to": "%1 has posted a reply to: %2", "user-posted-to-dual": "%1 and %2 have posted replies to: %3", "user-posted-to-triple": "%1, %2 and %3 have posted replies to: %4", @@ -62,6 +61,12 @@ "user-started-following-you-dual": "%1 and %2 started following you.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", "user-started-following-you-multiple": "%1, %2 and %3 others started following you.", + + "faculty-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", + "faculty-posted-to-dual" : "[COURSE FACULTY] %1 and [COURSE FACULTY] %2 have posted replies to: %3", + "faculty-posted-to-multiple" : "[COURSE FACULTY] %1, [COURSE FACULTY] %2 and %3 other faculty members have posted replies to: %4", + "notificationType-faculty-reply": "When a course faculty replies to your post", + "new-register": "%1 sent a registration request.", "new-register-multiple": "There are %1 registration requests awaiting review.", "flag-assigned-to-you": "Flag %1 has been assigned to you", diff --git a/public/language/es/notifications.json b/public/language/es/notifications.json index cfb6dba8de..966f93ac24 100644 --- a/public/language/es/notifications.json +++ b/public/language/es/notifications.json @@ -46,7 +46,6 @@ "user-flagged-user-dual": "%1 y %2 reportaron el perfil (%3)", "user-flagged-user-triple": "%1, %2 and %3 flagged a user profile (%4)", "user-flagged-user-multiple": "%1, %2 and %3 others flagged a user profile (%4)", - "admin-posted-to" : "[COURSE FACULTY] %1 ha respondido a: %2", "user-posted-to": "%1 ha respondido a: %2", "user-posted-to-dual": "%1 y %2 han respondido a %3", "user-posted-to-triple": "%1, %2 and %3 have posted replies to: %4", @@ -62,6 +61,12 @@ "user-started-following-you-dual": "%1 y %2 comenzaron a seguirte.", "user-started-following-you-triple": "%1, %2 and %3 started following you.", "user-started-following-you-multiple": "%1, %2 and %3 others started following you.", + + "faculty-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", + "faculty-posted-to-dual" : "[COURSE FACULTY] %1 and [COURSE FACULTY] %2 have posted replies to: %3", + "faculty-posted-to-multiple" : "[COURSE FACULTY] %1, [COURSE FACULTY] %2 and %3 other faculty members have posted replies to: %4", + "notificationType-faculty-reply": "When a course faculty replies to your post", + "new-register": "%1 envió una solicitud de registro.", "new-register-multiple": "Hay %1 peticiones de registros pendientes de revisión", "flag-assigned-to-you": "Reporte %1 te ha sido asignado.", diff --git a/public/language/fr/notifications.json b/public/language/fr/notifications.json index f136229a75..93623f96ca 100644 --- a/public/language/fr/notifications.json +++ b/public/language/fr/notifications.json @@ -46,7 +46,6 @@ "user-flagged-user-dual": "%1 et %2 ont signalé un profil utilisateur (%3)", "user-flagged-user-triple": "%1, %2 et %3 ont signalé un profil utilisateur (%4)", "user-flagged-user-multiple": "%1, %2 et %3 autres ont signalé un profil utilisateur (%4)", - "admin-posted-to": "[COURSE FACULTY] %1 a répondu à : %2", "user-posted-to": "%1 a répondu à : %2", "user-posted-to-dual": "%1 et %2 ont posté une réponse à : %3", "user-posted-to-triple": "%1, %2 et %3 ont publié des réponses à : %4", @@ -62,6 +61,12 @@ "user-started-following-you-dual": "%1 et %2 se sont abonnés à votre compte.", "user-started-following-you-triple": "%1, %2 et %3 ont commencé à vous suivre.", "user-started-following-you-multiple": "%1, %2 et %3 autres ont commencé à vous suivre.", + + "faculty-posted-to" : "[COURSE FACULTY] %1 has posted a reply to: %2", + "faculty-posted-to-dual" : "[COURSE FACULTY] %1 and [COURSE FACULTY] %2 have posted replies to: %3", + "faculty-posted-to-multiple" : "[COURSE FACULTY] %1, [COURSE FACULTY] %2 and %3 other faculty members have posted replies to: %4", + "notificationType-faculty-reply": "When a course faculty replies to your post", + "new-register": "%1 a envoyé une demande d'incription.", "new-register-multiple": "%1 inscription(s) est en attente de validation.", "flag-assigned-to-you": "Signalement %1 vous a été assigné", diff --git a/public/openapi/components/schemas/SettingsObj.yaml b/public/openapi/components/schemas/SettingsObj.yaml index 2ccc8e161c..97337ebf9e 100644 --- a/public/openapi/components/schemas/SettingsObj.yaml +++ b/public/openapi/components/schemas/SettingsObj.yaml @@ -1,6 +1,11 @@ Settings: type: object properties: + # Adding faculty reply to the api schema + notificationType_faculty-reply: + type: string + description: Notification type for when a course faculty replies to your post + enum: [none, notification, email, notificationemail] showemail: type: boolean description: Show user email in profile page @@ -161,4 +166,5 @@ Settings: - notificationType_new-user-flag - categoryWatchState - notificationType_group-request-membership - - uid \ No newline at end of file + - uid + - notificationType_faculty-reply \ No newline at end of file diff --git a/src/notifications.js b/src/notifications.js index fdb9998248..18db094a2a 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -42,6 +42,8 @@ Notifications.baseTypes = [ 'notificationType_group-leave', 'notificationType_group-request-membership', 'notificationType_new-reward', + // Adding the new notification type + 'notificationType_faculty-reply', ]; Notifications.privilegedTypes = [ @@ -155,6 +157,10 @@ Notifications.create = async function (data) { if (!result.data) { return null; } + // Handle faculty reply notification + if (result.data.type === 'faculty-reply') { + result.data.importance = result.data.importance || 6; // Setting a higher importance for faculty replies + } await Promise.all([ db.sortedSetAdd('notifications', now, data.nid), db.setObject(`notifications:${data.nid}`, data), @@ -241,7 +247,7 @@ async function pushToUids(uids, notification) { results = await getUidsBySettings(data.uids); } await sendNotification(results.uidsToNotify); - const delayNotificationTypes = ['new-chat', 'new-group-chat', 'new-public-chat']; + const delayNotificationTypes = ['new-chat', 'new-group-chat', 'new-public-chat', 'faculty-reply']; if (delayNotificationTypes.includes(notification.type)) { const cacheKey = `${notification.mergeId}|${results.uidsToEmail.join(',')}`; if (notificationCache.has(cacheKey)) { diff --git a/src/topics/create.js b/src/topics/create.js index f874f8d64f..dbad977afe 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -209,38 +209,34 @@ module.exports = function (Topics) { if (parseInt(uid, 10) || meta.config.allowGuestReplyNotifications) { const { displayname } = postData.user; - // Checks if the user is an admin - const isAdmin = await privileges.users.isAdministrator(uid); + let notificationType = 'new-reply'; let notificationMessage; + let nid; + let mergeId; if (isAdmin) { - // Handles admin/instructor notification separately so it doesn't get bundled with - // notifications of non-admin replies. - // It does this by using a different notification ID and merge ID for admin replies. - notificationMessage = translator.compile('notifications:admin-posted-to', displayname, postData.topic.title); - Topics.notifyFollowers(postData, uid, { - type: 'new-reply', - bodyShort: notificationMessage, - nid: `admin_post:tid:${postData.topic.tid}:pid:${postData.pid}:uid:${uid}`, - mergeId: `notifications:admin-posted-to|${postData.topic.tid}`, - }); + notificationType = 'faculty-reply'; + notificationMessage = translator.compile('notifications:faculty-posted-to', displayname, postData.topic.title); + nid = `faculty_post:tid:${postData.topic.tid}:pid:${postData.pid}:uid:${uid}`; + mergeId = `notifications:faculty-posted-to|${postData.topic.tid}`; } else { - // This code handles non-admin replies, allowing bundling. notificationMessage = translator.compile('notifications:user-posted-to', displayname, postData.topic.title); - - Topics.notifyFollowers(postData, uid, { - type: 'new-reply', - bodyShort: notificationMessage, - nid: `new_post:tid:${postData.topic.tid}:pid:${postData.pid}:uid:${uid}`, - mergeId: `notifications:user-posted-to|${postData.topic.tid}`, - }); + nid = `new_post:tid:${postData.topic.tid}:pid:${postData.pid}:uid:${uid}`; + mergeId = `notifications:user-posted-to|${postData.topic.tid}`; } + await Topics.notifyFollowers(postData, uid, { + type: notificationType, + bodyShort: notificationMessage, + nid: nid, + mergeId: mergeId, + }); + analytics.increment(['posts', `posts:byCid:${data.cid}`]); plugins.hooks.fire('action:topic.reply', { post: _.clone(postData), data: data }); - - return postData; } + + return postData; }; async function onNewPost(postData, data) { diff --git a/src/upgrades/1.7.6/notification_types.js b/src/upgrades/1.7.6/notification_types.js index 42d59cdd38..b90fce87da 100644 --- a/src/upgrades/1.7.6/notification_types.js +++ b/src/upgrades/1.7.6/notification_types.js @@ -13,6 +13,7 @@ module.exports = { notificationType_upvote: config.notificationType_upvote || 'notification', 'notificationType_new-topic': config['notificationType_new-topic'] || 'notification', 'notificationType_new-reply': config['notificationType_new-reply'] || postNotifications, + 'notificationType_faculty-reply': config['notificationType_faculty-reply'] || 'notification', notificationType_follow: config.notificationType_follow || 'notification', 'notificationType_new-chat': config['notificationType_new-chat'] || chatNotifications, 'notificationType_group-invite': config['notificationType_group-invite'] || 'notification', diff --git a/src/upgrades/3.8.4/add_faculty_reply_notification_setting.js b/src/upgrades/3.8.4/add_faculty_reply_notification_setting.js new file mode 100644 index 0000000000..608e40509f --- /dev/null +++ b/src/upgrades/3.8.4/add_faculty_reply_notification_setting.js @@ -0,0 +1,24 @@ +'use strict'; + +const db = require('../../database'); +const batch = require('../../batch'); + +module.exports = { + name: 'Add_faculty_reply_notification_setting_for_all_users', + timestamp: Date.UTC(2024, 10, 8), + method: async function () { + const { progress } = this; + + await batch.processSortedSet('users:joindate', async (uids) => { + await Promise.all(uids.map(async (uid) => { + await db.setObjectField(`user:${uid}:settings`, 'notificationType_faculty-reply', 'notification'); + })); + + if (progress) { + progress.incr(uids.length); + } + }, { + batch: 500, + }); + }, +}; diff --git a/src/user/settings.js b/src/user/settings.js index d85a712ba6..b6a5e19752 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -84,6 +84,9 @@ module.exports = function (User) { settings[notificationType] = getSetting(settings, notificationType, 'notification'); }); + // Load the faculty reply notification setting + settings['notificationType_faculty-reply'] = getSetting(settings, 'notificationType_faculty-reply', 'notification'); + return settings; } @@ -155,6 +158,12 @@ module.exports = function (User) { settings[notificationType] = data[notificationType]; } }); + + // Handling for faculty-reply notification + if (data['notificationType_faculty-reply']) { + settings['notificationType_faculty-reply'] = data['notificationType_faculty-reply']; + } + 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); diff --git a/test/notifications.js b/test/notifications.js index 0fdcc7f117..9f962b9192 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -12,6 +12,7 @@ const topics = require('../src/topics'); const categories = require('../src/categories'); const notifications = require('../src/notifications'); const socketNotifications = require('../src/socket.io/notifications'); +const groups = require('../src/groups'); const sleep = util.promisify(setTimeout); @@ -388,6 +389,119 @@ describe('Notifications', () => { assert(data); }); + // New tests for faculty reply notifications + // Asked help from ChatGPT to generate these tests. + describe('Faculty Reply Notifications', () => { + let adminUid; + let regularUid; + let cid; + let tid; + + before(async () => { + // Create test users + adminUid = await user.create({ username: 'admin' }); + regularUid = await user.create({ username: 'regular' }); + + // Make the admin user a part of the administrators group + await groups.join('administrators', adminUid); + + // Create test category and topic + cid = await categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + }).then(category => category.cid); + + tid = await topics.post({ + uid: regularUid, + cid: cid, + title: 'Test Topic', + content: 'This is a test topic', + }).then(result => result.topicData.tid); + }); + + it('should create a faculty-reply notification when an admin (faculty) replies', async () => { + const postData = await topics.reply({ + uid: adminUid, + tid: tid, + content: 'This is a faculty reply', + }); + + // Wait for notification to be created + await sleep(3000); + + const notifs = await db.getSortedSetRange(`uid:${regularUid}:notifications:unread`, 0, -1); + console.log('Notifications:', notifs); + + const notifData = await db.getObjects(notifs.map(nid => `notifications:${nid}`)); + console.log('Notification Data:', notifData); + + const facultyReplyNotif = notifData.find(n => n.type === 'faculty-reply'); + + if (!facultyReplyNotif) { + console.log('All notification types:', notifData.map(n => n.type)); + } + + assert(facultyReplyNotif, 'Faculty reply notification should exist'); + assert.strictEqual(facultyReplyNotif.bodyShort, `[[notifications:faculty-posted-to, ${postData.user.displayname}, ${postData.topic.title}]]`); + }); + + it('should not create a faculty-reply notification when a regular user replies', async () => { + // Clear existing notifications + await db.delete(`uid:${regularUid}:notifications:unread`); + + const beforeNotifs = await db.getSortedSetRange(`uid:${regularUid}:notifications:unread`, 0, -1); + console.log('Notifications before reply:', beforeNotifs); + + const replyData = await topics.reply({ + uid: regularUid, + tid: tid, + content: 'This is a regular user reply', + }); + console.log('Reply data:', replyData); + + // Wait for potential notification to be created + await sleep(3000); + + const afterNotifs = await db.getSortedSetRange(`uid:${regularUid}:notifications:unread`, 0, -1); + console.log('Notifications after reply:', afterNotifs); + + const notifData = await db.getObjects(afterNotifs.map(nid => `notifications:${nid}`)); + console.log('Notification Data:', notifData); + + // Log all notification types + console.log('All notification types:', notifData.map(n => n.type)); + + // Check that no new notifications were created + assert.strictEqual(afterNotifs.length, 0, 'No new notifications should be created for a regular user reply'); + + // Logging existing notifications + if (notifData.length > 0) { + console.log('Unexpected notifications:', notifData); + } + }); + + it('should allow users to configure faculty-reply notification preferences', async () => { + await user.setSetting(regularUid, 'notificationType_faculty-reply', 'notification'); + + const settings = await user.getSettings(regularUid); + assert.strictEqual(settings['notificationType_faculty-reply'], 'notification', 'Faculty reply notification preference should be set'); + }); + + after(async () => { + // Clean up users, category, and topic + await Promise.all([ + user.delete(adminUid), + user.delete(regularUid), + ].map(p => p.catch(() => { /* ignore errors */ }))); + await db.delete(`category:${cid}`); + await topics.purge(tid); + + // Remove admin from administrators group + await groups.leave('administrators', adminUid); + }); + }); + + it('should send welcome notification', (done) => { meta.config.welcomeNotification = 'welcome to the forums'; user.notifications.sendWelcomeNotification(uid, (err) => {