mirror of
https://github.com/discourse/discourse.git
synced 2025-03-25 12:31:36 +08:00
UX: implements draft threads (#21361)
This commit implements all the necessary logic to create thread seamlessly. For this it relies on the same logic used for messages and generates a `staged-id`(using the format: `staged-thread-CHANNEL_ID-MESSAGE_ID` which is used to re-conciliate state client sides once the thread has been persisted on the backend. Part of this change the client side is now always using real thread and channel objects instead of sometimes relying on a flat `threadId` or `channelId`. This PR also brings three UX changes: - thread starts from top - number of buttons on message actions is dependent of the width of the enclosing container - <kbd>shift + ArrowUp</kbd> will reply to the last message
This commit is contained in:
parent
fe10c61dfa
commit
187b59d376
@ -107,6 +107,7 @@ module Chat
|
|||||||
staged_id: params[:staged_id],
|
staged_id: params[:staged_id],
|
||||||
upload_ids: params[:upload_ids],
|
upload_ids: params[:upload_ids],
|
||||||
thread_id: params[:thread_id],
|
thread_id: params[:thread_id],
|
||||||
|
staged_thread_id: params[:staged_thread_id],
|
||||||
)
|
)
|
||||||
|
|
||||||
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
|
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
|
||||||
|
@ -14,7 +14,7 @@ module Chat
|
|||||||
"#{root_message_bus_channel(chat_channel_id)}/thread/#{thread_id}"
|
"#{root_message_bus_channel(chat_channel_id)}/thread/#{thread_id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.calculate_publish_targets(channel, message)
|
def self.calculate_publish_targets(channel, message, staged_thread_id: nil)
|
||||||
return [root_message_bus_channel(channel.id)] if !allow_publish_to_thread?(channel)
|
return [root_message_bus_channel(channel.id)] if !allow_publish_to_thread?(channel)
|
||||||
|
|
||||||
if message.thread_om?
|
if message.thread_om?
|
||||||
@ -22,8 +22,10 @@ module Chat
|
|||||||
root_message_bus_channel(channel.id),
|
root_message_bus_channel(channel.id),
|
||||||
thread_message_bus_channel(channel.id, message.thread_id),
|
thread_message_bus_channel(channel.id, message.thread_id),
|
||||||
]
|
]
|
||||||
elsif message.thread_reply?
|
elsif staged_thread_id || message.thread_reply?
|
||||||
[thread_message_bus_channel(channel.id, message.thread_id)]
|
targets = [thread_message_bus_channel(channel.id, message.thread_id)]
|
||||||
|
targets << thread_message_bus_channel(channel.id, staged_thread_id) if staged_thread_id
|
||||||
|
targets
|
||||||
else
|
else
|
||||||
[root_message_bus_channel(channel.id)]
|
[root_message_bus_channel(channel.id)]
|
||||||
end
|
end
|
||||||
@ -33,12 +35,16 @@ module Chat
|
|||||||
SiteSetting.enable_experimental_chat_threaded_discussions && channel.threading_enabled
|
SiteSetting.enable_experimental_chat_threaded_discussions && channel.threading_enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_new!(chat_channel, chat_message, staged_id)
|
def self.publish_new!(chat_channel, chat_message, staged_id, staged_thread_id: nil)
|
||||||
message_bus_targets = calculate_publish_targets(chat_channel, chat_message)
|
message_bus_targets =
|
||||||
|
calculate_publish_targets(chat_channel, chat_message, staged_thread_id: staged_thread_id)
|
||||||
publish_to_targets!(
|
publish_to_targets!(
|
||||||
message_bus_targets,
|
message_bus_targets,
|
||||||
chat_channel,
|
chat_channel,
|
||||||
serialize_message_with_type(chat_message, :sent).merge(staged_id: staged_id),
|
serialize_message_with_type(chat_message, :sent).merge(
|
||||||
|
staged_id: staged_id,
|
||||||
|
staged_thread_id: staged_thread_id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# NOTE: This means that the read count is only updated in the client
|
# NOTE: This means that the read count is only updated in the client
|
||||||
@ -70,8 +76,15 @@ module Chat
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_thread_created!(chat_channel, chat_message)
|
def self.publish_thread_created!(chat_channel, chat_message, thread_id, staged_thread_id)
|
||||||
publish_to_channel!(chat_channel, serialize_message_with_type(chat_message, :thread_created))
|
publish_to_channel!(
|
||||||
|
chat_channel,
|
||||||
|
serialize_message_with_type(
|
||||||
|
chat_message,
|
||||||
|
:thread_created,
|
||||||
|
{ thread_id: thread_id, staged_thread_id: staged_thread_id },
|
||||||
|
),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.publish_processed!(chat_message)
|
def self.publish_processed!(chat_message)
|
||||||
@ -215,11 +228,12 @@ module Chat
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.serialize_message_with_type(chat_message, type)
|
def self.serialize_message_with_type(chat_message, type, options = {})
|
||||||
Chat::MessageSerializer
|
Chat::MessageSerializer
|
||||||
.new(chat_message, { scope: anonymous_guardian, root: :chat_message })
|
.new(chat_message, { scope: anonymous_guardian, root: :chat_message })
|
||||||
.as_json
|
.as_json
|
||||||
.merge(type: type)
|
.merge(type: type)
|
||||||
|
.merge(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.user_tracking_state_message_bus_channel(user_id)
|
def self.user_tracking_state_message_bus_channel(user_id)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { capitalize } from "@ember/string";
|
import { capitalize } from "@ember/string";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||||
import Component from "@glimmer/component";
|
import Component from "@glimmer/component";
|
||||||
import { bind, debounce } from "discourse-common/utils/decorators";
|
import { bind, debounce } from "discourse-common/utils/decorators";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
@ -352,7 +353,13 @@ export default class ChatLivePane extends Component {
|
|||||||
messageData.newest = true;
|
messageData.newest = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
messages.push(ChatMessage.create(channel, messageData));
|
const message = ChatMessage.create(channel, messageData);
|
||||||
|
|
||||||
|
if (messageData.thread_id) {
|
||||||
|
message.thread = new ChatThread(channel, { id: messageData.thread_id });
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.push(message);
|
||||||
});
|
});
|
||||||
|
|
||||||
return [messages, results.meta];
|
return [messages, results.meta];
|
||||||
@ -548,7 +555,11 @@ export default class ChatLivePane extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
|
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
|
||||||
const stagedMessage = handleStagedMessage(this.#messagesManager, data);
|
const stagedMessage = handleStagedMessage(
|
||||||
|
this.args.channel,
|
||||||
|
this.#messagesManager,
|
||||||
|
data
|
||||||
|
);
|
||||||
if (stagedMessage) {
|
if (stagedMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
{{did-update this.didUpdateMessage this.currentMessage}}
|
{{did-update this.didUpdateMessage this.currentMessage}}
|
||||||
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
|
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
|
||||||
{{did-insert this.setupAppEvents}}
|
{{did-insert this.setupAppEvents}}
|
||||||
{{will-destroy this.teardownAppEvents}}
|
{{will-destroy this.teardown}}
|
||||||
{{will-destroy this.cancelPersistDraft}}
|
{{will-destroy this.cancelPersistDraft}}
|
||||||
>
|
>
|
||||||
<div class="chat-composer__outer-container">
|
<div class="chat-composer__outer-container">
|
||||||
|
@ -17,6 +17,8 @@ import I18n from "I18n";
|
|||||||
import { translations } from "pretty-text/emoji/data";
|
import { translations } from "pretty-text/emoji/data";
|
||||||
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
||||||
import { isEmpty, isPresent } from "@ember/utils";
|
import { isEmpty, isPresent } from "@ember/utils";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
|
||||||
export default class ChatComposer extends Component {
|
export default class ChatComposer extends Component {
|
||||||
@service capabilities;
|
@service capabilities;
|
||||||
@ -39,7 +41,10 @@ export default class ChatComposer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get shouldRenderMessageDetails() {
|
get shouldRenderMessageDetails() {
|
||||||
return this.currentMessage?.editing || this.currentMessage?.inReplyTo;
|
return (
|
||||||
|
this.currentMessage?.editing ||
|
||||||
|
(this.context === "channel" && this.currentMessage?.inReplyTo)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get inlineButtons() {
|
get inlineButtons() {
|
||||||
@ -70,6 +75,18 @@ export default class ChatComposer extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
sendMessage(raw) {
|
||||||
|
const message = ChatMessage.createDraftMessage(this.args.channel, {
|
||||||
|
user: this.currentUser,
|
||||||
|
message: raw,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.args.onSendMessage(message);
|
||||||
|
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
persistDraft() {}
|
persistDraft() {}
|
||||||
|
|
||||||
@ -133,13 +150,14 @@ export default class ChatComposer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
teardownAppEvents() {
|
teardown() {
|
||||||
this.appEvents.off("chat:modify-selection", this, "modifySelection");
|
this.appEvents.off("chat:modify-selection", this, "modifySelection");
|
||||||
this.appEvents.off(
|
this.appEvents.off(
|
||||||
"chat:open-insert-link-modal",
|
"chat:open-insert-link-modal",
|
||||||
this,
|
this,
|
||||||
"openInsertLinkModal"
|
"openInsertLinkModal"
|
||||||
);
|
);
|
||||||
|
this.pane.sending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -221,7 +239,7 @@ export default class ChatComposer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reportReplyingPresence() {
|
reportReplyingPresence() {
|
||||||
if (!this.args.channel) {
|
if (!this.args.channel || !this.currentMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,9 +323,13 @@ export default class ChatComposer extends Component {
|
|||||||
!this.hasContent &&
|
!this.hasContent &&
|
||||||
!this.currentMessage.editing
|
!this.currentMessage.editing
|
||||||
) {
|
) {
|
||||||
const editableMessage = this.pane?.lastCurrentUserMessage;
|
if (event.shiftKey) {
|
||||||
if (editableMessage) {
|
this.composer.replyTo(this.pane?.lastMessage);
|
||||||
this.composer.editMessage(editableMessage);
|
} else {
|
||||||
|
const editableMessage = this.pane?.lastCurrentUserMessage;
|
||||||
|
if (editableMessage) {
|
||||||
|
this.composer.editMessage(editableMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
{{did-update this.fetchChannelAndThread @params.threadId}}
|
{{did-update this.fetchChannelAndThread @params.threadId}}
|
||||||
>
|
>
|
||||||
{{#if this.chat.activeChannel.activeThread}}
|
{{#if this.chat.activeChannel.activeThread}}
|
||||||
<ChatThread />
|
<ChatThread @thread={{this.chat.activeChannel.activeThread}} />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
@ -1,13 +1,16 @@
|
|||||||
{{#if (and this.site.desktopView this.chat.activeMessage.model.id)}}
|
{{#if (and this.site.desktopView this.chat.activeMessage.model.id)}}
|
||||||
<div
|
<div
|
||||||
{{did-insert this.setupPopper}}
|
{{did-insert this.setup}}
|
||||||
{{did-update this.setupPopper this.chat.activeMessage.model.id}}
|
{{did-update this.setup this.chat.activeMessage.model.id}}
|
||||||
{{will-destroy this.teardownPopper}}
|
{{will-destroy this.teardown}}
|
||||||
class="chat-message-actions-container"
|
class={{concat-class
|
||||||
|
"chat-message-actions-container"
|
||||||
|
(concat "is-size-" this.size)
|
||||||
|
}}
|
||||||
data-id={{this.message.id}}
|
data-id={{this.message.id}}
|
||||||
>
|
>
|
||||||
<div class="chat-message-actions">
|
<div class="chat-message-actions">
|
||||||
{{#if this.chatStateManager.isFullPageActive}}
|
{{#if this.shouldRenderFavoriteReactions}}
|
||||||
{{#each
|
{{#each
|
||||||
this.messageInteractor.emojiReactions
|
this.messageInteractor.emojiReactions
|
||||||
key="emoji"
|
key="emoji"
|
||||||
@ -50,7 +53,12 @@
|
|||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.messageInteractor.secondaryButtons.length}}
|
{{#if
|
||||||
|
(and
|
||||||
|
this.messageInteractor.message
|
||||||
|
this.messageInteractor.secondaryButtons.length
|
||||||
|
)
|
||||||
|
}}
|
||||||
<DropdownSelectBox
|
<DropdownSelectBox
|
||||||
@class="more-buttons"
|
@class="more-buttons"
|
||||||
@options={{hash icon="ellipsis-v" placement="left"}}
|
@options={{hash icon="ellipsis-v" placement="left"}}
|
||||||
|
@ -6,15 +6,20 @@ import { schedule } from "@ember/runloop";
|
|||||||
import { createPopper } from "@popperjs/core";
|
import { createPopper } from "@popperjs/core";
|
||||||
import chatMessageContainer from "discourse/plugins/chat/discourse/lib/chat-message-container";
|
import chatMessageContainer from "discourse/plugins/chat/discourse/lib/chat-message-container";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
import { tracked } from "@glimmer/tracking";
|
||||||
|
|
||||||
const MSG_ACTIONS_VERTICAL_PADDING = -10;
|
const MSG_ACTIONS_VERTICAL_PADDING = -10;
|
||||||
|
const FULL = "full";
|
||||||
|
const REDUCED = "reduced";
|
||||||
|
const REDUCED_WIDTH_THRESHOLD = 500;
|
||||||
|
|
||||||
export default class ChatMessageActionsDesktop extends Component {
|
export default class ChatMessageActionsDesktop extends Component {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service chatStateManager;
|
|
||||||
@service chatEmojiPickerManager;
|
@service chatEmojiPickerManager;
|
||||||
@service site;
|
@service site;
|
||||||
|
|
||||||
|
@tracked size = FULL;
|
||||||
|
|
||||||
popper = null;
|
popper = null;
|
||||||
|
|
||||||
get message() {
|
get message() {
|
||||||
@ -35,8 +40,12 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get shouldRenderFavoriteReactions() {
|
||||||
|
return this.size === FULL;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
setupPopper(element) {
|
setup(element) {
|
||||||
this.popper?.destroy();
|
this.popper?.destroy();
|
||||||
|
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
@ -45,6 +54,10 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||||||
this.context
|
this.context
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const viewport = messageContainer.closest(".popper-viewport");
|
||||||
|
this.size =
|
||||||
|
viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL;
|
||||||
|
|
||||||
if (!messageContainer) {
|
if (!messageContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -57,7 +70,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||||||
name: "flip",
|
name: "flip",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
options: {
|
options: {
|
||||||
boundary: messageContainer.closest(".popper-viewport"),
|
boundary: viewport,
|
||||||
fallbackPlacements: ["bottom-end"],
|
fallbackPlacements: ["bottom-end"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -73,7 +86,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
teardownPopper() {
|
teardown() {
|
||||||
this.popper?.destroy();
|
this.popper?.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@
|
|||||||
@icon="discourse-emojis"
|
@icon="discourse-emojis"
|
||||||
@title="chat.react"
|
@title="chat.react"
|
||||||
@forwardEvent={{true}}
|
@forwardEvent={{true}}
|
||||||
|
data-id="react"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
@ -71,6 +72,7 @@
|
|||||||
<DButton
|
<DButton
|
||||||
@class="btn-flat bookmark-btn"
|
@class="btn-flat bookmark-btn"
|
||||||
@action={{action this.actAndCloseMenu "toggleBookmark"}}
|
@action={{action this.actAndCloseMenu "toggleBookmark"}}
|
||||||
|
data-id="bookmark"
|
||||||
>
|
>
|
||||||
<BookmarkIcon @bookmark={{this.message.bookmark}} />
|
<BookmarkIcon @bookmark={{this.message.bookmark}} />
|
||||||
</DButton>
|
</DButton>
|
||||||
@ -82,6 +84,7 @@
|
|||||||
@action={{action this.actAndCloseMenu "reply"}}
|
@action={{action this.actAndCloseMenu "reply"}}
|
||||||
@icon="reply"
|
@icon="reply"
|
||||||
@title="chat.reply"
|
@title="chat.reply"
|
||||||
|
data-id="reply"
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -16,7 +16,7 @@ export default class ChatMessageInReplyToIndicator extends Component {
|
|||||||
if (this.hasThread) {
|
if (this.hasThread) {
|
||||||
return [
|
return [
|
||||||
...this.args.message.channel.routeModels,
|
...this.args.message.channel.routeModels,
|
||||||
this.args.message.threadId,
|
this.args.message.thread.id,
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
@ -29,7 +29,7 @@ export default class ChatMessageInReplyToIndicator extends Component {
|
|||||||
get hasThread() {
|
get hasThread() {
|
||||||
return (
|
return (
|
||||||
this.args.message?.channel?.threadingEnabled &&
|
this.args.message?.channel?.threadingEnabled &&
|
||||||
this.args.message?.threadId
|
this.args.message?.thread?.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<LinkTo
|
<LinkTo
|
||||||
@route="chat.channel.thread"
|
@route="chat.channel.thread"
|
||||||
@models={{@message.threadRouteModels}}
|
@models={{@message.thread.routeModels}}
|
||||||
class="chat-message-thread-indicator"
|
class="chat-message-thread-indicator"
|
||||||
>
|
>
|
||||||
<span class="chat-message-thread-indicator__replies-count">
|
<span class="chat-message-thread-indicator__replies-count">
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
(if @message.highlighted "highlighted")
|
(if @message.highlighted "highlighted")
|
||||||
}}
|
}}
|
||||||
data-id={{@message.id}}
|
data-id={{@message.id}}
|
||||||
data-thread-id={{@message.threadId}}
|
data-thread-id={{@message.thread.id}}
|
||||||
{{chat/track-message
|
{{chat/track-message
|
||||||
(hash
|
(hash
|
||||||
didEnterViewport=(fn @messageDidEnterViewport @message)
|
didEnterViewport=(fn @messageDidEnterViewport @message)
|
||||||
|
@ -282,15 +282,15 @@ export default class ChatMessage extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get threadingEnabled() {
|
get threadingEnabled() {
|
||||||
return this.args.channel?.threadingEnabled && this.args.message?.threadId;
|
return this.args.channel?.threadingEnabled && !!this.args.message?.thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
get showThreadIndicator() {
|
get showThreadIndicator() {
|
||||||
return (
|
return (
|
||||||
this.args.context !== MESSAGE_CONTEXT_THREAD &&
|
this.args.context !== MESSAGE_CONTEXT_THREAD &&
|
||||||
this.threadingEnabled &&
|
this.threadingEnabled &&
|
||||||
this.args.message?.threadId !==
|
this.args.message?.thread &&
|
||||||
this.args.message?.previousMessage?.threadId
|
this.args.message?.threadReplyCount > 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,7 +352,7 @@ export default class ChatMessage extends Component {
|
|||||||
inviteMentioned() {
|
inviteMentioned() {
|
||||||
const userIds = this.mentionWarning.without_membership.mapBy("id");
|
const userIds = this.mentionWarning.without_membership.mapBy("id");
|
||||||
|
|
||||||
ajax(`/chat/${this.args.message.channelId}/invite`, {
|
ajax(`/chat/${this.args.message.channel.id}/invite`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
data: { user_ids: userIds, chat_message_id: this.args.message.id },
|
data: { user_ids: userIds, chat_message_id: this.args.message.id },
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
data-id={{this.thread.id}}
|
data-id={{this.thread.id}}
|
||||||
{{did-insert this.setUploadDropZone}}
|
{{did-insert this.setUploadDropZone}}
|
||||||
{{did-insert this.subscribeToUpdates}}
|
{{did-insert this.subscribeToUpdates}}
|
||||||
{{did-insert this.loadMessages}}
|
|
||||||
{{did-update this.subscribeToUpdates this.thread.id}}
|
{{did-update this.subscribeToUpdates this.thread.id}}
|
||||||
{{did-update this.loadMessages this.thread.id}}
|
{{did-insert this.loadMessages}}
|
||||||
|
{{did-update this.loadMessages this.thread}}
|
||||||
{{did-insert this.setupMessage}}
|
{{did-insert this.setupMessage}}
|
||||||
{{will-destroy this.unsubscribeFromUpdates}}
|
{{will-destroy this.unsubscribeFromUpdates}}
|
||||||
>
|
>
|
||||||
@ -42,7 +42,7 @@
|
|||||||
@context="thread"
|
@context="thread"
|
||||||
/>
|
/>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{#if (or this.loading this.loadingMoreFuture)}}
|
{{#if this.loading}}
|
||||||
<ChatSkeleton />
|
<ChatSkeleton />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,7 @@ import { bind, debounce } from "discourse-common/utils/decorators";
|
|||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import { schedule } from "@ember/runloop";
|
import { schedule } from "@ember/runloop";
|
||||||
import discourseLater from "discourse-common/lib/later";
|
import discourseLater from "discourse-common/lib/later";
|
||||||
|
import { resetIdle } from "discourse/lib/desktop-notifications";
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
|
|
||||||
@ -25,22 +26,16 @@ export default class ChatThreadPanel extends Component {
|
|||||||
@service capabilities;
|
@service capabilities;
|
||||||
|
|
||||||
@tracked loading;
|
@tracked loading;
|
||||||
@tracked loadingMorePast;
|
|
||||||
@tracked uploadDropZone;
|
@tracked uploadDropZone;
|
||||||
|
|
||||||
scrollable = null;
|
scrollable = null;
|
||||||
|
|
||||||
get thread() {
|
get thread() {
|
||||||
return this.channel.activeThread;
|
return this.args.thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
get channel() {
|
get channel() {
|
||||||
return this.chat.activeChannel;
|
return this.thread?.channel;
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
subscribeToUpdates() {
|
|
||||||
this.chatChannelThreadPaneSubscriptionsManager.subscribe(this.thread);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -56,6 +51,11 @@ export default class ChatThreadPanel extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
subscribeToUpdates() {
|
||||||
|
this.chatChannelThreadPaneSubscriptionsManager.subscribe(this.thread);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
unsubscribeFromUpdates() {
|
unsubscribeFromUpdates() {
|
||||||
this.chatChannelThreadPaneSubscriptionsManager.unsubscribe();
|
this.chatChannelThreadPaneSubscriptionsManager.unsubscribe();
|
||||||
@ -69,17 +69,7 @@ export default class ChatThreadPanel extends Component {
|
|||||||
@action
|
@action
|
||||||
loadMessages() {
|
loadMessages() {
|
||||||
this.thread.messagesManager.clearMessages();
|
this.thread.messagesManager.clearMessages();
|
||||||
|
|
||||||
if (this.args.targetMessageId) {
|
|
||||||
this.requestedTargetMessageId = parseInt(this.args.targetMessageId, 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO (martin) Loading/scrolling to selected message
|
|
||||||
// this.highlightOrFetchMessage(this.requestedTargetMessageId);
|
|
||||||
// if (this.requestedTargetMessageId) {
|
|
||||||
// } else {
|
|
||||||
this.fetchMessages();
|
this.fetchMessages();
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -97,21 +87,14 @@ export default class ChatThreadPanel extends Component {
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadingMorePast = true;
|
if (this.thread.staged) {
|
||||||
|
this.thread.messagesManager.addMessages([this.thread.originalMessage]);
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const findArgs = { pageSize: PAGE_SIZE };
|
const findArgs = { pageSize: PAGE_SIZE, threadId: this.thread.id };
|
||||||
|
|
||||||
// TODO (martin) Find arguments for last read etc.
|
|
||||||
// const fetchingFromLastRead = !options.fetchFromLastMessage;
|
|
||||||
// if (this.requestedTargetMessageId) {
|
|
||||||
// findArgs["targetMessageId"] = this.requestedTargetMessageId;
|
|
||||||
// } else if (fetchingFromLastRead) {
|
|
||||||
// findArgs["targetMessageId"] = this._getLastReadId();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
findArgs.threadId = this.thread.id;
|
|
||||||
|
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.messages(this.channel.id, findArgs)
|
.messages(this.channel.id, findArgs)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
@ -123,23 +106,13 @@ export default class ChatThreadPanel extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [messages, meta] = this.afterFetchCallback(this.channel, results);
|
const [messages, meta] = this.afterFetchCallback(
|
||||||
|
this.channel,
|
||||||
|
this.thread,
|
||||||
|
results
|
||||||
|
);
|
||||||
this.thread.messagesManager.addMessages(messages);
|
this.thread.messagesManager.addMessages(messages);
|
||||||
|
|
||||||
// TODO (martin) details needed for thread??
|
|
||||||
this.thread.details = meta;
|
this.thread.details = meta;
|
||||||
|
|
||||||
// TODO (martin) Scrolling to particular messages
|
|
||||||
// if (this.requestedTargetMessageId) {
|
|
||||||
// this.scrollToMessage(findArgs["targetMessageId"], {
|
|
||||||
// highlight: true,
|
|
||||||
// });
|
|
||||||
// } else if (fetchingFromLastRead) {
|
|
||||||
// this.scrollToMessage(findArgs["targetMessageId"]);
|
|
||||||
// } else if (messages.length) {
|
|
||||||
// this.scrollToMessage(messages.lastObject.id);
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
this.markThreadAsRead();
|
this.markThreadAsRead();
|
||||||
})
|
})
|
||||||
.catch(this.#handleErrors)
|
.catch(this.#handleErrors)
|
||||||
@ -148,16 +121,12 @@ export default class ChatThreadPanel extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.requestedTargetMessageId = null;
|
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.loadingMorePast = false;
|
|
||||||
|
|
||||||
// this.fillPaneAttempt();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
afterFetchCallback(channel, results) {
|
afterFetchCallback(channel, thread, results) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
results.chat_messages.forEach((messageData) => {
|
results.chat_messages.forEach((messageData) => {
|
||||||
@ -170,13 +139,10 @@ export default class ChatThreadPanel extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.requestedTargetMessageId === messageData.id) {
|
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
||||||
messageData.expanded = !messageData.hidden;
|
const message = ChatMessage.create(channel, messageData);
|
||||||
} else {
|
message.thread = thread;
|
||||||
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
messages.push(message);
|
||||||
}
|
|
||||||
|
|
||||||
messages.push(ChatMessage.create(channel, messageData));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return [messages, results.meta];
|
return [messages, results.meta];
|
||||||
@ -191,6 +157,8 @@ export default class ChatThreadPanel extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
onSendMessage(message) {
|
onSendMessage(message) {
|
||||||
|
resetIdle();
|
||||||
|
|
||||||
if (message.editing) {
|
if (message.editing) {
|
||||||
this.#sendEditMessage(message);
|
this.#sendEditMessage(message);
|
||||||
} else {
|
} else {
|
||||||
@ -200,7 +168,7 @@ export default class ChatThreadPanel extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
resetComposer() {
|
resetComposer() {
|
||||||
this.chatChannelThreadComposer.reset(this.channel);
|
this.chatChannelThreadComposer.reset(this.channel, this.thread);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -209,34 +177,27 @@ export default class ChatThreadPanel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sendNewMessage(message) {
|
#sendNewMessage(message) {
|
||||||
// TODO (martin) For desktop notifications
|
message.thread = this.thread;
|
||||||
// resetIdle()
|
|
||||||
if (this.chatChannelThreadPane.sending) {
|
if (this.chatChannelThreadPane.sending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.chatChannelThreadPane.sending = true;
|
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.
|
|
||||||
|
|
||||||
this.thread.stageMessage(message);
|
this.thread.stageMessage(message);
|
||||||
const stagedMessage = message;
|
const stagedMessage = message;
|
||||||
this.resetComposer();
|
this.resetComposer();
|
||||||
this.thread.messagesManager.addMessages([stagedMessage]);
|
this.thread.messagesManager.addMessages([stagedMessage]);
|
||||||
|
|
||||||
// TODO (martin) Scrolling!!
|
|
||||||
// if (!this.channel.canLoadMoreFuture) {
|
|
||||||
// this.scrollToBottom();
|
|
||||||
// }
|
|
||||||
|
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.sendMessage(this.channel.id, {
|
.sendMessage(this.channel.id, {
|
||||||
message: stagedMessage.message,
|
message: stagedMessage.message,
|
||||||
in_reply_to_id: stagedMessage.inReplyTo?.id,
|
in_reply_to_id: stagedMessage.inReplyTo?.id,
|
||||||
staged_id: stagedMessage.id,
|
staged_id: stagedMessage.id,
|
||||||
upload_ids: stagedMessage.uploads.map((upload) => upload.id),
|
upload_ids: stagedMessage.uploads.map((upload) => upload.id),
|
||||||
thread_id: stagedMessage.threadId,
|
thread_id: this.thread.staged ? null : stagedMessage.thread.id,
|
||||||
|
staged_thread_id: this.thread.staged ? stagedMessage.thread.id : null,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.scrollToBottom();
|
this.scrollToBottom();
|
||||||
@ -264,7 +225,7 @@ export default class ChatThreadPanel extends Component {
|
|||||||
this.resetComposer();
|
this.resetComposer();
|
||||||
|
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.editMessage(message.channelId, message.id, data)
|
.editMessage(message.channel.id, message.id, data)
|
||||||
.catch(popupAjaxError)
|
.catch(popupAjaxError)
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
this.chatChannelThreadPane.sending = false;
|
this.chatChannelThreadPane.sending = false;
|
||||||
@ -280,9 +241,9 @@ export default class ChatThreadPanel extends Component {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.scrollable.scrollTop = -1;
|
this.scrollable.scrollTop = this.scrollable.scrollHeight + 1;
|
||||||
this.forceRendering(() => {
|
this.forceRendering(() => {
|
||||||
this.scrollable.scrollTop = 0;
|
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,7 +280,6 @@ export default class ChatThreadPanel extends Component {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
resendStagedMessage() {}
|
resendStagedMessage() {}
|
||||||
// resendStagedMessage(stagedMessage) {}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
messageDidEnterViewport(message) {
|
messageDidEnterViewport(message) {
|
||||||
|
@ -3,8 +3,6 @@ import { inject as service } from "@ember/service";
|
|||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import discourseDebounce from "discourse-common/lib/debounce";
|
import discourseDebounce from "discourse-common/lib/debounce";
|
||||||
import { action } from "@ember/object";
|
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 {
|
export default class ChatComposerChannel extends ChatComposer {
|
||||||
@service("chat-channel-composer") composer;
|
@service("chat-channel-composer") composer;
|
||||||
@ -14,18 +12,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||||||
|
|
||||||
composerId = "channel-composer";
|
composerId = "channel-composer";
|
||||||
|
|
||||||
@action
|
|
||||||
sendMessage(raw) {
|
|
||||||
const message = ChatMessage.createDraftMessage(this.args.channel, {
|
|
||||||
user: this.currentUser,
|
|
||||||
message: raw,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.args.onSendMessage(message);
|
|
||||||
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
persistDraft() {
|
persistDraft() {
|
||||||
if (this.args.channel?.isDraft) {
|
if (this.args.channel?.isDraft) {
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
import ChatComposer from "../../chat-composer";
|
import ChatComposer from "../../chat-composer";
|
||||||
import { inject as service } from "@ember/service";
|
import { inject as service } from "@ember/service";
|
||||||
import I18n from "I18n";
|
import I18n from "I18n";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
|
||||||
import { Promise } from "rsvp";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
export default class ChatComposerThread extends ChatComposer {
|
export default class ChatComposerThread extends ChatComposer {
|
||||||
@service("chat-channel-thread-composer") composer;
|
@service("chat-channel-thread-composer") composer;
|
||||||
|
@service("chat-channel-composer") channelComposer;
|
||||||
@service("chat-channel-thread-pane") pane;
|
@service("chat-channel-thread-pane") pane;
|
||||||
|
@service router;
|
||||||
|
|
||||||
context = "thread";
|
context = "thread";
|
||||||
|
|
||||||
composerId = "thread-composer";
|
composerId = "thread-composer";
|
||||||
|
|
||||||
@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() {
|
get placeholder() {
|
||||||
return I18n.t("chat.placeholder_thread");
|
return I18n.t("chat.placeholder_thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.router.transitionTo(
|
||||||
|
"chat.channel",
|
||||||
|
...this.args.channel.routeModels
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onKeyDown(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,12 @@ registerUnbound("format-chat-date", function (message, mode) {
|
|||||||
|
|
||||||
if (message.staged) {
|
if (message.staged) {
|
||||||
return htmlSafe(
|
return htmlSafe(
|
||||||
`<span title='${title}' class='chat-time'>${display}</span>`
|
`<span title='${title}' tabindex="-1" class='chat-time'>${display}</span>`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`);
|
const url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`);
|
||||||
return htmlSafe(
|
return htmlSafe(
|
||||||
`<a title='${title}' class='chat-time' href='${url}'>${display}</a>`
|
`<a title='${title}' tabindex="-1" class='chat-time' href='${url}'>${display}</a>`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -229,8 +229,8 @@ export default class ChatMessageInteractor {
|
|||||||
|
|
||||||
copyLink() {
|
copyLink() {
|
||||||
const { protocol, host } = window.location;
|
const { protocol, host } = window.location;
|
||||||
const channelId = this.message.channelId;
|
const channelId = this.message.channel.id;
|
||||||
const threadId = this.message.threadId;
|
const threadId = this.message.thread?.id;
|
||||||
|
|
||||||
let url;
|
let url;
|
||||||
if (threadId) {
|
if (threadId) {
|
||||||
@ -276,7 +276,7 @@ export default class ChatMessageInteractor {
|
|||||||
|
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.publishReaction(
|
.publishReaction(
|
||||||
this.message.channelId,
|
this.message.channel.id,
|
||||||
this.message.id,
|
this.message.id,
|
||||||
emoji,
|
emoji,
|
||||||
reactAction
|
reactAction
|
||||||
@ -329,21 +329,21 @@ export default class ChatMessageInteractor {
|
|||||||
@action
|
@action
|
||||||
delete() {
|
delete() {
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.trashMessage(this.message.channelId, this.message.id)
|
.trashMessage(this.message.channel.id, this.message.id)
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
restore() {
|
restore() {
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.restoreMessage(this.message.channelId, this.message.id)
|
.restoreMessage(this.message.channel.id, this.message.id)
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
rebake() {
|
rebake() {
|
||||||
return this.chatApi
|
return this.chatApi
|
||||||
.rebakeMessage(this.message.channelId, this.message.id)
|
.rebakeMessage(this.message.channel.id, this.message.id)
|
||||||
.catch(popupAjaxError);
|
.catch(popupAjaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import Promise from "rsvp";
|
|||||||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
import { TrackedObject } from "@ember-compat/tracked-built-ins";
|
||||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The ChatThreadsManager is responsible for managing the loaded chat threads
|
The ChatThreadsManager is responsible for managing the loaded chat threads
|
||||||
@ -39,11 +38,16 @@ export default class ChatThreadsManager {
|
|||||||
return Object.values(this._cached);
|
return Object.values(this._cached);
|
||||||
}
|
}
|
||||||
|
|
||||||
store(threadObject) {
|
store(channel, threadObject) {
|
||||||
let model = this.#findStale(threadObject.id);
|
let model = this.#findStale(threadObject.id);
|
||||||
|
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = new ChatThread(threadObject);
|
if (threadObject instanceof ChatThread) {
|
||||||
|
model = threadObject;
|
||||||
|
} else {
|
||||||
|
model = new ChatThread(channel, threadObject);
|
||||||
|
}
|
||||||
|
|
||||||
this.#cache(model);
|
this.#cache(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,12 +63,7 @@ export default class ChatThreadsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async #find(channelId, threadId) {
|
async #find(channelId, threadId) {
|
||||||
return this.chatApi
|
return this.chatApi.thread(channelId, threadId);
|
||||||
.thread(channelId, threadId)
|
|
||||||
.catch(popupAjaxError)
|
|
||||||
.then((thread) => {
|
|
||||||
return thread;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#cache(thread) {
|
#cache(thread) {
|
||||||
|
@ -9,6 +9,7 @@ import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-thread
|
|||||||
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
|
import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
|
||||||
import { getOwner } from "discourse-common/lib/get-owner";
|
import { getOwner } from "discourse-common/lib/get-owner";
|
||||||
import guid from "pretty-text/guid";
|
import guid from "pretty-text/guid";
|
||||||
|
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||||
|
|
||||||
export const CHATABLE_TYPES = {
|
export const CHATABLE_TYPES = {
|
||||||
directMessageChannel: "DirectMessage",
|
directMessageChannel: "DirectMessage",
|
||||||
@ -71,6 +72,10 @@ export default class ChatChannel extends RestModel {
|
|||||||
return this.messages.findIndex((m) => m.id === message.id);
|
return this.messages.findIndex((m) => m.id === message.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findMessage(id) {
|
||||||
|
return this.messagesManager.findMessage(id);
|
||||||
|
}
|
||||||
|
|
||||||
get messages() {
|
get messages() {
|
||||||
return this.messagesManager.messages;
|
return this.messagesManager.messages;
|
||||||
}
|
}
|
||||||
@ -155,6 +160,23 @@ export default class ChatChannel extends RestModel {
|
|||||||
this.channelMessageBusLastId = details.channel_message_bus_last_id;
|
this.channelMessageBusLastId = details.channel_message_bus_last_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createStagedThread(message) {
|
||||||
|
const clonedMessage = message.duplicate();
|
||||||
|
|
||||||
|
const thread = new ChatThread(this, {
|
||||||
|
id: `staged-thread-${message.channel.id}-${message.id}`,
|
||||||
|
original_message: message,
|
||||||
|
staged: true,
|
||||||
|
created_at: moment.utc().format(),
|
||||||
|
});
|
||||||
|
|
||||||
|
clonedMessage.thread = thread;
|
||||||
|
this.threadsManager.store(this, thread);
|
||||||
|
thread.messagesManager.addMessages([clonedMessage]);
|
||||||
|
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
stageMessage(message) {
|
stageMessage(message) {
|
||||||
message.id = guid();
|
message.id = guid();
|
||||||
message.staged = true;
|
message.staged = true;
|
||||||
@ -163,11 +185,6 @@ export default class ChatChannel extends RestModel {
|
|||||||
message.cook();
|
message.cook();
|
||||||
|
|
||||||
if (message.inReplyTo) {
|
if (message.inReplyTo) {
|
||||||
if (!message.inReplyTo.threadId) {
|
|
||||||
message.inReplyTo.threadId = guid();
|
|
||||||
message.inReplyTo.threadReplyCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.threadingEnabled) {
|
if (!this.threadingEnabled) {
|
||||||
this.messagesManager.addMessages([message]);
|
this.messagesManager.addMessages([message]);
|
||||||
}
|
}
|
||||||
|
@ -31,8 +31,6 @@ export default class ChatMessage {
|
|||||||
@tracked deletedAt;
|
@tracked deletedAt;
|
||||||
@tracked uploads;
|
@tracked uploads;
|
||||||
@tracked excerpt;
|
@tracked excerpt;
|
||||||
@tracked threadId;
|
|
||||||
@tracked threadReplyCount;
|
|
||||||
@tracked reactions;
|
@tracked reactions;
|
||||||
@tracked reviewableId;
|
@tracked reviewableId;
|
||||||
@tracked user;
|
@tracked user;
|
||||||
@ -50,11 +48,12 @@ export default class ChatMessage {
|
|||||||
@tracked highlighted = false;
|
@tracked highlighted = false;
|
||||||
@tracked firstOfResults = false;
|
@tracked firstOfResults = false;
|
||||||
@tracked message;
|
@tracked message;
|
||||||
|
@tracked thread;
|
||||||
|
@tracked threadReplyCount;
|
||||||
@tracked _cooked;
|
@tracked _cooked;
|
||||||
|
|
||||||
constructor(channel, args = {}) {
|
constructor(channel, args = {}) {
|
||||||
this.channel = channel;
|
// when modifying constructor, be sure to update duplicate function accordingly
|
||||||
this.id = args.id;
|
this.id = args.id;
|
||||||
this.newest = args.newest;
|
this.newest = args.newest;
|
||||||
this.firstOfResults = args.firstOfResults;
|
this.firstOfResults = args.firstOfResults;
|
||||||
@ -62,23 +61,23 @@ export default class ChatMessage {
|
|||||||
this.edited = args.edited;
|
this.edited = args.edited;
|
||||||
this.availableFlags = args.availableFlags || args.available_flags;
|
this.availableFlags = args.availableFlags || args.available_flags;
|
||||||
this.hidden = args.hidden;
|
this.hidden = args.hidden;
|
||||||
this.threadId = args.threadId || args.thread_id;
|
|
||||||
this.threadReplyCount = args.threadReplyCount || args.thread_reply_count;
|
this.threadReplyCount = args.threadReplyCount || args.thread_reply_count;
|
||||||
this.channelId = args.channelId || args.chat_channel_id;
|
|
||||||
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
|
this.chatWebhookEvent = args.chatWebhookEvent || args.chat_webhook_event;
|
||||||
this.createdAt = args.createdAt || args.created_at;
|
this.createdAt = args.createdAt || args.created_at;
|
||||||
this.deletedAt = args.deletedAt || args.deleted_at;
|
this.deletedAt = args.deletedAt || args.deleted_at;
|
||||||
this.excerpt = args.excerpt;
|
this.excerpt = args.excerpt;
|
||||||
this.reviewableId = args.reviewableId || args.reviewable_id;
|
this.reviewableId = args.reviewableId || args.reviewable_id;
|
||||||
this.userFlagStatus = args.userFlagStatus || args.user_flag_status;
|
this.userFlagStatus = args.userFlagStatus || args.user_flag_status;
|
||||||
|
this.draft = args.draft;
|
||||||
|
this.message = args.message || "";
|
||||||
|
this._cooked = args.cooked || "";
|
||||||
|
this.thread = args.thread;
|
||||||
this.inReplyTo =
|
this.inReplyTo =
|
||||||
args.inReplyTo ||
|
args.inReplyTo ||
|
||||||
(args.in_reply_to || args.replyToMsg
|
(args.in_reply_to || args.replyToMsg
|
||||||
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
|
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
|
||||||
: null);
|
: null);
|
||||||
this.draft = args.draft;
|
this.channel = channel;
|
||||||
this.message = args.message || "";
|
|
||||||
this._cooked = args.cooked || "";
|
|
||||||
this.reactions = this.#initChatMessageReactionModel(
|
this.reactions = this.#initChatMessageReactionModel(
|
||||||
args.id,
|
args.id,
|
||||||
args.reactions
|
args.reactions
|
||||||
@ -88,6 +87,39 @@ export default class ChatMessage {
|
|||||||
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
|
this.bookmark = args.bookmark ? Bookmark.create(args.bookmark) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
duplicate() {
|
||||||
|
// This is important as a message can exist in the context of a channel or a thread
|
||||||
|
// The current strategy is to have a different message object in each cases to avoid
|
||||||
|
// side effects
|
||||||
|
const message = new ChatMessage(this.channel, {
|
||||||
|
id: this.id,
|
||||||
|
newest: this.newest,
|
||||||
|
staged: this.staged,
|
||||||
|
edited: this.edited,
|
||||||
|
availableFlags: this.availableFlags,
|
||||||
|
hidden: this.hidden,
|
||||||
|
threadReplyCount: this.threadReplyCount,
|
||||||
|
chatWebhookEvent: this.chatWebhookEvent,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
deletedAt: this.deletedAt,
|
||||||
|
excerpt: this.excerpt,
|
||||||
|
reviewableId: this.reviewableId,
|
||||||
|
userFlagStatus: this.userFlagStatus,
|
||||||
|
draft: this.draft,
|
||||||
|
message: this.message,
|
||||||
|
cooked: this.cooked,
|
||||||
|
});
|
||||||
|
|
||||||
|
message.thread = this.thread;
|
||||||
|
message.reactions = this.reactions;
|
||||||
|
message.user = this.user;
|
||||||
|
message.inReplyTo = this.inReplyTo;
|
||||||
|
message.bookmark = this.bookmark;
|
||||||
|
message.uploads = this.uploads;
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
get cooked() {
|
get cooked() {
|
||||||
return this._cooked;
|
return this._cooked;
|
||||||
}
|
}
|
||||||
@ -131,10 +163,6 @@ export default class ChatMessage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get threadRouteModels() {
|
|
||||||
return [...this.channel.routeModels, this.threadId];
|
|
||||||
}
|
|
||||||
|
|
||||||
get read() {
|
get read() {
|
||||||
return this.channel.currentUserMembership?.last_read_message_id >= this.id;
|
return this.channel.currentUserMembership?.last_read_message_id >= this.id;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import User from "discourse/models/user";
|
|||||||
import { escapeExpression } from "discourse/lib/utilities";
|
import { escapeExpression } from "discourse/lib/utilities";
|
||||||
import { tracked } from "@glimmer/tracking";
|
import { tracked } from "@glimmer/tracking";
|
||||||
import guid from "pretty-text/guid";
|
import guid from "pretty-text/guid";
|
||||||
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
|
|
||||||
export const THREAD_STATUSES = {
|
export const THREAD_STATUSES = {
|
||||||
open: "open",
|
open: "open",
|
||||||
@ -13,20 +14,25 @@ export const THREAD_STATUSES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default class ChatThread {
|
export default class ChatThread {
|
||||||
|
@tracked id;
|
||||||
@tracked title;
|
@tracked title;
|
||||||
@tracked status;
|
@tracked status;
|
||||||
|
@tracked draft;
|
||||||
|
@tracked staged;
|
||||||
|
@tracked channel;
|
||||||
|
@tracked originalMessage;
|
||||||
|
@tracked threadMessageBusLastId;
|
||||||
|
|
||||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||||
|
|
||||||
constructor(args = {}) {
|
constructor(channel, args = {}) {
|
||||||
this.title = args.title;
|
this.title = args.title;
|
||||||
this.id = args.id;
|
this.id = args.id;
|
||||||
this.channelId = args.channel_id;
|
this.channel = channel;
|
||||||
this.status = args.status;
|
this.status = args.status;
|
||||||
|
this.draft = args.draft;
|
||||||
this.originalMessageUser = this.#initUserModel(args.original_message_user);
|
this.staged = args.staged;
|
||||||
this.originalMessage = args.original_message;
|
this.originalMessage = ChatMessage.create(channel, args.original_message);
|
||||||
this.originalMessage.user = this.originalMessageUser;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stageMessage(message) {
|
stageMessage(message) {
|
||||||
@ -39,6 +45,10 @@ export default class ChatThread {
|
|||||||
this.messagesManager.addMessages([message]);
|
this.messagesManager.addMessages([message]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get routeModels() {
|
||||||
|
return [...this.channel.routeModels, this.id];
|
||||||
|
}
|
||||||
|
|
||||||
get messages() {
|
get messages() {
|
||||||
return this.messagesManager.messages;
|
return this.messagesManager.messages;
|
||||||
}
|
}
|
||||||
|
@ -5,24 +5,57 @@ export default class ChatChannelThread extends DiscourseRoute {
|
|||||||
@service router;
|
@service router;
|
||||||
@service chatStateManager;
|
@service chatStateManager;
|
||||||
@service chat;
|
@service chat;
|
||||||
|
@service chatStagedThreadMapping;
|
||||||
|
@service chatChannelThreadPane;
|
||||||
|
|
||||||
async model(params) {
|
model(params, transition) {
|
||||||
const channel = this.modelFor("chat.channel");
|
const channel = this.modelFor("chat.channel");
|
||||||
return channel.threadsManager.find(channel.id, params.threadId);
|
|
||||||
|
return channel.threadsManager
|
||||||
|
.find(channel.id, params.threadId)
|
||||||
|
.catch(() => {
|
||||||
|
transition.abort();
|
||||||
|
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||||
|
return;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate() {
|
deactivate() {
|
||||||
this.#closeThread();
|
this.chatChannelThreadPane.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
#closeThread() {
|
beforeModel(transition) {
|
||||||
this.chat.activeChannel.activeThread?.messagesManager?.clearMessages();
|
const channel = this.modelFor("chat.channel");
|
||||||
this.chat.activeChannel.activeThread = null;
|
|
||||||
this.chatStateManager.closeSidePanel();
|
if (!channel.threadingEnabled) {
|
||||||
|
transition.abort();
|
||||||
|
this.router.transitionTo("chat.channel", ...channel.routeModels);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a very special logic to attempt to reconciliate a staged thread id
|
||||||
|
// it happens after creating a new thread and having a temp ID in the URL
|
||||||
|
// if users presses reload at this moment, we would have a 404
|
||||||
|
// replacing the ID in the URL sooner would also cause a reload
|
||||||
|
const params = this.paramsFor("chat.channel.thread");
|
||||||
|
const threadId = params.threadId;
|
||||||
|
|
||||||
|
if (threadId?.startsWith("staged-thread-")) {
|
||||||
|
const mapping = this.chatStagedThreadMapping.getMapping();
|
||||||
|
|
||||||
|
if (mapping[threadId]) {
|
||||||
|
transition.abort();
|
||||||
|
|
||||||
|
this.router.transitionTo(
|
||||||
|
"chat.channel.thread",
|
||||||
|
...[...channel.routeModels, mapping[threadId]]
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
afterModel(model) {
|
afterModel(model) {
|
||||||
this.chat.activeChannel.activeThread = model;
|
this.chatChannelThreadPane.open(model);
|
||||||
this.chatStateManager.openSidePanel();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,11 @@ export default class ChatApi extends Service {
|
|||||||
*/
|
*/
|
||||||
thread(channelId, threadId) {
|
thread(channelId, threadId) {
|
||||||
return this.#getRequest(`/channels/${channelId}/threads/${threadId}`).then(
|
return this.#getRequest(`/channels/${channelId}/threads/${threadId}`).then(
|
||||||
(result) => this.chat.activeChannel.threadsManager.store(result.thread)
|
(result) =>
|
||||||
|
this.chat.activeChannel.threadsManager.store(
|
||||||
|
this.chat.activeChannel,
|
||||||
|
result.thread
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,60 +1,41 @@
|
|||||||
import { tracked } from "@glimmer/tracking";
|
import { inject as service } from "@ember/service";
|
||||||
import Service, { inject as service } from "@ember/service";
|
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatComposer from "./chat-composer";
|
||||||
|
import { next } from "@ember/runloop";
|
||||||
|
|
||||||
export default class ChatChannelComposer extends Service {
|
export default class ChatChannelComposer extends ChatComposer {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service chatApi;
|
@service chatChannelThreadComposer;
|
||||||
@service chatComposerPresenceManager;
|
@service router;
|
||||||
@service currentUser;
|
|
||||||
|
|
||||||
@tracked _message;
|
|
||||||
|
|
||||||
@action
|
|
||||||
cancel() {
|
|
||||||
if (this.message.editing) {
|
|
||||||
this.reset();
|
|
||||||
} else if (this.message.inReplyTo) {
|
|
||||||
this.message.inReplyTo = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
reset(channel) {
|
|
||||||
this.message = ChatMessage.createDraftMessage(channel, {
|
|
||||||
user: this.currentUser,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
clear() {
|
|
||||||
this.message.message = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
editMessage(message) {
|
|
||||||
this.chat.activeMessage = null;
|
|
||||||
message.editing = true;
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
|
||||||
onCancelEditing() {
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
@action
|
@action
|
||||||
replyTo(message) {
|
replyTo(message) {
|
||||||
this.chat.activeMessage = null;
|
this.chat.activeMessage = null;
|
||||||
this.message.inReplyTo = message;
|
const channel = message.channel;
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.siteSettings.enable_experimental_chat_threaded_discussions &&
|
||||||
|
channel.threadingEnabled
|
||||||
|
) {
|
||||||
|
let thread;
|
||||||
|
if (message.thread?.id) {
|
||||||
|
thread = message.thread;
|
||||||
|
} else {
|
||||||
|
thread = channel.createStagedThread(message);
|
||||||
|
message.thread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router
|
||||||
|
.transitionTo("chat.channel.thread", ...thread.routeModels)
|
||||||
|
.finally(() => this._setReplyToAfterTransition(message));
|
||||||
|
} else {
|
||||||
|
this.message.inReplyTo = message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get message() {
|
_setReplyToAfterTransition(message) {
|
||||||
return this._message;
|
next(() => {
|
||||||
}
|
this.chatChannelThreadComposer.replyTo(message);
|
||||||
|
});
|
||||||
set message(message) {
|
|
||||||
this._message = message;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,14 +20,6 @@ export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSub
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleThreadCreated(data) {
|
|
||||||
const message = this.messagesManager.findMessage(data.chat_message.id);
|
|
||||||
if (message) {
|
|
||||||
message.threadId = data.chat_message.thread_id;
|
|
||||||
message.threadReplyCount = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleThreadOriginalMessageUpdate(data) {
|
handleThreadOriginalMessageUpdate(data) {
|
||||||
const message = this.messagesManager.findMessage(data.original_message_id);
|
const message = this.messagesManager.findMessage(data.original_message_id);
|
||||||
if (message) {
|
if (message) {
|
||||||
|
@ -55,4 +55,8 @@ export default class ChatChannelPane extends Service {
|
|||||||
|
|
||||||
return lastCurrentUserMessage;
|
return lastCurrentUserMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get lastMessage() {
|
||||||
|
return this.chat.activeChannel.messages.lastObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import ChatChannelComposer from "./chat-channel-composer";
|
import ChatComposer from "./chat-composer";
|
||||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||||
import { action } from "@ember/object";
|
import { action } from "@ember/object";
|
||||||
|
|
||||||
export default class extends ChatChannelComposer {
|
export default class ChatChannelThreadComposer extends ChatComposer {
|
||||||
@action
|
@action
|
||||||
reset(channel) {
|
reset(channel, thread) {
|
||||||
this.message = ChatMessage.createDraftMessage(channel, {
|
this.message = ChatMessage.createDraftMessage(channel, {
|
||||||
user: this.currentUser,
|
user: this.currentUser,
|
||||||
thread_id: channel.activeThread.id,
|
|
||||||
});
|
});
|
||||||
|
this.message.thread = thread;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
replyTo(message) {
|
||||||
|
this.chat.activeMessage = null;
|
||||||
|
this.message.thread = message.thread;
|
||||||
|
this.message.inReplyTo = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import ChatPaneBaseSubscriptionsManager from "./chat-pane-base-subscriptions-man
|
|||||||
|
|
||||||
export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager {
|
export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager {
|
||||||
get messageBusChannel() {
|
get messageBusChannel() {
|
||||||
return `/chat/${this.model.channelId}/thread/${this.model.id}`;
|
return `/chat/${this.model.channel.id}/thread/${this.model.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get messageBusLastId() {
|
get messageBusLastId() {
|
||||||
@ -12,7 +12,10 @@ export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneB
|
|||||||
|
|
||||||
handleSentMessage(data) {
|
handleSentMessage(data) {
|
||||||
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
|
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
|
||||||
const stagedMessage = this.handleStagedMessageInternal(data);
|
const stagedMessage = this.handleStagedMessageInternal(
|
||||||
|
this.model.channel,
|
||||||
|
data
|
||||||
|
);
|
||||||
if (stagedMessage) {
|
if (stagedMessage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -23,15 +26,6 @@ export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneB
|
|||||||
data.chat_message
|
data.chat_message
|
||||||
);
|
);
|
||||||
this.messagesManager.addMessages([message]);
|
this.messagesManager.addMessages([message]);
|
||||||
|
|
||||||
// TODO (martin) All the scrolling and new message indicator shenanigans,
|
|
||||||
// as well as handling marking the thread as read.
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: noop, there is nothing to do when a thread is created
|
|
||||||
// inside the thread panel.
|
|
||||||
handleThreadCreated() {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: noop, there is nothing to do when a thread original message
|
// NOTE: noop, there is nothing to do when a thread original message
|
||||||
|
@ -3,6 +3,19 @@ import { inject as service } from "@ember/service";
|
|||||||
|
|
||||||
export default class ChatChannelThreadPane extends ChatChannelPane {
|
export default class ChatChannelThreadPane extends ChatChannelPane {
|
||||||
@service chatChannelThreadComposer;
|
@service chatChannelThreadComposer;
|
||||||
|
@service chat;
|
||||||
|
@service chatStateManager;
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.chat.activeChannel.activeThread?.messagesManager?.clearMessages();
|
||||||
|
this.chat.activeChannel.activeThread = null;
|
||||||
|
this.chatStateManager.closeSidePanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
open(thread) {
|
||||||
|
this.chat.activeChannel.activeThread = thread;
|
||||||
|
this.chatStateManager.openSidePanel();
|
||||||
|
}
|
||||||
|
|
||||||
get selectedMessageIds() {
|
get selectedMessageIds() {
|
||||||
return this.chat.activeChannel.activeThread.selectedMessages.mapBy("id");
|
return this.chat.activeChannel.activeThread.selectedMessages.mapBy("id");
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
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 ChatComposer extends Service {
|
||||||
|
@service chat;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
|
@tracked _message;
|
||||||
|
|
||||||
|
@action
|
||||||
|
cancel() {
|
||||||
|
if (this.message.editing) {
|
||||||
|
this.reset();
|
||||||
|
} else if (this.message.inReplyTo) {
|
||||||
|
this.message.inReplyTo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
reset(channel) {
|
||||||
|
this.message = ChatMessage.createDraftMessage(channel, {
|
||||||
|
user: this.currentUser,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
clear() {
|
||||||
|
this.message.message = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
editMessage(message) {
|
||||||
|
this.chat.activeMessage = null;
|
||||||
|
message.editing = true;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
onCancelEditing() {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
get message() {
|
||||||
|
return this._message;
|
||||||
|
}
|
||||||
|
|
||||||
|
set message(message) {
|
||||||
|
this._message = message;
|
||||||
|
}
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { bind } from "discourse-common/utils/decorators";
|
|||||||
|
|
||||||
// TODO (martin) This export can be removed once we move the handleSentMessage
|
// TODO (martin) This export can be removed once we move the handleSentMessage
|
||||||
// code completely out of ChatLivePane
|
// code completely out of ChatLivePane
|
||||||
export function handleStagedMessage(messagesManager, data) {
|
export function handleStagedMessage(channel, messagesManager, data) {
|
||||||
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
|
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
|
||||||
|
|
||||||
if (!stagedMessage) {
|
if (!stagedMessage) {
|
||||||
@ -17,17 +17,8 @@ export function handleStagedMessage(messagesManager, data) {
|
|||||||
stagedMessage.id = data.chat_message.id;
|
stagedMessage.id = data.chat_message.id;
|
||||||
stagedMessage.staged = false;
|
stagedMessage.staged = false;
|
||||||
stagedMessage.excerpt = data.chat_message.excerpt;
|
stagedMessage.excerpt = data.chat_message.excerpt;
|
||||||
stagedMessage.threadId = data.chat_message.thread_id;
|
stagedMessage.channel = channel;
|
||||||
stagedMessage.channelId = data.chat_message.chat_channel_id;
|
|
||||||
stagedMessage.createdAt = data.chat_message.created_at;
|
stagedMessage.createdAt = data.chat_message.created_at;
|
||||||
|
|
||||||
const inReplyToMsg = messagesManager.findMessage(
|
|
||||||
data.chat_message.in_reply_to?.id
|
|
||||||
);
|
|
||||||
if (inReplyToMsg && !inReplyToMsg.threadId) {
|
|
||||||
inReplyToMsg.threadId = data.chat_message.thread_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
stagedMessage.cooked = data.chat_message.cooked;
|
stagedMessage.cooked = data.chat_message.cooked;
|
||||||
|
|
||||||
return stagedMessage;
|
return stagedMessage;
|
||||||
@ -48,6 +39,7 @@ export function handleStagedMessage(messagesManager, data) {
|
|||||||
export default class ChatPaneBaseSubscriptionsManager extends Service {
|
export default class ChatPaneBaseSubscriptionsManager extends Service {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service currentUser;
|
@service currentUser;
|
||||||
|
@service chatStagedThreadMapping;
|
||||||
|
|
||||||
get messageBusChannel() {
|
get messageBusChannel() {
|
||||||
throw "not implemented";
|
throw "not implemented";
|
||||||
@ -75,14 +67,15 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||||||
if (!this.model) {
|
if (!this.model) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
|
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
|
||||||
this.model = null;
|
this.model = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (martin) This can be removed once we move the handleSentMessage
|
// TODO (martin) This can be removed once we move the handleSentMessage
|
||||||
// code completely out of ChatLivePane
|
// code completely out of ChatLivePane
|
||||||
handleStagedMessageInternal(data) {
|
handleStagedMessageInternal(channel, data) {
|
||||||
return handleStagedMessage(this.messagesManager, data);
|
return handleStagedMessage(channel, this.messagesManager, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@bind
|
@bind
|
||||||
@ -122,7 +115,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||||||
this.handleFlaggedMessage(busData);
|
this.handleFlaggedMessage(busData);
|
||||||
break;
|
break;
|
||||||
case "thread_created":
|
case "thread_created":
|
||||||
this.handleThreadCreated(busData);
|
this.handleNewThreadCreated(busData);
|
||||||
break;
|
break;
|
||||||
case "update_thread_original_message":
|
case "update_thread_original_message":
|
||||||
this.handleThreadOriginalMessageUpdate(busData);
|
this.handleThreadOriginalMessageUpdate(busData);
|
||||||
@ -223,8 +216,34 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleThreadCreated() {
|
handleNewThreadCreated(data) {
|
||||||
throw "not implemented";
|
this.model.threadsManager
|
||||||
|
.find(this.model.id, data.staged_thread_id, { fetchIfNotFound: false })
|
||||||
|
.then((stagedThread) => {
|
||||||
|
if (stagedThread) {
|
||||||
|
this.chatStagedThreadMapping.setMapping(
|
||||||
|
data.thread_id,
|
||||||
|
stagedThread.id
|
||||||
|
);
|
||||||
|
stagedThread.staged = false;
|
||||||
|
stagedThread.id = data.thread_id;
|
||||||
|
stagedThread.originalMessage.thread = stagedThread;
|
||||||
|
stagedThread.originalMessage.threadReplyCount ??= 1;
|
||||||
|
} else if (data.thread_id) {
|
||||||
|
this.model.threadsManager
|
||||||
|
.find(this.model.id, data.thread_id, { fetchIfNotFound: true })
|
||||||
|
.then((thread) => {
|
||||||
|
const channelOriginalMessage =
|
||||||
|
this.model.messagesManager.findMessage(
|
||||||
|
thread.originalMessage.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (channelOriginalMessage) {
|
||||||
|
channelOriginalMessage.thread = thread;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleThreadOriginalMessageUpdate() {
|
handleThreadOriginalMessageUpdate() {
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
import KeyValueStore from "discourse/lib/key-value-store";
|
||||||
|
import Service from "@ember/service";
|
||||||
|
|
||||||
|
export default class ChatStagedThreadMapping extends Service {
|
||||||
|
STORE_NAMESPACE = "discourse_chat_";
|
||||||
|
KEY = "staged_thread";
|
||||||
|
|
||||||
|
store = new KeyValueStore(this.STORE_NAMESPACE);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (!this.store.getObject(this.USER_EMOJIS_STORE_KEY)) {
|
||||||
|
this.storedFavorites = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getMapping() {
|
||||||
|
return JSON.parse(this.store.getObject(this.KEY) || "{}");
|
||||||
|
}
|
||||||
|
|
||||||
|
setMapping(id, stagedId) {
|
||||||
|
const mapping = {};
|
||||||
|
mapping[stagedId] = id;
|
||||||
|
this.store.setObject({
|
||||||
|
key: this.KEY,
|
||||||
|
value: JSON.stringify(mapping),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.store.setObject({ key: this.KEY, value: "{}" });
|
||||||
|
}
|
||||||
|
}
|
@ -21,9 +21,10 @@ export function resetChatDrawerStateCallbacks() {
|
|||||||
export default class ChatStateManager extends Service {
|
export default class ChatStateManager extends Service {
|
||||||
@service chat;
|
@service chat;
|
||||||
@service router;
|
@service router;
|
||||||
isDrawerExpanded = false;
|
|
||||||
isDrawerActive = false;
|
@tracked isSidePanelExpanded = false;
|
||||||
isSidePanelExpanded = false;
|
@tracked isDrawerExpanded = false;
|
||||||
|
@tracked isDrawerActive = false;
|
||||||
@tracked _chatURL = null;
|
@tracked _chatURL = null;
|
||||||
@tracked _appURL = null;
|
@tracked _appURL = null;
|
||||||
|
|
||||||
@ -44,16 +45,16 @@ export default class ChatStateManager extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
openSidePanel() {
|
openSidePanel() {
|
||||||
this.set("isSidePanelExpanded", true);
|
this.isSidePanelExpanded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeSidePanel() {
|
closeSidePanel() {
|
||||||
this.set("isSidePanelExpanded", false);
|
this.isSidePanelExpanded = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
didOpenDrawer(url = null) {
|
didOpenDrawer(url = null) {
|
||||||
this.set("isDrawerActive", true);
|
this.isDrawerActive = true;
|
||||||
this.set("isDrawerExpanded", true);
|
this.isDrawerExpanded = true;
|
||||||
|
|
||||||
if (url) {
|
if (url) {
|
||||||
this.storeChatURL(url);
|
this.storeChatURL(url);
|
||||||
@ -64,27 +65,27 @@ export default class ChatStateManager extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
didCloseDrawer() {
|
didCloseDrawer() {
|
||||||
this.set("isDrawerActive", false);
|
this.isDrawerActive = false;
|
||||||
this.set("isDrawerExpanded", false);
|
this.isDrawerExpanded = false;
|
||||||
this.chat.updatePresence();
|
this.chat.updatePresence();
|
||||||
this.#publishStateChange();
|
this.#publishStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
didExpandDrawer() {
|
didExpandDrawer() {
|
||||||
this.set("isDrawerActive", true);
|
this.isDrawerActive = true;
|
||||||
this.set("isDrawerExpanded", true);
|
this.isDrawerExpanded = true;
|
||||||
this.chat.updatePresence();
|
this.chat.updatePresence();
|
||||||
}
|
}
|
||||||
|
|
||||||
didCollapseDrawer() {
|
didCollapseDrawer() {
|
||||||
this.set("isDrawerActive", true);
|
this.isDrawerActive = true;
|
||||||
this.set("isDrawerExpanded", false);
|
this.isDrawerExpanded = false;
|
||||||
this.#publishStateChange();
|
this.#publishStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
didToggleDrawer() {
|
didToggleDrawer() {
|
||||||
this.set("isDrawerExpanded", !this.isDrawerExpanded);
|
this.isDrawerExpanded = !this.isDrawerExpanded;
|
||||||
this.set("isDrawerActive", true);
|
this.isDrawerActive = true;
|
||||||
this.#publishStateChange();
|
this.#publishStateChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,2 +1 @@
|
|||||||
{{! ChatThreadList will go here later }}
|
<ChatThread @thread={{this.model}} @includeHeader={{true}} />
|
||||||
<ChatThread @includeHeader={{true}} />
|
|
@ -25,8 +25,7 @@
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column;
|
||||||
will-change: transform;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-composer__wrapper {
|
.chat-composer__wrapper {
|
||||||
|
@ -13,6 +13,7 @@ module Chat
|
|||||||
chat_channel:,
|
chat_channel:,
|
||||||
in_reply_to_id: nil,
|
in_reply_to_id: nil,
|
||||||
thread_id: nil,
|
thread_id: nil,
|
||||||
|
staged_thread_id: nil,
|
||||||
user:,
|
user:,
|
||||||
content:,
|
content:,
|
||||||
staged_id: nil,
|
staged_id: nil,
|
||||||
@ -31,6 +32,7 @@ module Chat
|
|||||||
@incoming_chat_webhook = incoming_chat_webhook
|
@incoming_chat_webhook = incoming_chat_webhook
|
||||||
@upload_ids = upload_ids || []
|
@upload_ids = upload_ids || []
|
||||||
@thread_id = thread_id
|
@thread_id = thread_id
|
||||||
|
@staged_thread_id = staged_thread_id
|
||||||
@error = nil
|
@error = nil
|
||||||
|
|
||||||
@chat_message =
|
@chat_message =
|
||||||
@ -57,7 +59,12 @@ module Chat
|
|||||||
create_thread
|
create_thread
|
||||||
@chat_message.attach_uploads(uploads)
|
@chat_message.attach_uploads(uploads)
|
||||||
Chat::Draft.where(user_id: @user.id, chat_channel_id: @chat_channel.id).destroy_all
|
Chat::Draft.where(user_id: @user.id, chat_channel_id: @chat_channel.id).destroy_all
|
||||||
Chat::Publisher.publish_new!(@chat_channel, @chat_message, @staged_id)
|
Chat::Publisher.publish_new!(
|
||||||
|
@chat_channel,
|
||||||
|
@chat_message,
|
||||||
|
@staged_id,
|
||||||
|
staged_thread_id: @staged_thread_id,
|
||||||
|
)
|
||||||
resolved_thread&.increment_replies_count_cache
|
resolved_thread&.increment_replies_count_cache
|
||||||
Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id })
|
Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id })
|
||||||
Chat::Notifier.notify_new(chat_message: @chat_message, timestamp: @chat_message.created_at)
|
Chat::Notifier.notify_new(chat_message: @chat_message, timestamp: @chat_message.created_at)
|
||||||
@ -123,6 +130,8 @@ module Chat
|
|||||||
end
|
end
|
||||||
|
|
||||||
def validate_existing_thread!
|
def validate_existing_thread!
|
||||||
|
return if @staged_thread_id.present? && @thread_id.blank?
|
||||||
|
|
||||||
return if @thread_id.blank?
|
return if @thread_id.blank?
|
||||||
@existing_thread = Chat::Thread.find(@thread_id)
|
@existing_thread = Chat::Thread.find(@thread_id)
|
||||||
|
|
||||||
@ -165,7 +174,7 @@ module Chat
|
|||||||
|
|
||||||
def create_thread
|
def create_thread
|
||||||
return if @in_reply_to_id.blank?
|
return if @in_reply_to_id.blank?
|
||||||
return if @chat_message.in_thread?
|
return if @chat_message.in_thread? && !@staged_thread_id.present?
|
||||||
|
|
||||||
if @original_message.thread
|
if @original_message.thread
|
||||||
thread = @original_message.thread
|
thread = @original_message.thread
|
||||||
@ -177,12 +186,15 @@ module Chat
|
|||||||
channel: @chat_message.chat_channel,
|
channel: @chat_message.chat_channel,
|
||||||
)
|
)
|
||||||
@chat_message.in_reply_to.thread_id = thread.id
|
@chat_message.in_reply_to.thread_id = thread.id
|
||||||
Chat::Publisher.publish_thread_created!(
|
|
||||||
@chat_message.chat_channel,
|
|
||||||
@chat_message.in_reply_to,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Chat::Publisher.publish_thread_created!(
|
||||||
|
@chat_message.chat_channel,
|
||||||
|
@chat_message.in_reply_to,
|
||||||
|
thread.id,
|
||||||
|
@staged_thread_id,
|
||||||
|
)
|
||||||
|
|
||||||
@chat_message.thread_id = thread.id
|
@chat_message.thread_id = thread.id
|
||||||
|
|
||||||
# NOTE: We intentionally do not try to correct thread IDs within the chain
|
# NOTE: We intentionally do not try to correct thread IDs within the chain
|
||||||
|
@ -451,6 +451,30 @@ describe Chat::MessageCreator do
|
|||||||
expect(thread_created_message.channel).to eq("/chat/#{public_chat_channel.id}")
|
expect(thread_created_message.channel).to eq("/chat/#{public_chat_channel.id}")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when a staged_thread_id is provided" do
|
||||||
|
fab!(:existing_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }
|
||||||
|
|
||||||
|
it "creates a thread and publishes with the staged id" do
|
||||||
|
messages =
|
||||||
|
MessageBus.track_publish do
|
||||||
|
described_class.create(
|
||||||
|
chat_channel: public_chat_channel,
|
||||||
|
user: user1,
|
||||||
|
content: "this is a message",
|
||||||
|
in_reply_to_id: reply_message.id,
|
||||||
|
staged_thread_id: "stagedthreadid",
|
||||||
|
).chat_message
|
||||||
|
end
|
||||||
|
|
||||||
|
thread_event = messages.find { |m| m.data["type"] == "thread_created" }
|
||||||
|
expect(thread_event.data["staged_thread_id"]).to eq("stagedthreadid")
|
||||||
|
expect(Chat::Thread.find(thread_event.data["thread_id"])).to be_persisted
|
||||||
|
|
||||||
|
send_event = messages.find { |m| m.data["type"] == "sent" }
|
||||||
|
expect(send_event.data["staged_thread_id"]).to eq("stagedthreadid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when the thread_id is provided" do
|
context "when the thread_id is provided" do
|
||||||
fab!(:existing_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }
|
fab!(:existing_thread) { Fabricate(:chat_thread, channel: public_chat_channel) }
|
||||||
|
|
||||||
|
@ -385,6 +385,31 @@ RSpec.describe Chat::ChatController do
|
|||||||
expect(messages.first.data["last_read_message_id"]).to eq(Chat::Message.last.id)
|
expect(messages.first.data["last_read_message_id"]).to eq(Chat::Message.last.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when sending a message in a staged thread" do
|
||||||
|
it "creates the thread and publishes with the staged id" do
|
||||||
|
sign_in(user)
|
||||||
|
|
||||||
|
messages =
|
||||||
|
MessageBus.track_publish do
|
||||||
|
post "/chat/#{chat_channel.id}.json",
|
||||||
|
params: {
|
||||||
|
message: message,
|
||||||
|
in_reply_to_id: message_1.id,
|
||||||
|
staged_thread_id: "stagedthreadid",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
|
||||||
|
thread_event = messages.find { |m| m.data["type"] == "thread_created" }
|
||||||
|
expect(thread_event.data["staged_thread_id"]).to eq("stagedthreadid")
|
||||||
|
expect(Chat::Thread.find(thread_event.data["thread_id"])).to be_persisted
|
||||||
|
|
||||||
|
sent_event = messages.find { |m| m.data["type"] == "sent" }
|
||||||
|
expect(sent_event.data["staged_thread_id"]).to eq("stagedthreadid")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when sending a message in a thread" do
|
context "when sending a message in a thread" do
|
||||||
fab!(:thread) do
|
fab!(:thread) do
|
||||||
Fabricate(:chat_thread, channel: chat_channel, original_message: message_1)
|
Fabricate(:chat_thread, channel: chat_channel, original_message: message_1)
|
||||||
|
@ -111,6 +111,32 @@ describe Chat::Publisher do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when a staged thread has been provided" do
|
||||||
|
fab!(:thread) do
|
||||||
|
Fabricate(
|
||||||
|
:chat_thread,
|
||||||
|
original_message: Fabricate(:chat_message, chat_channel: channel),
|
||||||
|
channel: channel,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
before { message.update!(thread: thread) }
|
||||||
|
|
||||||
|
it "generates the correct targets" do
|
||||||
|
targets =
|
||||||
|
described_class.calculate_publish_targets(
|
||||||
|
channel,
|
||||||
|
message,
|
||||||
|
staged_thread_id: "stagedthreadid",
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(targets).to contain_exactly(
|
||||||
|
"/chat/#{channel.id}/thread/#{thread.id}",
|
||||||
|
"/chat/#{channel.id}/thread/stagedthreadid",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context "when the message is a thread reply" do
|
context "when the message is a thread reply" do
|
||||||
fab!(:thread) do
|
fab!(:thread) do
|
||||||
Fabricate(
|
Fabricate(
|
||||||
|
@ -61,11 +61,12 @@ RSpec.describe "Archive channel", type: :system, js: true do
|
|||||||
find("#split-topic-name").fill_in(with: "An interesting topic for cats")
|
find("#split-topic-name").fill_in(with: "An interesting topic for cats")
|
||||||
click_button(I18n.t("js.chat.channel_archive.title"))
|
click_button(I18n.t("js.chat.channel_archive.title"))
|
||||||
|
|
||||||
expect(page).to have_content(I18n.t("js.chat.channel_archive.process_started"))
|
expect(page).to have_css(".chat-channel-archive-status", wait: 15)
|
||||||
expect(page).to have_css(".chat-channel-archive-status")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "shows an error when the topic is invalid" do
|
it "shows an error when the topic is invalid" do
|
||||||
|
Jobs.run_immediately!
|
||||||
|
|
||||||
chat.visit_channel_settings(channel_1)
|
chat.visit_channel_settings(channel_1)
|
||||||
click_button(I18n.t("js.chat.channel_settings.archive_channel"))
|
click_button(I18n.t("js.chat.channel_settings.archive_channel"))
|
||||||
find("#split-topic-name").fill_in(
|
find("#split-topic-name").fill_in(
|
||||||
@ -73,7 +74,7 @@ RSpec.describe "Archive channel", type: :system, js: true do
|
|||||||
)
|
)
|
||||||
click_button(I18n.t("js.chat.channel_archive.title"))
|
click_button(I18n.t("js.chat.channel_archive.title"))
|
||||||
|
|
||||||
expect(page).not_to have_content(I18n.t("js.chat.channel_archive.process_started"))
|
expect(page).to have_no_content(I18n.t("js.chat.channel_archive.process_started"))
|
||||||
expect(page).to have_content("Title can't have more than 1 emoji")
|
expect(page).to have_content("Title can't have more than 1 emoji")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ RSpec.describe "Browse page", type: :system, js: true do
|
|||||||
context "when results are found" do
|
context "when results are found" do
|
||||||
it "lists expected results" do
|
it "lists expected results" do
|
||||||
visit("/chat/browse")
|
visit("/chat/browse")
|
||||||
find(".dc-filter-input").fill_in(with: category_channel_1.name)
|
find(".chat-browse-view .dc-filter-input").fill_in(with: category_channel_1.name)
|
||||||
|
|
||||||
expect(browse_view).to have_content(category_channel_1.name)
|
expect(browse_view).to have_content(category_channel_1.name)
|
||||||
expect(browse_view).to have_no_content(category_channel_2.name)
|
expect(browse_view).to have_no_content(category_channel_2.name)
|
||||||
@ -89,14 +89,14 @@ RSpec.describe "Browse page", type: :system, js: true do
|
|||||||
context "when results are not found" do
|
context "when results are not found" do
|
||||||
it "displays the correct message" do
|
it "displays the correct message" do
|
||||||
visit("/chat/browse")
|
visit("/chat/browse")
|
||||||
find(".dc-filter-input").fill_in(with: "x")
|
find(".chat-browse-view .dc-filter-input").fill_in(with: "x")
|
||||||
|
|
||||||
expect(browse_view).to have_content(I18n.t("js.chat.empty_state.title"))
|
expect(browse_view).to have_content(I18n.t("js.chat.empty_state.title"))
|
||||||
end
|
end
|
||||||
|
|
||||||
it "doesn’t display any channel" do
|
it "doesn’t display any channel" do
|
||||||
visit("/chat/browse")
|
visit("/chat/browse")
|
||||||
find(".dc-filter-input").fill_in(with: "x")
|
find(".chat-browse-view .dc-filter-input").fill_in(with: "x")
|
||||||
|
|
||||||
expect(browse_view).to have_no_content(category_channel_1.name)
|
expect(browse_view).to have_no_content(category_channel_1.name)
|
||||||
expect(browse_view).to have_no_content(category_channel_2.name)
|
expect(browse_view).to have_no_content(category_channel_2.name)
|
||||||
|
@ -69,8 +69,8 @@ describe "Channel thread message echoing", type: :system, js: true do
|
|||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.message_thread_indicator(thread.original_message).click
|
channel_page.message_thread_indicator(thread.original_message).click
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
open_thread.send_message(thread.id, "new thread message")
|
open_thread.send_message("new thread message")
|
||||||
expect(open_thread).to have_message(thread.id, text: "new thread message")
|
expect(open_thread).to have_message(thread_id: thread.id, text: "new thread message")
|
||||||
new_message = thread.reload.replies.last
|
new_message = thread.reload.replies.last
|
||||||
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(new_message.id))
|
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(new_message.id))
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe "Chat message", type: :system, js: true do
|
RSpec.describe "Chat message - channel", type: :system, js: true do
|
||||||
fab!(:current_user) { Fabricate(:user) }
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe "Chat message - channel", type: :system, js: true do
|
RSpec.describe "Chat message - thread", type: :system, js: true do
|
||||||
fab!(:current_user) { Fabricate(:user) }
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
fab!(:other_user) { Fabricate(:user) }
|
fab!(:other_user) { Fabricate(:user) }
|
||||||
fab!(:channel_1) { Fabricate(:chat_channel) }
|
fab!(:channel_1) { Fabricate(:chat_channel) }
|
||||||
@ -25,13 +25,13 @@ RSpec.describe "Chat message - channel", type: :system, js: true do
|
|||||||
|
|
||||||
context "when hovering a message" do
|
context "when hovering a message" do
|
||||||
it "adds an active class" do
|
it "adds an active class" do
|
||||||
last_message = thread_1.chat_messages.last
|
first_message = thread_1.chat_messages.first
|
||||||
chat.visit_thread(thread_1)
|
chat.visit_thread(thread_1)
|
||||||
|
|
||||||
thread.hover_message(last_message)
|
thread.hover_message(first_message)
|
||||||
|
|
||||||
expect(page).to have_css(
|
expect(page).to have_css(
|
||||||
".chat-thread[data-id='#{thread_1.id}'] [data-id='#{last_message.id}'] .chat-message.is-active",
|
".chat-thread[data-id='#{thread_1.id}'] [data-id='#{first_message.id}'] .chat-message.is-active",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -46,6 +46,7 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
|||||||
channel_1.update!(threading_enabled: true)
|
channel_1.update!(threading_enabled: true)
|
||||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||||
chat_system_user_bootstrap(user: other_user, channel: channel_1)
|
chat_system_user_bootstrap(user: other_user, channel: channel_1)
|
||||||
|
Chat::Thread.update_counts
|
||||||
end
|
end
|
||||||
|
|
||||||
it "hides the deleted messages" do
|
it "hides the deleted messages" do
|
||||||
@ -55,8 +56,8 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
|||||||
|
|
||||||
expect(channel_page).to have_message(id: message_1.id)
|
expect(channel_page).to have_message(id: message_1.id)
|
||||||
expect(channel_page).to have_message(id: message_2.id)
|
expect(channel_page).to have_message(id: message_2.id)
|
||||||
expect(open_thread).to have_message(thread.id, id: message_4.id)
|
expect(open_thread).to have_message(thread_id: thread.id, id: message_4.id)
|
||||||
expect(open_thread).to have_message(thread.id, id: message_5.id)
|
expect(open_thread).to have_message(thread_id: thread.id, id: message_5.id)
|
||||||
|
|
||||||
Chat::Publisher.publish_bulk_delete!(
|
Chat::Publisher.publish_bulk_delete!(
|
||||||
channel_1,
|
channel_1,
|
||||||
@ -65,8 +66,8 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
|||||||
|
|
||||||
expect(channel_page).to have_no_message(id: message_1.id)
|
expect(channel_page).to have_no_message(id: message_1.id)
|
||||||
expect(channel_page).to have_no_message(id: message_2.id)
|
expect(channel_page).to have_no_message(id: message_2.id)
|
||||||
expect(open_thread).to have_no_message(thread.id, id: message_4.id)
|
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_4.id)
|
||||||
expect(open_thread).to have_no_message(thread.id, id: message_5.id)
|
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_5.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -86,8 +86,8 @@ describe "Thread indicator for chat messages", type: :system, js: true do
|
|||||||
message_without_thread = Fabricate(:chat_message, chat_channel: channel, user: other_user)
|
message_without_thread = Fabricate(:chat_message, chat_channel: channel, user: other_user)
|
||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.reply_to(message_without_thread)
|
channel_page.reply_to(message_without_thread)
|
||||||
channel_page.fill_composer("this is a reply to make a new thread")
|
open_thread.fill_composer("this is a reply to make a new thread")
|
||||||
channel_page.click_send_message
|
open_thread.click_send_message
|
||||||
|
|
||||||
expect(channel_page).to have_thread_indicator(message_without_thread)
|
expect(channel_page).to have_thread_indicator(message_without_thread)
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ describe "Thread indicator for chat messages", type: :system, js: true do
|
|||||||
)
|
)
|
||||||
channel_page.message_thread_indicator(thread_1.original_message).click
|
channel_page.message_thread_indicator(thread_1.original_message).click
|
||||||
expect(side_panel).to have_open_thread(thread_1)
|
expect(side_panel).to have_open_thread(thread_1)
|
||||||
open_thread.send_message(thread_1.id, "new thread message")
|
open_thread.send_message("new thread message")
|
||||||
expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_css(
|
expect(channel_page.message_thread_indicator(thread_1.original_message)).to have_css(
|
||||||
".chat-message-thread-indicator__replies-count",
|
".chat-message-thread-indicator__replies-count",
|
||||||
text: I18n.t("js.chat.thread.replies", count: 4),
|
text: I18n.t("js.chat.thread.replies", count: 4),
|
||||||
|
@ -25,6 +25,7 @@ module PageObjects
|
|||||||
|
|
||||||
def visit_thread(thread)
|
def visit_thread(thread)
|
||||||
visit(thread.url)
|
visit(thread.url)
|
||||||
|
has_no_css?(".chat-skeleton")
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit_channel_settings(channel)
|
def visit_channel_settings(channel)
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
module PageObjects
|
module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
class ChatChannel < PageObjects::Pages::Base
|
class ChatChannel < PageObjects::Pages::Base
|
||||||
|
def replying_to?(message)
|
||||||
|
find(".chat-channel .chat-reply", text: message.message)
|
||||||
|
end
|
||||||
|
|
||||||
def type_in_composer(input)
|
def type_in_composer(input)
|
||||||
find(".chat-channel .chat-composer__input").click # makes helper more reliable by ensuring focus is not lost
|
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)
|
find(".chat-channel .chat-composer__input").send_keys(input)
|
||||||
@ -49,7 +53,7 @@ module PageObjects
|
|||||||
def click_message_action_mobile(message, message_action)
|
def click_message_action_mobile(message, message_action)
|
||||||
expand_message_actions_mobile(message, delay: 0.5)
|
expand_message_actions_mobile(message, delay: 0.5)
|
||||||
wait_for_animation(find(".chat-message-actions"), timeout: 5)
|
wait_for_animation(find(".chat-message-actions"), timeout: 5)
|
||||||
find(".chat-message-action-item[data-id=\"#{message_action}\"] button").click
|
find(".chat-message-actions [data-id=\"#{message_action}\"]").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def hover_message(message)
|
def hover_message(message)
|
||||||
@ -114,8 +118,12 @@ module PageObjects
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reply_to(message)
|
def reply_to(message)
|
||||||
hover_message(message)
|
if page.has_css?("html.mobile-view", wait: 0)
|
||||||
find(".reply-btn").click
|
click_message_action_mobile(message, "reply")
|
||||||
|
else
|
||||||
|
hover_message(message)
|
||||||
|
find(".reply-btn").click
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_bookmarked_message?(message)
|
def has_bookmarked_message?(message)
|
||||||
@ -167,18 +175,22 @@ module PageObjects
|
|||||||
def check_message_presence(exists: true, text: nil, id: nil)
|
def check_message_presence(exists: true, text: nil, id: nil)
|
||||||
css_method = exists ? :has_css? : :has_no_css?
|
css_method = exists ? :has_css? : :has_no_css?
|
||||||
if text
|
if text
|
||||||
send(css_method, ".chat-message-text", text: text, wait: 5)
|
find(".chat-channel").send(css_method, ".chat-message-text", text: text, wait: 5)
|
||||||
elsif id
|
elsif id
|
||||||
send(css_method, ".chat-message-container[data-id=\"#{id}\"]", wait: 10)
|
find(".chat-channel").send(
|
||||||
|
css_method,
|
||||||
|
".chat-message-container[data-id=\"#{id}\"]",
|
||||||
|
wait: 10,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_thread_indicator?(message)
|
def has_thread_indicator?(message, text: nil)
|
||||||
has_css?(message_thread_indicator_selector(message))
|
has_css?(message_thread_indicator_selector(message), text: text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_thread_indicator?(message)
|
def has_no_thread_indicator?(message, text: nil)
|
||||||
has_no_css?(message_thread_indicator_selector(message))
|
has_no_css?(message_thread_indicator_selector(message), text: text)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_thread_indicator(message)
|
def message_thread_indicator(message)
|
||||||
|
@ -3,8 +3,12 @@
|
|||||||
module PageObjects
|
module PageObjects
|
||||||
module Pages
|
module Pages
|
||||||
class ChatSidePanel < PageObjects::Pages::Base
|
class ChatSidePanel < PageObjects::Pages::Base
|
||||||
def has_open_thread?(thread)
|
def has_open_thread?(thread = nil)
|
||||||
has_css?(".chat-side-panel .chat-thread[data-id='#{thread.id}']")
|
if thread
|
||||||
|
has_css?(".chat-side-panel .chat-thread[data-id='#{thread.id}']")
|
||||||
|
else
|
||||||
|
has_css?(".chat-side-panel .chat-thread")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_open_thread?
|
def has_no_open_thread?
|
||||||
|
@ -19,10 +19,6 @@ module PageObjects
|
|||||||
header.has_content?(content)
|
header.has_content?(content)
|
||||||
end
|
end
|
||||||
|
|
||||||
def thread_selector_by_id(id)
|
|
||||||
".chat-thread[data-id=\"#{id}\"]"
|
|
||||||
end
|
|
||||||
|
|
||||||
def has_no_loading_skeleton?
|
def has_no_loading_skeleton?
|
||||||
has_no_css?(".chat-thread__messages .chat-skeleton")
|
has_no_css?(".chat-thread__messages .chat-skeleton")
|
||||||
end
|
end
|
||||||
@ -41,42 +37,32 @@ module PageObjects
|
|||||||
find(".chat-thread .chat-composer__input").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
|
end
|
||||||
|
|
||||||
def send_message(id, text = nil)
|
def send_message(text = nil)
|
||||||
text = text.chomp if text.present? # having \n on the end of the string counts as an Enter keypress
|
text = text.chomp if text.present? # having \n on the end of the string counts as an Enter keypress
|
||||||
fill_composer(text)
|
fill_composer(text)
|
||||||
click_send_message(id)
|
click_send_message
|
||||||
click_composer
|
click_composer
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_send_message(id)
|
def click_send_message
|
||||||
find(thread_selector_by_id(id)).find(
|
find(".chat-thread .chat-composer.is-send-enabled .chat-composer__send-btn").click
|
||||||
".chat-composer.is-send-enabled .chat-composer__send-btn",
|
|
||||||
).click
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_message?(thread_id, text: nil, id: nil)
|
def has_message?(text: nil, id: nil, thread_id: nil)
|
||||||
check_message_presence(thread_id, exists: true, text: text, id: id)
|
check_message_presence(exists: true, text: text, id: id, thread_id: thread_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_no_message?(thread_id, text: nil, id: nil)
|
def has_no_message?(text: nil, id: nil, thread_id: nil)
|
||||||
check_message_presence(thread_id, exists: false, text: text, id: id)
|
check_message_presence(exists: false, text: text, id: id, thread_id: thread_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_message_presence(thread_id, exists: true, text: nil, id: nil)
|
def check_message_presence(exists: true, text: nil, id: nil, thread_id: nil)
|
||||||
css_method = exists ? :has_css? : :has_no_css?
|
css_method = exists ? :has_css? : :has_no_css?
|
||||||
|
selector = thread_id ? ".chat-thread[data-id=\"#{thread_id}\"]" : ".chat-thread"
|
||||||
if text
|
if text
|
||||||
find(thread_selector_by_id(thread_id)).send(
|
find(selector).send(css_method, ".chat-message-text", text: text, wait: 5)
|
||||||
css_method,
|
|
||||||
".chat-message-text",
|
|
||||||
text: text,
|
|
||||||
wait: 5,
|
|
||||||
)
|
|
||||||
elsif id
|
elsif id
|
||||||
find(thread_selector_by_id(thread_id)).send(
|
find(selector).send(css_method, ".chat-message-container[data-id=\"#{id}\"]", wait: 10)
|
||||||
css_method,
|
|
||||||
".chat-message-container[data-id=\"#{id}\"]",
|
|
||||||
wait: 10,
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,8 +31,12 @@ module PageObjects
|
|||||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_open_thread?(thread)
|
def has_open_thread?(thread = nil)
|
||||||
has_css?("#{VISIBLE_DRAWER} .chat-thread[data-id='#{thread.id}']")
|
if thread
|
||||||
|
has_css?("#{VISIBLE_DRAWER} .chat-thread[data-id='#{thread.id}']")
|
||||||
|
else
|
||||||
|
has_css?("#{VISIBLE_DRAWER} .chat-thread")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_open_channel?(channel)
|
def has_open_channel?(channel)
|
||||||
|
99
plugins/chat/spec/system/reply_to_message/drawer_spec.rb
Normal file
99
plugins/chat/spec/system/reply_to_message/drawer_spec.rb
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Reply to message - channel - drawer", type: :system, js: true do
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||||
|
let(:drawer_page) { PageObjects::Pages::ChatDrawer.new }
|
||||||
|
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
fab!(:original_message) do
|
||||||
|
Fabricate(:chat_message, chat_channel: channel_1, user: Fabricate(:user))
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||||
|
chat_system_bootstrap
|
||||||
|
channel_1.update!(threading_enabled: true)
|
||||||
|
channel_1.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has not current thread" do
|
||||||
|
it "starts a thread" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_from_header
|
||||||
|
drawer_page.open_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
expect(drawer_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
drawer_page.back
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has an existing thread" do
|
||||||
|
fab!(:message_1) do
|
||||||
|
creator =
|
||||||
|
Chat::MessageCreator.new(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
in_reply_to_id: original_message.id,
|
||||||
|
user: Fabricate(:user),
|
||||||
|
content: Faker::Lorem.paragraph,
|
||||||
|
)
|
||||||
|
creator.create
|
||||||
|
creator.chat_message
|
||||||
|
end
|
||||||
|
|
||||||
|
it "replies to the existing thread" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_from_header
|
||||||
|
drawer_page.open_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "1")
|
||||||
|
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(drawer_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: message_1.message)
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
drawer_page.back
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "2")
|
||||||
|
expect(channel_page).to have_no_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with threading disabled" do
|
||||||
|
before { channel_1.update!(threading_enabled: false) }
|
||||||
|
|
||||||
|
it "makes a reply in the channel" do
|
||||||
|
visit("/")
|
||||||
|
chat_page.open_from_header
|
||||||
|
drawer_page.open_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(page).to have_selector(
|
||||||
|
".chat-channel .chat-reply__excerpt",
|
||||||
|
text: original_message.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_page.fill_composer("reply to message")
|
||||||
|
channel_page.click_send_message
|
||||||
|
|
||||||
|
expect(channel_page).to have_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
103
plugins/chat/spec/system/reply_to_message/full_page_spec.rb
Normal file
103
plugins/chat/spec/system/reply_to_message/full_page_spec.rb
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Reply to message - channel - full page", type: :system, js: true do
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||||
|
let(:side_panel_page) { PageObjects::Pages::ChatSidePanel.new }
|
||||||
|
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
fab!(:original_message) do
|
||||||
|
Fabricate(:chat_message, chat_channel: channel_1, user: Fabricate(:user))
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||||
|
chat_system_bootstrap
|
||||||
|
channel_1.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
channel_1.update!(threading_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has not current thread" do
|
||||||
|
it "starts a thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(side_panel_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when reloading after creating thread" do
|
||||||
|
it "correctly loads the thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has an existing thread" do
|
||||||
|
fab!(:message_1) do
|
||||||
|
creator =
|
||||||
|
Chat::MessageCreator.new(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
in_reply_to_id: original_message.id,
|
||||||
|
user: Fabricate(:user),
|
||||||
|
content: Faker::Lorem.paragraph,
|
||||||
|
)
|
||||||
|
creator.create
|
||||||
|
creator.chat_message
|
||||||
|
end
|
||||||
|
|
||||||
|
it "replies to the existing thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "1")
|
||||||
|
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(side_panel_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: message_1.message)
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "2")
|
||||||
|
expect(channel_page).to have_no_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with threading disabled" do
|
||||||
|
before { channel_1.update!(threading_enabled: false) }
|
||||||
|
|
||||||
|
it "makes a reply in the channel" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(page).to have_selector(
|
||||||
|
".chat-channel .chat-reply__excerpt",
|
||||||
|
text: original_message.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_page.fill_composer("reply to message")
|
||||||
|
channel_page.click_send_message
|
||||||
|
|
||||||
|
expect(channel_page).to have_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
109
plugins/chat/spec/system/reply_to_message/mobile_spec.rb
Normal file
109
plugins/chat/spec/system/reply_to_message/mobile_spec.rb
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Reply to message - channel - mobile", type: :system, js: true, mobile: true do
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||||
|
let(:side_panel_page) { PageObjects::Pages::ChatSidePanel.new }
|
||||||
|
|
||||||
|
fab!(:current_user) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
fab!(:original_message) do
|
||||||
|
Fabricate(:chat_message, chat_channel: channel_1, user: Fabricate(:user))
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||||
|
chat_system_bootstrap
|
||||||
|
channel_1.update!(threading_enabled: true)
|
||||||
|
channel_1.add(current_user)
|
||||||
|
sign_in(current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has not current thread" do
|
||||||
|
it "starts a thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(side_panel_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
thread_page.close
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when reloading after creating thread" do
|
||||||
|
it "correctly loads the thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when the message has an existing thread" do
|
||||||
|
fab!(:message_1) do
|
||||||
|
creator =
|
||||||
|
Chat::MessageCreator.new(
|
||||||
|
chat_channel: channel_1,
|
||||||
|
in_reply_to_id: original_message.id,
|
||||||
|
user: Fabricate(:user),
|
||||||
|
content: Faker::Lorem.paragraph,
|
||||||
|
)
|
||||||
|
creator.create
|
||||||
|
creator.chat_message
|
||||||
|
end
|
||||||
|
|
||||||
|
it "replies to the existing thread" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "1")
|
||||||
|
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(side_panel_page).to have_open_thread
|
||||||
|
|
||||||
|
thread_page.fill_composer("reply to message")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: message_1.message)
|
||||||
|
expect(thread_page).to have_message(text: "reply to message")
|
||||||
|
|
||||||
|
thread_page.close
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: "2")
|
||||||
|
expect(channel_page).to have_no_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with threading disabled" do
|
||||||
|
before { channel_1.update!(threading_enabled: false) }
|
||||||
|
|
||||||
|
it "makes a reply in the channel" do
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
|
||||||
|
expect(page).to have_selector(
|
||||||
|
".chat-channel .chat-reply__excerpt",
|
||||||
|
text: original_message.message,
|
||||||
|
)
|
||||||
|
|
||||||
|
channel_page.fill_composer("reply to message")
|
||||||
|
channel_page.click_send_message
|
||||||
|
|
||||||
|
expect(channel_page).to have_message(text: "reply to message")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
77
plugins/chat/spec/system/reply_to_message/smoke_spec.rb
Normal file
77
plugins/chat/spec/system/reply_to_message/smoke_spec.rb
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
RSpec.describe "Reply to message - smoke", type: :system, js: true do
|
||||||
|
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||||
|
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||||
|
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||||
|
|
||||||
|
fab!(:user_1) { Fabricate(:user) }
|
||||||
|
fab!(:user_2) { Fabricate(:user) }
|
||||||
|
fab!(:channel_1) { Fabricate(:category_channel) }
|
||||||
|
fab!(:original_message) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||||
|
chat_system_bootstrap
|
||||||
|
channel_1.add(user_1)
|
||||||
|
channel_1.add(user_2)
|
||||||
|
channel_1.update!(threading_enabled: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when two users create a thread on the same message" do
|
||||||
|
it "works" do
|
||||||
|
using_session(:user_1) do
|
||||||
|
sign_in(user_1)
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session(:user_2) do
|
||||||
|
sign_in(user_2)
|
||||||
|
chat_page.visit_channel(channel_1)
|
||||||
|
channel_page.reply_to(original_message)
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session(:user_1) do
|
||||||
|
thread_page.fill_composer("user1reply")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 1)
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "user1reply")
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session(:user_2) do |session|
|
||||||
|
expect(thread_page).to have_message(text: "user1reply")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 1)
|
||||||
|
|
||||||
|
thread_page.fill_composer("user2reply")
|
||||||
|
thread_page.click_send_message
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "user2reply")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 2)
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "user1reply")
|
||||||
|
expect(thread_page).to have_message(text: "user2reply")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 2)
|
||||||
|
|
||||||
|
session.quit
|
||||||
|
end
|
||||||
|
|
||||||
|
using_session(:user_1) do |session|
|
||||||
|
expect(thread_page).to have_message(text: "user2reply")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 2)
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(thread_page).to have_message(text: "user1reply")
|
||||||
|
expect(thread_page).to have_message(text: "user2reply")
|
||||||
|
expect(channel_page).to have_thread_indicator(original_message, text: 2)
|
||||||
|
|
||||||
|
session.quit
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -92,11 +92,10 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
|
|||||||
fab!(:message_1) do
|
fab!(:message_1) do
|
||||||
Fabricate(:chat_message, message: "message 1", chat_channel: channel_1, user: current_user)
|
Fabricate(:chat_message, message: "message 1", chat_channel: channel_1, user: current_user)
|
||||||
end
|
end
|
||||||
before { Fabricate(:chat_message, message: "message 2", chat_channel: channel_1) }
|
fab!(:message_2) { Fabricate(:chat_message, message: "message 2", chat_channel: channel_1) }
|
||||||
|
|
||||||
it "edits last editable message" do
|
it "edits last editable message" do
|
||||||
chat.visit_channel(channel_1)
|
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)
|
||||||
|
|
||||||
@ -116,5 +115,15 @@ RSpec.describe "Shortcuts | chat composer", type: :system, js: true do
|
|||||||
page.driver.browser.network_conditions = { offline: false }
|
page.driver.browser.network_conditions = { offline: false }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "with shift" do
|
||||||
|
it "starts replying to the last message" do
|
||||||
|
chat.visit_channel(channel_1)
|
||||||
|
|
||||||
|
find(".chat-composer__input").send_keys(%i[shift arrow_up])
|
||||||
|
|
||||||
|
expect(channel_page).to be_replying_to(message_2)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -123,8 +123,8 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.message_thread_indicator(thread.original_message).click
|
channel_page.message_thread_indicator(thread.original_message).click
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
thread_page.send_message(thread.id, "new thread message")
|
thread_page.send_message("new thread message")
|
||||||
expect(thread_page).to have_message(thread.id, text: "new thread message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "new thread message")
|
||||||
thread_message = thread.replies.last
|
thread_message = thread.replies.last
|
||||||
expect(thread_message.chat_channel_id).to eq(channel.id)
|
expect(thread_message.chat_channel_id).to eq(channel.id)
|
||||||
expect(thread_message.thread.channel_id).to eq(channel.id)
|
expect(thread_message.thread.channel_id).to eq(channel.id)
|
||||||
@ -134,8 +134,8 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||||||
chat_page.visit_channel(channel)
|
chat_page.visit_channel(channel)
|
||||||
channel_page.message_thread_indicator(thread.original_message).click
|
channel_page.message_thread_indicator(thread.original_message).click
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
thread_page.send_message(thread.id, "new thread message")
|
thread_page.send_message("new thread message")
|
||||||
expect(thread_page).to have_message(thread.id, text: "new thread message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "new thread message")
|
||||||
thread_message = thread.reload.replies.last
|
thread_message = thread.reload.replies.last
|
||||||
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(thread_message.id))
|
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(thread_message.id))
|
||||||
end
|
end
|
||||||
@ -157,19 +157,19 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||||||
|
|
||||||
using_session(:tab_2) do
|
using_session(:tab_2) do
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
thread_page.send_message(thread.id, "the other user message")
|
thread_page.send_message("the other user message")
|
||||||
expect(thread_page).to have_message(thread.id, text: "the other user message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
|
||||||
end
|
end
|
||||||
|
|
||||||
using_session(:tab_1) do
|
using_session(:tab_1) do
|
||||||
expect(side_panel).to have_open_thread(thread)
|
expect(side_panel).to have_open_thread(thread)
|
||||||
expect(thread_page).to have_message(thread.id, text: "the other user message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
|
||||||
thread_page.send_message(thread.id, "this is a test message")
|
thread_page.send_message("this is a test message")
|
||||||
expect(thread_page).to have_message(thread.id, text: "this is a test message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
|
||||||
end
|
end
|
||||||
|
|
||||||
using_session(:tab_2) do
|
using_session(:tab_2) do
|
||||||
expect(thread_page).to have_message(thread.id, text: "this is a test message")
|
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user