FEATURE: implements drafts for threads (#24483)
This commit implements drafts for threads by adding a new `thread_id` column to `chat_drafts` table. This column is used to create draft keys on the frontend which are a compound key of the channel and the thread. If the draft is only for the channel, the key will be `c-${channelId}`, if for a thread: `c-${channelId}:t-${threadId}`. This commit also moves the draft holder from the service to the channel or thread model. The current draft can now always be accessed by doing: `channel.draft` or `thread.draft`. Other notable changes of this commit: - moves ChatChannel to gjs - moves ChatThread to gjs
This commit is contained in:
parent
39aa70d7cb
commit
906caa63d7
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChannelsDraftsController < Chat::ApiController
|
||||
def create
|
||||
with_service(Chat::UpsertDraft) { on_model_not_found(:channel) { raise Discourse::NotFound } }
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::ChannelsThreadsDraftsController < Chat::ApiController
|
||||
def create
|
||||
with_service(Chat::UpsertDraft) do
|
||||
on_model_not_found(:channel) { raise Discourse::NotFound }
|
||||
on_failed_step(:check_thread_exists) { raise Discourse::NotFound }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -117,19 +117,6 @@ module Chat
|
|||
end
|
||||
end
|
||||
|
||||
def set_draft
|
||||
if params[:data].present?
|
||||
Chat::Draft.find_or_initialize_by(
|
||||
user: current_user,
|
||||
chat_channel_id: @chat_channel.id,
|
||||
).update!(data: params[:data])
|
||||
else
|
||||
Chat::Draft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all
|
||||
end
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def preloaded_chat_message_query
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Service responsible to create draft for a channel, or a channel’s thread.
|
||||
#
|
||||
# @example
|
||||
# ::Chat::UpsertDraft.call(
|
||||
# guardian: guardian,
|
||||
# channel_id: 1,
|
||||
# thread_id: 1,
|
||||
# data: { message: "foo" }
|
||||
# )
|
||||
#
|
||||
class UpsertDraft
|
||||
include Service::Base
|
||||
|
||||
# @!method call(guardian:, channel_id:, thread_id:, data:)
|
||||
# @param [Guardian] guardian
|
||||
# @param [Integer] channel_id of the channel
|
||||
# @param [String] json object as string containing the data of the draft (message, uploads, replyToMsg and editing keys)
|
||||
# @option [Integer] thread_id of the channel
|
||||
# @return [Service::Base::Context]
|
||||
contract
|
||||
model :channel
|
||||
policy :can_upsert_draft
|
||||
step :check_thread_exists
|
||||
step :upsert_draft
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :channel_id, :integer
|
||||
validates :channel_id, presence: true
|
||||
|
||||
attribute :thread_id, :integer
|
||||
attribute :data, :string
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_channel(contract:, **)
|
||||
Chat::Channel.find_by(id: contract.channel_id)
|
||||
end
|
||||
|
||||
def can_upsert_draft(guardian:, channel:, **)
|
||||
guardian.can_chat? && guardian.can_join_chat_channel?(channel)
|
||||
end
|
||||
|
||||
def check_thread_exists(contract:, channel:, **)
|
||||
if contract.thread_id.present?
|
||||
fail!("Thread not found") if !channel.threads.exists?(id: contract.thread_id)
|
||||
end
|
||||
end
|
||||
|
||||
def upsert_draft(contract:, guardian:, **)
|
||||
if contract.data.present?
|
||||
draft =
|
||||
Chat::Draft.find_or_initialize_by(
|
||||
user_id: guardian.user.id,
|
||||
chat_channel_id: contract.channel_id,
|
||||
thread_id: contract.thread_id,
|
||||
)
|
||||
draft.data = contract.data
|
||||
draft.save!
|
||||
else
|
||||
# when data is empty, we destroy the draft
|
||||
Chat::Draft.where(
|
||||
user: guardian.user,
|
||||
chat_channel_id: contract.channel_id,
|
||||
thread_id: contract.thread_id,
|
||||
).destroy_all
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,14 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import didUpdate from "@ember/render-modifiers/modifiers/did-update";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel, next, schedule } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { resetIdle } from "discourse/lib/desktop-notifications";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
|
@ -11,8 +16,11 @@ import {
|
|||
onPresenceChange,
|
||||
removeOnPresenceChange,
|
||||
} from "discourse/lib/user-presence";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import not from "truth-helpers/helpers/not";
|
||||
import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager";
|
||||
import {
|
||||
FUTURE,
|
||||
|
@ -31,6 +39,18 @@ import {
|
|||
} from "discourse/plugins/chat/discourse/lib/scroll-helpers";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { stackingContextFix } from "../lib/chat-ios-hacks";
|
||||
import ChatOnResize from "../modifiers/chat/on-resize";
|
||||
import ChatScrollableList from "../modifiers/chat/scrollable-list";
|
||||
import ChatComposerChannel from "./chat/composer/channel";
|
||||
import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow";
|
||||
import ChatSelectionManager from "./chat/selection-manager";
|
||||
import ChatChannelPreviewCard from "./chat-channel-preview-card";
|
||||
import ChatFullPageHeader from "./chat-full-page-header";
|
||||
import ChatMentionWarnings from "./chat-mention-warnings";
|
||||
import Message from "./chat-message";
|
||||
import ChatNotices from "./chat-notices";
|
||||
import ChatSkeleton from "./chat-skeleton";
|
||||
import ChatUploadDropZone from "./chat-upload-drop-zone";
|
||||
|
||||
export default class ChatChannel extends Component {
|
||||
@service appEvents;
|
||||
|
@ -75,23 +95,14 @@ export default class ChatChannel extends Component {
|
|||
return this.args.channel.currentUserMembership;
|
||||
}
|
||||
|
||||
@action
|
||||
setUploadDropZone(element) {
|
||||
this.uploadDropZone = element;
|
||||
}
|
||||
|
||||
@action
|
||||
setScrollable(element) {
|
||||
this.scrollable = element;
|
||||
}
|
||||
|
||||
@action
|
||||
setupListeners() {
|
||||
onPresenceChange({ callback: this.onPresenceChangeCallback });
|
||||
}
|
||||
|
||||
@action
|
||||
teardownListeners() {
|
||||
teardown() {
|
||||
document.removeEventListener("keydown", this._autoFocus);
|
||||
this.#cancelHandlers();
|
||||
removeOnPresenceChange(this.onPresenceChangeCallback);
|
||||
this.subscriptionManager.teardown();
|
||||
|
@ -104,7 +115,11 @@ export default class ChatChannel extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
didUpdateChannel() {
|
||||
setup(element) {
|
||||
this.uploadDropZone = element;
|
||||
document.addEventListener("keydown", this._autoFocus);
|
||||
onPresenceChange({ callback: this.onPresenceChangeCallback });
|
||||
|
||||
this.messagesManager.clear();
|
||||
|
||||
if (
|
||||
|
@ -114,14 +129,11 @@ export default class ChatChannel extends Component {
|
|||
this.chatChannelsManager.follow(this.args.channel);
|
||||
}
|
||||
|
||||
const existingDraft = this.chatDraftsManager.get({
|
||||
channelId: this.args.channel.id,
|
||||
});
|
||||
if (existingDraft) {
|
||||
this.composer.message = existingDraft;
|
||||
} else {
|
||||
this.resetComposerMessage();
|
||||
}
|
||||
this.args.channel.draft =
|
||||
this.chatDraftsManager.get(this.args.channel?.id) ||
|
||||
ChatMessage.createDraftMessage(this.args.channel, {
|
||||
user: this.currentUser,
|
||||
});
|
||||
|
||||
this.composer.focus();
|
||||
this.loadMessages();
|
||||
|
@ -485,7 +497,7 @@ export default class ChatChannel extends Component {
|
|||
|
||||
@action
|
||||
resetComposerMessage() {
|
||||
this.composer.reset(this.args.channel);
|
||||
this.args.channel.resetDraft(this.currentUser);
|
||||
}
|
||||
|
||||
async #sendEditMessage(message) {
|
||||
|
@ -506,7 +518,7 @@ export default class ChatChannel extends Component {
|
|||
popupAjaxError(e);
|
||||
} finally {
|
||||
message.editing = false;
|
||||
this.chatDraftsManager.remove({ channelId: this.args.channel.id });
|
||||
this.resetComposerMessage();
|
||||
this.pane.sending = false;
|
||||
}
|
||||
}
|
||||
|
@ -541,7 +553,7 @@ export default class ChatChannel extends Component {
|
|||
} catch (error) {
|
||||
this._onSendError(message.id, error);
|
||||
} finally {
|
||||
this.chatDraftsManager.remove({ channelId: this.args.channel.id });
|
||||
this.resetComposerMessage();
|
||||
this.pane.sending = false;
|
||||
}
|
||||
}
|
||||
|
@ -599,16 +611,6 @@ export default class ChatChannel extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
addAutoFocusEventListener() {
|
||||
document.addEventListener("keydown", this._autoFocus);
|
||||
}
|
||||
|
||||
@action
|
||||
removeAutoFocusEventListener() {
|
||||
document.removeEventListener("keydown", this._autoFocus);
|
||||
}
|
||||
|
||||
@bind
|
||||
_autoFocus(event) {
|
||||
if (this.chatStateManager.isDrawerActive) {
|
||||
|
@ -705,4 +707,92 @@ export default class ChatChannel extends Component {
|
|||
this._ignoreNextScroll = false;
|
||||
return prev;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-channel"
|
||||
(if this.messagesLoader.loading "loading")
|
||||
(if this.pane.sending "chat-channel--sending")
|
||||
(unless this.messagesLoader.fetchedOnce "chat-channel--not-loaded-once")
|
||||
}}
|
||||
{{willDestroy this.teardown}}
|
||||
{{didInsert this.setup}}
|
||||
{{didUpdate this.loadMessages @targetMessageId}}
|
||||
data-id={{@channel.id}}
|
||||
>
|
||||
<ChatFullPageHeader
|
||||
@channel={{@channel}}
|
||||
@onCloseFullScreen={{this.onCloseFullScreen}}
|
||||
@displayed={{this.includeHeader}}
|
||||
/>
|
||||
|
||||
<ChatNotices @channel={{@channel}} />
|
||||
|
||||
<ChatMentionWarnings />
|
||||
|
||||
<div
|
||||
class="chat-messages-scroll chat-messages-container popper-viewport"
|
||||
{{didInsert this.setScrollable}}
|
||||
{{ChatScrollableList
|
||||
(hash
|
||||
onScroll=this.onScroll onScrollEnd=this.onScrollEnd reverse=true
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-messages-container"
|
||||
{{ChatOnResize this.didResizePane (hash delay=100 immediate=true)}}
|
||||
>
|
||||
{{#each this.messagesManager.messages key="id" as |message|}}
|
||||
<Message
|
||||
@message={{message}}
|
||||
@disableMouseEvents={{this.isScrolling}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@fetchMessagesByDate={{this.fetchMessagesByDate}}
|
||||
@context="channel"
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.messagesLoader.fetchedOnce}}
|
||||
<ChatSkeleton />
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{! at bottom even if shown at top due to column-reverse }}
|
||||
{{#if this.messagesLoader.loadedPast}}
|
||||
<div class="all-loaded-message">
|
||||
{{i18n "chat.all_loaded"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ChatScrollToBottomArrow
|
||||
@onScrollToBottom={{this.scrollToLatestMessage}}
|
||||
@isVisible={{this.needsArrow}}
|
||||
/>
|
||||
|
||||
{{#if this.pane.selectingMessages}}
|
||||
<ChatSelectionManager
|
||||
@enableMove={{and
|
||||
(not @channel.isDirectMessageChannel)
|
||||
@channel.canModerate
|
||||
}}
|
||||
@pane={{this.pane}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if (and (not @channel.isFollowing) @channel.isCategoryChannel)}}
|
||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||
{{else}}
|
||||
<ChatComposerChannel
|
||||
@channel={{@channel}}
|
||||
@uploadDropZone={{this.uploadDropZone}}
|
||||
@onSendMessage={{this.onSendMessage}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<ChatUploadDropZone @model={{@channel}} />
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
<div
|
||||
class={{concat-class
|
||||
"chat-channel"
|
||||
(if this.messagesLoader.loading "loading")
|
||||
(if this.pane.sending "chat-channel--sending")
|
||||
(unless this.messagesLoader.fetchedOnce "chat-channel--not-loaded-once")
|
||||
}}
|
||||
{{did-insert this.setUploadDropZone}}
|
||||
{{did-insert this.setupListeners}}
|
||||
{{will-destroy this.teardownListeners}}
|
||||
{{did-insert this.didUpdateChannel}}
|
||||
{{did-insert this.addAutoFocusEventListener}}
|
||||
{{will-destroy this.removeAutoFocusEventListener}}
|
||||
{{did-update this.loadMessages @targetMessageId}}
|
||||
data-id={{@channel.id}}
|
||||
>
|
||||
<ChatFullPageHeader
|
||||
@channel={{@channel}}
|
||||
@onCloseFullScreen={{this.onCloseFullScreen}}
|
||||
@displayed={{this.includeHeader}}
|
||||
/>
|
||||
|
||||
<Chat::Notices @channel={{@channel}} />
|
||||
|
||||
<ChatMentionWarnings />
|
||||
|
||||
<div
|
||||
class="chat-messages-scroll chat-messages-container popper-viewport"
|
||||
{{did-insert this.setScrollable}}
|
||||
{{chat/scrollable-list
|
||||
(hash onScroll=this.onScroll onScrollEnd=this.onScrollEnd reverse=true)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-messages-container"
|
||||
{{chat/on-resize this.didResizePane (hash delay=100 immediate=true)}}
|
||||
>
|
||||
{{#each this.messagesManager.messages key="id" as |message|}}
|
||||
<ChatMessage
|
||||
@message={{message}}
|
||||
@disableMouseEvents={{this.isScrolling}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@fetchMessagesByDate={{this.fetchMessagesByDate}}
|
||||
@context="channel"
|
||||
/>
|
||||
{{else}}
|
||||
{{#unless this.messagesLoader.fetchedOnce}}
|
||||
<ChatSkeleton />
|
||||
{{/unless}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{! at bottom even if shown at top due to column-reverse }}
|
||||
{{#if this.messagesLoader.loadedPast}}
|
||||
<div class="all-loaded-message">
|
||||
{{i18n "chat.all_loaded"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<Chat::ScrollToBottomArrow
|
||||
@onScrollToBottom={{this.scrollToLatestMessage}}
|
||||
@isVisible={{this.needsArrow}}
|
||||
/>
|
||||
|
||||
{{#if this.pane.selectingMessages}}
|
||||
<Chat::SelectionManager
|
||||
@enableMove={{and
|
||||
(not @channel.isDirectMessageChannel)
|
||||
@channel.canModerate
|
||||
}}
|
||||
@pane={{this.pane}}
|
||||
/>
|
||||
{{else}}
|
||||
{{#if (and (not @channel.isFollowing) @channel.isCategoryChannel)}}
|
||||
<ChatChannelPreviewCard @channel={{@channel}} />
|
||||
{{else}}
|
||||
<Chat::Composer::Channel
|
||||
@channel={{@channel}}
|
||||
@uploadDropZone={{this.uploadDropZone}}
|
||||
@onSendMessage={{this.onSendMessage}}
|
||||
/>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<ChatUploadDropZone @model={{@channel}} />
|
||||
</div>
|
|
@ -4,12 +4,8 @@
|
|||
<div class="chat-composer__wrapper">
|
||||
{{#if this.shouldRenderMessageDetails}}
|
||||
<ChatComposerMessageDetails
|
||||
@message={{if
|
||||
this.currentMessage.editing
|
||||
this.currentMessage
|
||||
this.currentMessage.inReplyTo
|
||||
}}
|
||||
@cancelAction={{this.composer.cancel}}
|
||||
@message={{if this.draft.editing this.draft this.draft.inReplyTo}}
|
||||
@cancelAction={{this.resetDraft}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
@ -22,10 +18,10 @@
|
|||
(if this.pane.sending "is-sending")
|
||||
(if this.sendEnabled "is-send-enabled" "is-send-disabled")
|
||||
(if this.disabled "is-disabled" "is-enabled")
|
||||
(if this.currentMessage.draftSaved "is-draft-saved" "is-draft-unsaved")
|
||||
(if this.draft.draftSaved "is-draft-saved" "is-draft-unsaved")
|
||||
}}
|
||||
{{did-update this.didUpdateMessage this.currentMessage}}
|
||||
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
|
||||
{{did-update this.didUpdateMessage this.draft}}
|
||||
{{did-update this.didUpdateInReplyTo this.draft.inReplyTo}}
|
||||
{{did-insert this.setup}}
|
||||
{{will-destroy this.teardown}}
|
||||
{{will-destroy this.cancelPersistDraft}}
|
||||
|
@ -43,7 +39,7 @@
|
|||
>
|
||||
<DTextarea
|
||||
id={{this.composerId}}
|
||||
value={{readonly this.currentMessage.message}}
|
||||
value={{readonly this.draft.message}}
|
||||
type="text"
|
||||
class="chat-composer__input"
|
||||
disabled={{this.disabled}}
|
||||
|
@ -101,19 +97,15 @@
|
|||
<ChatComposerUploads
|
||||
@fileUploadElementId={{this.fileUploadElementId}}
|
||||
@onUploadChanged={{this.onUploadChanged}}
|
||||
@existingUploads={{this.currentMessage.uploads}}
|
||||
@existingUploads={{this.draft.uploads}}
|
||||
@uploadDropZone={{@uploadDropZone}}
|
||||
@composerInputEl={{this.composer.textarea.element}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.shouldRenderReplyingIndicator}}
|
||||
<div class="chat-replying-indicator-container">
|
||||
<ChatReplyingIndicator
|
||||
@presenceChannelName={{this.presenceChannelName}}
|
||||
/>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class="chat-replying-indicator-container">
|
||||
<ChatReplyingIndicator @presenceChannelName={{this.presenceChannelName}} />
|
||||
</div>
|
||||
|
||||
<ChatEmojiPicker
|
||||
@context={{this.context}}
|
||||
|
|
|
@ -48,8 +48,8 @@ export default class ChatComposer extends Component {
|
|||
|
||||
get shouldRenderMessageDetails() {
|
||||
return (
|
||||
this.currentMessage?.editing ||
|
||||
(this.context === "channel" && this.currentMessage?.inReplyTo)
|
||||
this.draft?.editing ||
|
||||
(this.context === "channel" && this.draft?.inReplyTo)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ export default class ChatComposer extends Component {
|
|||
@action
|
||||
didUpdateMessage() {
|
||||
this.cancelPersistDraft();
|
||||
this.composer.textarea.value = this.currentMessage.message;
|
||||
this.composer.textarea.value = this.draft.message;
|
||||
this.persistDraft();
|
||||
this.captureMentions({ skipDebounce: true });
|
||||
}
|
||||
|
@ -118,25 +118,21 @@ export default class ChatComposer extends Component {
|
|||
buttonAction();
|
||||
}
|
||||
|
||||
get currentMessage() {
|
||||
return this.composer.message;
|
||||
}
|
||||
|
||||
get hasContent() {
|
||||
const minLength = this.siteSettings.chat_minimum_message_length || 1;
|
||||
return (
|
||||
this.currentMessage?.message?.length >= minLength ||
|
||||
this.draft?.message?.length >= minLength ||
|
||||
(this.canAttachUploads && this.hasUploads)
|
||||
);
|
||||
}
|
||||
|
||||
get hasUploads() {
|
||||
return this.currentMessage?.uploads?.length > 0;
|
||||
return this.draft?.uploads?.length > 0;
|
||||
}
|
||||
|
||||
get sendEnabled() {
|
||||
return (
|
||||
(this.hasContent || this.currentMessage?.editing) &&
|
||||
(this.hasContent || this.draft?.editing) &&
|
||||
!this.pane.sending &&
|
||||
!this.inProgressUploadsCount > 0
|
||||
);
|
||||
|
@ -196,8 +192,8 @@ export default class ChatComposer extends Component {
|
|||
|
||||
@action
|
||||
onInput(event) {
|
||||
this.currentMessage.draftSaved = false;
|
||||
this.currentMessage.message = event.target.value;
|
||||
this.draft.draftSaved = false;
|
||||
this.draft.message = event.target.value;
|
||||
this.composer.textarea.refreshHeight();
|
||||
this.reportReplyingPresence();
|
||||
this.persistDraft();
|
||||
|
@ -206,7 +202,7 @@ export default class ChatComposer extends Component {
|
|||
|
||||
@action
|
||||
onUploadChanged(uploads, { inProgressUploadsCount }) {
|
||||
this.currentMessage.draftSaved = false;
|
||||
this.draft.draftSaved = false;
|
||||
|
||||
this.inProgressUploadsCount = inProgressUploadsCount || 0;
|
||||
|
||||
|
@ -214,9 +210,9 @@ export default class ChatComposer extends Component {
|
|||
typeof uploads !== "undefined" &&
|
||||
inProgressUploadsCount !== "undefined" &&
|
||||
inProgressUploadsCount === 0 &&
|
||||
this.currentMessage
|
||||
this.draft
|
||||
) {
|
||||
this.currentMessage.uploads = cloneJSON(uploads);
|
||||
this.draft.uploads = cloneJSON(uploads);
|
||||
}
|
||||
|
||||
this.composer.textarea?.focus();
|
||||
|
@ -238,26 +234,26 @@ export default class ChatComposer extends Component {
|
|||
event?.preventDefault();
|
||||
|
||||
if (
|
||||
this.currentMessage.editing &&
|
||||
this.draft.editing &&
|
||||
!this.hasUploads &&
|
||||
this.currentMessage.message.length === 0
|
||||
this.draft.message.length === 0
|
||||
) {
|
||||
this.#deleteEmptyMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.args.onSendMessage(this.currentMessage);
|
||||
await this.args.onSendMessage(this.draft);
|
||||
this.composer.textarea.refreshHeight();
|
||||
}
|
||||
|
||||
reportReplyingPresence() {
|
||||
if (!this.args.channel || !this.currentMessage) {
|
||||
if (!this.args.channel || !this.draft) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatComposerPresenceManager.notifyState(
|
||||
this.presenceChannelName,
|
||||
!this.currentMessage.editing && this.hasContent
|
||||
!this.draft.editing && this.hasContent
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -330,17 +326,14 @@ export default class ChatComposer extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === "ArrowUp" &&
|
||||
!this.hasContent &&
|
||||
!this.currentMessage.editing
|
||||
) {
|
||||
if (event.key === "ArrowUp" && !this.hasContent && !this.draft.editing) {
|
||||
if (event.shiftKey && this.lastMessage?.replyable) {
|
||||
this.composer.replyTo(this.lastMessage);
|
||||
} else {
|
||||
const editableMessage = this.lastUserMessage(this.currentUser);
|
||||
if (editableMessage?.editable) {
|
||||
this.composer.edit(editableMessage);
|
||||
this.args.channel.draft = editableMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,7 +374,7 @@ export default class ChatComposer extends Component {
|
|||
captureMentions(opts = { skipDebounce: false }) {
|
||||
if (this.hasContent) {
|
||||
this.chatComposerWarningsTracker.trackMentions(
|
||||
this.currentMessage,
|
||||
this.draft,
|
||||
opts.skipDebounce
|
||||
);
|
||||
} else {
|
||||
|
@ -398,7 +391,7 @@ export default class ChatComposer extends Component {
|
|||
|
||||
#addMentionedUser(userData) {
|
||||
const user = this.store.createRecord("user", userData);
|
||||
this.currentMessage.mentionedUsers.set(user.id, user);
|
||||
this.draft.mentionedUsers.set(user.id, user);
|
||||
}
|
||||
|
||||
#applyUserAutocomplete($textarea) {
|
||||
|
@ -598,9 +591,9 @@ export default class ChatComposer extends Component {
|
|||
#deleteEmptyMessage() {
|
||||
new ChatMessageInteractor(
|
||||
getOwner(this),
|
||||
this.currentMessage,
|
||||
this.draft,
|
||||
this.context
|
||||
).delete();
|
||||
this.reset(this.args.channel, this.args.thread);
|
||||
this.resetDraft();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached, tracked } from "@glimmer/tracking";
|
||||
import { getOwner } from "@ember/application";
|
||||
import { hash } from "@ember/helper";
|
||||
import { action } from "@ember/object";
|
||||
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
|
||||
import willDestroy from "@ember/render-modifiers/modifiers/will-destroy";
|
||||
import { cancel, next } from "@ember/runloop";
|
||||
import { inject as service } from "@ember/service";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { resetIdle } from "discourse/lib/desktop-notifications";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
|
@ -27,6 +31,15 @@ import {
|
|||
} from "discourse/plugins/chat/discourse/lib/scroll-helpers";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
|
||||
import ChatOnResize from "../modifiers/chat/on-resize";
|
||||
import ChatScrollableList from "../modifiers/chat/scrollable-list";
|
||||
import ChatComposerThread from "./chat/composer/thread";
|
||||
import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow";
|
||||
import ChatSelectionManager from "./chat/selection-manager";
|
||||
import ChatThreadHeader from "./chat/thread/header";
|
||||
import Message from "./chat-message";
|
||||
import ChatSkeleton from "./chat-skeleton";
|
||||
import ChatUploadDropZone from "./chat-upload-drop-zone";
|
||||
|
||||
export default class ChatThread extends Component {
|
||||
@service appEvents;
|
||||
|
@ -35,6 +48,7 @@ export default class ChatThread extends Component {
|
|||
@service chatApi;
|
||||
@service chatComposerPresenceManager;
|
||||
@service chatHistory;
|
||||
@service chatDraftsManager;
|
||||
@service chatThreadComposer;
|
||||
@service chatThreadPane;
|
||||
@service currentUser;
|
||||
|
@ -73,16 +87,21 @@ export default class ChatThread extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
didUpdateThread() {
|
||||
setup(element) {
|
||||
this.uploadDropZone = element;
|
||||
|
||||
this.messagesManager.clear();
|
||||
this.args.thread.draft =
|
||||
this.chatDraftsManager.get(
|
||||
this.args.thread.channel?.id,
|
||||
this.args.thread.id
|
||||
) ||
|
||||
ChatMessage.createDraftMessage(this.args.thread.channel, {
|
||||
user: this.currentUser,
|
||||
thread: this.args.thread,
|
||||
});
|
||||
this.chatThreadComposer.focus();
|
||||
this.loadMessages();
|
||||
this.resetComposerMessage();
|
||||
}
|
||||
|
||||
@action
|
||||
setUploadDropZone(element) {
|
||||
this.uploadDropZone = element;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -346,7 +365,13 @@ export default class ChatThread extends Component {
|
|||
|
||||
@action
|
||||
resetComposerMessage() {
|
||||
this.chatThreadComposer.reset(this.args.thread);
|
||||
this.args.thread.draft = ChatMessage.createDraftMessage(
|
||||
this.args.thread.channel,
|
||||
{
|
||||
user: this.currentUser,
|
||||
thread: this.args.thread,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async #sendNewMessage(message) {
|
||||
|
@ -387,6 +412,10 @@ export default class ChatThread extends Component {
|
|||
} catch (error) {
|
||||
this.#onSendError(message.id, error);
|
||||
} finally {
|
||||
this.chatDraftsManager.remove(
|
||||
this.args.thread.channel.id,
|
||||
this.args.thread.id
|
||||
);
|
||||
this.chatThreadPane.sending = false;
|
||||
}
|
||||
}
|
||||
|
@ -410,6 +439,10 @@ export default class ChatThread extends Component {
|
|||
} catch (e) {
|
||||
popupAjaxError(e);
|
||||
} finally {
|
||||
this.chatDraftsManager.remove(
|
||||
this.args.thread.channel.id,
|
||||
this.args.thread.id
|
||||
);
|
||||
this.chatThreadPane.sending = false;
|
||||
}
|
||||
}
|
||||
|
@ -449,4 +482,68 @@ export default class ChatThread extends Component {
|
|||
this._ignoreNextScroll = false;
|
||||
return prev;
|
||||
}
|
||||
|
||||
<template>
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-thread"
|
||||
(if this.messagesLoader.loading "loading")
|
||||
}}
|
||||
data-id={{@thread.id}}
|
||||
{{didInsert this.setup}}
|
||||
{{willDestroy this.teardown}}
|
||||
>
|
||||
{{#if @includeHeader}}
|
||||
<ChatThreadHeader @channel={{@thread.channel}} @thread={{@thread}} />
|
||||
{{/if}}
|
||||
|
||||
<div
|
||||
class="chat-thread__body popper-viewport chat-messages-scroll"
|
||||
{{didInsert this.setScrollable}}
|
||||
{{ChatScrollableList
|
||||
(hash
|
||||
onScroll=this.onScroll onScrollEnd=this.onScrollEnd reverse=true
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-messages-container"
|
||||
{{ChatOnResize this.didResizePane (hash delay=100 immediate=true)}}
|
||||
>
|
||||
{{#each this.messagesManager.messages key="id" as |message|}}
|
||||
<Message
|
||||
@message={{message}}
|
||||
@disableMouseEvents={{this.isScrolling}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@context="thread"
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#unless this.messagesLoader.fetchedOnce}}
|
||||
{{#if this.messagesLoader.loading}}
|
||||
<ChatSkeleton />
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ChatScrollToBottomArrow
|
||||
@onScrollToBottom={{this.scrollToLatestMessage}}
|
||||
@isVisible={{this.needsArrow}}
|
||||
/>
|
||||
|
||||
{{#if this.chatThreadPane.selectingMessages}}
|
||||
<ChatSelectionManager @pane={{this.chatThreadPane}} />
|
||||
{{else}}
|
||||
<ChatComposerThread
|
||||
@channel={{@channel}}
|
||||
@thread={{@thread}}
|
||||
@onSendMessage={{this.onSendMessage}}
|
||||
@uploadDropZone={{this.uploadDropZone}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<ChatUploadDropZone @model={{@thread}} />
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
<div
|
||||
class={{concat-class
|
||||
"chat-thread"
|
||||
(if this.messagesLoader.loading "loading")
|
||||
}}
|
||||
data-id={{@thread.id}}
|
||||
{{did-insert this.setUploadDropZone}}
|
||||
{{did-insert this.didUpdateThread}}
|
||||
{{will-destroy this.teardown}}
|
||||
>
|
||||
{{#if @includeHeader}}
|
||||
<Chat::Thread::Header @channel={{@thread.channel}} @thread={{@thread}} />
|
||||
{{/if}}
|
||||
|
||||
<div
|
||||
class="chat-thread__body popper-viewport chat-messages-scroll"
|
||||
{{did-insert this.setScrollable}}
|
||||
{{chat/scrollable-list
|
||||
(hash onScroll=this.onScroll onScrollEnd=this.onScrollEnd reverse=true)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="chat-messages-container"
|
||||
{{chat/on-resize this.didResizePane (hash delay=100 immediate=true)}}
|
||||
>
|
||||
{{#each this.messagesManager.messages key="id" as |message|}}
|
||||
<ChatMessage
|
||||
@message={{message}}
|
||||
@disableMouseEvents={{this.isScrolling}}
|
||||
@resendStagedMessage={{this.resendStagedMessage}}
|
||||
@context="thread"
|
||||
/>
|
||||
{{/each}}
|
||||
|
||||
{{#unless this.messagesLoader.fetchedOnce}}
|
||||
{{#if this.messagesLoader.loading}}
|
||||
<ChatSkeleton />
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Chat::ScrollToBottomArrow
|
||||
@onScrollToBottom={{this.scrollToLatestMessage}}
|
||||
@isVisible={{this.needsArrow}}
|
||||
/>
|
||||
|
||||
{{#if this.chatThreadPane.selectingMessages}}
|
||||
<Chat::SelectionManager @pane={{this.chatThreadPane}} />
|
||||
{{else}}
|
||||
<Chat::Composer::Thread
|
||||
@channel={{@channel}}
|
||||
@thread={{@thread}}
|
||||
@onSendMessage={{this.onSendMessage}}
|
||||
@uploadDropZone={{this.uploadDropZone}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
<ChatUploadDropZone @model={{@thread}} />
|
||||
</div>
|
|
@ -1,21 +1,36 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { debounce } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatComposer from "../../chat-composer";
|
||||
|
||||
export default class ChatComposerChannel extends ChatComposer {
|
||||
@service("chat-channel-composer") composer;
|
||||
@service("chat-channel-pane") pane;
|
||||
@service chatDraftsManager;
|
||||
@service currentUser;
|
||||
@service chatDraftsManager;
|
||||
|
||||
context = "channel";
|
||||
|
||||
composerId = "channel-composer";
|
||||
|
||||
get shouldRenderReplyingIndicator() {
|
||||
return this.args.channel;
|
||||
@debounce(2000)
|
||||
persistDraft() {
|
||||
this.chatDraftsManager.add(this.draft, this.args.channel.id);
|
||||
}
|
||||
|
||||
@action
|
||||
destroyDraft() {
|
||||
this.chatDraftsManager.remove(this.args.channel.id);
|
||||
}
|
||||
|
||||
@action
|
||||
resetDraft() {
|
||||
this.args.channel.resetDraft(this.currentUser);
|
||||
}
|
||||
|
||||
get draft() {
|
||||
return this.args.channel.draft;
|
||||
}
|
||||
|
||||
get presenceChannelName() {
|
||||
|
@ -30,33 +45,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||
);
|
||||
}
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.composer.reset(this.args.channel);
|
||||
}
|
||||
|
||||
@action
|
||||
persistDraft() {
|
||||
this.chatDraftsManager.add(this.currentMessage);
|
||||
|
||||
this._persistHandler = discourseDebounce(
|
||||
this,
|
||||
this._debouncedPersistDraft,
|
||||
this.args.channel.id,
|
||||
this.currentMessage.toJSONDraft(),
|
||||
2000
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
_debouncedPersistDraft(channelId, jsonDraft) {
|
||||
this.chatApi.saveDraft(channelId, jsonDraft).then(() => {
|
||||
if (this.currentMessage) {
|
||||
this.currentMessage.draftSaved = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return this.args.channel.lastMessage;
|
||||
}
|
||||
|
@ -82,10 +70,10 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||
handleEscape(event) {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.currentMessage?.inReplyTo) {
|
||||
this.reset();
|
||||
} else if (this.currentMessage?.editing) {
|
||||
this.composer.cancel(this.args.channel);
|
||||
if (this.draft?.inReplyTo) {
|
||||
this.draft.inReplyTo = null;
|
||||
} else if (this.draft?.editing) {
|
||||
this.args.channel.resetDraft(this.currentUser);
|
||||
} else {
|
||||
event.target.blur();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { debounce } from "discourse-common/utils/decorators";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import ChatComposer from "../../chat-composer";
|
||||
|
||||
export default class ChatComposerThread extends ChatComposer {
|
||||
|
@ -8,18 +10,36 @@ export default class ChatComposerThread extends ChatComposer {
|
|||
@service("chat-thread-composer") composer;
|
||||
@service("chat-thread-pane") pane;
|
||||
@service currentUser;
|
||||
@service chatDraftsManager;
|
||||
|
||||
context = "thread";
|
||||
|
||||
composerId = "thread-composer";
|
||||
|
||||
@action
|
||||
reset() {
|
||||
this.composer.reset(this.args.thread);
|
||||
@debounce(2000)
|
||||
persistDraft() {
|
||||
this.chatDraftsManager.add(
|
||||
this.draft,
|
||||
this.args.thread.channel.id,
|
||||
this.args.thread.id
|
||||
);
|
||||
}
|
||||
|
||||
get shouldRenderReplyingIndicator() {
|
||||
return this.args.thread;
|
||||
@action
|
||||
destroyDraft() {
|
||||
this.chatDraftsManager.remove(
|
||||
this.args.thread.channel.id,
|
||||
this.args.thread.id
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
resetDraft() {
|
||||
this.args.thread.resetDraft(this.currentUser);
|
||||
}
|
||||
|
||||
get draft() {
|
||||
return this.args.thread.draft;
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
|
@ -43,9 +63,13 @@ export default class ChatComposerThread extends ChatComposer {
|
|||
}
|
||||
|
||||
handleEscape(event) {
|
||||
if (this.currentMessage.editing) {
|
||||
if (this.draft.editing) {
|
||||
event.stopPropagation();
|
||||
this.composer.cancel(this.args.thread);
|
||||
this.args.thread.draft = ChatMessage.createDraftMessage(
|
||||
this.args.thread.channel,
|
||||
{ user: this.currentUser, thread: this.args.thread }
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -231,5 +231,7 @@ export default class ChatChannelSubscriptionManager {
|
|||
if (message?.thread) {
|
||||
message.thread.preview = ChatThreadPreview.create(data.preview);
|
||||
}
|
||||
|
||||
message.thread.preview.yolo = 1;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ export default class ChatChannel {
|
|||
@tracked archive;
|
||||
@tracked tracking;
|
||||
@tracked threadingEnabled = false;
|
||||
@tracked draft;
|
||||
|
||||
threadsManager = new ChatThreadsManager(getOwnerWithFallback(this));
|
||||
messagesManager = new ChatMessagesManager(getOwnerWithFallback(this));
|
||||
|
@ -204,6 +205,12 @@ export default class ChatChannel {
|
|||
message.manager = this.messagesManager;
|
||||
}
|
||||
|
||||
resetDraft(user) {
|
||||
this.draft = ChatMessage.createDraftMessage(this, {
|
||||
user,
|
||||
});
|
||||
}
|
||||
|
||||
canModifyMessages(user) {
|
||||
if (user.staff) {
|
||||
return !STAFF_READONLY_STATUSES.includes(this.status);
|
||||
|
|
|
@ -29,8 +29,8 @@ export default class ChatThread {
|
|||
@tracked threadMessageBusLastId;
|
||||
@tracked replyCount;
|
||||
@tracked tracking;
|
||||
@tracked currentUserMembership = null;
|
||||
@tracked preview = null;
|
||||
@tracked currentUserMembership;
|
||||
@tracked preview;
|
||||
|
||||
messagesManager = new ChatMessagesManager(getOwnerWithFallback(this));
|
||||
|
||||
|
@ -38,7 +38,6 @@ export default class ChatThread {
|
|||
this.id = args.id;
|
||||
this.channel = channel;
|
||||
this.status = args.status;
|
||||
this.draft = args.draft;
|
||||
this.staged = args.staged;
|
||||
this.replyCount = args.reply_count;
|
||||
|
||||
|
@ -58,6 +57,13 @@ export default class ChatThread {
|
|||
this.preview = ChatThreadPreview.create(args.preview);
|
||||
}
|
||||
|
||||
resetDraft(user) {
|
||||
this.draft = ChatMessage.createDraftMessage(this.channel, {
|
||||
user,
|
||||
thread: this,
|
||||
});
|
||||
}
|
||||
|
||||
async stageMessage(message) {
|
||||
message.id = guid();
|
||||
message.staged = true;
|
||||
|
|
|
@ -313,11 +313,16 @@ export default class ChatApi extends Service {
|
|||
* @param {object} data - The draft data, see ChatMessage.toJSONDraft() for more details.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
saveDraft(channelId, data) {
|
||||
return ajax("/chat/drafts", {
|
||||
saveDraft(channelId, data, options = {}) {
|
||||
let endpoint = `/chat/api/channels/${channelId}`;
|
||||
if (options.threadId) {
|
||||
endpoint += `/threads/${options.threadId}`;
|
||||
}
|
||||
endpoint += "/drafts";
|
||||
|
||||
return ajax(endpoint, {
|
||||
type: "POST",
|
||||
data: {
|
||||
chat_channel_id: channelId,
|
||||
data,
|
||||
},
|
||||
ignoreUnsent: false,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
|
||||
export default class ChatChannelComposer extends Service {
|
||||
@service chat;
|
||||
|
@ -11,7 +10,6 @@ export default class ChatChannelComposer extends Service {
|
|||
@service("chat-thread-composer") threadComposer;
|
||||
@service loadingSlider;
|
||||
|
||||
@tracked message;
|
||||
@tracked textarea;
|
||||
|
||||
@action
|
||||
|
@ -24,29 +22,11 @@ export default class ChatChannelComposer extends Service {
|
|||
this.textarea.blur();
|
||||
}
|
||||
|
||||
@action
|
||||
reset(channel) {
|
||||
this.message = ChatMessage.createDraftMessage(channel, {
|
||||
user: this.currentUser,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
if (this.message.editing) {
|
||||
this.reset(this.message.channel);
|
||||
} else if (this.message.inReplyTo) {
|
||||
this.message.inReplyTo = null;
|
||||
}
|
||||
|
||||
this.focus({ ensureAtEnd: true, refreshHeight: true });
|
||||
}
|
||||
|
||||
@action
|
||||
edit(message) {
|
||||
this.chat.activeMessage = null;
|
||||
message.editing = true;
|
||||
this.message = message;
|
||||
message.channel.draft = message;
|
||||
this.focus({ refreshHeight: true, ensureAtEnd: true });
|
||||
}
|
||||
|
||||
|
@ -56,20 +36,22 @@ export default class ChatChannelComposer extends Service {
|
|||
|
||||
if (message.channel.threadingEnabled) {
|
||||
if (!message.thread?.id) {
|
||||
this.loadingSlider.transitionStarted();
|
||||
const threadObject = await this.chatApi.createThread(
|
||||
message.channel.id,
|
||||
message.id
|
||||
);
|
||||
this.loadingSlider.transitionEnded();
|
||||
|
||||
message.thread = message.channel.threadsManager.add(
|
||||
message.channel,
|
||||
threadObject
|
||||
);
|
||||
try {
|
||||
this.loadingSlider.transitionStarted();
|
||||
const threadObject = await this.chatApi.createThread(
|
||||
message.channel.id,
|
||||
message.id
|
||||
);
|
||||
message.thread = message.channel.threadsManager.add(
|
||||
message.channel,
|
||||
threadObject
|
||||
);
|
||||
} finally {
|
||||
this.loadingSlider.transitionEnded();
|
||||
}
|
||||
}
|
||||
|
||||
this.reset(message.channel);
|
||||
message.channel.resetDraft(this.currentUser);
|
||||
|
||||
await this.router.transitionTo(
|
||||
"chat.channel.thread",
|
||||
|
@ -78,7 +60,7 @@ export default class ChatChannelComposer extends Service {
|
|||
|
||||
this.threadComposer.focus({ ensureAtEnd: true, refreshHeight: true });
|
||||
} else {
|
||||
this.message.inReplyTo = message;
|
||||
message.channel.draft.inReplyTo = message;
|
||||
this.focus({ ensureAtEnd: true, refreshHeight: true });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,53 @@
|
|||
import Service from "@ember/service";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { cancel } from "@ember/runloop";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
|
||||
export default class ChatDraftsManager extends Service {
|
||||
@service chatApi;
|
||||
|
||||
drafts = {};
|
||||
|
||||
add(message) {
|
||||
if (message instanceof ChatMessage) {
|
||||
this.drafts[message.channel.id] = message;
|
||||
} else {
|
||||
throw new Error("message must be an instance of ChatMessage");
|
||||
async add(message, channelId, threadId) {
|
||||
try {
|
||||
this.drafts[this.key(channelId, threadId)] = message;
|
||||
await this.persistDraft(message, channelId, threadId);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Couldn't save draft", e);
|
||||
}
|
||||
}
|
||||
|
||||
get({ channelId }) {
|
||||
return this.drafts[channelId];
|
||||
get(channelId, threadId) {
|
||||
return this.drafts[this.key(channelId, threadId)];
|
||||
}
|
||||
|
||||
remove({ channelId }) {
|
||||
delete this.drafts[channelId];
|
||||
remove(channelId, threadId) {
|
||||
delete this.drafts[this.key(channelId, threadId)];
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.drafts = {};
|
||||
}
|
||||
|
||||
key(channelId, threadId) {
|
||||
let key = `c-${channelId}`;
|
||||
if (threadId) {
|
||||
key += `:t-${threadId}`;
|
||||
}
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
async persistDraft(message, channelId, threadId) {
|
||||
try {
|
||||
await this.chatApi.saveDraft(channelId, message.toJSONDraft(), {
|
||||
threadId,
|
||||
});
|
||||
message.draftSaved = true;
|
||||
} catch (e) {
|
||||
// We don't want to throw an error if the draft fails to save
|
||||
}
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
cancel(this?._persistHandler);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
|
||||
export default class ChatThreadComposer extends Service {
|
||||
@service chat;
|
||||
|
||||
@tracked message;
|
||||
@tracked textarea;
|
||||
|
||||
@action
|
||||
|
@ -19,28 +17,11 @@ export default class ChatThreadComposer extends Service {
|
|||
this.textarea?.blur();
|
||||
}
|
||||
|
||||
@action
|
||||
reset(thread) {
|
||||
this.message = ChatMessage.createDraftMessage(thread.channel, {
|
||||
user: this.currentUser,
|
||||
thread,
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
cancel() {
|
||||
if (this.message.editing) {
|
||||
this.reset(this.message.thread);
|
||||
} else if (this.message.inReplyTo) {
|
||||
this.message.inReplyTo = null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
edit(message) {
|
||||
this.chat.activeMessage = null;
|
||||
message.editing = true;
|
||||
this.message = message;
|
||||
message.thread.draft = message;
|
||||
this.focus({ refreshHeight: true, ensureAtEnd: true });
|
||||
}
|
||||
|
||||
|
|
|
@ -183,11 +183,11 @@ export default class Chat extends Service {
|
|||
...channelsView.direct_message_channels,
|
||||
].forEach((channelObject) => {
|
||||
const storedChannel = this.chatChannelsManager.store(channelObject);
|
||||
const storedDraft = (this.currentUser?.chat_drafts || []).find(
|
||||
const storedDrafts = (this.currentUser?.chat_drafts || []).filter(
|
||||
(draft) => draft.channel_id === storedChannel.id
|
||||
);
|
||||
|
||||
if (storedDraft) {
|
||||
storedDrafts.forEach((storedDraft) => {
|
||||
this.chatDraftsManager.add(
|
||||
ChatMessage.createDraftMessage(
|
||||
storedChannel,
|
||||
|
@ -195,9 +195,11 @@ export default class Chat extends Service {
|
|||
{ user: this.currentUser },
|
||||
JSON.parse(storedDraft.data)
|
||||
)
|
||||
)
|
||||
),
|
||||
storedDraft.channel_id,
|
||||
storedDraft.thread_id
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (channelsView.unread_thread_overview?.[storedChannel.id]) {
|
||||
storedChannel.threadsManager.unreadThreadOverview =
|
||||
|
|
|
@ -8,6 +8,7 @@ Chat::Engine.routes.draw do
|
|||
post "/channels" => "channels#create"
|
||||
put "/channels/read/" => "reads#update_all"
|
||||
put "/channels/:channel_id/read/:message_id" => "reads#update"
|
||||
post "/channels/:channel_id/drafts" => "channels_drafts#create"
|
||||
delete "/channels/:channel_id" => "channels#destroy"
|
||||
put "/channels/:channel_id" => "channels#update"
|
||||
get "/channels/:channel_id" => "channels#show"
|
||||
|
@ -38,6 +39,7 @@ Chat::Engine.routes.draw do
|
|||
get "/channels/:channel_id/threads/:thread_id" => "channel_threads#show"
|
||||
get "/channels/:channel_id/threads/:thread_id/messages" => "channel_thread_messages#index"
|
||||
put "/channels/:channel_id/threads/:thread_id/read" => "thread_reads#update"
|
||||
post "/channels/:channel_id/threads/:thread_id/drafts" => "channels_threads_drafts#create"
|
||||
put "/channels/:channel_id/threads/:thread_id/notifications-settings/me" =>
|
||||
"channel_threads_current_user_notifications_settings#update"
|
||||
|
||||
|
@ -78,7 +80,6 @@ Chat::Engine.routes.draw do
|
|||
post "/:chat_channel_id/:message_id/flag" => "chat#flag"
|
||||
post "/:chat_channel_id/quote" => "chat#quote_messages"
|
||||
put "/user_chat_enabled/:user_id" => "chat#set_user_chat_status"
|
||||
post "/drafts" => "chat#set_draft"
|
||||
post "/:chat_channel_id" => "api/channel_messages#create"
|
||||
put "/flag" => "chat#flag"
|
||||
get "/emojis" => "emojis#index"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddsThreadIdToChatDrafts < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :chat_drafts, :thread_id, :bigint
|
||||
end
|
||||
end
|
|
@ -217,8 +217,8 @@ after_initialize do
|
|||
.where(user_id: object.id)
|
||||
.order(updated_at: :desc)
|
||||
.limit(20)
|
||||
.pluck(:chat_channel_id, :data)
|
||||
.map { |row| { channel_id: row[0], data: row[1] } }
|
||||
.pluck(:chat_channel_id, :data, :thread_id)
|
||||
.map { |row| { channel_id: row[0], data: row[1], thread_id: row[2] } }
|
||||
end
|
||||
|
||||
add_to_serializer(:user_option, :chat_enabled) { object.chat_enabled }
|
||||
|
|
|
@ -113,6 +113,18 @@ module ChatSpecHelpers
|
|||
service_failed!(result) if result.failure?
|
||||
result
|
||||
end
|
||||
|
||||
def create_draft(channel, thread: nil, user: Discourse.system_user, data: { message: "draft" })
|
||||
result =
|
||||
::Chat::UpsertDraft.call(
|
||||
guardian: user.guardian,
|
||||
channel_id: channel.id,
|
||||
thread_id: thread&.id,
|
||||
data: data.to_json,
|
||||
)
|
||||
service_failed!(result) if result.failure?
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.configure do |config|
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::Api::ChannelsDraftsController do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
|
||||
before do
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
describe "success" do
|
||||
it "works" do
|
||||
post "/chat/api/channels/#{channel_1.id}/drafts", params: { data: { message: "a" } }
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user can’t create drafts" do
|
||||
before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] }
|
||||
|
||||
it "returns a 403" do
|
||||
post "/chat/api/channels/#{channel_1.id}/drafts", params: { data: { message: "a" } }
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
expect(response.parsed_body["errors"].first).to eq(I18n.t("invalid_access"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
it "returns a 404" do
|
||||
post "/chat/api/channels/-999/drafts", params: { data: { message: "a" } }
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,71 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::Api::ChannelsThreadsDraftsController do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel_1) }
|
||||
|
||||
before do
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
describe "success" do
|
||||
it "works" do
|
||||
post "/chat/api/channels/#{channel_1.id}/threads/#{thread_1.id}/drafts",
|
||||
params: {
|
||||
data: {
|
||||
message: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user can’t create drafts" do
|
||||
before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] }
|
||||
|
||||
it "returns a 403" do
|
||||
post "/chat/api/channels/#{channel_1.id}/threads/#{thread_1.id}/drafts",
|
||||
params: {
|
||||
data: {
|
||||
message: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
expect(response.parsed_body["errors"].first).to eq(I18n.t("invalid_access"))
|
||||
end
|
||||
end
|
||||
|
||||
context "when thread is not found" do
|
||||
it "returns a 404" do
|
||||
post "/chat/api/channels/#{channel_1.id}/threads/-999/drafts",
|
||||
params: {
|
||||
data: {
|
||||
message: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
it "returns a 404" do
|
||||
post "/chat/api/channels/-999/threads/#{thread_1.id}/drafts",
|
||||
params: {
|
||||
data: {
|
||||
message: "a",
|
||||
},
|
||||
}
|
||||
|
||||
expect(response.status).to eq(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -539,61 +539,6 @@ RSpec.describe Chat::ChatController do
|
|||
end
|
||||
end
|
||||
|
||||
describe "#set_draft" do
|
||||
fab!(:chat_channel) { Fabricate(:category_channel) }
|
||||
let(:dm_channel) { Fabricate(:direct_message_channel) }
|
||||
|
||||
before { sign_in(user) }
|
||||
|
||||
it "can create and destroy chat drafts" do
|
||||
expect {
|
||||
post "/chat/drafts.json", params: { chat_channel_id: chat_channel.id, data: "{}" }
|
||||
}.to change { Chat::Draft.count }.by(1)
|
||||
|
||||
expect { post "/chat/drafts.json", params: { chat_channel_id: chat_channel.id } }.to change {
|
||||
Chat::Draft.count
|
||||
}.by(-1)
|
||||
end
|
||||
|
||||
it "cannot create chat drafts for a category channel the user cannot access" do
|
||||
group = Fabricate(:group)
|
||||
private_category = Fabricate(:private_category, group: group)
|
||||
chat_channel.update!(chatable: private_category)
|
||||
|
||||
post "/chat/drafts.json", params: { chat_channel_id: chat_channel.id, data: "{}" }
|
||||
expect(response.status).to eq(403)
|
||||
|
||||
GroupUser.create!(user: user, group: group)
|
||||
expect {
|
||||
post "/chat/drafts.json", params: { chat_channel_id: chat_channel.id, data: "{}" }
|
||||
}.to change { Chat::Draft.count }.by(1)
|
||||
end
|
||||
|
||||
it "cannot create chat drafts for a direct message channel the user cannot access" do
|
||||
post "/chat/drafts.json", params: { chat_channel_id: dm_channel.id, data: "{}" }
|
||||
expect(response.status).to eq(403)
|
||||
|
||||
Chat::DirectMessageUser.create(user: user, direct_message: dm_channel.chatable)
|
||||
|
||||
expect {
|
||||
post "/chat/drafts.json", params: { chat_channel_id: dm_channel.id, data: "{}" }
|
||||
}.to change { Chat::Draft.count }.by(1)
|
||||
end
|
||||
|
||||
it "cannot create a too long chat draft" do
|
||||
SiteSetting.max_chat_draft_length = 100
|
||||
|
||||
post "/chat/drafts.json",
|
||||
params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
data: { value: "a" * (SiteSetting.max_chat_draft_length + 1) }.to_json,
|
||||
}
|
||||
|
||||
expect(response.status).to eq(422)
|
||||
expect(response.parsed_body["errors"]).to eq([I18n.t("chat.errors.draft_too_long")])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#message_link" do
|
||||
it "ensures message's channel can be seen" do
|
||||
channel = Fabricate(:category_channel, chatable: Fabricate(:category))
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Chat::UpsertDraft do
|
||||
describe described_class::Contract, type: :model do
|
||||
subject(:contract) { described_class.new(data: nil, channel_id: nil, thread_id: nil) }
|
||||
|
||||
it { is_expected.to validate_presence_of :channel_id }
|
||||
end
|
||||
|
||||
describe ".call" do
|
||||
subject(:result) { described_class.call(params) }
|
||||
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel_1) }
|
||||
|
||||
let(:guardian) { Guardian.new(current_user) }
|
||||
let(:data) { nil }
|
||||
let(:channel_id) { channel_1.id }
|
||||
let(:thread_id) { nil }
|
||||
let(:params) do
|
||||
{ guardian: guardian, channel_id: channel_id, thread_id: thread_id, data: data }
|
||||
end
|
||||
|
||||
before do
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
channel_1.add(current_user)
|
||||
end
|
||||
|
||||
context "when all steps pass" do
|
||||
it "creates draft if data provided and not existing draft" do
|
||||
params[:data] = MultiJson.dump(message: "a")
|
||||
|
||||
expect { result }.to change { Chat::Draft.count }.by(1)
|
||||
expect(Chat::Draft.last.data).to eq(params[:data])
|
||||
end
|
||||
|
||||
it "updates draft if data provided and existing draft" do
|
||||
params[:data] = MultiJson.dump(message: "a")
|
||||
|
||||
described_class.call(**params)
|
||||
|
||||
params[:data] = MultiJson.dump(message: "b")
|
||||
|
||||
expect { result }.to_not change { Chat::Draft.count }
|
||||
expect(Chat::Draft.last.data).to eq(params[:data])
|
||||
end
|
||||
|
||||
it "destroys draft if empty data provided and existing draft" do
|
||||
params[:data] = MultiJson.dump(message: "a")
|
||||
|
||||
described_class.call(**params)
|
||||
|
||||
params[:data] = ""
|
||||
|
||||
expect { result }.to change { Chat::Draft.count }.by(-1)
|
||||
end
|
||||
|
||||
it "destroys draft if no data provided and existing draft" do
|
||||
params[:data] = MultiJson.dump(message: "a")
|
||||
|
||||
described_class.call(**params)
|
||||
|
||||
params[:data] = nil
|
||||
|
||||
expect { result }.to change { Chat::Draft.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user can’t chat" do
|
||||
before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] }
|
||||
|
||||
it { is_expected.to fail_a_policy(:can_upsert_draft) }
|
||||
end
|
||||
|
||||
context "when user can’t access the channel" do
|
||||
fab!(:channel_1) { Fabricate(:private_category_channel) }
|
||||
|
||||
it { is_expected.to fail_a_policy(:can_upsert_draft) }
|
||||
end
|
||||
|
||||
context "when channel is not found" do
|
||||
let(:channel_id) { -999 }
|
||||
|
||||
it { is_expected.to fail_to_find_a_model(:channel) }
|
||||
end
|
||||
|
||||
context "when thread is not found" do
|
||||
let(:thread_id) { -999 }
|
||||
|
||||
it { is_expected.to fail_a_step(:check_thread_exists) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,19 +13,13 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
|
||||
before { chat_system_bootstrap }
|
||||
|
||||
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
|
||||
|
||||
before do
|
||||
create_draft(channel_1, user: current_user)
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
@ -39,16 +33,11 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
context "when loading another channel and back" do
|
||||
fab!(:channel_2) { Fabricate(:chat_channel) }
|
||||
|
||||
fab!(:draft_2) do
|
||||
Chat::Draft.create!(
|
||||
chat_channel: channel_2,
|
||||
user: current_user,
|
||||
data: { message: "draft2" }.to_json,
|
||||
)
|
||||
before do
|
||||
create_draft(channel_2, user: current_user, data: { message: "draft2" })
|
||||
channel_2.add(current_user)
|
||||
end
|
||||
|
||||
before { channel_2.add(current_user) }
|
||||
|
||||
it "loads the correct drafts" do
|
||||
chat_page.visit_channel(channel_1)
|
||||
|
||||
|
@ -64,12 +53,16 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
end
|
||||
end
|
||||
|
||||
context "with editing" do
|
||||
fab!(:draft_1) do
|
||||
Chat::Draft.create!(
|
||||
chat_channel: channel_1,
|
||||
context "when editing" do
|
||||
before do
|
||||
create_draft(
|
||||
channel_1,
|
||||
user: current_user,
|
||||
data: { message: message_1.message, id: message_1.id, editing: true }.to_json,
|
||||
data: {
|
||||
message: message_1.message,
|
||||
id: message_1.id,
|
||||
editing: true,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -103,12 +96,8 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
)
|
||||
end
|
||||
|
||||
fab!(:draft_1) do
|
||||
Chat::Draft.create!(
|
||||
chat_channel: channel_1,
|
||||
user: current_user,
|
||||
data: { message: "draft", uploads: [upload_1] }.to_json,
|
||||
)
|
||||
before do
|
||||
create_draft(channel_1, user: current_user, data: { message: "draft", uploads: [upload_1] })
|
||||
end
|
||||
|
||||
it "loads the draft with the upload" do
|
||||
|
@ -120,9 +109,9 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
end
|
||||
|
||||
context "when replying" do
|
||||
fab!(:draft_1) do
|
||||
Chat::Draft.create!(
|
||||
chat_channel: channel_1,
|
||||
before do
|
||||
create_draft(
|
||||
channel_1,
|
||||
user: current_user,
|
||||
data: {
|
||||
message: "draft",
|
||||
|
@ -136,7 +125,7 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
username: message_1.user.username,
|
||||
},
|
||||
},
|
||||
}.to_json,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -149,4 +138,109 @@ RSpec.describe "Chat composer draft", type: :system do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when loading a thread with a draft" do
|
||||
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:thread_1) { Fabricate(:chat_thread, channel: channel_1) }
|
||||
|
||||
before do
|
||||
create_draft(channel_1, user: current_user, thread: thread_1)
|
||||
channel_1.add(current_user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
it "loads the draft" do
|
||||
chat_page.visit_thread(thread_1)
|
||||
|
||||
expect(thread_page.composer.value).to eq("draft")
|
||||
end
|
||||
|
||||
context "when loading another channel and back" do
|
||||
fab!(:channel_2) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
fab!(:thread_2) { Fabricate(:chat_thread, channel: channel_2) }
|
||||
|
||||
before do
|
||||
create_draft(channel_2, user: current_user, thread: thread_2, data: { message: "draft2" })
|
||||
channel_2.add(current_user)
|
||||
end
|
||||
|
||||
it "loads the correct drafts" do
|
||||
chat_page.visit_thread(thread_1)
|
||||
|
||||
expect(thread_page.composer.value).to eq("draft")
|
||||
|
||||
chat_page.visit_thread(thread_2)
|
||||
|
||||
expect(thread_page.composer.value).to eq("draft2")
|
||||
|
||||
chat_page.visit_thread(thread_1)
|
||||
|
||||
expect(thread_page.composer.value).to eq("draft")
|
||||
end
|
||||
end
|
||||
|
||||
context "when editing" do
|
||||
before do
|
||||
create_draft(
|
||||
channel_1,
|
||||
user: current_user,
|
||||
thread: thread_1,
|
||||
data: {
|
||||
message: message_1.message,
|
||||
id: message_1.id,
|
||||
editing: true,
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "loads the draft with the editing state" do
|
||||
chat_page.visit_thread(thread_1)
|
||||
|
||||
expect(thread_page.composer).to be_editing_message(message_1)
|
||||
end
|
||||
|
||||
context "when canceling editing" do
|
||||
it "resets the draft" do
|
||||
chat_page.visit_thread(thread_1)
|
||||
thread_page.composer.message_details.cancel_edit
|
||||
|
||||
expect(thread_page.composer).to be_blank
|
||||
expect(thread_page.composer).to have_unsaved_draft
|
||||
expect(thread_page.composer).to have_saved_draft
|
||||
end
|
||||
end
|
||||
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
|
||||
|
||||
before do
|
||||
create_draft(
|
||||
channel_1,
|
||||
user: current_user,
|
||||
thread: thread_1,
|
||||
data: {
|
||||
message: "draft",
|
||||
uploads: [upload_1],
|
||||
},
|
||||
)
|
||||
end
|
||||
|
||||
it "loads the draft with the upload" do
|
||||
chat_page.visit_thread(thread_1)
|
||||
|
||||
expect(thread_page.composer.value).to eq("draft")
|
||||
expect(page).to have_selector(".chat-composer-upload--image", count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export default function applyDefaultHandlers(helpers) {
|
||||
this.post("/chat/api/channels/:channel_id/drafts", () =>
|
||||
helpers.response({})
|
||||
);
|
||||
this.post("/chat/api/channels/:channel_id/threads/:thread_id/drafts", () =>
|
||||
helpers.response({})
|
||||
);
|
||||
}
|
|
@ -12,36 +12,24 @@ module(
|
|||
this.subject = getOwner(this).lookup("service:chat-drafts-manager");
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
this.subject.reset();
|
||||
});
|
||||
|
||||
test("storing and retrieving message", function (assert) {
|
||||
test("storing and retrieving message", async function (assert) {
|
||||
const message1 = fabricators.message();
|
||||
this.subject.add(message1);
|
||||
|
||||
assert.strictEqual(
|
||||
this.subject.get({ channelId: message1.channel.id }),
|
||||
message1
|
||||
);
|
||||
await this.subject.add(message1, message1.channel.id);
|
||||
|
||||
assert.strictEqual(this.subject.get(message1.channel.id), message1);
|
||||
|
||||
const message2 = fabricators.message();
|
||||
this.subject.add(message2);
|
||||
|
||||
assert.strictEqual(
|
||||
this.subject.get({ channelId: message2.channel.id }),
|
||||
message2
|
||||
);
|
||||
await this.subject.add(message2, message2.channel.id);
|
||||
|
||||
assert.strictEqual(this.subject.get(message2.channel.id), message2);
|
||||
});
|
||||
|
||||
test("stores only chat messages", function (assert) {
|
||||
assert.throws(function () {
|
||||
this.subject.add({ foo: "bar" });
|
||||
}, /instance of ChatMessage/);
|
||||
});
|
||||
test("#reset", async function (assert) {
|
||||
const message = fabricators.message();
|
||||
|
||||
test("#reset", function (assert) {
|
||||
this.subject.add(fabricators.message());
|
||||
await this.subject.add(message, message.channel.id);
|
||||
|
||||
assert.strictEqual(Object.keys(this.subject.drafts).length, 1);
|
||||
|
||||
|
|
Loading…
Reference in New Issue