DEV: Refactor chat HTML decorating (#31309)

Uses the new `<DecoratedHtml` component, which takes care of the full
decorate/render lifecycle. That means we can drop all the custom
modifiers/debouncing which chat was doing. It also naturally adds
support for `helper.renderGlimmer` in chat decorations.

This should resolve a number of subtle bugs related to chat message
decorations.
This commit is contained in:
David Taylor 2025-02-14 11:34:39 +00:00 committed by GitHub
parent b2b9657a0b
commit a0f681b256
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 86 additions and 94 deletions

View File

@ -1,9 +1,13 @@
<div class="chat-message-collapser">
{{#if this.hasUploads}}
{{html-safe @cooked}}
<DecoratedHtml
@html={{html-safe @cooked}}
@decorate={{@decorate}}
@className="chat-cooked"
/>
<Collapser @header={{this.uploadsHeader}} @onToggle={{@onToggleCollapse}}>
<div class="chat-uploads">
<div class="chat-uploads" {{didInsert this.lightbox}}>
{{#each @uploads as |upload|}}
<ChatUpload @upload={{upload}} />
{{/each}}
@ -18,11 +22,19 @@
<LazyVideo @videoAttributes={{cooked.videoAttributes}} />
</div>
{{else}}
{{cooked.body}}
<DecoratedHtml
@html={{html-safe cooked.body}}
@decorate={{@decorate}}
@className="chat-cooked"
/>
{{/if}}
</Collapser>
{{else}}
{{cooked.body}}
<DecoratedHtml
@html={{html-safe cooked.body}}
@decorate={{@decorate}}
@className="chat-cooked"
/>
{{/if}}
{{/each}}
{{/if}}

View File

@ -1,9 +1,11 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import domFromString from "discourse/lib/dom-from-string";
import { escapeExpression } from "discourse/lib/utilities";
import { i18n } from "discourse-i18n";
import lightbox from "../lib/lightbox";
export default class ChatMessageCollapser extends Component {
@service siteSettings;
@ -66,12 +68,17 @@ export default class ChatMessageCollapser extends Component {
`<a target="_blank" class="chat-message-collapser-link" rel="noopener noreferrer" href="${link}">${title}</a>`
);
acc.push({ header, body: e, videoAttributes, needsCollapser: true });
acc.push({
header,
body: e.outerHTML,
videoAttributes,
needsCollapser: true,
});
} else {
acc.push({ body: e, needsCollapser: false });
acc.push({ body: e.outerHTML, needsCollapser: false });
}
} else {
acc.push({ body: e, needsCollapser: false });
acc.push({ body: e.outerHTML, needsCollapser: false });
}
return acc;
}, []);
@ -88,9 +95,9 @@ export default class ChatMessageCollapser extends Component {
const header = htmlSafe(
`<a target="_blank" class="chat-message-collapser-link-small" rel="noopener noreferrer" href="${link}">${link}</a>`
);
acc.push({ header, body: e, needsCollapser: true });
acc.push({ header, body: e.outerHTML, needsCollapser: true });
} else {
acc.push({ body: e, needsCollapser: false });
acc.push({ body: e.outerHTML, needsCollapser: false });
}
return acc;
}, []);
@ -106,9 +113,9 @@ export default class ChatMessageCollapser extends Component {
alt || link
}</a>`
);
acc.push({ header, body: e, needsCollapser: true });
acc.push({ header, body: e.outerHTML, needsCollapser: true });
} else {
acc.push({ body: e, needsCollapser: false });
acc.push({ body: e.outerHTML, needsCollapser: false });
}
return acc;
}, []);
@ -125,13 +132,18 @@ export default class ChatMessageCollapser extends Component {
const header = htmlSafe(
`<a target="_blank" class="chat-message-collapser-link-small" rel="noopener noreferrer" href="${link}">${title}</a>`
);
acc.push({ header, body: e, needsCollapser: true });
acc.push({ header, body: e.outerHTML, needsCollapser: true });
} else {
acc.push({ body: e, needsCollapser: false });
acc.push({ body: e.outerHTML, needsCollapser: false });
}
return acc;
}, []);
}
@action
lightbox(element) {
lightbox(element.querySelectorAll("img.chat-img-upload"));
}
}
function lazyVideoPredicate(e) {

View File

@ -1,5 +1,6 @@
import Component from "@glimmer/component";
import { htmlSafe } from "@ember/template";
import DecoratedHtml from "discourse/components/decorated-html";
import { i18n } from "discourse-i18n";
import { isCollapsible } from "discourse/plugins/chat/discourse/components/chat-message-collapser";
import ChatMessageCollapser from "./chat-message-collapser";
@ -18,11 +19,16 @@ export default class ChatMessageText extends Component {
{{#if this.isCollapsible}}
<ChatMessageCollapser
@cooked={{@cooked}}
@decorate={{@decorate}}
@uploads={{@uploads}}
@onToggleCollapse={{@onToggleCollapse}}
/>
{{else}}
{{htmlSafe @cooked}}
<DecoratedHtml
@html={{htmlSafe @cooked}}
@decorate={{@decorate}}
@className=" chat-cooked"
/>
{{/if}}
{{#if this.isEdited}}

View File

@ -5,8 +5,6 @@ import { concat, fn } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { getOwner } from "@ember/owner";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
import { cancel, schedule } from "@ember/runloop";
import { service } from "@ember/service";
@ -186,7 +184,6 @@ export default class ChatMessage extends Component {
cancel(this._invitationSentTimer);
cancel(this._disableMessageActionsHandler);
cancel(this._makeMessageActiveHandler);
cancel(this._debounceDecorateCookedMessageHandler);
this.#teardownMentionedUsers();
this.chat.activeMessage = null;
}
@ -207,36 +204,6 @@ export default class ChatMessage extends Component {
});
}
@action
didInsertMessage(element) {
this.messageContainer = element;
this.initMentionedUsers();
this.decorateMentions(element);
this.debounceDecorateCookedMessage();
this.refreshStatusOnMentions();
}
@action
didUpdateMessageId() {
this.debounceDecorateCookedMessage();
}
@action
didUpdateMessageVersion() {
this.debounceDecorateCookedMessage();
this.refreshStatusOnMentions();
this.initMentionedUsers();
}
debounceDecorateCookedMessage() {
this._debounceDecorateCookedMessageHandler = discourseDebounce(
this,
this.decorateCookedMessage,
this.args.message,
100
);
}
initMentionedUsers() {
this.args.message.mentionedUsers.forEach((user) => {
if (!user.statusManager.isTrackingStatus()) {
@ -273,16 +240,18 @@ export default class ChatMessage extends Component {
mentions.forEach((mention) => {
mention.classList.add(...classes);
updateUserStatusOnMention(getOwner(this), mention, user.status);
});
});
}
@action
decorateCookedMessage(message) {
schedule("afterRender", () => {
_chatMessageDecorators.forEach((decorator) => {
decorator.call(this, this.messageContainer, message.channel);
});
@bind
decorateCookedMessage(element, helper) {
this.messageContainer = element;
this.initMentionedUsers();
this.decorateMentions(element);
_chatMessageDecorators.forEach((decorator) => {
decorator(element, helper);
});
}
@ -598,9 +567,6 @@ export default class ChatMessage extends Component {
}}
data-id={{@message.id}}
data-thread-id={{@message.thread.id}}
{{didInsert this.didInsertMessage}}
{{didUpdate this.didUpdateMessageId @message.id}}
{{didUpdate this.didUpdateMessageVersion @message.version}}
{{willDestroy this.willDestroyMessage}}
{{on "mouseenter" this.onMouseEnter passive=true}}
{{on "mouseleave" this.onMouseLeave passive=true}}
@ -665,6 +631,7 @@ export default class ChatMessage extends Component {
@cooked={{@message.cooked}}
@uploads={{@message.uploads}}
@edited={{@message.edited}}
@decorate={{this.decorateCookedMessage}}
>
{{#if @message.reactions.length}}
<div class="chat-message-reaction-list">

View File

@ -1,13 +1,11 @@
import $ from "jquery";
import { spinnerHTML } from "discourse/helpers/loading-spinner";
import { decorateGithubOneboxBody } from "discourse/instance-initializers/onebox-decorators";
import { samePrefix } from "discourse/lib/get-url";
import { decorateHashtags } from "discourse/lib/hashtag-decorator";
import highlightSyntax from "discourse/lib/highlight-syntax";
import loadScript from "discourse/lib/load-script";
import { withPluginApi } from "discourse/lib/plugin-api";
import DiscourseURL from "discourse/lib/url";
import { i18n } from "discourse-i18n";
import lightbox from "../lib/lightbox";
export default {
name: "chat-decorators",
@ -68,7 +66,7 @@ export default {
});
api.decorateChatMessage(
(element) =>
this.lightbox(element.querySelectorAll("img:not(.emoji, .avatar)")),
lightbox(element.querySelectorAll("img:not(.emoji, .avatar)")),
{
id: "lightbox",
}
@ -108,15 +106,9 @@ export default {
return;
}
if (this.currentUserTimezone) {
dateTimeLinkEl.innerText = moment
.tz(dateTimeRaw, this.currentUserTimezone)
.format(i18n("dates.long_no_year"));
} else {
dateTimeLinkEl.innerText = moment(dateTimeRaw).format(
i18n("dates.long_no_year")
);
}
dateTimeLinkEl.innerText = moment(dateTimeRaw).format(
i18n("dates.long_no_year")
);
});
},
@ -132,29 +124,6 @@ export default {
}
},
lightbox(images) {
loadScript("/javascripts/jquery.magnific-popup.min.js").then(function () {
$(images).magnificPopup({
type: "image",
closeOnContentClick: false,
mainClass: "mfp-zoom-in",
tClose: i18n("lightbox.close"),
tLoading: spinnerHTML,
image: {
verticalFit: true,
},
gallery: {
enabled: true,
},
callbacks: {
elementParse: (item) => {
item.src = item.el[0].dataset.largeSrc || item.el[0].src;
},
},
});
});
},
initialize(container) {
if (container.lookup("service:chat").userCanChat) {
withPluginApi("0.8.42", (api) =>

View File

@ -0,0 +1,27 @@
import $ from "jquery";
import { spinnerHTML } from "discourse/helpers/loading-spinner";
import loadScript from "discourse/lib/load-script";
import { i18n } from "discourse-i18n";
export default function lightbox(images) {
loadScript("/javascripts/jquery.magnific-popup.min.js").then(function () {
$(images).magnificPopup({
type: "image",
closeOnContentClick: false,
mainClass: "mfp-zoom-in",
tClose: i18n("lightbox.close"),
tLoading: spinnerHTML,
image: {
verticalFit: true,
},
gallery: {
enabled: true,
},
callbacks: {
elementParse: (item) => {
item.src = item.el[0].dataset.largeSrc || item.el[0].src;
},
},
});
});
}

View File

@ -200,8 +200,7 @@ body.has-full-page-chat {
margin-bottom: 0;
}
.chat-message-collapser,
.chat-message-text {
.chat-cooked {
> p {
margin: 0.5em 0 0.5em;
}