From 1dae3d222f342d791a4de89ef1a541bcf58a3972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 17 Feb 2026 19:58:10 -0500 Subject: [PATCH] feat: add invitedBy to user info page, closes #13972, closes #13997 --- install/package.json | 4 ++-- public/language/en-GB/user.json | 1 + public/openapi/read/user/userslug/info.yaml | 17 +++++++++++++++ public/src/client/account/info.js | 24 +++++++++++---------- src/controllers/accounts/info.js | 13 ++++++++++- src/controllers/authentication.js | 1 + src/user/invite.js | 10 +++++++++ 7 files changed, 56 insertions(+), 14 deletions(-) diff --git a/install/package.json b/install/package.json index e4fcfcdb62..0280368610 100644 --- a/install/package.json +++ b/install/package.json @@ -107,10 +107,10 @@ "nodebb-plugin-spam-be-gone": "2.3.2", "nodebb-plugin-web-push": "0.7.6", "nodebb-rewards-essentials": "1.0.2", - "nodebb-theme-harmony": "2.2.15", + "nodebb-theme-harmony": "2.2.16", "nodebb-theme-lavender": "7.1.21", "nodebb-theme-peace": "2.2.51", - "nodebb-theme-persona": "14.2.9", + "nodebb-theme-persona": "14.2.10", "nodebb-widget-essentials": "7.0.42", "nodemailer": "8.0.1", "nprogress": "0.2.0", diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 9186c80b74..c3e54eef2a 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -194,6 +194,7 @@ "sso.dissociate-confirm-title": "Confirm Dissociation", "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "info.invited-by": "Invited by", "info.latest-flags": "Latest Flags", "info.profile": "Profile", "info.post": "Post", diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml index 66c3ba0730..2e1f2fbd67 100644 --- a/public/openapi/read/user/userslug/info.yaml +++ b/public/openapi/read/user/userslug/info.yaml @@ -20,6 +20,23 @@ get: - $ref: ../../../components/schemas/UserObject.yaml#/UserObjectFull - type: object properties: + invitedBy: + type: object + nullable: true + properties: + username: + type: string + userslug: + type: string + picture: + type: string + uid: + type: number + icon:text: + type: string + icon:bgColor: + type: string + history: type: object properties: diff --git a/public/src/client/account/info.js b/public/src/client/account/info.js index f044860cd6..5dfc698769 100644 --- a/public/src/client/account/info.js +++ b/public/src/client/account/info.js @@ -11,6 +11,14 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s }; function handleModerationNote() { + const noteList = $('[component="account/moderation-note/list"]'); + + function adjustTextareaHeight(textarea) { + textarea.css({ + height: textarea.prop('scrollHeight') + 'px', + }); + } + $('[component="account/save-moderation-note"]').on('click', function () { const noteEl = $('[component="account/moderation-note"]'); const note = noteEl.val(); @@ -24,23 +32,24 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s noteEl.val(''); app.parseAndTranslate('account/info', 'moderationNotes', { moderationNotes: notes }, function (html) { - $('[component="account/moderation-note/list"]').prepend(html); + noteList.prepend(html); html.find('.timeago').timeago(); }); }); }); - $('[component="account/moderation-note/edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); contentArea.addClass('hidden'); editArea.removeClass('hidden'); + adjustTextareaHeight(editArea.find('textarea')); editArea.find('textarea').trigger('focus').putCursorAtEnd(); }); - $('[component="account/moderation-note/save-edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/save-edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); @@ -63,20 +72,13 @@ define('forum/account/info', ['forum/account/header', 'alerts', 'forum/account/s }); }); - $('[component="account/moderation-note/cancel-edit"]').on('click', function () { + noteList.on('click', '[component="account/moderation-note/cancel-edit"]', function () { const parent = $(this).parents('[data-id]'); const contentArea = parent.find('[component="account/moderation-note/content-area"]'); const editArea = parent.find('[component="account/moderation-note/edit-area"]'); contentArea.removeClass('hidden'); editArea.addClass('hidden'); }); - - $('[component="account/moderation-note/edit-area"] textarea').each((i, el) => { - const $el = $(el); - $el.css({ - height: $el.prop('scrollHeight') + 'px', - }).parent().addClass('hidden'); - }); } return Info; diff --git a/src/controllers/accounts/info.js b/src/controllers/accounts/info.js index 7081acc7df..002010bfd1 100644 --- a/src/controllers/accounts/info.js +++ b/src/controllers/accounts/info.js @@ -15,12 +15,13 @@ infoController.get = async function (req, res) { const payload = res.locals.userData; const { username, userslug } = payload; - const [isPrivileged, history, sessions, usernames, emails] = await Promise.all([ + const [isPrivileged, history, sessions, usernames, emails, invitedBy] = await Promise.all([ user.isPrivileged(req.uid), user.getModerationHistory(res.locals.uid), user.auth.getSessions(res.locals.uid, req.sessionID), user.getHistory(`user:${res.locals.uid}:usernames`), user.getHistory(`user:${res.locals.uid}:emails`), + getInvitedBy(res.locals.uid), ]); const notes = await getNotes({ uid: res.locals.uid, isPrivileged }, start, stop); @@ -29,6 +30,7 @@ infoController.get = async function (req, res) { payload.sessions = sessions; payload.usernames = usernames; payload.emails = emails; + payload.invitedBy = invitedBy; if (isPrivileged) { payload.moderationNotes = notes.notes; @@ -51,3 +53,12 @@ async function getNotes({ uid, isPrivileged }, start, stop) { ]); return { notes: notes, count: count }; } + +async function getInvitedBy(uid) { + const invitedBy = await user.getUserField(uid, 'invitedBy'); + if (!invitedBy) { + return null; + } + const inviterData = await user.getUserFields(invitedBy, ['uid', 'username', 'userslug', 'picture']); + return inviterData.userslug ? inviterData : null; +}; diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 3c5b337801..63c551c5af 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -59,6 +59,7 @@ async function registerAndLoginUser(req, res, userData) { await Promise.all([ user.confirmIfInviteEmailIsUsed(userData.token, userData.email, uid), user.joinGroupsFromInvitation(uid, userData.token), + user.setInviterUid(uid, userData.token), ]); } await user.deleteInvitationKey(userData.email, userData.token); diff --git a/src/user/invite.js b/src/user/invite.js index fd308af56c..9699a2a6fe 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -83,6 +83,16 @@ module.exports = function (User) { return await db.getObjectField(`invitation:token:${token}`, 'email'); }; + User.setInviterUid = async function (uid, token) { + if (!token) { + return; + } + const inviterUid = await db.getObjectField(`invitation:token:${token}`, 'inviter'); + if (inviterUid) { + await User.setUserField(uid, 'invitedBy', inviterUid); + } + }; + User.confirmIfInviteEmailIsUsed = async function (token, enteredEmail, uid) { if (!enteredEmail) { return;