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],
|
||||
upload_ids: params[:upload_ids],
|
||||
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?
|
||||
|
|
|
@ -14,7 +14,7 @@ module Chat
|
|||
"#{root_message_bus_channel(chat_channel_id)}/thread/#{thread_id}"
|
||||
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)
|
||||
|
||||
if message.thread_om?
|
||||
|
@ -22,8 +22,10 @@ module Chat
|
|||
root_message_bus_channel(channel.id),
|
||||
thread_message_bus_channel(channel.id, message.thread_id),
|
||||
]
|
||||
elsif message.thread_reply?
|
||||
[thread_message_bus_channel(channel.id, message.thread_id)]
|
||||
elsif staged_thread_id || message.thread_reply?
|
||||
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
|
||||
[root_message_bus_channel(channel.id)]
|
||||
end
|
||||
|
@ -33,12 +35,16 @@ module Chat
|
|||
SiteSetting.enable_experimental_chat_threaded_discussions && channel.threading_enabled
|
||||
end
|
||||
|
||||
def self.publish_new!(chat_channel, chat_message, staged_id)
|
||||
message_bus_targets = calculate_publish_targets(chat_channel, chat_message)
|
||||
def self.publish_new!(chat_channel, chat_message, staged_id, staged_thread_id: nil)
|
||||
message_bus_targets =
|
||||
calculate_publish_targets(chat_channel, chat_message, staged_thread_id: staged_thread_id)
|
||||
publish_to_targets!(
|
||||
message_bus_targets,
|
||||
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
|
||||
|
@ -70,8 +76,15 @@ module Chat
|
|||
)
|
||||
end
|
||||
|
||||
def self.publish_thread_created!(chat_channel, chat_message)
|
||||
publish_to_channel!(chat_channel, serialize_message_with_type(chat_message, :thread_created))
|
||||
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,
|
||||
{ thread_id: thread_id, staged_thread_id: staged_thread_id },
|
||||
),
|
||||
)
|
||||
end
|
||||
|
||||
def self.publish_processed!(chat_message)
|
||||
|
@ -215,11 +228,12 @@ module Chat
|
|||
end
|
||||
end
|
||||
|
||||
def self.serialize_message_with_type(chat_message, type)
|
||||
def self.serialize_message_with_type(chat_message, type, options = {})
|
||||
Chat::MessageSerializer
|
||||
.new(chat_message, { scope: anonymous_guardian, root: :chat_message })
|
||||
.as_json
|
||||
.merge(type: type)
|
||||
.merge(options)
|
||||
end
|
||||
|
||||
def self.user_tracking_state_message_bus_channel(user_id)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { capitalize } from "@ember/string";
|
||||
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 { bind, debounce } from "discourse-common/utils/decorators";
|
||||
import { action } from "@ember/object";
|
||||
|
@ -352,7 +353,13 @@ export default class ChatLivePane extends Component {
|
|||
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];
|
||||
|
@ -548,7 +555,11 @@ export default class ChatLivePane extends Component {
|
|||
}
|
||||
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
{{did-update this.didUpdateMessage this.currentMessage}}
|
||||
{{did-update this.didUpdateInReplyTo this.currentMessage.inReplyTo}}
|
||||
{{did-insert this.setupAppEvents}}
|
||||
{{will-destroy this.teardownAppEvents}}
|
||||
{{will-destroy this.teardown}}
|
||||
{{will-destroy this.cancelPersistDraft}}
|
||||
>
|
||||
<div class="chat-composer__outer-container">
|
||||
|
|
|
@ -17,6 +17,8 @@ import I18n from "I18n";
|
|||
import { translations } from "pretty-text/emoji/data";
|
||||
import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
|
||||
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 {
|
||||
@service capabilities;
|
||||
|
@ -39,7 +41,10 @@ export default class ChatComposer extends Component {
|
|||
}
|
||||
|
||||
get shouldRenderMessageDetails() {
|
||||
return this.currentMessage?.editing || this.currentMessage?.inReplyTo;
|
||||
return (
|
||||
this.currentMessage?.editing ||
|
||||
(this.context === "channel" && this.currentMessage?.inReplyTo)
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
persistDraft() {}
|
||||
|
||||
|
@ -133,13 +150,14 @@ export default class ChatComposer extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
teardownAppEvents() {
|
||||
teardown() {
|
||||
this.appEvents.off("chat:modify-selection", this, "modifySelection");
|
||||
this.appEvents.off(
|
||||
"chat:open-insert-link-modal",
|
||||
this,
|
||||
"openInsertLinkModal"
|
||||
);
|
||||
this.pane.sending = false;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -221,7 +239,7 @@ export default class ChatComposer extends Component {
|
|||
}
|
||||
|
||||
reportReplyingPresence() {
|
||||
if (!this.args.channel) {
|
||||
if (!this.args.channel || !this.currentMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -305,9 +323,13 @@ export default class ChatComposer extends Component {
|
|||
!this.hasContent &&
|
||||
!this.currentMessage.editing
|
||||
) {
|
||||
const editableMessage = this.pane?.lastCurrentUserMessage;
|
||||
if (editableMessage) {
|
||||
this.composer.editMessage(editableMessage);
|
||||
if (event.shiftKey) {
|
||||
this.composer.replyTo(this.pane?.lastMessage);
|
||||
} else {
|
||||
const editableMessage = this.pane?.lastCurrentUserMessage;
|
||||
if (editableMessage) {
|
||||
this.composer.editMessage(editableMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
{{did-update this.fetchChannelAndThread @params.threadId}}
|
||||
>
|
||||
{{#if this.chat.activeChannel.activeThread}}
|
||||
<ChatThread />
|
||||
<ChatThread @thread={{this.chat.activeChannel.activeThread}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -1,13 +1,16 @@
|
|||
{{#if (and this.site.desktopView this.chat.activeMessage.model.id)}}
|
||||
<div
|
||||
{{did-insert this.setupPopper}}
|
||||
{{did-update this.setupPopper this.chat.activeMessage.model.id}}
|
||||
{{will-destroy this.teardownPopper}}
|
||||
class="chat-message-actions-container"
|
||||
{{did-insert this.setup}}
|
||||
{{did-update this.setup this.chat.activeMessage.model.id}}
|
||||
{{will-destroy this.teardown}}
|
||||
class={{concat-class
|
||||
"chat-message-actions-container"
|
||||
(concat "is-size-" this.size)
|
||||
}}
|
||||
data-id={{this.message.id}}
|
||||
>
|
||||
<div class="chat-message-actions">
|
||||
{{#if this.chatStateManager.isFullPageActive}}
|
||||
{{#if this.shouldRenderFavoriteReactions}}
|
||||
{{#each
|
||||
this.messageInteractor.emojiReactions
|
||||
key="emoji"
|
||||
|
@ -50,7 +53,12 @@
|
|||
/>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.messageInteractor.secondaryButtons.length}}
|
||||
{{#if
|
||||
(and
|
||||
this.messageInteractor.message
|
||||
this.messageInteractor.secondaryButtons.length
|
||||
)
|
||||
}}
|
||||
<DropdownSelectBox
|
||||
@class="more-buttons"
|
||||
@options={{hash icon="ellipsis-v" placement="left"}}
|
||||
|
|
|
@ -6,15 +6,20 @@ import { schedule } from "@ember/runloop";
|
|||
import { createPopper } from "@popperjs/core";
|
||||
import chatMessageContainer from "discourse/plugins/chat/discourse/lib/chat-message-container";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
const MSG_ACTIONS_VERTICAL_PADDING = -10;
|
||||
const FULL = "full";
|
||||
const REDUCED = "reduced";
|
||||
const REDUCED_WIDTH_THRESHOLD = 500;
|
||||
|
||||
export default class ChatMessageActionsDesktop extends Component {
|
||||
@service chat;
|
||||
@service chatStateManager;
|
||||
@service chatEmojiPickerManager;
|
||||
@service site;
|
||||
|
||||
@tracked size = FULL;
|
||||
|
||||
popper = null;
|
||||
|
||||
get message() {
|
||||
|
@ -35,8 +40,12 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
get shouldRenderFavoriteReactions() {
|
||||
return this.size === FULL;
|
||||
}
|
||||
|
||||
@action
|
||||
setupPopper(element) {
|
||||
setup(element) {
|
||||
this.popper?.destroy();
|
||||
|
||||
schedule("afterRender", () => {
|
||||
|
@ -45,6 +54,10 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||
this.context
|
||||
);
|
||||
|
||||
const viewport = messageContainer.closest(".popper-viewport");
|
||||
this.size =
|
||||
viewport.clientWidth < REDUCED_WIDTH_THRESHOLD ? REDUCED : FULL;
|
||||
|
||||
if (!messageContainer) {
|
||||
return;
|
||||
}
|
||||
|
@ -57,7 +70,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||
name: "flip",
|
||||
enabled: true,
|
||||
options: {
|
||||
boundary: messageContainer.closest(".popper-viewport"),
|
||||
boundary: viewport,
|
||||
fallbackPlacements: ["bottom-end"],
|
||||
},
|
||||
},
|
||||
|
@ -73,7 +86,7 @@ export default class ChatMessageActionsDesktop extends Component {
|
|||
}
|
||||
|
||||
@action
|
||||
teardownPopper() {
|
||||
teardown() {
|
||||
this.popper?.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
@icon="discourse-emojis"
|
||||
@title="chat.react"
|
||||
@forwardEvent={{true}}
|
||||
data-id="react"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
|
@ -71,6 +72,7 @@
|
|||
<DButton
|
||||
@class="btn-flat bookmark-btn"
|
||||
@action={{action this.actAndCloseMenu "toggleBookmark"}}
|
||||
data-id="bookmark"
|
||||
>
|
||||
<BookmarkIcon @bookmark={{this.message.bookmark}} />
|
||||
</DButton>
|
||||
|
@ -82,6 +84,7 @@
|
|||
@action={{action this.actAndCloseMenu "reply"}}
|
||||
@icon="reply"
|
||||
@title="chat.reply"
|
||||
data-id="reply"
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@ export default class ChatMessageInReplyToIndicator extends Component {
|
|||
if (this.hasThread) {
|
||||
return [
|
||||
...this.args.message.channel.routeModels,
|
||||
this.args.message.threadId,
|
||||
this.args.message.thread.id,
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
|
@ -29,7 +29,7 @@ export default class ChatMessageInReplyToIndicator extends Component {
|
|||
get hasThread() {
|
||||
return (
|
||||
this.args.message?.channel?.threadingEnabled &&
|
||||
this.args.message?.threadId
|
||||
this.args.message?.thread?.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<LinkTo
|
||||
@route="chat.channel.thread"
|
||||
@models={{@message.threadRouteModels}}
|
||||
@models={{@message.thread.routeModels}}
|
||||
class="chat-message-thread-indicator"
|
||||
>
|
||||
<span class="chat-message-thread-indicator__replies-count">
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
(if @message.highlighted "highlighted")
|
||||
}}
|
||||
data-id={{@message.id}}
|
||||
data-thread-id={{@message.threadId}}
|
||||
data-thread-id={{@message.thread.id}}
|
||||
{{chat/track-message
|
||||
(hash
|
||||
didEnterViewport=(fn @messageDidEnterViewport @message)
|
||||
|
|
|
@ -282,15 +282,15 @@ export default class ChatMessage extends Component {
|
|||
}
|
||||
|
||||
get threadingEnabled() {
|
||||
return this.args.channel?.threadingEnabled && this.args.message?.threadId;
|
||||
return this.args.channel?.threadingEnabled && !!this.args.message?.thread;
|
||||
}
|
||||
|
||||
get showThreadIndicator() {
|
||||
return (
|
||||
this.args.context !== MESSAGE_CONTEXT_THREAD &&
|
||||
this.threadingEnabled &&
|
||||
this.args.message?.threadId !==
|
||||
this.args.message?.previousMessage?.threadId
|
||||
this.args.message?.thread &&
|
||||
this.args.message?.threadReplyCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -352,7 +352,7 @@ export default class ChatMessage extends Component {
|
|||
inviteMentioned() {
|
||||
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",
|
||||
data: { user_ids: userIds, chat_message_id: this.args.message.id },
|
||||
}).then(() => {
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
data-id={{this.thread.id}}
|
||||
{{did-insert this.setUploadDropZone}}
|
||||
{{did-insert this.subscribeToUpdates}}
|
||||
{{did-insert this.loadMessages}}
|
||||
{{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}}
|
||||
{{will-destroy this.unsubscribeFromUpdates}}
|
||||
>
|
||||
|
@ -42,7 +42,7 @@
|
|||
@context="thread"
|
||||
/>
|
||||
{{/each}}
|
||||
{{#if (or this.loading this.loadingMoreFuture)}}
|
||||
{{#if this.loading}}
|
||||
<ChatSkeleton />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { bind, debounce } from "discourse-common/utils/decorators";
|
|||
import { inject as service } from "@ember/service";
|
||||
import { schedule } from "@ember/runloop";
|
||||
import discourseLater from "discourse-common/lib/later";
|
||||
import { resetIdle } from "discourse/lib/desktop-notifications";
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
|
@ -25,22 +26,16 @@ export default class ChatThreadPanel extends Component {
|
|||
@service capabilities;
|
||||
|
||||
@tracked loading;
|
||||
@tracked loadingMorePast;
|
||||
@tracked uploadDropZone;
|
||||
|
||||
scrollable = null;
|
||||
|
||||
get thread() {
|
||||
return this.channel.activeThread;
|
||||
return this.args.thread;
|
||||
}
|
||||
|
||||
get channel() {
|
||||
return this.chat.activeChannel;
|
||||
}
|
||||
|
||||
@action
|
||||
subscribeToUpdates() {
|
||||
this.chatChannelThreadPaneSubscriptionsManager.subscribe(this.thread);
|
||||
return this.thread?.channel;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -56,6 +51,11 @@ export default class ChatThreadPanel extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
@action
|
||||
subscribeToUpdates() {
|
||||
this.chatChannelThreadPaneSubscriptionsManager.subscribe(this.thread);
|
||||
}
|
||||
|
||||
@action
|
||||
unsubscribeFromUpdates() {
|
||||
this.chatChannelThreadPaneSubscriptionsManager.unsubscribe();
|
||||
|
@ -69,17 +69,7 @@ export default class ChatThreadPanel extends Component {
|
|||
@action
|
||||
loadMessages() {
|
||||
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();
|
||||
// }
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -97,21 +87,14 @@ export default class ChatThreadPanel extends Component {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.loadingMorePast = true;
|
||||
if (this.thread.staged) {
|
||||
this.thread.messagesManager.addMessages([this.thread.originalMessage]);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
const findArgs = { pageSize: PAGE_SIZE };
|
||||
|
||||
// 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;
|
||||
|
||||
const findArgs = { pageSize: PAGE_SIZE, threadId: this.thread.id };
|
||||
return this.chatApi
|
||||
.messages(this.channel.id, findArgs)
|
||||
.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);
|
||||
|
||||
// TODO (martin) details needed for thread??
|
||||
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();
|
||||
})
|
||||
.catch(this.#handleErrors)
|
||||
|
@ -148,16 +121,12 @@ export default class ChatThreadPanel extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.requestedTargetMessageId = null;
|
||||
this.loading = false;
|
||||
this.loadingMorePast = false;
|
||||
|
||||
// this.fillPaneAttempt();
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
afterFetchCallback(channel, results) {
|
||||
afterFetchCallback(channel, thread, results) {
|
||||
const messages = [];
|
||||
|
||||
results.chat_messages.forEach((messageData) => {
|
||||
|
@ -170,13 +139,10 @@ export default class ChatThreadPanel extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.requestedTargetMessageId === messageData.id) {
|
||||
messageData.expanded = !messageData.hidden;
|
||||
} else {
|
||||
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
||||
}
|
||||
|
||||
messages.push(ChatMessage.create(channel, messageData));
|
||||
messageData.expanded = !(messageData.hidden || messageData.deleted_at);
|
||||
const message = ChatMessage.create(channel, messageData);
|
||||
message.thread = thread;
|
||||
messages.push(message);
|
||||
});
|
||||
|
||||
return [messages, results.meta];
|
||||
|
@ -191,6 +157,8 @@ export default class ChatThreadPanel extends Component {
|
|||
|
||||
@action
|
||||
onSendMessage(message) {
|
||||
resetIdle();
|
||||
|
||||
if (message.editing) {
|
||||
this.#sendEditMessage(message);
|
||||
} else {
|
||||
|
@ -200,7 +168,7 @@ export default class ChatThreadPanel extends Component {
|
|||
|
||||
@action
|
||||
resetComposer() {
|
||||
this.chatChannelThreadComposer.reset(this.channel);
|
||||
this.chatChannelThreadComposer.reset(this.channel, this.thread);
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -209,34 +177,27 @@ export default class ChatThreadPanel extends Component {
|
|||
}
|
||||
|
||||
#sendNewMessage(message) {
|
||||
// TODO (martin) For desktop notifications
|
||||
// resetIdle()
|
||||
message.thread = this.thread;
|
||||
|
||||
if (this.chatChannelThreadPane.sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
const stagedMessage = message;
|
||||
this.resetComposer();
|
||||
this.thread.messagesManager.addMessages([stagedMessage]);
|
||||
|
||||
// TODO (martin) Scrolling!!
|
||||
// if (!this.channel.canLoadMoreFuture) {
|
||||
// this.scrollToBottom();
|
||||
// }
|
||||
|
||||
return this.chatApi
|
||||
.sendMessage(this.channel.id, {
|
||||
message: stagedMessage.message,
|
||||
in_reply_to_id: stagedMessage.inReplyTo?.id,
|
||||
staged_id: stagedMessage.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(() => {
|
||||
this.scrollToBottom();
|
||||
|
@ -264,7 +225,7 @@ export default class ChatThreadPanel extends Component {
|
|||
this.resetComposer();
|
||||
|
||||
return this.chatApi
|
||||
.editMessage(message.channelId, message.id, data)
|
||||
.editMessage(message.channel.id, message.id, data)
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
this.chatChannelThreadPane.sending = false;
|
||||
|
@ -280,9 +241,9 @@ export default class ChatThreadPanel extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
this.scrollable.scrollTop = -1;
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight + 1;
|
||||
this.forceRendering(() => {
|
||||
this.scrollable.scrollTop = 0;
|
||||
this.scrollable.scrollTop = this.scrollable.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -319,7 +280,6 @@ export default class ChatThreadPanel extends Component {
|
|||
|
||||
@action
|
||||
resendStagedMessage() {}
|
||||
// resendStagedMessage(stagedMessage) {}
|
||||
|
||||
@action
|
||||
messageDidEnterViewport(message) {
|
||||
|
|
|
@ -3,8 +3,6 @@ import { inject as service } from "@ember/service";
|
|||
import I18n from "I18n";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { action } from "@ember/object";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
export default class ChatComposerChannel extends ChatComposer {
|
||||
@service("chat-channel-composer") composer;
|
||||
|
@ -14,18 +12,6 @@ export default class ChatComposerChannel extends ChatComposer {
|
|||
|
||||
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
|
||||
persistDraft() {
|
||||
if (this.args.channel?.isDraft) {
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
import ChatComposer from "../../chat-composer";
|
||||
import { inject as service } from "@ember/service";
|
||||
import I18n from "I18n";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
import { Promise } from "rsvp";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default class ChatComposerThread extends ChatComposer {
|
||||
@service("chat-channel-thread-composer") composer;
|
||||
@service("chat-channel-composer") channelComposer;
|
||||
@service("chat-channel-thread-pane") pane;
|
||||
@service router;
|
||||
|
||||
context = "thread";
|
||||
|
||||
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() {
|
||||
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) {
|
||||
return htmlSafe(
|
||||
`<span title='${title}' class='chat-time'>${display}</span>`
|
||||
`<span title='${title}' tabindex="-1" class='chat-time'>${display}</span>`
|
||||
);
|
||||
} else {
|
||||
const url = getURL(`/chat/c/-/${message.channel.id}/${message.id}`);
|
||||
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() {
|
||||
const { protocol, host } = window.location;
|
||||
const channelId = this.message.channelId;
|
||||
const threadId = this.message.threadId;
|
||||
const channelId = this.message.channel.id;
|
||||
const threadId = this.message.thread?.id;
|
||||
|
||||
let url;
|
||||
if (threadId) {
|
||||
|
@ -276,7 +276,7 @@ export default class ChatMessageInteractor {
|
|||
|
||||
return this.chatApi
|
||||
.publishReaction(
|
||||
this.message.channelId,
|
||||
this.message.channel.id,
|
||||
this.message.id,
|
||||
emoji,
|
||||
reactAction
|
||||
|
@ -329,21 +329,21 @@ export default class ChatMessageInteractor {
|
|||
@action
|
||||
delete() {
|
||||
return this.chatApi
|
||||
.trashMessage(this.message.channelId, this.message.id)
|
||||
.trashMessage(this.message.channel.id, this.message.id)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
restore() {
|
||||
return this.chatApi
|
||||
.restoreMessage(this.message.channelId, this.message.id)
|
||||
.restoreMessage(this.message.channel.id, this.message.id)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
@action
|
||||
rebake() {
|
||||
return this.chatApi
|
||||
.rebakeMessage(this.message.channelId, this.message.id)
|
||||
.rebakeMessage(this.message.channel.id, this.message.id)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import Promise from "rsvp";
|
|||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
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
|
||||
|
@ -39,11 +38,16 @@ export default class ChatThreadsManager {
|
|||
return Object.values(this._cached);
|
||||
}
|
||||
|
||||
store(threadObject) {
|
||||
store(channel, threadObject) {
|
||||
let model = this.#findStale(threadObject.id);
|
||||
|
||||
if (!model) {
|
||||
model = new ChatThread(threadObject);
|
||||
if (threadObject instanceof ChatThread) {
|
||||
model = threadObject;
|
||||
} else {
|
||||
model = new ChatThread(channel, threadObject);
|
||||
}
|
||||
|
||||
this.#cache(model);
|
||||
}
|
||||
|
||||
|
@ -59,12 +63,7 @@ export default class ChatThreadsManager {
|
|||
}
|
||||
|
||||
async #find(channelId, threadId) {
|
||||
return this.chatApi
|
||||
.thread(channelId, threadId)
|
||||
.catch(popupAjaxError)
|
||||
.then((thread) => {
|
||||
return thread;
|
||||
});
|
||||
return this.chatApi.thread(channelId, threadId);
|
||||
}
|
||||
|
||||
#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 { getOwner } from "discourse-common/lib/get-owner";
|
||||
import guid from "pretty-text/guid";
|
||||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||
|
||||
export const CHATABLE_TYPES = {
|
||||
directMessageChannel: "DirectMessage",
|
||||
|
@ -71,6 +72,10 @@ export default class ChatChannel extends RestModel {
|
|||
return this.messages.findIndex((m) => m.id === message.id);
|
||||
}
|
||||
|
||||
findMessage(id) {
|
||||
return this.messagesManager.findMessage(id);
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return this.messagesManager.messages;
|
||||
}
|
||||
|
@ -155,6 +160,23 @@ export default class ChatChannel extends RestModel {
|
|||
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) {
|
||||
message.id = guid();
|
||||
message.staged = true;
|
||||
|
@ -163,11 +185,6 @@ export default class ChatChannel extends RestModel {
|
|||
message.cook();
|
||||
|
||||
if (message.inReplyTo) {
|
||||
if (!message.inReplyTo.threadId) {
|
||||
message.inReplyTo.threadId = guid();
|
||||
message.inReplyTo.threadReplyCount = 1;
|
||||
}
|
||||
|
||||
if (!this.threadingEnabled) {
|
||||
this.messagesManager.addMessages([message]);
|
||||
}
|
||||
|
|
|
@ -31,8 +31,6 @@ export default class ChatMessage {
|
|||
@tracked deletedAt;
|
||||
@tracked uploads;
|
||||
@tracked excerpt;
|
||||
@tracked threadId;
|
||||
@tracked threadReplyCount;
|
||||
@tracked reactions;
|
||||
@tracked reviewableId;
|
||||
@tracked user;
|
||||
|
@ -50,11 +48,12 @@ export default class ChatMessage {
|
|||
@tracked highlighted = false;
|
||||
@tracked firstOfResults = false;
|
||||
@tracked message;
|
||||
|
||||
@tracked thread;
|
||||
@tracked threadReplyCount;
|
||||
@tracked _cooked;
|
||||
|
||||
constructor(channel, args = {}) {
|
||||
this.channel = channel;
|
||||
// when modifying constructor, be sure to update duplicate function accordingly
|
||||
this.id = args.id;
|
||||
this.newest = args.newest;
|
||||
this.firstOfResults = args.firstOfResults;
|
||||
|
@ -62,23 +61,23 @@ export default class ChatMessage {
|
|||
this.edited = args.edited;
|
||||
this.availableFlags = args.availableFlags || args.available_flags;
|
||||
this.hidden = args.hidden;
|
||||
this.threadId = args.threadId || args.thread_id;
|
||||
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.createdAt = args.createdAt || args.created_at;
|
||||
this.deletedAt = args.deletedAt || args.deleted_at;
|
||||
this.excerpt = args.excerpt;
|
||||
this.reviewableId = args.reviewableId || args.reviewable_id;
|
||||
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 =
|
||||
args.inReplyTo ||
|
||||
(args.in_reply_to || args.replyToMsg
|
||||
? ChatMessage.create(channel, args.in_reply_to || args.replyToMsg)
|
||||
: null);
|
||||
this.draft = args.draft;
|
||||
this.message = args.message || "";
|
||||
this._cooked = args.cooked || "";
|
||||
this.channel = channel;
|
||||
this.reactions = this.#initChatMessageReactionModel(
|
||||
args.id,
|
||||
args.reactions
|
||||
|
@ -88,6 +87,39 @@ export default class ChatMessage {
|
|||
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() {
|
||||
return this._cooked;
|
||||
}
|
||||
|
@ -131,10 +163,6 @@ export default class ChatMessage {
|
|||
}
|
||||
}
|
||||
|
||||
get threadRouteModels() {
|
||||
return [...this.channel.routeModels, this.threadId];
|
||||
}
|
||||
|
||||
get read() {
|
||||
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 { tracked } from "@glimmer/tracking";
|
||||
import guid from "pretty-text/guid";
|
||||
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
|
||||
|
||||
export const THREAD_STATUSES = {
|
||||
open: "open",
|
||||
|
@ -13,20 +14,25 @@ export const THREAD_STATUSES = {
|
|||
};
|
||||
|
||||
export default class ChatThread {
|
||||
@tracked id;
|
||||
@tracked title;
|
||||
@tracked status;
|
||||
@tracked draft;
|
||||
@tracked staged;
|
||||
@tracked channel;
|
||||
@tracked originalMessage;
|
||||
@tracked threadMessageBusLastId;
|
||||
|
||||
messagesManager = new ChatMessagesManager(getOwner(this));
|
||||
|
||||
constructor(args = {}) {
|
||||
constructor(channel, args = {}) {
|
||||
this.title = args.title;
|
||||
this.id = args.id;
|
||||
this.channelId = args.channel_id;
|
||||
this.channel = channel;
|
||||
this.status = args.status;
|
||||
|
||||
this.originalMessageUser = this.#initUserModel(args.original_message_user);
|
||||
this.originalMessage = args.original_message;
|
||||
this.originalMessage.user = this.originalMessageUser;
|
||||
this.draft = args.draft;
|
||||
this.staged = args.staged;
|
||||
this.originalMessage = ChatMessage.create(channel, args.original_message);
|
||||
}
|
||||
|
||||
stageMessage(message) {
|
||||
|
@ -39,6 +45,10 @@ export default class ChatThread {
|
|||
this.messagesManager.addMessages([message]);
|
||||
}
|
||||
|
||||
get routeModels() {
|
||||
return [...this.channel.routeModels, this.id];
|
||||
}
|
||||
|
||||
get messages() {
|
||||
return this.messagesManager.messages;
|
||||
}
|
||||
|
|
|
@ -5,24 +5,57 @@ export default class ChatChannelThread extends DiscourseRoute {
|
|||
@service router;
|
||||
@service chatStateManager;
|
||||
@service chat;
|
||||
@service chatStagedThreadMapping;
|
||||
@service chatChannelThreadPane;
|
||||
|
||||
async model(params) {
|
||||
model(params, transition) {
|
||||
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() {
|
||||
this.#closeThread();
|
||||
this.chatChannelThreadPane.close();
|
||||
}
|
||||
|
||||
#closeThread() {
|
||||
this.chat.activeChannel.activeThread?.messagesManager?.clearMessages();
|
||||
this.chat.activeChannel.activeThread = null;
|
||||
this.chatStateManager.closeSidePanel();
|
||||
beforeModel(transition) {
|
||||
const channel = this.modelFor("chat.channel");
|
||||
|
||||
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) {
|
||||
this.chat.activeChannel.activeThread = model;
|
||||
this.chatStateManager.openSidePanel();
|
||||
this.chatChannelThreadPane.open(model);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,11 @@ export default class ChatApi extends Service {
|
|||
*/
|
||||
thread(channelId, threadId) {
|
||||
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 Service, { inject as service } from "@ember/service";
|
||||
import { inject as service } from "@ember/service";
|
||||
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 chatApi;
|
||||
@service chatComposerPresenceManager;
|
||||
@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();
|
||||
}
|
||||
@service chatChannelThreadComposer;
|
||||
@service router;
|
||||
|
||||
@action
|
||||
replyTo(message) {
|
||||
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() {
|
||||
return this._message;
|
||||
}
|
||||
|
||||
set message(message) {
|
||||
this._message = message;
|
||||
_setReplyToAfterTransition(message) {
|
||||
next(() => {
|
||||
this.chatChannelThreadComposer.replyTo(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,14 +20,6 @@ export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSub
|
|||
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) {
|
||||
const message = this.messagesManager.findMessage(data.original_message_id);
|
||||
if (message) {
|
||||
|
|
|
@ -55,4 +55,8 @@ export default class ChatChannelPane extends Service {
|
|||
|
||||
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 { action } from "@ember/object";
|
||||
|
||||
export default class extends ChatChannelComposer {
|
||||
export default class ChatChannelThreadComposer extends ChatComposer {
|
||||
@action
|
||||
reset(channel) {
|
||||
reset(channel, thread) {
|
||||
this.message = ChatMessage.createDraftMessage(channel, {
|
||||
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 {
|
||||
get messageBusChannel() {
|
||||
return `/chat/${this.model.channelId}/thread/${this.model.id}`;
|
||||
return `/chat/${this.model.channel.id}/thread/${this.model.id}`;
|
||||
}
|
||||
|
||||
get messageBusLastId() {
|
||||
|
@ -12,7 +12,10 @@ export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneB
|
|||
|
||||
handleSentMessage(data) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
|
@ -23,15 +26,6 @@ export default class ChatChannelThreadPaneSubscriptionsManager extends ChatPaneB
|
|||
data.chat_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
|
||||
|
|
|
@ -3,6 +3,19 @@ import { inject as service } from "@ember/service";
|
|||
|
||||
export default class ChatChannelThreadPane extends ChatChannelPane {
|
||||
@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() {
|
||||
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
|
||||
// code completely out of ChatLivePane
|
||||
export function handleStagedMessage(messagesManager, data) {
|
||||
export function handleStagedMessage(channel, messagesManager, data) {
|
||||
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
|
||||
|
||||
if (!stagedMessage) {
|
||||
|
@ -17,17 +17,8 @@ export function handleStagedMessage(messagesManager, data) {
|
|||
stagedMessage.id = data.chat_message.id;
|
||||
stagedMessage.staged = false;
|
||||
stagedMessage.excerpt = data.chat_message.excerpt;
|
||||
stagedMessage.threadId = data.chat_message.thread_id;
|
||||
stagedMessage.channelId = data.chat_message.chat_channel_id;
|
||||
stagedMessage.channel = channel;
|
||||
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;
|
||||
|
||||
return stagedMessage;
|
||||
|
@ -48,6 +39,7 @@ export function handleStagedMessage(messagesManager, data) {
|
|||
export default class ChatPaneBaseSubscriptionsManager extends Service {
|
||||
@service chat;
|
||||
@service currentUser;
|
||||
@service chatStagedThreadMapping;
|
||||
|
||||
get messageBusChannel() {
|
||||
throw "not implemented";
|
||||
|
@ -75,14 +67,15 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||
if (!this.model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
// TODO (martin) This can be removed once we move the handleSentMessage
|
||||
// code completely out of ChatLivePane
|
||||
handleStagedMessageInternal(data) {
|
||||
return handleStagedMessage(this.messagesManager, data);
|
||||
handleStagedMessageInternal(channel, data) {
|
||||
return handleStagedMessage(channel, this.messagesManager, data);
|
||||
}
|
||||
|
||||
@bind
|
||||
|
@ -122,7 +115,7 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||
this.handleFlaggedMessage(busData);
|
||||
break;
|
||||
case "thread_created":
|
||||
this.handleThreadCreated(busData);
|
||||
this.handleNewThreadCreated(busData);
|
||||
break;
|
||||
case "update_thread_original_message":
|
||||
this.handleThreadOriginalMessageUpdate(busData);
|
||||
|
@ -223,8 +216,34 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
|
|||
}
|
||||
}
|
||||
|
||||
handleThreadCreated() {
|
||||
throw "not implemented";
|
||||
handleNewThreadCreated(data) {
|
||||
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() {
|
||||
|
|
|
@ -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 {
|
||||
@service chat;
|
||||
@service router;
|
||||
isDrawerExpanded = false;
|
||||
isDrawerActive = false;
|
||||
isSidePanelExpanded = false;
|
||||
|
||||
@tracked isSidePanelExpanded = false;
|
||||
@tracked isDrawerExpanded = false;
|
||||
@tracked isDrawerActive = false;
|
||||
@tracked _chatURL = null;
|
||||
@tracked _appURL = null;
|
||||
|
||||
|
@ -44,16 +45,16 @@ export default class ChatStateManager extends Service {
|
|||
}
|
||||
|
||||
openSidePanel() {
|
||||
this.set("isSidePanelExpanded", true);
|
||||
this.isSidePanelExpanded = true;
|
||||
}
|
||||
|
||||
closeSidePanel() {
|
||||
this.set("isSidePanelExpanded", false);
|
||||
this.isSidePanelExpanded = false;
|
||||
}
|
||||
|
||||
didOpenDrawer(url = null) {
|
||||
this.set("isDrawerActive", true);
|
||||
this.set("isDrawerExpanded", true);
|
||||
this.isDrawerActive = true;
|
||||
this.isDrawerExpanded = true;
|
||||
|
||||
if (url) {
|
||||
this.storeChatURL(url);
|
||||
|
@ -64,27 +65,27 @@ export default class ChatStateManager extends Service {
|
|||
}
|
||||
|
||||
didCloseDrawer() {
|
||||
this.set("isDrawerActive", false);
|
||||
this.set("isDrawerExpanded", false);
|
||||
this.isDrawerActive = false;
|
||||
this.isDrawerExpanded = false;
|
||||
this.chat.updatePresence();
|
||||
this.#publishStateChange();
|
||||
}
|
||||
|
||||
didExpandDrawer() {
|
||||
this.set("isDrawerActive", true);
|
||||
this.set("isDrawerExpanded", true);
|
||||
this.isDrawerActive = true;
|
||||
this.isDrawerExpanded = true;
|
||||
this.chat.updatePresence();
|
||||
}
|
||||
|
||||
didCollapseDrawer() {
|
||||
this.set("isDrawerActive", true);
|
||||
this.set("isDrawerExpanded", false);
|
||||
this.isDrawerActive = true;
|
||||
this.isDrawerExpanded = false;
|
||||
this.#publishStateChange();
|
||||
}
|
||||
|
||||
didToggleDrawer() {
|
||||
this.set("isDrawerExpanded", !this.isDrawerExpanded);
|
||||
this.set("isDrawerActive", true);
|
||||
this.isDrawerExpanded = !this.isDrawerExpanded;
|
||||
this.isDrawerActive = true;
|
||||
this.#publishStateChange();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
{{! ChatThreadList will go here later }}
|
||||
<ChatThread @includeHeader={{true}} />
|
||||
<ChatThread @thread={{this.model}} @includeHeader={{true}} />
|
|
@ -25,8 +25,7 @@
|
|||
flex-grow: 1;
|
||||
overscroll-behavior: contain;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
will-change: transform;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-composer__wrapper {
|
||||
|
|
|
@ -13,6 +13,7 @@ module Chat
|
|||
chat_channel:,
|
||||
in_reply_to_id: nil,
|
||||
thread_id: nil,
|
||||
staged_thread_id: nil,
|
||||
user:,
|
||||
content:,
|
||||
staged_id: nil,
|
||||
|
@ -31,6 +32,7 @@ module Chat
|
|||
@incoming_chat_webhook = incoming_chat_webhook
|
||||
@upload_ids = upload_ids || []
|
||||
@thread_id = thread_id
|
||||
@staged_thread_id = staged_thread_id
|
||||
@error = nil
|
||||
|
||||
@chat_message =
|
||||
|
@ -57,7 +59,12 @@ module Chat
|
|||
create_thread
|
||||
@chat_message.attach_uploads(uploads)
|
||||
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
|
||||
Jobs.enqueue(Jobs::Chat::ProcessMessage, { chat_message_id: @chat_message.id })
|
||||
Chat::Notifier.notify_new(chat_message: @chat_message, timestamp: @chat_message.created_at)
|
||||
|
@ -123,6 +130,8 @@ module Chat
|
|||
end
|
||||
|
||||
def validate_existing_thread!
|
||||
return if @staged_thread_id.present? && @thread_id.blank?
|
||||
|
||||
return if @thread_id.blank?
|
||||
@existing_thread = Chat::Thread.find(@thread_id)
|
||||
|
||||
|
@ -165,7 +174,7 @@ module Chat
|
|||
|
||||
def create_thread
|
||||
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
|
||||
thread = @original_message.thread
|
||||
|
@ -177,12 +186,15 @@ module Chat
|
|||
channel: @chat_message.chat_channel,
|
||||
)
|
||||
@chat_message.in_reply_to.thread_id = thread.id
|
||||
Chat::Publisher.publish_thread_created!(
|
||||
@chat_message.chat_channel,
|
||||
@chat_message.in_reply_to,
|
||||
)
|
||||
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
|
||||
|
||||
# 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}")
|
||||
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
|
||||
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)
|
||||
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
|
||||
fab!(:thread) do
|
||||
Fabricate(:chat_thread, channel: chat_channel, original_message: message_1)
|
||||
|
|
|
@ -111,6 +111,32 @@ describe Chat::Publisher do
|
|||
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
|
||||
fab!(:thread) do
|
||||
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")
|
||||
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")
|
||||
expect(page).to have_css(".chat-channel-archive-status", wait: 15)
|
||||
end
|
||||
|
||||
it "shows an error when the topic is invalid" do
|
||||
Jobs.run_immediately!
|
||||
|
||||
chat.visit_channel_settings(channel_1)
|
||||
click_button(I18n.t("js.chat.channel_settings.archive_channel"))
|
||||
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"))
|
||||
|
||||
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")
|
||||
end
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ RSpec.describe "Browse page", type: :system, js: true do
|
|||
context "when results are found" do
|
||||
it "lists expected results" do
|
||||
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_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
|
||||
it "displays the correct message" do
|
||||
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"))
|
||||
end
|
||||
|
||||
it "doesn’t display any channel" do
|
||||
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_2.name)
|
||||
|
|
|
@ -69,8 +69,8 @@ describe "Channel thread message echoing", type: :system, js: true do
|
|||
chat_page.visit_channel(channel)
|
||||
channel_page.message_thread_indicator(thread.original_message).click
|
||||
expect(side_panel).to have_open_thread(thread)
|
||||
open_thread.send_message(thread.id, "new thread message")
|
||||
expect(open_thread).to have_message(thread.id, text: "new thread message")
|
||||
open_thread.send_message("new thread message")
|
||||
expect(open_thread).to have_message(thread_id: thread.id, text: "new thread message")
|
||||
new_message = thread.reload.replies.last
|
||||
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(new_message.id))
|
||||
end
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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!(:channel_1) { Fabricate(:chat_channel) }
|
||||
fab!(:message_1) { Fabricate(:chat_message, chat_channel: channel_1) }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# 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!(:other_user) { Fabricate(:user) }
|
||||
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
|
||||
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)
|
||||
|
||||
thread.hover_message(last_message)
|
||||
thread.hover_message(first_message)
|
||||
|
||||
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
|
||||
|
|
|
@ -46,6 +46,7 @@ RSpec.describe "Deleted message", type: :system, js: true do
|
|||
channel_1.update!(threading_enabled: true)
|
||||
SiteSetting.enable_experimental_chat_threaded_discussions = true
|
||||
chat_system_user_bootstrap(user: other_user, channel: channel_1)
|
||||
Chat::Thread.update_counts
|
||||
end
|
||||
|
||||
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_2.id)
|
||||
expect(open_thread).to have_message(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_4.id)
|
||||
expect(open_thread).to have_message(thread_id: thread.id, id: message_5.id)
|
||||
|
||||
Chat::Publisher.publish_bulk_delete!(
|
||||
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_2.id)
|
||||
expect(open_thread).to have_no_message(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_4.id)
|
||||
expect(open_thread).to have_no_message(thread_id: thread.id, id: message_5.id)
|
||||
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)
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.reply_to(message_without_thread)
|
||||
channel_page.fill_composer("this is a reply to make a new thread")
|
||||
channel_page.click_send_message
|
||||
open_thread.fill_composer("this is a reply to make a new thread")
|
||||
open_thread.click_send_message
|
||||
|
||||
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
|
||||
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(
|
||||
".chat-message-thread-indicator__replies-count",
|
||||
text: I18n.t("js.chat.thread.replies", count: 4),
|
||||
|
|
|
@ -25,6 +25,7 @@ module PageObjects
|
|||
|
||||
def visit_thread(thread)
|
||||
visit(thread.url)
|
||||
has_no_css?(".chat-skeleton")
|
||||
end
|
||||
|
||||
def visit_channel_settings(channel)
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
module PageObjects
|
||||
module Pages
|
||||
class ChatChannel < PageObjects::Pages::Base
|
||||
def replying_to?(message)
|
||||
find(".chat-channel .chat-reply", text: message.message)
|
||||
end
|
||||
|
||||
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").send_keys(input)
|
||||
|
@ -49,7 +53,7 @@ module PageObjects
|
|||
def click_message_action_mobile(message, message_action)
|
||||
expand_message_actions_mobile(message, delay: 0.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
|
||||
|
||||
def hover_message(message)
|
||||
|
@ -114,8 +118,12 @@ module PageObjects
|
|||
end
|
||||
|
||||
def reply_to(message)
|
||||
hover_message(message)
|
||||
find(".reply-btn").click
|
||||
if page.has_css?("html.mobile-view", wait: 0)
|
||||
click_message_action_mobile(message, "reply")
|
||||
else
|
||||
hover_message(message)
|
||||
find(".reply-btn").click
|
||||
end
|
||||
end
|
||||
|
||||
def has_bookmarked_message?(message)
|
||||
|
@ -167,18 +175,22 @@ module PageObjects
|
|||
def check_message_presence(exists: true, text: nil, id: nil)
|
||||
css_method = exists ? :has_css? : :has_no_css?
|
||||
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
|
||||
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
|
||||
|
||||
def has_thread_indicator?(message)
|
||||
has_css?(message_thread_indicator_selector(message))
|
||||
def has_thread_indicator?(message, text: nil)
|
||||
has_css?(message_thread_indicator_selector(message), text: text)
|
||||
end
|
||||
|
||||
def has_no_thread_indicator?(message)
|
||||
has_no_css?(message_thread_indicator_selector(message))
|
||||
def has_no_thread_indicator?(message, text: nil)
|
||||
has_no_css?(message_thread_indicator_selector(message), text: text)
|
||||
end
|
||||
|
||||
def message_thread_indicator(message)
|
||||
|
|
|
@ -3,8 +3,12 @@
|
|||
module PageObjects
|
||||
module Pages
|
||||
class ChatSidePanel < PageObjects::Pages::Base
|
||||
def has_open_thread?(thread)
|
||||
has_css?(".chat-side-panel .chat-thread[data-id='#{thread.id}']")
|
||||
def has_open_thread?(thread = nil)
|
||||
if thread
|
||||
has_css?(".chat-side-panel .chat-thread[data-id='#{thread.id}']")
|
||||
else
|
||||
has_css?(".chat-side-panel .chat-thread")
|
||||
end
|
||||
end
|
||||
|
||||
def has_no_open_thread?
|
||||
|
|
|
@ -19,10 +19,6 @@ module PageObjects
|
|||
header.has_content?(content)
|
||||
end
|
||||
|
||||
def thread_selector_by_id(id)
|
||||
".chat-thread[data-id=\"#{id}\"]"
|
||||
end
|
||||
|
||||
def has_no_loading_skeleton?
|
||||
has_no_css?(".chat-thread__messages .chat-skeleton")
|
||||
end
|
||||
|
@ -41,42 +37,32 @@ module PageObjects
|
|||
find(".chat-thread .chat-composer__input").click # ensures autocomplete is closed and not masking anything
|
||||
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
|
||||
fill_composer(text)
|
||||
click_send_message(id)
|
||||
click_send_message
|
||||
click_composer
|
||||
end
|
||||
|
||||
def click_send_message(id)
|
||||
find(thread_selector_by_id(id)).find(
|
||||
".chat-composer.is-send-enabled .chat-composer__send-btn",
|
||||
).click
|
||||
def click_send_message
|
||||
find(".chat-thread .chat-composer.is-send-enabled .chat-composer__send-btn").click
|
||||
end
|
||||
|
||||
def has_message?(thread_id, text: nil, id: nil)
|
||||
check_message_presence(thread_id, exists: true, text: text, id: id)
|
||||
def has_message?(text: nil, id: nil, thread_id: nil)
|
||||
check_message_presence(exists: true, text: text, id: id, thread_id: thread_id)
|
||||
end
|
||||
|
||||
def has_no_message?(thread_id, text: nil, id: nil)
|
||||
check_message_presence(thread_id, exists: false, text: text, id: id)
|
||||
def has_no_message?(text: nil, id: nil, thread_id: nil)
|
||||
check_message_presence(exists: false, text: text, id: id, thread_id: thread_id)
|
||||
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?
|
||||
selector = thread_id ? ".chat-thread[data-id=\"#{thread_id}\"]" : ".chat-thread"
|
||||
if text
|
||||
find(thread_selector_by_id(thread_id)).send(
|
||||
css_method,
|
||||
".chat-message-text",
|
||||
text: text,
|
||||
wait: 5,
|
||||
)
|
||||
find(selector).send(css_method, ".chat-message-text", text: text, wait: 5)
|
||||
elsif id
|
||||
find(thread_selector_by_id(thread_id)).send(
|
||||
css_method,
|
||||
".chat-message-container[data-id=\"#{id}\"]",
|
||||
wait: 10,
|
||||
)
|
||||
find(selector).send(css_method, ".chat-message-container[data-id=\"#{id}\"]", wait: 10)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -31,8 +31,12 @@ module PageObjects
|
|||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||
end
|
||||
|
||||
def has_open_thread?(thread)
|
||||
has_css?("#{VISIBLE_DRAWER} .chat-thread[data-id='#{thread.id}']")
|
||||
def has_open_thread?(thread = nil)
|
||||
if thread
|
||||
has_css?("#{VISIBLE_DRAWER} .chat-thread[data-id='#{thread.id}']")
|
||||
else
|
||||
has_css?("#{VISIBLE_DRAWER} .chat-thread")
|
||||
end
|
||||
end
|
||||
|
||||
def has_open_channel?(channel)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
Fabricate(:chat_message, message: "message 1", chat_channel: channel_1, user: current_user)
|
||||
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
|
||||
chat.visit_channel(channel_1)
|
||||
expect(channel_page).to have_message(id: message_1.id)
|
||||
|
||||
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 }
|
||||
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
|
||||
|
|
|
@ -123,8 +123,8 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||
chat_page.visit_channel(channel)
|
||||
channel_page.message_thread_indicator(thread.original_message).click
|
||||
expect(side_panel).to have_open_thread(thread)
|
||||
thread_page.send_message(thread.id, "new thread message")
|
||||
expect(thread_page).to have_message(thread.id, text: "new thread message")
|
||||
thread_page.send_message("new thread message")
|
||||
expect(thread_page).to have_message(thread_id: thread.id, text: "new thread message")
|
||||
thread_message = thread.replies.last
|
||||
expect(thread_message.chat_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)
|
||||
channel_page.message_thread_indicator(thread.original_message).click
|
||||
expect(side_panel).to have_open_thread(thread)
|
||||
thread_page.send_message(thread.id, "new thread message")
|
||||
expect(thread_page).to have_message(thread.id, text: "new thread message")
|
||||
thread_page.send_message("new thread message")
|
||||
expect(thread_page).to have_message(thread_id: thread.id, text: "new thread message")
|
||||
thread_message = thread.reload.replies.last
|
||||
expect(channel_page).not_to have_css(channel_page.message_by_id_selector(thread_message.id))
|
||||
end
|
||||
|
@ -157,19 +157,19 @@ describe "Single thread in side panel", type: :system, js: true do
|
|||
|
||||
using_session(:tab_2) do
|
||||
expect(side_panel).to have_open_thread(thread)
|
||||
thread_page.send_message(thread.id, "the other user message")
|
||||
expect(thread_page).to have_message(thread.id, text: "the other user message")
|
||||
thread_page.send_message("the other user message")
|
||||
expect(thread_page).to have_message(thread_id: thread.id, text: "the other user message")
|
||||
end
|
||||
|
||||
using_session(:tab_1) do
|
||||
expect(side_panel).to have_open_thread(thread)
|
||||
expect(thread_page).to have_message(thread.id, text: "the other user message")
|
||||
thread_page.send_message(thread.id, "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: "the other user message")
|
||||
thread_page.send_message("this is a test message")
|
||||
expect(thread_page).to have_message(thread_id: thread.id, text: "this is a test message")
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in New Issue