diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb
index ffcad435e5..df9da9b669 100644
--- a/plugins/chat/app/controllers/chat_controller.rb
+++ b/plugins/chat/app/controllers/chat_controller.rb
@@ -209,6 +209,7 @@ class Chat::ChatController < Chat::ChatBaseController
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 ? "<" : ">"
@@ -299,6 +300,7 @@ class Chat::ChatController < Chat::ChatBaseController
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-thread.hbs b/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
index 3238d28a61..43990a7e5e 100644
--- a/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-thread.hbs
@@ -1,4 +1,8 @@
-
+
+
+ {{#each this.thread.messages as |message|}}
+ - {{message.user.username}} sez: {{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 1c07f0c538..e268b12ab2 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,238 @@ 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(options = {}) {
+ if (this._selfDeleted) {
+ return;
+ }
+
+ this.loadingMorePast = true;
+ this.loading = true;
+ this.thread.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.appendMessages(messages);
+
+ // TODO (martin) ECHO MODE
+ this.channel.appendMessages(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.
+
+ debugger;
+ const stagedMessage = ChatMessage.createStagedMessage(this.channel, {
+ message,
+ created_at: new Date(),
+ uploads: cloneJSON(uploads),
+ user: this.currentUser,
+ thread_id: this.thread.id,
+ });
+
+ this.thread.appendMessages([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(chatMessage, newContent, uploads) {}
+
+ @action
+ setReplyTo(messageId) {}
+
+ @action
+ setInReplyToMsg(inReplyMsg) {
+ this.replyToMsg = inReplyMsg;
+ }
+
+ @action
+ cancelEditing() {
+ this.editingMessage = null;
+ }
+
+ @action
+ editLastMessageRequested() {}
+
+ @action
+ 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.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-threads-manager.js b/plugins/chat/assets/javascripts/discourse/lib/chat-threads-manager.js
index a19aa7492e..fb278ae783 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,7 @@ export default class ChatThreadsManager {
.thread(channelId, threadId)
.catch(popupAjaxError)
.then((thread) => {
- this.#cache(thread);
+ console.log(thread);
return thread;
});
}
diff --git a/plugins/chat/assets/javascripts/discourse/models/chat-thread.js b/plugins/chat/assets/javascripts/discourse/models/chat-thread.js
index abe42551d0..c08178a6df 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 { TrackedArray } from "@ember-compat/tracked-built-ins";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import User from "discourse/models/user";
import { escapeExpression } from "discourse/lib/utilities";
import { tracked } from "@glimmer/tracking";
@@ -10,22 +11,64 @@ export const THREAD_STATUSES = {
archived: "archived",
};
-export default class ChatThread extends RestModel {
+export default class ChatThread {
@tracked title;
@tracked status;
+ @tracked messages = new TrackedArray();
+
+ 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 = new ChatMessage(args.original_message);
+ this.originalMessage = args.original_message;
+ this.originalMessage.user = this.originalMessageUser;
+ }
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);
+ clearMessages() {
+ this.messages.clear();
+
+ this.canLoadMoreFuture = null;
+ this.canLoadMorePast = null;
+ }
+
+ appendMessages(messages) {
+ this.messages.pushObjects(messages);
+ }
+
+ prependMessages(messages) {
+ this.messages.unshiftObjects(messages);
+ }
+
+ 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.stagedId === stagedMessageId
+ );
+ }
+
+ #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 34ca9343de..3d99c4164b 100644
--- a/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
+++ b/plugins/chat/assets/javascripts/discourse/routes/chat-channel.js
@@ -12,8 +12,11 @@ export default class ChatChannelRoute extends DiscourseRoute {
willTransition(transition) {
// Technically we could keep messages to avoid re-fetching them, but
// it's not worth the complexity for now
- this.chat.activeChannel?.clearMessages();
+ if (!transition?.to?.name?.startsWith("chat.channel.thread")) {
+ // this.chat.activeChannel?.clearMessages();
+ }
+ this.chat.activeChannel.activeThread?.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 4b51143681..2fe788e711 100644
--- a/plugins/chat/assets/javascripts/discourse/services/chat-api.js
+++ b/plugins/chat/assets/javascripts/discourse/services/chat-api.js
@@ -137,6 +137,7 @@ export default class ChatApi extends Service {
* @returns {Promise}
*/
sendMessage(channelId, data = {}) {
+ debugger;
return ajax(`/chat/${channelId}`, {
ignoreUnsent: false,
type: "POST",
@@ -261,6 +262,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 });