From 6c0964d8efc3112881b8cf40634e357da533224f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Fri, 13 Jun 2025 19:04:59 -0300 Subject: [PATCH 1/6] DEV: Add compatibility with the Glimmer Post Stream Refactored the user notes plugin by splitting logic into multiple files, introducing value transformers, and adopting Glimmer components. Added a new setting `user_notes_icon_placement` to control icon placement (options: `name` or `avatar`). [skip ci] --- .../components/post-metadata-user-notes.gjs | 9 + .../initializers/enable-user-notes.gjs | 166 ++++++++++++++++++ .../initializers/enable-user-notes.js | 114 ------------ .../pre-initializers/transformers.js | 11 ++ config/settings.yml | 7 + 5 files changed, 193 insertions(+), 114 deletions(-) create mode 100644 assets/javascripts/discourse/components/post-metadata-user-notes.gjs create mode 100644 assets/javascripts/discourse/initializers/enable-user-notes.gjs delete mode 100644 assets/javascripts/discourse/initializers/enable-user-notes.js create mode 100644 assets/javascripts/discourse/pre-initializers/transformers.js diff --git a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs new file mode 100644 index 0000000..da22b1a --- /dev/null +++ b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs @@ -0,0 +1,9 @@ +import Component from "@glimmer/component"; +import { service } from "@ember/service"; +import icon from "discourse/helpers/d-icon"; + +export default class PostMetadataUserNotes extends Component { + @service siteSettings; + + +} diff --git a/assets/javascripts/discourse/initializers/enable-user-notes.gjs b/assets/javascripts/discourse/initializers/enable-user-notes.gjs new file mode 100644 index 0000000..3a5b7f6 --- /dev/null +++ b/assets/javascripts/discourse/initializers/enable-user-notes.gjs @@ -0,0 +1,166 @@ +import Component from "@glimmer/component"; +import { withSilencedDeprecations } from "discourse/lib/deprecated"; +import { iconNode } from "discourse/lib/icon-library"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { applyValueTransformer } from "discourse/lib/transformer"; +import PostMetadataUserNotes from "../components/post-metadata-user-notes"; +import { showUserNotes } from "../lib/user-notes"; + +export default { + name: "enable-user-notes", + initialize(container) { + const siteSettings = container.lookup("service:site-settings"); + const currentUser = container.lookup("service:current-user"); + + if (!siteSettings.user_notes_enabled || !currentUser?.staff) { + return; + } + + withPluginApi((api) => { + customizePost(api, container); + customizePostMenu(api, container); + }); + }, +}; + +function customizePost(api, container) { + const siteSettings = container.lookup("service:site-settings"); + + const iconPlacement = applyValueTransformer( + "user-notes-icon-placement", + siteSettings.user_notes_icon_placement + ); + + if (iconPlacement === "name") { + api.renderBeforeWrapperOutlet( + "post-meta-data-poster-name", + class extends Component { + static shouldRender(args, context) { + return context.site.mobileView; + } + + + } + ); + + api.renderAfterWrapperOutlet( + "post-meta-data-poster-name", + class extends Component { + static shouldRender(args, context) { + return !context.site.mobileView; + } + + + } + ); + } else if (iconPlacement === "avatar") { + api.renderAfterWrapperOutlet("poster-avatar", PostMetadataUserNotes); + } + + withSilencedDeprecations("discourse.post-stream-widget-overrides", () => + customizeWidgetPost(api) + ); +} + +function customizeWidgetPost(api) { + function widgetShowUserNotes() { + showUserNotes( + this.store, + this.attrs.user_id, + (count) => { + this.sendWidgetAction("refreshUserNotes", count); + }, + { + postId: this.attrs.id, + } + ); + } + + api.attachWidgetAction("post", "refreshUserNotes", function (count) { + const cfs = this.model.user_custom_fields || {}; + cfs.user_notes_count = count; + this.model.set("user_custom_fields", cfs); + }); + + const mobileView = api.container.lookup("service:site").mobileView; + const loc = mobileView ? "before" : "after"; + + api.decorateWidget(`poster-name:${loc}`, (dec) => { + if (dec.widget.settings.hideNotes) { + return; + } + + const post = dec.getModel(); + if (!post) { + return; + } + + const ucf = post.user_custom_fields || {}; + if (ucf.user_notes_count > 0) { + return dec.attach("user-notes-icon"); + } + }); + + api.decorateWidget(`post-avatar:after`, (dec) => { + if (!dec.widget.settings.showNotes) { + return; + } + + const post = dec.getModel(); + if (!post) { + return; + } + + const ucf = post.user_custom_fields || {}; + if (ucf.user_notes_count > 0) { + return dec.attach("user-notes-icon"); + } + }); + + api.attachWidgetAction("post", "showUserNotes", widgetShowUserNotes); + + api.createWidget("user-notes-icon", { + services: ["site-settings"], + + tagName: "span.user-notes-icon", + click: widgetShowUserNotes, + + html() { + if (this.siteSettings.enable_emoji) { + return this.attach("emoji", { name: "memo" }); + } else { + return iconNode("pen-to-square"); + } + }, + }); +} + +function customizePostMenu(api, container) { + const appEvents = container.lookup("service:app-events"); + const store = container.lookup("service:store"); + + api.addPostAdminMenuButton((attrs) => { + return { + icon: "pen-to-square", + label: "user_notes.attach", + action: (post) => { + showUserNotes( + store, + attrs.user_id, + (count) => { + const ucf = post.user_custom_fields || {}; + ucf.user_notes_count = count; + post.set("user_custom_fields", ucf); + + appEvents.trigger("post-stream:refresh", { + id: post.id, + }); + }, + { postId: attrs.id } + ); + }, + secondaryAction: "closeAdminMenu", + className: "add-user-note", + }; + }); +} diff --git a/assets/javascripts/discourse/initializers/enable-user-notes.js b/assets/javascripts/discourse/initializers/enable-user-notes.js deleted file mode 100644 index b23ad64..0000000 --- a/assets/javascripts/discourse/initializers/enable-user-notes.js +++ /dev/null @@ -1,114 +0,0 @@ -import { iconNode } from "discourse/lib/icon-library"; -import { withPluginApi } from "discourse/lib/plugin-api"; -import { showUserNotes } from "../lib/user-notes"; - -export default { - name: "enable-user-notes", - initialize(container) { - const siteSettings = container.lookup("service:site-settings"); - const currentUser = container.lookup("service:current-user"); - const appEvents = container.lookup("service:app-events"); - - if (!siteSettings.user_notes_enabled || !currentUser?.staff) { - return; - } - - const store = container.lookup("service:store"); - - withPluginApi("0.8.15", (api) => { - function widgetShowUserNotes() { - showUserNotes( - this.store, - this.attrs.user_id, - (count) => { - this.sendWidgetAction("refreshUserNotes", count); - }, - { - postId: this.attrs.id, - } - ); - } - - api.attachWidgetAction("post", "refreshUserNotes", function (count) { - const cfs = this.model.user_custom_fields || {}; - cfs.user_notes_count = count; - this.model.set("user_custom_fields", cfs); - }); - - const mobileView = api.container.lookup("service:site").mobileView; - const loc = mobileView ? "before" : "after"; - api.decorateWidget(`poster-name:${loc}`, (dec) => { - if (dec.widget.settings.hideNotes) { - return; - } - - const post = dec.getModel(); - if (!post) { - return; - } - - const ucf = post.user_custom_fields || {}; - if (ucf.user_notes_count > 0) { - return dec.attach("user-notes-icon"); - } - }); - - api.decorateWidget(`post-avatar:after`, (dec) => { - if (!dec.widget.settings.showNotes) { - return; - } - - const post = dec.getModel(); - if (!post) { - return; - } - - const ucf = post.user_custom_fields || {}; - if (ucf.user_notes_count > 0) { - return dec.attach("user-notes-icon"); - } - }); - api.addPostAdminMenuButton((attrs) => { - return { - icon: "pen-to-square", - label: "user_notes.attach", - action: (post) => { - showUserNotes( - store, - attrs.user_id, - (count) => { - const ucf = post.user_custom_fields || {}; - ucf.user_notes_count = count; - post.set("user_custom_fields", ucf); - - appEvents.trigger("post-stream:refresh", { - id: post.id, - }); - }, - { postId: attrs.id } - ); - }, - secondaryAction: "closeAdminMenu", - className: "add-user-note", - }; - }); - - api.attachWidgetAction("post", "showUserNotes", widgetShowUserNotes); - - api.createWidget("user-notes-icon", { - services: ["site-settings"], - - tagName: "span.user-notes-icon", - click: widgetShowUserNotes, - - html() { - if (this.siteSettings.enable_emoji) { - return this.attach("emoji", { name: "memo" }); - } else { - return iconNode("pen-to-square"); - } - }, - }); - }); - }, -}; diff --git a/assets/javascripts/discourse/pre-initializers/transformers.js b/assets/javascripts/discourse/pre-initializers/transformers.js new file mode 100644 index 0000000..6117a6f --- /dev/null +++ b/assets/javascripts/discourse/pre-initializers/transformers.js @@ -0,0 +1,11 @@ +import { withPluginApi } from "discourse/lib/plugin-api"; + +export default { + before: "freeze-valid-transformers", + + initialize() { + withPluginApi("1.33.0", (api) => { + api.addValueTransformerName("user-notes-icon-placement"); + }); + }, +}; diff --git a/config/settings.yml b/config/settings.yml index 5f6a42a..d2beb64 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -2,6 +2,13 @@ plugins: user_notes_enabled: default: false client: true + user_notes_icon_placement: + client: true + type: enum + default: "name" + choices: + - name + - avatar user_notes_moderators_delete: default: true client: false From 7fc2f9cfe203864c2bff3a470182fd1cf86e7367 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Tue, 24 Jun 2025 21:11:57 -0300 Subject: [PATCH 2/6] DEV: Rename transformers.js to user-notes-transformers.js --- .../{transformers.js => user-notes-transformers.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename assets/javascripts/discourse/pre-initializers/{transformers.js => user-notes-transformers.js} (100%) diff --git a/assets/javascripts/discourse/pre-initializers/transformers.js b/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js similarity index 100% rename from assets/javascripts/discourse/pre-initializers/transformers.js rename to assets/javascripts/discourse/pre-initializers/user-notes-transformers.js From f1c00292c4819747723d732338d808345c137428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Tue, 24 Jun 2025 21:53:49 -0300 Subject: [PATCH 3/6] DEV: Refactor user notes component and initialization logic - Added functionality to display user notes count based on `user_custom_fields`. - Updated `PostMetadataUserNotes` component to dynamically toggle between emoji or icon based on site settings. - Refactored logic for rendering the user notes icon in different UI contexts (mobile and desktop). - Removed hard-coded version constraint for `withPluginApi` --- .../components/post-metadata-user-notes.gjs | 31 ++++++++++++++++++- .../initializers/enable-user-notes.gjs | 21 +++++++++++-- .../user-notes-transformers.js | 2 +- config/locales/server.en.yml | 1 + 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs index da22b1a..3687cff 100644 --- a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs +++ b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs @@ -1,9 +1,38 @@ import Component from "@glimmer/component"; +import { on } from "@ember/modifier"; +import { action } from "@ember/object"; import { service } from "@ember/service"; import icon from "discourse/helpers/d-icon"; +import emoji from "discourse/helpers/emoji"; +import { showUserNotes } from "../lib/user-notes"; export default class PostMetadataUserNotes extends Component { @service siteSettings; + @service store; - + @action + showNotes() { + showUserNotes( + this.store, + this.args.post.user_id, + (count) => { + const cfs = this.args.post.user_custom_fields || {}; + cfs.user_notes_count = count; + this.args.post.user_custom_fields = cfs; + }, + { + postId: this.args.post.id, + } + ); + } + + } diff --git a/assets/javascripts/discourse/initializers/enable-user-notes.gjs b/assets/javascripts/discourse/initializers/enable-user-notes.gjs index 3a5b7f6..9c972be 100644 --- a/assets/javascripts/discourse/initializers/enable-user-notes.gjs +++ b/assets/javascripts/discourse/initializers/enable-user-notes.gjs @@ -36,7 +36,10 @@ function customizePost(api, container) { "post-meta-data-poster-name", class extends Component { static shouldRender(args, context) { - return context.site.mobileView; + return ( + context.site.mobileView && + args.post?.user_custom_fields?.user_notes_count > 0 + ); } @@ -47,14 +50,26 @@ function customizePost(api, container) { "post-meta-data-poster-name", class extends Component { static shouldRender(args, context) { - return !context.site.mobileView; + return ( + !context.site.mobileView && + args.post?.user_custom_fields?.user_notes_count > 0 + ); } } ); } else if (iconPlacement === "avatar") { - api.renderAfterWrapperOutlet("poster-avatar", PostMetadataUserNotes); + api.renderAfterWrapperOutlet( + "poster-avatar", + class extends Component { + static shouldRender(args) { + return args.post?.user_custom_fields?.user_notes_count > 0; + } + + + } + ); } withSilencedDeprecations("discourse.post-stream-widget-overrides", () => diff --git a/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js b/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js index 6117a6f..9bb6c72 100644 --- a/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js +++ b/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js @@ -4,7 +4,7 @@ export default { before: "freeze-valid-transformers", initialize() { - withPluginApi("1.33.0", (api) => { + withPluginApi((api) => { api.addValueTransformerName("user-notes-icon-placement"); }); }, diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 4aa464b..96d2187 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1,6 +1,7 @@ en: site_settings: user_notes_enabled: "Allow staff users to attach notes to users" + user_notes_icon_placement: "Placement of the user notes indicative icon in the posts" user_notes_moderators_delete: "Allow moderators to delete user notes" user_notes: From 73cc269f9c236a3e6f226f375499be835a573d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Tue, 24 Jun 2025 23:23:23 -0300 Subject: [PATCH 4/6] DEV: Refactor user notes count update logic into reusable function Extracted the logic for updating the `user_notes_count` field into a new `updatePostUserNotesCount` function. Updated all related components and initializers to use this function, reducing duplication and ensuring consistency in handling user notes updates. --- .../components/post-metadata-user-notes.gjs | 8 +- .../initializers/enable-user-notes.gjs | 133 ++++++++++-------- .../javascripts/discourse/lib/user-notes.js | 6 + 3 files changed, 81 insertions(+), 66 deletions(-) diff --git a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs index 3687cff..2997a78 100644 --- a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs +++ b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs @@ -4,7 +4,7 @@ import { action } from "@ember/object"; import { service } from "@ember/service"; import icon from "discourse/helpers/d-icon"; import emoji from "discourse/helpers/emoji"; -import { showUserNotes } from "../lib/user-notes"; +import { showUserNotes, updatePostUserNotesCount } from "../lib/user-notes"; export default class PostMetadataUserNotes extends Component { @service siteSettings; @@ -15,11 +15,7 @@ export default class PostMetadataUserNotes extends Component { showUserNotes( this.store, this.args.post.user_id, - (count) => { - const cfs = this.args.post.user_custom_fields || {}; - cfs.user_notes_count = count; - this.args.post.user_custom_fields = cfs; - }, + (count) => updatePostUserNotesCount(this.args.post, count), { postId: this.args.post.id, } diff --git a/assets/javascripts/discourse/initializers/enable-user-notes.gjs b/assets/javascripts/discourse/initializers/enable-user-notes.gjs index 9c972be..94ee7d3 100644 --- a/assets/javascripts/discourse/initializers/enable-user-notes.gjs +++ b/assets/javascripts/discourse/initializers/enable-user-notes.gjs @@ -4,8 +4,11 @@ import { iconNode } from "discourse/lib/icon-library"; import { withPluginApi } from "discourse/lib/plugin-api"; import { applyValueTransformer } from "discourse/lib/transformer"; import PostMetadataUserNotes from "../components/post-metadata-user-notes"; -import { showUserNotes } from "../lib/user-notes"; +import { showUserNotes, updatePostUserNotesCount } from "../lib/user-notes"; +/** + * Plugin initializer for enabling user notes functionality + */ export default { name: "enable-user-notes", initialize(container) { @@ -23,52 +26,59 @@ export default { }, }; +/** + * Customizes how user notes are displayed in posts + * + * @param {Object} api - Plugin API instance + * @param {Object} container - Container instance + */ function customizePost(api, container) { const siteSettings = container.lookup("service:site-settings"); - const iconPlacement = applyValueTransformer( + const placement = applyValueTransformer( "user-notes-icon-placement", siteSettings.user_notes_icon_placement ); - if (iconPlacement === "name") { - api.renderBeforeWrapperOutlet( - "post-meta-data-poster-name", - class extends Component { - static shouldRender(args, context) { - return ( - context.site.mobileView && - args.post?.user_custom_fields?.user_notes_count > 0 - ); - } - - - } - ); + // Component to display user notes flair icon + class UserNotesPostMetadataFlairIcon extends Component { + static shouldRender(args) { + return args.post?.user_custom_fields?.user_notes_count > 0; + } - api.renderAfterWrapperOutlet( - "post-meta-data-poster-name", - class extends Component { - static shouldRender(args, context) { - return ( - !context.site.mobileView && - args.post?.user_custom_fields?.user_notes_count > 0 - ); - } - - - } - ); - } else if (iconPlacement === "avatar") { + + } + + // Handle placement next to avatar + if (placement === "avatar") { api.renderAfterWrapperOutlet( "poster-avatar", - class extends Component { - static shouldRender(args) { - return args.post?.user_custom_fields?.user_notes_count > 0; - } + UserNotesPostMetadataFlairIcon + ); + } + // Handle placement next to username + else if (placement === "name") { + // Mobile-specific version + class MobileUserNotesIcon extends UserNotesPostMetadataFlairIcon { + static shouldRender(args, context) { + return context.site.mobileView && super.shouldRender(args); + } + } - + // Desktop-specific version + class DesktopUserNotesIcon extends UserNotesPostMetadataFlairIcon { + static shouldRender(args, context) { + return !context.site.mobileView && super.shouldRender(args); } + } + + api.renderBeforeWrapperOutlet( + "post-meta-data-poster-name", + MobileUserNotesIcon + ); + api.renderAfterWrapperOutlet( + "post-meta-data-poster-name", + DesktopUserNotesIcon ); } @@ -77,7 +87,13 @@ function customizePost(api, container) { ); } +/** + * Customizes the post widget to display user notes + * + * @param {Object} api - Plugin API instance + */ function customizeWidgetPost(api) { + // Handler for showing user notes modal function widgetShowUserNotes() { showUserNotes( this.store, @@ -91,49 +107,43 @@ function customizeWidgetPost(api) { ); } + // Update post when notes are changed api.attachWidgetAction("post", "refreshUserNotes", function (count) { - const cfs = this.model.user_custom_fields || {}; - cfs.user_notes_count = count; - this.model.set("user_custom_fields", cfs); + updatePostUserNotesCount(this.model, count); }); const mobileView = api.container.lookup("service:site").mobileView; const loc = mobileView ? "before" : "after"; - api.decorateWidget(`poster-name:${loc}`, (dec) => { - if (dec.widget.settings.hideNotes) { - return; + // Helper to attach notes icon if user has notes + const attachUserNotesIconIfPresent = (dec) => { + const post = dec.getModel(); + if (post?.user_custom_fields?.user_notes_count > 0) { + return dec.attach("user-notes-icon"); } + }; - const post = dec.getModel(); - if (!post) { + // Add notes icon to poster name + api.decorateWidget(`poster-name:${loc}`, (dec) => { + if (dec.widget.settings.hideNotes) { return; } - const ucf = post.user_custom_fields || {}; - if (ucf.user_notes_count > 0) { - return dec.attach("user-notes-icon"); - } + return attachUserNotesIconIfPresent(dec); }); + // Add notes icon after avatar api.decorateWidget(`post-avatar:after`, (dec) => { if (!dec.widget.settings.showNotes) { return; } - const post = dec.getModel(); - if (!post) { - return; - } - - const ucf = post.user_custom_fields || {}; - if (ucf.user_notes_count > 0) { - return dec.attach("user-notes-icon"); - } + return attachUserNotesIconIfPresent(dec); }); api.attachWidgetAction("post", "showUserNotes", widgetShowUserNotes); + // Create the user notes icon widget api.createWidget("user-notes-icon", { services: ["site-settings"], @@ -150,6 +160,12 @@ function customizeWidgetPost(api) { }); } +/** + * Adds user notes button to post admin menu + * + * @param {Object} api - Plugin API instance + * @param {Object} container - Container instance + */ function customizePostMenu(api, container) { const appEvents = container.lookup("service:app-events"); const store = container.lookup("service:store"); @@ -163,10 +179,7 @@ function customizePostMenu(api, container) { store, attrs.user_id, (count) => { - const ucf = post.user_custom_fields || {}; - ucf.user_notes_count = count; - post.set("user_custom_fields", ucf); - + updatePostUserNotesCount(post, count); appEvents.trigger("post-stream:refresh", { id: post.id, }); diff --git a/assets/javascripts/discourse/lib/user-notes.js b/assets/javascripts/discourse/lib/user-notes.js index 05eaf53..b20de37 100644 --- a/assets/javascripts/discourse/lib/user-notes.js +++ b/assets/javascripts/discourse/lib/user-notes.js @@ -16,3 +16,9 @@ export function showUserNotes(store, userId, callback, opts) { }); }); } + +export function updatePostUserNotesCount(post, count) { + const cfs = post.user_custom_fields || {}; + cfs.user_notes_count = count; + post.user_custom_fields = cfs; +} From 801ff650796377ef23277a9037f237422d6e264a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Saquetim?= Date: Tue, 24 Jun 2025 23:59:05 -0300 Subject: [PATCH 5/6] Fix linting issue --- .../discourse/components/post-metadata-user-notes.gjs | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs index 2997a78..bb3a6a4 100644 --- a/assets/javascripts/discourse/components/post-metadata-user-notes.gjs +++ b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs @@ -23,6 +23,7 @@ export default class PostMetadataUserNotes extends Component { }