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 in90efdd7f9d
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 />` since90efdd7f9d
to ensure atomicity has been applied to `<ChatThread />` too
This commit is contained in:
parent
1d68ff430b
commit
dcaa719363
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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}}
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue