diff --git a/.discourse-compatibility b/.discourse-compatibility
index 2f02d14..60c44c7 100644
--- a/.discourse-compatibility
+++ b/.discourse-compatibility
@@ -1,3 +1,4 @@
+< 3.5.0.beta8-dev: 52682ad08a831c7e1a1e67ce47e20796b3fa9df0
< 3.5.0.beta5-dev: be64a5ea30dcda658a74e22a9e7b5fd8cd7632c8
< 3.5.0.beta1-dev: b7181ad63238adf843d27b2d0db13cb6354df379
< 3.4.0.beta2-dev: ff810c65d88e3a208b1126e94ec9ba637d6e997e
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..bb3a6a4
--- /dev/null
+++ b/assets/javascripts/discourse/components/post-metadata-user-notes.gjs
@@ -0,0 +1,35 @@
+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, updatePostUserNotesCount } 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) => updatePostUserNotesCount(this.args.post, count),
+ {
+ postId: this.args.post.id,
+ }
+ );
+ }
+
+
+ {{! template-lint-disable no-invalid-interactive }}
+
+ {{#if this.siteSettings.enable_emoji}}
+ {{emoji "memo"}}
+ {{else}}
+ {{icon "pen-to-square"}}
+ {{/if}}
+
+
+}
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..94ee7d3
--- /dev/null
+++ b/assets/javascripts/discourse/initializers/enable-user-notes.gjs
@@ -0,0 +1,194 @@
+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, updatePostUserNotesCount } from "../lib/user-notes";
+
+/**
+ * Plugin initializer for enabling user notes functionality
+ */
+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);
+ });
+ },
+};
+
+/**
+ * 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 placement = applyValueTransformer(
+ "user-notes-icon-placement",
+ siteSettings.user_notes_icon_placement
+ );
+
+ // Component to display user notes flair icon
+ class UserNotesPostMetadataFlairIcon extends Component {
+ static shouldRender(args) {
+ return args.post?.user_custom_fields?.user_notes_count > 0;
+ }
+
+
+ }
+
+ // Handle placement next to avatar
+ if (placement === "avatar") {
+ api.renderAfterWrapperOutlet(
+ "poster-avatar",
+ 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
+ );
+ }
+
+ withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
+ customizeWidgetPost(api)
+ );
+}
+
+/**
+ * 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,
+ this.attrs.user_id,
+ (count) => {
+ this.sendWidgetAction("refreshUserNotes", count);
+ },
+ {
+ postId: this.attrs.id,
+ }
+ );
+ }
+
+ // Update post when notes are changed
+ api.attachWidgetAction("post", "refreshUserNotes", function (count) {
+ updatePostUserNotesCount(this.model, count);
+ });
+
+ const mobileView = api.container.lookup("service:site").mobileView;
+ const loc = mobileView ? "before" : "after";
+
+ // 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");
+ }
+ };
+
+ // Add notes icon to poster name
+ api.decorateWidget(`poster-name:${loc}`, (dec) => {
+ if (dec.widget.settings.hideNotes) {
+ return;
+ }
+
+ return attachUserNotesIconIfPresent(dec);
+ });
+
+ // Add notes icon after avatar
+ api.decorateWidget(`post-avatar:after`, (dec) => {
+ if (!dec.widget.settings.showNotes) {
+ return;
+ }
+
+ return attachUserNotesIconIfPresent(dec);
+ });
+
+ api.attachWidgetAction("post", "showUserNotes", widgetShowUserNotes);
+
+ // Create the user notes icon widget
+ 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");
+ }
+ },
+ });
+}
+
+/**
+ * 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");
+
+ api.addPostAdminMenuButton((attrs) => {
+ return {
+ icon: "pen-to-square",
+ label: "user_notes.attach",
+ action: (post) => {
+ showUserNotes(
+ store,
+ attrs.user_id,
+ (count) => {
+ updatePostUserNotesCount(post, count);
+ 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/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;
+}
diff --git a/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js b/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js
new file mode 100644
index 0000000..9bb6c72
--- /dev/null
+++ b/assets/javascripts/discourse/pre-initializers/user-notes-transformers.js
@@ -0,0 +1,11 @@
+import { withPluginApi } from "discourse/lib/plugin-api";
+
+export default {
+ before: "freeze-valid-transformers",
+
+ initialize() {
+ 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:
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