mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 18:51:08 +08:00
DEV: Refactoring chat message actions for ChatMessage component usage in thread panel (#20756)
This commit is a major overhaul of how chat message actions work, to make it so they are reusable between the main chat channel and the chat thread panel, as well as many improvements and fixes for the thread panel. There are now several new classes and concepts: * ChatMessageInteractor - This is initialized from the ChatMessage, ChatMessageActionsDesktop, and ChatMessageActionsMobile components. This handles permissions about what actions can be done for each message based on the context (thread or channel), handles the actions themselves (e.g. copyLink, delete, edit), and interacts with the pane of the current context to modify the UI * ChatChannelThreadPane and ChatChannelPane services - This represents the UI context which contains the messages, and are mostly used for state management for things like message selection. * ChatChannelThreadComposer and ChatChannelComposer - This handles interaction between the pane, the message actions, and the composer, dealing with reply and edit message state. * Scrolling logic for the messages has now been moved to a helper so it can be shared between the main channel pane and the thread pane * Various improvements with the emoji picker on both mobile and desktop. The DOM node of each component is now located outside of the message which prevents a large range of issues. The thread panel now also works in the chat drawer, and the thread messages have less actions than the main panel, since some do not make sense there (e.g. moving messages to a different channel). The thread panel title, excerpt, and message sender have also been removed for now to save space. This gives us a solid base to keep expanding on and fixing up threads. Subsequent PRs will make the thread MessageBus subscriptions work and disable echo mode for the initial release of threads. Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
@ -0,0 +1,7 @@
|
|||||||
|
<ChatEmojiPicker
|
||||||
|
@context="chat-channel-message"
|
||||||
|
@didInsert={{this.didInsert}}
|
||||||
|
@willDestroy={{this.willDestroy}}
|
||||||
|
@didSelectEmoji={{this.didSelectEmoji}}
|
||||||
|
@class="hidden"
|
||||||
|
/>
|
@ -0,0 +1,51 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { headerOffset } from "discourse/lib/offset-calculator";
|
||||||
|
import { createPopper } from "@popperjs/core";
|
||||||
|
|
||||||
|
export default class ChatChannelMessageEmojiPicker extends Component {
|
||||||
|
@service site;
|
||||||
|
@service chatEmojiPickerManager;
|
||||||
|
|
||||||
|
context = "chat-channel-message";
|
||||||
|
|
||||||
|
@action
|
||||||
|
didSelectEmoji(emoji) {
|
||||||
|
this.chatEmojiPickerManager.picker?.didSelectEmoji(emoji);
|
||||||
|
this.chatEmojiPickerManager.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
didInsert(element) {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
element.classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._popper = createPopper(
|
||||||
|
this.chatEmojiPickerManager.picker?.trigger,
|
||||||
|
element,
|
||||||
|
{
|
||||||
|
placement: "top",
|
||||||
|
modifiers: [
|
||||||
|
{
|
||||||
|
name: "eventListeners",
|
||||||
|
options: { scroll: false, resize: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flip",
|
||||||
|
options: { padding: { top: headerOffset() } },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
element.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
willDestroy() {
|
||||||
|
this._popper?.destroy();
|
||||||
|
}
|
||||||
|
}
|
@ -1,26 +1,32 @@
|
|||||||
{{#if @buttons.length}}
|
{{#if @buttons.length}}
|
||||||
<DPopover
|
<DButton
|
||||||
@class="chat-composer-dropdown"
|
@disabled={{@isDisabled}}
|
||||||
@options={{hash arrow=null}}
|
@class="chat-composer-dropdown__trigger-btn btn-flat btn-icon"
|
||||||
as |state|
|
@title="chat.composer.toggle_toolbar"
|
||||||
>
|
@icon={{if @hasActivePanel "times" "plus"}}
|
||||||
<FlatButton
|
@action={{this.toggleExpand}}
|
||||||
@disabled={{@isDisabled}}
|
{{did-insert this.setupTrigger}}
|
||||||
@class="chat-composer-dropdown__trigger-btn d-popover-trigger"
|
/>
|
||||||
@title="chat.composer.toggle_toolbar"
|
|
||||||
@icon={{if state.isExpanded "times" "plus"}}
|
{{#if this.isExpanded}}
|
||||||
/>
|
<ul
|
||||||
<ul class="chat-composer-dropdown__list">
|
class="chat-composer-dropdown__list"
|
||||||
|
{{did-insert this.setupPanel}}
|
||||||
|
{{will-destroy this.teardownPanel}}
|
||||||
|
>
|
||||||
{{#each @buttons as |button|}}
|
{{#each @buttons as |button|}}
|
||||||
<li class="chat-composer-dropdown__item {{button.id}}">
|
<li class={{concat-class "chat-composer-dropdown__item" button.id}}>
|
||||||
<DButton
|
<DButton
|
||||||
@class={{concat "chat-composer-dropdown__action-btn " button.id}}
|
@class={{concat-class
|
||||||
|
"chat-composer-dropdown__action-btn"
|
||||||
|
button.id
|
||||||
|
}}
|
||||||
@icon={{button.icon}}
|
@icon={{button.icon}}
|
||||||
@action={{button.action}}
|
@action={{(fn this.onButtonClick button)}}
|
||||||
@label={{button.label}}
|
@label={{button.label}}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
||||||
</DPopover>
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
@ -0,0 +1,63 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||||
|
import tippy from "tippy.js";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { hideOnEscapePlugin } from "discourse/lib/d-popover";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
|
export default class ChatComposerDropdown extends Component {
|
||||||
|
@tracked isExpanded = false;
|
||||||
|
|
||||||
|
trigger = null;
|
||||||
|
|
||||||
|
@action
|
||||||
|
setupTrigger(element) {
|
||||||
|
this.trigger = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleExpand() {
|
||||||
|
if (this.args.hasActivePanel) {
|
||||||
|
this.args.onCloseActivePanel?.();
|
||||||
|
} else {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onButtonClick(button) {
|
||||||
|
this._tippyInstance.hide();
|
||||||
|
button.action();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setupPanel(element) {
|
||||||
|
this._tippyInstance = tippy(this.trigger, {
|
||||||
|
theme: "chat-composer-drodown",
|
||||||
|
trigger: "click",
|
||||||
|
zIndex: 1400,
|
||||||
|
arrow: iconHTML("tippy-rounded-arrow"),
|
||||||
|
interactive: true,
|
||||||
|
allowHTML: false,
|
||||||
|
appendTo: "parent",
|
||||||
|
hideOnClick: true,
|
||||||
|
plugins: [hideOnEscapePlugin],
|
||||||
|
content: element,
|
||||||
|
onShow: () => {
|
||||||
|
this.isExpanded = true;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onHide: () => {
|
||||||
|
this.isExpanded = false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this._tippyInstance.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
teardownPanel() {
|
||||||
|
this._tippyInstance?.destroy();
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +1,35 @@
|
|||||||
{{#if this.replyToMsg}}
|
{{#if this.composerService.replyToMsg}}
|
||||||
<ChatComposerMessageDetails
|
<ChatComposerMessageDetails
|
||||||
@message={{this.replyToMsg}}
|
@message={{this.composerService.replyToMsg}}
|
||||||
@icon="reply"
|
@icon="reply"
|
||||||
@action={{action "cancelReplyTo"}}
|
@action={{action "cancelReplyTo"}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.editingMessage}}
|
{{#if this.composerService.editingMessage}}
|
||||||
<ChatComposerMessageDetails
|
<ChatComposerMessageDetails
|
||||||
@message={{this.editingMessage}}
|
@message={{this.composerService.editingMessage}}
|
||||||
@icon="pencil-alt"
|
@icon="pencil-alt"
|
||||||
@action={{action "cancelEditing"}}
|
@action={{action "cancelEditing"}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div class="chat-composer-emoji-picker-anchor"></div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
role="region"
|
role="region"
|
||||||
aria-label={{i18n "chat.aria_roles.composer"}}
|
aria-label={{i18n "chat.aria_roles.composer"}}
|
||||||
class="chat-composer {{if this.disableComposer 'is-disabled'}}"
|
class="chat-composer {{if this.disableComposer 'is-disabled'}}"
|
||||||
|
{{did-update this.updateEditingMessage this.composerService.editingMessage}}
|
||||||
>
|
>
|
||||||
{{#if
|
|
||||||
(and
|
<ChatComposerDropdown
|
||||||
this.chatEmojiPickerManager.opened
|
@buttons={{this.dropdownButtons}}
|
||||||
(eq this.chatEmojiPickerManager.context "chat-composer")
|
@isDisabled={{this.disableComposer}}
|
||||||
)
|
@hasActivePanel={{and
|
||||||
}}
|
this.chatEmojiPickerManager.picker
|
||||||
<DButton
|
(eq this.chatEmojiPickerManager.picker.context @context)
|
||||||
@icon="times"
|
}}
|
||||||
@action={{this.chatEmojiPickerManager.close}}
|
@onCloseActivePanel={{this.chatEmojiPickerManager.close}}
|
||||||
@class="chat-composer__close-emoji-picker-btn btn-flat"
|
/>
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
{{#unless this.disableComposer}}
|
|
||||||
<ChatComposerDropdown
|
|
||||||
@buttons={{this.dropdownButtons}}
|
|
||||||
@isDisabled={{this.disableComposer}}
|
|
||||||
/>
|
|
||||||
{{/unless}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<DTextarea
|
<DTextarea
|
||||||
@value={{readonly this.value}}
|
@value={{readonly this.value}}
|
||||||
@ -82,7 +72,7 @@
|
|||||||
@onUploadChanged={{this.uploadsChanged}}
|
@onUploadChanged={{this.uploadsChanged}}
|
||||||
@existingUploads={{or
|
@existingUploads={{or
|
||||||
this.chatChannel.draft.uploads
|
this.chatChannel.draft.uploads
|
||||||
this.editingMessage.uploads
|
this.composerService.editingMessage.uploads
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
@ -92,3 +82,8 @@
|
|||||||
<ChatReplyingIndicator @chatChannel={{this.chatChannel}} />
|
<ChatReplyingIndicator @chatChannel={{this.chatChannel}} />
|
||||||
</div>
|
</div>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
|
<ChatEmojiPicker
|
||||||
|
@context={{@context}}
|
||||||
|
@didSelectEmoji={{this.didSelectEmoji}}
|
||||||
|
/>
|
@ -15,7 +15,7 @@ import { findRawTemplate } from "discourse-common/lib/raw-templates";
|
|||||||
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||||
import { emojiUrlFor } from "discourse/lib/text";
|
import { emojiUrlFor } from "discourse/lib/text";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { readOnly, reads } from "@ember/object/computed";
|
import { reads } from "@ember/object/computed";
|
||||||
import { SKIP } from "discourse/lib/autocomplete";
|
import { SKIP } from "discourse/lib/autocomplete";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
@ -32,12 +32,9 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
chat: service(),
|
chat: service(),
|
||||||
classNames: ["chat-composer-container"],
|
classNames: ["chat-composer-container"],
|
||||||
classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
|
classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
|
||||||
userSilenced: readOnly("chatChannel.userSilenced"),
|
|
||||||
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
|
chatEmojiReactionStore: service("chat-emoji-reaction-store"),
|
||||||
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
|
chatEmojiPickerManager: service("chat-emoji-picker-manager"),
|
||||||
chatStateManager: service("chat-state-manager"),
|
chatStateManager: service("chat-state-manager"),
|
||||||
editingMessage: null,
|
|
||||||
onValueChange: null,
|
|
||||||
timer: null,
|
timer: null,
|
||||||
value: "",
|
value: "",
|
||||||
inProgressUploads: null,
|
inProgressUploads: null,
|
||||||
@ -50,12 +47,12 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
@discourseComputed(...chatComposerButtonsDependentKeys())
|
@discourseComputed(...chatComposerButtonsDependentKeys())
|
||||||
inlineButtons() {
|
inlineButtons() {
|
||||||
return chatComposerButtons(this, "inline");
|
return chatComposerButtons(this, "inline", this.context);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed(...chatComposerButtonsDependentKeys())
|
@discourseComputed(...chatComposerButtonsDependentKeys())
|
||||||
dropdownButtons() {
|
dropdownButtons() {
|
||||||
return chatComposerButtons(this, "dropdown");
|
return chatComposerButtons(this, "dropdown", this.context);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("chatEmojiPickerManager.{opened,context}")
|
@discourseComputed("chatEmojiPickerManager.{opened,context}")
|
||||||
@ -71,7 +68,6 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
this.appEvents.on("chat-composer:reply-to-set", this, "_replyToMsgChanged");
|
|
||||||
this.appEvents.on(
|
this.appEvents.on(
|
||||||
"upload-mixin:chat-composer-uploader:in-progress-uploads",
|
"upload-mixin:chat-composer-uploader:in-progress-uploads",
|
||||||
this,
|
this,
|
||||||
@ -82,6 +78,10 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
inProgressUploads: [],
|
inProgressUploads: [],
|
||||||
_uploads: [],
|
_uploads: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.composerService?.registerFocusHandler(() => {
|
||||||
|
this._focusTextArea();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
@ -92,7 +92,6 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
this._applyUserAutocomplete(this._$textarea);
|
this._applyUserAutocomplete(this._$textarea);
|
||||||
this._applyCategoryHashtagAutocomplete(this._$textarea);
|
this._applyCategoryHashtagAutocomplete(this._$textarea);
|
||||||
this._applyEmojiAutocomplete(this._$textarea);
|
this._applyEmojiAutocomplete(this._$textarea);
|
||||||
this.appEvents.on("chat:focus-composer", this, "_focusTextArea");
|
|
||||||
this.appEvents.on("chat:insert-text", this, "insertText");
|
this.appEvents.on("chat:insert-text", this, "insertText");
|
||||||
this._focusTextArea();
|
this._focusTextArea();
|
||||||
|
|
||||||
@ -134,11 +133,6 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
this.appEvents.off(
|
|
||||||
"chat-composer:reply-to-set",
|
|
||||||
this,
|
|
||||||
"_replyToMsgChanged"
|
|
||||||
);
|
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
"upload-mixin:chat-composer-uploader:in-progress-uploads",
|
"upload-mixin:chat-composer-uploader:in-progress-uploads",
|
||||||
this,
|
this,
|
||||||
@ -147,7 +141,6 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
cancel(this.timer);
|
cancel(this.timer);
|
||||||
|
|
||||||
this.appEvents.off("chat:focus-composer", this, "_focusTextArea");
|
|
||||||
this.appEvents.off("chat:insert-text", this, "insertText");
|
this.appEvents.off("chat:insert-text", this, "insertText");
|
||||||
this.appEvents.off("chat:modify-selection", this, "_modifySelection");
|
this.appEvents.off("chat:modify-selection", this, "_modifySelection");
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
@ -192,19 +185,19 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
if (
|
if (
|
||||||
event.key === "ArrowUp" &&
|
event.key === "ArrowUp" &&
|
||||||
this._messageIsEmpty() &&
|
this._messageIsEmpty() &&
|
||||||
!this.editingMessage
|
!this.composerService?.editingMessage
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.onEditLastMessageRequested();
|
this.paneService?.editLastMessageRequested();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
// keyCode for 'Escape'
|
// keyCode for 'Escape'
|
||||||
if (this.replyToMsg) {
|
if (this.composerService?.replyToMsg) {
|
||||||
this.set("value", "");
|
this.set("value", "");
|
||||||
this._replyToMsgChanged(null);
|
this.composerService?.setReplyTo(null);
|
||||||
return false;
|
return false;
|
||||||
} else if (this.editingMessage) {
|
} else if (this.composerService?.editingMessage) {
|
||||||
this.set("value", "");
|
this.set("value", "");
|
||||||
this.cancelEditing();
|
this.cancelEditing();
|
||||||
return false;
|
return false;
|
||||||
@ -218,34 +211,36 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!this.editingMessage &&
|
!this.composerService?.editingMessage &&
|
||||||
this.chatChannel?.draft &&
|
this.chatChannel?.draft &&
|
||||||
this.chatChannel?.canModifyMessages(this.currentUser)
|
this.chatChannel?.canModifyMessages(this.currentUser)
|
||||||
) {
|
) {
|
||||||
// uses uploads from draft here...
|
// uses uploads from draft here...
|
||||||
this.setProperties({
|
this.set("value", this.chatChannel.draft.message);
|
||||||
value: this.chatChannel.draft.message,
|
this.composerService?.setReplyTo(this.chatChannel.draft.replyToMsg);
|
||||||
replyToMsg: this.chatChannel.draft.replyToMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._captureMentions();
|
this._captureMentions();
|
||||||
this._syncUploads(this.chatChannel.draft.uploads);
|
this._syncUploads(this.chatChannel.draft.uploads);
|
||||||
this.setInReplyToMsg(this.chatChannel.draft.replyToMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.editingMessage && !this.loading) {
|
|
||||||
this.setProperties({
|
|
||||||
replyToMsg: null,
|
|
||||||
value: this.editingMessage.message,
|
|
||||||
});
|
|
||||||
|
|
||||||
this._syncUploads(this.editingMessage.uploads);
|
|
||||||
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: false });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.resizeTextarea();
|
this.resizeTextarea();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
updateEditingMessage() {
|
||||||
|
if (
|
||||||
|
this.composerService?.editingMessage &&
|
||||||
|
!this.paneService?.sendingLoading
|
||||||
|
) {
|
||||||
|
this.set("value", this.composerService?.editingMessage.message);
|
||||||
|
|
||||||
|
this.composerService?.setReplyTo(null);
|
||||||
|
|
||||||
|
this._syncUploads(this.composerService?.editingMessage.uploads);
|
||||||
|
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: false });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// the chat-composer needs to be able to set the internal list of uploads
|
// the chat-composer needs to be able to set the internal list of uploads
|
||||||
// for chat-composer-uploads to preload in existing uploads for drafts
|
// for chat-composer-uploads to preload in existing uploads for drafts
|
||||||
// and for when messages are being edited.
|
// and for when messages are being edited.
|
||||||
@ -281,11 +276,6 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_replyToMsgChanged(replyToMsg) {
|
|
||||||
this.set("replyToMsg", replyToMsg);
|
|
||||||
this.onValueChange?.({ replyToMsg });
|
|
||||||
},
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onTextareaInput(value) {
|
onTextareaInput(value) {
|
||||||
this.set("value", value);
|
this.set("value", value);
|
||||||
@ -299,7 +289,7 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
@bind
|
@bind
|
||||||
_handleTextareaInput() {
|
_handleTextareaInput() {
|
||||||
this.onValueChange?.({ value: this.value });
|
this.composerService?.onComposerValueChange?.({ value: this.value });
|
||||||
},
|
},
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
@ -324,6 +314,18 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
const code = `:${emoji}:`;
|
const code = `:${emoji}:`;
|
||||||
this.chatEmojiReactionStore.track(code);
|
this.chatEmojiReactionStore.track(code);
|
||||||
this.addText(this.getSelected(), code);
|
this.addText(this.getSelected(), code);
|
||||||
|
|
||||||
|
if (this.site.desktopView) {
|
||||||
|
this._focusTextArea();
|
||||||
|
} else {
|
||||||
|
this.chatEmojiPickerManager.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
closeComposerDropdown() {
|
||||||
|
this.chatEmojiPickerManager.close();
|
||||||
|
this.appEvents.trigger("d-popover:close");
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -420,8 +422,9 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
return `${v.code}:`;
|
return `${v.code}:`;
|
||||||
} else {
|
} else {
|
||||||
$textarea.autocomplete({ cancel: true });
|
$textarea.autocomplete({ cancel: true });
|
||||||
this.chatEmojiPickerManager.startFromComposer(this.emojiSelected, {
|
this.chatEmojiPickerManager.open({
|
||||||
filter: v.term,
|
context: this.context,
|
||||||
|
initialFilter: v.term,
|
||||||
});
|
});
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@ -551,18 +554,21 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"chatChannel.{id,chatable.users.[]}",
|
"chatChannel.{id,chatable.users.[]}",
|
||||||
"canInteractWithChat"
|
"chat.userCanInteractWithChat"
|
||||||
)
|
)
|
||||||
disableComposer(channel, canInteractWithChat) {
|
disableComposer(channel, userCanInteractWithChat) {
|
||||||
return (
|
return (
|
||||||
(channel.isDraft && isEmpty(channel?.chatable?.users)) ||
|
(channel.isDraft && isEmpty(channel?.chatable?.users)) ||
|
||||||
!canInteractWithChat ||
|
!userCanInteractWithChat ||
|
||||||
!channel.canModifyMessages(this.currentUser)
|
!channel.canModifyMessages(this.currentUser)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@discourseComputed("userSilenced", "chatChannel.{chatable.users.[],id}")
|
@discourseComputed(
|
||||||
placeholder(userSilenced, chatChannel) {
|
"chatChannel.{chatable.users.[],id}",
|
||||||
|
"chat.userCanInteractWithChat"
|
||||||
|
)
|
||||||
|
placeholder(chatChannel, userCanInteractWithChat) {
|
||||||
if (!chatChannel.canModifyMessages(this.currentUser)) {
|
if (!chatChannel.canModifyMessages(this.currentUser)) {
|
||||||
return I18n.t(
|
return I18n.t(
|
||||||
`chat.placeholder_new_message_disallowed.${chatChannel.status}`
|
`chat.placeholder_new_message_disallowed.${chatChannel.status}`
|
||||||
@ -581,7 +587,7 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userSilenced) {
|
if (!userCanInteractWithChat) {
|
||||||
return I18n.t("chat.placeholder_silenced");
|
return I18n.t("chat.placeholder_silenced");
|
||||||
} else {
|
} else {
|
||||||
return this.messageRecipient(chatChannel);
|
return this.messageRecipient(chatChannel);
|
||||||
@ -612,7 +618,7 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
|
|
||||||
@discourseComputed(
|
@discourseComputed(
|
||||||
"value",
|
"value",
|
||||||
"loading",
|
"paneService.sendingLoading",
|
||||||
"disableComposer",
|
"disableComposer",
|
||||||
"inProgressUploads.[]"
|
"inProgressUploads.[]"
|
||||||
)
|
)
|
||||||
@ -636,23 +642,30 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.editingMessage
|
this.composerService?.editingMessage
|
||||||
? this.internalEditMessage()
|
? this.internalEditMessage()
|
||||||
: this.internalSendMessage();
|
: this.internalSendMessage();
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
internalSendMessage() {
|
internalSendMessage() {
|
||||||
return this.sendMessage(this.value, this._uploads).then(this.reset);
|
// FIXME: This is fairly hacky, we should have a nicer
|
||||||
|
// flow and relationship between the panes for resetting
|
||||||
|
// the value here on send.
|
||||||
|
const _previousValue = this.value;
|
||||||
|
this.set("value", "");
|
||||||
|
return this.sendMessage(_previousValue, this._uploads)
|
||||||
|
.then(this.reset)
|
||||||
|
.catch(() => {
|
||||||
|
this.set("value", _previousValue);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
internalEditMessage() {
|
internalEditMessage() {
|
||||||
return this.editMessage(
|
return this.paneService
|
||||||
this.editingMessage,
|
?.editMessage(this.value, this._uploads)
|
||||||
this.value,
|
.then(this.reset);
|
||||||
this._uploads
|
|
||||||
).then(this.reset);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_messageIsValid() {
|
_messageIsValid() {
|
||||||
@ -691,19 +704,21 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
this._captureMentions();
|
this._captureMentions();
|
||||||
this._syncUploads([]);
|
this._syncUploads([]);
|
||||||
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
|
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
|
||||||
this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
|
this.composerService?.onComposerValueChange?.(
|
||||||
|
this.value,
|
||||||
|
this._uploads,
|
||||||
|
this.composerService?.replyToMsg
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
cancelReplyTo() {
|
cancelReplyTo() {
|
||||||
this.set("replyToMsg", null);
|
this.composerService?.setReplyTo(null);
|
||||||
this.setInReplyToMsg(null);
|
|
||||||
this.onValueChange?.({ replyToMsg: null });
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
cancelEditing() {
|
cancelEditing() {
|
||||||
this.onCancelEditing();
|
this.composerService?.cancelEditing();
|
||||||
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
|
this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -721,7 +736,10 @@ export default Component.extend(TextareaTextManipulation, {
|
|||||||
@action
|
@action
|
||||||
uploadsChanged(uploads, { inProgressUploadsCount }) {
|
uploadsChanged(uploads, { inProgressUploadsCount }) {
|
||||||
this.set("_uploads", cloneJSON(uploads));
|
this.set("_uploads", cloneJSON(uploads));
|
||||||
this.onValueChange?.({ uploads: this._uploads, inProgressUploadsCount });
|
this.composerService?.onComposerValueChange?.({
|
||||||
|
uploads: this._uploads,
|
||||||
|
inProgressUploadsCount,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{{#if this.chatStateManager.isDrawerActive}}
|
{{#if this.chatStateManager.isDrawerActive}}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-chat-channel-id={{this.chat.activeChannel.id}}
|
data-chat-channel-id={{this.chat.activeChannel.id}}
|
||||||
|
data-chat-thread-id={{this.chat.activeChannel.activeThread.id}}
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-drawer"
|
"chat-drawer"
|
||||||
(if this.chatStateManager.isDrawerExpanded "is-expanded")
|
(if this.chatStateManager.isDrawerExpanded "is-expanded")
|
||||||
|
@ -22,6 +22,7 @@ export default Component.extend({
|
|||||||
|
|
||||||
didInsertElement() {
|
didInsertElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (!this.chat.userCanChat) {
|
if (!this.chat.userCanChat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export default Component.extend({
|
|||||||
|
|
||||||
willDestroyElement() {
|
willDestroyElement() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
if (!this.chat.userCanChat) {
|
if (!this.chat.userCanChat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<ChatDrawer::Header>
|
||||||
|
<ChatDrawer::Header::LeftActions />
|
||||||
|
|
||||||
|
<ChatDrawer::Header::ChannelTitle
|
||||||
|
@channel={{this.chat.activeChannel}}
|
||||||
|
@drawerActions={{@drawerActions}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ChatDrawer::Header::RightActions @drawerActions={{@drawerActions}} />
|
||||||
|
</ChatDrawer::Header>
|
||||||
|
|
||||||
|
{{#if this.chatStateManager.isDrawerExpanded}}
|
||||||
|
<div
|
||||||
|
class="chat-drawer-content"
|
||||||
|
{{did-insert this.fetchChannelAndThread}}
|
||||||
|
{{did-update this.fetchChannelAndThread @params.channelId}}
|
||||||
|
{{did-update this.fetchChannelAndThread @params.threadId}}
|
||||||
|
>
|
||||||
|
{{#if this.chat.activeChannel.activeThread}}
|
||||||
|
<ChatThread />
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
@ -0,0 +1,29 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
|
export default class ChatDrawerThread extends Component {
|
||||||
|
@service appEvents;
|
||||||
|
@service chat;
|
||||||
|
@service chatStateManager;
|
||||||
|
@service chatChannelsManager;
|
||||||
|
|
||||||
|
@action
|
||||||
|
fetchChannelAndThread() {
|
||||||
|
if (!this.args.params?.channelId || !this.args.params?.threadId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chatChannelsManager
|
||||||
|
.find(this.args.params.channelId)
|
||||||
|
.then((channel) => {
|
||||||
|
this.chat.activeChannel = channel;
|
||||||
|
|
||||||
|
channel.threadsManager
|
||||||
|
.find(channel.id, this.args.params.threadId)
|
||||||
|
.then((thread) => {
|
||||||
|
this.chat.activeChannel.activeThread = thread;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,175 +1,137 @@
|
|||||||
{{! template-lint-disable no-invalid-interactive }}
|
{{! template-lint-disable no-invalid-interactive }}
|
||||||
{{! template-lint-disable no-nested-interactive }}
|
{{! template-lint-disable no-nested-interactive }}
|
||||||
{{! template-lint-disable no-down-event-binding }}
|
{{! template-lint-disable no-down-event-binding }}
|
||||||
<div
|
|
||||||
class={{concat-class
|
{{#if (eq this.chatEmojiPickerManager.picker.context @context)}}
|
||||||
"chat-emoji-picker"
|
<div
|
||||||
(if this.chatEmojiPickerManager.closing "closing")
|
class={{concat-class
|
||||||
}}
|
"chat-emoji-picker"
|
||||||
{{did-insert this.addClickOutsideEventListener}}
|
@class
|
||||||
{{will-destroy this.removeClickOutsideEventListener}}
|
(if this.chatEmojiPickerManager.closing "closing")
|
||||||
{{on "keydown" this.trapKeyDownEvents}}
|
}}
|
||||||
>
|
{{did-insert this.addClickOutsideEventListener}}
|
||||||
<div class="chat-emoji-picker__filter-container">
|
{{did-insert this.chatEmojiPickerManager.loadEmojis}}
|
||||||
<DcFilterInput
|
{{did-insert (if @didInsert @didInsert (noop))}}
|
||||||
@class="chat-emoji-picker__filter"
|
{{will-destroy (if @willDestroy @willDestroy (noop))}}
|
||||||
@value={{this.chatEmojiPickerManager.initialFilter}}
|
{{will-destroy this.removeClickOutsideEventListener}}
|
||||||
@filterAction={{action this.didInputFilter value="target.value"}}
|
{{on "keydown" this.trapKeyDownEvents}}
|
||||||
@icons={{hash left="search"}}
|
>
|
||||||
placeholder={{i18n "chat.emoji_picker.search_placeholder"}}
|
<div class="chat-emoji-picker__filter-container">
|
||||||
autofocus={{true}}
|
<DcFilterInput
|
||||||
{{did-insert this.focusFilter}}
|
@class="chat-emoji-picker__filter"
|
||||||
{{did-insert
|
@value={{this.chatEmojiPickerManager.picker.initialFilter}}
|
||||||
(fn this.didInputFilter this.chatEmojiPickerManager.initialFilter)
|
@filterAction={{action this.didInputFilter value="target.value"}}
|
||||||
}}
|
@icons={{hash left="search"}}
|
||||||
>
|
placeholder={{i18n "chat.emoji_picker.search_placeholder"}}
|
||||||
<div
|
autofocus={{true}}
|
||||||
class="chat-emoji-picker__fitzpatrick-scale"
|
{{did-insert (if this.site.desktopView this.focusFilter (noop))}}
|
||||||
role="toolbar"
|
{{did-insert
|
||||||
{{on "keyup" this.didNavigateFitzpatrickScale}}
|
(fn
|
||||||
|
this.didInputFilter this.chatEmojiPickerManager.picker.initialFilter
|
||||||
|
)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{{#if this.isExpandedFitzpatrickScale}}
|
|
||||||
{{#each this.fitzpatrickModifiers as |fitzpatrick|}}
|
|
||||||
|
|
||||||
{{#if
|
|
||||||
(not (eq fitzpatrick.scale this.chatEmojiReactionStore.diversity))
|
|
||||||
}}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={{concat "t" fitzpatrick.scale}}
|
|
||||||
tabindex="-1"
|
|
||||||
class={{concat-class
|
|
||||||
"chat-emoji-picker__fitzpatrick-modifier-btn"
|
|
||||||
(concat "t" fitzpatrick.scale)
|
|
||||||
}}
|
|
||||||
{{on
|
|
||||||
"keyup"
|
|
||||||
(fn this.didRequestFitzpatrickScale fitzpatrick.scale)
|
|
||||||
}}
|
|
||||||
{{on
|
|
||||||
"click"
|
|
||||||
(fn this.didRequestFitzpatrickScale fitzpatrick.scale)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{{d-icon "check"}}
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
title={{concat "t" this.fitzpatrick.scale}}
|
|
||||||
class={{concat-class
|
|
||||||
"chat-emoji-picker__fitzpatrick-modifier-btn current"
|
|
||||||
(concat "t" this.chatEmojiReactionStore.diversity)
|
|
||||||
}}
|
|
||||||
{{on "keyup" this.didToggleFitzpatrickScale}}
|
|
||||||
{{on "click" this.didToggleFitzpatrickScale}}
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
</DcFilterInput>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if this.chatEmojiPickerManager.sections.length}}
|
|
||||||
{{#if (not (gte this.filteredEmojis.length 0))}}
|
|
||||||
<div class="chat-emoji-picker__sections-nav">
|
|
||||||
<div
|
<div
|
||||||
class="chat-emoji-picker__sections-nav__indicator"
|
class="chat-emoji-picker__fitzpatrick-scale"
|
||||||
style={{this.navIndicatorStyle}}
|
role="toolbar"
|
||||||
></div>
|
{{on "keyup" this.didNavigateFitzpatrickScale}}
|
||||||
|
>
|
||||||
|
{{#if this.isExpandedFitzpatrickScale}}
|
||||||
|
{{#each this.fitzpatrickModifiers as |fitzpatrick|}}
|
||||||
|
|
||||||
{{#each-in this.groups as |section emojis|}}
|
{{#if
|
||||||
<DButton
|
(not
|
||||||
class={{concat-class
|
(eq fitzpatrick.scale this.chatEmojiReactionStore.diversity)
|
||||||
"btn-flat"
|
)
|
||||||
"chat-emoji-picker__section-btn"
|
|
||||||
(if
|
|
||||||
(eq this.chatEmojiPickerManager.lastVisibleSection section)
|
|
||||||
"active"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
tabindex="-1"
|
|
||||||
style={{this.navBtnStyle}}
|
|
||||||
@action={{fn this.didRequestSection section}}
|
|
||||||
data-section={{section}}
|
|
||||||
>
|
|
||||||
{{#if (eq section "favorites")}}
|
|
||||||
{{replace-emoji ":star:"}}
|
|
||||||
{{else}}
|
|
||||||
<img
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
class="emoji"
|
|
||||||
src={{emojis.firstObject.url}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</DButton>
|
|
||||||
{{/each-in}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="chat-emoji-picker__scrollable-content"
|
|
||||||
{{chat/emoji-picker-scroll-listener}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="chat-emoji-picker__sections"
|
|
||||||
{{on "click" this.didSelectEmoji}}
|
|
||||||
{{on "keydown" this.onSectionsKeyDown}}
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{{#if (gte this.filteredEmojis.length 0)}}
|
|
||||||
<div class="chat-emoji-picker__section filtered">
|
|
||||||
{{#each this.filteredEmojis as |emoji|}}
|
|
||||||
<img
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
class="emoji"
|
|
||||||
src={{tonable-emoji-url
|
|
||||||
emoji
|
|
||||||
this.chatEmojiReactionStore.diversity
|
|
||||||
}}
|
|
||||||
tabindex="0"
|
|
||||||
data-emoji={{emoji.name}}
|
|
||||||
data-tonable={{if emoji.tonable "true"}}
|
|
||||||
alt={{emoji.name}}
|
|
||||||
title={{tonable-emoji-title
|
|
||||||
emoji
|
|
||||||
this.chatEmojiReactionStore.diversity
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{{else}}
|
|
||||||
<p class="chat-emoji-picker__no-reults">
|
|
||||||
{{i18n "chat.emoji_picker.no_results"}}
|
|
||||||
</p>
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#each-in this.groups as |section emojis|}}
|
|
||||||
<div
|
|
||||||
class={{concat-class
|
|
||||||
"chat-emoji-picker__section"
|
|
||||||
(if (gte this.filteredEmojis.length 0) "hidden")
|
|
||||||
}}
|
|
||||||
data-section={{section}}
|
|
||||||
role="region"
|
|
||||||
aria-label={{i18n
|
|
||||||
(concat "chat.emoji_picker." section)
|
|
||||||
translatedFallback=section
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2 class="chat-emoji-picker__section-title">
|
|
||||||
{{i18n
|
|
||||||
(concat "chat.emoji_picker." section)
|
|
||||||
translatedFallback=section
|
|
||||||
}}
|
}}
|
||||||
</h2>
|
<button
|
||||||
<div class="chat-emoji-picker__section-emojis">
|
type="button"
|
||||||
{{! we always want the first emoji for tabbing}}
|
title={{concat "t" fitzpatrick.scale}}
|
||||||
{{#let emojis.firstObject as |emoji|}}
|
tabindex="-1"
|
||||||
|
class={{concat-class
|
||||||
|
"chat-emoji-picker__fitzpatrick-modifier-btn"
|
||||||
|
(concat "t" fitzpatrick.scale)
|
||||||
|
}}
|
||||||
|
{{on
|
||||||
|
"keyup"
|
||||||
|
(fn this.didRequestFitzpatrickScale fitzpatrick.scale)
|
||||||
|
}}
|
||||||
|
{{on
|
||||||
|
"click"
|
||||||
|
(fn this.didRequestFitzpatrickScale fitzpatrick.scale)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{{d-icon "check"}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
title={{concat "t" this.fitzpatrick.scale}}
|
||||||
|
class={{concat-class
|
||||||
|
"chat-emoji-picker__fitzpatrick-modifier-btn current"
|
||||||
|
(concat "t" this.chatEmojiReactionStore.diversity)
|
||||||
|
}}
|
||||||
|
{{on "keyup" this.didToggleFitzpatrickScale}}
|
||||||
|
{{on "click" this.didToggleFitzpatrickScale}}
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
</DcFilterInput>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if this.chatEmojiPickerManager.sections.length}}
|
||||||
|
{{#if (not (gte this.filteredEmojis.length 0))}}
|
||||||
|
<div class="chat-emoji-picker__sections-nav">
|
||||||
|
<div
|
||||||
|
class="chat-emoji-picker__sections-nav__indicator"
|
||||||
|
style={{this.navIndicatorStyle}}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{{#each-in this.groups as |section emojis|}}
|
||||||
|
<DButton
|
||||||
|
class={{concat-class
|
||||||
|
"btn-flat"
|
||||||
|
"chat-emoji-picker__section-btn"
|
||||||
|
(if
|
||||||
|
(eq this.chatEmojiPickerManager.lastVisibleSection section)
|
||||||
|
"active"
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
tabindex="-1"
|
||||||
|
style={{this.navBtnStyle}}
|
||||||
|
@action={{fn this.didRequestSection section}}
|
||||||
|
data-section={{section}}
|
||||||
|
>
|
||||||
|
{{#if (eq section "favorites")}}
|
||||||
|
{{replace-emoji ":star:"}}
|
||||||
|
{{else}}
|
||||||
|
<img
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
class="emoji"
|
||||||
|
src={{emojis.firstObject.url}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</DButton>
|
||||||
|
{{/each-in}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="chat-emoji-picker__scrollable-content"
|
||||||
|
{{chat/emoji-picker-scroll-listener}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="chat-emoji-picker__sections"
|
||||||
|
{{on "click" this.didSelectEmoji}}
|
||||||
|
{{on "keydown" this.onSectionsKeyDown}}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
{{#if (gte this.filteredEmojis.length 0)}}
|
||||||
|
<div class="chat-emoji-picker__section filtered">
|
||||||
|
{{#each this.filteredEmojis as |emoji|}}
|
||||||
<img
|
<img
|
||||||
width="32"
|
width="32"
|
||||||
height="32"
|
height="32"
|
||||||
@ -187,53 +149,101 @@
|
|||||||
this.chatEmojiReactionStore.diversity
|
this.chatEmojiReactionStore.diversity
|
||||||
}}
|
}}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
{{on "focus" this.didFocusFirstEmoji}}
|
|
||||||
/>
|
/>
|
||||||
{{/let}}
|
{{else}}
|
||||||
|
<p class="chat-emoji-picker__no-results">
|
||||||
{{#if
|
{{i18n "chat.emoji_picker.no_results"}}
|
||||||
(includes this.chatEmojiPickerManager.visibleSections section)
|
</p>
|
||||||
}}
|
{{/each}}
|
||||||
{{#each emojis as |emoji index|}}
|
|
||||||
{{! first emoji has already been rendered, we don't want to re render or would lose focus}}
|
|
||||||
{{#if (gt index 0)}}
|
|
||||||
<img
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
class="emoji"
|
|
||||||
src={{tonable-emoji-url
|
|
||||||
emoji
|
|
||||||
this.chatEmojiReactionStore.diversity
|
|
||||||
}}
|
|
||||||
tabindex="-1"
|
|
||||||
data-emoji={{emoji.name}}
|
|
||||||
data-tonable={{if emoji.tonable "true"}}
|
|
||||||
alt={{emoji.name}}
|
|
||||||
title={{tonable-emoji-title
|
|
||||||
emoji
|
|
||||||
this.chatEmojiReactionStore.diversity
|
|
||||||
}}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{/if}}
|
||||||
{{/each-in}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div class="spinner medium"></div>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{#if
|
{{#each-in this.groups as |section emojis|}}
|
||||||
(and
|
<div
|
||||||
this.chatEmojiPickerManager.opened
|
class={{concat-class
|
||||||
this.site.mobileView
|
"chat-emoji-picker__section"
|
||||||
(eq this.chatEmojiPickerManager.context "chat-message")
|
(if (gte this.filteredEmojis.length 0) "hidden")
|
||||||
)
|
}}
|
||||||
}}
|
data-section={{section}}
|
||||||
<div class="chat-emoji-picker__backdrop"></div>
|
role="region"
|
||||||
|
aria-label={{i18n
|
||||||
|
(concat "chat.emoji_picker." section)
|
||||||
|
translatedFallback=section
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h2 class="chat-emoji-picker__section-title">
|
||||||
|
{{i18n
|
||||||
|
(concat "chat.emoji_picker." section)
|
||||||
|
translatedFallback=section
|
||||||
|
}}
|
||||||
|
</h2>
|
||||||
|
<div class="chat-emoji-picker__section-emojis">
|
||||||
|
{{! we always want the first emoji for tabbing}}
|
||||||
|
{{#let emojis.firstObject as |emoji|}}
|
||||||
|
<img
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
class="emoji"
|
||||||
|
src={{tonable-emoji-url
|
||||||
|
emoji
|
||||||
|
this.chatEmojiReactionStore.diversity
|
||||||
|
}}
|
||||||
|
tabindex="0"
|
||||||
|
data-emoji={{emoji.name}}
|
||||||
|
data-tonable={{if emoji.tonable "true"}}
|
||||||
|
alt={{emoji.name}}
|
||||||
|
title={{tonable-emoji-title
|
||||||
|
emoji
|
||||||
|
this.chatEmojiReactionStore.diversity
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
{{on "focus" this.didFocusFirstEmoji}}
|
||||||
|
/>
|
||||||
|
{{/let}}
|
||||||
|
|
||||||
|
{{#if
|
||||||
|
(includes this.chatEmojiPickerManager.visibleSections section)
|
||||||
|
}}
|
||||||
|
{{#each emojis as |emoji index|}}
|
||||||
|
{{! first emoji has already been rendered, we don't want to re render or would lose focus}}
|
||||||
|
{{#if (gt index 0)}}
|
||||||
|
<img
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
class="emoji"
|
||||||
|
src={{tonable-emoji-url
|
||||||
|
emoji
|
||||||
|
this.chatEmojiReactionStore.diversity
|
||||||
|
}}
|
||||||
|
tabindex="-1"
|
||||||
|
data-emoji={{emoji.name}}
|
||||||
|
data-tonable={{if emoji.tonable "true"}}
|
||||||
|
alt={{emoji.name}}
|
||||||
|
title={{tonable-emoji-title
|
||||||
|
emoji
|
||||||
|
this.chatEmojiReactionStore.diversity
|
||||||
|
}}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{/each-in}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="spinner medium"></div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if
|
||||||
|
(and
|
||||||
|
this.site.mobileView
|
||||||
|
(eq this.chatEmojiPickerManager.picker.context "chat-channel-message")
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<div class="chat-emoji-picker__backdrop"></div>
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
@ -1,4 +1,4 @@
|
|||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { htmlSafe } from "@ember/template";
|
import { htmlSafe } from "@ember/template";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
@ -40,9 +40,11 @@ export default class ChatEmojiPicker extends Component {
|
|||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
@service emojiPickerScrollObserver;
|
@service emojiPickerScrollObserver;
|
||||||
@service chatEmojiReactionStore;
|
@service chatEmojiReactionStore;
|
||||||
|
@service capabilities;
|
||||||
|
@service site;
|
||||||
|
|
||||||
@tracked filteredEmojis = null;
|
@tracked filteredEmojis = null;
|
||||||
@tracked isExpandedFitzpatrickScale = false;
|
@tracked isExpandedFitzpatrickScale = false;
|
||||||
tagName = "";
|
|
||||||
|
|
||||||
fitzpatrickModifiers = FITZPATRICK_MODIFIERS;
|
fitzpatrickModifiers = FITZPATRICK_MODIFIERS;
|
||||||
|
|
||||||
@ -163,7 +165,7 @@ export default class ChatEmojiPicker extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.toggleProperty("isExpandedFitzpatrickScale");
|
this.isExpandedFitzpatrickScale = !this.isExpandedFitzpatrickScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -210,7 +212,9 @@ export default class ChatEmojiPicker extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
focusFilter(target) {
|
focusFilter(target) {
|
||||||
target.focus();
|
schedule("afterRender", () => {
|
||||||
|
target?.focus();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncedDidInputFilter(filter = "") {
|
debouncedDidInputFilter(filter = "") {
|
||||||
@ -347,8 +351,7 @@ export default class ChatEmojiPicker extends Component {
|
|||||||
emoji = `${emoji}:t${diversity}`;
|
emoji = `${emoji}:t${diversity}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chatEmojiPickerManager.didSelectEmoji(emoji);
|
this.args.didSelectEmoji?.(emoji);
|
||||||
this.appEvents.trigger("chat:focus-composer");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-live-pane"
|
"chat-live-pane"
|
||||||
(if this.loading "loading")
|
(if this.loading "loading")
|
||||||
(if this.sendingLoading "sending-loading")
|
(if this.chatChannelPane.sendingLoading "sending-loading")
|
||||||
(unless this.loadedOnce "not-loaded-once")
|
(unless this.loadedOnce "not-loaded-once")
|
||||||
}}
|
}}
|
||||||
{{did-insert this.setupListeners}}
|
{{did-insert this.setupListeners}}
|
||||||
@ -23,47 +23,26 @@
|
|||||||
|
|
||||||
<ChatMentionWarnings />
|
<ChatMentionWarnings />
|
||||||
|
|
||||||
<div class="chat-message-actions-mobile-anchor"></div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={{concat-class
|
|
||||||
"chat-message-emoji-picker-anchor"
|
|
||||||
(if
|
|
||||||
(and
|
|
||||||
this.chatEmojiPickerManager.opened
|
|
||||||
(eq this.chatEmojiPickerManager.context "chat-message")
|
|
||||||
)
|
|
||||||
"-opened"
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="chat-messages-scroll chat-messages-container"
|
class="chat-messages-scroll chat-messages-container"
|
||||||
{{on "scroll" this.computeScrollState passive=true}}
|
{{on "scroll" this.computeScrollState passive=true}}
|
||||||
{{chat/on-scroll this.resetIdle (hash delay=500)}}
|
{{chat/on-scroll this.resetIdle (hash delay=500)}}
|
||||||
{{chat/on-scroll this.computeArrow (hash delay=150)}}
|
{{chat/on-scroll this.computeArrow (hash delay=150)}}
|
||||||
|
{{did-insert this.setScrollable}}
|
||||||
>
|
>
|
||||||
<div class="chat-message-actions-desktop-anchor"></div>
|
<div
|
||||||
<div class="chat-messages-container" {{chat/on-resize this.didResizePane}}>
|
class="chat-messages-container"
|
||||||
|
{{chat/on-resize this.didResizePane (hash delay=10)}}
|
||||||
|
>
|
||||||
{{#if this.loadedOnce}}
|
{{#if this.loadedOnce}}
|
||||||
{{#each @channel.messages key="id" as |message|}}
|
{{#each @channel.messages key="id" as |message|}}
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
@message={{message}}
|
@message={{message}}
|
||||||
@canInteractWithChat={{this.canInteractWithChat}}
|
|
||||||
@channel={{@channel}}
|
@channel={{@channel}}
|
||||||
@setReplyTo={{this.setReplyTo}}
|
|
||||||
@replyMessageClicked={{this.replyMessageClicked}}
|
|
||||||
@editButtonClicked={{this.editButtonClicked}}
|
|
||||||
@selectingMessages={{this.selectingMessages}}
|
|
||||||
@onStartSelectingMessages={{this.onStartSelectingMessages}}
|
|
||||||
@onSelectMessage={{this.onSelectMessage}}
|
|
||||||
@bulkSelectMessages={{this.bulkSelectMessages}}
|
|
||||||
@isHovered={{eq message.id this.hoveredMessageId}}
|
|
||||||
@onHoverMessage={{this.onHoverMessage}}
|
|
||||||
@resendStagedMessage={{this.resendStagedMessage}}
|
@resendStagedMessage={{this.resendStagedMessage}}
|
||||||
@messageDidEnterViewport={{this.messageDidEnterViewport}}
|
@messageDidEnterViewport={{this.messageDidEnterViewport}}
|
||||||
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
|
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
|
||||||
|
@context="channel"
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{else}}
|
{{else}}
|
||||||
@ -86,26 +65,25 @@
|
|||||||
@channel={{@channel}}
|
@channel={{@channel}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{{#if this.selectingMessages}}
|
{{#if this.chatChannelPane.selectingMessages}}
|
||||||
<ChatSelectionManager
|
<ChatSelectionManager
|
||||||
@selectedMessageIds={{this.selectedMessageIds}}
|
@selectedMessageIds={{this.chatChannelPane.selectedMessageIds}}
|
||||||
@chatChannel={{@channel}}
|
@chatChannel={{@channel}}
|
||||||
@cancelSelecting={{this.cancelSelecting}}
|
@cancelSelecting={{action
|
||||||
|
this.chatChannelPane.cancelSelecting
|
||||||
|
@channel.selectedMessages
|
||||||
|
}}
|
||||||
|
@context="channel"
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
{{#if (or @channel.isDraft @channel.isFollowing)}}
|
||||||
<ChatComposer
|
<ChatComposer
|
||||||
@canInteractWithChat={{this.canInteractWithChat}}
|
|
||||||
@sendMessage={{this.sendMessage}}
|
@sendMessage={{this.sendMessage}}
|
||||||
@editMessage={{this.editMessage}}
|
|
||||||
@setReplyTo={{this.setReplyTo}}
|
|
||||||
@loading={{this.sendingLoading}}
|
|
||||||
@editingMessage={{readonly this.editingMessage}}
|
|
||||||
@onCancelEditing={{this.cancelEditing}}
|
@onCancelEditing={{this.cancelEditing}}
|
||||||
@setInReplyToMsg={{this.setInReplyToMsg}}
|
|
||||||
@onEditLastMessageRequested={{this.editLastMessageRequested}}
|
|
||||||
@onValueChange={{this.composerValueChanged}}
|
|
||||||
@chatChannel={{@channel}}
|
@chatChannel={{@channel}}
|
||||||
|
@composerService={{this.chatChannelComposer}}
|
||||||
|
@paneService={{this.chatChannelPane}}
|
||||||
|
@context="channel"
|
||||||
/>
|
/>
|
||||||
{{else}}
|
{{else}}
|
||||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||||
|
@ -32,6 +32,8 @@ export default class ChatLivePane extends Component {
|
|||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
@service chatComposerPresenceManager;
|
@service chatComposerPresenceManager;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
|
@service chatChannelComposer;
|
||||||
|
@service chatChannelPane;
|
||||||
@service chatApi;
|
@service chatApi;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
@ -41,28 +43,26 @@ export default class ChatLivePane extends Component {
|
|||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
@tracked loadingMorePast = false;
|
@tracked loadingMorePast = false;
|
||||||
@tracked loadingMoreFuture = false;
|
@tracked loadingMoreFuture = false;
|
||||||
@tracked hoveredMessageId = null;
|
|
||||||
@tracked sendingLoading = false;
|
@tracked sendingLoading = false;
|
||||||
@tracked selectingMessages = false;
|
|
||||||
@tracked showChatQuoteSuccess = false;
|
@tracked showChatQuoteSuccess = false;
|
||||||
@tracked includeHeader = true;
|
@tracked includeHeader = true;
|
||||||
@tracked editingMessage = null;
|
|
||||||
@tracked replyToMsg = null;
|
|
||||||
@tracked hasNewMessages = false;
|
@tracked hasNewMessages = false;
|
||||||
@tracked needsArrow = false;
|
@tracked needsArrow = false;
|
||||||
@tracked loadedOnce = false;
|
@tracked loadedOnce = false;
|
||||||
|
|
||||||
|
scrollable = null;
|
||||||
_loadedChannelId = null;
|
_loadedChannelId = null;
|
||||||
_scrollerEl = null;
|
|
||||||
_lastSelectedMessage = null;
|
|
||||||
_mentionWarningsSeen = {};
|
_mentionWarningsSeen = {};
|
||||||
_unreachableGroupMentions = [];
|
_unreachableGroupMentions = [];
|
||||||
_overMembersLimitGroupMentions = [];
|
_overMembersLimitGroupMentions = [];
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setupListeners(element) {
|
setScrollable(element) {
|
||||||
this._scrollerEl = element.querySelector(".chat-messages-scroll");
|
this.scrollable = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setupListeners() {
|
||||||
document.addEventListener("scroll", this._forceBodyScroll, {
|
document.addEventListener("scroll", this._forceBodyScroll, {
|
||||||
passive: true,
|
passive: true,
|
||||||
});
|
});
|
||||||
@ -102,8 +102,8 @@ export default class ChatLivePane extends Component {
|
|||||||
|
|
||||||
if (this._loadedChannelId !== this.args.channel?.id) {
|
if (this._loadedChannelId !== this.args.channel?.id) {
|
||||||
this._unsubscribeToUpdates(this._loadedChannelId);
|
this._unsubscribeToUpdates(this._loadedChannelId);
|
||||||
this.selectingMessages = false;
|
this.chatChannelPane.selectingMessages = false;
|
||||||
this.cancelEditing();
|
this.chatChannelComposer.cancelEditing();
|
||||||
this._loadedChannelId = this.args.channel?.id;
|
this._loadedChannelId = this.args.channel?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,7 +239,8 @@ export default class ChatLivePane extends Component {
|
|||||||
.then((results) => {
|
.then((results) => {
|
||||||
if (
|
if (
|
||||||
this._selfDeleted ||
|
this._selfDeleted ||
|
||||||
this.args.channel.id !== results.meta.channel_id
|
this.args.channel.id !== results.meta.channel_id ||
|
||||||
|
!this.scrollable
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -372,11 +373,15 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
const messageEl = this._scrollerEl.querySelector(
|
if (this._selfDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageEl = this.scrollable.querySelector(
|
||||||
`.chat-message-container[data-id='${messageId}']`
|
`.chat-message-container[data-id='${messageId}']`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!messageEl || this._selfDeleted) {
|
if (!messageEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,13 +433,13 @@ export default class ChatLivePane extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const element = this._scrollerEl.querySelector(
|
const element = this.scrollable.querySelector(
|
||||||
`[data-id='${lastUnreadVisibleMessage.id}']`
|
`[data-id='${lastUnreadVisibleMessage.id}']`
|
||||||
);
|
);
|
||||||
|
|
||||||
// if the last visible message is not fully visible, we don't want to mark it as read
|
// if the last visible message is not fully visible, we don't want to mark it as read
|
||||||
// attempt to mark previous one as read
|
// attempt to mark previous one as read
|
||||||
if (!this.#isBottomOfMessageVisible(element, this._scrollerEl)) {
|
if (!this.#isBottomOfMessageVisible(element, this.scrollable)) {
|
||||||
lastUnreadVisibleMessage = lastUnreadVisibleMessage.previousMessage;
|
lastUnreadVisibleMessage = lastUnreadVisibleMessage.previousMessage;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -449,23 +454,6 @@ export default class ChatLivePane extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
scrollToBottom() {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A more consistent way to scroll to the bottom when we are sure this is our goal
|
|
||||||
// it will also limit issues with any element changing the height while we are scrolling
|
|
||||||
// to the bottom
|
|
||||||
this._scrollerEl.scrollTop = -1;
|
|
||||||
this.forceRendering(() => {
|
|
||||||
this._scrollerEl.scrollTop = 0;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
scrollToLatestMessage() {
|
scrollToLatestMessage() {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
@ -485,13 +473,21 @@ export default class ChatLivePane extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
computeArrow() {
|
computeArrow() {
|
||||||
this.needsArrow = Math.abs(this._scrollerEl.scrollTop) >= 250;
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.needsArrow = Math.abs(this.scrollable.scrollTop) >= 250;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
computeScrollState() {
|
computeScrollState() {
|
||||||
cancel(this.onScrollEndedHandler);
|
cancel(this.onScrollEndedHandler);
|
||||||
|
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.#isAtTop()) {
|
if (this.#isAtTop()) {
|
||||||
this.fetchMoreMessages({ direction: PAST });
|
this.fetchMoreMessages({ direction: PAST });
|
||||||
this.onScrollEnded();
|
this.onScrollEnded();
|
||||||
@ -719,6 +715,8 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO (martin) Maybe change this to public, since its referred to by
|
||||||
|
// livePanel.linkedComponent at the moment.
|
||||||
get _selfDeleted() {
|
get _selfDeleted() {
|
||||||
return this.isDestroying || this.isDestroyed;
|
return this.isDestroying || this.isDestroyed;
|
||||||
}
|
}
|
||||||
@ -731,11 +729,11 @@ export default class ChatLivePane extends Component {
|
|||||||
sendMessage(message, uploads = []) {
|
sendMessage(message, uploads = []) {
|
||||||
resetIdle();
|
resetIdle();
|
||||||
|
|
||||||
if (this.sendingLoading) {
|
if (this.chatChannelPane.sendingLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendingLoading = true;
|
this.chatChannelPane.sendingLoading = true;
|
||||||
this.args.channel.draft = ChatMessageDraft.create();
|
this.args.channel.draft = ChatMessageDraft.create();
|
||||||
|
|
||||||
// TODO: all send message logic is due for massive refactoring
|
// TODO: all send message logic is due for massive refactoring
|
||||||
@ -758,8 +756,8 @@ export default class ChatLivePane extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.sendingLoading = false;
|
this.chatChannelPane.sendingLoading = false;
|
||||||
this._resetAfterSend();
|
this.chatChannelPane.resetAfterSend();
|
||||||
this.scrollToLatestMessage();
|
this.scrollToLatestMessage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -771,8 +769,8 @@ export default class ChatLivePane extends Component {
|
|||||||
user: this.currentUser,
|
user: this.currentUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.replyToMsg) {
|
if (this.chatChannelComposer.replyToMsg) {
|
||||||
stagedMessage.inReplyTo = this.replyToMsg;
|
stagedMessage.inReplyTo = this.chatChannelComposer.replyToMsg;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.args.channel.messagesManager.addMessages([stagedMessage]);
|
this.args.channel.messagesManager.addMessages([stagedMessage]);
|
||||||
@ -797,8 +795,8 @@ export default class ChatLivePane extends Component {
|
|||||||
if (this._selfDeleted) {
|
if (this._selfDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.sendingLoading = false;
|
this.chatChannelPane.sendingLoading = false;
|
||||||
this._resetAfterSend();
|
this.chatChannelPane.resetAfterSend();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -836,12 +834,12 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this._resetAfterSend();
|
this.chatChannelPane.resetAfterSend();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
resendStagedMessage(stagedMessage) {
|
resendStagedMessage(stagedMessage) {
|
||||||
this.sendingLoading = true;
|
this.chatChannelPane.sendingLoading = true;
|
||||||
|
|
||||||
stagedMessage.error = null;
|
stagedMessage.error = null;
|
||||||
|
|
||||||
@ -864,154 +862,14 @@ export default class ChatLivePane extends Component {
|
|||||||
if (this._selfDeleted) {
|
if (this._selfDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.sendingLoading = false;
|
this.chatChannelPane.sendingLoading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
editMessage(chatMessage, newContent, uploads) {
|
|
||||||
this.sendingLoading = true;
|
|
||||||
let data = {
|
|
||||||
new_message: newContent,
|
|
||||||
upload_ids: (uploads || []).map((upload) => upload.id),
|
|
||||||
};
|
|
||||||
return ajax(`/chat/${this.args.channel.id}/edit/${chatMessage.id}`, {
|
|
||||||
type: "PUT",
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this._resetAfterSend();
|
|
||||||
})
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.finally(() => {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.sendingLoading = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_resetAfterSend() {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.replyToMsg = null;
|
|
||||||
this.editingMessage = null;
|
|
||||||
this.chatComposerPresenceManager.notifyState(this.args.channel.id, false);
|
|
||||||
this.appEvents.trigger("chat-composer:reply-to-set", null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
editLastMessageRequested() {
|
|
||||||
const lastUserMessage = this.args.channel.messages.findLast(
|
|
||||||
(message) => message.user.id === this.currentUser.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!lastUserMessage) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastUserMessage.staged || lastUserMessage.error) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.editingMessage = lastUserMessage;
|
|
||||||
this._focusComposer();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setReplyTo(messageId) {
|
|
||||||
if (messageId) {
|
|
||||||
this.cancelEditing();
|
|
||||||
|
|
||||||
const message = this.args.channel.messagesManager.findMessage(messageId);
|
|
||||||
this.replyToMsg = message;
|
|
||||||
this.appEvents.trigger("chat-composer:reply-to-set", message);
|
|
||||||
this._focusComposer();
|
|
||||||
} else {
|
|
||||||
this.replyToMsg = null;
|
|
||||||
this.appEvents.trigger("chat-composer:reply-to-set", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
replyMessageClicked(message) {
|
|
||||||
const replyMessageFromLookup =
|
|
||||||
this.args.channel.messagesManager.findMessage(message.id);
|
|
||||||
if (replyMessageFromLookup) {
|
|
||||||
this.scrollToMessage(replyMessageFromLookup.id, {
|
|
||||||
highlight: true,
|
|
||||||
position: "start",
|
|
||||||
autoExpand: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Message is not present in the loaded messages. Fetch it!
|
|
||||||
this.requestedTargetMessageId = message.id;
|
|
||||||
this.fetchMessages();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
editButtonClicked(messageId) {
|
|
||||||
const message = this.args.channel.messagesManager.findMessage(messageId);
|
|
||||||
this.editingMessage = message;
|
|
||||||
this.scrollToLatestMessage();
|
|
||||||
this._focusComposer();
|
|
||||||
}
|
|
||||||
|
|
||||||
get canInteractWithChat() {
|
|
||||||
return !this.args.channel?.userSilenced;
|
|
||||||
}
|
|
||||||
|
|
||||||
get chatProgressBarContainer() {
|
get chatProgressBarContainer() {
|
||||||
return document.querySelector("#chat-progress-bar-container");
|
return document.querySelector("#chat-progress-bar-container");
|
||||||
}
|
}
|
||||||
|
|
||||||
get selectedMessageIds() {
|
|
||||||
return this.args.channel?.messages
|
|
||||||
?.filter((m) => m.selected)
|
|
||||||
?.map((m) => m.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onStartSelectingMessages(message) {
|
|
||||||
this._lastSelectedMessage = message;
|
|
||||||
this.selectingMessages = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
cancelSelecting() {
|
|
||||||
this.selectingMessages = false;
|
|
||||||
this.args.channel.messages.forEach((message) => {
|
|
||||||
message.selected = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onSelectMessage(message) {
|
|
||||||
this._lastSelectedMessage = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
bulkSelectMessages(message, checked) {
|
|
||||||
const lastSelectedIndex = this._findIndexOfMessage(
|
|
||||||
this._lastSelectedMessage
|
|
||||||
);
|
|
||||||
const newlySelectedIndex = this._findIndexOfMessage(message);
|
|
||||||
const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort(
|
|
||||||
(a, b) => a - b
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let i = sortedIndices[0]; i <= sortedIndices[1]; i++) {
|
|
||||||
this.args.channel.messages[i].selected = checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_findIndexOfMessage(message) {
|
|
||||||
return this.args.channel.messages.findIndex((m) => m.id === message.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onCloseFullScreen() {
|
onCloseFullScreen() {
|
||||||
this.chatStateManager.prefersDrawer();
|
this.chatStateManager.prefersDrawer();
|
||||||
@ -1023,144 +881,6 @@ export default class ChatLivePane extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
cancelEditing() {
|
|
||||||
this.editingMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
setInReplyToMsg(inReplyMsg) {
|
|
||||||
this.replyToMsg = inReplyMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
composerValueChanged({ value, uploads, replyToMsg, inProgressUploadsCount }) {
|
|
||||||
if (!this.editingMessage && !this.args.channel.isDraft) {
|
|
||||||
if (typeof value !== "undefined") {
|
|
||||||
this.args.channel.draft.message = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only save the uploads to the draft if we are not still uploading other
|
|
||||||
// ones, otherwise we get into a cycle where we pass the draft uploads as
|
|
||||||
// existingUploads back to the upload component and cause in progress ones
|
|
||||||
// to be cancelled
|
|
||||||
if (
|
|
||||||
typeof uploads !== "undefined" &&
|
|
||||||
inProgressUploadsCount !== "undefined" &&
|
|
||||||
inProgressUploadsCount === 0
|
|
||||||
) {
|
|
||||||
this.args.channel.draft.uploads = uploads;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof replyToMsg !== "undefined") {
|
|
||||||
this.args.channel.draft.replyToMsg = replyToMsg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.args.channel.isDraft) {
|
|
||||||
this._reportReplyingPresence(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._persistDraft();
|
|
||||||
}
|
|
||||||
|
|
||||||
@debounce(2000)
|
|
||||||
_persistDraft() {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.args.channel.draft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ajax("/chat/drafts.json", {
|
|
||||||
type: "POST",
|
|
||||||
data: {
|
|
||||||
chat_channel_id: this.args.channel.id,
|
|
||||||
data: this.args.channel.draft.toJSON(),
|
|
||||||
},
|
|
||||||
ignoreUnsent: false,
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
this.chat.markNetworkAsReliable();
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
// we ignore a draft which can't be saved because it's too big
|
|
||||||
// and only deal with network error for now
|
|
||||||
if (!error.jqXHR?.responseJSON?.errors?.length) {
|
|
||||||
this.chat.markNetworkAsUnreliable();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onHoverMessage(message, options = {}, event) {
|
|
||||||
if (this.site.mobileView && options.desktopOnly) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isScrolling) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message?.staged) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.hoveredMessageId &&
|
|
||||||
message?.id &&
|
|
||||||
this.hoveredMessageId === message?.id
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event) {
|
|
||||||
if (
|
|
||||||
event.type === "mouseleave" &&
|
|
||||||
(event.toElement || event.relatedTarget)?.closest(
|
|
||||||
".chat-message-actions-desktop-anchor"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
event.type === "mouseenter" &&
|
|
||||||
(event.fromElement || event.relatedTarget)?.closest(
|
|
||||||
".chat-message-actions-desktop-anchor"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.hoveredMessageId = message?.id;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.hoveredMessageId =
|
|
||||||
message?.id && message.id !== this.hoveredMessageId ? message.id : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_reportReplyingPresence(composerValue) {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.args.channel.isDraft) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const replying = !this.editingMessage && !!composerValue;
|
|
||||||
this.chatComposerPresenceManager.notifyState(
|
|
||||||
this.args.channel.id,
|
|
||||||
replying
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_focusComposer() {
|
|
||||||
this.appEvents.trigger("chat:focus-composer");
|
|
||||||
}
|
|
||||||
|
|
||||||
_unsubscribeToUpdates(channelId) {
|
_unsubscribeToUpdates(channelId) {
|
||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
return;
|
return;
|
||||||
@ -1213,33 +933,6 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
|
|
||||||
// we now use this hack to disable it
|
|
||||||
@bind
|
|
||||||
forceRendering(callback) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
if (!this._scrollerEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.capabilities.isIOS) {
|
|
||||||
this._scrollerEl.style.overflow = "hidden";
|
|
||||||
}
|
|
||||||
|
|
||||||
callback?.();
|
|
||||||
|
|
||||||
if (this.capabilities.isIOS) {
|
|
||||||
discourseLater(() => {
|
|
||||||
if (!this._scrollerEl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._scrollerEl.style.overflow = "auto";
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
addAutoFocusEventListener() {
|
addAutoFocusEventListener() {
|
||||||
document.addEventListener("keydown", this._autoFocus);
|
document.addEventListener("keydown", this._autoFocus);
|
||||||
@ -1277,14 +970,14 @@ export default class ChatLivePane extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const composer = document.querySelector(".chat-composer-input");
|
const composer = document.querySelector(".chat-composer-input");
|
||||||
if (composer && !this.args.channel.isDraft) {
|
if (composer && !this.args.channel.isDraft) {
|
||||||
this.appEvents.trigger("chat:insert-text", key);
|
|
||||||
composer.focus();
|
composer.focus();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -1292,12 +985,66 @@ export default class ChatLivePane extends Component {
|
|||||||
throttle(this, this._computeDatesSeparators, 50, false);
|
throttle(this, this._computeDatesSeparators, 50, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A more consistent way to scroll to the bottom when we are sure this is our goal
|
||||||
|
// it will also limit issues with any element changing the height while we are scrolling
|
||||||
|
// to the bottom
|
||||||
|
@action
|
||||||
|
scrollToBottom() {
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollable.scrollTop = -1;
|
||||||
|
this.forceRendering(() => {
|
||||||
|
this.scrollable.scrollTop = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
|
||||||
|
// we now use this hack to disable it
|
||||||
|
@bind
|
||||||
|
forceRendering(callback) {
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
if (this._selfDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.capabilities.isIOS) {
|
||||||
|
this.scrollable.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback?.();
|
||||||
|
|
||||||
|
if (this.capabilities.isIOS) {
|
||||||
|
discourseLater(() => {
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollable.style.overflow = "auto";
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_computeDatesSeparators() {
|
_computeDatesSeparators() {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
|
if (this._selfDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const dates = [
|
const dates = [
|
||||||
...this._scrollerEl.querySelectorAll(".chat-message-separator-date"),
|
...this.scrollable.querySelectorAll(".chat-message-separator-date"),
|
||||||
].reverse();
|
].reverse();
|
||||||
const height = this._scrollerEl.querySelector(
|
const height = this.scrollable.querySelector(
|
||||||
".chat-messages-container"
|
".chat-messages-container"
|
||||||
).clientHeight;
|
).clientHeight;
|
||||||
|
|
||||||
@ -1336,17 +1083,29 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#isAtBottom() {
|
#isAtBottom() {
|
||||||
return Math.abs(this._scrollerEl.scrollTop) <= 2;
|
if (!this.scrollable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(this.scrollable.scrollTop) <= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#isTowardsBottom() {
|
#isTowardsBottom() {
|
||||||
return Math.abs(this._scrollerEl.scrollTop) <= 50;
|
if (!this.scrollable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.abs(this.scrollable.scrollTop) <= 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
#isAtTop() {
|
#isAtTop() {
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
Math.abs(this._scrollerEl.scrollTop) >=
|
Math.abs(this.scrollable.scrollTop) >=
|
||||||
this._scrollerEl.scrollHeight - this._scrollerEl.offsetHeight - 2
|
this.scrollable.scrollHeight - this.scrollable.offsetHeight - 2
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,63 +1,72 @@
|
|||||||
<div
|
{{#if (and this.site.desktopView this.chat.activeMessage.model.id)}}
|
||||||
class="chat-message-actions-container"
|
<div
|
||||||
data-id={{@message.id}}
|
{{did-insert this.setupPopper}}
|
||||||
{{did-insert this.attachPopper}}
|
{{did-update this.setupPopper this.chat.activeMessage.model.id}}
|
||||||
{{will-destroy this.destroyPopper}}
|
{{will-destroy this.teardownPopper}}
|
||||||
>
|
class="chat-message-actions-container"
|
||||||
<div class="chat-message-actions">
|
data-id={{this.message.id}}
|
||||||
{{#if this.chatStateManager.isFullPageActive}}
|
>
|
||||||
{{#each @emojiReactions key="emoji" as |reaction|}}
|
<div class="chat-message-actions">
|
||||||
<ChatMessageReaction
|
{{#if this.chatStateManager.isFullPageActive}}
|
||||||
@reaction={{reaction}}
|
{{#each
|
||||||
@react={{@messageActions.react}}
|
this.messageInteractor.emojiReactions
|
||||||
@showCount={{false}}
|
key="emoji"
|
||||||
|
as |reaction|
|
||||||
|
}}
|
||||||
|
<ChatMessageReaction
|
||||||
|
@reaction={{reaction}}
|
||||||
|
@onReaction={{this.messageInteractor.react}}
|
||||||
|
@message={{this.message}}
|
||||||
|
@showCount={{false}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.messageInteractor.canInteractWithMessage}}
|
||||||
|
<DButton
|
||||||
|
@class="btn-flat react-btn"
|
||||||
|
@action={{this.messageInteractor.openEmojiPicker}}
|
||||||
|
@icon="discourse-emojis"
|
||||||
|
@title="chat.react"
|
||||||
|
@forwardEvent={{true}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/if}}
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if @messageCapabilities.canReact}}
|
{{#if this.messageInteractor.canBookmark}}
|
||||||
<DButton
|
<DButton
|
||||||
@class="btn-flat react-btn"
|
@class="btn-flat bookmark-btn"
|
||||||
@action={{@messageActions.startReactionForMessageActions}}
|
@action={{this.messageInteractor.toggleBookmark}}
|
||||||
@icon="discourse-emojis"
|
>
|
||||||
@title="chat.react"
|
<BookmarkIcon @bookmark={{this.message.bookmark}} />
|
||||||
/>
|
</DButton>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if @messageCapabilities.canBookmark}}
|
{{#if this.messageInteractor.canReply}}
|
||||||
<DButton
|
<DButton
|
||||||
@class="btn-flat bookmark-btn"
|
@class="btn-flat reply-btn"
|
||||||
@action={{@messageActions.toggleBookmark}}
|
@action={{this.messageInteractor.reply}}
|
||||||
>
|
@icon="reply"
|
||||||
<BookmarkIcon @bookmark={{@message.bookmark}} />
|
@title="chat.reply"
|
||||||
</DButton>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if @messageCapabilities.canReply}}
|
{{#if this.messageInteractor.canOpenThread}}
|
||||||
<DButton
|
<DButton
|
||||||
@class="btn-flat reply-btn"
|
@class="btn-flat chat-message-thread-btn"
|
||||||
@action={{@messageActions.reply}}
|
@action={{this.messageInteractor.openThread}}
|
||||||
@icon="reply"
|
@icon="puzzle-piece"
|
||||||
@title="chat.reply"
|
@title="chat.threads.open"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if @messageCapabilities.hasThread}}
|
{{#if this.messageInteractor.secondaryButtons.length}}
|
||||||
<DButton
|
<DropdownSelectBox
|
||||||
@class="btn-flat chat-message-thread-btn"
|
@class="more-buttons"
|
||||||
@action={{@messageActions.openThread}}
|
@options={{hash icon="ellipsis-v" placement="left"}}
|
||||||
@icon="puzzle-piece"
|
@content={{this.messageInteractor.secondaryButtons}}
|
||||||
@title="chat.threads.open"
|
@onChange={{action this.messageInteractor.handleSecondaryButtons}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
</div>
|
||||||
{{#if @secondaryButtons.length}}
|
|
||||||
<DropdownSelectBox
|
|
||||||
@class="more-buttons"
|
|
||||||
@options={{hash icon="ellipsis-v" placement="left"}}
|
|
||||||
@content={{@secondaryButtons}}
|
|
||||||
@onChange={{action "handleSecondaryButtons"}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{/if}}
|
@ -1,34 +1,48 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { action } from "@ember/object";
|
|
||||||
import { createPopper } from "@popperjs/core";
|
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
|
||||||
|
import { getOwner } from "@ember/application";
|
||||||
|
import { schedule } from "@ember/runloop";
|
||||||
|
import { createPopper } from "@popperjs/core";
|
||||||
|
import chatMessageContainer from "discourse/plugins/chat/discourse/lib/chat-message-container";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
const MSG_ACTIONS_VERTICAL_PADDING = -10;
|
const MSG_ACTIONS_VERTICAL_PADDING = -10;
|
||||||
|
|
||||||
export default class ChatMessageActionsDesktop extends Component {
|
export default class ChatMessageActionsDesktop extends Component {
|
||||||
|
@service chat;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
|
@service chatEmojiPickerManager;
|
||||||
|
@service site;
|
||||||
|
|
||||||
popper = null;
|
popper = null;
|
||||||
|
|
||||||
@action
|
get message() {
|
||||||
destroyPopper() {
|
return this.chat.activeMessage.model;
|
||||||
this.popper?.destroy();
|
}
|
||||||
this.popper = null;
|
|
||||||
|
get context() {
|
||||||
|
return this.chat.activeMessage.context;
|
||||||
|
}
|
||||||
|
|
||||||
|
get messageInteractor() {
|
||||||
|
const activeMessage = this.chat.activeMessage;
|
||||||
|
|
||||||
|
return new ChatMessageInteractor(
|
||||||
|
getOwner(this),
|
||||||
|
activeMessage.model,
|
||||||
|
activeMessage.context
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
attachPopper() {
|
setupPopper(element) {
|
||||||
this.destroyPopper();
|
this.popper?.destroy();
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
this.popper = createPopper(
|
this.popper = createPopper(
|
||||||
document.querySelector(
|
chatMessageContainer(this.message.id, this.context),
|
||||||
`.chat-message-container[data-id="${this.args.message.id}"]`
|
element,
|
||||||
),
|
|
||||||
document.querySelector(
|
|
||||||
`.chat-message-actions-container[data-id="${this.args.message.id}"] .chat-message-actions`
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
placement: "top-end",
|
placement: "top-end",
|
||||||
strategy: "fixed",
|
strategy: "fixed",
|
||||||
@ -46,7 +60,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleSecondaryButtons(id) {
|
teardownPopper() {
|
||||||
this.args.messageActions?.[id]?.();
|
this.popper?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,91 +1,91 @@
|
|||||||
<div
|
{{#if (and this.site.mobileView this.chat.activeMessage)}}
|
||||||
class={{concat-class
|
|
||||||
"chat-message-actions-backdrop"
|
|
||||||
(if this.showFadeIn "fade-in")
|
|
||||||
}}
|
|
||||||
{{did-insert this.fadeAndVibrate}}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
role="button"
|
class={{concat-class
|
||||||
class="collapse-area"
|
"chat-message-actions-backdrop"
|
||||||
{{on "touchstart" this.collapseMenu passive=true}}
|
(if this.showFadeIn "fade-in")
|
||||||
|
}}
|
||||||
|
{{did-insert this.fadeAndVibrate}}
|
||||||
>
|
>
|
||||||
</div>
|
<div
|
||||||
|
role="button"
|
||||||
<div class="chat-message-actions">
|
class="collapse-area"
|
||||||
<div class="selected-message-container">
|
{{on "touchstart" this.collapseMenu passive=true}}
|
||||||
<div class="selected-message">
|
>
|
||||||
<ChatUserAvatar @user={{@message.user}} />
|
|
||||||
<span
|
|
||||||
{{on "touchstart" this.expandReply passive=true}}
|
|
||||||
role="button"
|
|
||||||
class={{concat-class
|
|
||||||
"selected-message-reply"
|
|
||||||
(if this.hasExpandedReply "is-expanded")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{{@message.message}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="secondary-actions">
|
<div class="chat-message-actions">
|
||||||
{{#each @secondaryButtons as |button|}}
|
<div class="selected-message-container">
|
||||||
<li class="chat-message-action-item" data-id={{button.id}}>
|
<div class="selected-message">
|
||||||
<DButton
|
<ChatUserAvatar @user={{this.message.user}} />
|
||||||
@class="chat-message-action"
|
<span
|
||||||
@translatedLabel={{button.name}}
|
{{on "touchstart" this.expandReply passive=true}}
|
||||||
@icon={{button.icon}}
|
role="button"
|
||||||
@actionParam={{button.id}}
|
class={{concat-class
|
||||||
@action={{action
|
"selected-message-reply"
|
||||||
this.actAndCloseMenu
|
(if this.hasExpandedReply "is-expanded")
|
||||||
(get @messageActions button.id)
|
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{{#if (or @messageCapabilities.canReact @messageCapabilities.canReply)}}
|
|
||||||
<div class="main-actions">
|
|
||||||
{{#if @messageCapabilities.canReact}}
|
|
||||||
{{#each @emojiReactions as |reaction|}}
|
|
||||||
<ChatMessageReaction
|
|
||||||
@reaction={{reaction}}
|
|
||||||
@react={{@messageActions.react}}
|
|
||||||
@showCount={{false}}
|
|
||||||
/>
|
|
||||||
{{/each}}
|
|
||||||
|
|
||||||
<DButton
|
|
||||||
@class="btn-flat react-btn"
|
|
||||||
@action={{action
|
|
||||||
this.actAndCloseMenu
|
|
||||||
@messageActions.startReactionForMessageActions
|
|
||||||
}}
|
|
||||||
@icon="discourse-emojis"
|
|
||||||
@title="chat.react"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if @messageCapabilities.canBookmark}}
|
|
||||||
<DButton
|
|
||||||
@class="btn-flat bookmark-btn"
|
|
||||||
@action={{@messageActions.toggleBookmark}}
|
|
||||||
>
|
>
|
||||||
<BookmarkIcon @bookmark={{@message.bookmark}} />
|
{{this.message.message}}
|
||||||
</DButton>
|
</span>
|
||||||
{{/if}}
|
</div>
|
||||||
|
|
||||||
{{#if @messageCapabilities.canReply}}
|
|
||||||
<DButton
|
|
||||||
@class="chat-message-action reply-btn btn-flat"
|
|
||||||
@action={{action "actAndCloseMenu" @messageActions.reply}}
|
|
||||||
@icon="reply"
|
|
||||||
@title="chat.reply"
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
|
||||||
|
<ul class="secondary-actions">
|
||||||
|
{{#each this.messageInteractor.secondaryButtons as |button|}}
|
||||||
|
<li class="chat-message-action-item" data-id={{button.id}}>
|
||||||
|
<DButton
|
||||||
|
@class="chat-message-action"
|
||||||
|
@translatedLabel={{button.name}}
|
||||||
|
@icon={{button.icon}}
|
||||||
|
@actionParam={{button.id}}
|
||||||
|
@action={{action this.actAndCloseMenu button.id}}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{{#if
|
||||||
|
(or this.messageInteractor.canReact this.messageInteractor.canReply)
|
||||||
|
}}
|
||||||
|
<div class="main-actions">
|
||||||
|
{{#if this.messageInteractor.canReact}}
|
||||||
|
{{#each this.messageInteractor.emojiReactions as |reaction|}}
|
||||||
|
<ChatMessageReaction
|
||||||
|
@reaction={{reaction}}
|
||||||
|
@onReaction={{this.messageInteractor.react}}
|
||||||
|
@message={{this.message}}
|
||||||
|
@showCount={{false}}
|
||||||
|
/>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
|
<DButton
|
||||||
|
@class="btn-flat react-btn"
|
||||||
|
@action={{this.openEmojiPicker}}
|
||||||
|
@icon="discourse-emojis"
|
||||||
|
@title="chat.react"
|
||||||
|
@forwardEvent={{true}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.messageInteractor.canBookmark}}
|
||||||
|
<DButton
|
||||||
|
@class="btn-flat bookmark-btn"
|
||||||
|
@action={{action this.actAndCloseMenu "toggleBookmark"}}
|
||||||
|
>
|
||||||
|
<BookmarkIcon @bookmark={{this.message.bookmark}} />
|
||||||
|
</DButton>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if this.messageInteractor.canReply}}
|
||||||
|
<DButton
|
||||||
|
@class="chat-message-action reply-btn btn-flat"
|
||||||
|
@action={{action this.actAndCloseMenu "reply"}}
|
||||||
|
@icon="reply"
|
||||||
|
@title="chat.reply"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{/if}}
|
@ -1,4 +1,6 @@
|
|||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
|
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
|
||||||
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
@ -6,6 +8,8 @@ import { isTesting } from "discourse-common/config/environment";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
export default class ChatMessageActionsMobile extends Component {
|
export default class ChatMessageActionsMobile extends Component {
|
||||||
|
@service chat;
|
||||||
|
@service site;
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
|
|
||||||
@tracked hasExpandedReply = false;
|
@tracked hasExpandedReply = false;
|
||||||
@ -13,6 +17,20 @@ export default class ChatMessageActionsMobile extends Component {
|
|||||||
|
|
||||||
messageActions = null;
|
messageActions = null;
|
||||||
|
|
||||||
|
get message() {
|
||||||
|
return this.chat.activeMessage.model;
|
||||||
|
}
|
||||||
|
|
||||||
|
get messageInteractor() {
|
||||||
|
const activeMessage = this.chat.activeMessage;
|
||||||
|
|
||||||
|
return new ChatMessageInteractor(
|
||||||
|
getOwner(this),
|
||||||
|
activeMessage.model,
|
||||||
|
activeMessage.context
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
fadeAndVibrate() {
|
fadeAndVibrate() {
|
||||||
discourseLater(this.#addFadeIn.bind(this));
|
discourseLater(this.#addFadeIn.bind(this));
|
||||||
@ -35,8 +53,14 @@ export default class ChatMessageActionsMobile extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
actAndCloseMenu(fn) {
|
actAndCloseMenu(fnId) {
|
||||||
fn?.();
|
this.messageInteractor[fnId]();
|
||||||
|
this.#onCloseMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openEmojiPicker(_, event) {
|
||||||
|
this.messageInteractor.openEmojiPicker(_, event);
|
||||||
this.#onCloseMenu();
|
this.#onCloseMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +76,7 @@ export default class ChatMessageActionsMobile extends Component {
|
|||||||
|
|
||||||
// by ensuring we are not hovering any message anymore
|
// by ensuring we are not hovering any message anymore
|
||||||
// we also ensure the menu is fully removed
|
// we also ensure the menu is fully removed
|
||||||
this.args.onHoverMessage?.(null);
|
this.chat.activeMessage = null;
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ export default class ChatMessageInReplyToIndicator extends Component {
|
|||||||
|
|
||||||
get hasThread() {
|
get hasThread() {
|
||||||
return (
|
return (
|
||||||
this.args.message?.channel?.get("threading_enabled") &&
|
this.args.message?.channel?.threadingEnabled &&
|
||||||
this.args.message?.threadId
|
this.args.message?.threadId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -52,10 +52,11 @@ export default class ChatMessageReaction extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
handleClick() {
|
handleClick() {
|
||||||
this.args.react?.(
|
this.args.onReaction?.(
|
||||||
this.args.reaction.emoji,
|
this.args.reaction.emoji,
|
||||||
this.args.reaction.reacted ? "remove" : "add"
|
this.args.reaction.reacted ? "remove" : "add"
|
||||||
);
|
);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,57 +1,23 @@
|
|||||||
{{! template-lint-disable no-invalid-interactive }}
|
{{! template-lint-disable no-invalid-interactive }}
|
||||||
|
|
||||||
<ChatMessageSeparatorDate @message={{@message}} />
|
{{#if (eq @context "channel")}}
|
||||||
<ChatMessageSeparatorNew @message={{@message}} />
|
<ChatMessageSeparatorDate @message={{@message}} />
|
||||||
|
<ChatMessageSeparatorNew @message={{@message}} />
|
||||||
{{#if
|
|
||||||
(and
|
|
||||||
this.showActions this.site.mobileView this.chatMessageActionsMobileAnchor
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{#in-element this.chatMessageActionsMobileAnchor}}
|
|
||||||
<ChatMessageActionsMobile
|
|
||||||
@message={{@message}}
|
|
||||||
@emojiReactions={{this.emojiReactions}}
|
|
||||||
@secondaryButtons={{this.secondaryButtons}}
|
|
||||||
@messageActions={{this.messageActions}}
|
|
||||||
@messageCapabilities={{this.messageCapabilities}}
|
|
||||||
@onHoverMessage={{@onHoverMessage}}
|
|
||||||
/>
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
|
||||||
|
|
||||||
{{#if
|
|
||||||
(and
|
|
||||||
this.showActions this.site.desktopView this.chatMessageActionsDesktopAnchor
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{#in-element this.chatMessageActionsDesktopAnchor}}
|
|
||||||
<ChatMessageActionsDesktop
|
|
||||||
@message={{@message}}
|
|
||||||
@emojiReactions={{this.emojiReactions}}
|
|
||||||
@secondaryButtons={{this.secondaryButtons}}
|
|
||||||
@messageActions={{this.messageActions}}
|
|
||||||
@messageCapabilities={{this.messageCapabilities}}
|
|
||||||
/>
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
{{will-destroy this.teardownChatMessage}}
|
{{will-destroy this.teardownChatMessage}}
|
||||||
{{did-insert this.setMessageActionsAnchors}}
|
|
||||||
{{did-insert this.decorateCookedMessage}}
|
{{did-insert this.decorateCookedMessage}}
|
||||||
{{did-update this.decorateCookedMessage @message.id}}
|
{{did-update this.decorateCookedMessage @message.id}}
|
||||||
{{did-update this.decorateCookedMessage @message.version}}
|
{{did-update this.decorateCookedMessage @message.version}}
|
||||||
{{on "touchmove" this.handleTouchMove passive=true}}
|
{{on "touchmove" this.handleTouchMove passive=true}}
|
||||||
{{on "touchstart" this.handleTouchStart passive=true}}
|
{{on "touchstart" this.handleTouchStart passive=true}}
|
||||||
{{on "touchend" this.handleTouchEnd passive=true}}
|
{{on "touchend" this.handleTouchEnd passive=true}}
|
||||||
{{on "mouseenter" (fn @onHoverMessage @message (hash desktopOnly=true))}}
|
{{on "mouseenter" this.onMouseEnter}}
|
||||||
{{on "mousemove" (fn @onHoverMessage @message (hash desktopOnly=true))}}
|
{{on "mouseleave" this.onMouseLeave}}
|
||||||
{{on "mouseleave" (fn @onHoverMessage null (hash desktopOnly=true))}}
|
|
||||||
class={{concat-class
|
class={{concat-class
|
||||||
"chat-message-container"
|
"chat-message-container"
|
||||||
(if @isHovered "is-hovered")
|
(if this.pane.selectingMessages "selecting-messages")
|
||||||
(if @selectingMessages "selecting-messages")
|
|
||||||
(if @message.highlighted "highlighted")
|
(if @message.highlighted "highlighted")
|
||||||
}}
|
}}
|
||||||
data-id={{@message.id}}
|
data-id={{@message.id}}
|
||||||
@ -63,7 +29,7 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{#if this.show}}
|
{{#if this.show}}
|
||||||
{{#if @selectingMessages}}
|
{{#if this.pane.selectingMessages}}
|
||||||
<Input
|
<Input
|
||||||
@type="checkbox"
|
@type="checkbox"
|
||||||
class="chat-message-selector"
|
class="chat-message-selector"
|
||||||
@ -98,7 +64,6 @@
|
|||||||
(if this.hideUserInfo "user-info-hidden")
|
(if this.hideUserInfo "user-info-hidden")
|
||||||
(if @message.error "errored")
|
(if @message.error "errored")
|
||||||
(if @message.bookmark "chat-message-bookmarked")
|
(if @message.bookmark "chat-message-bookmarked")
|
||||||
(if @isHovered "chat-message-selected")
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{{#unless this.hideReplyToInfo}}
|
{{#unless this.hideReplyToInfo}}
|
||||||
@ -132,18 +97,20 @@
|
|||||||
{{#each @message.reactions as |reaction|}}
|
{{#each @message.reactions as |reaction|}}
|
||||||
<ChatMessageReaction
|
<ChatMessageReaction
|
||||||
@reaction={{reaction}}
|
@reaction={{reaction}}
|
||||||
@react={{this.react}}
|
@onReaction={{this.messageInteractor.react}}
|
||||||
|
@message={{@message}}
|
||||||
@showTooltip={{true}}
|
@showTooltip={{true}}
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|
||||||
{{#if @canInteractWithChat}}
|
{{#if this.chat.userCanInteractWithChat}}
|
||||||
{{#unless this.site.mobileView}}
|
{{#unless this.site.mobileView}}
|
||||||
<DButton
|
<DButton
|
||||||
@class="chat-message-react-btn"
|
@class="chat-message-react-btn"
|
||||||
@action={{this.startReactionForReactionList}}
|
@action={{this.messageInteractor.openEmojiPicker}}
|
||||||
@icon="discourse-emojis"
|
@icon="discourse-emojis"
|
||||||
@title="chat.react"
|
@title="chat.react"
|
||||||
|
@forwardEvent={{true}}
|
||||||
/>
|
/>
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import Bookmark from "discourse/models/bookmark";
|
|
||||||
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
|
||||||
import optionalService from "discourse/lib/optional-service";
|
import optionalService from "discourse/lib/optional-service";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { cancel, schedule } from "@ember/runloop";
|
import { cancel, schedule } from "@ember/runloop";
|
||||||
import { clipboardCopy } from "discourse/lib/utilities";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
|
import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
|
||||||
import showModal from "discourse/lib/show-modal";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
import ChatMessageInteractor from "discourse/plugins/chat/discourse/lib/chat-message-interactor";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
|
||||||
let _chatMessageDecorators = [];
|
let _chatMessageDecorators = [];
|
||||||
|
|
||||||
@ -29,8 +24,7 @@ export function resetChatMessageDecorators() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const MENTION_KEYWORDS = ["here", "all"];
|
export const MENTION_KEYWORDS = ["here", "all"];
|
||||||
|
export const MESSAGE_CONTEXT_THREAD = "thread";
|
||||||
export const REACTIONS = { add: "add", remove: "remove" };
|
|
||||||
|
|
||||||
export default class ChatMessage extends Component {
|
export default class ChatMessage extends Component {
|
||||||
@service site;
|
@service site;
|
||||||
@ -42,21 +36,25 @@ export default class ChatMessage extends Component {
|
|||||||
@service chatApi;
|
@service chatApi;
|
||||||
@service chatEmojiReactionStore;
|
@service chatEmojiReactionStore;
|
||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
|
@service chatChannelPane;
|
||||||
|
@service chatChannelThreadPane;
|
||||||
@service chatChannelsManager;
|
@service chatChannelsManager;
|
||||||
@service router;
|
@service router;
|
||||||
|
|
||||||
@tracked chatMessageActionsMobileAnchor = null;
|
|
||||||
@tracked chatMessageActionsDesktopAnchor = null;
|
|
||||||
|
|
||||||
@optionalService adminTools;
|
@optionalService adminTools;
|
||||||
|
|
||||||
cachedFavoritesReactions = null;
|
get pane() {
|
||||||
reacting = false;
|
return this.args.context === MESSAGE_CONTEXT_THREAD
|
||||||
|
? this.chatChannelThreadPane
|
||||||
|
: this.chatChannelPane;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
get messageInteractor() {
|
||||||
super(...arguments);
|
return new ChatMessageInteractor(
|
||||||
|
getOwner(this),
|
||||||
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
this.args.message,
|
||||||
|
this.args.context
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get deletedAndCollapsed() {
|
get deletedAndCollapsed() {
|
||||||
@ -72,15 +70,17 @@ export default class ChatMessage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setMessageActionsAnchors() {
|
expand() {
|
||||||
schedule("afterRender", () => {
|
this.args.message.expanded = true;
|
||||||
this.chatMessageActionsDesktopAnchor = document.querySelector(
|
}
|
||||||
".chat-message-actions-desktop-anchor"
|
|
||||||
);
|
@action
|
||||||
this.chatMessageActionsMobileAnchor = document.querySelector(
|
toggleChecked(event) {
|
||||||
".chat-message-actions-mobile-anchor"
|
if (event.shiftKey) {
|
||||||
);
|
this.messageInteractor.bulkSelect(event.target.checked);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
this.messageInteractor.select(event.target.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -108,114 +108,6 @@ export default class ChatMessage extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get showActions() {
|
|
||||||
return (
|
|
||||||
this.args.canInteractWithChat &&
|
|
||||||
!this.args.message?.staged &&
|
|
||||||
this.args.isHovered
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get secondaryButtons() {
|
|
||||||
const buttons = [];
|
|
||||||
|
|
||||||
buttons.push({
|
|
||||||
id: "copyLinkToMessage",
|
|
||||||
name: I18n.t("chat.copy_link"),
|
|
||||||
icon: "link",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.showEditButton) {
|
|
||||||
buttons.push({
|
|
||||||
id: "edit",
|
|
||||||
name: I18n.t("chat.edit"),
|
|
||||||
icon: "pencil-alt",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.args.selectingMessages) {
|
|
||||||
buttons.push({
|
|
||||||
id: "selectMessage",
|
|
||||||
name: I18n.t("chat.select"),
|
|
||||||
icon: "tasks",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.canFlagMessage) {
|
|
||||||
buttons.push({
|
|
||||||
id: "flag",
|
|
||||||
name: I18n.t("chat.flag"),
|
|
||||||
icon: "flag",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showDeleteButton) {
|
|
||||||
buttons.push({
|
|
||||||
id: "deleteMessage",
|
|
||||||
name: I18n.t("chat.delete"),
|
|
||||||
icon: "trash-alt",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showRestoreButton) {
|
|
||||||
buttons.push({
|
|
||||||
id: "restore",
|
|
||||||
name: I18n.t("chat.restore"),
|
|
||||||
icon: "undo",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.showRebakeButton) {
|
|
||||||
buttons.push({
|
|
||||||
id: "rebakeMessage",
|
|
||||||
name: I18n.t("chat.rebake_message"),
|
|
||||||
icon: "sync-alt",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.hasThread) {
|
|
||||||
buttons.push({
|
|
||||||
id: "openThread",
|
|
||||||
name: I18n.t("chat.threads.open"),
|
|
||||||
icon: "puzzle-piece",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return buttons;
|
|
||||||
}
|
|
||||||
|
|
||||||
get messageActions() {
|
|
||||||
return {
|
|
||||||
reply: this.reply,
|
|
||||||
react: this.react,
|
|
||||||
copyLinkToMessage: this.copyLinkToMessage,
|
|
||||||
edit: this.edit,
|
|
||||||
selectMessage: this.selectMessage,
|
|
||||||
flag: this.flag,
|
|
||||||
deleteMessage: this.deleteMessage,
|
|
||||||
restore: this.restore,
|
|
||||||
rebakeMessage: this.rebakeMessage,
|
|
||||||
toggleBookmark: this.toggleBookmark,
|
|
||||||
openThread: this.openThread,
|
|
||||||
startReactionForMessageActions: this.startReactionForMessageActions,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get messageCapabilities() {
|
|
||||||
return {
|
|
||||||
canReact: this.canReact,
|
|
||||||
canReply: this.canReply,
|
|
||||||
canBookmark: this.showBookmarkButton,
|
|
||||||
hasThread: this.canReply && this.hasThread,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasThread() {
|
|
||||||
return (
|
|
||||||
this.args.channel?.get("threading_enabled") && this.args.message?.threadId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get show() {
|
get show() {
|
||||||
return (
|
return (
|
||||||
!this.args.message?.deletedAt ||
|
!this.args.message?.deletedAt ||
|
||||||
@ -225,6 +117,58 @@ export default class ChatMessage extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onMouseEnter() {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pane.hoveredMessageId === this.args.message.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._onHoverMessageDebouncedHandler = discourseDebounce(
|
||||||
|
this,
|
||||||
|
this._debouncedOnHoverMessage,
|
||||||
|
250
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onMouseLeave(event) {
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(event.toElement || event.relatedTarget)?.closest(
|
||||||
|
".chat-message-actions-container"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(this._onHoverMessageDebouncedHandler);
|
||||||
|
|
||||||
|
this.chat.activeMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
_debouncedOnHoverMessage() {
|
||||||
|
if (!this.chat.userCanInteractWithChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setActiveMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setActiveMessage() {
|
||||||
|
this.chat.activeMessage = {
|
||||||
|
model: this.args.message,
|
||||||
|
context: this.args.context,
|
||||||
|
};
|
||||||
|
this.pane.hoveredMessageId = this.args.message.id;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTouchStart() {
|
handleTouchStart() {
|
||||||
// if zoomed don't track long press
|
// if zoomed don't track long press
|
||||||
@ -232,24 +176,20 @@ export default class ChatMessage extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.args.isHovered) {
|
// when testing this must be triggered immediately because there
|
||||||
// when testing this must be triggered immediately because there
|
// is no concept of "long press" there, the Ember `tap` test helper
|
||||||
// is no concept of "long press" there, the Ember `tap` test helper
|
// does send the touchstart/touchend events but immediately, see
|
||||||
// does send the touchstart/touchend events but immediately, see
|
// https://github.com/emberjs/ember-test-helpers/blob/master/API.md#tap
|
||||||
// https://github.com/emberjs/ember-test-helpers/blob/master/API.md#tap
|
if (isTesting()) {
|
||||||
if (isTesting()) {
|
this._handleLongPress();
|
||||||
this._handleLongPress();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isPressingHandler = discourseLater(this._handleLongPress, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._isPressingHandler = discourseLater(this._handleLongPress, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
handleTouchMove() {
|
handleTouchMove() {
|
||||||
if (!this.args.isHovered) {
|
cancel(this._isPressingHandler);
|
||||||
cancel(this._isPressingHandler);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -267,7 +207,7 @@ export default class ChatMessage extends Component {
|
|||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
document.querySelector(".chat-composer-input")?.blur();
|
document.querySelector(".chat-composer-input")?.blur();
|
||||||
|
|
||||||
this.args.onHoverMessage?.(this.args.message);
|
this._setActiveMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hideUserInfo() {
|
get hideUserInfo() {
|
||||||
@ -297,81 +237,12 @@ export default class ChatMessage extends Component {
|
|||||||
|
|
||||||
get hideReplyToInfo() {
|
get hideReplyToInfo() {
|
||||||
return (
|
return (
|
||||||
|
this.args.context === MESSAGE_CONTEXT_THREAD ||
|
||||||
this.args.message?.inReplyTo?.id ===
|
this.args.message?.inReplyTo?.id ===
|
||||||
this.args.message?.previousMessage?.id
|
this.args.message?.previousMessage?.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showEditButton() {
|
|
||||||
return (
|
|
||||||
!this.args.message?.deletedAt &&
|
|
||||||
this.currentUser?.id === this.args.message?.user?.id &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get canFlagMessage() {
|
|
||||||
return (
|
|
||||||
this.currentUser?.id !== this.args.message?.user?.id &&
|
|
||||||
!this.args.channel?.isDirectMessageChannel &&
|
|
||||||
this.args.message?.userFlagStatus === undefined &&
|
|
||||||
this.args.channel?.canFlag &&
|
|
||||||
!this.args.message?.chatWebhookEvent &&
|
|
||||||
!this.args.message?.deletedAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get canManageDeletion() {
|
|
||||||
return this.currentUser?.id === this.args.message.user.id
|
|
||||||
? this.args.channel?.canDeleteSelf
|
|
||||||
: this.args.channel?.canDeleteOthers;
|
|
||||||
}
|
|
||||||
|
|
||||||
get canReply() {
|
|
||||||
return (
|
|
||||||
!this.args.message?.deletedAt &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get canReact() {
|
|
||||||
return (
|
|
||||||
!this.args.message?.deletedAt &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showDeleteButton() {
|
|
||||||
return (
|
|
||||||
this.canManageDeletion &&
|
|
||||||
!this.args.message?.deletedAt &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showRestoreButton() {
|
|
||||||
return (
|
|
||||||
this.canManageDeletion &&
|
|
||||||
this.args.message?.deletedAt &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showBookmarkButton() {
|
|
||||||
return this.args.channel?.canModifyMessages?.(this.currentUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
get showRebakeButton() {
|
|
||||||
return (
|
|
||||||
this.currentUser?.staff &&
|
|
||||||
this.args.channel?.canModifyMessages?.(this.currentUser)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get hasReactions() {
|
|
||||||
return Object.values(this.args.message.reactions).some((r) => r.count > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
get mentionWarning() {
|
get mentionWarning() {
|
||||||
return this.args.message.mentionWarning;
|
return this.args.message.mentionWarning;
|
||||||
}
|
}
|
||||||
@ -447,261 +318,4 @@ export default class ChatMessage extends Component {
|
|||||||
dismissMentionWarning() {
|
dismissMentionWarning() {
|
||||||
this.args.message.mentionWarning = null;
|
this.args.message.mentionWarning = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
|
||||||
startReactionForMessageActions() {
|
|
||||||
this.chatEmojiPickerManager.startFromMessageActions(
|
|
||||||
this.args.message,
|
|
||||||
this.selectReaction,
|
|
||||||
{ desktop: this.site.desktopView }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
startReactionForReactionList() {
|
|
||||||
this.chatEmojiPickerManager.startFromMessageReactionList(
|
|
||||||
this.args.message,
|
|
||||||
this.selectReaction,
|
|
||||||
{ desktop: this.site.desktopView }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deselectReaction(emoji) {
|
|
||||||
if (!this.args.canInteractWithChat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.react(emoji, REACTIONS.remove);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectReaction(emoji) {
|
|
||||||
if (!this.args.canInteractWithChat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.react(emoji, REACTIONS.add);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
react(emoji, reactAction) {
|
|
||||||
if (!this.args.canInteractWithChat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.reacting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.capabilities.canVibrate && !isTesting()) {
|
|
||||||
navigator.vibrate(5);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.site.mobileView) {
|
|
||||||
this.args.onHoverMessage(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reactAction === REACTIONS.add) {
|
|
||||||
this.chatEmojiReactionStore.track(`:${emoji}:`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reacting = true;
|
|
||||||
|
|
||||||
this.args.message.react(
|
|
||||||
emoji,
|
|
||||||
reactAction,
|
|
||||||
this.currentUser,
|
|
||||||
this.currentUser.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return ajax(
|
|
||||||
`/chat/${this.args.message.channelId}/react/${this.args.message.id}`,
|
|
||||||
{
|
|
||||||
type: "PUT",
|
|
||||||
data: {
|
|
||||||
react_action: reactAction,
|
|
||||||
emoji,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.catch((errResult) => {
|
|
||||||
popupAjaxError(errResult);
|
|
||||||
this.args.message.react(
|
|
||||||
emoji,
|
|
||||||
REACTIONS.remove,
|
|
||||||
this.currentUser,
|
|
||||||
this.currentUser.id
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.reacting = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(roman): For backwards-compatibility.
|
|
||||||
// Remove after the 3.0 release.
|
|
||||||
_legacyFlag() {
|
|
||||||
this.dialog.yesNoConfirm({
|
|
||||||
message: I18n.t("chat.confirm_flag", {
|
|
||||||
username: this.args.message.user?.username,
|
|
||||||
}),
|
|
||||||
didConfirm: () => {
|
|
||||||
return ajax("/chat/flag", {
|
|
||||||
method: "PUT",
|
|
||||||
data: {
|
|
||||||
chat_message_id: this.args.message.id,
|
|
||||||
flag_type_id: 7, // notify_moderators
|
|
||||||
},
|
|
||||||
}).catch(popupAjaxError);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
reply() {
|
|
||||||
this.args.setReplyTo(this.args.message.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
edit() {
|
|
||||||
this.args.editButtonClicked(this.args.message.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
flag() {
|
|
||||||
const targetFlagSupported =
|
|
||||||
requirejs.entries["discourse/lib/flag-targets/flag"];
|
|
||||||
|
|
||||||
if (targetFlagSupported) {
|
|
||||||
const model = this.args.message;
|
|
||||||
model.username = model.user?.username;
|
|
||||||
model.user_id = model.user?.id;
|
|
||||||
let controller = showModal("flag", { model });
|
|
||||||
controller.set("flagTarget", new ChatMessageFlag());
|
|
||||||
} else {
|
|
||||||
this._legacyFlag();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
expand() {
|
|
||||||
this.args.message.expanded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
restore() {
|
|
||||||
return ajax(
|
|
||||||
`/chat/${this.args.message.channelId}/restore/${this.args.message.id}`,
|
|
||||||
{
|
|
||||||
type: "PUT",
|
|
||||||
}
|
|
||||||
).catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
openThread() {
|
|
||||||
this.router.transitionTo("chat.channel.thread", this.args.message.threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleBookmark() {
|
|
||||||
return openBookmarkModal(
|
|
||||||
this.args.message.bookmark ||
|
|
||||||
Bookmark.createFor(
|
|
||||||
this.currentUser,
|
|
||||||
"Chat::Message",
|
|
||||||
this.args.message.id
|
|
||||||
),
|
|
||||||
{
|
|
||||||
onAfterSave: (savedData) => {
|
|
||||||
const bookmark = Bookmark.create(savedData);
|
|
||||||
this.args.message.bookmark = bookmark;
|
|
||||||
this.appEvents.trigger(
|
|
||||||
"bookmarks:changed",
|
|
||||||
savedData,
|
|
||||||
bookmark.attachedTo()
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onAfterDelete: () => {
|
|
||||||
this.args.message.bookmark = null;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
rebakeMessage() {
|
|
||||||
return ajax(
|
|
||||||
`/chat/${this.args.message.channelId}/${this.args.message.id}/rebake`,
|
|
||||||
{
|
|
||||||
type: "PUT",
|
|
||||||
}
|
|
||||||
).catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
deleteMessage() {
|
|
||||||
return this.chatApi
|
|
||||||
.trashMessage(this.args.message.channelId, this.args.message.id)
|
|
||||||
.catch(popupAjaxError);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
selectMessage() {
|
|
||||||
this.args.message.selected = true;
|
|
||||||
this.args.onStartSelectingMessages(this.args.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
toggleChecked(e) {
|
|
||||||
if (e.shiftKey) {
|
|
||||||
this.args.bulkSelectMessages(this.args.message, e.target.checked);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.args.onSelectMessage(this.args.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
copyLinkToMessage() {
|
|
||||||
if (!this.messageContainer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messageContainer
|
|
||||||
.querySelector(".link-to-message-btn")
|
|
||||||
?.classList?.add("copied");
|
|
||||||
|
|
||||||
const { protocol, host } = window.location;
|
|
||||||
let url = getURL(
|
|
||||||
`/chat/c/-/${this.args.message.channelId}/${this.args.message.id}`
|
|
||||||
);
|
|
||||||
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
|
|
||||||
clipboardCopy(url);
|
|
||||||
|
|
||||||
discourseLater(() => {
|
|
||||||
this.messageContainer
|
|
||||||
?.querySelector(".link-to-message-btn")
|
|
||||||
?.classList?.remove("copied");
|
|
||||||
}, 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
get emojiReactions() {
|
|
||||||
let favorites = this.cachedFavoritesReactions;
|
|
||||||
|
|
||||||
// may be a {} if no defaults defined in some production builds
|
|
||||||
if (!favorites || !favorites.slice) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return favorites.slice(0, 3).map((emoji) => {
|
|
||||||
return (
|
|
||||||
this.args.message.reactions.find(
|
|
||||||
(reaction) => reaction.emoji === emoji
|
|
||||||
) ||
|
|
||||||
ChatMessageReaction.create({
|
|
||||||
emoji,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,13 @@ import { schedule } from "@ember/runloop";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
|
|
||||||
export default class AdminCustomizeColorsShowController extends Component {
|
export default class ChatSelectionManager extends Component {
|
||||||
@service router;
|
@service router;
|
||||||
tagName = "";
|
tagName = "";
|
||||||
chatChannel = null;
|
chatChannel = null;
|
||||||
|
context = null;
|
||||||
selectedMessageIds = null;
|
selectedMessageIds = null;
|
||||||
chatCopySuccess = false;
|
chatCopySuccess = false;
|
||||||
showChatCopySuccess = false;
|
showChatCopySuccess = false;
|
||||||
@ -28,7 +30,9 @@ export default class AdminCustomizeColorsShowController extends Component {
|
|||||||
@computed("chatChannel.isDirectMessageChannel", "chatChannel.canModerate")
|
@computed("chatChannel.isDirectMessageChannel", "chatChannel.canModerate")
|
||||||
get showMoveMessageButton() {
|
get showMoveMessageButton() {
|
||||||
return (
|
return (
|
||||||
!this.chatChannel.isDirectMessageChannel && this.chatChannel.canModerate
|
this.context !== MESSAGE_CONTEXT_THREAD &&
|
||||||
|
!this.chatChannel.isDirectMessageChannel &&
|
||||||
|
this.chatChannel.canModerate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,61 +2,58 @@
|
|||||||
class={{concat-class "chat-thread" (if this.loading "loading")}}
|
class={{concat-class "chat-thread" (if this.loading "loading")}}
|
||||||
data-id={{this.thread.id}}
|
data-id={{this.thread.id}}
|
||||||
{{did-insert this.loadMessages}}
|
{{did-insert this.loadMessages}}
|
||||||
|
{{did-update this.thread.id this.loadMessages}}
|
||||||
>
|
>
|
||||||
<div class="chat-thread__header">
|
<div class="chat-thread__header">
|
||||||
<div class="chat-thread__info">
|
<span class="chat-thread__label">{{i18n "chat.thread.label"}}</span>
|
||||||
<div class="chat-thread__title">
|
<LinkTo
|
||||||
<h2>{{this.title}}</h2>
|
class="chat-thread__close"
|
||||||
|
@route="chat.channel"
|
||||||
|
@models={{this.chat.activeChannel.routeModels}}
|
||||||
|
>
|
||||||
|
{{d-icon "times"}}
|
||||||
|
</LinkTo>
|
||||||
|
</div>
|
||||||
|
|
||||||
<LinkTo
|
<div class="chat-thread__body" {{did-insert this.setScrollable}}>
|
||||||
class="chat-thread__close"
|
<div
|
||||||
@route="chat.channel"
|
class="chat-thread__messages chat-messages-container"
|
||||||
@models={{this.chat.activeChannel.routeModels}}
|
{{chat/on-resize this.didResizePane (hash delay=10)}}
|
||||||
>
|
>
|
||||||
{{d-icon "times"}}
|
{{#each this.thread.messages key="id" as |message|}}
|
||||||
</LinkTo>
|
<ChatMessage
|
||||||
</div>
|
@message={{message}}
|
||||||
|
@channel={{this.channel}}
|
||||||
<p class="chat-thread__om">
|
@resendStagedMessage={{this.resendStagedMessage}}
|
||||||
{{replace-emoji this.thread.originalMessage.excerpt}}
|
@messageDidEnterViewport={{this.messageDidEnterViewport}}
|
||||||
</p>
|
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
|
||||||
|
@context="thread"
|
||||||
<div class="chat-thread__omu">
|
|
||||||
<span class="chat-thread__started-by">{{i18n
|
|
||||||
"chat.threads.started_by"
|
|
||||||
}}</span>
|
|
||||||
<ChatMessageAvatar
|
|
||||||
class="chat-thread__omu-avatar"
|
|
||||||
@message={{this.thread.originalMessage}}
|
|
||||||
/>
|
/>
|
||||||
<span
|
{{/each}}
|
||||||
class="chat-thread__omu-username"
|
{{#if (or this.loading this.loadingMoreFuture)}}
|
||||||
>{{this.thread.originalMessageUser.username}}</span>
|
<ChatSkeleton />
|
||||||
</div>
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-thread__messages">
|
|
||||||
<ul>
|
|
||||||
{{#each this.thread.messages as |message|}}
|
|
||||||
<li><strong>{{message.user.username}}</strong>: {{message.message}}</li>
|
|
||||||
{{/each}}
|
|
||||||
</ul>
|
|
||||||
{{#if (or this.loading this.loadingMoreFuture)}}
|
|
||||||
<ChatSkeleton />
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ChatComposer
|
{{#if this.chatChannelThreadPane.selectingMessages}}
|
||||||
@canInteractWithChat="true"
|
<ChatSelectionManager
|
||||||
@sendMessage={{this.sendMessage}}
|
@selectedMessageIds={{this.chatChannelThreadPane.selectedMessageIds}}
|
||||||
@editMessage={{this.editMessage}}
|
@chatChannel={{this.chat.activeChannel}}
|
||||||
@setReplyTo={{this.setReplyTo}}
|
@cancelSelecting={{action
|
||||||
@loading={{this.sendingLoading}}
|
this.chatChannelThreadPane.cancelSelecting
|
||||||
@editingMessage={{readonly this.editingMessage}}
|
this.chat.activeChannel.selectedMessages
|
||||||
@onCancelEditing={{this.cancelEditing}}
|
}}
|
||||||
@setInReplyToMsg={{this.setInReplyToMsg}}
|
@context="thread"
|
||||||
@onEditLastMessageRequested={{this.editLastMessageRequested}}
|
/>
|
||||||
@onValueChange={{this.composerValueChanged}}
|
{{else}}
|
||||||
@chatChannel={{this.channel}}
|
<ChatComposer
|
||||||
/>
|
@sendMessage={{this.sendMessage}}
|
||||||
|
@onCancelEditing={{this.cancelEditing}}
|
||||||
|
@chatChannel={{this.channel}}
|
||||||
|
@composerService={{this.chatChannelThreadComposer}}
|
||||||
|
@paneService={{this.chatChannelThreadPane}}
|
||||||
|
@context="thread"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
@ -6,8 +6,9 @@ import { action } from "@ember/object";
|
|||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { bind, debounce } from "discourse-common/utils/decorators";
|
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||||
import I18n from "I18n";
|
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
|
import { schedule } from "@ember/runloop";
|
||||||
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -18,11 +19,16 @@ export default class ChatThreadPanel extends Component {
|
|||||||
@service router;
|
@service router;
|
||||||
@service chatApi;
|
@service chatApi;
|
||||||
@service chatComposerPresenceManager;
|
@service chatComposerPresenceManager;
|
||||||
|
@service chatChannelThreadComposer;
|
||||||
|
@service chatChannelThreadPane;
|
||||||
@service appEvents;
|
@service appEvents;
|
||||||
|
@service capabilities;
|
||||||
|
|
||||||
@tracked loading;
|
@tracked loading;
|
||||||
@tracked loadingMorePast;
|
@tracked loadingMorePast;
|
||||||
|
|
||||||
|
scrollable = null;
|
||||||
|
|
||||||
get thread() {
|
get thread() {
|
||||||
return this.channel.activeThread;
|
return this.channel.activeThread;
|
||||||
}
|
}
|
||||||
@ -31,12 +37,9 @@ export default class ChatThreadPanel extends Component {
|
|||||||
return this.chat.activeChannel;
|
return this.chat.activeChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
get title() {
|
@action
|
||||||
if (this.thread.title) {
|
setScrollable(element) {
|
||||||
this.thread.escapedTitle;
|
this.scrollable = element;
|
||||||
}
|
|
||||||
|
|
||||||
return I18n.t("chat.threads.op_said");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -53,6 +56,11 @@ export default class ChatThreadPanel extends Component {
|
|||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
didResizePane() {
|
||||||
|
this.forceRendering();
|
||||||
|
}
|
||||||
|
|
||||||
get _selfDeleted() {
|
get _selfDeleted() {
|
||||||
return this.isDestroying || this.isDestroyed;
|
return this.isDestroying || this.isDestroyed;
|
||||||
}
|
}
|
||||||
@ -93,9 +101,6 @@ export default class ChatThreadPanel extends Component {
|
|||||||
const [messages, meta] = this.afterFetchCallback(this.channel, results);
|
const [messages, meta] = this.afterFetchCallback(this.channel, results);
|
||||||
this.thread.messagesManager.addMessages(messages);
|
this.thread.messagesManager.addMessages(messages);
|
||||||
|
|
||||||
// TODO (martin) ECHO MODE
|
|
||||||
this.channel.messagesManager.addMessages(messages);
|
|
||||||
|
|
||||||
// TODO (martin) details needed for thread??
|
// TODO (martin) details needed for thread??
|
||||||
this.thread.details = meta;
|
this.thread.details = meta;
|
||||||
|
|
||||||
@ -127,7 +132,6 @@ export default class ChatThreadPanel extends Component {
|
|||||||
@bind
|
@bind
|
||||||
afterFetchCallback(channel, results) {
|
afterFetchCallback(channel, results) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
let foundFirstNew = false;
|
|
||||||
|
|
||||||
results.chat_messages.forEach((messageData) => {
|
results.chat_messages.forEach((messageData) => {
|
||||||
// If a message has been hidden it is because the current user is ignoring
|
// If a message has been hidden it is because the current user is ignoring
|
||||||
@ -145,16 +149,6 @@ export default class ChatThreadPanel extends Component {
|
|||||||
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
||||||
}
|
}
|
||||||
|
|
||||||
// newest has to be in after fetch callback as we don't want to make it
|
|
||||||
// dynamic or it will make the pane jump around, it will disappear on reload
|
|
||||||
if (
|
|
||||||
!foundFirstNew &&
|
|
||||||
messageData.id > channel.currentUserMembership.last_read_message_id
|
|
||||||
) {
|
|
||||||
foundFirstNew = true;
|
|
||||||
messageData.newest = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
messages.push(ChatMessage.create(channel, messageData));
|
messages.push(ChatMessage.create(channel, messageData));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -165,11 +159,11 @@ export default class ChatThreadPanel extends Component {
|
|||||||
sendMessage(message, uploads = []) {
|
sendMessage(message, uploads = []) {
|
||||||
// TODO (martin) For desktop notifications
|
// TODO (martin) For desktop notifications
|
||||||
// resetIdle()
|
// resetIdle()
|
||||||
if (this.sendingLoading) {
|
if (this.chatChannelThreadPane.sendingLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendingLoading = true;
|
this.chatChannelThreadPane.sendingLoading = true;
|
||||||
this.channel.draft = ChatMessageDraft.create();
|
this.channel.draft = ChatMessageDraft.create();
|
||||||
|
|
||||||
// TODO (martin) Handling case when channel is not followed???? IDK if we
|
// TODO (martin) Handling case when channel is not followed???? IDK if we
|
||||||
@ -199,8 +193,7 @@ export default class ChatThreadPanel extends Component {
|
|||||||
thread_id: stagedMessage.threadId,
|
thread_id: stagedMessage.threadId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// TODO (martin) Scrolling!!
|
this.scrollToBottom();
|
||||||
// this.scrollToBottom();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.#onSendError(stagedMessage.stagedId, error);
|
this.#onSendError(stagedMessage.stagedId, error);
|
||||||
@ -209,35 +202,70 @@ export default class ChatThreadPanel extends Component {
|
|||||||
if (this._selfDeleted) {
|
if (this._selfDeleted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.sendingLoading = false;
|
this.chatChannelThreadPane.sendingLoading = false;
|
||||||
this.#resetAfterSend();
|
this.chatChannelThreadPane.resetAfterSend();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A more consistent way to scroll to the bottom when we are sure this is our goal
|
||||||
|
// it will also limit issues with any element changing the height while we are scrolling
|
||||||
|
// to the bottom
|
||||||
@action
|
@action
|
||||||
editMessage() {}
|
scrollToBottom() {
|
||||||
// editMessage(chatMessage, newContent, uploads) {}
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
this.scrollable.scrollTop = -1;
|
||||||
setReplyTo() {}
|
this.forceRendering(() => {
|
||||||
// setReplyTo(messageId) {}
|
this.scrollable.scrollTop = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
// since -webkit-overflow-scrolling: touch can't be used anymore to disable momentum scrolling
|
||||||
setInReplyToMsg(inReplyMsg) {
|
// we now use this hack to disable it
|
||||||
this.replyToMsg = inReplyMsg;
|
@bind
|
||||||
|
forceRendering(callback) {
|
||||||
|
schedule("afterRender", () => {
|
||||||
|
if (this._selfDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.capabilities.isIOS) {
|
||||||
|
this.scrollable.style.overflow = "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
callback?.();
|
||||||
|
|
||||||
|
if (this.capabilities.isIOS) {
|
||||||
|
discourseLater(() => {
|
||||||
|
if (!this.scrollable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scrollable.style.overflow = "auto";
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
cancelEditing() {
|
resendStagedMessage() {}
|
||||||
this.editingMessage = null;
|
// resendStagedMessage(stagedMessage) {}
|
||||||
|
|
||||||
|
@action
|
||||||
|
messageDidEnterViewport(message) {
|
||||||
|
message.visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
editLastMessageRequested() {}
|
messageDidLeaveViewport(message) {
|
||||||
|
message.visible = false;
|
||||||
@action
|
}
|
||||||
composerValueChanged() {}
|
|
||||||
// composerValueChanged(value, uploads, replyToMsg) {}
|
|
||||||
|
|
||||||
#handleErrors(error) {
|
#handleErrors(error) {
|
||||||
switch (error?.jqXHR?.status) {
|
switch (error?.jqXHR?.status) {
|
||||||
@ -262,17 +290,6 @@ export default class ChatThreadPanel extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#resetAfterSend();
|
this.chatChannelThreadPane.resetAfterSend();
|
||||||
}
|
|
||||||
|
|
||||||
#resetAfterSend() {
|
|
||||||
if (this._selfDeleted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.replyToMsg = null;
|
|
||||||
this.editingMessage = null;
|
|
||||||
this.chatComposerPresenceManager.notifyState(this.channel.id, false);
|
|
||||||
this.appEvents.trigger("chat-composer:reply-to-set", null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<ChatChannelMessageEmojiPicker />
|
@ -0,0 +1 @@
|
|||||||
|
<ChatMessageActionsDesktop />
|
@ -0,0 +1 @@
|
|||||||
|
<ChatMessageActionsMobile />
|
@ -60,11 +60,27 @@ export default {
|
|||||||
class: "chat-emoji-btn",
|
class: "chat-emoji-btn",
|
||||||
icon: "discourse-emojis",
|
icon: "discourse-emojis",
|
||||||
position: "dropdown",
|
position: "dropdown",
|
||||||
|
context: "channel",
|
||||||
action() {
|
action() {
|
||||||
const chatEmojiPickerManager = container.lookup(
|
const chatEmojiPickerManager = container.lookup(
|
||||||
"service:chat-emoji-picker-manager"
|
"service:chat-emoji-picker-manager"
|
||||||
);
|
);
|
||||||
chatEmojiPickerManager.startFromComposer(this.didSelectEmoji);
|
chatEmojiPickerManager.open({ context: "channel" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerChatComposerButton({
|
||||||
|
label: "chat.emoji",
|
||||||
|
id: "channel-emoji",
|
||||||
|
class: "chat-emoji-btn",
|
||||||
|
icon: "discourse-emojis",
|
||||||
|
position: "dropdown",
|
||||||
|
context: "thread",
|
||||||
|
action() {
|
||||||
|
const chatEmojiPickerManager = container.lookup(
|
||||||
|
"service:chat-emoji-picker-manager"
|
||||||
|
);
|
||||||
|
chatEmojiPickerManager.open({ context: "thread" });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -66,54 +66,60 @@ export function chatComposerButtonsDependentKeys() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function chatComposerButtons(context, position) {
|
export function chatComposerButtons(composer, position, context) {
|
||||||
return Object.values(_chatComposerButtons)
|
return Object.values(_chatComposerButtons)
|
||||||
.filter(
|
.filter((button) => {
|
||||||
(button) =>
|
let valid =
|
||||||
computeButton(context, button, "displayed") &&
|
computeButton(composer, button, "displayed") &&
|
||||||
computeButton(context, button, "position") === position
|
computeButton(composer, button, "position") === position;
|
||||||
)
|
|
||||||
|
if (button.context) {
|
||||||
|
valid = valid && computeButton(composer, button, "context") === context;
|
||||||
|
}
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
})
|
||||||
.map((button) => {
|
.map((button) => {
|
||||||
const result = { id: button.id };
|
const result = { id: button.id };
|
||||||
|
|
||||||
const label = computeButton(context, button, "label");
|
const label = computeButton(composer, button, "label");
|
||||||
result.label = label
|
result.label = label
|
||||||
? label
|
? label
|
||||||
: computeButton(context, button, "translatedLabel");
|
: computeButton(composer, button, "translatedLabel");
|
||||||
|
|
||||||
const ariaLabel = computeButton(context, button, "ariaLabel");
|
const ariaLabel = computeButton(composer, button, "ariaLabel");
|
||||||
if (ariaLabel) {
|
if (ariaLabel) {
|
||||||
result.ariaLabel = I18n.t(ariaLabel);
|
result.ariaLabel = I18n.t(ariaLabel);
|
||||||
} else {
|
} else {
|
||||||
const translatedAriaLabel = computeButton(
|
const translatedAriaLabel = computeButton(
|
||||||
context,
|
composer,
|
||||||
button,
|
button,
|
||||||
"translatedAriaLabel"
|
"translatedAriaLabel"
|
||||||
);
|
);
|
||||||
result.ariaLabel = translatedAriaLabel || result.label;
|
result.ariaLabel = translatedAriaLabel || result.label;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = computeButton(context, button, "title");
|
const title = computeButton(composer, button, "title");
|
||||||
result.title = title
|
result.title = title
|
||||||
? I18n.t(title)
|
? I18n.t(title)
|
||||||
: computeButton(context, button, "translatedTitle");
|
: computeButton(composer, button, "translatedTitle");
|
||||||
|
|
||||||
result.classNames = (
|
result.classNames = (
|
||||||
computeButton(context, button, "classNames") || []
|
computeButton(composer, button, "classNames") || []
|
||||||
).join(" ");
|
).join(" ");
|
||||||
|
|
||||||
result.icon = computeButton(context, button, "icon");
|
result.icon = computeButton(composer, button, "icon");
|
||||||
result.disabled = computeButton(context, button, "disabled");
|
result.disabled = computeButton(composer, button, "disabled");
|
||||||
result.priority = computeButton(context, button, "priority");
|
result.priority = computeButton(composer, button, "priority");
|
||||||
|
|
||||||
if (isFunction(button.action)) {
|
if (isFunction(button.action)) {
|
||||||
result.action = () => {
|
result.action = () => {
|
||||||
button.action.apply(context);
|
button.action.apply(composer);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const actionName = button.action;
|
const actionName = button.action;
|
||||||
result.action = () => {
|
result.action = () => {
|
||||||
context[actionName]();
|
composer[actionName]();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
|
|
||||||
|
export default function chatMessageContainer(id, context) {
|
||||||
|
let selector;
|
||||||
|
|
||||||
|
if (context === MESSAGE_CONTEXT_THREAD) {
|
||||||
|
selector = `.chat-thread .chat-message-container[data-id="${id}"]`;
|
||||||
|
} else {
|
||||||
|
selector = `.chat-live-pane .chat-message-container[data-id="${id}"]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
@ -0,0 +1,399 @@
|
|||||||
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
|
import showModal from "discourse/lib/show-modal";
|
||||||
|
import ChatMessageFlag from "discourse/plugins/chat/discourse/lib/chat-message-flag";
|
||||||
|
import Bookmark from "discourse/models/bookmark";
|
||||||
|
import { openBookmarkModal } from "discourse/controllers/bookmark";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
import { clipboardCopy } from "discourse/lib/utilities";
|
||||||
|
import ChatMessageReaction, {
|
||||||
|
REACTIONS,
|
||||||
|
} from "discourse/plugins/chat/discourse/models/chat-message-reaction";
|
||||||
|
import { getOwner, setOwner } from "@ember/application";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
|
import I18n from "I18n";
|
||||||
|
|
||||||
|
export default class ChatMessageInteractor {
|
||||||
|
@service appEvents;
|
||||||
|
@service dialog;
|
||||||
|
@service chat;
|
||||||
|
@service chatEmojiReactionStore;
|
||||||
|
@service chatEmojiPickerManager;
|
||||||
|
@service chatChannelComposer;
|
||||||
|
@service chatChannelThreadComposer;
|
||||||
|
@service chatChannelPane;
|
||||||
|
@service chatChannelThreadPane;
|
||||||
|
@service chatApi;
|
||||||
|
@service currentUser;
|
||||||
|
@service site;
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
@tracked message = null;
|
||||||
|
@tracked context = null;
|
||||||
|
|
||||||
|
cachedFavoritesReactions = null;
|
||||||
|
|
||||||
|
constructor(owner, message, context) {
|
||||||
|
setOwner(this, owner);
|
||||||
|
|
||||||
|
this.message = message;
|
||||||
|
this.context = context;
|
||||||
|
this.cachedFavoritesReactions = this.chatEmojiReactionStore.favorites;
|
||||||
|
}
|
||||||
|
|
||||||
|
get capabilities() {
|
||||||
|
return getOwner(this).lookup("capabilities:main");
|
||||||
|
}
|
||||||
|
|
||||||
|
get pane() {
|
||||||
|
return this.context === MESSAGE_CONTEXT_THREAD
|
||||||
|
? this.chatChannelThreadPane
|
||||||
|
: this.chatChannelPane;
|
||||||
|
}
|
||||||
|
|
||||||
|
get emojiReactions() {
|
||||||
|
let favorites = this.cachedFavoritesReactions;
|
||||||
|
|
||||||
|
// may be a {} if no defaults defined in some production builds
|
||||||
|
if (!favorites || !favorites.slice) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return favorites.slice(0, 3).map((emoji) => {
|
||||||
|
return (
|
||||||
|
this.message.reactions.find((reaction) => reaction.emoji === emoji) ||
|
||||||
|
ChatMessageReaction.create({ emoji })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get canEdit() {
|
||||||
|
return (
|
||||||
|
!this.message.deletedAt &&
|
||||||
|
this.currentUser.id === this.message.user.id &&
|
||||||
|
this.message.channel?.canModifyMessages?.(this.currentUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canInteractWithMessage() {
|
||||||
|
return (
|
||||||
|
!this.message?.deletedAt &&
|
||||||
|
this.message?.channel?.canModifyMessages(this.currentUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canRestoreMessage() {
|
||||||
|
return (
|
||||||
|
this.canDelete &&
|
||||||
|
this.message?.deletedAt &&
|
||||||
|
this.message.channel?.canModifyMessages?.(this.currentUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canBookmark() {
|
||||||
|
return this.message?.channel?.canModifyMessages?.(this.currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canReply() {
|
||||||
|
return (
|
||||||
|
this.canInteractWithMessage && this.context !== MESSAGE_CONTEXT_THREAD
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canReact() {
|
||||||
|
return this.canInteractWithMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canFlagMessage() {
|
||||||
|
return (
|
||||||
|
this.currentUser?.id !== this.message?.user?.id &&
|
||||||
|
!this.message.channel?.isDirectMessageChannel &&
|
||||||
|
this.message?.userFlagStatus === undefined &&
|
||||||
|
this.message.channel?.canFlag &&
|
||||||
|
!this.message?.chatWebhookEvent &&
|
||||||
|
!this.message?.deletedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canOpenThread() {
|
||||||
|
return (
|
||||||
|
this.context !== MESSAGE_CONTEXT_THREAD &&
|
||||||
|
this.message.channel?.threadingEnabled &&
|
||||||
|
this.message?.threadId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canRebakeMessage() {
|
||||||
|
return (
|
||||||
|
this.currentUser?.staff &&
|
||||||
|
this.message.channel?.canModifyMessages?.(this.currentUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDeleteMessage() {
|
||||||
|
return (
|
||||||
|
this.canDelete &&
|
||||||
|
!this.message?.deletedAt &&
|
||||||
|
this.message.channel?.canModifyMessages?.(this.currentUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get canDelete() {
|
||||||
|
return this.currentUser?.id === this.message.user.id
|
||||||
|
? this.message.channel?.canDeleteSelf
|
||||||
|
: this.message.channel?.canDeleteOthers;
|
||||||
|
}
|
||||||
|
|
||||||
|
get composer() {
|
||||||
|
return this.context === MESSAGE_CONTEXT_THREAD
|
||||||
|
? this.chatChannelThreadComposer
|
||||||
|
: this.chatChannelComposer;
|
||||||
|
}
|
||||||
|
|
||||||
|
get secondaryButtons() {
|
||||||
|
const buttons = [];
|
||||||
|
|
||||||
|
buttons.push({
|
||||||
|
id: "copyLink",
|
||||||
|
name: I18n.t("chat.copy_link"),
|
||||||
|
icon: "link",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.canEdit) {
|
||||||
|
buttons.push({
|
||||||
|
id: "edit",
|
||||||
|
name: I18n.t("chat.edit"),
|
||||||
|
icon: "pencil-alt",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.pane.selectingMessages) {
|
||||||
|
buttons.push({
|
||||||
|
id: "select",
|
||||||
|
name: I18n.t("chat.select"),
|
||||||
|
icon: "tasks",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canFlagMessage) {
|
||||||
|
buttons.push({
|
||||||
|
id: "flag",
|
||||||
|
name: I18n.t("chat.flag"),
|
||||||
|
icon: "flag",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canDeleteMessage) {
|
||||||
|
buttons.push({
|
||||||
|
id: "delete",
|
||||||
|
name: I18n.t("chat.delete"),
|
||||||
|
icon: "trash-alt",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canRestoreMessage) {
|
||||||
|
buttons.push({
|
||||||
|
id: "restore",
|
||||||
|
name: I18n.t("chat.restore"),
|
||||||
|
icon: "undo",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canRebakeMessage) {
|
||||||
|
buttons.push({
|
||||||
|
id: "rebake",
|
||||||
|
name: I18n.t("chat.rebake_message"),
|
||||||
|
icon: "sync-alt",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canOpenThread) {
|
||||||
|
buttons.push({
|
||||||
|
id: "openThread",
|
||||||
|
name: I18n.t("chat.threads.open"),
|
||||||
|
icon: "puzzle-piece",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
select(checked = true) {
|
||||||
|
this.message.selected = checked;
|
||||||
|
this.pane.onSelectMessage(this.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
bulkSelect(checked) {
|
||||||
|
const channel = this.message.channel;
|
||||||
|
const lastSelectedIndex = channel.findIndexOfMessage(
|
||||||
|
this.pane.lastSelectedMessage
|
||||||
|
);
|
||||||
|
const newlySelectedIndex = channel.findIndexOfMessage(this.message);
|
||||||
|
const sortedIndices = [lastSelectedIndex, newlySelectedIndex].sort(
|
||||||
|
(a, b) => a - b
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = sortedIndices[0]; i <= sortedIndices[1]; i++) {
|
||||||
|
channel.messages[i].selected = checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyLink() {
|
||||||
|
const { protocol, host } = window.location;
|
||||||
|
let url = getURL(`/chat/c/-/${this.message.channelId}/${this.message.id}`);
|
||||||
|
url = url.indexOf("/") === 0 ? protocol + "//" + host + url : url;
|
||||||
|
clipboardCopy(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
react(emoji, reactAction) {
|
||||||
|
if (!this.chat.userCanInteractWithChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pane.reacting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.capabilities.canVibrate && !isTesting()) {
|
||||||
|
navigator.vibrate(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.site.mobileView) {
|
||||||
|
this.chat.activeMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactAction === REACTIONS.add) {
|
||||||
|
this.chatEmojiReactionStore.track(`:${emoji}:`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pane.reacting = true;
|
||||||
|
|
||||||
|
this.message.react(
|
||||||
|
emoji,
|
||||||
|
reactAction,
|
||||||
|
this.currentUser,
|
||||||
|
this.currentUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.chatApi
|
||||||
|
.publishReaction(
|
||||||
|
this.message.channelId,
|
||||||
|
this.message.id,
|
||||||
|
emoji,
|
||||||
|
reactAction
|
||||||
|
)
|
||||||
|
.catch((errResult) => {
|
||||||
|
popupAjaxError(errResult);
|
||||||
|
this.message.react(
|
||||||
|
emoji,
|
||||||
|
REACTIONS.remove,
|
||||||
|
this.currentUser,
|
||||||
|
this.currentUser.id
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.pane.reacting = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
toggleBookmark() {
|
||||||
|
return openBookmarkModal(
|
||||||
|
this.message.bookmark ||
|
||||||
|
Bookmark.createFor(this.currentUser, "Chat::Message", this.message.id),
|
||||||
|
{
|
||||||
|
onAfterSave: (savedData) => {
|
||||||
|
const bookmark = Bookmark.create(savedData);
|
||||||
|
this.message.bookmark = bookmark;
|
||||||
|
this.appEvents.trigger(
|
||||||
|
"bookmarks:changed",
|
||||||
|
savedData,
|
||||||
|
bookmark.attachedTo()
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAfterDelete: () => {
|
||||||
|
this.message.bookmark = null;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
flag() {
|
||||||
|
const model = new ChatMessage(this.message.channel, this.message);
|
||||||
|
model.username = this.message.user?.username;
|
||||||
|
model.user_id = this.message.user?.id;
|
||||||
|
const controller = showModal("flag", { model });
|
||||||
|
controller.set("flagTarget", new ChatMessageFlag());
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
delete() {
|
||||||
|
return this.chatApi
|
||||||
|
.trashMessage(this.message.channelId, this.message.id)
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
restore() {
|
||||||
|
return this.chatApi
|
||||||
|
.restoreMessage(this.message.channelId, this.message.id)
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
rebake() {
|
||||||
|
return this.chatApi
|
||||||
|
.rebakeMessage(this.message.channelId, this.message.id)
|
||||||
|
.catch(popupAjaxError);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
reply() {
|
||||||
|
this.composer.setReplyTo(this.message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
edit() {
|
||||||
|
this.composer.editButtonClicked(this.message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openThread() {
|
||||||
|
this.router.transitionTo(
|
||||||
|
"chat.channel.thread",
|
||||||
|
...this.message.channel.routeModels,
|
||||||
|
this.message.threadId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
openEmojiPicker(_, { target }) {
|
||||||
|
const pickerState = {
|
||||||
|
didSelectEmoji: this.selectReaction,
|
||||||
|
trigger: target,
|
||||||
|
context: "chat-channel-message",
|
||||||
|
};
|
||||||
|
this.chatEmojiPickerManager.open(pickerState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@bind
|
||||||
|
selectReaction(emoji) {
|
||||||
|
if (!this.chat.userCanInteractWithChat) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.react(emoji, REACTIONS.add);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
handleSecondaryButtons(id) {
|
||||||
|
this[id](this.message);
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,10 @@ export default class ChatChannel extends RestModel {
|
|||||||
threadsManager = new ChatThreadsManager(getOwner(this));
|
threadsManager = new ChatThreadsManager(getOwner(this));
|
||||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||||
|
|
||||||
|
findIndexOfMessage(message) {
|
||||||
|
return this.messages.findIndex((m) => m.id === message.id);
|
||||||
|
}
|
||||||
|
|
||||||
get messages() {
|
get messages() {
|
||||||
return this.messagesManager.messages;
|
return this.messagesManager.messages;
|
||||||
}
|
}
|
||||||
@ -90,6 +94,10 @@ export default class ChatChannel extends RestModel {
|
|||||||
return [this.slugifiedTitle, this.id];
|
return [this.slugifiedTitle, this.id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedMessages() {
|
||||||
|
return this.messages.filter((message) => message.selected);
|
||||||
|
}
|
||||||
|
|
||||||
get isDirectMessageChannel() {
|
get isDirectMessageChannel() {
|
||||||
return this.chatableType === CHATABLE_TYPES.directMessageChannel;
|
return this.chatableType === CHATABLE_TYPES.directMessageChannel;
|
||||||
}
|
}
|
||||||
@ -186,6 +194,7 @@ ChatChannel.reopenClass({
|
|||||||
this._remapKey(args, "chatable_type", "chatableType");
|
this._remapKey(args, "chatable_type", "chatableType");
|
||||||
this._remapKey(args, "memberships_count", "membershipsCount");
|
this._remapKey(args, "memberships_count", "membershipsCount");
|
||||||
this._remapKey(args, "last_message_sent_at", "lastMessageSentAt");
|
this._remapKey(args, "last_message_sent_at", "lastMessageSentAt");
|
||||||
|
this._remapKey(args, "threading_enabled", "threadingEnabled");
|
||||||
|
|
||||||
return this._super(args);
|
return this._super(args);
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,8 @@ import { tracked } from "@glimmer/tracking";
|
|||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
|
|
||||||
|
export const REACTIONS = { add: "add", remove: "remove" };
|
||||||
|
|
||||||
export default class ChatMessageReaction {
|
export default class ChatMessageReaction {
|
||||||
static create(args = {}) {
|
static create(args = {}) {
|
||||||
return new ChatMessageReaction(args);
|
return new ChatMessageReaction(args);
|
||||||
|
@ -56,19 +56,20 @@ export default class ChatMessage {
|
|||||||
this.firstOfResults = args.firstOfResults;
|
this.firstOfResults = args.firstOfResults;
|
||||||
this.staged = args.staged;
|
this.staged = args.staged;
|
||||||
this.edited = args.edited;
|
this.edited = args.edited;
|
||||||
this.availableFlags = args.available_flags;
|
this.availableFlags = args.availableFlags || args.available_flags;
|
||||||
this.hidden = args.hidden;
|
this.hidden = args.hidden;
|
||||||
this.threadId = args.thread_id;
|
this.threadId = args.threadId || args.thread_id;
|
||||||
this.channelId = args.chat_channel_id;
|
this.channelId = args.channelId || args.chat_channel_id;
|
||||||
this.chatWebhookEvent = args.chat_webhook_event;
|
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
|
||||||
this.createdAt = args.created_at;
|
this.createdAt = args.createdAt || args.created_at;
|
||||||
this.deletedAt = args.deleted_at;
|
this.deletedAt = args.deletedAt || args.deleted_at;
|
||||||
this.excerpt = args.excerpt;
|
this.excerpt = args.excerpt;
|
||||||
this.reviewableId = args.reviewable_id;
|
this.reviewableId = args.reviewableId || args.reviewable_id;
|
||||||
this.userFlagStatus = args.user_flag_status;
|
this.userFlagStatus = args.userFlagStatus || args.user_flag_status;
|
||||||
this.inReplyTo = args.in_reply_to
|
this.inReplyTo =
|
||||||
? ChatMessage.create(channel, args.in_reply_to)
|
args.inReplyTo || args.in_reply_to
|
||||||
: null;
|
? ChatMessage.create(channel, args.in_reply_to)
|
||||||
|
: null;
|
||||||
this.message = args.message;
|
this.message = args.message;
|
||||||
this.cooked = args.cooked || ChatMessage.cookFunction(this.message);
|
this.cooked = args.cooked || ChatMessage.cookFunction(this.message);
|
||||||
this.reactions = this.#initChatMessageReactionModel(
|
this.reactions = this.#initChatMessageReactionModel(
|
||||||
|
@ -20,12 +20,10 @@ export default class ChatThread {
|
|||||||
constructor(args = {}) {
|
constructor(args = {}) {
|
||||||
this.title = args.title;
|
this.title = args.title;
|
||||||
this.id = args.id;
|
this.id = args.id;
|
||||||
|
this.channelId = args.channel_id;
|
||||||
this.status = args.status;
|
this.status = args.status;
|
||||||
|
|
||||||
this.originalMessageUser = this.#initUserModel(args.original_message_user);
|
this.originalMessageUser = this.#initUserModel(args.original_message_user);
|
||||||
|
|
||||||
// TODO (martin) Not sure if ChatMessage is needed here, original_message
|
|
||||||
// only has a small subset of message stuff.
|
|
||||||
this.originalMessage = args.original_message;
|
this.originalMessage = args.original_message;
|
||||||
this.originalMessage.user = this.originalMessageUser;
|
this.originalMessage.user = this.originalMessageUser;
|
||||||
}
|
}
|
||||||
@ -38,6 +36,10 @@ export default class ChatThread {
|
|||||||
this.messagesManager.messages = messages;
|
this.messagesManager.messages = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get selectedMessages() {
|
||||||
|
return this.messages.filter((message) => message.selected);
|
||||||
|
}
|
||||||
|
|
||||||
get escapedTitle() {
|
get escapedTitle() {
|
||||||
return escapeExpression(this.title);
|
return escapeExpression(this.title);
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,10 @@ export default function withChatChannel(extendedClass) {
|
|||||||
this.controllerFor("chat-channel").set("targetMessageId", null);
|
this.controllerFor("chat-channel").set("targetMessageId", null);
|
||||||
this.chat.activeChannel = model;
|
this.chat.activeChannel = model;
|
||||||
|
|
||||||
|
if (!model) {
|
||||||
|
return this.router.replaceWith("chat");
|
||||||
|
}
|
||||||
|
|
||||||
let { messageId, channelTitle } = this.paramsFor(this.routeName);
|
let { messageId, channelTitle } = this.paramsFor(this.routeName);
|
||||||
|
|
||||||
// messageId query param backwards-compatibility
|
// messageId query param backwards-compatibility
|
||||||
|
@ -22,6 +22,7 @@ export default class ChatRoute extends DiscourseRoute {
|
|||||||
|
|
||||||
const INTERCEPTABLE_ROUTES = [
|
const INTERCEPTABLE_ROUTES = [
|
||||||
"chat.channel",
|
"chat.channel",
|
||||||
|
"chat.channel.thread",
|
||||||
"chat.channel.index",
|
"chat.channel.index",
|
||||||
"chat.channel.near-message",
|
"chat.channel.near-message",
|
||||||
"chat.channel-legacy",
|
"chat.channel-legacy",
|
||||||
|
@ -296,6 +296,96 @@ export default class ChatApi extends Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a draft for the channel, which includes message contents and uploads.
|
||||||
|
* @param {number} channelId - The ID of the channel.
|
||||||
|
* @param {object} data - The draft data, see ChatMessageDraft.toJSON() for more details.
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
saveDraft(channelId, data) {
|
||||||
|
// TODO (martin) Change this to postRequest after moving DraftsController into Api::DraftsController
|
||||||
|
return ajax("/chat/drafts", {
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
chat_channel_id: channelId,
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
ignoreUnsent: false,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
this.chat.markNetworkAsReliable();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// we ignore a draft which can't be saved because it's too big
|
||||||
|
// and only deal with network error for now
|
||||||
|
if (!error.jqXHR?.responseJSON?.errors?.length) {
|
||||||
|
this.chat.markNetworkAsUnreliable();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or removes an emoji reaction for a message inside a channel.
|
||||||
|
* @param {number} channelId - The ID of the channel.
|
||||||
|
* @param {number} messageId - The ID of the message to react on.
|
||||||
|
* @param {string} emoji - The text version of the emoji without colons, e.g. tada
|
||||||
|
* @param {string} reaction - Either "add" or "remove"
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
publishReaction(channelId, messageId, emoji, reactAction) {
|
||||||
|
// TODO (martin) Not ideal, this should have a chat API controller endpoint.
|
||||||
|
return ajax(`/chat/${channelId}/react/${messageId}`, {
|
||||||
|
type: "PUT",
|
||||||
|
data: {
|
||||||
|
react_action: reactAction,
|
||||||
|
emoji,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a single deleted chat message in a channel.
|
||||||
|
*
|
||||||
|
* @param {number} channelId - The ID of the channel for the message being restored.
|
||||||
|
* @param {number} messageId - The ID of the message being restored.
|
||||||
|
*/
|
||||||
|
restoreMessage(channelId, messageId) {
|
||||||
|
// TODO (martin) Not ideal, this should have a chat API controller endpoint.
|
||||||
|
return ajax(`/chat/${channelId}/restore/${messageId}`, {
|
||||||
|
type: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebakes the cooked HTML of a single message in a channel.
|
||||||
|
*
|
||||||
|
* @param {number} channelId - The ID of the channel for the message being restored.
|
||||||
|
* @param {number} messageId - The ID of the message being restored.
|
||||||
|
*/
|
||||||
|
rebakeMessage(channelId, messageId) {
|
||||||
|
// TODO (martin) Not ideal, this should have a chat API controller endpoint.
|
||||||
|
return ajax(`/chat/${channelId}/${messageId}/rebake`, {
|
||||||
|
type: "PUT",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an edit to a message's contents in a channel.
|
||||||
|
*
|
||||||
|
* @param {number} channelId - The ID of the channel for the message being edited.
|
||||||
|
* @param {number} messageId - The ID of the message being edited.
|
||||||
|
* @param {object} data - Params of the edit.
|
||||||
|
* @param {string} data.new_message - The edited content of the message.
|
||||||
|
* @param {Array<number>} data.upload_ids - The uploads attached to the message after editing.
|
||||||
|
*/
|
||||||
|
editMessage(channelId, messageId, data) {
|
||||||
|
// TODO (martin) Not ideal, this should have a chat API controller endpoint.
|
||||||
|
return ajax(`/chat/${channelId}/edit/${messageId}`, {
|
||||||
|
type: "PUT",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks messages for all of a user's chat channel memberships as read.
|
* Marks messages for all of a user's chat channel memberships as read.
|
||||||
*
|
*
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import { debounce } from "discourse-common/utils/decorators";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import Service, { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatChannelComposer extends Service {
|
||||||
|
@service chat;
|
||||||
|
@service chatApi;
|
||||||
|
@service chatComposerPresenceManager;
|
||||||
|
|
||||||
|
@tracked editingMessage = null;
|
||||||
|
@tracked replyToMsg = null;
|
||||||
|
@tracked linkedComponent = null;
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.editingMessage = null;
|
||||||
|
this.replyToMsg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get #model() {
|
||||||
|
return this.chat.activeChannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReplyTo(messageOrId) {
|
||||||
|
if (messageOrId) {
|
||||||
|
this.cancelEditing();
|
||||||
|
|
||||||
|
const message =
|
||||||
|
typeof messageOrId === "number"
|
||||||
|
? this.#model.messagesManager.findMessage(messageOrId)
|
||||||
|
: messageOrId;
|
||||||
|
this.replyToMsg = message;
|
||||||
|
this.focusComposer();
|
||||||
|
} else {
|
||||||
|
this.replyToMsg = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onComposerValueChange({ replyToMsg: this.replyToMsg });
|
||||||
|
}
|
||||||
|
|
||||||
|
editButtonClicked(messageId) {
|
||||||
|
const message = this.#model.messagesManager.findMessage(messageId);
|
||||||
|
this.editingMessage = message;
|
||||||
|
|
||||||
|
// TODO (martin) Move scrollToLatestMessage to live panel.
|
||||||
|
// this.scrollToLatestMessage();
|
||||||
|
|
||||||
|
this.focusComposer();
|
||||||
|
}
|
||||||
|
|
||||||
|
onComposerValueChange({
|
||||||
|
value,
|
||||||
|
uploads,
|
||||||
|
replyToMsg,
|
||||||
|
inProgressUploadsCount,
|
||||||
|
}) {
|
||||||
|
if (!this.#model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.editingMessage && !this.#model.isDraft) {
|
||||||
|
if (typeof value !== "undefined") {
|
||||||
|
this.#model.draft.message = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only save the uploads to the draft if we are not still uploading other
|
||||||
|
// ones, otherwise we get into a cycle where we pass the draft uploads as
|
||||||
|
// existingUploads back to the upload component and cause in progress ones
|
||||||
|
// to be cancelled
|
||||||
|
if (
|
||||||
|
typeof uploads !== "undefined" &&
|
||||||
|
inProgressUploadsCount !== "undefined" &&
|
||||||
|
inProgressUploadsCount === 0
|
||||||
|
) {
|
||||||
|
this.#model.draft.uploads = uploads;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof replyToMsg !== "undefined") {
|
||||||
|
this.#model.draft.replyToMsg = replyToMsg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#model.isDraft) {
|
||||||
|
this.#reportReplyingPresence(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._persistDraft();
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEditing() {
|
||||||
|
this.editingMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
registerFocusHandler(handlerFn) {
|
||||||
|
this.focusHandler = handlerFn;
|
||||||
|
}
|
||||||
|
|
||||||
|
focusComposer() {
|
||||||
|
this.focusHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
#reportReplyingPresence(composerValue) {
|
||||||
|
if (this.#componentDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.#model.isDraft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const replying = !this.editingMessage && !!composerValue;
|
||||||
|
this.chatComposerPresenceManager.notifyState(this.#model.id, replying);
|
||||||
|
}
|
||||||
|
|
||||||
|
@debounce(2000)
|
||||||
|
_persistDraft() {
|
||||||
|
if (this.#componentDeleted || !this.#model) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#model.draft) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.chatApi.saveDraft(this.#model.id, this.#model.draft.toJSON());
|
||||||
|
}
|
||||||
|
|
||||||
|
get #componentDeleted() {
|
||||||
|
// note I didn't set this in the new version, not sure yet what to do with it
|
||||||
|
// return this.linkedComponent._selfDeleted;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
import ChatEmojiPickerManager from "./chat-emoji-picker-manager";
|
||||||
|
|
||||||
|
export default class ChatChannelEmojiPickerManager extends ChatEmojiPickerManager {}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
|
import Service, { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatChannelPane extends Service {
|
||||||
|
@service appEvents;
|
||||||
|
@service chat;
|
||||||
|
@service chatChannelComposer;
|
||||||
|
@service chatApi;
|
||||||
|
@service chatComposerPresenceManager;
|
||||||
|
|
||||||
|
@tracked reacting = false;
|
||||||
|
@tracked selectingMessages = false;
|
||||||
|
@tracked hoveredMessageId = false;
|
||||||
|
@tracked lastSelectedMessage = null;
|
||||||
|
@tracked sendingLoading = false;
|
||||||
|
|
||||||
|
get selectedMessageIds() {
|
||||||
|
return this.chat.activeChannel.selectedMessages.mapBy("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
get composerService() {
|
||||||
|
return this.chatChannelComposer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancelSelecting(selectedMessages) {
|
||||||
|
this.selectingMessages = false;
|
||||||
|
|
||||||
|
selectedMessages.forEach((message) => {
|
||||||
|
message.selected = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectMessage(message) {
|
||||||
|
this.lastSelectedMessage = message;
|
||||||
|
this.selectingMessages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
editMessage(newContent, uploads) {
|
||||||
|
this.sendingLoading = true;
|
||||||
|
let data = {
|
||||||
|
new_message: newContent,
|
||||||
|
upload_ids: (uploads || []).map((upload) => upload.id),
|
||||||
|
};
|
||||||
|
return this.chatApi
|
||||||
|
.editMessage(
|
||||||
|
this.composerService.editingMessage.channelId,
|
||||||
|
this.composerService.editingMessage.id,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.resetAfterSend();
|
||||||
|
})
|
||||||
|
.catch(popupAjaxError)
|
||||||
|
.finally(() => {
|
||||||
|
if (this._selfDeleted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.sendingLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
resetAfterSend() {
|
||||||
|
const channelId = this.composerService.editingMessage?.channelId;
|
||||||
|
if (channelId) {
|
||||||
|
this.chatComposerPresenceManager.notifyState(channelId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composerService.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
editLastMessageRequested() {
|
||||||
|
const lastUserMessage = this.chat.activeChannel.messages.findLast(
|
||||||
|
(message) => message.user.id === this.currentUser.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!lastUserMessage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastUserMessage.staged || lastUserMessage.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.composerService.editingMessage = lastUserMessage;
|
||||||
|
this.composerService.focusComposer();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
import ChatChannelComposer from "./chat-channel-composer";
|
||||||
|
|
||||||
|
export default class extends ChatChannelComposer {
|
||||||
|
get #model() {
|
||||||
|
return this.chat.activeChannel.activeThread;
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistDraft() {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.debug(
|
||||||
|
"Drafts are unsupported for chat threads at this point in time"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import ChatChannelPane from "./chat-channel-pane";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatChannelThreadPane extends ChatChannelPane {
|
||||||
|
@service chatChannelThreadComposer;
|
||||||
|
|
||||||
|
get selectedMessageIds() {
|
||||||
|
return this.chat.activeChannel.activeThread.selectedMessages.mapBy("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
get composerService() {
|
||||||
|
return this.chatChannelThreadComposer;
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,21 @@ import Service, { inject as service } from "@ember/service";
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import ChatDrawerDraftChannel from "discourse/plugins/chat/discourse/components/chat-drawer/draft-channel";
|
import ChatDrawerDraftChannel from "discourse/plugins/chat/discourse/components/chat-drawer/draft-channel";
|
||||||
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
|
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
|
||||||
|
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
|
||||||
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
||||||
|
|
||||||
const COMPONENTS_MAP = {
|
const COMPONENTS_MAP = {
|
||||||
"chat.draft-channel": { name: ChatDrawerDraftChannel },
|
"chat.draft-channel": { name: ChatDrawerDraftChannel },
|
||||||
"chat.channel": { name: ChatDrawerChannel },
|
"chat.channel": { name: ChatDrawerChannel },
|
||||||
|
"chat.channel.thread": {
|
||||||
|
name: ChatDrawerThread,
|
||||||
|
extractParams: (route) => {
|
||||||
|
return {
|
||||||
|
channelId: route.parent.params.channelId,
|
||||||
|
threadId: route.params.threadId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
chat: { name: ChatDrawerIndex },
|
chat: { name: ChatDrawerIndex },
|
||||||
"chat.channel.near-message": {
|
"chat.channel.near-message": {
|
||||||
name: ChatDrawerChannel,
|
name: ChatDrawerChannel,
|
||||||
|
@ -1,49 +1,42 @@
|
|||||||
import { headerOffset } from "discourse/lib/offset-calculator";
|
|
||||||
import { createPopper } from "@popperjs/core";
|
|
||||||
import Service from "@ember/service";
|
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { ajax } from "discourse/lib/ajax";
|
import { ajax } from "discourse/lib/ajax";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
import { bind } from "discourse-common/utils/decorators";
|
||||||
import { later, schedule } from "@ember/runloop";
|
import { later } from "@ember/runloop";
|
||||||
import { makeArray } from "discourse-common/lib/helpers";
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
import { Promise } from "rsvp";
|
import { Promise } from "rsvp";
|
||||||
import { computed } from "@ember/object";
|
|
||||||
import { isTesting } from "discourse-common/config/environment";
|
import { isTesting } from "discourse-common/config/environment";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import Service, { inject as service } from "@ember/service";
|
||||||
|
|
||||||
const TRANSITION_TIME = isTesting() ? 0 : 125; // CSS transition time
|
const TRANSITION_TIME = isTesting() ? 0 : 125; // CSS transition time
|
||||||
const DEFAULT_VISIBLE_SECTIONS = ["favorites", "smileys_&_emotion"];
|
const DEFAULT_VISIBLE_SECTIONS = ["favorites", "smileys_&_emotion"];
|
||||||
const DEFAULT_LAST_SECTION = "favorites";
|
const DEFAULT_LAST_SECTION = "favorites";
|
||||||
|
|
||||||
export default class ChatEmojiPickerManager extends Service {
|
export default class ChatEmojiPickerManager extends Service {
|
||||||
@tracked opened = false;
|
@service appEvents;
|
||||||
|
|
||||||
@tracked closing = false;
|
@tracked closing = false;
|
||||||
@tracked loading = false;
|
@tracked loading = false;
|
||||||
@tracked context = null;
|
@tracked picker = null;
|
||||||
@tracked emojis = null;
|
@tracked emojis = null;
|
||||||
@tracked visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
@tracked visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
||||||
@tracked lastVisibleSection = DEFAULT_LAST_SECTION;
|
@tracked lastVisibleSection = DEFAULT_LAST_SECTION;
|
||||||
@tracked initialFilter = null;
|
|
||||||
@tracked element = null;
|
@tracked element = null;
|
||||||
@tracked callback;
|
|
||||||
|
|
||||||
@computed("emojis.[]", "loading")
|
|
||||||
get sections() {
|
get sections() {
|
||||||
return !this.loading && this.emojis ? Object.keys(this.emojis) : [];
|
return !this.loading && this.emojis ? Object.keys(this.emojis) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
closeExisting() {
|
closeExisting() {
|
||||||
this.callback = null;
|
|
||||||
this.opened = false;
|
|
||||||
this.initialFilter = null;
|
|
||||||
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
||||||
this.lastVisibleSection = DEFAULT_LAST_SECTION;
|
this.lastVisibleSection = DEFAULT_LAST_SECTION;
|
||||||
|
this.picker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
close() {
|
close() {
|
||||||
this.callback = null;
|
|
||||||
this.closing = true;
|
this.closing = true;
|
||||||
|
|
||||||
later(() => {
|
later(() => {
|
||||||
@ -53,9 +46,8 @@ export default class ChatEmojiPickerManager extends Service {
|
|||||||
|
|
||||||
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
this.visibleSections = DEFAULT_VISIBLE_SECTIONS;
|
||||||
this.lastVisibleSection = DEFAULT_LAST_SECTION;
|
this.lastVisibleSection = DEFAULT_LAST_SECTION;
|
||||||
this.initialFilter = null;
|
|
||||||
this.closing = false;
|
this.closing = false;
|
||||||
this.opened = false;
|
this.picker = null;
|
||||||
}, TRANSITION_TIME);
|
}, TRANSITION_TIME);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,80 +57,23 @@ export default class ChatEmojiPickerManager extends Service {
|
|||||||
.uniq();
|
.uniq();
|
||||||
}
|
}
|
||||||
|
|
||||||
didSelectEmoji(emoji) {
|
open(picker) {
|
||||||
this?.callback(emoji);
|
this.loadEmojis();
|
||||||
this.callback = null;
|
|
||||||
this.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
startFromMessageReactionList(message, callback, options = {}) {
|
if (this.picker) {
|
||||||
const trigger = document.querySelector(
|
if (this.picker.trigger === picker.trigger) {
|
||||||
`.chat-message-container[data-id="${message.id}"] .chat-message-react-btn`
|
this.closeExisting();
|
||||||
);
|
} else {
|
||||||
this.startFromMessage(callback, trigger, options);
|
this.closeExisting();
|
||||||
}
|
this.picker = picker;
|
||||||
|
}
|
||||||
startFromMessageActions(message, callback, options = {}) {
|
} else {
|
||||||
const trigger = document.querySelector(
|
this.picker = picker;
|
||||||
`.chat-message-actions-container[data-id="${message.id}"] .chat-message-actions`
|
|
||||||
);
|
|
||||||
this.startFromMessage(callback, trigger, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
startFromMessage(
|
|
||||||
callback,
|
|
||||||
trigger,
|
|
||||||
options = { filter: null, desktop: true }
|
|
||||||
) {
|
|
||||||
this.initialFilter = options.filter;
|
|
||||||
this.context = "chat-message";
|
|
||||||
this.element = document.querySelector(".chat-message-emoji-picker-anchor");
|
|
||||||
this.open(callback);
|
|
||||||
this._popper?.destroy();
|
|
||||||
|
|
||||||
if (options.desktop) {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
this._popper = createPopper(trigger, this.element, {
|
|
||||||
placement: "top",
|
|
||||||
modifiers: [
|
|
||||||
{
|
|
||||||
name: "eventListeners",
|
|
||||||
options: {
|
|
||||||
scroll: false,
|
|
||||||
resize: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "flip",
|
|
||||||
options: {
|
|
||||||
padding: { top: headerOffset() },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
startFromComposer(callback, options = { filter: null }) {
|
@action
|
||||||
this.initialFilter = options.filter;
|
loadEmojis() {
|
||||||
this.context = "chat-composer";
|
|
||||||
this.element = document.querySelector(".chat-composer-emoji-picker-anchor");
|
|
||||||
this.open(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
open(callback) {
|
|
||||||
if (this.opened) {
|
|
||||||
this.closeExisting();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._loadEmojisData();
|
|
||||||
|
|
||||||
this.callback = callback;
|
|
||||||
this.opened = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_loadEmojisData() {
|
|
||||||
if (this.emojis) {
|
if (this.emojis) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import { and } from "@ember/object/computed";
|
|||||||
import { computed } from "@ember/object";
|
import { computed } from "@ember/object";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
|
import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
|
||||||
|
import { MESSAGE_CONTEXT_THREAD } from "discourse/plugins/chat/discourse/components/chat-message";
|
||||||
|
|
||||||
const CHAT_ONLINE_OPTIONS = {
|
const CHAT_ONLINE_OPTIONS = {
|
||||||
userUnseenTime: 300000, // 5 minutes seconds with no interaction
|
userUnseenTime: 300000, // 5 minutes seconds with no interaction
|
||||||
@ -26,6 +27,9 @@ export default class Chat extends Service {
|
|||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
@service chatChannelsManager;
|
@service chatChannelsManager;
|
||||||
|
@service chatChannelPane;
|
||||||
|
@service chatChannelThreadPane;
|
||||||
|
|
||||||
@tracked activeChannel = null;
|
@tracked activeChannel = null;
|
||||||
|
|
||||||
cook = null;
|
cook = null;
|
||||||
@ -35,6 +39,8 @@ export default class Chat extends Service {
|
|||||||
|
|
||||||
@and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat;
|
@and("currentUser.has_chat_enabled", "siteSettings.chat_enabled") userCanChat;
|
||||||
|
|
||||||
|
@tracked _activeMessage = null;
|
||||||
|
|
||||||
@computed("currentUser.staff", "currentUser.groups.[]")
|
@computed("currentUser.staff", "currentUser.groups.[]")
|
||||||
get userCanDirectMessage() {
|
get userCanDirectMessage() {
|
||||||
if (!this.currentUser) {
|
if (!this.currentUser) {
|
||||||
@ -51,6 +57,32 @@ export default class Chat extends Service {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed("activeChannel.userSilenced")
|
||||||
|
get userCanInteractWithChat() {
|
||||||
|
return !this.activeChannel?.userSilenced;
|
||||||
|
}
|
||||||
|
|
||||||
|
get activeMessage() {
|
||||||
|
return this._activeMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activeMessage(hash) {
|
||||||
|
this.chatChannelPane.hoveredMessageId = null;
|
||||||
|
this.chatChannelThreadPane.hoveredMessageId = null;
|
||||||
|
|
||||||
|
if (hash) {
|
||||||
|
this._activeMessage = hash;
|
||||||
|
|
||||||
|
if (hash.context === MESSAGE_CONTEXT_THREAD) {
|
||||||
|
this.chatChannelThreadPane.hoveredMessageId = hash.model.id;
|
||||||
|
} else {
|
||||||
|
this.chatChannelPane.hoveredMessageId = hash.model.id;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._activeMessage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(...arguments);
|
super.init(...arguments);
|
||||||
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
{{#if
|
|
||||||
(and this.chatEmojiPickerManager.opened this.chatEmojiPickerManager.element)
|
|
||||||
}}
|
|
||||||
{{#in-element this.chatEmojiPickerManager.element}}
|
|
||||||
<ChatEmojiPicker />
|
|
||||||
{{/in-element}}
|
|
||||||
{{/if}}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
setupComponent(args, component) {
|
|
||||||
const container = getOwner(this);
|
|
||||||
const chatEmojiPickerManager = container.lookup(
|
|
||||||
"service:chat-emoji-picker-manager"
|
|
||||||
);
|
|
||||||
|
|
||||||
component.set("chatEmojiPickerManager", chatEmojiPickerManager);
|
|
||||||
},
|
|
||||||
};
|
|
@ -5,7 +5,7 @@ $float-height: 530px;
|
|||||||
--full-page-border-radius: 12px;
|
--full-page-border-radius: 12px;
|
||||||
--full-page-sidebar-width: 275px;
|
--full-page-sidebar-width: 275px;
|
||||||
--channel-list-avatar-size: 30px;
|
--channel-list-avatar-size: 30px;
|
||||||
--chat-header-offset: 65px;
|
--chat-header-offset: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-move-to-channel-modal-modal {
|
.chat-message-move-to-channel-modal-modal {
|
||||||
@ -289,9 +289,6 @@ $float-height: 530px;
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
margin: 0 1px 0 0;
|
margin: 0 1px 0 0;
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
overflow-x: hidden;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
|
|
||||||
@include chat-scrollbar();
|
@include chat-scrollbar();
|
||||||
|
|
||||||
.join-channel-btn.in-float {
|
.join-channel-btn.in-float {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
.chat-composer-dropdown {
|
[data-theme="chat-composer-drodown"] {
|
||||||
margin-left: 0.2rem;
|
margin-left: 0.2rem;
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-composer-dropdown__trigger-btn {
|
.chat-composer-dropdown__trigger-btn {
|
||||||
padding: 5px;
|
padding: 5px !important; // overwrite ios rule
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
background: var(--primary-med-or-secondary-high);
|
background: var(--primary-med-or-secondary-high);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
@ -30,20 +30,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-composer-dropdown__list {
|
.chat-composer-dropdown__list {
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-composer-dropdown__item {
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-composer-dropdown__action-btn {
|
.chat-composer-dropdown__action-btn {
|
||||||
background: none;
|
background: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -103,6 +103,8 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@include chat-scrollbar();
|
||||||
}
|
}
|
||||||
|
|
||||||
&__unreliable-network {
|
&__unreliable-network {
|
||||||
|
@ -54,9 +54,11 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
@include chat-scrollbar();
|
||||||
|
margin: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__no-reults {
|
&__no-results {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,12 +199,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-emoji-picker-anchor {
|
.chat-channel-message-emoji-picker-connector {
|
||||||
z-index: z("header") + 1;
|
position: relative;
|
||||||
|
|
||||||
.chat-emoji-picker {
|
.chat-emoji-picker {
|
||||||
border: 1px solid var(--primary-low);
|
border: 1px solid var(--primary-low);
|
||||||
width: 320px;
|
width: 320px;
|
||||||
|
z-index: z("header") + 1;
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
width: 22px;
|
width: 22px;
|
||||||
@ -210,31 +213,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-view {
|
|
||||||
.chat-message-emoji-picker-anchor.-opened {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
box-shadow: shadowcreatePopper("card");
|
|
||||||
|
|
||||||
.chat-emoji-picker {
|
|
||||||
height: 50vh;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-composer-container.with-emoji-picker {
|
|
||||||
background: var(--primary-very-low);
|
|
||||||
|
|
||||||
.chat-emoji-picker {
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
|
|
||||||
&.closing {
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
.chat-message-actions-container {
|
.chat-message-actions-container {
|
||||||
@include unselectable;
|
@include unselectable;
|
||||||
position: relative;
|
z-index: z("dropdown") - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-actions {
|
.chat-message-actions {
|
||||||
@ -47,6 +47,10 @@
|
|||||||
width: 2.5em;
|
width: 2.5em;
|
||||||
transition: background 0.2s, border-color 0.2s;
|
transition: background 0.2s, border-color 0.2s;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
.d-icon {
|
.d-icon {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
width: var(--message-left-width);
|
width: var(--message-left-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-container.is-hovered .chat-message-left-gutter {
|
.chat-message-container:hover .chat-message-left-gutter {
|
||||||
.chat-time {
|
.chat-time {
|
||||||
color: var(--secondary-mediumy);
|
color: var(--secondary-mediumy);
|
||||||
}
|
}
|
||||||
|
@ -131,6 +131,10 @@
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.d-icon {
|
.d-icon {
|
||||||
color: var(--primary-high);
|
color: var(--primary-high);
|
||||||
}
|
}
|
||||||
@ -181,26 +185,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-messages-container {
|
.chat-messages-container {
|
||||||
.not-mobile-device & .chat-message:hover,
|
.chat-message {
|
||||||
.chat-message.chat-message-selected {
|
&.chat-message-bookmarked {
|
||||||
background: var(--primary-very-low);
|
background: var(--highlight-bg);
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.chat-message-bookmarked {
|
|
||||||
background: var(--highlight-bg);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--highlight-medium);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.not-mobile-device & .chat-message-reaction-list .chat-message-react-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.not-mobile-device & .chat-message:hover {
|
|
||||||
.chat-message-reaction-list .chat-message-react-btn {
|
.chat-message-reaction-list .chat-message-react-btn {
|
||||||
display: inline-block;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch & {
|
||||||
|
&:active {
|
||||||
|
background: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chat-message-bookmarked {
|
||||||
|
&:active {
|
||||||
|
background: var(--highlight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-touch & {
|
||||||
|
&:hover,
|
||||||
|
&:active {
|
||||||
|
background: var(--primary-very-low);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.chat-message-react-btn {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.chat-message-bookmarked {
|
||||||
|
&:hover {
|
||||||
|
background: var(--highlight-medium);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -222,15 +244,6 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message-container.is-hovered,
|
|
||||||
.chat-message.chat-message-selected {
|
|
||||||
background: var(--primary-very-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-message.chat-message-bookmarked {
|
|
||||||
background: var(--highlight-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.has-full-page-chat .chat-message .onebox:not(img),
|
.has-full-page-chat .chat-message .onebox:not(img),
|
||||||
.chat-drawer-container .chat-message .onebox {
|
.chat-drawer-container .chat-message .onebox {
|
||||||
margin: 0.5em 0;
|
margin: 0.5em 0;
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
grid-area: threads;
|
grid-area: threads;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-left: 1px solid var(--primary-medium);
|
border-left: 1px solid var(--primary-low);
|
||||||
|
|
||||||
&__list {
|
&__list {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -1,11 +1,31 @@
|
|||||||
.chat-thread {
|
.chat-thread {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding-block: 1rem;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
&__header {
|
&__header {
|
||||||
|
height: var(--chat-header-offset);
|
||||||
|
min-height: var(--chat-header-offset);
|
||||||
|
border-bottom: 1px solid var(--primary-low);
|
||||||
|
border-top: 1px solid var(--primary-low);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-inline: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
overflow-y: scroll;
|
||||||
|
@include chat-scrollbar();
|
||||||
|
margin: 2px;
|
||||||
|
padding-right: 2px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex-grow: 1;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__close {
|
&__close {
|
||||||
@ -15,41 +35,4 @@
|
|||||||
color: var(--primary-medium);
|
color: var(--primary-medium);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
|
||||||
padding-inline: 1.5rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid var(--primary-low);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__om {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__omu {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.chat-message-avatar {
|
|
||||||
width: var(--message-left-width);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__started-by {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__messages {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
padding-inline: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
.chat-channel-message-emoji-picker-connector {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.chat-emoji-picker {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 50vh;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: shadow("card");
|
||||||
|
z-index: z("header") + 2;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
|
&__backdrop {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: var(--primary);
|
||||||
|
opacity: 0.8;
|
||||||
|
z-index: z("header") + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,3 +5,4 @@
|
|||||||
@import "chat-message-actions";
|
@import "chat-message-actions";
|
||||||
@import "chat-message";
|
@import "chat-message";
|
||||||
@import "chat-selection-manager";
|
@import "chat-selection-manager";
|
||||||
|
@import "chat-emoji-picker";
|
||||||
|
@ -532,8 +532,9 @@ en:
|
|||||||
search_placeholder: "Search by emoji name and alias..."
|
search_placeholder: "Search by emoji name and alias..."
|
||||||
no_results: "No results"
|
no_results: "No results"
|
||||||
|
|
||||||
|
thread:
|
||||||
|
label: Thread
|
||||||
threads:
|
threads:
|
||||||
op_said: "OP said:"
|
|
||||||
started_by: "Started by"
|
started_by: "Started by"
|
||||||
open: "Open Thread"
|
open: "Open Thread"
|
||||||
|
|
||||||
|
@ -75,13 +75,13 @@ module PageObjects
|
|||||||
def select_message(message)
|
def select_message(message)
|
||||||
hover_message(message)
|
hover_message(message)
|
||||||
click_more_button
|
click_more_button
|
||||||
find("[data-value='selectMessage']").click
|
find("[data-value='select']").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_message(message)
|
def delete_message(message)
|
||||||
hover_message(message)
|
hover_message(message)
|
||||||
click_more_button
|
click_more_button
|
||||||
find("[data-value='deleteMessage']").click
|
find("[data-value='delete']").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def open_edit_message(message)
|
def open_edit_message(message)
|
||||||
|
@ -30,6 +30,10 @@ module PageObjects
|
|||||||
def maximize
|
def maximize
|
||||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_open_thread?(thread)
|
||||||
|
has_css?("#{VISIBLE_DRAWER} .chat-thread[data-id='#{thread.id}']")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
RSpec.describe "React to message", type: :system, js: true do
|
RSpec.describe "React to message", type: :system, js: true do
|
||||||
fab!(:current_user) { Fabricate(:user) }
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:other_user) { Fabricate(:user) }
|
||||||
fab!(:category_channel_1) { Fabricate(:category_channel) }
|
fab!(:category_channel_1) { Fabricate(:category_channel) }
|
||||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: category_channel_1) }
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: category_channel_1) }
|
||||||
|
|
||||||
@ -11,11 +12,12 @@ RSpec.describe "React to message", type: :system, js: true do
|
|||||||
before do
|
before do
|
||||||
chat_system_bootstrap
|
chat_system_bootstrap
|
||||||
category_channel_1.add(current_user)
|
category_channel_1.add(current_user)
|
||||||
|
category_channel_1.add(other_user)
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when other user has reacted" do
|
context "when other user has reacted" do
|
||||||
fab!(:reaction_1) do
|
fab!(:reaction_1) do
|
||||||
Chat::MessageReactor.new(Fabricate(:user), category_channel_1).react!(
|
Chat::MessageReactor.new(other_user, category_channel_1).react!(
|
||||||
message_id: message_1.id,
|
message_id: message_1.id,
|
||||||
react_action: :add,
|
react_action: :add,
|
||||||
emoji: "female_detective",
|
emoji: "female_detective",
|
||||||
@ -48,7 +50,7 @@ RSpec.describe "React to message", type: :system, js: true do
|
|||||||
|
|
||||||
context "when current user reacts" do
|
context "when current user reacts" do
|
||||||
fab!(:reaction_1) do
|
fab!(:reaction_1) do
|
||||||
Chat::MessageReactor.new(Fabricate(:user), category_channel_1).react!(
|
Chat::MessageReactor.new(other_user, category_channel_1).react!(
|
||||||
message_id: message_1.id,
|
message_id: message_1.id,
|
||||||
react_action: :add,
|
react_action: :add,
|
||||||
emoji: "female_detective",
|
emoji: "female_detective",
|
||||||
@ -62,14 +64,14 @@ RSpec.describe "React to message", type: :system, js: true do
|
|||||||
chat.visit_channel(category_channel_1)
|
chat.visit_channel(category_channel_1)
|
||||||
channel.hover_message(message_1)
|
channel.hover_message(message_1)
|
||||||
find(".chat-message-react-btn").click
|
find(".chat-message-react-btn").click
|
||||||
find(".chat-emoji-picker [data-emoji=\"nerd_face\"]").click
|
find(".chat-emoji-picker [data-emoji=\"grimacing\"]").click
|
||||||
|
|
||||||
expect(channel).to have_reaction(message_1, reaction_1.emoji)
|
expect(channel).to have_reaction(message_1, "grimacing")
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when current user has multiple sessions" do
|
context "when current user has multiple sessions" do
|
||||||
it "adds reaction on each session" do
|
it "adds reaction on each session" do
|
||||||
reaction = OpenStruct.new(emoji: "nerd_face")
|
reaction = OpenStruct.new(emoji: "grimacing")
|
||||||
|
|
||||||
using_session(:tab_1) do
|
using_session(:tab_1) do
|
||||||
sign_in(current_user)
|
sign_in(current_user)
|
||||||
|
@ -7,6 +7,7 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
let(:side_panel) { PageObjects::Pages::ChatSidePanel.new }
|
let(:side_panel) { PageObjects::Pages::ChatSidePanel.new }
|
||||||
let(:open_thread) { PageObjects::Pages::ChatThread.new }
|
let(:open_thread) { PageObjects::Pages::ChatThread.new }
|
||||||
|
let(:chat_drawer_page) { PageObjects::Pages::ChatDrawer.new }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
chat_system_bootstrap(current_user, [channel])
|
chat_system_bootstrap(current_user, [channel])
|
||||||
@ -49,19 +50,27 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||||||
|
|
||||||
before { SiteSetting.enable_experimental_chat_threaded_discussions = true }
|
before { SiteSetting.enable_experimental_chat_threaded_discussions = true }
|
||||||
|
|
||||||
|
it "opens the single thread in the drawer from the message actions menu" do
|
||||||
|
visit("/latest")
|
||||||
|
chat_page.open_from_header
|
||||||
|
chat_drawer_page.open_channel(channel)
|
||||||
|
channel_page.open_message_thread(thread.chat_messages.order(:created_at).last)
|
||||||
|
expect(chat_drawer_page).to have_open_thread(thread)
|
||||||
|
end
|
||||||
|
|
||||||
it "opens the side panel for a single thread from the message actions menu" do
|
it "opens the side panel for a single thread from the message actions menu" do
|
||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.open_message_thread(thread.original_message)
|
channel_page.open_message_thread(thread.original_message)
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows the excerpt of the thread original message" do
|
xit "shows the excerpt of the thread original message" do
|
||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.open_message_thread(thread.original_message)
|
channel_page.open_message_thread(thread.original_message)
|
||||||
expect(open_thread).to have_header_content(thread.excerpt)
|
expect(open_thread).to have_header_content(thread.excerpt)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows the avatar and username of the original message user" do
|
xit "shows the avatar and username of the original message user" do
|
||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.open_message_thread(thread.original_message)
|
channel_page.open_message_thread(thread.original_message)
|
||||||
expect(open_thread.omu).to have_css(".chat-user-avatar img.avatar")
|
expect(open_thread.omu).to have_css(".chat-user-avatar img.avatar")
|
||||||
|
@ -21,7 +21,7 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
|||||||
chat_channel_page.message_by_id(message.id).hover
|
chat_channel_page.message_by_id(message.id).hover
|
||||||
expect(page).to have_css(".chat-message-actions .more-buttons")
|
expect(page).to have_css(".chat-message-actions .more-buttons")
|
||||||
find(".chat-message-actions .more-buttons").click
|
find(".chat-message-actions .more-buttons").click
|
||||||
find(".select-kit-row[data-value=\"selectMessage\"]").click
|
find(".select-kit-row[data-value=\"select\"]").click
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ RSpec.describe "Quoting chat message transcripts", type: :system, js: true do
|
|||||||
mobile: true do
|
mobile: true do
|
||||||
chat_page.visit_channel(chat_channel_1)
|
chat_page.visit_channel(chat_channel_1)
|
||||||
|
|
||||||
chat_channel_page.click_message_action_mobile(message_1, "selectMessage")
|
chat_channel_page.click_message_action_mobile(message_1, "select")
|
||||||
click_selection_button("quote")
|
click_selection_button("quote")
|
||||||
|
|
||||||
expect(topic_page).to have_expanded_composer
|
expect(topic_page).to have_expanded_composer
|
||||||
|
@ -4,6 +4,7 @@ import hbs from "htmlbars-inline-precompile";
|
|||||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import { render } from "@ember/test-helpers";
|
import { render } from "@ember/test-helpers";
|
||||||
|
import pretender from "discourse/tests/helpers/create-pretender";
|
||||||
|
|
||||||
module(
|
module(
|
||||||
"Discourse Chat | Component | chat-composer placeholder",
|
"Discourse Chat | Component | chat-composer placeholder",
|
||||||
@ -11,6 +12,8 @@ module(
|
|||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
test("direct message to self shows Jot something down", async function (assert) {
|
test("direct message to self shows Jot something down", async function (assert) {
|
||||||
|
pretender.get("/chat/emojis.json", () => [200, [], {}]);
|
||||||
|
|
||||||
this.currentUser.set("id", 1);
|
this.currentUser.set("id", 1);
|
||||||
this.set(
|
this.set(
|
||||||
"chatChannel",
|
"chatChannel",
|
||||||
@ -31,6 +34,8 @@ module(
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("direct message to multiple folks shows their names", async function (assert) {
|
test("direct message to multiple folks shows their names", async function (assert) {
|
||||||
|
pretender.get("/chat/emojis.json", () => [200, [], {}]);
|
||||||
|
|
||||||
this.set(
|
this.set(
|
||||||
"chatChannel",
|
"chatChannel",
|
||||||
ChatChannel.create({
|
ChatChannel.create({
|
||||||
@ -54,6 +59,8 @@ module(
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("message to channel shows send message to channel name", async function (assert) {
|
test("message to channel shows send message to channel name", async function (assert) {
|
||||||
|
pretender.get("/chat/emojis.json", () => [200, [], {}]);
|
||||||
|
|
||||||
this.set(
|
this.set(
|
||||||
"chatChannel",
|
"chatChannel",
|
||||||
ChatChannel.create({
|
ChatChannel.create({
|
||||||
|
@ -68,7 +68,7 @@ module("Discourse Chat | Component | chat-emoji-picker", function (hooks) {
|
|||||||
this.chatEmojiPickerManager = this.container.lookup(
|
this.chatEmojiPickerManager = this.container.lookup(
|
||||||
"service:chat-emoji-picker-manager"
|
"service:chat-emoji-picker-manager"
|
||||||
);
|
);
|
||||||
this.chatEmojiPickerManager.startFromComposer(() => {});
|
this.chatEmojiPickerManager.open(() => {});
|
||||||
this.chatEmojiPickerManager.addVisibleSections([
|
this.chatEmojiPickerManager.addVisibleSections([
|
||||||
"smileys_&_emotion",
|
"smileys_&_emotion",
|
||||||
"people_&_body",
|
"people_&_body",
|
||||||
@ -164,10 +164,13 @@ module("Discourse Chat | Component | chat-emoji-picker", function (hooks) {
|
|||||||
|
|
||||||
test("When selecting an emoji", async function (assert) {
|
test("When selecting an emoji", async function (assert) {
|
||||||
let selection;
|
let selection;
|
||||||
this.chatEmojiPickerManager.didSelectEmoji = (emoji) => {
|
this.didSelectEmoji = (emoji) => {
|
||||||
selection = emoji;
|
selection = emoji;
|
||||||
};
|
};
|
||||||
await render(hbs`<ChatEmojiPicker />`);
|
|
||||||
|
await render(
|
||||||
|
hbs`<ChatEmojiPicker @didSelectEmoji={{this.didSelectEmoji}} />`
|
||||||
|
);
|
||||||
await click('img.emoji[data-emoji="grinning"]');
|
await click('img.emoji[data-emoji="grinning"]');
|
||||||
|
|
||||||
assert.strictEqual(selection, "grinning");
|
assert.strictEqual(selection, "grinning");
|
||||||
@ -241,10 +244,13 @@ module("Discourse Chat | Component | chat-emoji-picker", function (hooks) {
|
|||||||
|
|
||||||
test("When selecting a toned an emoji", async function (assert) {
|
test("When selecting a toned an emoji", async function (assert) {
|
||||||
let selection;
|
let selection;
|
||||||
this.chatEmojiPickerManager.didSelectEmoji = (emoji) => {
|
this.didSelectEmoji = (emoji) => {
|
||||||
selection = emoji;
|
selection = emoji;
|
||||||
};
|
};
|
||||||
await render(hbs`<ChatEmojiPicker />`);
|
|
||||||
|
await render(
|
||||||
|
hbs`<ChatEmojiPicker @didSelectEmoji={{this.didSelectEmoji}} />`
|
||||||
|
);
|
||||||
this.emojiReactionStore.diversity = 1;
|
this.emojiReactionStore.diversity = 1;
|
||||||
await click('img.emoji[data-emoji="man_rowing_boat"]');
|
await click('img.emoji[data-emoji="man_rowing_boat"]');
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ module("Discourse Chat | Component | chat-message-reaction", function (hooks) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await render(hbs`
|
await render(hbs`
|
||||||
<ChatMessageReaction class="show" @reaction={{hash emoji="heart" count=this.count}} @react={{this.react}} />
|
<ChatMessageReaction class="show" @reaction={{hash emoji="heart" count=this.count}} @onReaction={{this.react}} />
|
||||||
`);
|
`);
|
||||||
|
|
||||||
assert.false(exists(".chat-message-reaction .count"));
|
assert.false(exists(".chat-message-reaction .count"));
|
||||||
|
@ -21,7 +21,6 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
|
|||||||
unread_count: 0,
|
unread_count: 0,
|
||||||
muted: false,
|
muted: false,
|
||||||
},
|
},
|
||||||
canInteractWithChat: true,
|
|
||||||
canDeleteSelf: true,
|
canDeleteSelf: true,
|
||||||
canDeleteOthers: true,
|
canDeleteOthers: true,
|
||||||
canFlag: true,
|
canFlag: true,
|
||||||
@ -46,14 +45,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
chatChannel,
|
chatChannel,
|
||||||
setReplyTo: () => {},
|
|
||||||
replyMessageClicked: () => {},
|
|
||||||
editButtonClicked: () => {},
|
|
||||||
afterExpand: () => {},
|
afterExpand: () => {},
|
||||||
selectingMessages: false,
|
|
||||||
onStartSelectingMessages: () => {},
|
|
||||||
onSelectMessage: () => {},
|
|
||||||
bulkSelectMessages: () => {},
|
|
||||||
onHoverMessage: () => {},
|
onHoverMessage: () => {},
|
||||||
messageDidEnterViewport: () => {},
|
messageDidEnterViewport: () => {},
|
||||||
messageDidLeaveViewport: () => {},
|
messageDidLeaveViewport: () => {},
|
||||||
@ -63,16 +55,7 @@ module("Discourse Chat | Component | chat-message", function (hooks) {
|
|||||||
const template = hbs`
|
const template = hbs`
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
@message={{this.message}}
|
@message={{this.message}}
|
||||||
@canInteractWithChat={{this.canInteractWithChat}}
|
|
||||||
@channel={{this.chatChannel}}
|
@channel={{this.chatChannel}}
|
||||||
@setReplyTo={{this.setReplyTo}}
|
|
||||||
@replyMessageClicked={{this.replyMessageClicked}}
|
|
||||||
@editButtonClicked={{this.editButtonClicked}}
|
|
||||||
@selectingMessages={{this.selectingMessages}}
|
|
||||||
@onStartSelectingMessages={{this.onStartSelectingMessages}}
|
|
||||||
@onSelectMessage={{this.onSelectMessage}}
|
|
||||||
@bulkSelectMessages={{this.bulkSelectMessages}}
|
|
||||||
@onHoverMessage={{this.onHoverMessage}}
|
|
||||||
@messageDidEnterViewport={{this.messageDidEnterViewport}}
|
@messageDidEnterViewport={{this.messageDidEnterViewport}}
|
||||||
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
|
@messageDidLeaveViewport={{this.messageDidLeaveViewport}}
|
||||||
/>
|
/>
|
||||||
|
@ -22,46 +22,6 @@ module(
|
|||||||
this.manager.close();
|
this.manager.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("startFromMessageReactionList", async function (assert) {
|
|
||||||
const callback = () => {};
|
|
||||||
this.manager.startFromMessageReactionList({ id: 1 }, callback);
|
|
||||||
|
|
||||||
assert.ok(this.manager.loading);
|
|
||||||
assert.ok(this.manager.opened);
|
|
||||||
assert.strictEqual(this.manager.context, "chat-message");
|
|
||||||
assert.strictEqual(this.manager.callback, callback);
|
|
||||||
assert.deepEqual(this.manager.visibleSections, [
|
|
||||||
"favorites",
|
|
||||||
"smileys_&_emotion",
|
|
||||||
]);
|
|
||||||
assert.strictEqual(this.manager.lastVisibleSection, "favorites");
|
|
||||||
|
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.deepEqual(this.manager.emojis, emojisReponse());
|
|
||||||
assert.strictEqual(this.manager.loading, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("startFromMessageActions", async function (assert) {
|
|
||||||
const callback = () => {};
|
|
||||||
this.manager.startFromMessageReactionList({ id: 1 }, callback);
|
|
||||||
|
|
||||||
assert.ok(this.manager.loading);
|
|
||||||
assert.ok(this.manager.opened);
|
|
||||||
assert.strictEqual(this.manager.context, "chat-message");
|
|
||||||
assert.strictEqual(this.manager.callback, callback);
|
|
||||||
assert.deepEqual(this.manager.visibleSections, [
|
|
||||||
"favorites",
|
|
||||||
"smileys_&_emotion",
|
|
||||||
]);
|
|
||||||
assert.strictEqual(this.manager.lastVisibleSection, "favorites");
|
|
||||||
|
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.deepEqual(this.manager.emojis, emojisReponse());
|
|
||||||
assert.strictEqual(this.manager.loading, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("addVisibleSections", async function (assert) {
|
test("addVisibleSections", async function (assert) {
|
||||||
this.manager.addVisibleSections(["favorites", "objects"]);
|
this.manager.addVisibleSections(["favorites", "objects"]);
|
||||||
|
|
||||||
@ -75,7 +35,7 @@ module(
|
|||||||
test("sections", async function (assert) {
|
test("sections", async function (assert) {
|
||||||
assert.deepEqual(this.manager.sections, []);
|
assert.deepEqual(this.manager.sections, []);
|
||||||
|
|
||||||
this.manager.startFromComposer(() => {});
|
this.manager.open({});
|
||||||
|
|
||||||
assert.deepEqual(this.manager.sections, []);
|
assert.deepEqual(this.manager.sections, []);
|
||||||
|
|
||||||
@ -84,14 +44,12 @@ module(
|
|||||||
assert.deepEqual(this.manager.sections, ["favorites"]);
|
assert.deepEqual(this.manager.sections, ["favorites"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("startFromComposer", async function (assert) {
|
test("open", async function (assert) {
|
||||||
const callback = () => {};
|
this.manager.open({ context: "chat-composer" });
|
||||||
this.manager.startFromComposer(callback);
|
|
||||||
|
|
||||||
assert.ok(this.manager.loading);
|
assert.ok(this.manager.loading);
|
||||||
assert.ok(this.manager.opened);
|
assert.ok(this.manager.picker);
|
||||||
assert.strictEqual(this.manager.context, "chat-composer");
|
assert.strictEqual(this.manager.picker.context, "chat-composer");
|
||||||
assert.strictEqual(this.manager.callback, callback);
|
|
||||||
assert.deepEqual(this.manager.visibleSections, [
|
assert.deepEqual(this.manager.visibleSections, [
|
||||||
"favorites",
|
"favorites",
|
||||||
"smileys_&_emotion",
|
"smileys_&_emotion",
|
||||||
@ -104,28 +62,16 @@ module(
|
|||||||
assert.strictEqual(this.manager.loading, false);
|
assert.strictEqual(this.manager.loading, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("startFromComposer with filter option", async function (assert) {
|
|
||||||
const callback = () => {};
|
|
||||||
this.manager.startFromComposer(callback, { filter: "foofilter" });
|
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.strictEqual(this.manager.initialFilter, "foofilter");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("closeExisting", async function (assert) {
|
test("closeExisting", async function (assert) {
|
||||||
const callback = () => {
|
this.manager.open({ context: "channel-composer", trigger: "foo" });
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
this.manager.startFromComposer(() => {});
|
|
||||||
this.manager.addVisibleSections("objects");
|
this.manager.addVisibleSections("objects");
|
||||||
this.manager.lastVisibleSection = "objects";
|
this.manager.lastVisibleSection = "objects";
|
||||||
this.manager.startFromComposer(callback);
|
this.manager.open({ context: "thread-composer", trigger: "bar" });
|
||||||
|
|
||||||
assert.strictEqual(
|
assert.strictEqual(
|
||||||
this.manager.callback,
|
this.manager.picker.context,
|
||||||
callback,
|
"thread-composer",
|
||||||
"it resets the callback to latest picker"
|
"it resets the picker to latest picker"
|
||||||
);
|
);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
this.manager.visibleSections,
|
this.manager.visibleSections,
|
||||||
@ -139,39 +85,21 @@ module(
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("didSelectEmoji", async function (assert) {
|
|
||||||
let value;
|
|
||||||
const callback = (emoji) => {
|
|
||||||
value = emoji.name;
|
|
||||||
};
|
|
||||||
this.manager.startFromComposer(callback);
|
|
||||||
this.manager.didSelectEmoji({ name: "joy" });
|
|
||||||
|
|
||||||
assert.notOk(this.manager.callback);
|
|
||||||
assert.strictEqual(value, "joy");
|
|
||||||
|
|
||||||
await settled();
|
|
||||||
|
|
||||||
assert.notOk(this.manager.opened, "it closes the picker after selection");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("close", async function (assert) {
|
test("close", async function (assert) {
|
||||||
this.manager.startFromComposer(() => {});
|
this.manager.open({ context: "channel-composer" });
|
||||||
|
|
||||||
assert.ok(this.manager.opened);
|
assert.ok(this.manager.picker);
|
||||||
assert.ok(this.manager.callback);
|
|
||||||
|
|
||||||
this.manager.addVisibleSections("objects");
|
this.manager.addVisibleSections("objects");
|
||||||
this.manager.lastVisibleSection = "objects";
|
this.manager.lastVisibleSection = "objects";
|
||||||
this.manager.close();
|
this.manager.close();
|
||||||
|
|
||||||
assert.notOk(this.manager.callback);
|
|
||||||
assert.ok(this.manager.closing);
|
assert.ok(this.manager.closing);
|
||||||
assert.ok(this.manager.opened);
|
assert.ok(this.manager.picker);
|
||||||
|
|
||||||
await settled();
|
await settled();
|
||||||
|
|
||||||
assert.notOk(this.manager.opened);
|
assert.notOk(this.manager.picker);
|
||||||
assert.notOk(this.manager.closing);
|
assert.notOk(this.manager.closing);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
this.manager.visibleSections,
|
this.manager.visibleSections,
|
||||||
|
Reference in New Issue
Block a user