diff --git a/plugins/chat/app/controllers/chat/chat_controller.rb b/plugins/chat/app/controllers/chat/chat_controller.rb
index fcb918f1e43..0be60a23526 100644
--- a/plugins/chat/app/controllers/chat/chat_controller.rb
+++ b/plugins/chat/app/controllers/chat/chat_controller.rb
@@ -107,6 +107,7 @@ module Chat
content: content,
staged_id: params[:staged_id],
upload_ids: params[:upload_ids],
+ thread_id: params[:thread_id],
)
return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
@@ -215,6 +216,7 @@ module Chat
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
+ messages = messages.where(thread_id: params[:thread_id]) if params[:thread_id]
if message_id.present?
condition = direction == PAST ? "<" : ">"
@@ -305,6 +307,7 @@ module Chat
messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
+ messages = messages.where(thread_id: params[:thread_id]) if params[:thread_id]
past_messages =
messages
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
index 9c5d9bf1305..15cd7bdf434 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.hbs
@@ -75,7 +75,7 @@
{{/if}}
- {{#if (and this.loadedOnce (not @channel.canLoadMorePast))}}
+ {{#if (and this.loadedOnce (not @channel.messagesManager.canLoadMorePast))}}
+
+
+ {{#each this.thread.messages as |message|}}
+ - {{message.user.username}}: {{message.message}}
+ {{/each}}
+
+ {{#if (or this.loading this.loadingMoreFuture)}}
+
+ {{/if}}
+
+
\ No newline at end of file
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-thread.js b/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
index 1c07f0c538b..94cbc482651 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread.js
@@ -1,15 +1,34 @@
import Component from "@glimmer/component";
+import { cloneJSON } from "discourse-common/lib/object";
+import ChatMessageDraft from "discourse/plugins/chat/discourse/models/chat-message-draft";
+import { tracked } from "@glimmer/tracking";
+import { action } from "@ember/object";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { bind, debounce } from "discourse-common/utils/decorators";
import I18n from "I18n";
import { inject as service } from "@ember/service";
+const PAGE_SIZE = 50;
+
export default class ChatThreadPanel extends Component {
@service siteSettings;
@service currentUser;
@service chat;
@service router;
+ @service chatApi;
+ @service chatComposerPresenceManager;
+ @service appEvents;
+
+ @tracked loading;
+ @tracked loadingMorePast;
get thread() {
- return this.chat.activeChannel.activeThread;
+ return this.channel.activeThread;
+ }
+
+ get channel() {
+ return this.chat.activeChannel;
}
get title() {
@@ -19,4 +38,241 @@ export default class ChatThreadPanel extends Component {
return I18n.t("chat.threads.op_said");
}
+
+ @action
+ loadMessages() {
+ if (this.args.targetMessageId) {
+ this.requestedTargetMessageId = parseInt(this.args.targetMessageId, 10);
+ }
+
+ // TODO (martin) Loading/scrolling to selected messagew
+ // this.highlightOrFetchMessage(this.requestedTargetMessageId);
+ // if (this.requestedTargetMessageId) {
+ // } else {
+ this.fetchMessages();
+ // }
+ }
+
+ get _selfDeleted() {
+ return this.isDestroying || this.isDestroyed;
+ }
+
+ @debounce(100)
+ fetchMessages() {
+ if (this._selfDeleted) {
+ return;
+ }
+
+ this.loadingMorePast = true;
+ this.loading = true;
+ this.thread.messagesManager.clearMessages();
+
+ 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;
+
+ return this.chatApi
+ .messages(this.channel.id, findArgs)
+ .then((results) => {
+ if (this._selfDeleted || this.channel.id !== results.meta.channel_id) {
+ this.router.transitionTo(
+ "chat.channel",
+ "-",
+ results.meta.channel_id
+ );
+ }
+
+ const [messages, meta] = this.afterFetchCallback(this.channel, results);
+ this.thread.messagesManager.addMessages(messages);
+
+ // TODO (martin) ECHO MODE
+ this.channel.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);
+ // }
+ })
+ .catch(this.#handleErrors)
+ .finally(() => {
+ if (this._selfDeleted) {
+ return;
+ }
+
+ this.requestedTargetMessageId = null;
+ this.loading = false;
+ this.loadingMorePast = false;
+
+ // this.fillPaneAttempt();
+ });
+ }
+
+ @bind
+ afterFetchCallback(channel, results) {
+ const messages = [];
+ let foundFirstNew = false;
+
+ results.chat_messages.forEach((messageData) => {
+ // If a message has been hidden it is because the current user is ignoring
+ // the user who sent it, so we want to unconditionally hide it, even if
+ // we are going directly to the target
+ if (this.currentUser.ignored_users) {
+ messageData.hidden = this.currentUser.ignored_users.includes(
+ messageData.user.username
+ );
+ }
+
+ if (this.requestedTargetMessageId === messageData.id) {
+ messageData.expanded = !messageData.hidden;
+ } else {
+ messageData.expanded = !(messageData.hidden || messageData.deleted_at);
+ }
+
+ // newest has to be in after fetcg callback as we don't want to make it
+ // dynamic or it will make the pane jump around, it will disappear on reload
+ if (
+ !foundFirstNew &&
+ messageData.id > channel.currentUserMembership.last_read_message_id
+ ) {
+ foundFirstNew = true;
+ messageData.newest = true;
+ }
+
+ messages.push(ChatMessage.create(channel, messageData));
+ });
+
+ return [messages, results.meta];
+ }
+
+ @action
+ sendMessage(message, uploads = []) {
+ // TODO (martin) For desktop notifications
+ // resetIdle()
+ if (this.sendingLoading) {
+ return;
+ }
+
+ this.sendingLoading = true;
+ this.channel.draft = ChatMessageDraft.create();
+
+ // TODO (martin) Handling case when channel is not followed???? IDK if we
+ // even let people send messages in threads without this, seems weird.
+
+ const stagedMessage = ChatMessage.createStagedMessage(this.channel, {
+ message,
+ created_at: new Date(),
+ uploads: cloneJSON(uploads),
+ user: this.currentUser,
+ thread_id: this.thread.id,
+ });
+
+ this.thread.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.stagedId,
+ upload_ids: stagedMessage.uploads.map((upload) => upload.id),
+ thread_id: stagedMessage.threadId,
+ })
+ .then(() => {
+ // TODO (martin) Scrolling!!
+ // this.scrollToBottom();
+ })
+ .catch((error) => {
+ this.#onSendError(stagedMessage.stagedId, error);
+ })
+ .finally(() => {
+ if (this._selfDeleted) {
+ return;
+ }
+ this.sendingLoading = false;
+ this.#resetAfterSend();
+ });
+ }
+
+ @action
+ editMessage() {}
+ // editMessage(chatMessage, newContent, uploads) {}
+
+ @action
+ setReplyTo() {}
+ // setReplyTo(messageId) {}
+
+ @action
+ setInReplyToMsg(inReplyMsg) {
+ this.replyToMsg = inReplyMsg;
+ }
+
+ @action
+ cancelEditing() {
+ this.editingMessage = null;
+ }
+
+ @action
+ editLastMessageRequested() {}
+
+ @action
+ composerValueChanged() {}
+ // composerValueChanged(value, uploads, replyToMsg) {}
+
+ #handleErrors(error) {
+ switch (error?.jqXHR?.status) {
+ case 429:
+ case 404:
+ popupAjaxError(error);
+ break;
+ default:
+ throw error;
+ }
+ }
+
+ #onSendError(stagedId, error) {
+ const stagedMessage =
+ this.thread.messagesManager.findStagedMessage(stagedId);
+ if (stagedMessage) {
+ if (error.jqXHR?.responseJSON?.errors?.length) {
+ stagedMessage.error = error.jqXHR.responseJSON.errors[0];
+ } else {
+ this.chat.markNetworkAsUnreliable();
+ stagedMessage.error = "network_error";
+ }
+ }
+
+ this.#resetAfterSend();
+ }
+
+ #resetAfterSend() {
+ if (this._selfDeleted) {
+ return;
+ }
+
+ this.replyToMsg = null;
+ this.editingMessage = null;
+ this.chatComposerPresenceManager.notifyState(this.channel.id, false);
+ this.appEvents.trigger("chat-composer:reply-to-set", null);
+ }
}
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-messages-manager.js b/plugins/chat/assets/javascripts/discourse/lib/chat-messages-manager.js
new file mode 100644
index 00000000000..0158698f339
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-messages-manager.js
@@ -0,0 +1,43 @@
+import { tracked } from "@glimmer/tracking";
+import { TrackedArray } from "@ember-compat/tracked-built-ins";
+import { setOwner } from "@ember/application";
+
+export default class ChatMessagesManager {
+ @tracked messages = new TrackedArray();
+ @tracked canLoadMoreFuture;
+ @tracked canLoadMorePast;
+
+ constructor(owner) {
+ setOwner(this, owner);
+ }
+
+ clearMessages() {
+ this.messages.clear();
+
+ this.canLoadMoreFuture = null;
+ this.canLoadMorePast = null;
+ }
+
+ addMessages(messages = []) {
+ this.messages = this.messages
+ .concat(messages)
+ .uniqBy("id")
+ .sortBy("createdAt");
+ }
+
+ findMessage(messageId) {
+ return this.messages.find(
+ (message) => message.id === parseInt(messageId, 10)
+ );
+ }
+
+ removeMessage(message) {
+ return this.messages.removeObject(message);
+ }
+
+ findStagedMessage(stagedMessageId) {
+ return this.messages.find(
+ (message) => message.staged && message.id === stagedMessageId
+ );
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/lib/chat-threads-manager.js b/plugins/chat/assets/javascripts/discourse/lib/chat-threads-manager.js
index a19aa7492e5..0fdd45d1bcf 100644
--- a/plugins/chat/assets/javascripts/discourse/lib/chat-threads-manager.js
+++ b/plugins/chat/assets/javascripts/discourse/lib/chat-threads-manager.js
@@ -43,7 +43,7 @@ export default class ChatThreadsManager {
let model = this.#findStale(threadObject.id);
if (!model) {
- model = ChatThread.create(threadObject);
+ model = new ChatThread(threadObject);
this.#cache(model);
}
@@ -55,7 +55,6 @@ export default class ChatThreadsManager {
.thread(channelId, threadId)
.catch(popupAjaxError)
.then((thread) => {
- this.#cache(thread);
return thread;
});
}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
index e1eb2b58405..ec45c76b929 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-channel.js
@@ -6,8 +6,8 @@ import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
import ChatThreadsManager from "discourse/plugins/chat/discourse/lib/chat-threads-manager";
+import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import { getOwner } from "discourse-common/lib/get-owner";
-import { TrackedArray } from "@ember-compat/tracked-built-ins";
export const CHATABLE_TYPES = {
directMessageChannel: "DirectMessage",
@@ -55,18 +55,24 @@ export default class ChatChannel extends RestModel {
@tracked chatableType;
@tracked status;
@tracked activeThread;
- @tracked messages = new TrackedArray();
@tracked lastMessageSentAt;
@tracked canDeleteOthers;
@tracked canDeleteSelf;
@tracked canFlag;
- @tracked canLoadMoreFuture;
- @tracked canLoadMorePast;
@tracked canModerate;
@tracked userSilenced;
@tracked draft;
threadsManager = new ChatThreadsManager(getOwner(this));
+ messagesManager = new ChatMessagesManager(getOwner(this));
+
+ get messages() {
+ return this.messagesManager.messages;
+ }
+
+ set messages(messages) {
+ this.messagesManager.messages = messages;
+ }
get escapedTitle() {
return escapeExpression(this.title);
@@ -126,46 +132,16 @@ export default class ChatChannel extends RestModel {
this.canFlag = details.can_flag ?? false;
this.canModerate = details.can_moderate ?? false;
if (details.can_load_more_future !== undefined) {
- this.canLoadMoreFuture = details.can_load_more_future;
+ this.messagesManager.canLoadMoreFuture = details.can_load_more_future;
}
if (details.can_load_more_past !== undefined) {
- this.canLoadMorePast = details.can_load_more_past;
+ this.messagesManager.canLoadMorePast = details.can_load_more_past;
}
this.userSilenced = details.user_silenced ?? false;
this.status = details.channel_status;
this.channelMessageBusLastId = details.channel_message_bus_last_id;
}
- clearMessages() {
- this.messages.clear();
-
- this.canLoadMoreFuture = null;
- this.canLoadMorePast = null;
- }
-
- addMessages(messages = []) {
- this.messages = this.messages
- .concat(messages)
- .uniqBy("id")
- .sortBy("createdAt");
- }
-
- findMessage(messageId) {
- return this.messages.find(
- (message) => message.id === parseInt(messageId, 10)
- );
- }
-
- removeMessage(message) {
- return this.messages.removeObject(message);
- }
-
- findStagedMessage(stagedMessageId) {
- return this.messages.find(
- (message) => message.staged && message.id === stagedMessageId
- );
- }
-
canModifyMessages(user) {
if (user.staff) {
return !STAFF_READONLY_STATUSES.includes(this.status);
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
index abe42551d0b..599bacccdfb 100644
--- a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
+++ b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
@@ -1,4 +1,5 @@
-import RestModel from "discourse/models/rest";
+import { getOwner } from "discourse-common/lib/get-owner";
+import ChatMessagesManager from "discourse/plugins/chat/discourse/lib/chat-messages-manager";
import User from "discourse/models/user";
import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking";
@@ -10,22 +11,42 @@ export const THREAD_STATUSES = {
archived: "archived",
};
-export default class ChatThread extends RestModel {
+export default class ChatThread {
@tracked title;
@tracked status;
+ messagesManager = new ChatMessagesManager(getOwner(this));
+
+ constructor(args = {}) {
+ this.title = args.title;
+ this.id = args.id;
+ this.status = args.status;
+
+ this.originalMessageUser = this.#initUserModel(args.original_message_user);
+
+ // TODO (martin) Not sure if ChatMessage is needed here, original_message
+ // only has a small subset of message stuff.
+ this.originalMessage = args.original_message;
+ this.originalMessage.user = this.originalMessageUser;
+ }
+
+ get messages() {
+ return this.messagesManager.messages;
+ }
+
+ set messages(messages) {
+ this.messagesManager.messages = messages;
+ }
+
get escapedTitle() {
return escapeExpression(this.title);
}
-}
-ChatThread.reopenClass({
- create(args) {
- args = args || {};
- if (!args.original_message_user instanceof User) {
- args.original_message_user = User.create(args.original_message_user);
+ #initUserModel(user) {
+ if (!user || user instanceof User) {
+ return user;
}
- args.original_message.user = args.original_message_user;
- return this._super(args);
- },
-});
+
+ return User.create(user);
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
index 743c28c3440..f610f301c08 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
@@ -10,8 +10,7 @@ export default class ChatChannelRoute extends DiscourseRoute {
@action
willTransition(transition) {
- this.chat.activeChannel.activeThread = null;
- this.chatStateManager.closeSidePanel();
+ this.#closeThread();
if (transition?.to?.name === "chat.channel.index") {
const targetChannelId = transition?.to?.parent?.params?.channelId;
@@ -19,7 +18,7 @@ export default class ChatChannelRoute extends DiscourseRoute {
targetChannelId &&
parseInt(targetChannelId, 10) !== this.chat.activeChannel.id
) {
- this.chat.activeChannel.clearMessages();
+ this.chat.activeChannel.messagesManager.clearMessages();
}
}
@@ -29,4 +28,10 @@ export default class ChatChannelRoute extends DiscourseRoute {
this.chat.updatePresence();
}
}
+
+ #closeThread() {
+ this.chat.activeChannel.activeThread?.messagesManager?.clearMessages();
+ this.chat.activeChannel.activeThread = null;
+ this.chatStateManager.closeSidePanel();
+ }
}
diff --git a/plugins/chat/assets/javascripts/discourse/services/chat-api.js b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
index 44ad75dc5c4..2f7f41f4a22 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -261,6 +261,10 @@ export default class ChatApi extends Service {
if (data.direction) {
args.direction = data.direction;
}
+
+ if (data.threadId) {
+ args.thread_id = data.threadId;
+ }
}
return ajax(path, { data: args });