FIX: correctly handle subscriptions (#24270)

Subscriptions manager have been a pain since the beginning, one of the problem is that thread and channels behave mostly the same but with various small difference which I expect to increase over time.

Trying to use subclasses for this case has proven to be a mistake, this commit now uses a class for each case (channel, thread) which for now contains a lot of duplication, which might be reduced in the future but has the merit to make reasoning about each case very simple.

This refactor is fixing a bug introduced in 90efdd7f9d which was causing the wrong channel to be unsubscribed, this shouldn't be possible anymore. We had tests for this which were disabled due to flakeyness, I will consider re-enabling them in the future.

Other notes:
- notices had been added to the subscriptions manager service, they have been moved into their own dedicated service: `ChatChannelNoticesManager`
- the `(each model)` trick used in `<ChatChannel />` since 90efdd7f9d to ensure atomicity has been applied to `<ChatThread />` too
This commit is contained in:
Joffrey JAFFEUX 2023-11-07 16:37:42 +01:00 committed by GitHub
parent 1d68ff430b
commit dcaa719363
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 358 additions and 265 deletions

View File

@ -13,6 +13,7 @@ import {
} from "discourse/lib/user-presence"; } from "discourse/lib/user-presence";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager";
import { import {
FUTURE, FUTURE,
PAST, PAST,
@ -29,9 +30,6 @@ import {
scrollListToMessage, scrollListToMessage,
} from "discourse/plugins/chat/discourse/lib/scroll-helpers"; } from "discourse/plugins/chat/discourse/lib/scroll-helpers";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
// TODO (martin) Remove this when the handleSentMessage logic inside chatChannelPaneSubscriptionsManager
// is moved over from this file completely.
import { handleStagedMessage } from "discourse/plugins/chat/discourse/services/chat-pane-base-subscriptions-manager";
import { stackingContextFix } from "../lib/chat-ios-hacks"; import { stackingContextFix } from "../lib/chat-ios-hacks";
export default class ChatChannel extends Component { export default class ChatChannel extends Component {
@ -40,7 +38,6 @@ export default class ChatChannel extends Component {
@service chat; @service chat;
@service chatApi; @service chatApi;
@service chatChannelsManager; @service chatChannelsManager;
@service chatChannelPaneSubscriptionsManager;
@service chatComposerPresenceManager; @service chatComposerPresenceManager;
@service chatDraftsManager; @service chatDraftsManager;
@service chatEmojiPickerManager; @service chatEmojiPickerManager;
@ -97,7 +94,7 @@ export default class ChatChannel extends Component {
teardownListeners() { teardownListeners() {
this.#cancelHandlers(); this.#cancelHandlers();
removeOnPresenceChange(this.onPresenceChangeCallback); removeOnPresenceChange(this.onPresenceChangeCallback);
this.unsubscribeToUpdates(this.args.channel.id); this.subscriptionManager.teardown();
} }
@action @action
@ -140,7 +137,11 @@ export default class ChatChannel extends Component {
return; return;
} }
this.subscribeToUpdates(this.args.channel); this.subscriptionManager = new ChatChannelSubscriptionManager(
this,
this.args.channel,
{ onNewMessage: this.onNewMessage }
);
if (this.args.targetMessageId) { if (this.args.targetMessageId) {
this.debounceHighlightOrFetchMessage(this.args.targetMessageId); this.debounceHighlightOrFetchMessage(this.args.targetMessageId);
@ -149,6 +150,14 @@ export default class ChatChannel extends Component {
} }
} }
@bind
onNewMessage(message) {
stackingContextFix(this.scrollable, () => {
this.messagesManager.addMessages([message]);
});
this.debouncedUpdateLastReadMessage();
}
@bind @bind
onPresenceChangeCallback(present) { onPresenceChangeCallback(present) {
if (present) { if (present) {
@ -464,36 +473,6 @@ export default class ChatChannel extends Component {
} }
} }
@bind
onMessage(data) {
switch (data.type) {
case "sent":
this.handleSentMessage(data);
break;
}
}
handleSentMessage(data) {
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
const stagedMessage = handleStagedMessage(
this.args.channel,
this.messagesManager,
data
);
if (stagedMessage) {
return;
}
}
const message = ChatMessage.create(this.args.channel, data.chat_message);
message.manager = this.args.channel.messagesManager;
stackingContextFix(this.scrollable, () => {
this.messagesManager.addMessages([message]);
});
this.debouncedUpdateLastReadMessage();
this.args.channel.lastMessage = message;
}
@action @action
async onSendMessage(message) { async onSendMessage(message) {
await message.cook(); await message.cook();
@ -620,28 +599,6 @@ export default class ChatChannel extends Component {
}); });
} }
unsubscribeToUpdates(channelId) {
if (!channelId) {
return;
}
this.chatChannelPaneSubscriptionsManager.unsubscribe();
this.messageBus.unsubscribe(`/chat/${channelId}`, this.onMessage);
}
subscribeToUpdates(channel) {
if (!channel) {
return;
}
this.messageBus.subscribe(
`/chat/${channel.id}`,
this.onMessage,
channel.channelMessageBusLastId
);
this.chatChannelPaneSubscriptionsManager.subscribe(channel);
}
@action @action
addAutoFocusEventListener() { addAutoFocusEventListener() {
document.addEventListener("keydown", this._autoFocus); document.addEventListener("keydown", this._autoFocus);

View File

@ -17,10 +17,12 @@
> >
{{#if this.chat.activeChannel}} {{#if this.chat.activeChannel}}
{{#each (array this.chat.activeChannel) as |channel|}} {{#each (array this.chat.activeChannel) as |channel|}}
<ChatChannel {{#if channel}}
@targetMessageId={{readonly @params.messageId}} <ChatChannel
@channel={{channel}} @targetMessageId={{readonly @params.messageId}}
/> @channel={{channel}}
/>
{{/if}}
{{/each}} {{/each}}
{{/if}} {{/if}}
</div> </div>

View File

@ -1,4 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { array } from "@ember/helper";
import { action } from "@ember/object"; import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import didUpdate from "@ember/render-modifiers/modifiers/did-update";
@ -88,12 +89,14 @@ export default class ChatDrawerThread extends Component {
{{didUpdate this.fetchChannelAndThread @params.channelId}} {{didUpdate this.fetchChannelAndThread @params.channelId}}
{{didUpdate this.fetchChannelAndThread @params.threadId}} {{didUpdate this.fetchChannelAndThread @params.threadId}}
> >
{{#if this.chat.activeChannel.activeThread}} {{#each (array this.chat.activeChannel.activeThread) as |thread|}}
<ChatThread {{#if thread}}
@thread={{this.chat.activeChannel.activeThread}} <ChatThread
@targetMessageId={{@params.messageId}} @thread={{thread}}
/> @targetMessageId={{@params.messageId}}
{{/if}} />
{{/if}}
{{/each}}
</div> </div>
{{/if}} {{/if}}
</template> </template>

View File

@ -8,11 +8,11 @@ const COMPONENT_DICT = {
}; };
export default class ChatNotices extends Component { export default class ChatNotices extends Component {
@service("chat-channel-pane-subscriptions-manager") subscriptionsManager; @service("chat-channel-notices-manager") noticesManager;
@action @action
clearNotice() { clearNotice() {
this.subscriptionsManager.clearNotice(this.args.notice); this.noticesManager.clearNotice(this.args.notice);
} }
get component() { get component() {

View File

@ -2,10 +2,10 @@ import Component from "@glimmer/component";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
export default class ChatNotices extends Component { export default class ChatNotices extends Component {
@service("chat-channel-pane-subscriptions-manager") subscriptionsManager; @service("chat-channel-notices-manager") noticesManager;
get noticesForChannel() { get noticesForChannel() {
return this.subscriptionsManager.notices.filter( return this.noticesManager.notices.filter(
(notice) => notice.channelId === this.args.channel.id (notice) => notice.channelId === this.args.channel.id
); );
} }

View File

@ -6,7 +6,6 @@
data-id={{@thread.id}} data-id={{@thread.id}}
{{did-insert this.setUploadDropZone}} {{did-insert this.setUploadDropZone}}
{{did-insert this.didUpdateThread}} {{did-insert this.didUpdateThread}}
{{did-update this.didUpdateThread @thread.id}}
{{will-destroy this.teardown}} {{will-destroy this.teardown}}
> >
{{#if @includeHeader}} {{#if @includeHeader}}

View File

@ -9,6 +9,7 @@ import { resetIdle } from "discourse/lib/desktop-notifications";
import { NotificationLevels } from "discourse/lib/notification-levels"; import { NotificationLevels } from "discourse/lib/notification-levels";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import ChatChannelThreadSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-thread-subscription-manager";
import { import {
FUTURE, FUTURE,
PAST, PAST,
@ -36,7 +37,6 @@ export default class ChatThread extends Component {
@service chatHistory; @service chatHistory;
@service chatThreadComposer; @service chatThreadComposer;
@service chatThreadPane; @service chatThreadPane;
@service chatThreadPaneSubscriptionsManager;
@service currentUser; @service currentUser;
@service router; @service router;
@service siteSettings; @service siteSettings;
@ -85,14 +85,9 @@ export default class ChatThread extends Component {
this.uploadDropZone = element; this.uploadDropZone = element;
} }
@action
subscribeToUpdates() {
this.chatThreadPaneSubscriptionsManager.subscribe(this.args.thread);
}
@action @action
teardown() { teardown() {
this.chatThreadPaneSubscriptionsManager.unsubscribe(); this.subscriptionManager.teardown();
cancel(this._debouncedFillPaneAttemptHandler); cancel(this._debouncedFillPaneAttemptHandler);
cancel(this._debounceUpdateLastReadMessageHandler); cancel(this._debounceUpdateLastReadMessageHandler);
} }
@ -166,7 +161,11 @@ export default class ChatThread extends Component {
@action @action
loadMessages() { loadMessages() {
this.fetchMessages(); this.fetchMessages();
this.subscribeToUpdates(); this.subscriptionManager = new ChatChannelThreadSubscriptionManager(
this,
this.args.thread,
{ onNewMessage: this.onNewMessage }
);
} }
@action @action
@ -296,6 +295,11 @@ export default class ChatThread extends Component {
scrollListToMessage(this.scrollable, message, opts); scrollListToMessage(this.scrollable, message, opts);
} }
@bind
onNewMessage(message) {
this.messagesManager.addMessages([message]);
}
@bind @bind
processMessages(thread, result) { processMessages(thread, result) {
const messages = result.messages.map((messageData) => { const messages = result.messages.map((messageData) => {

View File

@ -1,82 +1,46 @@
import Service, { inject as service } from "@ember/service"; import { tracked } from "@glimmer/tracking";
import { getOwner, setOwner } from "@ember/application";
import { inject as service } from "@ember/service";
import { cloneJSON } from "discourse-common/lib/object"; import { cloneJSON } from "discourse-common/lib/object";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatThreadPreview from "discourse/plugins/chat/discourse/models/chat-thread-preview";
export function handleStagedMessage(channel, messagesManager, data) { export default class ChatChannelSubscriptionManager {
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
if (!stagedMessage) {
return;
}
stagedMessage.error = null;
stagedMessage.id = data.chat_message.id;
stagedMessage.staged = false;
stagedMessage.excerpt = data.chat_message.excerpt;
stagedMessage.channel = channel;
stagedMessage.createdAt = new Date(data.chat_message.created_at);
return stagedMessage;
}
/**
* Handles subscriptions for MessageBus messages sent from Chat::Publisher
* to the channel and thread panes. There are individual services for
* each (ChatChannelPaneSubscriptionsManager and ChatThreadPaneSubscriptionsManager)
* that implement their own logic where necessary. Functions which will
* always be different between the two raise a "not implemented" error in
* the base class, and the child class must define the associated function,
* even if it is a noop in that context.
*
* For example, in the thread context there is no need to handle the thread
* creation event, because the panel will not be open in that case.
*/
export default class ChatPaneBaseSubscriptionsManager extends Service {
@service chat;
@service currentUser; @service currentUser;
@service chatChannelNoticesManager;
@service messageBus;
messageBusChannel = null; @tracked channel;
messageBusLastId = null;
get messagesManager() { constructor(context, channel, { onNewMessage } = {}) {
return this.model.messagesManager; setOwner(this, getOwner(context));
}
beforeSubscribe() {} this.channel = channel;
afterMessage() {} this.onNewMessage = onNewMessage;
subscribe(model) {
this.unsubscribe();
this.beforeSubscribe(model);
this.model = model;
if (!this.messageBusChannel) {
return;
}
this.messageBus.subscribe( this.messageBus.subscribe(
this.messageBusChannel, this.messageBusChannel,
this.onMessage, this.onMessage,
this.messageBusLastId this.channel.channelMessageBusLastId
); );
} }
unsubscribe() { get messagesManager() {
if (!this.model) { return this.channel.messagesManager;
return;
}
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
this.model = null;
} }
handleStagedMessageInternal(channel, data) { get messageBusChannel() {
return handleStagedMessage(channel, this.messagesManager, data); return `/chat/${this.channel.id}`;
}
teardown() {
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
this.modelId = null;
} }
@bind @bind
onMessage(busData) { onMessage(busData, _, __, lastMessageBusId) {
switch (busData.type) { switch (busData.type) {
case "sent": case "sent":
this.handleSentMessage(busData); this.handleSentMessage(busData);
@ -119,11 +83,42 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
break; break;
} }
this.afterMessage(this.model, ...arguments); this.channel.channelMessageBusLastId = lastMessageBusId;
} }
handleSentMessage() { handleSentMessage(data) {
throw "not implemented"; if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
const stagedMessage = this.handleStagedMessage(
this.channel,
this.messagesManager,
data
);
if (stagedMessage) {
return;
}
}
const message = ChatMessage.create(this.channel, data.chat_message);
message.manager = this.channel.messagesManager;
this.onNewMessage?.(message);
this.channel.lastMessage = message;
}
handleStagedMessage(channel, messagesManager, data) {
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
if (!stagedMessage) {
return;
}
stagedMessage.error = null;
stagedMessage.id = data.chat_message.id;
stagedMessage.staged = false;
stagedMessage.excerpt = data.chat_message.excerpt;
stagedMessage.channel = channel;
stagedMessage.createdAt = new Date(data.chat_message.created_at);
return stagedMessage;
} }
handleProcessedMessage(data) { handleProcessedMessage(data) {
@ -182,7 +177,10 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
this.messagesManager.removeMessage(targetMsg); this.messagesManager.removeMessage(targetMsg);
} }
this._afterDeleteMessage(targetMsg, data); if (this.channel.currentUserMembership.lastReadMessageId === targetMsg.id) {
this.channel.currentUserMembership.lastReadMessageId =
data.latest_not_deleted_message_id;
}
} }
handleRestoreMessage(data) { handleRestoreMessage(data) {
@ -211,10 +209,10 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
} }
handleNewThreadCreated(data) { handleNewThreadCreated(data) {
this.model.threadsManager this.channel.threadsManager
.find(this.model.id, data.thread_id, { fetchIfNotFound: true }) .find(this.channel.id, data.thread_id, { fetchIfNotFound: true })
.then((thread) => { .then((thread) => {
const channelOriginalMessage = this.model.messagesManager.findMessage( const channelOriginalMessage = this.channel.messagesManager.findMessage(
thread.originalMessage.id thread.originalMessage.id
); );
@ -224,15 +222,14 @@ export default class ChatPaneBaseSubscriptionsManager extends Service {
}); });
} }
handleThreadOriginalMessageUpdate() { handleNotice(data) {
throw "not implemented"; this.chatChannelNoticesManager.handleNotice(data);
} }
handleNotice() { handleThreadOriginalMessageUpdate(data) {
throw "not implemented"; const message = this.messagesManager.findMessage(data.original_message_id);
} if (message?.thread) {
message.thread.preview = ChatThreadPreview.create(data.preview);
_afterDeleteMessage() { }
throw "not implemented";
} }
} }

View File

@ -0,0 +1,215 @@
import { tracked } from "@glimmer/tracking";
import { getOwner, setOwner } from "@ember/application";
import { inject as service } from "@ember/service";
import { cloneJSON } from "discourse-common/lib/object";
import { bind } from "discourse-common/utils/decorators";
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
export default class ChatChannelThreadSubscriptionManager {
@service currentUser;
@service messageBus;
@tracked channel;
constructor(context, thread, { onNewMessage } = {}) {
setOwner(this, getOwner(context));
this.thread = thread;
this.onNewMessage = onNewMessage;
this.messageBus.subscribe(
this.messageBusChannel,
this.onMessage,
this.thread.channelMessageBusLastId
);
}
get messagesManager() {
return this.thread.messagesManager;
}
get messageBusChannel() {
return `/chat/${this.thread.channel.id}/thread/${this.thread.id}`;
}
teardown() {
this.messageBus.unsubscribe(this.messageBusChannel, this.onMessage);
}
@bind
onMessage(busData, _, __, lastMessageBusId) {
switch (busData.type) {
case "sent":
this.handleSentMessage(busData);
break;
case "reaction":
this.handleReactionMessage(busData);
break;
case "processed":
this.handleProcessedMessage(busData);
break;
case "edit":
this.handleEditMessage(busData);
break;
case "refresh":
this.handleRefreshMessage(busData);
break;
case "delete":
this.handleDeleteMessage(busData);
break;
case "bulk_delete":
this.handleBulkDeleteMessage(busData);
break;
case "restore":
this.handleRestoreMessage(busData);
break;
case "self_flagged":
this.handleSelfFlaggedMessage(busData);
break;
case "flag":
this.handleFlaggedMessage(busData);
break;
case "thread_created":
this.handleNewThreadCreated(busData);
break;
}
this.thread.threadMessageBusLastId = lastMessageBusId;
}
handleSentMessage(data) {
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
const stagedMessage = this.handleStagedMessage(
this.thread.channel,
this.messagesManager,
data
);
if (stagedMessage) {
return;
}
}
const message = ChatMessage.create(this.thread.channel, data.chat_message);
message.thread = this.thread;
message.manager = this.messagesManager;
this.onNewMessage?.(message);
}
handleStagedMessage(channel, messagesManager, data) {
const stagedMessage = messagesManager.findStagedMessage(data.staged_id);
if (!stagedMessage) {
return;
}
stagedMessage.error = null;
stagedMessage.id = data.chat_message.id;
stagedMessage.staged = false;
stagedMessage.excerpt = data.chat_message.excerpt;
stagedMessage.channel = channel;
stagedMessage.createdAt = new Date(data.chat_message.created_at);
return stagedMessage;
}
handleProcessedMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.cooked = data.chat_message.cooked;
message.processed = true;
}
}
handleReactionMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) {
message.react(data.emoji, data.action, data.user, this.currentUser.id);
}
}
handleEditMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.excerpt = data.chat_message.excerpt;
message.uploads = cloneJSON(data.chat_message.uploads || []);
message.edited = data.chat_message.edited;
}
}
handleRefreshMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.incrementVersion();
}
}
handleBulkDeleteMessage(data) {
data.deleted_ids.forEach((deletedId) => {
this.handleDeleteMessage({
deleted_id: deletedId,
deleted_at: data.deleted_at,
});
});
}
handleDeleteMessage(data) {
const deletedId = data.deleted_id;
const targetMsg = this.messagesManager.findMessage(deletedId);
if (!targetMsg) {
return;
}
if (this.currentUser.staff || this.currentUser.id === targetMsg.user.id) {
targetMsg.deletedAt = data.deleted_at;
targetMsg.deletedById = data.deleted_by_id;
targetMsg.expanded = false;
} else {
this.messagesManager.removeMessage(targetMsg);
}
if (this.thread.currentUserMembership?.lastReadMessageId === targetMsg.id) {
this.thread.currentUserMembership.lastReadMessageId =
data.latest_not_deleted_message_id;
}
}
handleRestoreMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message.id);
if (message) {
message.deletedAt = null;
} else {
const newMessage = ChatMessage.create(this.model, data.chat_message);
newMessage.manager = this.messagesManager;
this.messagesManager.addMessages([newMessage]);
}
}
handleSelfFlaggedMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) {
message.userFlagStatus = data.user_flag_status;
}
}
handleFlaggedMessage(data) {
const message = this.messagesManager.findMessage(data.chat_message_id);
if (message) {
message.reviewableId = data.reviewable_id;
}
}
handleNewThreadCreated(data) {
this.thread.threadsManager
.find(this.thread.id, data.thread_id, { fetchIfNotFound: true })
.then((thread) => {
const channelOriginalMessage = this.thread.messagesManager.findMessage(
thread.originalMessage.id
);
if (channelOriginalMessage) {
channelOriginalMessage.thread = thread;
}
});
}
}

View File

@ -0,0 +1,16 @@
import { tracked } from "@glimmer/tracking";
import Service from "@ember/service";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import ChatNotice from "../models/chat-notice";
export default class ChatChannelNoticesManager extends Service {
@tracked notices = new TrackedArray();
handleNotice(data) {
this.notices.pushObject(ChatNotice.create(data));
}
clearNotice(notice) {
this.notices.removeObject(notice);
}
}

View File

@ -1,48 +0,0 @@
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
import { TrackedArray } from "@ember-compat/tracked-built-ins";
import ChatNotice from "../models/chat-notice";
import ChatThreadPreview from "../models/chat-thread-preview";
import ChatPaneBaseSubscriptionsManager from "./chat-pane-base-subscriptions-manager";
export default class ChatChannelPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager {
@service chat;
@service currentUser;
@tracked notices = new TrackedArray();
beforeSubscribe(model) {
this.messageBusChannel = `/chat/${model.id}`;
this.messageBusLastId = model.channelMessageBusLastId;
}
afterMessage(model, _, __, lastMessageBusId) {
model.channelMessageBusLastId = lastMessageBusId;
}
handleSentMessage() {
return;
}
handleNotice(data) {
this.notices.pushObject(ChatNotice.create(data));
}
clearNotice(notice) {
this.notices.removeObject(notice);
}
handleThreadOriginalMessageUpdate(data) {
const message = this.messagesManager.findMessage(data.original_message_id);
if (message?.thread) {
message.thread.preview = ChatThreadPreview.create(data.preview);
}
}
_afterDeleteMessage(targetMsg, data) {
if (this.model.currentUserMembership.lastReadMessageId === targetMsg.id) {
this.model.currentUserMembership.lastReadMessageId =
data.latest_not_deleted_message_id;
}
}
}

View File

@ -1,48 +0,0 @@
import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
import ChatPaneBaseSubscriptionsManager from "./chat-pane-base-subscriptions-manager";
export default class ChatThreadPaneSubscriptionsManager extends ChatPaneBaseSubscriptionsManager {
beforeSubscribe(model) {
this.messageBusChannel = `/chat/${model.channel.id}/thread/${model.id}`;
this.messageBusLastId = model.threadMessageBusLastId;
}
afterMessage(model, _, __, lastMessageBusId) {
model.threadMessageBusLastId = lastMessageBusId;
}
handleSentMessage(data) {
if (data.chat_message.user.id === this.currentUser.id && data.staged_id) {
const stagedMessage = this.handleStagedMessageInternal(
this.model.channel,
data
);
if (stagedMessage) {
return;
}
}
const message = ChatMessage.create(this.model.channel, data.chat_message);
message.thread = this.model;
message.manager = this.messagesManager;
this.messagesManager.addMessages([message]);
}
// NOTE: noop, there is nothing to do when a thread original message
// is updated inside the thread panel (for now).
handleThreadOriginalMessageUpdate() {
return;
}
// NOTE: We don't yet handle notices inside of threads so do nothing.
handleNotice() {
return;
}
_afterDeleteMessage(targetMsg, data) {
if (this.model.currentUserMembership?.lastReadMessageId === targetMsg.id) {
this.model.currentUserMembership.lastReadMessageId =
data.latest_not_deleted_message_id;
}
}
}

View File

@ -1,5 +1,7 @@
<ChatThread {{#each (array this.model) as |thread|}}
@thread={{this.model}} <ChatThread
@targetMessageId={{this.targetMessageId}} @thread={{thread}}
@includeHeader={{true}} @targetMessageId={{this.targetMessageId}}
/> @includeHeader={{true}}
/>
{{/each}}

View File

@ -12,9 +12,7 @@ module("Discourse Chat | Component | chat-notice", function (hooks) {
test("displays all notices for a channel", async function (assert) { test("displays all notices for a channel", async function (assert) {
this.channel = fabricators.channel(); this.channel = fabricators.channel();
this.manager = this.container.lookup( this.manager = this.container.lookup("service:chatChannelNoticesManager");
"service:chatChannelPaneSubscriptionsManager"
);
this.manager.handleNotice({ this.manager.handleNotice({
channel_id: this.channel.id, channel_id: this.channel.id,
text_content: "hello", text_content: "hello",
@ -40,9 +38,7 @@ module("Discourse Chat | Component | chat-notice", function (hooks) {
test("Notices can be cleared", async function (assert) { test("Notices can be cleared", async function (assert) {
this.channel = fabricators.channel(); this.channel = fabricators.channel();
this.manager = this.container.lookup( this.manager = this.container.lookup("service:chatChannelNoticesManager");
"service:chatChannelPaneSubscriptionsManager"
);
this.manager.handleNotice({ this.manager.handleNotice({
channel_id: this.channel.id, channel_id: this.channel.id,
text_content: "hello", text_content: "hello",
@ -66,9 +62,7 @@ module("Discourse Chat | Component | chat-notice", function (hooks) {
}); });
test("MentionWithoutMembership notice renders", async function (assert) { test("MentionWithoutMembership notice renders", async function (assert) {
this.channel = fabricators.channel(); this.channel = fabricators.channel();
this.manager = this.container.lookup( this.manager = this.container.lookup("service:chatChannelNoticesManager");
"service:chatChannelPaneSubscriptionsManager"
);
const text = "Joffrey can't chat, hermano"; const text = "Joffrey can't chat, hermano";
this.manager.handleNotice({ this.manager.handleNotice({
channel_id: this.channel.id, channel_id: this.channel.id,