DEV: Refactor PostList cooked HTML rendering (#31307)

- Remove JQuery

- Move decoration into `components/post-item`, instead of managing it
from the top-level user-stream component

- Use new `DecoratedHtml` component
This commit is contained in:
David Taylor 2025-02-14 09:46:32 +00:00 committed by GitHub
parent b471e3d5ba
commit 35084d3089
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 35 additions and 57 deletions

View File

@ -2,18 +2,22 @@ import Component from "@glimmer/component";
import { fn } from "@ember/helper"; import { fn } from "@ember/helper";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
import DecoratedHtml from "discourse/components/decorated-html";
import ExpandPost from "discourse/components/expand-post"; import ExpandPost from "discourse/components/expand-post";
import PostListItemDetails from "discourse/components/post-list/item/details"; import PostListItemDetails from "discourse/components/post-list/item/details";
import avatar from "discourse/helpers/avatar"; import avatar from "discourse/helpers/avatar";
import concatClass from "discourse/helpers/concat-class"; import concatClass from "discourse/helpers/concat-class";
import icon from "discourse/helpers/d-icon"; import icon from "discourse/helpers/d-icon";
import { bind } from "discourse/lib/decorators";
import { userPath } from "discourse/lib/url"; import { userPath } from "discourse/lib/url";
export default class PostListItem extends Component { export default class PostListItem extends Component {
@service site; @service site;
@service siteSettings; @service siteSettings;
@service currentUser; @service currentUser;
@service appEvents;
get moderatorActionClass() { get moderatorActionClass() {
return this.args.post.post_type === this.site.post_types.moderator_action return this.args.post.post_type === this.site.post_types.moderator_action
@ -70,6 +74,15 @@ export default class PostListItem extends Component {
} }
} }
@bind
decoratePostContent(element, helper) {
this.appEvents.trigger(
"decorate-non-stream-cooked-element",
element,
helper
);
}
<template> <template>
<div <div
class="post-list-item class="post-list-item
@ -147,11 +160,11 @@ export default class PostListItem extends Component {
data-user-id={{@post.user_id}} data-user-id={{@post.user_id}}
class="excerpt" class="excerpt"
> >
{{#if @post.expandedExcerpt}} <DecoratedHtml
{{~htmlSafe @post.expandedExcerpt~}} @html={{htmlSafe (or @post.expandedExcerpt @post.excerpt)}}
{{else}} @decorate={{this.decoratePostContent}}
{{~htmlSafe @post.excerpt~}} @className="cooked"
{{/if}} />
</div> </div>
{{yield to="belowPostItem"}} {{yield to="belowPostItem"}}

View File

@ -1,12 +1,9 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { hash } from "@ember/helper"; import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { getOwner } from "@ember/owner"; import { getOwner } from "@ember/owner";
import { later } from "@ember/runloop";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { modifier } from "ember-modifier";
import $ from "jquery";
import PluginOutlet from "discourse/components/plugin-outlet"; import PluginOutlet from "discourse/components/plugin-outlet";
import PostActionDescription from "discourse/components/post-action-description"; import PostActionDescription from "discourse/components/post-action-description";
import PostList from "discourse/components/post-list"; import PostList from "discourse/components/post-list";
@ -26,28 +23,6 @@ export default class UserStreamComponent extends Component {
@service appEvents; @service appEvents;
@service currentUser; @service currentUser;
@service router; @service router;
@tracked lastDecoratedElement;
eventListeners = modifier((element) => {
$(element).on("click.details-disabled", "details.disabled", () => false);
$(element).on("click.discourse-redirect", ".excerpt a", (e) => {
return ClickTrack.trackClick(e, getOwner(this));
});
later(() => {
this.updateLastDecoratedElement();
this.appEvents.trigger("decorate-non-stream-cooked-element", element);
});
return () => {
$(element).off("click.details-disabled", "details.disabled");
// Unbind link tracking
$(element).off("click.discourse-redirect", ".excerpt a");
};
});
constructor() {
super(...arguments);
}
get filterClassName() { get filterClassName() {
const filter = this.args.stream?.filter; const filter = this.args.stream?.filter;
@ -68,20 +43,6 @@ export default class UserStreamComponent extends Component {
return "username"; return "username";
} }
@action
updateLastDecoratedElement() {
const nodes = document.querySelectorAll(".user-stream-item");
if (!nodes || nodes.length === 0) {
return;
}
const lastElement = nodes[nodes.length - 1];
if (lastElement === this.lastDecoratedElement) {
return;
}
this.lastDecoratedElement = lastElement;
}
@action @action
async removeBookmark(userAction) { async removeBookmark(userAction) {
try { try {
@ -140,19 +101,21 @@ export default class UserStreamComponent extends Component {
return []; return [];
} }
later(() => {
let element = this.lastDecoratedElement?.nextElementSibling;
while (element) {
this.appEvents.trigger("user-stream:new-item-inserted", element);
this.appEvents.trigger("decorate-non-stream-cooked-element", element);
element = element.nextElementSibling;
}
this.updateLastDecoratedElement();
});
return this.args.stream.content; return this.args.stream.content;
} }
@action
handleClick(event) {
if (event.target.matches("details.disabled")) {
event.preventDefault();
return;
}
if (event.target.matches(".excerpt a")) {
return ClickTrack.trackClick(event, getOwner(this));
}
}
<template> <template>
<PostList <PostList
@posts={{@stream.content}} @posts={{@stream.content}}
@ -166,7 +129,7 @@ export default class UserStreamComponent extends Component {
@resumeDraft={{this.resumeDraft}} @resumeDraft={{this.resumeDraft}}
@removeDraft={{this.removeDraft}} @removeDraft={{this.removeDraft}}
class={{concatClass "user-stream" this.filterClassName}} class={{concatClass "user-stream" this.filterClassName}}
{{this.eventListeners @stream}} {{on "click" this.handleClick}}
> >
<:abovePostItemHeader as |post|> <:abovePostItemHeader as |post|>
<PluginOutlet <PluginOutlet

View File

@ -165,6 +165,8 @@ export default class Post extends RestModel {
@trackedPostProperty user_deleted; @trackedPostProperty user_deleted;
@trackedPostProperty user_id; @trackedPostProperty user_id;
@trackedPostProperty yours; @trackedPostProperty yours;
@trackedPostProperty expandedExcerpt;
@trackedPostProperty excerpt;
customShare = null; customShare = null;

View File

@ -40,7 +40,7 @@ acceptance("User Drafts", function (needs) {
assert.dom(".user-stream-item").exists("has drafts"); assert.dom(".user-stream-item").exists("has drafts");
assert.dom(".user-stream-item:nth-child(3) .category").hasText("meta"); assert.dom(".user-stream-item:nth-child(3) .category").hasText("meta");
assert assert
.dom(".user-stream-item:nth-child(3) .excerpt") .dom(".user-stream-item:nth-child(3) .excerpt .cooked")
.hasHtml( .hasHtml(
`here goes a reply to a PM <img src="/images/emoji/twitter/slight_smile.png?v=${IMAGE_VERSION}" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;">`, `here goes a reply to a PM <img src="/images/emoji/twitter/slight_smile.png?v=${IMAGE_VERSION}" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20" style="aspect-ratio: 20 / 20;">`,
"shows the excerpt" "shows the excerpt"