mirror of
https://github.com/hanzoai/forum.git
synced 2026-09-30 21:51:19 +00:00
`discourse-common` was created in the past to share logic between the 'wizard' app and the main 'discourse' app. Since then, the wizard has been consolidated into the main app, so the separation of `discourse-common` is no longer useful. This commit moves `discourse-common/(lib|utils)/*` into `discourse/lib/*`, adds shims for the imports, and updates existing uses in core.
724 lines
20 KiB
Plaintext
724 lines
20 KiB
Plaintext
import Component from "@glimmer/component";
|
|
import { tracked } from "@glimmer/tracking";
|
|
import { Input } from "@ember/component";
|
|
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";
|
|
import { modifier } from "ember-modifier";
|
|
import { eq, lt, not } from "truth-helpers";
|
|
import DButton from "discourse/components/d-button";
|
|
import EmojiPicker from "discourse/components/emoji-picker";
|
|
import concatClass from "discourse/helpers/concat-class";
|
|
import discourseDebounce from "discourse/lib/debounce";
|
|
import { bind } from "discourse/lib/decorators";
|
|
import getURL from "discourse/lib/get-url";
|
|
import discourseLater from "discourse/lib/later";
|
|
import { applyValueTransformer } from "discourse/lib/transformer";
|
|
import { updateUserStatusOnMention } from "discourse/lib/update-user-status-on-mention";
|
|
import isZoomed from "discourse/lib/zoom-check";
|
|
import { i18n } from "discourse-i18n";
|
|
import ChatMessageAvatar from "discourse/plugins/chat/discourse/components/chat/message/avatar";
|
|
import ChatMessageError from "discourse/plugins/chat/discourse/components/chat/message/error";
|
|
import ChatMessageInfo from "discourse/plugins/chat/discourse/components/chat/message/info";
|
|
import ChatMessageLeftGutter from "discourse/plugins/chat/discourse/components/chat/message/left-gutter";
|
|
import ChatMessageBlocks from "discourse/plugins/chat/discourse/components/chat-message/blocks";
|
|
import ChatMessageActionsMobileModal from "discourse/plugins/chat/discourse/components/chat-message-actions-mobile";
|
|
import ChatMessageInReplyToIndicator from "discourse/plugins/chat/discourse/components/chat-message-in-reply-to-indicator";
|
|
import ChatMessageReaction from "discourse/plugins/chat/discourse/components/chat-message-reaction";
|
|
import ChatMessageSeparator from "discourse/plugins/chat/discourse/components/chat-message-separator";
|
|
import ChatMessageText from "discourse/plugins/chat/discourse/components/chat-message-text";
|
|
import ChatMessageThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
|
|
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
|
|
import ChatOnLongPress from "discourse/plugins/chat/discourse/modifiers/chat/on-long-press";
|
|
|
|
let _chatMessageDecorators = [];
|
|
|
|
export function addChatMessageDecorator(decorator) {
|
|
_chatMessageDecorators.push(decorator);
|
|
}
|
|
|
|
export function resetChatMessageDecorators() {
|
|
_chatMessageDecorators = [];
|
|
}
|
|
|
|
export const MENTION_KEYWORDS = ["here", "all"];
|
|
export const MESSAGE_CONTEXT_THREAD = "thread";
|
|
|
|
export default class ChatMessage extends Component {
|
|
@service site;
|
|
@service dialog;
|
|
@service currentUser;
|
|
@service appEvents;
|
|
@service capabilities;
|
|
@service chat;
|
|
@service chatApi;
|
|
@service chatChannelPane;
|
|
@service chatThreadPane;
|
|
@service chatChannelsManager;
|
|
@service router;
|
|
@service toasts;
|
|
@service modal;
|
|
@service interactedChatMessage;
|
|
|
|
@tracked isActive = false;
|
|
|
|
toggleCheckIfPossible = modifier((element) => {
|
|
let addedListener = false;
|
|
|
|
const handler = () => {
|
|
if (!this.pane.selectingMessages) {
|
|
return;
|
|
}
|
|
|
|
if (event.shiftKey) {
|
|
this.messageInteractor.bulkSelect(!this.args.message.selected);
|
|
return;
|
|
}
|
|
|
|
this.messageInteractor.select(!this.args.message.selected);
|
|
};
|
|
|
|
if (this.pane.selectingMessages) {
|
|
element.addEventListener("click", handler, { passive: true });
|
|
addedListener = true;
|
|
}
|
|
|
|
return () => {
|
|
if (addedListener) {
|
|
element.removeEventListener("click", handler);
|
|
}
|
|
};
|
|
});
|
|
|
|
get pane() {
|
|
return this.threadContext ? this.chatThreadPane : this.chatChannelPane;
|
|
}
|
|
|
|
get messageInteractor() {
|
|
return new ChatMessageInteractor(
|
|
getOwner(this),
|
|
this.args.message,
|
|
this.args.context
|
|
);
|
|
}
|
|
|
|
get deletedAndCollapsed() {
|
|
return this.args.message?.deletedAt && this.collapsed;
|
|
}
|
|
|
|
get hiddenAndCollapsed() {
|
|
return this.args.message?.hidden && this.collapsed;
|
|
}
|
|
|
|
get collapsed() {
|
|
return !this.args.message?.expanded;
|
|
}
|
|
|
|
get deletedMessageLabel() {
|
|
let count = 1;
|
|
|
|
const recursiveCount = (message) => {
|
|
const previousMessage = message.previousMessage;
|
|
if (previousMessage?.deletedAt) {
|
|
count++;
|
|
recursiveCount(previousMessage);
|
|
}
|
|
};
|
|
|
|
recursiveCount(this.args.message);
|
|
|
|
return i18n("chat.deleted", { count });
|
|
}
|
|
|
|
get shouldRender() {
|
|
return (
|
|
this.args.message.expanded ||
|
|
!this.args.message.deletedAt ||
|
|
(this.args.message.deletedAt && !this.args.message.nextMessage?.deletedAt)
|
|
);
|
|
}
|
|
|
|
get shouldRenderOpenEmojiPickerButton() {
|
|
return this.chat.userCanInteractWithChat && this.site.desktopView;
|
|
}
|
|
|
|
get secondaryActionsIsExpanded() {
|
|
return document.querySelector(
|
|
".more-buttons.secondary-actions.is-expanded"
|
|
);
|
|
}
|
|
|
|
@action
|
|
expand() {
|
|
const recursiveExpand = (message) => {
|
|
const previousMessage = message.previousMessage;
|
|
if (previousMessage?.deletedAt) {
|
|
previousMessage.expanded = true;
|
|
recursiveExpand(previousMessage);
|
|
}
|
|
};
|
|
|
|
this.args.message.expanded = true;
|
|
this.refreshStatusOnMentions();
|
|
recursiveExpand(this.args.message);
|
|
}
|
|
|
|
@action
|
|
toggleChecked(event) {
|
|
event.stopPropagation();
|
|
|
|
if (event.shiftKey) {
|
|
this.messageInteractor.bulkSelect(event.target.checked);
|
|
return;
|
|
}
|
|
|
|
this.messageInteractor.select(event.target.checked);
|
|
}
|
|
|
|
@action
|
|
willDestroyMessage() {
|
|
cancel(this._invitationSentTimer);
|
|
cancel(this._disableMessageActionsHandler);
|
|
cancel(this._makeMessageActiveHandler);
|
|
cancel(this._debounceDecorateCookedMessageHandler);
|
|
this.#teardownMentionedUsers();
|
|
this.chat.activeMessage = null;
|
|
}
|
|
|
|
@action
|
|
refreshStatusOnMentions() {
|
|
schedule("afterRender", () => {
|
|
this.args.message.mentionedUsers.forEach((user) => {
|
|
const href = `/u/${user.username.toLowerCase()}`;
|
|
const mentions = this.messageContainer.querySelectorAll(
|
|
`a.mention[href="${href}"]`
|
|
);
|
|
|
|
mentions.forEach((mention) => {
|
|
updateUserStatusOnMention(getOwner(this), mention, user.status);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
@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()) {
|
|
user.statusManager.trackStatus();
|
|
user.on("status-changed", this, "refreshStatusOnMentions");
|
|
}
|
|
});
|
|
}
|
|
|
|
decorateMentions(cooked) {
|
|
if (this.args.message.channel.allowChannelWideMentions) {
|
|
const wideMentions = [...cooked.querySelectorAll("span.mention")];
|
|
MENTION_KEYWORDS.forEach((keyword) => {
|
|
const mentions = wideMentions.filter((node) => {
|
|
return node.textContent.trim() === `@${keyword}`;
|
|
});
|
|
|
|
const classes = applyValueTransformer("mentions-class", [], {
|
|
user: { username: keyword },
|
|
});
|
|
|
|
mentions.forEach((mention) => {
|
|
mention.classList.add(...classes);
|
|
});
|
|
});
|
|
}
|
|
|
|
this.args.message.mentionedUsers.forEach((user) => {
|
|
const href = getURL(`/u/${user.username.toLowerCase()}`);
|
|
const mentions = cooked.querySelectorAll(`a.mention[href="${href}"]`);
|
|
const classes = applyValueTransformer("mentions-class", [], {
|
|
user,
|
|
});
|
|
|
|
mentions.forEach((mention) => {
|
|
mention.classList.add(...classes);
|
|
});
|
|
});
|
|
}
|
|
|
|
@action
|
|
decorateCookedMessage(message) {
|
|
schedule("afterRender", () => {
|
|
_chatMessageDecorators.forEach((decorator) => {
|
|
decorator.call(this, this.messageContainer, message.channel);
|
|
});
|
|
});
|
|
}
|
|
|
|
get show() {
|
|
return (
|
|
!this.args.message?.deletedAt ||
|
|
this.currentUser.id === this.args.message?.user?.id ||
|
|
this.currentUser.staff ||
|
|
this.args.message?.channel?.canModerate
|
|
);
|
|
}
|
|
|
|
@action
|
|
onMouseEnter() {
|
|
if (this.site.mobileView) {
|
|
return;
|
|
}
|
|
|
|
if (this.chat.activeMessage?.model?.id === this.args.message.id) {
|
|
return;
|
|
}
|
|
|
|
if (this.interactedChatMessage.emojiPickerOpen) {
|
|
return;
|
|
}
|
|
|
|
if (!this.secondaryActionsIsExpanded) {
|
|
this._onMouseEnterMessageDebouncedHandler = discourseDebounce(
|
|
this,
|
|
this._debouncedOnHoverMessage,
|
|
250
|
|
);
|
|
}
|
|
}
|
|
|
|
@action
|
|
onMouseMove() {
|
|
if (this.site.mobileView) {
|
|
return;
|
|
}
|
|
|
|
if (this.chat.activeMessage?.model?.id === this.args.message.id) {
|
|
return;
|
|
}
|
|
|
|
if (this.secondaryActionsIsExpanded) {
|
|
return;
|
|
}
|
|
|
|
if (this.interactedChatMessage.emojiPickerOpen) {
|
|
return;
|
|
}
|
|
|
|
this._setActiveMessage();
|
|
}
|
|
|
|
@action
|
|
onMouseLeave(event) {
|
|
cancel(this._onMouseEnterMessageDebouncedHandler);
|
|
|
|
if (this.site.mobileView) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
(event.toElement || event.relatedTarget)?.closest(
|
|
".chat-message-actions-container"
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (this.interactedChatMessage.emojiPickerOpen) {
|
|
return;
|
|
}
|
|
|
|
if (this.secondaryActionsIsExpanded) {
|
|
return;
|
|
}
|
|
|
|
this.chat.activeMessage = null;
|
|
}
|
|
|
|
@bind
|
|
_debouncedOnHoverMessage() {
|
|
this._setActiveMessage();
|
|
}
|
|
|
|
_setActiveMessage() {
|
|
if (this.args.disableMouseEvents) {
|
|
return;
|
|
}
|
|
|
|
cancel(this._onMouseEnterMessageDebouncedHandler);
|
|
|
|
if (!this.chat.userCanInteractWithChat) {
|
|
return;
|
|
}
|
|
|
|
if (!this.args.message.expanded) {
|
|
return;
|
|
}
|
|
|
|
this.chat.activeMessage = {
|
|
model: this.args.message,
|
|
context: this.args.context,
|
|
};
|
|
}
|
|
|
|
@action
|
|
onLongPressStart(element, event) {
|
|
if (!this.args.message.expanded || !this.args.message.persisted) {
|
|
return;
|
|
}
|
|
|
|
if (event.target.tagName === "IMG") {
|
|
return;
|
|
}
|
|
|
|
// prevents message to show as active when starting scroll
|
|
// at this moment scroll has no momentum and the row can
|
|
// capture the touch event instead of a scroll
|
|
this._makeMessageActiveHandler = discourseLater(() => {
|
|
this.isActive = true;
|
|
}, 125);
|
|
}
|
|
|
|
@action
|
|
onLongPressCancel() {
|
|
cancel(this._makeMessageActiveHandler);
|
|
this.isActive = false;
|
|
|
|
// this a tricky bit of code which is needed to prevent the long press
|
|
// from triggering a click on the message actions panel when releasing finger press
|
|
// we can't prevent default as we need to keep the event passive for performance reasons
|
|
// this class will prevent any click from being triggered until removed
|
|
// this number has been chosen from testing but might need to be increased
|
|
this._disableMessageActionsHandler = discourseLater(() => {
|
|
document.documentElement.classList.remove(
|
|
"disable-message-actions-touch"
|
|
);
|
|
}, 200);
|
|
}
|
|
|
|
@action
|
|
onLongPressEnd(element, event) {
|
|
if (event.target.tagName === "IMG") {
|
|
return;
|
|
}
|
|
|
|
cancel(this._makeMessageActiveHandler);
|
|
this.isActive = false;
|
|
|
|
if (isZoomed()) {
|
|
// if zoomed don't handle long press
|
|
return;
|
|
}
|
|
|
|
document.documentElement.classList.add("disable-message-actions-touch");
|
|
document.activeElement.blur();
|
|
document.querySelector(".chat-composer__input")?.blur();
|
|
|
|
this._setActiveMessage();
|
|
this.modal.show(ChatMessageActionsMobileModal);
|
|
}
|
|
|
|
get hasActiveState() {
|
|
return (
|
|
this.isActive ||
|
|
this.chat.activeMessage?.model?.id === this.args.message.id
|
|
);
|
|
}
|
|
|
|
get hasReply() {
|
|
return this.args.message.inReplyTo && !this.hideReplyToInfo;
|
|
}
|
|
|
|
get hideUserInfo() {
|
|
const message = this.args.message;
|
|
|
|
const previousMessage = message.previousMessage;
|
|
|
|
if (!previousMessage) {
|
|
return false;
|
|
}
|
|
|
|
// this is a micro optimization to avoid layout changes when we load more messages
|
|
if (message.firstOfResults) {
|
|
return false;
|
|
}
|
|
|
|
if (message.chatWebhookEvent) {
|
|
return false;
|
|
}
|
|
|
|
if (previousMessage.deletedAt) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
Math.abs(
|
|
new Date(message.createdAt) - new Date(previousMessage.createdAt)
|
|
) > 300000
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (message.inReplyTo) {
|
|
if (message.inReplyTo?.id === previousMessage.id) {
|
|
return message.user?.id === previousMessage.user?.id;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return message.user?.id === previousMessage.user?.id;
|
|
}
|
|
|
|
get hideReplyToInfo() {
|
|
return (
|
|
this.threadContext ||
|
|
this.args.message?.inReplyTo?.id ===
|
|
this.args.message?.previousMessage?.id ||
|
|
this.threadingEnabled
|
|
);
|
|
}
|
|
|
|
get threadingEnabled() {
|
|
return (
|
|
(this.args.message?.channel?.threadingEnabled ||
|
|
this.args.message?.thread?.force) &&
|
|
!!this.args.message?.thread
|
|
);
|
|
}
|
|
|
|
get showThreadIndicator() {
|
|
return (
|
|
!this.threadContext &&
|
|
this.threadingEnabled &&
|
|
this.args.message?.thread &&
|
|
this.args.message?.thread.preview.replyCount > 0
|
|
);
|
|
}
|
|
|
|
get threadContext() {
|
|
return this.args.context === MESSAGE_CONTEXT_THREAD;
|
|
}
|
|
|
|
get shouldRenderStopMessageStreamingButton() {
|
|
return (
|
|
this.args.message.streaming &&
|
|
(this.currentUser.admin ||
|
|
this.args.message.inReplyTo?.user?.id === this.currentUser.id)
|
|
);
|
|
}
|
|
|
|
@action
|
|
onEmojiPickerClose() {
|
|
this.interactedChatMessage.emojiPickerOpen = false;
|
|
}
|
|
|
|
@action
|
|
onEmojiPickerShow() {
|
|
this.interactedChatMessage.emojiPickerOpen = true;
|
|
}
|
|
|
|
@action
|
|
stopMessageStreaming(message) {
|
|
this.chatApi.stopMessageStreaming(message.channel.id, message.id);
|
|
}
|
|
|
|
#teardownMentionedUsers() {
|
|
this.args.message.mentionedUsers.forEach((user) => {
|
|
user.statusManager.stopTrackingStatus();
|
|
user.off("status-changed", this, "refreshStatusOnMentions");
|
|
});
|
|
}
|
|
|
|
<template>
|
|
{{! template-lint-disable no-invalid-interactive }}
|
|
{{#if this.shouldRender}}
|
|
<ChatMessageSeparator
|
|
@fetchMessagesByDate={{@fetchMessagesByDate}}
|
|
@message={{@message}}
|
|
/>
|
|
|
|
<div
|
|
class={{concatClass
|
|
"chat-message-container"
|
|
(if this.pane.selectingMessages "-selectable")
|
|
(if @message.highlighted "-highlighted")
|
|
(if @message.streaming "-streaming")
|
|
(if (lt @message.user.id 0) "is-bot")
|
|
(if (eq @message.user.id this.currentUser.id) "is-by-current-user")
|
|
(if (eq @message.id this.currentUser.id) "is-by-current-user")
|
|
(if
|
|
(eq
|
|
@message.id
|
|
@message.channel.currentUserMembership.lastReadMessageId
|
|
)
|
|
"-last-read"
|
|
)
|
|
(if @message.staged "-staged" "-persisted")
|
|
(if @message.processed "-processed" "-not-processed")
|
|
(if this.hasActiveState "-active")
|
|
(if @message.bookmark "-bookmarked")
|
|
(if @message.deletedAt "-deleted")
|
|
(if @message.selected "-selected")
|
|
(if @message.error "-errored")
|
|
(if this.showThreadIndicator "has-thread-indicator")
|
|
(if this.hideUserInfo "-user-info-hidden")
|
|
(if this.hasReply "has-reply")
|
|
}}
|
|
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}}
|
|
{{on "mousemove" this.onMouseMove passive=true}}
|
|
{{this.toggleCheckIfPossible}}
|
|
{{ChatOnLongPress
|
|
this.onLongPressStart
|
|
this.onLongPressEnd
|
|
this.onLongPressCancel
|
|
}}
|
|
...attributes
|
|
>
|
|
{{#if this.show}}
|
|
{{#if this.pane.selectingMessages}}
|
|
<Input
|
|
@type="checkbox"
|
|
class="chat-message-selector"
|
|
@checked={{@message.selected}}
|
|
{{on "click" this.toggleChecked}}
|
|
/>
|
|
{{/if}}
|
|
|
|
{{#if this.deletedAndCollapsed}}
|
|
<div class="chat-message-text -deleted">
|
|
<DButton
|
|
@action={{this.expand}}
|
|
@translatedLabel={{this.deletedMessageLabel}}
|
|
class="btn-flat chat-message-expand"
|
|
/>
|
|
</div>
|
|
{{else if this.hiddenAndCollapsed}}
|
|
<div class="chat-message-text -hidden">
|
|
<DButton
|
|
@action={{this.expand}}
|
|
@label="chat.hidden"
|
|
class="btn-flat chat-message-expand"
|
|
/>
|
|
</div>
|
|
{{else}}
|
|
<div class="chat-message">
|
|
{{#unless this.hideReplyToInfo}}
|
|
<ChatMessageInReplyToIndicator @message={{@message}} />
|
|
{{/unless}}
|
|
|
|
{{#if this.hideUserInfo}}
|
|
<ChatMessageLeftGutter
|
|
@message={{@message}}
|
|
@threadContext={{this.threadContext}}
|
|
/>
|
|
{{else}}
|
|
<ChatMessageAvatar @message={{@message}} />
|
|
{{/if}}
|
|
|
|
<div class="chat-message-content">
|
|
<ChatMessageInfo
|
|
@message={{@message}}
|
|
@show={{not this.hideUserInfo}}
|
|
@threadContext={{this.threadContext}}
|
|
/>
|
|
|
|
<ChatMessageText
|
|
@cooked={{@message.cooked}}
|
|
@uploads={{@message.uploads}}
|
|
@edited={{@message.edited}}
|
|
>
|
|
{{#if @message.reactions.length}}
|
|
<div class="chat-message-reaction-list">
|
|
{{#each @message.reactions as |reaction|}}
|
|
<ChatMessageReaction
|
|
@reaction={{reaction}}
|
|
@onReaction={{this.messageInteractor.react}}
|
|
@message={{@message}}
|
|
@showTooltip={{true}}
|
|
/>
|
|
{{/each}}
|
|
|
|
{{#if this.shouldRenderOpenEmojiPickerButton}}
|
|
<EmojiPicker
|
|
@context={{concat "channel_" @message.channel.id}}
|
|
@didSelectEmoji={{this.messageInteractor.selectReaction}}
|
|
@btnClass="btn-flat react-btn chat-message-react-btn"
|
|
@onClose={{this.onEmojiPickerClose}}
|
|
@onShow={{this.onEmojiPickerShow}}
|
|
class="chat-message-reaction"
|
|
/>
|
|
{{/if}}
|
|
</div>
|
|
{{/if}}
|
|
</ChatMessageText>
|
|
|
|
{{#if this.shouldRenderStopMessageStreamingButton}}
|
|
<div class="stop-streaming-btn-container">
|
|
<DButton
|
|
class="stop-streaming-btn"
|
|
@icon="circle-stop"
|
|
@label="cancel"
|
|
@action={{fn this.stopMessageStreaming @message}}
|
|
/>
|
|
|
|
</div>
|
|
{{/if}}
|
|
|
|
<ChatMessageBlocks @message={{@message}} />
|
|
|
|
<ChatMessageError
|
|
@message={{@message}}
|
|
@onRetry={{@resendStagedMessage}}
|
|
/>
|
|
</div>
|
|
|
|
{{#if this.showThreadIndicator}}
|
|
<ChatMessageThreadIndicator @message={{@message}} />
|
|
{{/if}}
|
|
</div>
|
|
{{/if}}
|
|
{{/if}}
|
|
</div>
|
|
{{/if}}
|
|
</template>
|
|
}
|