FEATURE: Add decorateCookedSignature plugin API method (#109)
* FEATURE: Add decorateCookedSignature plugin API method
Adds a new plugin API method that allows themes and plugins to
decorate signature content, similar to decorateCookedElement for
posts and decorateChatMessage for chat messages.
Usage:
api.decorateCookedSignature((element, helper, post) => {
// modify signature element
});
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Gabriel Grubba <gabriel@discourse.org>
This commit is contained in:
@@ -1,6 +1,18 @@
|
||||
import Component from "@glimmer/component";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { trustHTML } from "@ember/template";
|
||||
import DecoratedHtml from "discourse/components/decorated-html";
|
||||
import { bind } from "discourse/lib/decorators";
|
||||
|
||||
let _signatureDecorators = [];
|
||||
|
||||
export function addSignatureDecorator(decorator) {
|
||||
_signatureDecorators.push(decorator);
|
||||
}
|
||||
|
||||
export function resetSignatureDecorators() {
|
||||
_signatureDecorators = [];
|
||||
}
|
||||
|
||||
export default class PostSignature extends Component {
|
||||
static shouldRender(args, context) {
|
||||
@@ -44,14 +56,21 @@ export default class PostSignature extends Component {
|
||||
return `max-height: ${this.siteSettings.signatures_max_image_height}px`;
|
||||
}
|
||||
|
||||
@bind
|
||||
decorateSignature(element, helper) {
|
||||
_signatureDecorators.forEach((decorator) => {
|
||||
decorator(element, helper, this.args.post);
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<hr />
|
||||
{{#if this.isAdvancedModeEnabled}}
|
||||
<div>
|
||||
<div class="user-signature">
|
||||
{{htmlSafe @post.user_signature}}
|
||||
</div>
|
||||
</div>
|
||||
<DecoratedHtml
|
||||
@html={{trustHTML @post.user_signature}}
|
||||
@decorate={{this.decorateSignature}}
|
||||
@className="user-signature"
|
||||
/>
|
||||
{{else}}
|
||||
<img
|
||||
class="signature-img"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import {
|
||||
addSignatureDecorator,
|
||||
resetSignatureDecorators,
|
||||
} from "discourse/plugins/discourse-signatures/discourse/components/post-signature";
|
||||
|
||||
/**
|
||||
* Callback used to decorate a signature
|
||||
*
|
||||
* @callback PluginApi~decorateCookedSignatureCallback
|
||||
* @param {HTMLElement} element - The signature DOM element
|
||||
* @param {Object} helper - Decorator helper object
|
||||
* @param {Object} post - The post model containing the signature
|
||||
*/
|
||||
|
||||
/**
|
||||
* Decorate a cooked signature element
|
||||
*
|
||||
* @memberof PluginApi
|
||||
* @instance
|
||||
* @function decorateCookedSignature
|
||||
* @param {PluginApi~decorateCookedSignatureCallback} decorator
|
||||
* @example
|
||||
*
|
||||
* api.decorateCookedSignature((element, helper, post) => {
|
||||
* element.classList.add('decorated-signature');
|
||||
* });
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: "signatures-plugin-api",
|
||||
after: "inject-discourse-objects",
|
||||
|
||||
initialize(container) {
|
||||
const siteSettings = container.lookup("service:site-settings");
|
||||
if (!siteSettings.signatures_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
withPluginApi((api) => {
|
||||
const apiPrototype = Object.getPrototypeOf(api);
|
||||
|
||||
if (!apiPrototype.hasOwnProperty("decorateCookedSignature")) {
|
||||
Object.defineProperty(apiPrototype, "decorateCookedSignature", {
|
||||
value(decorator) {
|
||||
addSignatureDecorator(decorator);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
teardown() {
|
||||
resetSignatureDecorators();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,199 @@
|
||||
import { visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
|
||||
import { resetSignatureDecorators } from "discourse/plugins/discourse-signatures/discourse/components/post-signature";
|
||||
|
||||
acceptance("Acceptance | decorateCookedSignature", function (needs) {
|
||||
needs.user();
|
||||
needs.settings({
|
||||
signatures_enabled: true,
|
||||
signatures_advanced_mode: true,
|
||||
signatures_visible_by_default: true,
|
||||
});
|
||||
|
||||
needs.hooks.afterEach(() => {
|
||||
resetSignatureDecorators();
|
||||
});
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/t/999.json", () => {
|
||||
return helper.response({
|
||||
post_stream: {
|
||||
posts: [
|
||||
{
|
||||
id: 1,
|
||||
username: "signature_user",
|
||||
avatar_template:
|
||||
"/letter_avatar_proxy/v4/letter/s/8edcca/{size}.png",
|
||||
created_at: "2024-01-01T12:00:00.000Z",
|
||||
cooked: "<p>This is a post with a signature</p>",
|
||||
post_number: 1,
|
||||
post_type: 1,
|
||||
updated_at: "2024-01-01T12:00:00.000Z",
|
||||
reply_count: 0,
|
||||
reply_to_post_number: null,
|
||||
quote_count: 0,
|
||||
incoming_link_count: 0,
|
||||
reads: 1,
|
||||
readers_count: 0,
|
||||
score: 0,
|
||||
yours: false,
|
||||
topic_id: 999,
|
||||
topic_slug: "topic-with-signature",
|
||||
display_username: "Signature User",
|
||||
primary_group_name: null,
|
||||
flair_url: null,
|
||||
flair_bg_color: null,
|
||||
flair_color: null,
|
||||
version: 1,
|
||||
can_edit: false,
|
||||
can_delete: false,
|
||||
can_recover: false,
|
||||
can_wiki: false,
|
||||
read: true,
|
||||
user_title: null,
|
||||
bookmarked: false,
|
||||
bookmarks: [],
|
||||
actions_summary: [],
|
||||
moderator: false,
|
||||
admin: false,
|
||||
staff: false,
|
||||
user_id: 2,
|
||||
hidden: false,
|
||||
trust_level: 1,
|
||||
deleted_at: null,
|
||||
user_deleted: false,
|
||||
edit_reason: null,
|
||||
can_view_edit_history: true,
|
||||
wiki: false,
|
||||
user_signature: "<p>My <strong>awesome</strong> signature</p>",
|
||||
},
|
||||
],
|
||||
stream: [1],
|
||||
},
|
||||
timeline_lookup: [[1, 0]],
|
||||
id: 999,
|
||||
title: "Topic with signature",
|
||||
fancy_title: "Topic with signature",
|
||||
posts_count: 1,
|
||||
created_at: "2024-01-01T12:00:00.000Z",
|
||||
views: 1,
|
||||
reply_count: 0,
|
||||
like_count: 0,
|
||||
last_posted_at: "2024-01-01T12:00:00.000Z",
|
||||
visible: true,
|
||||
closed: false,
|
||||
archived: false,
|
||||
has_summary: false,
|
||||
archetype: "regular",
|
||||
slug: "topic-with-signature",
|
||||
category_id: 1,
|
||||
word_count: 10,
|
||||
deleted_at: null,
|
||||
user_id: 2,
|
||||
featured_link: null,
|
||||
pinned_globally: false,
|
||||
pinned_at: null,
|
||||
pinned_until: null,
|
||||
image_url: null,
|
||||
draft: null,
|
||||
draft_key: "topic_999",
|
||||
draft_sequence: 0,
|
||||
posted: false,
|
||||
unpinned: null,
|
||||
pinned: false,
|
||||
current_post_number: 1,
|
||||
highest_post_number: 1,
|
||||
deleted_by: null,
|
||||
has_deleted: false,
|
||||
actions_summary: [],
|
||||
chunk_size: 20,
|
||||
bookmarked: false,
|
||||
bookmarks: [],
|
||||
topic_timer: null,
|
||||
message_bus_last_id: 0,
|
||||
participant_count: 1,
|
||||
show_read_indicator: false,
|
||||
thumbnails: null,
|
||||
details: {
|
||||
can_create_post: true,
|
||||
participants: [
|
||||
{
|
||||
id: 2,
|
||||
username: "signature_user",
|
||||
avatar_template:
|
||||
"/letter_avatar_proxy/v4/letter/s/8edcca/{size}.png",
|
||||
post_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("decorateCookedSignature applies decorators to signature content", async function (assert) {
|
||||
let decoratorCalled = false;
|
||||
let receivedElement = null;
|
||||
let receivedPost = null;
|
||||
|
||||
withPluginApi((api) => {
|
||||
api.decorateCookedSignature((element, helper, post) => {
|
||||
decoratorCalled = true;
|
||||
receivedElement = element;
|
||||
receivedPost = post;
|
||||
element.classList.add("decorated-test-signature");
|
||||
});
|
||||
});
|
||||
|
||||
await visit("/t/topic-with-signature/999");
|
||||
|
||||
assert.true(decoratorCalled, "decorator was called");
|
||||
|
||||
assert.notStrictEqual(
|
||||
receivedElement,
|
||||
null,
|
||||
"element was passed to decorator"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
receivedPost.topic_slug,
|
||||
"topic-with-signature",
|
||||
"correct post was passed to decorator"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
receivedPost.user_signature,
|
||||
"<p>My <strong>awesome</strong> signature</p>",
|
||||
"post contains user_signature"
|
||||
);
|
||||
assert.dom(".user-signature.decorated-test-signature").exists();
|
||||
assert.dom(".user-signature strong").hasText("awesome");
|
||||
});
|
||||
|
||||
test("multiple decorators are applied in order", async function (assert) {
|
||||
const callOrder = [];
|
||||
|
||||
withPluginApi((api) => {
|
||||
api.decorateCookedSignature((element) => {
|
||||
callOrder.push("first");
|
||||
element.dataset.firstDecorator = "applied";
|
||||
});
|
||||
|
||||
api.decorateCookedSignature((element) => {
|
||||
callOrder.push("second");
|
||||
element.dataset.secondDecorator = "applied";
|
||||
});
|
||||
});
|
||||
|
||||
await visit("/t/topic-with-signature/999");
|
||||
|
||||
assert.deepEqual(
|
||||
callOrder,
|
||||
["first", "second"],
|
||||
"decorators called in registration order"
|
||||
);
|
||||
assert.dom(".user-signature[data-first-decorator='applied']").exists();
|
||||
assert.dom(".user-signature[data-second-decorator='applied']").exists();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user