- {{#if this.isDone}}
-
{{this.upload.extension}}
- {{else}}
- {{#if this.upload.processing}}
-
{{i18n "processing"}}
+{{#if @upload}}
+
+
+ {{#if this.isImage}}
+ {{#if @isDone}}
+
{{else}}
-
{{i18n "uploading"}}
+ {{d-icon "far-image"}}
{{/if}}
-
-
+ {{else}}
+ {{d-icon "file-alt"}}
{{/if}}
-
-
\ No newline at end of file
+
+
+ {{#unless this.isImage}}
+
+ {{this.fileName}}
+
+ {{/unless}}
+
+
+ {{#if @isDone}}
+ {{#unless this.isImage}}
+
{{@upload.extension}}
+ {{/unless}}
+ {{else}}
+ {{#if @upload.processing}}
+
{{i18n "processing"}}
+ {{else}}
+
{{i18n "uploading"}}
+ {{/if}}
+
+
+ {{/if}}
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js
index a2a89a2119c..ca2280cf67a 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js
@@ -1,25 +1,16 @@
-import Component from "@ember/component";
-import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@glimmer/component";
import { isImage } from "discourse/lib/uploads";
-export default Component.extend({
- IMAGE_TYPE: "image",
+export default class ChatComposerUpload extends Component {
+ get isImage() {
+ return isImage(
+ this.args.upload.original_filename || this.args.upload.fileName
+ );
+ }
- tagName: "",
- classNames: "chat-upload",
- isDone: false,
- upload: null,
- onCancel: null,
-
- @discourseComputed("upload.{original_filename,fileName}")
- type(upload) {
- if (isImage(upload.original_filename || upload.fileName)) {
- return this.IMAGE_TYPE;
- }
- },
-
- @discourseComputed("isDone", "upload.{original_filename,fileName}")
- fileName(isDone, upload) {
- return isDone ? upload.original_filename : upload.fileName;
- },
-});
+ get fileName() {
+ return this.args.isDone
+ ? this.args.upload.original_filename
+ : this.args.upload.fileName;
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.hbs
index 8bb20bf6d23..2f07ecc996f 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.hbs
@@ -21,19 +21,4 @@
@allowMultiple={{true}}
@fileInputId={{this.fileUploadElementId}}
@fileInputClass="hidden-upload-field"
-/>
-
-
-
-
- {{d-icon "file-audio"}}
- {{d-icon "file-video"}}
- {{d-icon "file-image"}}
-
-
-
- {{d-icon "upload"}}
- Drop a file to upload it.
-
-
-
\ No newline at end of file
+/>
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js
index a6b7f6568fc..6ecd680cbb0 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js
@@ -16,6 +16,7 @@ export default Component.extend(UppyUploadMixin, {
existingUploads: null,
uploads: null,
useMultipartUploadsIfAvailable: true,
+ uploadDropZone: null,
init() {
this._super(...arguments);
@@ -38,7 +39,7 @@ export default Component.extend(UppyUploadMixin, {
didInsertElement() {
this._super(...arguments);
- this.composerInputEl = document.querySelector(".chat-composer-input");
+ this.composerInputEl = document.querySelector(".chat-composer__input");
this.composerInputEl?.addEventListener("paste", this._pasteEventListener);
},
@@ -77,19 +78,8 @@ export default Component.extend(UppyUploadMixin, {
},
_uploadDropTargetOptions() {
- let targetEl;
- if (this.chatStateManager.isFullPageActive) {
- targetEl = document.querySelector(".full-page-chat");
- } else {
- targetEl = document.querySelector(".chat-drawer.is-expanded");
- }
-
- if (!targetEl) {
- return this._super();
- }
-
return {
- target: targetEl,
+ target: this.uploadDropZone || document.body,
};
},
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs
index fe10dcbcec8..effa6885436 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.hbs
@@ -1,93 +1,105 @@
-{{#if this.composerService.replyToMsg}}
-
-{{/if}}
-
-{{#if this.composerService.editingMessage}}
-
-{{/if}}
-
-
-
-
-
-
-
- {{#if this.isNetworkUnreliable}}
-
- {{d-icon "exclamation-circle"}}
-
+{{! template-lint-disable no-down-event-binding }}
+
+ {{#if this.shouldRenderMessageDetails}}
+
{{/if}}
-
-
- {{#unless this.disableComposer}}
-
- {{/unless}}
-
-
-{{#if this.canAttachUploads}}
-
-{{/if}}
+ {{did-update this.didUpdateMessage this.currentMessage}}
+ {{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
+ {{did-insert this.setupAppEvents}}
+ {{will-destroy this.teardownAppEvents}}
+ >
+
+
+
-{{#unless this.chatChannel.isDraft}}
-
-
+
+
+
+
+
+
+ {{#unless this.disabled}}
+
+ {{/unless}}
+
+
+
-{{/unless}}
-
\ No newline at end of file
+ {{#if this.canAttachUploads}}
+
+ {{/if}}
+
+ {{#if this.shouldRenderReplyingIndicator}}
+
+
+
+ {{/if}}
+
+
+
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
index a3236f394c6..ead10d17c57 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
@@ -1,172 +1,268 @@
-import { isEmpty } from "@ember/utils";
-import Component from "@ember/component";
-import showModal from "discourse/lib/show-modal";
-import discourseComputed, {
- afterRender,
- bind,
-} from "discourse-common/utils/decorators";
-import I18n from "I18n";
-import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
-import userSearch from "discourse/lib/user-search";
+import Component from "@glimmer/component";
import { action } from "@ember/object";
-import { cancel, next, schedule, throttle } from "@ember/runloop";
+import { inject as service } from "@ember/service";
+import { tracked } from "@glimmer/tracking";
+import { cancel, next } from "@ember/runloop";
import { cloneJSON } from "discourse-common/lib/object";
+import { chatComposerButtons } from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
+import showModal from "discourse/lib/show-modal";
+import TextareaInteractor from "discourse/plugins/chat/discourse/lib/textarea-interactor";
+import { getOwner } from "discourse-common/lib/get-owner";
+import userSearch from "discourse/lib/user-search";
import { findRawTemplate } from "discourse-common/lib/raw-templates";
import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
import { emojiUrlFor } from "discourse/lib/text";
-import { inject as service } from "@ember/service";
-import { reads } from "@ember/object/computed";
import { SKIP } from "discourse/lib/autocomplete";
-import { Promise } from "rsvp";
+import I18n from "I18n";
import { translations } from "pretty-text/emoji/data";
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
-import {
- chatComposerButtons,
- chatComposerButtonsDependentKeys,
-} from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
+import { isEmpty, isPresent } from "@ember/utils";
-const THROTTLE_MS = 150;
+export default class ChatComposer extends Component {
+ @service capabilities;
+ @service site;
+ @service siteSettings;
+ @service chat;
+ @service chatComposerPresenceManager;
+ @service chatComposerWarningsTracker;
+ @service appEvents;
+ @service chatEmojiReactionStore;
+ @service chatEmojiPickerManager;
+ @service currentUser;
+ @service chatApi;
-export default Component.extend(TextareaTextManipulation, {
- chatChannel: null,
- chat: service(),
- classNames: ["chat-composer-container"],
- classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
- chatEmojiReactionStore: service("chat-emoji-reaction-store"),
- chatEmojiPickerManager: service("chat-emoji-picker-manager"),
- chatStateManager: service("chat-state-manager"),
- timer: null,
- value: "",
- inProgressUploads: null,
- composerEventPrefix: "chat",
- composerFocusSelector: ".chat-composer-input",
- canAttachUploads: reads("siteSettings.chat_allow_uploads"),
- isNetworkUnreliable: reads("chat.isNetworkUnreliable"),
- typingMention: false,
- chatComposerWarningsTracker: service(),
+ @tracked isFocused = false;
- @discourseComputed(...chatComposerButtonsDependentKeys())
- inlineButtons() {
+ get shouldRenderReplyingIndicator() {
+ return this.context === "channel" && !this.args.channel?.isDraft;
+ }
+
+ get shouldRenderMessageDetails() {
+ return this.currentMessage?.editing || this.currentMessage?.inReplyTo;
+ }
+
+ get inlineButtons() {
return chatComposerButtons(this, "inline", this.context);
- },
+ }
- @discourseComputed(...chatComposerButtonsDependentKeys())
- dropdownButtons() {
+ get dropdownButtons() {
return chatComposerButtons(this, "dropdown", this.context);
- },
+ }
- @discourseComputed("chatEmojiPickerManager.{opened,context}")
- emojiPickerVisible(picker) {
- return picker.opened && picker.context === "chat-composer";
- },
+ get fileUploadElementId() {
+ return this.context + "-file-uploader";
+ }
- @discourseComputed("chatStateManager.isFullPageActive")
- fileUploadElementId(fullPage) {
- return fullPage ? "chat-full-page-uploader" : "chat-widget-uploader";
- },
-
- init() {
- this._super(...arguments);
-
- this.appEvents.on(
- "upload-mixin:chat-composer-uploader:in-progress-uploads",
- this,
- "_inProgressUploadsChanged"
+ get canAttachUploads() {
+ return (
+ this.siteSettings.chat_allow_uploads &&
+ isPresent(this.args.uploadDropZone)
);
+ }
- this.setProperties({
- inProgressUploads: [],
- _uploads: [],
- });
+ get disabled() {
+ return (
+ (this.args.channel.isDraft &&
+ isEmpty(this.args.channel?.chatable?.users)) ||
+ !this.chat.userCanInteractWithChat ||
+ !this.args.channel.canModifyMessages(this.currentUser)
+ );
+ }
- this.composerService?.registerFocusHandler(() => {
- this._focusTextArea();
- });
- },
+ @action
+ persistDraft() {}
- didInsertElement() {
- this._super(...arguments);
+ @action
+ setupAutocomplete(textarea) {
+ const $textarea = $(textarea);
+ this.#applyUserAutocomplete($textarea);
+ this.#applyEmojiAutocomplete($textarea);
+ this.#applyCategoryHashtagAutocomplete($textarea);
+ }
- this._textarea = this.element.querySelector(".chat-composer-input");
- this._$textarea = $(this._textarea);
- this._applyUserAutocomplete(this._$textarea);
- this._applyCategoryHashtagAutocomplete(this._$textarea);
- this._applyEmojiAutocomplete(this._$textarea);
- this.appEvents.on("chat:insert-text", this, "insertText");
- this._focusTextArea();
+ @action
+ setupTextareaInteractor(textarea) {
+ this.textareaInteractor = new TextareaInteractor(getOwner(this), textarea);
+ }
- this.appEvents.on("chat:modify-selection", this, "_modifySelection");
+ @action
+ didUpdateMessage() {
+ cancel(this._persistHandler);
+ this.textareaInteractor.value = this.currentMessage.message || "";
+ this.textareaInteractor.focus({ refreshHeight: true });
+ }
+
+ @action
+ didUpdateInReplyTo() {
+ this.textareaInteractor.focus({ ensureAtEnd: true, refreshHeight: true });
+ this.persistDraft();
+ }
+
+ get currentMessage() {
+ return this.composer.message;
+ }
+
+ get hasContent() {
+ const minLength = this.siteSettings.chat_minimum_message_length || 0;
+ return (
+ this.currentMessage?.message?.length > minLength ||
+ (this.canAttachUploads && this.currentMessage?.uploads?.length > 0)
+ );
+ }
+
+ get sendEnabled() {
+ return this.hasContent && !this.pane.sending;
+ }
+
+ @action
+ setupAppEvents() {
+ this.appEvents.on("chat:modify-selection", this, "modifySelection");
this.appEvents.on(
"chat:open-insert-link-modal",
this,
- "_openInsertLinkModal"
+ "openInsertLinkModal"
);
- document.addEventListener("visibilitychange", this._blurInput);
- document.addEventListener("resume", this._blurInput);
- document.addEventListener("freeze", this._blurInput);
+ }
- this.set("ready", true);
- },
+ @action
+ teardownAppEvents() {
+ this.appEvents.off("chat:modify-selection", this, "modifySelection");
+ this.appEvents.off(
+ "chat:open-insert-link-modal",
+ this,
+ "openInsertLinkModal"
+ );
+ }
- _modifySelection(opts = { type: null, context: null }) {
- if (opts.context !== this.context) {
- return;
- }
- const sel = this.getSelected("", { lineVal: true });
- if (opts.type === "bold") {
- this.applySurround(sel, "**", "**", "bold_text");
- } else if (opts.type === "italic") {
- this.applySurround(sel, "_", "_", "italic_text");
- } else if (opts.type === "code") {
- this.applySurround(sel, "`", "`", "code_text");
- }
- },
-
- _openInsertLinkModal() {
- const selected = this.getSelected("", { lineVal: true });
- const linkText = selected?.value;
- showModal("insert-hyperlink").setProperties({
- linkText,
- toolbarEvent: {
- addText: (text) => this.addText(selected, text),
+ @action
+ insertDiscourseLocalDate() {
+ showModal("discourse-local-dates-create-modal").setProperties({
+ insertDate: (markup) => {
+ this.textareaInteractor.addText(
+ this.textareaInteractor.getSelected(),
+ markup
+ );
+ this.textareaInteractor.focus();
},
});
- },
+ }
- willDestroyElement() {
- this._super(...arguments);
+ @action
+ uploadClicked() {
+ document.querySelector(`#${this.fileUploadElementId}`).click();
+ }
- this.appEvents.off(
- "upload-mixin:chat-composer-uploader:in-progress-uploads",
- this,
- "_inProgressUploadsChanged"
- );
+ @action
+ computeIsFocused(isFocused) {
+ next(() => {
+ this.isFocused = isFocused;
+ });
+ }
- cancel(this.timer);
+ @action
+ onInput(event) {
+ this.currentMessage.message = event.target.value;
+ this.textareaInteractor.refreshHeight();
+ this.reportReplyingPresence();
+ this.persistDraft();
+ this.captureMentions();
+ }
- this.appEvents.off("chat:insert-text", this, "insertText");
- this.appEvents.off("chat:modify-selection", this, "_modifySelection");
- this.appEvents.off(
- "chat:open-insert-link-modal",
- this,
- "_openInsertLinkModal"
- );
- document.removeEventListener("visibilitychange", this._blurInput);
- document.removeEventListener("resume", this._blurInput);
- document.removeEventListener("freeze", this._blurInput);
- },
+ @action
+ onUploadChanged(uploads, { inProgressUploadsCount }) {
+ if (
+ typeof uploads !== "undefined" &&
+ inProgressUploadsCount !== "undefined" &&
+ inProgressUploadsCount === 0 &&
+ this.currentMessage
+ ) {
+ this.currentMessage.uploads = cloneJSON(uploads);
+ }
- // It is important that this is keyDown and not keyUp, otherwise
- // we add new lines to chat message on send and on edit, because
- // you cannot prevent default with a keyUp event -- it is like trying
- // to shut the gate after the horse has already bolted!
- keyDown(event) {
- if (this.site.mobileView || event.altKey || event.metaKey) {
+ this.textareaInteractor.focus();
+ this.reportReplyingPresence();
+ this.persistDraft();
+ }
+
+ @action
+ onSend() {
+ if (!this.sendEnabled) {
return;
}
- // keyCode for 'Enter'
- if (event.keyCode === 13) {
+ if (this.site.mobileView) {
+ // prevents to hide the keyboard after sending a message
+ // we use direct DOM manipulation here because textareaInteractor.focus()
+ // is using the runloop which is too late
+ this.textareaInteractor.textarea.focus();
+ }
+
+ this.args.onSendMessage(this.currentMessage);
+ this.textareaInteractor.focus({ refreshHeight: true });
+ }
+
+ @action
+ onCancel() {
+ this.composer.cancel();
+ }
+
+ reportReplyingPresence() {
+ if (this.args.channel.isDraft) {
+ return;
+ }
+
+ this.chatComposerPresenceManager.notifyState(
+ this.args.channel.id,
+ !this.currentMessage.editing && this.hasContent
+ );
+ }
+
+ @action
+ modifySelection(event, options = { type: null, context: null }) {
+ if (options.context !== this.context) {
+ return;
+ }
+
+ const sel = this.textareaInteractor.getSelected("", { lineVal: true });
+ if (options.type === "bold") {
+ this.textareaInteractor.applySurround(sel, "**", "**", "bold_text");
+ } else if (options.type === "italic") {
+ this.textareaInteractor.applySurround(sel, "_", "_", "italic_text");
+ } else if (options.type === "code") {
+ this.textareaInteractor.applySurround(sel, "`", "`", "code_text");
+ }
+ }
+
+ @action
+ onTextareaFocusIn(textarea) {
+ if (!this.capabilities.isIOS) {
+ return;
+ }
+
+ // hack to prevent the whole viewport
+ // to move on focus input
+ textarea = document.querySelector(".chat-composer__input");
+ textarea.style.transform = "translateY(-99999px)";
+ textarea.focus();
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ textarea.style.transform = "";
+ });
+ });
+ }
+
+ @action
+ onKeyDown(event) {
+ if (
+ this.site.mobileView ||
+ event.altKey ||
+ event.metaKey ||
+ this.#isAutocompleteDisplayed()
+ ) {
+ return;
+ }
+
+ if (event.key === "Enter") {
if (event.shiftKey) {
// Shift+Enter: insert newline
return;
@@ -175,224 +271,139 @@ export default Component.extend(TextareaTextManipulation, {
// Ctrl+Enter, plain Enter: send
if (!event.ctrlKey) {
// if we are inside a code block just insert newline
- const { pre } = this.getSelected(null, { lineVal: true });
- if (this.isInside(pre, /(^|\n)```/g)) {
+ const { pre } = this.textareaInteractor.getSelected({ lineVal: true });
+ if (this.textareaInteractor.isInside(pre, /(^|\n)```/g)) {
return;
}
}
- this.sendClicked();
+ this.onSend();
+ event.preventDefault();
return false;
}
if (
event.key === "ArrowUp" &&
- this._messageIsEmpty() &&
- !this.composerService?.editingMessage
+ !this.hasContent &&
+ !this.currentMessage.editing
) {
- event.preventDefault();
- this.paneService?.editLastMessageRequested();
+ const editableMessage = this.pane?.lastCurrentUserMessage;
+ if (editableMessage) {
+ this.composer.editMessage(editableMessage);
+ }
}
if (event.key === "Escape") {
- if (this.composerService?.replyToMsg) {
- this.set("value", "");
- this.composerService?.setReplyTo(null);
+ if (this.currentMessage?.inReplyTo) {
+ this.reset();
return false;
- } else if (this.composerService?.editingMessage) {
- this.cancelEditing();
+ } else if (this.currentMessage?.editing) {
+ this.composer.onCancelEditing();
return false;
} else {
- this._textarea.blur();
+ event.target.blur();
}
}
- },
-
- didReceiveAttrs() {
- this._super(...arguments);
-
- if (
- !this.composerService?.editingMessage &&
- this.chatChannel?.draft &&
- this.chatChannel?.canModifyMessages(this.currentUser)
- ) {
- // uses uploads from draft here...
- this.set("value", this.chatChannel.draft.message);
- this.composerService?.setReplyTo(this.chatChannel.draft.replyToMsg);
-
- this._captureMentions();
- this._syncUploads(this.chatChannel.draft.uploads);
- }
-
- this.resizeTextarea();
- },
+ }
@action
- updateEditingMessage() {
- if (
- this.composerService?.editingMessage &&
- !this.paneService?.sendingLoading
- ) {
- this.set("value", this.composerService?.editingMessage.message);
+ reset() {
+ this.composer.reset(this.args.channel);
+ }
- 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
- // for chat-composer-uploads to preload in existing uploads for drafts
- // and for when messages are being edited.
- //
- // the opposite is true as well -- when an upload is completed the chat-composer
- // needs its internal state updated so drafts can be saved, which is handled
- // by the uploadsChanged action
- _syncUploads(newUploads = []) {
- const currentUploadIds = this._uploads.mapBy("id");
- const newUploadIds = newUploads.mapBy("id");
-
- // don't need to load the uploads into chat-composer-uploads if
- // nothing has changed otherwise we would rerender for no reason
- if (
- currentUploadIds.length === newUploadIds.length &&
- newUploadIds.every((newUploadId) =>
- currentUploadIds.includes(newUploadId)
- )
- ) {
+ @action
+ openInsertLinkModal(event, options = { context: null }) {
+ if (options.context !== this.context) {
return;
}
- this.set("_uploads", cloneJSON(newUploads));
- },
-
- _inProgressUploadsChanged(inProgressUploads) {
- next(() => {
- if (this.isDestroying || this.isDestroyed) {
- return;
- }
-
- this.set("inProgressUploads", inProgressUploads);
+ const selected = this.textareaInteractor.getSelected("", { lineVal: true });
+ const linkText = selected?.value;
+ showModal("insert-hyperlink").setProperties({
+ linkText,
+ toolbarEvent: {
+ addText: (text) => this.textareaInteractor.addText(selected, text),
+ },
});
- },
+ }
@action
- onTextareaInput(value) {
- this.set("value", value);
- this.resizeTextarea();
-
- this._captureMentions();
-
- // throttle, not debounce, because we do eventually want to react during the typing
- this.timer = throttle(this, this._handleTextareaInput, THROTTLE_MS);
- },
-
- @bind
- _handleTextareaInput() {
- this.composerService?.onComposerValueChange?.({ value: this.value });
- },
-
- @bind
- _captureMentions() {
- if (this.value) {
- this.chatComposerWarningsTracker.trackMentions(this.value);
- }
- },
-
- @bind
- _blurInput() {
- document.activeElement?.blur();
- },
-
- @action
- uploadClicked() {
- this.element.querySelector(`#${this.fileUploadElementId}`).click();
- },
-
- @bind
- didSelectEmoji(emoji) {
+ onSelectEmoji(emoji) {
const code = `:${emoji}:`;
this.chatEmojiReactionStore.track(code);
- this.addText(this.getSelected(), code);
+ this.textareaInteractor.addText(
+ this.textareaInteractor.getSelected(),
+ code
+ );
if (this.site.desktopView) {
- this._focusTextArea();
+ this.textareaInteractor.focus();
} else {
this.chatEmojiPickerManager.close();
}
- },
+ }
@action
- closeComposerDropdown() {
- this.chatEmojiPickerManager.close();
- this.appEvents.trigger("d-popover:close");
- },
+ captureMentions() {
+ if (this.hasContent) {
+ this.chatComposerWarningsTracker.trackMentions(
+ this.currentMessage.message
+ );
+ }
+ }
- @action
- insertDiscourseLocalDate() {
- showModal("discourse-local-dates-create-modal").setProperties({
- insertDate: (markup) => {
- this.addText(this.getSelected(), markup);
+ #applyUserAutocomplete($textarea) {
+ if (!this.siteSettings.enable_mentions) {
+ return;
+ }
+
+ $textarea.autocomplete({
+ template: findRawTemplate("user-selector-autocomplete"),
+ key: "@",
+ width: "100%",
+ treatAsTextarea: true,
+ autoSelectFirstSuggestion: true,
+ transformComplete: (v) => v.username || v.name,
+ dataSource: (term) => {
+ return userSearch({ term, includeGroups: true }).then((result) => {
+ if (result?.users?.length > 0) {
+ const presentUserNames =
+ this.chat.presenceChannel.users?.mapBy("username");
+ result.users.forEach((user) => {
+ if (presentUserNames.includes(user.username)) {
+ user.cssClasses = "is-online";
+ }
+ });
+ }
+ return result;
+ });
+ },
+ afterComplete: (text, event) => {
+ event.preventDefault();
+ this.textareaInteractor.value = text;
+ this.textareaInteractor.focus();
+ this.captureMentions();
},
});
- },
+ }
- // text-area-manipulation mixin override
- addText() {
- this._super(...arguments);
-
- this.resizeTextarea();
- },
-
- _applyUserAutocomplete($textarea) {
- if (this.siteSettings.enable_mentions) {
- $textarea.autocomplete({
- template: findRawTemplate("user-selector-autocomplete"),
- key: "@",
- width: "100%",
- treatAsTextarea: true,
- autoSelectFirstSuggestion: true,
- transformComplete: (v) => v.username || v.name,
- dataSource: (term) => {
- return userSearch({ term, includeGroups: true }).then((result) => {
- if (result?.users?.length > 0) {
- const presentUserNames =
- this.chat.presenceChannel.users?.mapBy("username");
- result.users.forEach((user) => {
- if (presentUserNames.includes(user.username)) {
- user.cssClasses = "is-online";
- }
- });
- }
- return result;
- });
- },
- afterComplete: (text) => {
- this.set("value", text);
- this._focusTextArea();
- this._captureMentions();
- },
- });
- }
- },
-
- _applyCategoryHashtagAutocomplete($textarea) {
+ #applyCategoryHashtagAutocomplete($textarea) {
setupHashtagAutocomplete(
this.site.hashtag_configurations["chat-composer"],
$textarea,
this.siteSettings,
{
treatAsTextarea: true,
- afterComplete: (value) => {
- this.set("value", value);
- return this._focusTextArea();
+ afterComplete: (text, event) => {
+ event.preventDefault();
+ this.textareaInteractor.value = text;
+ this.textareaInteractor.focus();
},
}
);
- },
+ }
- _applyEmojiAutocomplete($textarea) {
+ #applyEmojiAutocomplete($textarea) {
if (!this.siteSettings.enable_emoji) {
return;
}
@@ -400,12 +411,12 @@ export default Component.extend(TextareaTextManipulation, {
$textarea.autocomplete({
template: findRawTemplate("emoji-selector-autocomplete"),
key: ":",
- afterComplete: (text) => {
- this.set("value", text);
- this._focusTextArea();
+ afterComplete: (text, event) => {
+ event.preventDefault();
+ this.textareaInteractor.value = text;
+ this.textareaInteractor.focus();
},
treatAsTextarea: true,
-
onKeyUp: (text, cp) => {
const matches =
/(?:^|[\s.\?,@\/#!%&*;:\[\]{}=\-_()])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
@@ -416,7 +427,6 @@ export default Component.extend(TextareaTextManipulation, {
return [matches[1]];
}
},
-
transformComplete: (v) => {
if (v.code) {
this.chatEmojiReactionStore.track(v.code);
@@ -430,7 +440,6 @@ export default Component.extend(TextareaTextManipulation, {
return "";
}
},
-
dataSource: (term) => {
return new Promise((resolve) => {
const full = `:${term}`;
@@ -472,8 +481,7 @@ export default Component.extend(TextareaTextManipulation, {
// note this will only work for emojis starting with :
// eg: :-)
- const emojiTranslation =
- this.get("site.custom_emoji_translation") || {};
+ const emojiTranslation = this.site.custom_emoji_translation || {};
const allTranslations = Object.assign(
{},
translations,
@@ -483,7 +491,7 @@ export default Component.extend(TextareaTextManipulation, {
return resolve([allTranslations[full]]);
}
- const emojiDenied = this.get("site.denied_emojis") || [];
+ const emojiDenied = this.site.denied_emojis || [];
const match = term.match(/^:?(.*?):t([2-6])?$/);
if (match) {
const name = match[1];
@@ -520,264 +528,9 @@ export default Component.extend(TextareaTextManipulation, {
});
},
});
- },
+ }
- @afterRender
- _focusTextArea(opts = { ensureAtEnd: false, resizeTextarea: true }) {
- if (this.chatChannel.isDraft) {
- return;
- }
-
- if (!this._textarea) {
- return;
- }
-
- if (opts.resizeTextarea) {
- this.resizeTextarea();
- }
-
- if (opts.ensureAtEnd) {
- this._textarea.setSelectionRange(this.value.length, this.value.length);
- }
-
- if (this.capabilities.isIpadOS || this.site.mobileView) {
- return;
- }
-
- schedule("afterRender", () => {
- this._textarea?.focus();
- });
- },
-
- @action
- onEmojiSelected(code) {
- this.emojiSelected(code);
- this.set("emojiPickerIsActive", false);
- },
-
- @discourseComputed(
- "chatChannel.{id,chatable.users.[]}",
- "chat.userCanInteractWithChat"
- )
- disableComposer(channel, userCanInteractWithChat) {
- return (
- (channel.isDraft && isEmpty(channel?.chatable?.users)) ||
- !userCanInteractWithChat ||
- !channel.canModifyMessages(this.currentUser)
- );
- },
-
- @discourseComputed(
- "chatChannel.{chatable.users.[],id}",
- "chat.userCanInteractWithChat"
- )
- placeholder(chatChannel, userCanInteractWithChat) {
- if (!chatChannel.canModifyMessages(this.currentUser)) {
- return I18n.t(
- `chat.placeholder_new_message_disallowed.${chatChannel.status}`
- );
- }
-
- if (chatChannel.isDraft) {
- if (chatChannel?.chatable?.users?.length) {
- return I18n.t("chat.placeholder_start_conversation_users", {
- commaSeparatedUsernames: chatChannel.chatable.users
- .mapBy("username")
- .join(I18n.t("word_connector.comma")),
- });
- } else {
- return I18n.t("chat.placeholder_start_conversation");
- }
- }
-
- if (!userCanInteractWithChat) {
- return I18n.t("chat.placeholder_silenced");
- } else {
- return this.messageRecipient(chatChannel);
- }
- },
-
- messageRecipient(chatChannel) {
- if (chatChannel.isDirectMessageChannel) {
- const directMessageRecipients = chatChannel.chatable.users;
- if (
- directMessageRecipients.length === 1 &&
- directMessageRecipients[0].id === this.currentUser.id
- ) {
- return I18n.t("chat.placeholder_self");
- }
-
- return I18n.t("chat.placeholder_users", {
- commaSeparatedNames: directMessageRecipients
- .map((u) => u.name || `@${u.username}`)
- .join(I18n.t("word_connector.comma")),
- });
- } else {
- return I18n.t("chat.placeholder_channel", {
- channelName: `#${chatChannel.title}`,
- });
- }
- },
-
- @discourseComputed(
- "value",
- "paneService.sendingLoading",
- "disableComposer",
- "inProgressUploads.[]"
- )
- sendDisabled(value, loading, disableComposer, inProgressUploads) {
- if (loading || disableComposer || inProgressUploads.length > 0) {
- return true;
- }
-
- return !this._messageIsValid();
- },
-
- @action
- sendClicked() {
- if (this.site.mobileView) {
- // prevents android to hide the keyboard after sending a message
- // we do a focusTextarea later but it's too late for android
- document.querySelector(this.composerFocusSelector).focus();
- }
-
- if (this.sendDisabled) {
- return;
- }
-
- this.composerService?.editingMessage
- ? this.internalEditMessage()
- : this.internalSendMessage();
- },
-
- @action
- internalSendMessage() {
- // 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
- internalEditMessage() {
- return this.paneService
- ?.editMessage(this.value, this._uploads)
- .then(this.reset);
- },
-
- _messageIsValid() {
- const validLength =
- (this.value || "").trim().length >=
- (this.siteSettings.chat_minimum_message_length || 0);
-
- if (this.canAttachUploads) {
- if (this._messageIsEmpty()) {
- // If message is empty, an an upload must present for sending to be enabled
- return this._uploads.length;
- } else {
- // Message is non-empty. Make sure it's long enough to be valid.
- return validLength;
- }
- }
-
- // Attachments are disabled so for a message to be valid it must be long enough.
- return validLength;
- },
-
- _messageIsEmpty() {
- return (this.value || "").trim() === "";
- },
-
- @action
- reset() {
- if (this.isDestroyed || this.isDestroying) {
- return;
- }
-
- this.setProperties({
- value: "",
- inReplyMsg: null,
- });
- this._captureMentions();
- this._syncUploads([]);
- this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
- this.composerService?.onComposerValueChange?.(
- this.value,
- this._uploads,
- this.composerService?.replyToMsg
- );
- },
-
- @action
- cancelReplyTo() {
- this.composerService?.setReplyTo(null);
- },
-
- @action
- cancelEditing() {
- this.composerService?.cancelEditing();
- this.set("value", "");
- this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
- },
-
- _cursorIsOnEmptyLine() {
- const selectionStart = this._textarea.selectionStart;
- if (selectionStart === 0) {
- return true;
- } else if (this._textarea.value.charAt(selectionStart - 1) === "\n") {
- return true;
- } else {
- return false;
- }
- },
-
- @action
- uploadsChanged(uploads, { inProgressUploadsCount }) {
- this.set("_uploads", cloneJSON(uploads));
- this.composerService?.onComposerValueChange?.({
- uploads: this._uploads,
- inProgressUploadsCount,
- });
- },
-
- @action
- onTextareaFocusIn(target) {
- if (!this.capabilities.isIOS) {
- return;
- }
-
- // hack to prevent the whole viewport
- // to move on focus input
- target = document.querySelector(".chat-composer-input");
- target.style.transform = "translateY(-99999px)";
- target.focus();
- window.requestAnimationFrame(() => {
- window.requestAnimationFrame(() => {
- target.style.transform = "";
- });
- });
- },
-
- @action
- resizeTextarea() {
- schedule("afterRender", () => {
- if (!this._textarea) {
- return;
- }
-
- // this is a quirk which forces us to `auto` first or textarea
- // won't resize
- this._textarea.style.height = "auto";
-
- // +1 is to workaround a rounding error visible on electron
- // causing scrollbars to show when they shouldn’t
- this._textarea.style.height = this._textarea.scrollHeight + 1 + "px";
- });
- },
-});
+ #isAutocompleteDisplayed() {
+ return document.querySelector(".autocomplete");
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.hbs
index fccac8b872a..35d7499f23b 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.hbs
@@ -19,6 +19,6 @@
/>
{{#if this.previewedChannel}}
-
+
{{/if}}
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel.hbs
index 7a94b39267e..f78588d7632 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-drawer/channel.hbs
@@ -16,7 +16,7 @@
{{did-update this.fetchChannel @params.channelId}}
>
{{#if this.chat.activeChannel}}
-
+
{{yield}}
+
{{/if}}
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-side-panel.js b/plugins/chat/assets/javascripts/discourse/components/chat-side-panel.js
index 53be6e18eb9..5c046ab32d2 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-side-panel.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-side-panel.js
@@ -1,6 +1,49 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
+import { action } from "@ember/object";
+import { htmlSafe } from "@ember/template";
+import { tracked } from "@glimmer/tracking";
+
+const MIN_CHAT_CHANNEL_WIDTH = 300;
export default class ChatSidePanel extends Component {
@service chatStateManager;
+ @service chatSidePanelSize;
+ @service site;
+
+ @tracked sidePanel;
+
+ @action
+ setSidePanel(element) {
+ this.sidePanel = element;
+ }
+
+ get width() {
+ if (!this.sidePanel) {
+ return;
+ }
+
+ const maxWidth = Math.min(
+ this.#maxWidth(this.sidePanel),
+ this.chatSidePanelSize.width
+ );
+
+ return htmlSafe(`width:${maxWidth}px`);
+ }
+
+ @action
+ didResize(element, size) {
+ const parentWidth = element.parentElement.getBoundingClientRect().width;
+ const mainPanelWidth = parentWidth - size.width;
+
+ if (mainPanelWidth > MIN_CHAT_CHANNEL_WIDTH) {
+ this.chatSidePanelSize.width = size.width;
+ element.style.width = size.width + "px";
+ }
+ }
+
+ #maxWidth(element) {
+ const parentWidth = element.parentElement.getBoundingClientRect().width;
+ return parentWidth - MIN_CHAT_CHANNEL_WIDTH;
+ }
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
index 1c00da944af..43f265938ad 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
@@ -1,10 +1,12 @@
{{#if @includeHeader}}
@@ -13,7 +15,8 @@
{{d-icon "times"}}
@@ -47,20 +50,20 @@
{{#if this.chatChannelThreadPane.selectingMessages}}
{{else}}
-
{{/if}}
+
+
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread.js b/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
index ee63ef2ed39..7eb9fc2851f 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
@@ -1,6 +1,4 @@
import Component from "@glimmer/component";
-import { cloneJSON } from "discourse-common/lib/object";
-import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
@@ -27,6 +25,7 @@ export default class ChatThreadPanel extends Component {
@tracked loading;
@tracked loadingMorePast;
+ @tracked uploadDropZone;
scrollable = null;
@@ -43,6 +42,19 @@ export default class ChatThreadPanel extends Component {
this.chatChannelThreadPaneSubscriptionsManager.subscribe(this.thread);
}
+ @action
+ setUploadDropZone(element) {
+ this.uploadDropZone = element;
+ }
+
+ @action
+ setupMessage() {
+ this.chatChannelThreadComposer.message = ChatMessage.createDraftMessage(
+ this.channel,
+ { user: this.currentUser, thread_id: this.thread.id }
+ );
+ }
+
@action
unsubscribeFromUpdates() {
this.chatChannelThreadPaneSubscriptionsManager.unsubscribe();
@@ -168,27 +180,34 @@ export default class ChatThreadPanel extends Component {
}
@action
- sendMessage(message, uploads = []) {
+ onSendMessage(message) {
+ if (message.editing) {
+ this.#sendEditMessage(message);
+ } else {
+ this.#sendNewMessage(message);
+ }
+ }
+
+ @action
+ resetComposer() {
+ this.chatChannelThreadComposer.reset(this.channel);
+ }
+
+ #sendNewMessage(message) {
// TODO (martin) For desktop notifications
// resetIdle()
- if (this.chatChannelThreadPane.sendingLoading) {
+ if (this.chatChannelThreadPane.sending) {
return;
}
- this.chatChannelThreadPane.sendingLoading = true;
- this.channel.draft = ChatMessageDraft.create();
+ this.chatChannelThreadPane.sending = true;
// TODO (martin) Handling case when channel is not followed???? IDK if we
// even let people send messages in threads without this, seems weird.
- const stagedMessage = ChatMessage.createStagedMessage(this.channel, {
- message,
- created_at: new Date(),
- uploads: cloneJSON(uploads),
- user: this.currentUser,
- thread_id: this.thread.id,
- });
-
+ this.thread.stageMessage(message);
+ const stagedMessage = message;
+ this.resetComposer();
this.thread.messagesManager.addMessages([stagedMessage]);
// TODO (martin) Scrolling!!
@@ -214,8 +233,25 @@ export default class ChatThreadPanel extends Component {
if (this._selfDeleted) {
return;
}
- this.chatChannelThreadPane.sendingLoading = false;
- this.chatChannelThreadPane.resetAfterSend();
+ this.chatChannelThreadPane.sending = false;
+ });
+ }
+
+ #sendEditMessage(message) {
+ this.chatChannelThreadPane.sending = true;
+
+ const data = {
+ new_message: message.message,
+ upload_ids: message.uploads.map((upload) => upload.id),
+ };
+
+ this.resetComposer();
+
+ return this.chatApi
+ .editMessage(message.channelId, message.id, data)
+ .catch(popupAjaxError)
+ .finally(() => {
+ this.chatChannelThreadPane.sending = false;
});
}
@@ -302,6 +338,6 @@ export default class ChatThreadPanel extends Component {
}
}
- this.chatChannelThreadPane.resetAfterSend();
+ this.resetComposer();
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.hbs
new file mode 100644
index 00000000000..08980dba7aa
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.hbs
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ {{this.title}}
+
+
+
+
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.js b/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.js
new file mode 100644
index 00000000000..3e6b2487685
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-upload-drop-zone.js
@@ -0,0 +1,19 @@
+import Component from "@glimmer/component";
+import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
+import I18n from "I18n";
+
+export default class ChatUploadDropZone extends Component {
+ get title() {
+ if (this.#isThread()) {
+ return I18n.t("chat.upload_to_thread");
+ } else {
+ return I18n.t("chat.upload_to_channel", {
+ title: this.args.model.title,
+ });
+ }
+ }
+
+ #isThread() {
+ return this.args.model instanceof ChatThread;
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/composer/channel.js b/plugins/chat/assets/javascripts/discourse/components/chat/composer/channel.js
new file mode 100644
index 00000000000..5f5e66e38d7
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat/composer/channel.js
@@ -0,0 +1,95 @@
+import ChatComposer from "../../chat-composer";
+import { inject as service } from "@ember/service";
+import I18n from "I18n";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { action } from "@ember/object";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import { Promise } from "rsvp";
+
+export default class ChatComposerChannel extends ChatComposer {
+ @service("chat-channel-composer") composer;
+ @service("chat-channel-pane") pane;
+
+ context = "channel";
+
+ @action
+ sendMessage(raw) {
+ const message = ChatMessage.createDraftMessage(this.args.channel, {
+ user: this.currentUser,
+ message: raw,
+ });
+
+ this.args.onSendMessage(message);
+
+ return Promise.resolve();
+ }
+
+ @action
+ persistDraft() {
+ if (this.args.channel?.isDraft) {
+ return;
+ }
+
+ this._persistHandler = discourseDebounce(
+ this,
+ this._debouncedPersistDraft,
+ 2000
+ );
+ }
+
+ @action
+ _debouncedPersistDraft() {
+ this.chatApi.saveDraft(
+ this.args.channel.id,
+ this.currentMessage.toJSONDraft()
+ );
+ }
+
+ get placeholder() {
+ if (!this.args.channel.canModifyMessages(this.currentUser)) {
+ return I18n.t(
+ `chat.placeholder_new_message_disallowed.${this.args.channel.status}`
+ );
+ }
+
+ if (this.args.channel.isDraft) {
+ if (this.args.channel?.chatable?.users?.length) {
+ return I18n.t("chat.placeholder_start_conversation_users", {
+ commaSeparatedUsernames: this.args.channel.chatable.users
+ .mapBy("username")
+ .join(I18n.t("word_connector.comma")),
+ });
+ } else {
+ return I18n.t("chat.placeholder_start_conversation");
+ }
+ }
+
+ if (!this.chat.userCanInteractWithChat) {
+ return I18n.t("chat.placeholder_silenced");
+ } else {
+ return this.#messageRecipients(this.args.channel);
+ }
+ }
+
+ #messageRecipients(channel) {
+ if (channel.isDirectMessageChannel) {
+ const directMessageRecipients = channel.chatable.users;
+ if (
+ directMessageRecipients.length === 1 &&
+ directMessageRecipients[0].id === this.currentUser.id
+ ) {
+ return I18n.t("chat.placeholder_self");
+ }
+
+ return I18n.t("chat.placeholder_users", {
+ commaSeparatedNames: directMessageRecipients
+ .map((u) => u.name || `@${u.username}`)
+ .join(I18n.t("word_connector.comma")),
+ });
+ } else {
+ return I18n.t("chat.placeholder_channel", {
+ channelName: `#${channel.title}`,
+ });
+ }
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat/composer/thread.js b/plugins/chat/assets/javascripts/discourse/components/chat/composer/thread.js
new file mode 100644
index 00000000000..76a6c15860c
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat/composer/thread.js
@@ -0,0 +1,30 @@
+import ChatComposer from "../../chat-composer";
+import { inject as service } from "@ember/service";
+import I18n from "I18n";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import { Promise } from "rsvp";
+import { action } from "@ember/object";
+
+export default class ChatComposerThread extends ChatComposer {
+ @service("chat-channel-thread-composer") composer;
+ @service("chat-channel-thread-pane") pane;
+
+ context = "thread";
+
+ @action
+ sendMessage(raw) {
+ const message = ChatMessage.createDraftMessage(this.args.channel, {
+ user: this.currentUser,
+ message: raw,
+ thread_id: this.args.channel.activeThread.id,
+ });
+
+ this.args.onSendMessage(message);
+
+ return Promise.resolve();
+ }
+
+ get placeholder() {
+ return I18n.t("chat.placeholder_thread");
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/direct-message-creator.js b/plugins/chat/assets/javascripts/discourse/components/direct-message-creator.js
index 60ce768975e..b70382d4274 100644
--- a/plugins/chat/assets/javascripts/discourse/components/direct-message-creator.js
+++ b/plugins/chat/assets/javascripts/discourse/components/direct-message-creator.js
@@ -163,7 +163,7 @@ export default Component.extend({
@action
handleFilterKeyUp(event) {
if (event.key === "Tab") {
- const enabledComposer = document.querySelector(".chat-composer-input");
+ const enabledComposer = document.querySelector(".chat-composer__input");
if (enabledComposer && !enabledComposer.disabled) {
event.preventDefault();
event.stopPropagation();
diff --git a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
index b30ea425d5a..97fc62b9593 100644
--- a/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/full-page-chat.hbs
@@ -1,5 +1,5 @@
{{#if @channel.id}}
-
diff --git a/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js b/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
index 5d91f205e4f..cd68b634d5c 100644
--- a/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
+++ b/plugins/chat/assets/javascripts/discourse/helpers/format-chat-date.js
@@ -8,15 +8,21 @@ registerUnbound("format-chat-date", function (message, mode) {
const currentUser = User.current();
const tz = currentUser ? currentUser.user_option.timezone : moment.tz.guess();
const date = moment(new Date(message.createdAt), tz);
- const url = getURL(`/chat/c/-/${message.channelId}/${message.id}`);
- const title = date.format(I18n.t("dates.long_with_year"));
+ const title = date.format(I18n.t("dates.long_with_year"));
const display =
mode === "tiny"
? date.format(I18n.t("chat.dates.time_tiny"))
: date.format(I18n.t("dates.time"));
- return htmlSafe(
- `
${display}`
- );
+ if (message.staged) {
+ return htmlSafe(
+ `
${display}`
+ );
+ } else {
+ const url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`);
+ return htmlSafe(
+ `
${display}`
+ );
+ }
});
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js
deleted file mode 100644
index 5d436f6a4f7..00000000000
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-cook-function.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
-import { generateCookFunction } from "discourse/lib/text";
-import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
-
-export default {
- name: "chat-cook-function",
-
- before: "chat-setup",
-
- initialize(container) {
- const site = container.lookup("service:site");
-
- const markdownOptions = {
- featuresOverride:
- site.markdown_additional_options?.chat?.limited_pretty_text_features,
- markdownItRules:
- site.markdown_additional_options?.chat
- ?.limited_pretty_text_markdown_rules,
- hashtagTypesInPriorityOrder:
- site.hashtag_configurations?.["chat-composer"],
- hashtagIcons: site.hashtag_icons,
- };
-
- generateCookFunction(markdownOptions).then((cookFunction) => {
- ChatMessage.cookFunction = (raw) => {
- return simpleCategoryHashMentionTransform(
- cookFunction(raw),
- site.categories
- );
- };
- });
- },
-};
diff --git a/plugins/chat/assets/javascripts/discourse/initializers/chat-keyboard-shortcuts.js b/plugins/chat/assets/javascripts/discourse/initializers/chat-keyboard-shortcuts.js
index 8439c53d8eb..b535c4e119d 100644
--- a/plugins/chat/assets/javascripts/discourse/initializers/chat-keyboard-shortcuts.js
+++ b/plugins/chat/assets/javascripts/discourse/initializers/chat-keyboard-shortcuts.js
@@ -42,7 +42,8 @@ export default {
chatService.switchChannelUpOrDown("down");
};
- const isChatComposer = (el) => el.classList.contains("chat-composer-input");
+ const isChatComposer = (el) =>
+ el.classList.contains("chat-composer__input");
const isInputSelection = (el) => {
const inputs = ["input", "textarea", "select", "button"];
const elementTagName = el?.tagName.toLowerCase();
@@ -58,7 +59,7 @@ export default {
}
event.preventDefault();
event.stopPropagation();
- appEvents.trigger("chat:modify-selection", {
+ appEvents.trigger("chat:modify-selection", event, {
type,
context: event.target.dataset.chatComposerContext,
});
@@ -70,7 +71,9 @@ export default {
}
event.preventDefault();
event.stopPropagation();
- appEvents.trigger("chat:open-insert-link-modal", { event });
+ appEvents.trigger("chat:open-insert-link-modal", event, {
+ context: event.target.dataset.chatComposerContext,
+ });
};
const openChatDrawer = (event) => {
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-message-container.js b/plugins/chat/assets/javascripts/discourse/lib/chat-message-container.js
index 2be3b6aec36..ccf5aef3f14 100644
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-message-container.js
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-message-container.js
@@ -6,7 +6,7 @@ export default function chatMessageContainer(id, context) {
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}"]`;
+ selector = `.chat-channel .chat-message-container[data-id="${id}"]`;
}
return document.querySelector(selector);
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-message-interactor.js b/plugins/chat/assets/javascripts/discourse/lib/chat-message-interactor.js
index 349b83f577e..d65a3e532ae 100644
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-message-interactor.js
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-message-interactor.js
@@ -349,12 +349,12 @@ export default class ChatMessageInteractor {
@action
reply() {
- this.composer.setReplyTo(this.message.id);
+ this.composer.replyTo(this.message);
}
@action
edit() {
- this.composer.editButtonClicked(this.message.id);
+ this.composer.editMessage(this.message);
}
@action
diff --git a/plugins/chat/assets/javascripts/discourse/lib/textarea-interactor.js b/plugins/chat/assets/javascripts/discourse/lib/textarea-interactor.js
new file mode 100644
index 00000000000..025f9c316d8
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/lib/textarea-interactor.js
@@ -0,0 +1,72 @@
+import EmberObject from "@ember/object";
+import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
+import { next, schedule } from "@ember/runloop";
+import { setOwner } from "@ember/application";
+import { inject as service } from "@ember/service";
+
+// This class sole purpose is to provide a way to interact with the textarea
+// using the existing TextareaTextManipulation mixin without using it directly
+// in the composer component. It will make future migration easier.
+export default class TextareaInteractor extends EmberObject.extend(
+ TextareaTextManipulation
+) {
+ @service capabilities;
+ @service site;
+
+ constructor(owner, textarea) {
+ super(...arguments);
+ setOwner(this, owner);
+ this.textarea = textarea;
+ this._textarea = textarea;
+ this.element = this._textarea;
+ this.ready = true;
+ }
+
+ set value(value) {
+ this._textarea.value = value;
+ const event = new Event("input", {
+ bubbles: true,
+ cancelable: true,
+ });
+ this._textarea.dispatchEvent(event);
+ }
+
+ focus(opts = { ensureAtEnd: false, refreshHeight: true }) {
+ next(() => {
+ if (opts.refreshHeight) {
+ this.refreshHeight();
+ }
+
+ if (opts.ensureAtEnd) {
+ this.ensureCaretAtEnd();
+ }
+
+ if (this.capabilities.isIpadOS || this.site.mobileView) {
+ return;
+ }
+
+ this.focusTextArea();
+ });
+ }
+
+ ensureCaretAtEnd() {
+ schedule("afterRender", () => {
+ this._textarea.setSelectionRange(
+ this._textarea.value.length,
+ this._textarea.value.length
+ );
+ });
+ }
+
+ refreshHeight() {
+ schedule("afterRender", () => {
+ // this is a quirk which forces us to `auto` first or textarea
+ // won't resize
+ this._textarea.style.height = "auto";
+
+ // +1 is to workaround a rounding error visible on electron
+ // causing scrollbars to show when they shouldn’t
+ this._textarea.style.height = this._textarea.scrollHeight + 1 + "px";
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
index 207174e7de9..76ba2a1209b 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
@@ -8,6 +8,7 @@ import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel
import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-threads-manager";
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { getOwner } from "discourse-common/lib/get-owner";
+import guid from "pretty-text/guid";
export const CHATABLE_TYPES = {
directMessageChannel: "DirectMessage",
@@ -78,6 +79,10 @@ export default class ChatChannel extends RestModel {
this.messagesManager.messages = messages;
}
+ get canLoadMoreFuture() {
+ return this.messagesManager.canLoadMoreFuture;
+ }
+
get escapedTitle() {
return escapeExpression(this.title);
}
@@ -150,6 +155,27 @@ export default class ChatChannel extends RestModel {
this.channelMessageBusLastId = details.channel_message_bus_last_id;
}
+ stageMessage(message) {
+ message.id = guid();
+ message.staged = true;
+ message.draft = false;
+ message.createdAt ??= moment.utc().format();
+ message.cook();
+
+ if (message.inReplyTo) {
+ if (!message.inReplyTo.threadId) {
+ message.inReplyTo.threadId = guid();
+ message.inReplyTo.threadReplyCount = 1;
+ }
+
+ if (!this.threadingEnabled) {
+ this.messagesManager.addMessages([message]);
+ }
+ } else {
+ this.messagesManager.addMessages([message]);
+ }
+ }
+
canModifyMessages(user) {
if (user.staff) {
return !STAFF_READONLY_STATUSES.includes(this.status);
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js b/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js
deleted file mode 100644
index 00709add3f3..00000000000
--- a/plugins/chat/assets/javascripts/discourse/models/chat-message-draft.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { tracked } from "@glimmer/tracking";
-
-export default class ChatMessageDraft {
- static create(args = {}) {
- return new ChatMessageDraft(args ?? {});
- }
-
- @tracked uploads;
- @tracked message;
- @tracked _replyToMsg;
-
- constructor(args = {}) {
- this.message = args.message ?? "";
- this.uploads = args.uploads ?? [];
- this.replyToMsg = args.replyToMsg;
- }
-
- get replyToMsg() {
- return this._replyToMsg;
- }
-
- set replyToMsg(message) {
- this._replyToMsg = message
- ? {
- id: message.id,
- excerpt: message.excerpt,
- user: {
- id: message.user.id,
- name: message.user.name,
- avatar_template: message.user.avatar_template,
- username: message.user.username,
- },
- }
- : null;
- }
-
- toJSON() {
- if (
- this.message?.length === 0 &&
- this.uploads?.length === 0 &&
- !this.replyToMsg
- ) {
- return null;
- }
-
- const data = {};
-
- if (this.uploads?.length > 0) {
- data.uploads = this.uploads;
- }
-
- if (this.message?.length > 0) {
- data.message = this.message;
- }
-
- if (this.replyToMsg) {
- data.replyToMsg = this.replyToMsg;
- }
-
- return JSON.stringify(data);
- }
-}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-message.js b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
index 814964baa0e..880e7d03feb 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-message.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-message.js
@@ -4,7 +4,9 @@ import { TrackedArray, TrackedObject } from "@ember-compat/tracked-built-ins";
import ChatMessageReaction from "discourse/plugins/chat/discourse/models/chat-message-reaction";
import Bookmark from "discourse/models/bookmark";
import I18n from "I18n";
-import guid from "pretty-text/guid";
+import { generateCookFunction } from "discourse/lib/text";
+import simpleCategoryHashMentionTransform from "discourse/plugins/chat/discourse/lib/simple-category-hash-mention-transform";
+import { getOwner } from "discourse-common/lib/get-owner";
export default class ChatMessage {
static cookFunction = null;
@@ -13,10 +15,9 @@ export default class ChatMessage {
return new ChatMessage(channel, args);
}
- static createStagedMessage(channel, args = {}) {
- args.id = guid();
- args.staged = true;
- return new ChatMessage(channel, args);
+ static createDraftMessage(channel, args = {}) {
+ args.draft = true;
+ return ChatMessage.create(channel, args);
}
@tracked id;
@@ -24,6 +25,7 @@ export default class ChatMessage {
@tracked selected;
@tracked channel;
@tracked staged = false;
+ @tracked draft = false;
@tracked channelId;
@tracked createdAt;
@tracked deletedAt;
@@ -69,11 +71,20 @@ export default class ChatMessage {
this.reviewableId = args.reviewableId || args.reviewable_id;
this.userFlagStatus = args.userFlagStatus || args.user_flag_status;
this.inReplyTo =
- args.inReplyTo || args.in_reply_to
- ? ChatMessage.create(channel, args.in_reply_to)
- : null;
- this.message = args.message;
- this.cooked = args.cooked || ChatMessage.cookFunction(this.message);
+ args.inReplyTo ||
+ (args.in_reply_to || args.replyToMsg
+ ? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
+ : null);
+ this.draft = args.draft;
+ this.message = args.message || "";
+
+ if (args.cooked) {
+ this.cooked = args.cooked;
+ } else {
+ this.cooked = "";
+ this.cook();
+ }
+
this.reactions = this.#initChatMessageReactionModel(
args.id,
args.reactions
@@ -83,6 +94,38 @@ export default class ChatMessage {
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
}
+ cook() {
+ const site = getOwner(this).lookup("service:site");
+
+ const markdownOptions = {
+ featuresOverride:
+ site.markdown_additional_options?.chat?.limited_pretty_text_features,
+ markdownItRules:
+ site.markdown_additional_options?.chat
+ ?.limited_pretty_text_markdown_rules,
+ hashtagTypesInPriorityOrder:
+ site.hashtag_configurations?.["chat-composer"],
+ hashtagIcons: site.hashtag_icons,
+ };
+
+ if (ChatMessage.cookFunction) {
+ this.cooked = ChatMessage.cookFunction(this.message);
+ this.incrementVersion();
+ } else {
+ generateCookFunction(markdownOptions).then((cookFunction) => {
+ ChatMessage.cookFunction = (raw) => {
+ return simpleCategoryHashMentionTransform(
+ cookFunction(raw),
+ site.categories
+ );
+ };
+
+ this.cooked = ChatMessage.cookFunction(this.message);
+ this.incrementVersion();
+ });
+ }
+ }
+
get threadRouteModels() {
return [...this.channel.routeModels, this.threadId];
}
@@ -134,6 +177,41 @@ export default class ChatMessage {
this.version++;
}
+ toJSONDraft() {
+ if (
+ this.message?.length === 0 &&
+ this.uploads?.length === 0 &&
+ !this.inReplyTo
+ ) {
+ return null;
+ }
+
+ const data = {};
+
+ if (this.uploads?.length > 0) {
+ data.uploads = this.uploads;
+ }
+
+ if (this.message?.length > 0) {
+ data.message = this.message;
+ }
+
+ if (this.inReplyTo) {
+ data.replyToMsg = {
+ id: this.inReplyTo.id,
+ excerpt: this.inReplyTo.excerpt,
+ user: {
+ id: this.inReplyTo.user.id,
+ name: this.inReplyTo.user.name,
+ avatar_template: this.inReplyTo.user.avatar_template,
+ username: this.inReplyTo.user.username,
+ },
+ };
+ }
+
+ return JSON.stringify(data);
+ }
+
react(emoji, action, actor, currentUserId) {
const selfReaction = actor.id === currentUserId;
const existingReaction = this.reactions.find(
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
index 1ee2c07f79b..e44b4406780 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
@@ -3,6 +3,7 @@ import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messa
import User from "discourse/models/user";
import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking";
+import guid from "pretty-text/guid";
export const THREAD_STATUSES = {
open: "open",
@@ -28,6 +29,16 @@ export default class ChatThread {
this.originalMessage.user = this.originalMessageUser;
}
+ stageMessage(message) {
+ message.id = guid();
+ message.staged = true;
+ message.draft = false;
+ message.createdAt ??= moment.utc().format();
+ message.cook();
+
+ this.messagesManager.addMessages([message]);
+ }
+
get messages() {
return this.messagesManager.messages;
}
diff --git a/plugins/chat/assets/javascripts/discourse/modifiers/chat/resizable-node.js b/plugins/chat/assets/javascripts/discourse/modifiers/chat/resizable-node.js
index 3ac6d835601..1ab09e0e100 100644
--- a/plugins/chat/assets/javascripts/discourse/modifiers/chat/resizable-node.js
+++ b/plugins/chat/assets/javascripts/discourse/modifiers/chat/resizable-node.js
@@ -9,6 +9,7 @@ export default class ResizableNode extends Modifier {
element = null;
resizerSelector = null;
didResizeContainer = null;
+ options = null;
_originalWidth = 0;
_originalHeight = 0;
@@ -22,10 +23,14 @@ export default class ResizableNode extends Modifier {
registerDestructor(this, (instance) => instance.cleanup());
}
- modify(element, [resizerSelector, didResizeContainer]) {
+ modify(element, [resizerSelector, didResizeContainer, options = {}]) {
this.resizerSelector = resizerSelector;
this.element = element;
this.didResizeContainer = didResizeContainer;
+ this.options = Object.assign(
+ { vertical: true, horizontal: true, position: true, mutate: true },
+ options
+ );
this.element
.querySelector(this.resizerSelector)
@@ -97,21 +102,29 @@ export default class ResizableNode extends Modifier {
const newStyle = {};
- if (width > MINIMUM_SIZE) {
+ if (this.options.horizontal && width > MINIMUM_SIZE) {
newStyle.width = width + "px";
- newStyle.left =
- Math.ceil(this._originalX + (event.pageX - this._originalMouseX)) +
- "px";
+
+ if (this.options.position) {
+ newStyle.left =
+ Math.ceil(this._originalX + (event.pageX - this._originalMouseX)) +
+ "px";
+ }
}
- if (height > MINIMUM_SIZE) {
+ if (this.options.vertical && height > MINIMUM_SIZE) {
newStyle.height = height + "px";
- newStyle.top =
- Math.ceil(this._originalY + (event.pageY - this._originalMouseY)) +
- "px";
+
+ if (this.options.position) {
+ newStyle.top =
+ Math.ceil(this._originalY + (event.pageY - this._originalMouseY)) +
+ "px";
+ }
}
- Object.assign(this.element.style, newStyle);
+ if (this.options.mutate) {
+ Object.assign(this.element.style, newStyle);
+ }
this.didResizeContainer?.(this.element, { width, height });
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index 7be02b5ed3c..3c29d8041f3 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -299,7 +299,7 @@ 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.
+ * @param {object} data - The draft data, see ChatMessage.toJSONDraft() for more details.
* @returns {Promise}
*/
saveDraft(channelId, data) {
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
index e06ed71af0b..6afa1a96a76 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-composer.js
@@ -1,132 +1,60 @@
-import { debounce } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
import Service, { inject as service } from "@ember/service";
+import { action } from "@ember/object";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
export default class ChatChannelComposer extends Service {
@service chat;
@service chatApi;
@service chatComposerPresenceManager;
+ @service currentUser;
- @tracked editingMessage = null;
- @tracked replyToMsg = null;
- @tracked linkedComponent = null;
+ @tracked _message;
- 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;
+ @action
+ cancel() {
+ if (this.message.editing) {
+ this.reset();
+ } else if (this.message.inReplyTo) {
+ this.message.inReplyTo = 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();
+ @action
+ reset(channel) {
+ this.message = ChatMessage.createDraftMessage(channel, {
+ user: this.currentUser,
+ });
}
- onComposerValueChange({
- value,
- uploads,
- replyToMsg,
- inProgressUploadsCount,
- }) {
- if (!this.model) {
- return;
- }
-
- if (!this.editingMessage && !this.model.isDraft) {
- if (typeof value !== "undefined" && this.model.draft) {
- 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
- ) {
- this.model.draft.uploads = uploads;
- }
-
- if (typeof replyToMsg !== "undefined" && this.model.draft) {
- this.model.draft.replyToMsg = replyToMsg;
- }
- }
-
- if (!this.model.isDraft) {
- this.#reportReplyingPresence(value);
- }
-
- this._persistDraft();
+ @action
+ clear() {
+ this.message.message = "";
}
- cancelEditing() {
- this.editingMessage = null;
+ @action
+ editMessage(message) {
+ this.chat.activeMessage = null;
+ message.editing = true;
+ this.message = message;
}
- registerFocusHandler(handlerFn) {
- this.focusHandler = handlerFn;
+ @action
+ onCancelEditing() {
+ this.reset();
}
- focusComposer() {
- this.focusHandler();
+ @action
+ replyTo(message) {
+ this.chat.activeMessage = null;
+ this.message.inReplyTo = message;
}
- #reportReplyingPresence(composerValue) {
- if (this.#componentDeleted) {
- return;
- }
-
- if (this.model.isDraft) {
- return;
- }
-
- const replying = !this.editingMessage && !!composerValue;
- this.chatComposerPresenceManager.notifyState(this.model.id, replying);
+ get message() {
+ return this._message;
}
- @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;
+ set message(message) {
+ this._message = message;
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane.js
index 91b118dbcff..86b29e08290 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-pane.js
@@ -1,6 +1,5 @@
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 {
@@ -13,7 +12,7 @@ export default class ChatChannelPane extends Service {
@tracked reacting = false;
@tracked selectingMessages = false;
@tracked lastSelectedMessage = null;
- @tracked sendingLoading = false;
+ @tracked sending = false;
get selectedMessageIds() {
return this.chat.activeChannel?.selectedMessages?.mapBy("id") || [];
@@ -23,6 +22,10 @@ export default class ChatChannelPane extends Service {
return this.chatChannelComposer;
}
+ get channel() {
+ return this.chat.activeChannel;
+ }
+
@action
cancelSelecting(selectedMessages) {
this.selectingMessages = false;
@@ -37,55 +40,19 @@ export default class ChatChannelPane extends Service {
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(
+ get lastCurrentUserMessage() {
+ const lastCurrentUserMessage = this.chat.activeChannel.messages.findLast(
(message) => message.user.id === this.currentUser.id
);
- if (!lastUserMessage) {
+ if (!lastCurrentUserMessage) {
return;
}
- if (lastUserMessage.staged || lastUserMessage.error) {
+ if (lastCurrentUserMessage.staged || lastCurrentUserMessage.error) {
return;
}
- this.composerService.editingMessage = lastUserMessage;
- this.composerService.focusComposer();
+ return lastCurrentUserMessage;
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-composer.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-composer.js
index bd691b7ce44..81749d4e725 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-composer.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-composer.js
@@ -1,15 +1,13 @@
import ChatChannelComposer from "./chat-channel-composer";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import { action } from "@ember/object";
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;
+ @action
+ reset(channel) {
+ this.message = ChatMessage.createDraftMessage(channel, {
+ user: this.currentUser,
+ thread_id: channel.activeThread.id,
+ });
}
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-pane.js b/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-pane.js
index 839f690fb85..dd39dc6dffc 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-pane.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-channel-thread-pane.js
@@ -11,4 +11,21 @@ export default class ChatChannelThreadPane extends ChatChannelPane {
get composerService() {
return this.chatChannelThreadComposer;
}
+
+ get lastCurrentUserMessage() {
+ const lastCurrentUserMessage =
+ this.chat.activeChannel.activeThread.messages.findLast(
+ (message) => message.user.id === this.currentUser.id
+ );
+
+ if (!lastCurrentUserMessage) {
+ return;
+ }
+
+ if (lastCurrentUserMessage.staged || lastCurrentUserMessage.error) {
+ return;
+ }
+
+ return lastCurrentUserMessage;
+ }
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-side-panel-size.js b/plugins/chat/assets/javascripts/discourse/services/chat-side-panel-size.js
new file mode 100644
index 00000000000..e09e354fc67
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-side-panel-size.js
@@ -0,0 +1,24 @@
+import Service from "@ember/service";
+import KeyValueStore from "discourse/lib/key-value-store";
+
+export default class ChatSidePanelSize extends Service {
+ STORE_NAMESPACE = "discourse_chat_side_panel_size_";
+ MIN_WIDTH = 250;
+
+ store = new KeyValueStore(this.STORE_NAMESPACE);
+
+ get width() {
+ return this.store.getObject("width") || this.MIN_WIDTH;
+ }
+
+ set width(width) {
+ this.store.setObject({
+ key: "width",
+ value: this.#min(width, this.MIN_WIDTH),
+ });
+ }
+
+ #min(number, min) {
+ return Math.max(number, min);
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat.js b/plugins/chat/assets/javascripts/discourse/services/chat.js
index c5e58e52c44..169a2bbdadd 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat.js
@@ -8,7 +8,7 @@ import { cancel, next } from "@ember/runloop";
import { and } from "@ember/object/computed";
import { computed } from "@ember/object";
import discourseLater from "discourse-common/lib/later";
-import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
const CHAT_ONLINE_OPTIONS = {
userUnseenTime: 300000, // 5 minutes seconds with no interaction
@@ -126,8 +126,15 @@ export default class Chat extends Service {
const storedDraft = this.currentUser.chat_drafts.find(
(draft) => draft.channel_id === channel.id
);
- channel.draft = ChatMessageDraft.create(
- storedDraft ? JSON.parse(storedDraft.data) : null
+
+ channel.draft = ChatMessage.createDraftMessage(
+ channel,
+ Object.assign(
+ {
+ user: this.currentUser,
+ },
+ storedDraft ? JSON.parse(storedDraft.data) : {}
+ )
);
}
diff --git a/plugins/chat/assets/stylesheets/common/base-common.scss b/plugins/chat/assets/stylesheets/common/base-common.scss
index af91d41e948..3ec626ce2a3 100644
--- a/plugins/chat/assets/stylesheets/common/base-common.scss
+++ b/plugins/chat/assets/stylesheets/common/base-common.scss
@@ -145,13 +145,9 @@ $float-height: 530px;
word-wrap: break-word;
white-space: normal;
position: relative;
- will-change: transform;
- transform: translateZ(0);
.chat-message-container {
display: grid;
- will-change: transform;
- transform: translateZ(0);
&.selecting-messages {
grid-template-columns: 1.5em 1fr;
@@ -256,7 +252,7 @@ $float-height: 530px;
}
}
-.chat-live-pane {
+.chat-channel {
display: flex;
flex-direction: column;
width: 100%;
@@ -432,9 +428,8 @@ body.has-full-page-chat {
}
}
- .chat-live-pane,
.chat-messages-scroll,
- .chat-live-pane {
+ .chat-channel {
box-sizing: border-box;
height: 100%;
}
@@ -629,7 +624,7 @@ html.has-full-page-chat {
}
.full-page-chat,
- .chat-live-pane,
+ .chat-channel,
#main-outlet {
// allows containers to shrink to fit
min-height: 0;
diff --git a/plugins/chat/assets/stylesheets/common/chat-channel.scss b/plugins/chat/assets/stylesheets/common/chat-channel.scss
new file mode 100644
index 00000000000..3db4cb4eabb
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-channel.scss
@@ -0,0 +1,127 @@
+.chat-channel {
+ display: flex;
+ flex-direction: column;
+ min-height: 1px;
+ position: relative;
+ overflow: hidden;
+ grid-area: main;
+ width: 100%;
+ min-width: 300px;
+
+ .open-drawer-btn {
+ color: var(--primary-low-mid);
+
+ &:visited {
+ color: var(--primary-low-mid);
+ }
+
+ &:hover {
+ color: var(--primary);
+ }
+
+ > * {
+ pointer-events: none;
+ }
+ }
+
+ .chat-messages-scroll {
+ flex-grow: 1;
+ overflow-y: scroll;
+ overscroll-behavior: contain;
+ display: flex;
+ flex-direction: column-reverse;
+ z-index: 1;
+ margin: 0 1px 0 0;
+ will-change: transform;
+ @include chat-scrollbar();
+
+ .join-channel-btn.in-float {
+ position: absolute;
+ transform: translateX(-50%);
+ left: 50%;
+ top: 10px;
+ z-index: 10;
+ }
+
+ .all-loaded-message {
+ text-align: center;
+ color: var(--primary-medium);
+ font-size: var(--font-down-1);
+ padding: 0.5em 0.25em 0.25em;
+ }
+ }
+
+ .scroll-stick-wrap {
+ display: flex;
+ justify-content: center;
+ margin: 0 1rem;
+ position: relative;
+ }
+
+ .chat-scroll-to-bottom {
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ z-index: 1;
+ flex-direction: column;
+ bottom: -25px;
+ background: none;
+ opacity: 0;
+ transition: opacity 0.25s ease, transform 0.5s ease;
+ transform: scale(0.1);
+ padding: 0;
+
+ > * {
+ pointer-events: none;
+ }
+
+ &:hover,
+ &:active,
+ &:focus {
+ background: none !important;
+ }
+
+ &.visible {
+ transform: translateY(-32px) scale(1);
+ opacity: 0.8;
+ }
+
+ &__text {
+ color: var(--secondary);
+ padding: 0.5rem;
+ margin-bottom: 0.5rem;
+ background: var(--primary-medium);
+ border-radius: 3px;
+ text-align: center;
+ font-size: var(--font-down-1);
+ bottom: 40px;
+ position: absolute;
+ }
+
+ &__arrow {
+ display: flex;
+ background: var(--primary-medium);
+ border-radius: 100%;
+ align-items: center;
+ justify-content: center;
+ height: 32px;
+ width: 32px;
+ position: relative;
+
+ .d-icon {
+ color: var(--secondary);
+ margin-left: 1px; // "fixes" the 1px svg shift
+ }
+ }
+
+ &:hover {
+ opacity: 1;
+
+ .chat-scroll-to-bottom__arrow {
+ .d-icon {
+ color: var(--secondary);
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-composer-upload.scss b/plugins/chat/assets/stylesheets/common/chat-composer-upload.scss
index 5d4b36303d2..0b1ef875bb2 100644
--- a/plugins/chat/assets/stylesheets/common/chat-composer-upload.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-composer-upload.scss
@@ -1,30 +1,62 @@
.chat-composer-upload {
display: inline-flex;
- height: 50px;
+ height: 64px;
padding: 0.5rem;
border: 1px solid var(--primary-low-mid);
margin-right: 0.5em;
+ position: relative;
+ border-radius: 5px;
+ box-sizing: border-box;
+
+ &--image:not(.chat-composer-upload--in-progress) {
+ padding: 0;
+
+ .preview-img {
+ height: 62px;
+ width: 62px;
+ box-sizing: border-box;
+ }
+ }
&:last-child {
margin-right: 0;
}
+ &:hover {
+ .chat-composer-upload__remove-btn {
+ visibility: visible;
+ background: rgba(var(--always-black-rgb), 0.9);
+ padding: 5px;
+ border-radius: 100%;
+ font-size: var(--font-down-2);
+ }
+ }
+
+ &__remove-btn {
+ border: 1px solid var(--primary-medium);
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ visibility: hidden;
+ }
+
.preview {
- width: 50px;
+ width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
- margin: 0 1em 0 0;
- border-radius: 8px;
+ margin: 0;
.d-icon {
font-size: var(--font-up-6);
+ margin-right: 0.5rem;
}
.preview-img {
- max-width: 100%;
- max-height: 100%;
+ object-position: center;
+ object-fit: cover;
+ border-radius: 5px;
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-composer-uploads.scss b/plugins/chat/assets/stylesheets/common/chat-composer-uploads.scss
index bdae3a83f08..81693b18010 100644
--- a/plugins/chat/assets/stylesheets/common/chat-composer-uploads.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-composer-uploads.scss
@@ -2,7 +2,7 @@
max-width: 100%;
.chat-composer-uploads-container {
- padding: 0.5rem 10px;
+ padding: 0.5rem 0.25rem;
display: flex;
white-space: nowrap;
overflow-x: auto;
diff --git a/plugins/chat/assets/stylesheets/common/chat-composer.scss b/plugins/chat/assets/stylesheets/common/chat-composer.scss
index 2077128b215..8a020f1fe35 100644
--- a/plugins/chat/assets/stylesheets/common/chat-composer.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-composer.scss
@@ -1,60 +1,104 @@
-.chat-composer-container {
- display: flex;
- flex-direction: column;
- z-index: 3;
- background-color: var(--secondary);
-
- #chat-full-page-uploader,
- #chat-widget-uploader {
- display: none;
- }
-
- .drop-a-file {
- display: none;
- }
-}
-
.chat-composer {
display: flex;
align-items: center;
- background-color: var(--secondary);
- border: 1px solid var(--primary-low-mid);
- border-radius: 5px;
- padding: 0.15rem 0.25rem;
- margin-top: 0.5rem;
- &.is-disabled {
- background-color: var(--primary-low);
- border: 1px solid var(--primary-low-mid);
+ &__wrapper {
+ display: flex;
+ flex-direction: column;
+ z-index: 3;
+ background-color: var(--secondary);
+ margin-top: 0.1rem;
+
+ #chat-full-page-uploader,
+ #chat-widget-uploader {
+ display: none;
+ }
+
+ .drop-a-file {
+ display: none;
+ }
}
- .send-btn {
- padding: 0.4rem 0.5rem;
- border: 1px solid transparent;
- border-radius: 5px;
+ &__outer-container {
display: flex;
align-items: center;
+ padding-inline: 0.25rem;
+ box-sizing: border-box;
+ width: 100%;
+ }
- .d-icon {
- color: var(--tertiary);
+ &__inner-container {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ width: 100%;
+ flex-direction: row;
+ border: 1px solid var(--primary-low-mid);
+ border-radius: 5px;
+ background-color: var(--secondary);
+ padding-inline: 0.25rem;
+ height: 42px;
+
+ .chat-composer--focused & {
+ border-color: var(--primary-medium);
}
- &:disabled {
- cursor: not-allowed;
+ .chat-composer--disabled & {
+ background: var(--primary-low);
+ }
+ }
+
+ &__send-btn {
+ border-radius: 3px;
+ background: none;
+ will-change: scale;
+
+ .chat-composer--send-enabled & {
+ &:hover {
+ background: none;
+ }
+
+ &:focus {
+ background: none;
+ outline: auto;
+ }
.d-icon {
- color: var(--primary-low);
+ color: var(--tertiary) !important;
}
}
- &:not(:disabled) {
- &:hover,
- &:focus {
- background: var(--tertiary);
- .d-icon {
- color: var(--secondary);
- }
+ @keyframes sendingScales {
+ 0% {
+ transform: scale(0.8);
}
+ 50% {
+ transform: scale(1.2);
+ }
+ 100% {
+ transform: scale(0.8);
+ }
+ }
+
+ .chat-composer--sending & {
+ animation: sendingScales 1s infinite;
+ }
+
+ .chat-composer--send-disabled & {
+ cursor: default;
+ opacity: 0.6 !important;
+
+ &:hover {
+ background: none !important;
+ }
+ }
+
+ > * {
+ pointer-events: none;
+ }
+
+ .d-icon {
+ color: var(--primary) !important;
}
}
@@ -82,7 +126,14 @@
}
}
- .chat-composer-input {
+ &__input-container {
+ display: flex;
+ align-items: center;
+ box-sizing: border-box;
+ width: 100%;
+ }
+
+ &__input {
overflow-x: hidden;
width: 100%;
appearance: none;
@@ -97,14 +148,21 @@
@include chat-scrollbar();
+ &[disabled] {
+ background: none;
+ }
+
+ &:focus,
+ &:active {
+ outline: none;
+ }
+
&:placeholder-shown,
&::placeholder {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
-
- @include chat-scrollbar();
}
&__unreliable-network {
diff --git a/plugins/chat/assets/stylesheets/common/chat-draft-channel.scss b/plugins/chat/assets/stylesheets/common/chat-draft-channel.scss
index cc2510d9bea..6f5e4d1f9e9 100644
--- a/plugins/chat/assets/stylesheets/common/chat-draft-channel.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-draft-channel.scss
@@ -37,7 +37,7 @@
}
}
- .chat-composer-container {
+ .chat-composer__wrapper {
padding-bottom: 0.5em;
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-drawer.scss b/plugins/chat/assets/stylesheets/common/chat-drawer.scss
index 296491de2c9..be3f22e40e4 100644
--- a/plugins/chat/assets/stylesheets/common/chat-drawer.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-drawer.scss
@@ -94,7 +94,7 @@ html.rtl {
height: auto !important;
}
- .chat-live-pane {
+ .chat-channel {
height: 100%;
}
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-side-panel-resizer.scss b/plugins/chat/assets/stylesheets/common/chat-side-panel-resizer.scss
new file mode 100644
index 00000000000..291d9e53c1b
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-side-panel-resizer.scss
@@ -0,0 +1,16 @@
+.chat-side-panel-resizer {
+ top: 0;
+ bottom: 0;
+ left: -3px;
+ width: 5px;
+ position: absolute;
+ z-index: z("max");
+ transition: background-color 0.15s 0.15s;
+ background-color: transparent;
+
+ &:hover,
+ &:active {
+ cursor: col-resize;
+ background: var(--tertiary);
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/chat-side-panel.scss b/plugins/chat/assets/stylesheets/common/chat-side-panel.scss
index 09e2e22c3e0..f3139712ab6 100644
--- a/plugins/chat/assets/stylesheets/common/chat-side-panel.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-side-panel.scss
@@ -1,13 +1,9 @@
#main-chat-outlet.chat-view {
min-height: 0;
display: grid;
- grid-template-rows: 1fr;
+ grid-template-rows: 100%;
grid-template-areas: "main threads";
- grid-template-columns: 1fr;
-
- &.has-side-panel-expanded {
- grid-template-columns: 3fr 2fr;
- }
+ grid-template-columns: 1fr auto;
}
.chat-side-panel {
@@ -15,6 +11,8 @@
min-height: 100%;
box-sizing: border-box;
border-left: 1px solid var(--primary-low);
+ position: relative;
+ min-width: 250px;
&__list {
flex-grow: 1;
diff --git a/plugins/chat/assets/stylesheets/common/chat-thread.scss b/plugins/chat/assets/stylesheets/common/chat-thread.scss
index 4204eeead4b..d124fbd8ad4 100644
--- a/plugins/chat/assets/stylesheets/common/chat-thread.scss
+++ b/plugins/chat/assets/stylesheets/common/chat-thread.scss
@@ -2,6 +2,7 @@
display: flex;
flex-direction: column;
height: 100%;
+ position: relative;
&__header {
height: var(--chat-header-offset);
@@ -27,4 +28,8 @@
flex-direction: column-reverse;
will-change: transform;
}
+
+ .chat-composer {
+ padding-bottom: 28px;
+ }
}
diff --git a/plugins/chat/assets/stylesheets/common/chat-upload-drop-zone.scss b/plugins/chat/assets/stylesheets/common/chat-upload-drop-zone.scss
new file mode 100644
index 00000000000..b313056f3aa
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/common/chat-upload-drop-zone.scss
@@ -0,0 +1,77 @@
+.chat-upload-drop-zone {
+ position: absolute;
+ visibility: hidden;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ z-index: z("max");
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ background: rgba(var(--always-black-rgb), 0.85);
+
+ .uppy-is-drag-over & {
+ visibility: visible;
+ }
+
+ &__content {
+ position: relative;
+ width: 50%;
+ height: 50%;
+ }
+
+ &__background {
+ svg {
+ transform: scale(0.1);
+ transition: transform 200ms ease-in-out;
+ height: 80px;
+
+ .uppy-is-drag-over & {
+ transform: scale(1);
+ }
+ }
+
+ position: absolute;
+ top: 0;
+ left: calc(50% - 100px / 2);
+ z-index: 1;
+ }
+
+ &__illustration {
+ svg {
+ transform: scale(0.1);
+ transition: transform 200ms ease-in-out;
+ height: 80px;
+
+ .uppy-is-drag-over & {
+ transform: scale(1);
+ }
+ }
+
+ position: absolute;
+ top: 0;
+ left: calc(50% - 100px / 2);
+ z-index: 1;
+ }
+
+ &__text {
+ position: absolute;
+ top: 100px;
+ left: 0;
+ right: 0;
+ width: 100%;
+ z-index: 1;
+ display: flex;
+ justify-content: center;
+
+ &__title {
+ width: 100%;
+ font-weight: 600;
+ text-align: center;
+ font-size: var(--font-up-2);
+ padding-inline: 1rem;
+ color: var(--secondary-or-primary);
+ }
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/common/index.scss b/plugins/chat/assets/stylesheets/common/index.scss
index 6f5cad90861..713e5ef5460 100644
--- a/plugins/chat/assets/stylesheets/common/index.scss
+++ b/plugins/chat/assets/stylesheets/common/index.scss
@@ -1,6 +1,7 @@
@import "base-common";
@import "sidebar-extensions";
@import "chat-browse";
+@import "chat-channel";
@import "chat-channel-card";
@import "chat-channel-info";
@import "chat-channel-preview-card";
@@ -35,6 +36,8 @@
@import "chat-skeleton";
@import "chat-tabs";
@import "chat-thread";
+@import "chat-side-panel-resizer";
+@import "chat-upload-drop-zone";
@import "chat-transcript";
@import "core-extensions";
@import "create-channel-modal";
diff --git a/plugins/chat/assets/stylesheets/desktop/base-desktop.scss b/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
index 3eb8d02902b..121e85ab6e5 100644
--- a/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
+++ b/plugins/chat/assets/stylesheets/desktop/base-desktop.scss
@@ -7,7 +7,7 @@
&.teams-sidebar-on {
grid-template-columns: 1fr;
- .chat-live-pane {
+ .chat-channel {
border-radius: var(--full-page-border-radius);
}
}
@@ -19,7 +19,7 @@
flex-shrink: 0;
}
- .chat-live-pane {
+ .chat-channel {
.chat-messages-container {
.chat-message {
&.is-reply {
@@ -87,7 +87,7 @@
border-right: 1px solid var(--primary-low);
border-left: 1px solid var(--primary-low);
- .chat-live-pane {
+ .chat-channel {
border-radius: unset;
}
}
@@ -112,7 +112,7 @@
}
.full-page-chat.teams-sidebar-on {
- .chat-live-pane {
+ .chat-channel {
border-radius: 0;
}
diff --git a/plugins/chat/assets/stylesheets/mobile/base-mobile.scss b/plugins/chat/assets/stylesheets/mobile/base-mobile.scss
index 107c8629dcc..d44ae726388 100644
--- a/plugins/chat/assets/stylesheets/mobile/base-mobile.scss
+++ b/plugins/chat/assets/stylesheets/mobile/base-mobile.scss
@@ -15,7 +15,7 @@ html.has-full-page-chat {
padding: 0;
.main-chat-outlet {
- .chat-live-pane {
+ .chat-channel {
min-width: 0;
}
@@ -23,7 +23,7 @@ html.has-full-page-chat {
grid-template-columns: 1fr;
grid-template-areas: "threads";
- .chat-live-pane {
+ .chat-channel {
display: none;
}
}
@@ -45,7 +45,7 @@ html.has-full-page-chat {
}
}
- .chat-live-pane {
+ .chat-channel {
border-radius: 0;
padding: 0;
}
diff --git a/plugins/chat/assets/stylesheets/mobile/chat-composer-upload.scss b/plugins/chat/assets/stylesheets/mobile/chat-composer-upload.scss
new file mode 100644
index 00000000000..b99e9877373
--- /dev/null
+++ b/plugins/chat/assets/stylesheets/mobile/chat-composer-upload.scss
@@ -0,0 +1,11 @@
+.chat-composer-upload {
+ &__remove-btn {
+ visibility: visible;
+ background: rgba(var(--always-black-rgb), 0.9);
+ border-radius: 100%;
+
+ // overwrite ios style
+ font-size: var(--font-down-2) !important;
+ padding: 5px !important;
+ }
+}
diff --git a/plugins/chat/assets/stylesheets/mobile/chat-message.scss b/plugins/chat/assets/stylesheets/mobile/chat-message.scss
index c6f772e9b60..84874803e56 100644
--- a/plugins/chat/assets/stylesheets/mobile/chat-message.scss
+++ b/plugins/chat/assets/stylesheets/mobile/chat-message.scss
@@ -2,7 +2,7 @@
#skip-link,
.d-header,
.chat-message-actions-mobile-outlet,
- .chat-live-pane,
+ .chat-channel,
.chat-thread {
> * {
@include user-select(none);
diff --git a/plugins/chat/assets/stylesheets/mobile/index.scss b/plugins/chat/assets/stylesheets/mobile/index.scss
index 1fa0be669d6..90fce33e9d0 100644
--- a/plugins/chat/assets/stylesheets/mobile/index.scss
+++ b/plugins/chat/assets/stylesheets/mobile/index.scss
@@ -6,3 +6,4 @@
@import "chat-message";
@import "chat-selection-manager";
@import "chat-emoji-picker";
+@import "chat-composer-upload";
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
index 4ccb13f1199..3261b7f2ac2 100644
--- a/plugins/chat/config/locales/client.en.yml
+++ b/plugins/chat/config/locales/client.en.yml
@@ -221,7 +221,8 @@ en:
close_full_page: "Close full-screen chat"
open_message: "Open message in chat"
placeholder_self: "Jot something down"
- placeholder_channel: "Chat with %{channelName}"
+ placeholder_channel: "Chat in %{channelName}"
+ placeholder_thread: "Chat in thread"
placeholder_users: "Chat with %{commaSeparatedNames}"
placeholder_new_message_disallowed:
archived: "Channel is archived, you cannot send new messages right now."
@@ -257,6 +258,8 @@ en:
title: "chat"
title_capitalized: "Chat"
upload: "Attach a file"
+ upload_to_channel: "Upload to %{title}"
+ upload_to_thread: "Upload to thread"
uploaded_files:
one: "%{count} file"
other: "%{count} files"
@@ -399,6 +402,7 @@ en:
italic_text: "emphasized text"
bold_text: "strong text"
code_text: "code text"
+ send: "Send"
quote:
original_channel: 'Originally sent in
%{channel}'
@@ -539,6 +543,7 @@ en:
one: "%{count} reply"
other: "%{count} replies"
label: Thread
+ close: "Close Thread"
threads:
started_by: "Started by"
open: "Open Thread"
diff --git a/plugins/chat/spec/system/chat_composer_spec.rb b/plugins/chat/spec/system/chat_composer_spec.rb
index fde6408d1a0..22d59dd6e55 100644
--- a/plugins/chat/spec/system/chat_composer_spec.rb
+++ b/plugins/chat/spec/system/chat_composer_spec.rb
@@ -10,10 +10,83 @@ RSpec.describe "Chat composer", type: :system, js: true do
before { chat_system_bootstrap }
- xit "it stores draft in replies" do
- end
+ context "when loading a channel with a draft" do
+ fab!(:draft_1) do
+ Chat::Draft.create!(
+ chat_channel: channel_1,
+ user: current_user,
+ data: { message: "draft" }.to_json,
+ )
+ end
- xit "it stores draft" do
+ before do
+ channel_1.add(current_user)
+ sign_in(current_user)
+ end
+
+ it "loads the draft" do
+ chat.visit_channel(channel_1)
+
+ expect(find(".chat-composer__input").value).to eq("draft")
+ end
+
+ context "with uploads" do
+ fab!(:upload_1) do
+ Fabricate(
+ :upload,
+ url: "/images/logo-dark.png",
+ original_filename: "logo_dark.png",
+ width: 400,
+ height: 300,
+ extension: "png",
+ )
+ end
+
+ fab!(:draft_1) do
+ Chat::Draft.create!(
+ chat_channel: channel_1,
+ user: current_user,
+ data: { message: "draft", uploads: [upload_1] }.to_json,
+ )
+ end
+
+ it "loads the draft with the upload" do
+ chat.visit_channel(channel_1)
+
+ expect(find(".chat-composer__input").value).to eq("draft")
+ expect(page).to have_selector(".chat-composer-upload--image", count: 1)
+ end
+ end
+
+ context "when replying" do
+ fab!(:draft_1) do
+ Chat::Draft.create!(
+ chat_channel: channel_1,
+ user: current_user,
+ data: {
+ message: "draft",
+ replyToMsg: {
+ id: message_1.id,
+ excerpt: message_1.excerpt,
+ user: {
+ id: message_1.user.id,
+ name: nil,
+ avatar_template: message_1.user.avatar_template,
+ username: message_1.user.username,
+ },
+ },
+ }.to_json,
+ )
+ end
+
+ it "loads the draft with replied to mesage" do
+ chat.visit_channel(channel_1)
+
+ expect(find(".chat-composer__input").value).to eq("draft")
+ expect(page).to have_selector(".chat-reply__username", text: message_1.user.username)
+ expect(page).to have_selector(".chat-reply__excerpt", text: message_1.excerpt)
+ end
+ end
end
context "when replying to a message" do
@@ -62,17 +135,17 @@ RSpec.describe "Chat composer", type: :system, js: true do
".chat-composer-message-details .chat-reply__username",
text: current_user.username,
)
- expect(find(".chat-composer-input").value).to eq(message_2.message)
+ expect(find(".chat-composer__input").value).to eq(message_2.message)
end
context "when pressing escape" do
it "cancels editing" do
chat.visit_channel(channel_1)
channel.edit_message(message_2)
- find(".chat-composer-input").send_keys(:escape)
+ find(".chat-composer__input").send_keys(:escape)
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
- expect(find(".chat-composer-input").value).to eq("")
+ expect(find(".chat-composer__input").value).to eq("")
end
end
@@ -83,7 +156,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
find(".cancel-message-action").click
expect(page).to have_no_selector(".chat-composer-message-details .chat-reply__username")
- expect(find(".chat-composer-input").value).to eq("")
+ expect(find(".chat-composer__input").value).to eq("")
end
end
end
@@ -100,7 +173,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
channel.click_action_button("emoji")
find("[data-emoji='grimacing']").click(wait: 0.5)
- expect(find(".chat-composer-input").value).to eq(":grimacing:")
+ expect(find(".chat-composer__input").value).to eq(":grimacing:")
end
it "removes denied emojis from insert emoji picker" do
@@ -123,20 +196,20 @@ RSpec.describe "Chat composer", type: :system, js: true do
it "adds the emoji to the composer" do
chat.visit_channel(channel_1)
- find(".chat-composer-input").fill_in(with: ":gri")
+ find(".chat-composer__input").fill_in(with: ":gri")
find(".emoji-shortname", text: "grimacing").click
- expect(find(".chat-composer-input").value).to eq(":grimacing: ")
+ expect(find(".chat-composer__input").value).to eq(":grimacing: ")
end
it "doesn't suggest denied emojis and aliases" do
SiteSetting.emoji_deny_list = "peach|poop"
chat.visit_channel(channel_1)
- find(".chat-composer-input").fill_in(with: ":peac")
+ find(".chat-composer__input").fill_in(with: ":peac")
expect(page).to have_no_selector(".emoji-shortname", text: "peach")
- find(".chat-composer-input").fill_in(with: ":hank") # alias
+ find(".chat-composer__input").fill_in(with: ":hank") # alias
expect(page).to have_no_selector(".emoji-shortname", text: "poop")
end
end
@@ -149,7 +222,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
xit "prefills the emoji picker filter input" do
chat.visit_channel(channel_1)
- find(".chat-composer-input").fill_in(with: ":gri")
+ find(".chat-composer__input").fill_in(with: ":gri")
click_link(I18n.t("js.composer.more_emoji"))
@@ -158,7 +231,7 @@ RSpec.describe "Chat composer", type: :system, js: true do
it "filters with the prefilled input" do
chat.visit_channel(channel_1)
- find(".chat-composer-input").fill_in(with: ":fr")
+ find(".chat-composer__input").fill_in(with: ":fr")
click_link(I18n.t("js.composer.more_emoji"))
@@ -178,15 +251,15 @@ RSpec.describe "Chat composer", type: :system, js: true do
find("body").send_keys("b")
- expect(find(".chat-composer-input").value).to eq("b")
+ expect(find(".chat-composer__input").value).to eq("b")
find("body").send_keys("b")
- expect(find(".chat-composer-input").value).to eq("bb")
+ expect(find(".chat-composer__input").value).to eq("bb")
find("body").send_keys(:enter) # special case
- expect(find(".chat-composer-input").value).to eq("bb")
+ expect(find(".chat-composer__input").value).to eq("bb")
end
end
end
diff --git a/plugins/chat/spec/system/chat_message/channel_spec.rb b/plugins/chat/spec/system/chat_message/channel_spec.rb
index 39d9556bd06..a91c6b94511 100644
--- a/plugins/chat/spec/system/chat_message/channel_spec.rb
+++ b/plugins/chat/spec/system/chat_message/channel_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe "Chat message", type: :system, js: true do
channel.hover_message(message_1)
expect(page).to have_css(
- ".chat-live-pane[data-id='#{channel_1.id}'] [data-id='#{message_1.id}'] .chat-message.is-active",
+ ".chat-channel[data-id='#{channel_1.id}'] [data-id='#{message_1.id}'] .chat-message.is-active",
)
end
end
diff --git a/plugins/chat/spec/system/chat_message/thread_spec.rb b/plugins/chat/spec/system/chat_message/thread_spec.rb
index aaa8aa1287a..187a6aef2cc 100644
--- a/plugins/chat/spec/system/chat_message/thread_spec.rb
+++ b/plugins/chat/spec/system/chat_message/thread_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe "Chat message - channel", type: :system, js: true do
let(:cdp) { PageObjects::CDP.new }
let(:chat) { PageObjects::Pages::Chat.new }
let(:channel) { PageObjects::Pages::ChatChannel.new }
+ let(:thread) { PageObjects::Pages::ChatThread.new }
let(:message_1) { thread_1.chat_messages.first }
before do
@@ -24,12 +25,13 @@ RSpec.describe "Chat message - channel", type: :system, js: true do
context "when hovering a message" do
it "adds an active class" do
+ last_message = thread_1.chat_messages.last
chat.visit_thread(thread_1)
- channel.hover_message(message_1)
+ thread.hover_message(last_message)
expect(page).to have_css(
- ".chat-thread[data-id='#{thread_1.id}'] [data-id='#{message_1.id}'] .chat-message.is-active",
+ ".chat-thread[data-id='#{thread_1.id}'] [data-id='#{last_message.id}'] .chat-message.is-active",
)
end
end
diff --git a/plugins/chat/spec/system/drawer_spec.rb b/plugins/chat/spec/system/drawer_spec.rb
index 125b1cfdc42..e85004efa27 100644
--- a/plugins/chat/spec/system/drawer_spec.rb
+++ b/plugins/chat/spec/system/drawer_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe "Drawer", type: :system, js: true do
channel_page.hover_message(message_1)
expect(page).to have_css(".chat-message-actions-container")
- find(".chat-composer-input").send_keys(:escape)
+ find(".chat-composer__input").send_keys(:escape)
expect(page).to have_no_css(".chat-message-actions-container")
end
diff --git a/plugins/chat/spec/system/message_thread_indicator_spec.rb b/plugins/chat/spec/system/message_thread_indicator_spec.rb
index 8135a1023fa..d1985046012 100644
--- a/plugins/chat/spec/system/message_thread_indicator_spec.rb
+++ b/plugins/chat/spec/system/message_thread_indicator_spec.rb
@@ -88,8 +88,15 @@ describe "Thread indicator for chat messages", type: :system, js: true do
channel_page.reply_to(message_without_thread)
channel_page.fill_composer("this is a reply to make a new thread")
channel_page.click_send_message
+
expect(channel_page).to have_thread_indicator(message_without_thread)
- new_thread = message_without_thread.reload.thread
+
+ new_thread = nil
+ try_until_success(timeout: 5) do
+ new_thread = message_without_thread.reload.thread
+ expect(new_thread).to be_present
+ end
+
expect(page).not_to have_css(channel_page.message_by_id_selector(new_thread.replies.first))
end
diff --git a/plugins/chat/spec/system/page_objects/chat/chat.rb b/plugins/chat/spec/system/page_objects/chat/chat.rb
index 0e80ff632d1..8079a10f60b 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat.rb
@@ -19,7 +19,7 @@ module PageObjects
def visit_channel(channel, mobile: false)
visit(channel.url + (mobile ? "?mobile_view=1" : ""))
- has_no_css?(".not-loaded-once")
+ has_no_css?(".chat-channel--not-loaded-once")
has_no_css?(".chat-skeleton")
end
diff --git a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
index f2e193ade82..6975f561410 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat_channel.rb
@@ -4,25 +4,25 @@ module PageObjects
module Pages
class ChatChannel < PageObjects::Pages::Base
def type_in_composer(input)
- find(".chat-composer-input--channel").click # makes helper more reliable by ensuring focus is not lost
- find(".chat-composer-input--channel").send_keys(input)
+ find(".chat-channel .chat-composer__input").click # makes helper more reliable by ensuring focus is not lost
+ find(".chat-channel .chat-composer__input").send_keys(input)
end
def fill_composer(input)
- find(".chat-composer-input--channel").click # makes helper more reliable by ensuring focus is not lost
- find(".chat-composer-input--channel").fill_in(with: input)
+ find(".chat-channel .chat-composer__input").click # makes helper more reliable by ensuring focus is not lost
+ find(".chat-channel .chat-composer__input").fill_in(with: input)
end
def click_composer
- find(".chat-composer-input--channel").click # ensures autocomplete is closed and not masking anything
+ find(".chat-channel .chat-composer__input").click # ensures autocomplete is closed and not masking anything
end
def click_send_message
- find(".chat-composer .send-btn:enabled").click
+ find(".chat-composer--send-enabled .chat-composer__send-btn").click
end
def message_by_id_selector(id)
- ".chat-live-pane .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
+ ".chat-channel .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
end
def message_by_id(id)
diff --git a/plugins/chat/spec/system/page_objects/chat/chat_thread.rb b/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
index cf857d805fb..c76e4d68945 100644
--- a/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
+++ b/plugins/chat/spec/system/page_objects/chat/chat_thread.rb
@@ -24,17 +24,17 @@ module PageObjects
end
def type_in_composer(input)
- find(".chat-composer-input--thread").click # makes helper more reliable by ensuring focus is not lost
- find(".chat-composer-input--thread").send_keys(input)
+ find(".chat-thread .chat-composer__input").click # makes helper more reliable by ensuring focus is not lost
+ find(".chat-thread .chat-composer__input").send_keys(input)
end
def fill_composer(input)
- find(".chat-composer-input--thread").click # makes helper more reliable by ensuring focus is not lost
- find(".chat-composer-input--thread").fill_in(with: input)
+ find(".chat-thread .chat-composer__input").click # makes helper more reliable by ensuring focus is not lost
+ find(".chat-thread .chat-composer__input").fill_in(with: input)
end
def click_composer
- find(".chat-composer-input--thread").click # ensures autocomplete is closed and not masking anything
+ find(".chat-thread .chat-composer__input").click # ensures autocomplete is closed and not masking anything
end
def send_message(id, text = nil)
@@ -45,7 +45,9 @@ module PageObjects
end
def click_send_message(id)
- find(thread_selector_by_id(id)).find(".chat-composer .send-btn:enabled").click
+ find(thread_selector_by_id(id)).find(
+ ".chat-composer--send-enabled .chat-composer__send-btn",
+ ).click
end
def has_message?(thread_id, text: nil, id: nil)
@@ -73,6 +75,18 @@ module PageObjects
)
end
end
+
+ def hover_message(message)
+ message_by_id(message.id).hover
+ end
+
+ def message_by_id(id)
+ find(message_by_id_selector(id))
+ end
+
+ def message_by_id_selector(id)
+ ".chat-thread .chat-messages-container .chat-message-container[data-id=\"#{id}\"]"
+ end
end
end
end
diff --git a/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb b/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
index 64f5816abbc..a6befde3676 100644
--- a/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
+++ b/plugins/chat/spec/system/page_objects/chat_drawer/chat_drawer.rb
@@ -36,7 +36,7 @@ module PageObjects
end
def has_open_channel?(channel)
- has_css?("#{VISIBLE_DRAWER} .chat-live-pane[data-id='#{channel.id}']")
+ has_css?("#{VISIBLE_DRAWER} .chat-channel[data-id='#{channel.id}']")
end
end
end
diff --git a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
index 07084cdddf4..15f3ffd003e 100644
--- a/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
+++ b/plugins/chat/spec/system/shortcuts/chat_composer_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
it "adds bold text" do
chat.visit_channel(channel_1)
- composer = find(".chat-composer-input")
+ composer = find(".chat-composer__input")
composer.send_keys([key_modifier, "b"])
expect(composer.value).to eq("**strong text**")
@@ -34,7 +34,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
it "adds italic text" do
chat.visit_channel(channel_1)
- composer = find(".chat-composer-input")
+ composer = find(".chat-composer__input")
composer.send_keys([key_modifier, "i"])
expect(composer.value).to eq("_emphasized text_")
@@ -45,7 +45,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
it "adds preformatted text" do
chat.visit_channel(channel_1)
- composer = find(".chat-composer-input")
+ composer = find(".chat-composer__input")
composer.send_keys([key_modifier, "e"])
expect(composer.value).to eq("`indent preformatted text by 4 spaces`")
@@ -71,8 +71,8 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
chat.visit_channel(channel_1)
channel_page.message_thread_indicator(thread.original_message).click
- composer = find(".chat-composer-input--channel")
- thread_composer = find(".chat-composer-input--thread")
+ composer = find(".chat-channel .chat-composer__input")
+ thread_composer = find(".chat-thread .chat-composer__input")
composer.send_keys([key_modifier, "i"])
expect(composer.value).to eq("_emphasized text_")
@@ -98,7 +98,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
chat.visit_channel(channel_1)
expect(channel_page).to have_message(id: message_1.id)
- find(".chat-composer-input").send_keys(:arrow_up)
+ find(".chat-composer__input").send_keys(:arrow_up)
expect(page.find(".chat-composer-message-details")).to have_content(message_1.message)
end
@@ -111,7 +111,7 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
page.driver.browser.network_conditions = { offline: true }
channel_page.send_message("Hello world")
- find(".chat-composer-input").send_keys(:arrow_up)
+ find(".chat-composer__input").send_keys(:arrow_up)
expect(page).to have_no_css(".chat-composer-message-details")
end
diff --git a/plugins/chat/spec/system/shortcuts/drawer_spec.rb b/plugins/chat/spec/system/shortcuts/drawer_spec.rb
index d9c3640df2b..6333afca295 100644
--- a/plugins/chat/spec/system/shortcuts/drawer_spec.rb
+++ b/plugins/chat/spec/system/shortcuts/drawer_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe "Shortcuts | drawer", type: :system, js: true do
expect(page).to have_css(".chat-drawer.is-expanded")
drawer.open_channel(channel_1)
- find(".chat-composer-input").send_keys(:escape)
+ find(".chat-composer__input").send_keys(:escape)
expect(page).to have_no_css(".chat-drawer.is-expanded")
end
@@ -49,7 +49,7 @@ RSpec.describe "Shortcuts | drawer", type: :system, js: true do
page.send_keys("e")
- expect(find(".chat-composer-input").value).to eq("")
+ expect(find(".chat-composer__input").value).to eq("")
end
end
@@ -59,15 +59,15 @@ RSpec.describe "Shortcuts | drawer", type: :system, js: true do
expect(page).to have_selector(".chat-drawer[data-chat-channel-id=\"#{channel_1.id}\"]")
- find(".chat-composer-input").send_keys(%i[alt arrow_down])
+ find(".chat-composer__input").send_keys(%i[alt arrow_down])
expect(page).to have_selector(".chat-drawer[data-chat-channel-id=\"#{channel_2.id}\"]")
- find(".chat-composer-input").send_keys(%i[alt arrow_down])
+ find(".chat-composer__input").send_keys(%i[alt arrow_down])
expect(page).to have_selector(".chat-drawer[data-chat-channel-id=\"#{channel_1.id}\"]")
- find(".chat-composer-input").send_keys(%i[alt arrow_up])
+ find(".chat-composer__input").send_keys(%i[alt arrow_up])
expect(page).to have_selector(".chat-drawer[data-chat-channel-id=\"#{channel_2.id}\"]")
end
diff --git a/plugins/chat/spec/system/shortcuts/full_page_spec.rb b/plugins/chat/spec/system/shortcuts/full_page_spec.rb
index 84478929be6..6b833b11dae 100644
--- a/plugins/chat/spec/system/shortcuts/full_page_spec.rb
+++ b/plugins/chat/spec/system/shortcuts/full_page_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe "Shortcuts | full page", type: :system, js: true do
page.send_keys("e")
- expect(find(".chat-composer-input").value).to eq("e")
+ expect(find(".chat-composer__input").value).to eq("e")
end
end
end
diff --git a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
index 869ef68f2e0..1be4ec80d91 100644
--- a/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
+++ b/plugins/chat/spec/system/sidebar_navigation_menu_spec.rb
@@ -182,7 +182,7 @@ RSpec.describe "Sidebar navigation menu", type: :system, js: true do
visit("/")
expect(sidebar_page.dms_section.find(".channel-#{dm_channel_1.id}")["title"]).to eq(
- "Chat with @<script>alert('hello')</script>",
+ "Chat in @<script>alert('hello')</script>",
)
end
end
diff --git a/plugins/chat/spec/system/silenced_user_spec.rb b/plugins/chat/spec/system/silenced_user_spec.rb
index 50a73ee1497..5c845134d59 100644
--- a/plugins/chat/spec/system/silenced_user_spec.rb
+++ b/plugins/chat/spec/system/silenced_user_spec.rb
@@ -27,10 +27,10 @@ RSpec.describe "Silenced user", type: :system, js: true do
)
end
- it "removes the send button" do
+ it "disables the send button" do
chat.visit_channel(channel_1)
- expect(page).to have_css(".send-btn[disabled]")
+ expect(page).to have_css(".chat-composer__send-btn[disabled]")
end
it "prevents reactions" do
diff --git a/plugins/chat/spec/system/uploads_spec.rb b/plugins/chat/spec/system/uploads_spec.rb
index 7000e8e178c..ab9339efbea 100644
--- a/plugins/chat/spec/system/uploads_spec.rb
+++ b/plugins/chat/spec/system/uploads_spec.rb
@@ -25,7 +25,6 @@ describe "Uploading files in chat messages", type: :system, js: true do
end
expect(page).to have_css(".chat-composer-upload .preview .preview-img")
- expect(page).to have_content(File.basename(file_path))
channel.send_message("upload testing")
@@ -61,7 +60,6 @@ describe "Uploading files in chat messages", type: :system, js: true do
channel.click_action_button("chat-upload-btn")
end
- expect(page).to have_content(File.basename(file_path))
expect(find(".chat-composer-upload")).to have_content("Processing")
# image processing clientside is slow! here we are waiting for processing
@@ -101,10 +99,11 @@ describe "Uploading files in chat messages", type: :system, js: true do
it "allows deleting uploads" do
chat.visit_channel(channel_1)
channel.open_edit_message(message_2)
- find(".chat-composer-upload").find(".remove-upload").click
+ find(".chat-composer-upload").hover
+ find(".chat-composer-upload__remove-btn").click
channel.click_send_message
expect(channel.message_by_id(message_2.id)).not_to have_css(".chat-uploads")
- expect(message_2.reload.uploads).to be_empty
+ try_until_success(timeout: 5) { expect(message_2.reload.uploads).to be_empty }
end
it "allows adding more uploads" do
@@ -118,13 +117,13 @@ describe "Uploading files in chat messages", type: :system, js: true do
end
expect(page).to have_css(".chat-composer-upload .preview .preview-img", count: 2)
- expect(page).to have_content(File.basename(file_path))
channel.click_send_message
expect(page).not_to have_css(".chat-composer-upload")
expect(page).to have_css(".chat-img-upload", count: 2)
- expect(message_2.reload.uploads.count).to eq(2)
+
+ try_until_success(timeout: 5) { expect(message_2.reload.uploads.count).to eq(2) }
end
end
diff --git a/plugins/chat/test/javascripts/acceptance/chat-composer-test.js b/plugins/chat/test/javascripts/acceptance/chat-composer-test.js
index b842f618814..26f06d481f7 100644
--- a/plugins/chat/test/javascripts/acceptance/chat-composer-test.js
+++ b/plugins/chat/test/javascripts/acceptance/chat-composer-test.js
@@ -58,12 +58,12 @@ acceptance("Discourse Chat - Composer", function (needs) {
};
document
- .querySelector(".chat-composer-input")
+ .querySelector(".chat-composer__input")
.dispatchEvent(clipboardEvent);
await settled();
- assert.equal(document.querySelector(".chat-composer-input").value, "Foo");
+ assert.equal(document.querySelector(".chat-composer__input").value, "Foo");
});
});
@@ -97,7 +97,7 @@ acceptance("Discourse Chat - Composer - unreliable network", function (needs) {
skip("Sending a message with unreliable network", async function (assert) {
await visit("/chat/c/-/11");
- await fillIn(".chat-composer-input", "network-error-message");
+ await fillIn(".chat-composer__input", "network-error-message");
await click(".send-btn");
assert.ok(
@@ -105,7 +105,7 @@ acceptance("Discourse Chat - Composer - unreliable network", function (needs) {
"it adds a retry button"
);
- await fillIn(".chat-composer-input", "network-error-message");
+ await fillIn(".chat-composer__input", "network-error-message");
await click(".send-btn");
await publishToMessageBus(`/chat/11`, {
type: "sent",
@@ -126,7 +126,7 @@ acceptance("Discourse Chat - Composer - unreliable network", function (needs) {
"it sends the message"
);
assert.strictEqual(
- query(".chat-composer-input").value,
+ query(".chat-composer__input").value,
"",
"it clears the input"
);
diff --git a/plugins/chat/test/javascripts/components/chat-composer-placeholder-test.js b/plugins/chat/test/javascripts/components/chat-composer-placeholder-test.js
index 718b40e4a9c..eb9342fb608 100644
--- a/plugins/chat/test/javascripts/components/chat-composer-placeholder-test.js
+++ b/plugins/chat/test/javascripts/components/chat-composer-placeholder-test.js
@@ -15,20 +15,17 @@ module(
pretender.get("/chat/emojis.json", () => [200, [], {}]);
this.currentUser.set("id", 1);
- this.set(
- "chatChannel",
- ChatChannel.create({
- chatable_type: "DirectMessage",
- chatable: {
- users: [{ id: 1 }],
- },
- })
- );
+ this.channel = ChatChannel.create({
+ chatable_type: "DirectMessage",
+ chatable: {
+ users: [{ id: 1 }],
+ },
+ });
- await render(hbs`
`);
+ await render(hbs`
`);
assert.strictEqual(
- query(".chat-composer-input").placeholder,
+ query(".chat-composer__input").placeholder,
"Jot something down"
);
});
@@ -36,24 +33,21 @@ module(
test("direct message to multiple folks shows their names", async function (assert) {
pretender.get("/chat/emojis.json", () => [200, [], {}]);
- this.set(
- "chatChannel",
- ChatChannel.create({
- chatable_type: "DirectMessage",
- chatable: {
- users: [
- { name: "Tomtom" },
- { name: "Steaky" },
- { username: "zorro" },
- ],
- },
- })
- );
+ this.channel = ChatChannel.create({
+ chatable_type: "DirectMessage",
+ chatable: {
+ users: [
+ { name: "Tomtom" },
+ { name: "Steaky" },
+ { username: "zorro" },
+ ],
+ },
+ });
- await render(hbs`
`);
+ await render(hbs`
`);
assert.strictEqual(
- query(".chat-composer-input").placeholder,
+ query(".chat-composer__input").placeholder,
"Chat with Tomtom, Steaky, @zorro"
);
});
@@ -61,19 +55,16 @@ module(
test("message to channel shows send message to channel name", async function (assert) {
pretender.get("/chat/emojis.json", () => [200, [], {}]);
- this.set(
- "chatChannel",
- ChatChannel.create({
- chatable_type: "Category",
- title: "just-cats",
- })
- );
+ this.channel = ChatChannel.create({
+ chatable_type: "Category",
+ title: "just-cats",
+ });
- await render(hbs`
`);
+ await render(hbs`
`);
assert.strictEqual(
- query(".chat-composer-input").placeholder,
- "Chat with #just-cats"
+ query(".chat-composer__input").placeholder,
+ "Chat in #just-cats"
);
});
}
diff --git a/plugins/chat/test/javascripts/components/chat-composer-upload-test.js b/plugins/chat/test/javascripts/components/chat-composer-upload-test.js
index 7a5d3526b2a..f57820c9f70 100644
--- a/plugins/chat/test/javascripts/components/chat-composer-upload-test.js
+++ b/plugins/chat/test/javascripts/components/chat-composer-upload-test.js
@@ -86,8 +86,6 @@ module("Discourse Chat | Component | chat-composer-upload", function (hooks) {
);
assert.true(exists("img.preview-img[src='/images/avatar.png']"));
- assert.strictEqual(query(".file-name").innerText.trim(), "bar_image.png");
- assert.strictEqual(query(".extension-pill").innerText.trim(), "png");
});
test("removing completed upload", async function (assert) {
@@ -106,7 +104,7 @@ module("Discourse Chat | Component | chat-composer-upload", function (hooks) {
hbs`
`
);
- await click(".remove-upload");
+ await click(".chat-composer-upload__remove-btn");
assert.strictEqual(this.uploadRemoved, true);
});
@@ -126,7 +124,7 @@ module("Discourse Chat | Component | chat-composer-upload", function (hooks) {
hbs`
`
);
- await click(".remove-upload");
+ await click(".chat-composer-upload__remove-btn");
assert.strictEqual(this.uploadRemoved, true);
});
});
diff --git a/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js b/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
index 1a3872bd2c2..6d119715028 100644
--- a/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
+++ b/plugins/chat/test/javascripts/components/chat-composer-uploads-test.js
@@ -97,7 +97,7 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
assert.dom(".chat-composer-upload").exists({ count: 1 });
- await click(".remove-upload");
+ await click(".chat-composer-upload__remove-btn");
assert.dom(".chat-composer-upload").exists({ count: 0 });
});
@@ -138,7 +138,7 @@ module("Discourse Chat | Component | chat-composer-uploads", function (hooks) {
await waitFor(".chat-composer-upload");
assert.strictEqual(count(".chat-composer-upload"), 1);
- await click(".remove-upload");
+ await click(".chat-composer-upload__remove-btn");
assert.strictEqual(count(".chat-composer-upload"), 0);
});
});
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 666e83a87b7..f75359e5e00 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -296,7 +296,7 @@ RSpec.configure do |config|
Selenium::WebDriver::Chrome::Options
.new(logging_prefs: { "browser" => "INFO", "driver" => "ALL" })
.tap do |options|
- options.add_argument("--window-size=390,950")
+ options.add_argument("--window-size=390,960")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_emulation(device_name: "iPhone 12 Pro")