WIP: Chat message loading in thread
This commit is contained in:
parent
f7c6069742
commit
5218b97652
@ -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
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<div class="chat-thread" data-id={{this.thread.id}}>
|
||||
<div
|
||||
class={{concat-class "chat-thread" (if this.loading "loading")}}
|
||||
data-id={{this.thread.id}}
|
||||
{{did-insert this.loadMessages}}
|
||||
>
|
||||
<div class="chat-thread__header">
|
||||
<div class="chat-thread__info">
|
||||
<div class="chat-thread__title">
|
||||
@ -14,7 +18,7 @@
|
||||
</div>
|
||||
|
||||
<p class="chat-thread__om">
|
||||
{{replace-emoji this.thread.original_message.excerpt}}
|
||||
{{replace-emoji this.thread.originalMessage.excerpt}}
|
||||
</p>
|
||||
|
||||
<div class="chat-thread__omu">
|
||||
@ -23,14 +27,36 @@
|
||||
}}</span>
|
||||
<ChatMessageAvatar
|
||||
class="chat-thread__omu-avatar"
|
||||
@message={{this.thread.original_message}}
|
||||
@message={{this.thread.originalMessage}}
|
||||
/>
|
||||
<span
|
||||
class="chat-thread__omu-username"
|
||||
>{{this.thread.original_message_user.username}}</span>
|
||||
>{{this.thread.originalMessageUser.username}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-thread__messages">
|
||||
<ul>
|
||||
{{#each this.thread.messages as |message|}}
|
||||
<li>{{message.user.username}} sez: {{message.message}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{#if (or this.loading this.loadingMoreFuture)}}
|
||||
<ChatSkeleton />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ChatComposer
|
||||
@canInteractWithChat="true"
|
||||
@sendMessage={{this.sendMessage}}
|
||||
@editMessage={{this.editMessage}}
|
||||
@setReplyTo={{this.setReplyTo}}
|
||||
@loading={{this.sendingLoading}}
|
||||
@editingMessage={{readonly this.editingMessage}}
|
||||
@onCancelEditing={{this.cancelEditing}}
|
||||
@setInReplyToMsg={{this.setInReplyToMsg}}
|
||||
@onEditLastMessageRequested={{this.editLastMessageRequested}}
|
||||
@onValueChange={{this.composerValueChanged}}
|
||||
@chatChannel={{this.channel}}
|
||||
/>
|
||||
</div>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user